#
tokens: 46525/50000 6/349 files (page 8/9)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 8 of 9. Use http://codebase.md/higress-group/himarket?lines=true&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/pages/McpDetail.tsx:
--------------------------------------------------------------------------------

```typescript
  1 | import { useEffect, useState, useCallback } from "react";
  2 | import { useParams } from "react-router-dom";
  3 | import api from "../lib/api";
  4 | import { Layout } from "../components/Layout";
  5 | import { ProductHeader } from "../components/ProductHeader";
  6 | import {
  7 |   Card,
  8 |   Alert,
  9 |   Button,
 10 |   message,
 11 |   Tabs,
 12 |   Row,
 13 |   Col,
 14 |   Collapse,
 15 | } from "antd";
 16 | import { CopyOutlined } from "@ant-design/icons";
 17 | import ReactMarkdown from "react-markdown";
 18 | import { ProductType } from "../types";
 19 | import type {
 20 |   Product,
 21 |   McpConfig,
 22 |   McpServerProduct,
 23 |   ApiResponse,
 24 | } from "../types";
 25 | import * as yaml from "js-yaml";
 26 | import remarkGfm from 'remark-gfm';
 27 | import 'react-markdown-editor-lite/lib/index.css'
 28 | 
 29 | function McpDetail() {
 30 |   const { mcpName } = useParams();
 31 |   const [loading, setLoading] = useState(true);
 32 |   const [error, setError] = useState("");
 33 |   const [data, setData] = useState<Product | null>(null);
 34 |   const [mcpConfig, setMcpConfig] = useState<McpConfig | null>(null);
 35 |   const [parsedTools, setParsedTools] = useState<
 36 |     Array<{
 37 |       name: string;
 38 |       description: string;
 39 |       args?: Array<{
 40 |         name: string;
 41 |         description: string;
 42 |         type: string;
 43 |         required: boolean;
 44 |         position: string;
 45 |         default?: string;
 46 |         enum?: string[];
 47 |       }>;
 48 |     }>
 49 |   >([]);
 50 |   const [httpJson, setHttpJson] = useState("");
 51 |   const [sseJson, setSseJson] = useState("");
 52 |   const [localJson, setLocalJson] = useState("");
 53 | 
 54 |   // 解析YAML配置的函数
 55 |   const parseYamlConfig = (
 56 |     yamlString: string
 57 |   ): {
 58 |     tools?: Array<{
 59 |       name: string;
 60 |       description: string;
 61 |       args?: Array<{
 62 |         name: string;
 63 |         description: string;
 64 |         type: string;
 65 |         required: boolean;
 66 |         position: string;
 67 |         default?: string;
 68 |         enum?: string[];
 69 |       }>;
 70 |     }>;
 71 |   } | null => {
 72 |     try {
 73 |       const parsed = yaml.load(yamlString) as {
 74 |         tools?: Array<{
 75 |           name: string;
 76 |           description: string;
 77 |           args?: Array<{
 78 |             name: string;
 79 |             description: string;
 80 |             type: string;
 81 |             required: boolean;
 82 |             position: string;
 83 |             default?: string;
 84 |             enum?: string[];
 85 |           }>;
 86 |         }>;
 87 |       };
 88 |       return parsed;
 89 |     } catch (error) {
 90 |       console.warn("解析YAML配置失败:", error);
 91 |       return null;
 92 |     }
 93 |   };
 94 | 
 95 |   // 生成连接配置的函数
 96 |   const generateConnectionConfig = useCallback((
 97 |     domains: Array<{ domain: string; protocol: string }> | null | undefined,
 98 |     path: string | null | undefined,
 99 |     serverName: string,
100 |     localConfig?: unknown,
101 |     protocolType?: string
102 |   ) => {
103 |     // 互斥:优先判断本地模式
104 |     if (localConfig) {
105 |       const localConfigJson = JSON.stringify(localConfig, null, 2);
106 |       setLocalJson(localConfigJson);
107 |       setHttpJson("");
108 |       setSseJson("");
109 |       return;
110 |     }
111 | 
112 |     // HTTP/SSE 模式
113 |     if (domains && domains.length > 0 && path) {
114 |       const domain = domains[0];
115 |       const baseUrl = `${domain.protocol}://${domain.domain}`;
116 |       let endpoint = `${baseUrl}${path}`;
117 | 
118 |       if (mcpConfig?.meta?.source === 'ADP_AI_GATEWAY') {
119 |         endpoint = `${baseUrl}/mcp-servers${path}`;
120 |       }
121 | 
122 |       if (protocolType === 'SSE') {
123 |         // 仅生成SSE配置,不追加/sse
124 |         const sseConfig = `{
125 |   "mcpServers": {
126 |     "${serverName}": {
127 |       "type": "sse",
128 |       "url": "${endpoint}"
129 |     }
130 |   }
131 | }`;
132 |         setSseJson(sseConfig);
133 |         setHttpJson("");
134 |         setLocalJson("");
135 |         return;
136 |       } else if (protocolType === 'StreamableHTTP') {
137 |         // 仅生成HTTP配置
138 |         const httpConfig = `{
139 |   "mcpServers": {
140 |     "${serverName}": {
141 |       "url": "${endpoint}"
142 |     }
143 |   }
144 | }`;
145 |         setHttpJson(httpConfig);
146 |         setSseJson("");
147 |         setLocalJson("");
148 |         return;
149 |       } else {
150 |         // protocol为null或其他值:生成两种配置
151 |         const httpConfig = `{
152 |   "mcpServers": {
153 |     "${serverName}": {
154 |       "url": "${endpoint}"
155 |     }
156 |   }
157 | }`;
158 | 
159 |         const sseConfig = `{
160 |   "mcpServers": {
161 |     "${serverName}": {
162 |       "type": "sse",
163 |       "url": "${endpoint}/sse"
164 |     }
165 |   }
166 | }`;
167 | 
168 |         setHttpJson(httpConfig);
169 |         setSseJson(sseConfig);
170 |         setLocalJson("");
171 |         return;
172 |       }
173 |     }
174 | 
175 |     // 无有效配置
176 |     setHttpJson("");
177 |     setSseJson("");
178 |     setLocalJson("");
179 |   }, [mcpConfig]);
180 | 
181 |   useEffect(() => {
182 |     const fetchDetail = async () => {
183 |       if (!mcpName) {
184 |         return;
185 |       }
186 |       setLoading(true);
187 |       setError("");
188 |       try {
189 |         const response: ApiResponse<Product> = await api.get(`/products/${mcpName}`);
190 |         if (response.code === "SUCCESS" && response.data) {
191 |           setData(response.data);
192 | 
193 |           // 处理MCP配置(统一使用新结构 mcpConfig)
194 |           if (response.data.type === ProductType.MCP_SERVER) {
195 |             const mcpProduct = response.data as McpServerProduct;
196 | 
197 |             if (mcpProduct.mcpConfig) {
198 |               setMcpConfig(mcpProduct.mcpConfig);
199 | 
200 |               // 解析tools配置
201 |               if (mcpProduct.mcpConfig.tools) {
202 |                 const parsedConfig = parseYamlConfig(
203 |                   mcpProduct.mcpConfig.tools
204 |                 );
205 |                 if (parsedConfig && parsedConfig.tools) {
206 |                   setParsedTools(parsedConfig.tools);
207 |                 }
208 |               }
209 |             }
210 |           }
211 |         } else {
212 |           setError(response.message || "数据加载失败");
213 |         }
214 |       } catch (error) {
215 |         console.error("API请求失败:", error);
216 |         setError("加载失败,请稍后重试");
217 |       } finally {
218 |         setLoading(false);
219 |       }
220 |     };
221 |     fetchDetail();
222 |   }, [mcpName]);
223 | 
224 |   // 监听 mcpConfig 变化,重新生成连接配置
225 |   useEffect(() => {
226 |     if (mcpConfig) {
227 |       generateConnectionConfig(
228 |         mcpConfig.mcpServerConfig.domains,
229 |         mcpConfig.mcpServerConfig.path,
230 |         mcpConfig.mcpServerName,
231 |         mcpConfig.mcpServerConfig.rawConfig,
232 | (mcpConfig.meta as any)?.protocol
233 |       );
234 |     }
235 |   }, [mcpConfig, generateConnectionConfig]);
236 | 
237 |   const handleCopy = async (text: string) => {
238 |     try {
239 |       if (navigator.clipboard && window.isSecureContext) {
240 |         await navigator.clipboard.writeText(text);
241 |       } else {
242 |         // 非安全上下文降级处理
243 |         const textarea = document.createElement("textarea");
244 |         textarea.value = text;
245 |         textarea.style.position = "fixed";
246 |         document.body.appendChild(textarea);
247 |         textarea.focus();
248 |         textarea.select();
249 |         document.execCommand("copy");
250 |         document.body.removeChild(textarea);
251 |       }
252 |       message.success("已复制到剪贴板", 1);
253 |     } catch {
254 |       message.error("复制失败,请手动复制");
255 |     }
256 |   };
257 | 
258 | 
259 |   if (error) {
260 |     return (
261 |       <Layout loading={loading}>
262 |         <Alert message={error} type="error" showIcon className="my-8" />
263 |       </Layout>
264 |     );
265 |   }
266 |   if (!data) {
267 |     return (
268 |       <Layout loading={loading}>
269 |         <Alert
270 |           message="未找到相关数据"
271 |           type="warning"
272 |           showIcon
273 |           className="my-8"
274 |         />
275 |       </Layout>
276 |     );
277 |   }
278 | 
279 |   const { name, description } = data;
280 |   const hasLocalConfig = Boolean(mcpConfig?.mcpServerConfig.rawConfig);
281 | 
282 | 
283 | 
284 |   return (
285 |     <Layout loading={loading}>
286 |       <div className="mb-6">
287 |         <ProductHeader
288 |           name={name}
289 |           description={description}
290 |           icon={data.icon}
291 |           defaultIcon="/MCP.svg"
292 |           mcpConfig={mcpConfig}
293 |           updatedAt={data.updatedAt}
294 |           productType="MCP_SERVER"
295 |         />
296 |         <hr className="border-gray-200 mt-4" />
297 |       </div>
298 | 
299 |       {/* 主要内容区域 - 左右布局 */}
300 |       <Row gutter={24}>
301 |         {/* 左侧内容 */}
302 |         <Col span={15}>
303 |           <Card className="mb-6 rounded-lg border-gray-200">
304 |             <Tabs
305 |               defaultActiveKey="overview"
306 |               items={[
307 |                 {
308 |                   key: "overview",
309 |                   label: "Overview",
310 |                   children: data.document ? (
311 |                     <div className="min-h-[400px]">
312 |                       <div 
313 |                         className="prose prose-lg max-w-none"
314 |                         style={{
315 |                           lineHeight: '1.7',
316 |                           color: '#374151',
317 |                           fontSize: '16px',
318 |                           fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif'
319 |                         }}
320 |                       >
321 |                         <style>{`
322 |                           .prose h1 {
323 |                             color: #111827;
324 |                             font-weight: 700;
325 |                             font-size: 2.25rem;
326 |                             line-height: 1.2;
327 |                             margin-top: 0;
328 |                             margin-bottom: 1.5rem;
329 |                             border-bottom: 2px solid #e5e7eb;
330 |                             padding-bottom: 0.5rem;
331 |                           }
332 |                           .prose h2 {
333 |                             color: #1f2937;
334 |                             font-weight: 600;
335 |                             font-size: 1.875rem;
336 |                             line-height: 1.3;
337 |                             margin-top: 2rem;
338 |                             margin-bottom: 1rem;
339 |                             border-bottom: 1px solid #e5e7eb;
340 |                             padding-bottom: 0.25rem;
341 |                           }
342 |                           .prose h3 {
343 |                             color: #374151;
344 |                             font-weight: 600;
345 |                             font-size: 1.5rem;
346 |                             margin-top: 1.5rem;
347 |                             margin-bottom: 0.75rem;
348 |                           }
349 |                           .prose p {
350 |                             margin-bottom: 1.25rem;
351 |                             color: #4b5563;
352 |                             line-height: 1.7;
353 |                             font-size: 16px;
354 |                           }
355 |                           .prose code {
356 |                             background-color: #f3f4f6;
357 |                             border: 1px solid #e5e7eb;
358 |                             border-radius: 0.375rem;
359 |                             padding: 0.125rem 0.375rem;
360 |                             font-size: 0.875rem;
361 |                             color: #374151;
362 |                             font-weight: 500;
363 |                           }
364 |                           .prose pre {
365 |                             background-color: #1f2937;
366 |                             border-radius: 0.5rem;
367 |                             padding: 1.25rem;
368 |                             overflow-x: auto;
369 |                             margin: 1.5rem 0;
370 |                             border: 1px solid #374151;
371 |                           }
372 |                           .prose pre code {
373 |                             background-color: transparent;
374 |                             border: none;
375 |                             color: #f9fafb;
376 |                             padding: 0;
377 |                             font-size: 0.875rem;
378 |                             font-weight: normal;
379 |                           }
380 |                           .prose blockquote {
381 |                             border-left: 4px solid #3b82f6;
382 |                             padding-left: 1rem;
383 |                             margin: 1.5rem 0;
384 |                             color: #6b7280;
385 |                             font-style: italic;
386 |                             background-color: #f8fafc;
387 |                             padding: 1rem;
388 |                             border-radius: 0.375rem;
389 |                             font-size: 16px;
390 |                           }
391 |                           .prose ul, .prose ol {
392 |                             margin: 1.25rem 0;
393 |                             padding-left: 1.5rem;
394 |                           }
395 |                           .prose ol {
396 |                             list-style-type: decimal;
397 |                             list-style-position: outside;
398 |                           }
399 |                           .prose ul {
400 |                             list-style-type: disc;
401 |                             list-style-position: outside;
402 |                           }
403 |                           .prose li {
404 |                             margin: 0.5rem 0;
405 |                             color: #4b5563;
406 |                             display: list-item;
407 |                             font-size: 16px;
408 |                           }
409 |                           .prose ol li {
410 |                             padding-left: 0.25rem;
411 |                           }
412 |                           .prose ul li {
413 |                             padding-left: 0.25rem;
414 |                           }
415 |                           .prose table {
416 |                             width: 100%;
417 |                             border-collapse: collapse;
418 |                             margin: 1.5rem 0;
419 |                             font-size: 16px;
420 |                           }
421 |                           .prose th, .prose td {
422 |                             border: 1px solid #d1d5db;
423 |                             padding: 0.75rem;
424 |                             text-align: left;
425 |                           }
426 |                           .prose th {
427 |                             background-color: #f9fafb;
428 |                             font-weight: 600;
429 |                             color: #374151;
430 |                             font-size: 16px;
431 |                           }
432 |                           .prose td {
433 |                             color: #4b5563;
434 |                             font-size: 16px;
435 |                           }
436 |                           .prose a {
437 |                             color: #3b82f6;
438 |                             text-decoration: underline;
439 |                             font-weight: 500;
440 |                             transition: color 0.2s;
441 |                             font-size: inherit;
442 |                           }
443 |                           .prose a:hover {
444 |                             color: #1d4ed8;
445 |                           }
446 |                           .prose strong {
447 |                             color: #111827;
448 |                             font-weight: 600;
449 |                             font-size: inherit;
450 |                           }
451 |                           .prose em {
452 |                             color: #6b7280;
453 |                             font-style: italic;
454 |                             font-size: inherit;
455 |                           }
456 |                           .prose hr {
457 |                             border: none;
458 |                             height: 1px;
459 |                             background-color: #e5e7eb;
460 |                             margin: 2rem 0;
461 |                           }
462 |                         `}</style>
463 |                         <ReactMarkdown remarkPlugins={[remarkGfm]}>{data.document}</ReactMarkdown>
464 |                       </div>
465 |                     </div>
466 |                   ) : (
467 |                     <div className="text-gray-500 text-center py-8">
468 |                       No overview available
469 |                     </div>
470 |                   ),
471 |                 },
472 |                 {
473 |                   key: "tools",
474 |                   label: `Tools (${parsedTools.length})`,
475 |                   children: parsedTools.length > 0 ? (
476 |                     <div className="border border-gray-200 rounded-lg bg-gray-50">
477 |                       {parsedTools.map((tool, idx) => (
478 |                         <div key={idx} className={idx < parsedTools.length - 1 ? "border-b border-gray-200" : ""}>
479 |                           <Collapse
480 |                             ghost
481 |                             expandIconPosition="end"
482 |                             items={[{
483 |                               key: idx.toString(),
484 |                               label: tool.name,
485 |                               children: (
486 |                                 <div className="px-4 pb-2">
487 |                                   <div className="text-gray-600 mb-4">{tool.description}</div>
488 |                                   
489 |                                   {tool.args && tool.args.length > 0 && (
490 |                                     <div>
491 |                                       <p className="font-medium text-gray-700 mb-3">输入参数:</p>
492 |                                       {tool.args.map((arg, argIdx) => (
493 |                                         <div key={argIdx} className="mb-3">
494 |                                           <div className="flex items-center mb-2">
495 |                                             <span className="font-medium text-gray-800 mr-2">{arg.name}</span>
496 |                                             <span className="text-xs bg-gray-200 text-gray-600 px-2 py-1 rounded mr-2">
497 |                                               {arg.type}
498 |                                             </span>
499 |                                             {arg.required && (
500 |                                               <span className="text-red-500 text-xs mr-2">*</span>
501 |                                             )}
502 |                                             {arg.description && (
503 |                                               <span className="text-xs text-gray-500">
504 |                                                 {arg.description}
505 |                                               </span>
506 |                                             )}
507 |                                           </div>
508 |                                           <input
509 |                                             type="text"
510 |                                             placeholder={arg.description || `请输入${arg.name}`}
511 |                                             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"
512 |                                           />
513 |                                         </div>
514 |                                       ))}
515 |                                     </div>
516 |                                   )}
517 |                                   
518 |                                   {(!tool.args || tool.args.length === 0) && (
519 |                                     <div className="text-gray-500 text-sm">No parameters required</div>
520 |                                   )}
521 |                                 </div>
522 |                               ),
523 |                             }]}
524 |                           />
525 |                         </div>
526 |                       ))}
527 |                     </div>
528 |                   ) : (
529 |                     <div className="text-gray-500 text-center py-8">
530 |                       No tools available
531 |                     </div>
532 |                   ),
533 |                 },
534 |               ]}
535 |             />
536 |           </Card>
537 |         </Col>
538 | 
539 |         {/* 右侧连接指导 */}
540 |         <Col span={9}>
541 |           {mcpConfig && (
542 |             <Card className="mb-6 rounded-lg border-gray-200">
543 |               <div className="mb-4">
544 |                 <h3 className="text-sm font-semibold mb-3">连接点配置</h3>
545 |                 <Tabs
546 |                   size="small" 
547 |                   defaultActiveKey={hasLocalConfig ? "local" : (sseJson ? "sse" : "http")}
548 |                   items={(() => {
549 |                     const tabs = [];
550 |                     
551 |                     if (hasLocalConfig) {
552 |                       tabs.push({
553 |                         key: "local",
554 |                         label: "Stdio",
555 |                         children: (
556 |                           <div className="relative bg-gray-50 border border-gray-200 rounded-md p-3">
557 |                             <Button
558 |                               type="text"
559 |                               size="small"
560 |                               icon={<CopyOutlined />}
561 |                               className="absolute top-2 right-2 z-10"
562 |                               onClick={() => handleCopy(localJson)}
563 |                             />
564 |                             <div className="text-gray-800 font-mono text-xs overflow-x-auto">
565 |                               <pre className="whitespace-pre-wrap">{localJson}</pre>
566 |                             </div>
567 |                           </div>
568 |                         ),
569 |                       });
570 |                     } else {
571 |                       if (sseJson) {
572 |                         tabs.push({
573 |                           key: "sse",
574 |                           label: "SSE",
575 |                           children: (
576 |                             <div className="relative bg-gray-50 border border-gray-200 rounded-md p-3">
577 |                               <Button
578 |                                 type="text"
579 |                                 size="small"
580 |                                 icon={<CopyOutlined />}
581 |                                 className="absolute top-2 right-2 z-10"
582 |                                 onClick={() => handleCopy(sseJson)}
583 |                               />
584 |                               <div className="text-gray-800 font-mono text-xs overflow-x-auto">
585 |                                 <pre className="whitespace-pre-wrap">{sseJson}</pre>
586 |                               </div>
587 |                             </div>
588 |                           ),
589 |                         });
590 |                       }
591 |                       
592 |                       if (httpJson) {
593 |                         tabs.push({
594 |                           key: "http",
595 |                           label: "Streaming HTTP",
596 |                           children: (
597 |                             <div className="relative bg-gray-50 border border-gray-200 rounded-md p-3">
598 |                               <Button
599 |                                 type="text"
600 |                                 size="small"
601 |                                 icon={<CopyOutlined />}
602 |                                 className="absolute top-2 right-2 z-10"
603 |                                 onClick={() => handleCopy(httpJson)}
604 |                               />
605 |                               <div className="text-gray-800 font-mono text-xs overflow-x-auto">
606 |                                 <pre className="whitespace-pre-wrap">{httpJson}</pre>
607 |                               </div>
608 |                             </div>
609 |                           ),
610 |                         });
611 |                       }
612 |                     }
613 |                     
614 |                     return tabs;
615 |                   })()}
616 |                 />
617 |               </div>
618 |             </Card>
619 |           )}
620 |         </Col>
621 |       </Row>
622 |     </Layout>
623 |   );
624 | }
625 | 
626 | export default McpDetail;
627 | 
```

--------------------------------------------------------------------------------
/portal-server/src/main/java/com/alibaba/apiopenplatform/service/impl/NacosServiceImpl.java:
--------------------------------------------------------------------------------

```java
  1 | /*
  2 |  * Licensed to the Apache Software Foundation (ASF) under one
  3 |  * or more contributor license agreements.  See the NOTICE file
  4 |  * distributed with this work for additional information
  5 |  * regarding copyright ownership.  The ASF licenses this file
  6 |  * to you under the Apache License, Version 2.0 (the
  7 |  * "License"); you may not use this file except in compliance
  8 |  * with the License.  You may obtain a copy of the License at
  9 |  *
 10 |  *   http://www.apache.org/licenses/LICENSE-2.0
 11 |  *
 12 |  * Unless required by applicable law or agreed to in writing,
 13 |  * software distributed under the License is distributed on an
 14 |  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 15 |  * KIND, either express or implied.  See the License for the
 16 |  * specific language governing permissions and limitations
 17 |  * under the License.
 18 |  */
 19 | 
 20 | package com.alibaba.apiopenplatform.service.impl;
 21 | 
 22 | import cn.hutool.core.util.StrUtil;
 23 | import com.alibaba.apiopenplatform.core.constant.Resources;
 24 | import com.alibaba.apiopenplatform.core.exception.BusinessException;
 25 | import com.alibaba.apiopenplatform.core.exception.ErrorCode;
 26 | import com.alibaba.apiopenplatform.core.security.ContextHolder;
 27 | import com.alibaba.apiopenplatform.core.utils.IdGenerator;
 28 | import com.alibaba.apiopenplatform.dto.params.nacos.CreateNacosParam;
 29 | import com.alibaba.apiopenplatform.dto.params.nacos.QueryNacosParam;
 30 | import com.alibaba.apiopenplatform.dto.params.nacos.UpdateNacosParam;
 31 | import com.alibaba.apiopenplatform.dto.result.NacosMCPServerResult;
 32 | import com.alibaba.apiopenplatform.dto.result.NacosNamespaceResult;
 33 | import com.alibaba.apiopenplatform.dto.result.NacosResult;
 34 | import com.alibaba.apiopenplatform.dto.result.PageResult;
 35 | import com.alibaba.apiopenplatform.dto.result.MCPConfigResult;
 36 | import com.alibaba.apiopenplatform.dto.result.MseNacosResult;
 37 | import com.alibaba.apiopenplatform.entity.NacosInstance;
 38 | import com.alibaba.apiopenplatform.repository.NacosInstanceRepository;
 39 | import com.alibaba.apiopenplatform.service.NacosService;
 40 | import com.alibaba.apiopenplatform.support.enums.SourceType;
 41 | import com.alibaba.apiopenplatform.support.product.NacosRefConfig;
 42 | import com.alibaba.apiopenplatform.dto.converter.NacosToGatewayToolsConverter;
 43 | import cn.hutool.json.JSONUtil;
 44 | import com.alibaba.nacos.api.PropertyKeyConst;
 45 | import com.alibaba.nacos.api.ai.model.mcp.McpServerBasicInfo;
 46 | import lombok.RequiredArgsConstructor;
 47 | import lombok.extern.slf4j.Slf4j;
 48 | import org.springframework.data.domain.Page;
 49 | import org.springframework.data.domain.Pageable;
 50 | import org.springframework.stereotype.Service;
 51 | import com.alibaba.nacos.maintainer.client.ai.AiMaintainerFactory;
 52 | import com.alibaba.nacos.maintainer.client.ai.McpMaintainerService;
 53 | import com.alibaba.nacos.maintainer.client.naming.NamingMaintainerFactory;
 54 | import com.alibaba.nacos.maintainer.client.naming.NamingMaintainerService;
 55 | import com.alibaba.nacos.api.exception.NacosException;
 56 | import com.aliyun.mse20190531.Client;
 57 | import com.aliyun.mse20190531.models.ListClustersRequest;
 58 | import com.aliyun.mse20190531.models.ListClustersResponse;
 59 | import com.aliyun.mse20190531.models.ListClustersResponseBody;
 60 | import com.aliyun.teautil.models.RuntimeOptions;
 61 | import com.alibaba.nacos.api.ai.model.mcp.McpServerDetailInfo;
 62 | 
 63 | import java.util.HashMap;
 64 | import java.util.List;
 65 | import java.util.Map;
 66 | import java.util.Objects;
 67 | import java.util.Optional;
 68 | import java.util.Properties;
 69 | import java.util.stream.Collectors;
 70 | 
 71 | @Service
 72 | @Slf4j
 73 | @RequiredArgsConstructor
 74 | public class NacosServiceImpl implements NacosService {
 75 | 
 76 |     private static final String DEFAULT_CONTEXT_PATH = "nacos";
 77 | 
 78 |     private final NacosInstanceRepository nacosInstanceRepository;
 79 | 
 80 |     private final ContextHolder contextHolder;
 81 | 
 82 |     @Override
 83 |     public PageResult<NacosResult> listNacosInstances(Pageable pageable) {
 84 |         Page<NacosInstance> nacosInstances = nacosInstanceRepository.findAll(pageable);
 85 |         return new PageResult<NacosResult>().convertFrom(nacosInstances, nacosInstance -> new NacosResult().convertFrom(nacosInstance));
 86 |     }
 87 | 
 88 |     @Override
 89 |     public NacosResult getNacosInstance(String nacosId) {
 90 |         NacosInstance nacosInstance = findNacosInstance(nacosId);
 91 |         return new NacosResult().convertFrom(nacosInstance);
 92 |     }
 93 | 
 94 |     @Override
 95 |     public void createNacosInstance(CreateNacosParam param) {
 96 |         nacosInstanceRepository.findByNacosName(param.getNacosName())
 97 |                 .ifPresent(nacos -> {
 98 |                     throw new BusinessException(ErrorCode.CONFLICT, StrUtil.format("{}:{}已存在", Resources.NACOS_INSTANCE, param.getNacosName()));
 99 |                 });
100 | 
101 |         NacosInstance nacosInstance = param.convertTo();
102 | 
103 |         // If client provided nacosId use it after checking uniqueness, otherwise generate one
104 |         String providedId = param.getNacosId();
105 |         if (providedId != null && !providedId.trim().isEmpty()) {
106 |             // ensure not already exist
107 |             boolean exists = nacosInstanceRepository.findByNacosId(providedId).isPresent();
108 |             if (exists) {
109 |                 throw new BusinessException(ErrorCode.CONFLICT, StrUtil.format("{}:{}已存在", Resources.NACOS_INSTANCE, providedId));
110 |             }
111 |             nacosInstance.setNacosId(providedId);
112 |         } else {
113 |             nacosInstance.setNacosId(IdGenerator.genNacosId());
114 |         }
115 | 
116 |         nacosInstance.setAdminId(contextHolder.getUser());
117 | 
118 |         nacosInstanceRepository.save(nacosInstance);
119 |     }
120 | 
121 |     @Override
122 |     public void updateNacosInstance(String nacosId, UpdateNacosParam param) {
123 |         NacosInstance instance = findNacosInstance(nacosId);
124 | 
125 |         Optional.ofNullable(param.getNacosName())
126 |                 .filter(name -> !name.equals(instance.getNacosName()))
127 |                 .flatMap(nacosInstanceRepository::findByNacosName)
128 |                 .ifPresent(nacos -> {
129 |                     throw new BusinessException(ErrorCode.CONFLICT, StrUtil.format("{}:{}已存在", Resources.NACOS_INSTANCE, param.getNacosName()));
130 |                 });
131 | 
132 |         param.update(instance);
133 |         nacosInstanceRepository.saveAndFlush(instance);
134 |     }
135 | 
136 |     @Override
137 |     public void deleteNacosInstance(String nacosId) {
138 |         NacosInstance nacosInstance = findNacosInstance(nacosId);
139 |         nacosInstanceRepository.delete(nacosInstance);
140 |     }
141 | 
142 |     @Override
143 |     public PageResult<MseNacosResult> fetchNacos(QueryNacosParam param, Pageable pageable) {
144 |         try {
145 |             // 创建MSE客户端
146 |             Client client = new Client(param.toClientConfig());
147 | 
148 |             // 构建请求
149 |             ListClustersRequest request = new ListClustersRequest()
150 |                     .setRegionId(param.getRegionId())
151 |                     .setPageNum(pageable.getPageNumber() + 1)
152 |                     .setPageSize(pageable.getPageSize());
153 | 
154 |             RuntimeOptions runtime = new RuntimeOptions();
155 | 
156 |             // 调用MSE API获取集群列表
157 |             ListClustersResponse response =
158 |                     client.listClustersWithOptions(request, runtime);
159 | 
160 |             // 转换响应结果,并过滤掉 clusterType 为 "Nacos-Ans" 的实例
161 |             Optional<List<MseNacosResult>> nacosResults = Optional.ofNullable(response.getBody())
162 |                     .map(ListClustersResponseBody::getData)
163 |                     .map(clusters -> clusters.stream()
164 |                             .filter(cluster -> {
165 |                                 String type = cluster.getClusterType();
166 |                                 return (type == null || "Nacos-Ans".equalsIgnoreCase(type))
167 |                                         && cluster.getVersionCode().startsWith("NACOS_3");
168 |                             })
169 |                             .map(MseNacosResult::fromListClustersResponseBodyData)
170 |                             .collect(Collectors.toList())
171 |                     );
172 | 
173 |             if (nacosResults.isPresent()) {
174 |                 // 返回分页结果
175 |                 int total = response.getBody() != null && response.getBody().getTotalCount() != null ?
176 |                         response.getBody().getTotalCount().intValue() : 0;
177 |                 return PageResult.of(nacosResults.get(), pageable.getPageNumber(), pageable.getPageSize(), total);
178 |             }
179 |             return PageResult.empty(pageable.getPageNumber(), pageable.getPageSize());
180 |         } catch (Exception e) {
181 |             log.error("Error fetching Nacos clusters from MSE", e);
182 |             throw new BusinessException(ErrorCode.INTERNAL_ERROR, "Failed to fetch Nacos clusters from MSE: " + e.getMessage());
183 |         }
184 |     }
185 | 
186 |     @Override
187 |     public PageResult<NacosMCPServerResult> fetchMcpServers(String nacosId, String namespaceId, Pageable pageable) throws Exception {
188 |         NacosInstance nacosInstance = findNacosInstance(nacosId);
189 |         McpMaintainerService service = buildDynamicMcpService(nacosInstance);
190 |         String ns = namespaceId == null ? "" : namespaceId;
191 |         com.alibaba.nacos.api.model.Page<McpServerBasicInfo> page = service.listMcpServer(ns, "", 1, Integer.MAX_VALUE);
192 |         if (page == null || page.getPageItems() == null) {
193 |             return PageResult.empty(pageable.getPageNumber(), pageable.getPageSize());
194 |         }
195 |         return page.getPageItems().stream()
196 |                 .map(basicInfo -> new NacosMCPServerResult().convertFrom(basicInfo))
197 |                 .skip(pageable.getOffset())
198 |                 .limit(pageable.getPageSize())
199 |                 .collect(Collectors.collectingAndThen(
200 |                         Collectors.toList(),
201 |                         list -> PageResult.of(list, pageable.getPageNumber(), pageable.getPageSize(), page.getPageItems().size())
202 |                 ));
203 |     }
204 | 
205 |     @Override
206 |     public PageResult<NacosNamespaceResult> fetchNamespaces(String nacosId, Pageable pageable) throws Exception {
207 |         NacosInstance nacosInstance = findNacosInstance(nacosId);
208 |         // 使用空 namespace 构建 (列出全部命名空间)
209 |         NamingMaintainerService namingService = buildDynamicNamingService(nacosInstance, "");
210 |         List<?> namespaces;
211 |         try {
212 |             namespaces = namingService.getNamespaceList();
213 |         } catch (NacosException e) {
214 |             log.error("Error fetching namespaces from Nacos by nacosId {}", nacosId, e);
215 |             throw new BusinessException(ErrorCode.INTERNAL_ERROR, "Failed to fetch namespaces: " + e.getErrMsg());
216 |         }
217 | 
218 |         if (namespaces == null || namespaces.isEmpty()) {
219 |             return PageResult.empty(pageable.getPageNumber(), pageable.getPageSize());
220 |         }
221 | 
222 |         List<NacosNamespaceResult> list = namespaces.stream()
223 |                 .map(o -> new NacosNamespaceResult().convertFrom(o))
224 |                 .skip(pageable.getOffset())
225 |                 .limit(pageable.getPageSize())
226 |                 .collect(Collectors.toList());
227 | 
228 |         return PageResult.of(list, pageable.getPageNumber(), pageable.getPageSize(), namespaces.size());
229 |     }
230 | 
231 |     @Override
232 |     public String fetchMcpConfig(String nacosId, NacosRefConfig nacosRefConfig) {
233 |         NacosInstance nacosInstance = findNacosInstance(nacosId);
234 | 
235 |         McpMaintainerService service = buildDynamicMcpService(nacosInstance);
236 |         try {
237 |             McpServerDetailInfo detail = service.getMcpServerDetail(nacosRefConfig.getNamespaceId(),
238 |                     nacosRefConfig.getMcpServerName(), null);
239 |             if (detail == null) {
240 |                 return null;
241 |             }
242 | 
243 |             MCPConfigResult mcpConfig = buildMCPConfigResult(detail);
244 |             return JSONUtil.toJsonStr(mcpConfig);
245 |         } catch (Exception e) {
246 |             log.error("Error fetching Nacos MCP servers", e);
247 |             throw new BusinessException(ErrorCode.INTERNAL_ERROR, "Failed to fetch Nacos MCP config");
248 |         }
249 |     }
250 | 
251 |     private MCPConfigResult buildMCPConfigResult(McpServerDetailInfo detail) {
252 |         MCPConfigResult mcpConfig = new MCPConfigResult();
253 |         mcpConfig.setMcpServerName(detail.getName());
254 | 
255 |         MCPConfigResult.MCPServerConfig serverConfig = new MCPConfigResult.MCPServerConfig();
256 | 
257 |         if (detail.getLocalServerConfig() != null) {
258 |             serverConfig.setRawConfig(detail.getLocalServerConfig());
259 |             serverConfig.setTransportMode(MCPConfigResult.MCPTransportMode.LOCAL.getMode());
260 |         } else if (detail.getRemoteServerConfig() != null || (detail.getBackendEndpoints() != null && !detail.getBackendEndpoints().isEmpty())) {
261 |             Object remoteConfig = buildRemoteConnectionConfig(detail);
262 |             serverConfig.setRawConfig(remoteConfig);
263 |         } else {
264 |             Map<String, Object> defaultConfig = new HashMap<>();
265 |             defaultConfig.put("type", "unknown");
266 |             defaultConfig.put("name", detail.getName());
267 |             serverConfig.setRawConfig(defaultConfig);
268 |         }
269 | 
270 |         mcpConfig.setMcpServerConfig(serverConfig);
271 | 
272 |         if (detail.getToolSpec() != null) {
273 |             try {
274 |                 NacosToGatewayToolsConverter converter = new NacosToGatewayToolsConverter();
275 |                 converter.convertFromNacos(detail);
276 |                 String gatewayFormatYaml = converter.toYaml();
277 |                 mcpConfig.setTools(gatewayFormatYaml);
278 |             } catch (Exception e) {
279 |                 log.error("Error converting tools to gateway format", e);
280 |                 mcpConfig.setTools(null);
281 |             }
282 |         } else {
283 |             mcpConfig.setTools(null);
284 |         }
285 | 
286 |         MCPConfigResult.McpMetadata meta = new MCPConfigResult.McpMetadata();
287 |         meta.setSource(SourceType.NACOS.name());
288 |         mcpConfig.setMeta(meta);
289 | 
290 |         return mcpConfig;
291 |     }
292 | 
293 |     private Object buildRemoteConnectionConfig(McpServerDetailInfo detail) {
294 |         List<?> backendEndpoints = detail.getBackendEndpoints();
295 | 
296 |         if (backendEndpoints != null && !backendEndpoints.isEmpty()) {
297 |             Object firstEndpoint = backendEndpoints.get(0);
298 | 
299 |             Map<String, Object> connectionConfig = new HashMap<>();
300 |             Map<String, Object> mcpServers = new HashMap<>();
301 |             Map<String, Object> serverConfig = new HashMap<>();
302 | 
303 |             String endpointUrl = extractEndpointUrl(firstEndpoint);
304 |             if (endpointUrl != null) {
305 |                 serverConfig.put("url", endpointUrl);
306 |             }
307 | 
308 |             mcpServers.put(detail.getName(), serverConfig);
309 |             connectionConfig.put("mcpServers", mcpServers);
310 | 
311 |             return connectionConfig;
312 |         }
313 | 
314 |         Map<String, Object> basicConfig = new HashMap<>();
315 |         basicConfig.put("type", "remote");
316 |         basicConfig.put("name", detail.getName());
317 |         basicConfig.put("protocol", "http");
318 |         return basicConfig;
319 |     }
320 | 
321 |     private String extractEndpointUrl(Object endpoint) {
322 |         if (endpoint == null) {
323 |             return null;
324 |         }
325 | 
326 |         if (endpoint instanceof String) {
327 |             return (String) endpoint;
328 |         }
329 | 
330 |         if (endpoint instanceof Map) {
331 |             Map<?, ?> endpointMap = (Map<?, ?>) endpoint;
332 | 
333 |             String url = getStringValue(endpointMap, "url");
334 |             if (url != null) return url;
335 | 
336 |             String endpointUrl = getStringValue(endpointMap, "endpointUrl");
337 |             if (endpointUrl != null) return endpointUrl;
338 | 
339 |             String host = getStringValue(endpointMap, "host");
340 |             String port = getStringValue(endpointMap, "port");
341 |             String path = getStringValue(endpointMap, "path");
342 | 
343 |             if (host != null) {
344 |                 StringBuilder urlBuilder = new StringBuilder();
345 |                 String protocol = getStringValue(endpointMap, "protocol");
346 |                 urlBuilder.append(protocol != null ? protocol : "http").append("://");
347 |                 urlBuilder.append(host);
348 | 
349 |                 if (port != null && !port.isEmpty()) {
350 |                     urlBuilder.append(":").append(port);
351 |                 }
352 | 
353 |                 if (path != null && !path.isEmpty()) {
354 |                     if (!path.startsWith("/")) {
355 |                         urlBuilder.append("/");
356 |                     }
357 |                     urlBuilder.append(path);
358 |                 }
359 | 
360 |                 return urlBuilder.toString();
361 |             }
362 |         }
363 | 
364 |         if (endpoint.getClass().getName().contains("McpEndpointInfo")) {
365 |             return extractUrlFromMcpEndpointInfo(endpoint);
366 |         }
367 | 
368 |         return endpoint.toString();
369 |     }
370 | 
371 |     private String getStringValue(Map<?, ?> map, String key) {
372 |         Object value = map.get(key);
373 |         return value != null ? value.toString() : null;
374 |     }
375 | 
376 |     private String extractUrlFromMcpEndpointInfo(Object endpoint) {
377 |         String[] possibleFieldNames = {"url", "endpointUrl", "address", "host", "endpoint"};
378 | 
379 |         for (String fieldName : possibleFieldNames) {
380 |             try {
381 |                 java.lang.reflect.Field field = endpoint.getClass().getDeclaredField(fieldName);
382 |                 field.setAccessible(true);
383 |                 Object value = field.get(endpoint);
384 |                 if (value != null && !value.toString().trim().isEmpty()) {
385 |                     if (value.toString().contains("://") || value.toString().contains(":")) {
386 |                         return value.toString();
387 |                     }
388 |                 }
389 |             } catch (Exception e) {
390 |                 continue;
391 |             }
392 |         }
393 | 
394 |         java.lang.reflect.Field[] fields = endpoint.getClass().getDeclaredFields();
395 | 
396 |         String host = null;
397 |         String port = null;
398 |         String path = null;
399 |         String protocol = null;
400 | 
401 |         for (java.lang.reflect.Field field : fields) {
402 |             try {
403 |                 field.setAccessible(true);
404 |                 Object value = field.get(endpoint);
405 |                 if (value != null && !value.toString().trim().isEmpty()) {
406 |                     String fieldName = field.getName().toLowerCase();
407 | 
408 |                     if (fieldName.contains("host") || fieldName.contains("ip") || fieldName.contains("address")) {
409 |                         host = value.toString();
410 |                     } else if (fieldName.contains("port")) {
411 |                         port = value.toString();
412 |                     } else if (fieldName.contains("path") || fieldName.contains("endpoint") || fieldName.contains("uri")) {
413 |                         path = value.toString();
414 |                     } else if (fieldName.contains("protocol") || fieldName.contains("scheme")) {
415 |                         protocol = value.toString();
416 |                     }
417 |                 }
418 |             } catch (Exception e) {
419 |                 continue;
420 |             }
421 |         }
422 | 
423 |         if (host != null) {
424 |             StringBuilder urlBuilder = new StringBuilder();
425 |             urlBuilder.append(protocol != null ? protocol : "http").append("://");
426 |             urlBuilder.append(host);
427 | 
428 |             if (port != null && !port.isEmpty()) {
429 |                 urlBuilder.append(":").append(port);
430 |             }
431 | 
432 |             if (path != null && !path.isEmpty()) {
433 |                 if (!path.startsWith("/")) {
434 |                     urlBuilder.append("/");
435 |                 }
436 |                 urlBuilder.append(path);
437 |             }
438 | 
439 |             return urlBuilder.toString();
440 |         }
441 | 
442 |         return endpoint.toString();
443 |     }
444 | 
445 |     private NacosInstance findNacosInstance(String nacosId) {
446 |         return nacosInstanceRepository.findByNacosId(nacosId)
447 |                 .orElseThrow(() -> new BusinessException(ErrorCode.NOT_FOUND, Resources.NACOS_INSTANCE, nacosId));
448 |     }
449 | 
450 |     private McpMaintainerService buildDynamicMcpService(NacosInstance nacosInstance) {
451 |         Properties properties = new Properties();
452 |         properties.setProperty(PropertyKeyConst.SERVER_ADDR, nacosInstance.getServerUrl());
453 |         if (Objects.nonNull(nacosInstance.getUsername())) {
454 |             properties.setProperty(PropertyKeyConst.USERNAME, nacosInstance.getUsername());
455 |         }
456 | 
457 |         if (Objects.nonNull(nacosInstance.getPassword())) {
458 |             properties.setProperty(PropertyKeyConst.PASSWORD, nacosInstance.getPassword());
459 |         }
460 |         properties.setProperty(PropertyKeyConst.CONTEXT_PATH, DEFAULT_CONTEXT_PATH);
461 |         // instance no longer stores namespace; leave namespace empty to let requests use default/public
462 |         // if consumers need a specific namespace, they should call an overload that accepts it
463 |         if (Objects.nonNull(nacosInstance.getAccessKey())) {
464 |             properties.setProperty(PropertyKeyConst.ACCESS_KEY, nacosInstance.getAccessKey());
465 |         }
466 | 
467 |         if (Objects.nonNull(nacosInstance.getSecretKey())) {
468 |             properties.setProperty(PropertyKeyConst.SECRET_KEY, nacosInstance.getSecretKey());
469 |         }
470 | 
471 |         try {
472 |             return AiMaintainerFactory.createAiMaintainerService(properties);
473 |         } catch (Exception e) {
474 |             log.error("Error init Nacos AiMaintainerService", e);
475 |             throw new BusinessException(ErrorCode.INTERNAL_ERROR, "Error init Nacos AiMaintainerService");
476 |         }
477 |     }
478 | 
479 |     // removed unused no-namespace overload; use the runtime-namespace overload instead
480 | 
481 |     // overload to build NamingMaintainerService with a runtime namespace value
482 |     private NamingMaintainerService buildDynamicNamingService(NacosInstance nacosInstance, String runtimeNamespace) {
483 |         Properties properties = new Properties();
484 |         properties.setProperty(PropertyKeyConst.SERVER_ADDR, nacosInstance.getServerUrl());
485 |         if (Objects.nonNull(nacosInstance.getUsername())) {
486 |             properties.setProperty(PropertyKeyConst.USERNAME, nacosInstance.getUsername());
487 |         }
488 | 
489 |         if (Objects.nonNull(nacosInstance.getPassword())) {
490 |             properties.setProperty(PropertyKeyConst.PASSWORD, nacosInstance.getPassword());
491 |         }
492 |         properties.setProperty(PropertyKeyConst.CONTEXT_PATH, DEFAULT_CONTEXT_PATH);
493 |         properties.setProperty(PropertyKeyConst.NAMESPACE, runtimeNamespace == null ? "" : runtimeNamespace);
494 | 
495 |         if (Objects.nonNull(nacosInstance.getAccessKey())) {
496 |             properties.setProperty(PropertyKeyConst.ACCESS_KEY, nacosInstance.getAccessKey());
497 |         }
498 | 
499 |         if (Objects.nonNull(nacosInstance.getSecretKey())) {
500 |             properties.setProperty(PropertyKeyConst.SECRET_KEY, nacosInstance.getSecretKey());
501 |         }
502 | 
503 |         try {
504 |             return NamingMaintainerFactory.createNamingMaintainerService(properties);
505 |         } catch (Exception e) {
506 |             log.error("Error init Nacos NamingMaintainerService", e);
507 |             throw new BusinessException(ErrorCode.INTERNAL_ERROR, "Error init Nacos NamingMaintainerService");
508 |         }
509 |     }
510 | }
```

--------------------------------------------------------------------------------
/portal-server/src/main/java/com/alibaba/apiopenplatform/service/impl/ConsumerServiceImpl.java:
--------------------------------------------------------------------------------

```java
  1 | /*
  2 |  * Licensed to the Apache Software Foundation (ASF) under one
  3 |  * or more contributor license agreements.  See the NOTICE file
  4 |  * distributed with this work for additional information
  5 |  * regarding copyright ownership.  The ASF licenses this file
  6 |  * to you under the Apache License, Version 2.0 (the
  7 |  * "License"); you may not use this file except in compliance
  8 |  * with the License.  You may obtain a copy of the License at
  9 |  *
 10 |  *   http://www.apache.org/licenses/LICENSE-2.0
 11 |  *
 12 |  * Unless required by applicable law or agreed to in writing,
 13 |  * software distributed under the License is distributed on an
 14 |  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 15 |  * KIND, either express or implied.  See the License for the
 16 |  * specific language governing permissions and limitations
 17 |  * under the License.
 18 |  */
 19 | 
 20 | package com.alibaba.apiopenplatform.service.impl;
 21 | 
 22 | import cn.hutool.core.util.StrUtil;
 23 | import cn.hutool.json.JSONUtil;
 24 | 
 25 | import com.alibaba.apiopenplatform.core.constant.Resources;
 26 | import com.alibaba.apiopenplatform.core.event.DeveloperDeletingEvent;
 27 | import com.alibaba.apiopenplatform.core.event.ProductDeletingEvent;
 28 | import com.alibaba.apiopenplatform.core.exception.BusinessException;
 29 | import com.alibaba.apiopenplatform.core.exception.ErrorCode;
 30 | import com.alibaba.apiopenplatform.core.security.ContextHolder;
 31 | import com.alibaba.apiopenplatform.core.utils.IdGenerator;
 32 | import com.alibaba.apiopenplatform.dto.params.consumer.QueryConsumerParam;
 33 | import com.alibaba.apiopenplatform.dto.params.consumer.CreateConsumerParam;
 34 | import com.alibaba.apiopenplatform.dto.params.consumer.CreateCredentialParam;
 35 | import com.alibaba.apiopenplatform.dto.params.consumer.UpdateCredentialParam;
 36 | import com.alibaba.apiopenplatform.dto.result.*;
 37 | import com.alibaba.apiopenplatform.dto.params.consumer.CreateSubscriptionParam;
 38 | import com.alibaba.apiopenplatform.dto.params.consumer.QuerySubscriptionParam;
 39 | import com.alibaba.apiopenplatform.entity.*;
 40 | import com.alibaba.apiopenplatform.repository.ConsumerRepository;
 41 | import com.alibaba.apiopenplatform.repository.ConsumerCredentialRepository;
 42 | import com.alibaba.apiopenplatform.repository.SubscriptionRepository;
 43 | import com.alibaba.apiopenplatform.service.ConsumerService;
 44 | import com.alibaba.apiopenplatform.service.GatewayService;
 45 | import com.alibaba.apiopenplatform.service.PortalService;
 46 | import com.alibaba.apiopenplatform.service.ProductService;
 47 | import com.alibaba.apiopenplatform.support.consumer.ApiKeyConfig;
 48 | import com.alibaba.apiopenplatform.support.consumer.ConsumerAuthConfig;
 49 | import com.alibaba.apiopenplatform.support.consumer.HmacConfig;
 50 | import com.alibaba.apiopenplatform.support.enums.CredentialMode;
 51 | import com.alibaba.apiopenplatform.support.enums.SourceType;
 52 | import com.alibaba.apiopenplatform.support.gateway.GatewayConfig;
 53 | import cn.hutool.core.util.BooleanUtil;
 54 | import lombok.RequiredArgsConstructor;
 55 | import lombok.extern.slf4j.Slf4j;
 56 | import org.springframework.context.event.EventListener;
 57 | import org.springframework.data.domain.Page;
 58 | import org.springframework.data.domain.Pageable;
 59 | import org.springframework.data.jpa.domain.Specification;
 60 | 
 61 | import javax.persistence.criteria.Predicate;
 62 | 
 63 | import org.springframework.scheduling.annotation.Async;
 64 | import org.springframework.stereotype.Service;
 65 | 
 66 | import javax.persistence.criteria.Root;
 67 | import javax.persistence.criteria.Subquery;
 68 | import javax.transaction.Transactional;
 69 | import java.util.*;
 70 | import java.util.stream.Collectors;
 71 | 
 72 | import com.alibaba.apiopenplatform.support.enums.SubscriptionStatus;
 73 | import com.alibaba.apiopenplatform.repository.ConsumerRefRepository;
 74 | 
 75 | @Service
 76 | @RequiredArgsConstructor
 77 | @Transactional
 78 | @Slf4j
 79 | public class ConsumerServiceImpl implements ConsumerService {
 80 | 
 81 |     private final PortalService portalService;
 82 | 
 83 |     private final ConsumerRepository consumerRepository;
 84 | 
 85 |     private final GatewayService gatewayService;
 86 | 
 87 |     private final ContextHolder contextHolder;
 88 | 
 89 |     private final ConsumerCredentialRepository credentialRepository;
 90 | 
 91 |     private final SubscriptionRepository subscriptionRepository;
 92 | 
 93 |     private final ProductService productService;
 94 | 
 95 |     private final ConsumerRefRepository consumerRefRepository;
 96 | 
 97 |     @Override
 98 |     public ConsumerResult createConsumer(CreateConsumerParam param) {
 99 |         PortalResult portal = portalService.getPortal(contextHolder.getPortal());
100 | 
101 |         String consumerId = IdGenerator.genConsumerId();
102 |         Consumer consumer = param.convertTo();
103 |         consumer.setConsumerId(consumerId);
104 |         consumer.setDeveloperId(contextHolder.getUser());
105 |         consumer.setPortalId(portal.getPortalId());
106 | 
107 |         consumerRepository.save(consumer);
108 | 
109 |         // 初始化Credential
110 |         ConsumerCredential credential = initCredential(consumerId);
111 |         credentialRepository.save(credential);
112 | 
113 |         return getConsumer(consumerId);
114 |     }
115 | 
116 |     @Override
117 |     public PageResult<ConsumerResult> listConsumers(QueryConsumerParam param, Pageable pageable) {
118 |         Page<Consumer> consumers = consumerRepository.findAll(buildConsumerSpec(param), pageable);
119 | 
120 |         return new PageResult<ConsumerResult>().convertFrom(consumers, consumer -> new ConsumerResult().convertFrom(consumer));
121 |     }
122 | 
123 |     @Override
124 |     public ConsumerResult getConsumer(String consumerId) {
125 |         Consumer consumer = contextHolder.isDeveloper() ? findDevConsumer(consumerId) : findConsumer(consumerId);
126 | 
127 |         return new ConsumerResult().convertFrom(consumer);
128 |     }
129 | 
130 |     @Override
131 |     public void deleteConsumer(String consumerId) {
132 |         Consumer consumer = contextHolder.isDeveloper() ? findDevConsumer(consumerId) : findConsumer(consumerId);
133 |         // 订阅
134 |         subscriptionRepository.deleteAllByConsumerId(consumerId);
135 | 
136 |         // 凭证
137 |         credentialRepository.deleteAllByConsumerId(consumerId);
138 | 
139 |         // 删除网关上的Consumer
140 |         List<ConsumerRef> consumerRefs = consumerRefRepository.findAllByConsumerId(consumerId);
141 |         for (ConsumerRef consumerRef : consumerRefs) {
142 |             try {
143 |                 gatewayService.deleteConsumer(consumerRef.getGwConsumerId(), consumerRef.getGatewayConfig());
144 |             } catch (Exception e) {
145 |                 log.error("deleteConsumer gatewayConsumer error, gwConsumerId: {}", consumerRef.getGwConsumerId(), e);
146 |             }
147 |         }
148 | 
149 |         consumerRepository.delete(consumer);
150 |     }
151 | 
152 |     @Override
153 |     public void createCredential(String consumerId, CreateCredentialParam param) {
154 |         existsConsumer(consumerId);
155 |         // Consumer仅一份Credential
156 |         credentialRepository.findByConsumerId(consumerId)
157 |                 .ifPresent(c -> {
158 |                     throw new BusinessException(ErrorCode.CONFLICT, StrUtil.format("{}:{}已存在凭证", Resources.CONSUMER, consumerId));
159 |                 });
160 |         ConsumerCredential credential = param.convertTo();
161 |         credential.setConsumerId(consumerId);
162 |         complementCredentials(credential);
163 |         credentialRepository.save(credential);
164 |     }
165 | 
166 |     private ConsumerCredential initCredential(String consumerId) {
167 |         ConsumerCredential credential = new ConsumerCredential();
168 |         credential.setConsumerId(consumerId);
169 | 
170 |         ApiKeyConfig.ApiKeyCredential apiKeyCredential = new ApiKeyConfig.ApiKeyCredential();
171 |         ApiKeyConfig apiKeyConfig = new ApiKeyConfig();
172 |         apiKeyConfig.setCredentials(Collections.singletonList(apiKeyCredential));
173 | 
174 |         credential.setApiKeyConfig(apiKeyConfig);
175 |         complementCredentials(credential);
176 | 
177 |         return credential;
178 |     }
179 | 
180 |     @Override
181 |     public ConsumerCredentialResult getCredential(String consumerId) {
182 |         existsConsumer(consumerId);
183 | 
184 |         return credentialRepository.findByConsumerId(consumerId)
185 |                 .map(credential -> new ConsumerCredentialResult().convertFrom(credential))
186 |                 .orElse(new ConsumerCredentialResult());
187 |     }
188 | 
189 |     @Override
190 |     public void updateCredential(String consumerId, UpdateCredentialParam param) {
191 |         ConsumerCredential credential = credentialRepository.findByConsumerId(consumerId)
192 |                 .orElseThrow(() -> new BusinessException(ErrorCode.NOT_FOUND, Resources.CONSUMER_CREDENTIAL, consumerId));
193 | 
194 |         param.update(credential);
195 | 
196 |         List<ConsumerRef> consumerRefs = consumerRefRepository.findAllByConsumerId(consumerId);
197 |         for (ConsumerRef consumerRef : consumerRefs) {
198 |             try {
199 |                 gatewayService.updateConsumer(consumerRef.getGwConsumerId(), credential, consumerRef.getGatewayConfig());
200 |             } catch (Exception e) {
201 |                 log.error("update gatewayConsumer error, gwConsumerId: {}", consumerRef.getGwConsumerId(), e);
202 |             }
203 |         }
204 | 
205 |         credentialRepository.saveAndFlush(credential);
206 |     }
207 | 
208 |     @Override
209 |     public void deleteCredential(String consumerId) {
210 |         existsConsumer(consumerId);
211 |         credentialRepository.deleteAllByConsumerId(consumerId);
212 |     }
213 | 
214 |     @Override
215 |     public SubscriptionResult subscribeProduct(String consumerId, CreateSubscriptionParam param) {
216 | 
217 |         Consumer consumer = contextHolder.isDeveloper() ?
218 |                 findDevConsumer(consumerId) : findConsumer(consumerId);
219 |         // 勿重复订阅
220 |         if (subscriptionRepository.findByConsumerIdAndProductId(consumerId, param.getProductId()).isPresent()) {
221 |             throw new BusinessException(ErrorCode.INVALID_REQUEST, "重复订阅");
222 |         }
223 | 
224 |         ProductResult product = productService.getProduct(param.getProductId());
225 |         ProductRefResult productRef = productService.getProductRef(param.getProductId());
226 |         if (productRef == null) {
227 |             throw new BusinessException(ErrorCode.INTERNAL_ERROR, "API产品未关联API");
228 |         }
229 | 
230 |         // 非网关型不支持订阅
231 |         if (productRef.getSourceType() != SourceType.GATEWAY) {
232 |             throw new BusinessException(ErrorCode.INVALID_REQUEST, "API产品不支持订阅");
233 |         }
234 | 
235 |         ConsumerCredential credential = credentialRepository.findByConsumerId(consumerId)
236 |                 .orElseThrow(() -> new BusinessException(ErrorCode.NOT_FOUND, Resources.CONSUMER_CREDENTIAL, consumerId));
237 | 
238 |         ProductSubscription subscription = param.convertTo();
239 |         subscription.setConsumerId(consumerId);
240 | 
241 |         // 检查产品级别的自动审批设置
242 |         boolean autoApprove = false;
243 | 
244 |         // 优先检查产品级别的autoApprove配置
245 |         if (product.getAutoApprove() != null) {
246 |             // 如果产品配置了autoApprove,直接使用产品级别的配置
247 |             autoApprove = product.getAutoApprove();
248 |             log.info("使用产品级别自动审批配置: productId={}, autoApprove={}", param.getProductId(), autoApprove);
249 |         } else {
250 |             // 如果产品未配置autoApprove,则使用平台级别的配置
251 |             PortalResult portal = portalService.getPortal(consumer.getPortalId());
252 |             log.info("portal: {}", JSONUtil.toJsonStr(portal));
253 |             autoApprove = portal.getPortalSettingConfig() != null
254 |                     && BooleanUtil.isTrue(portal.getPortalSettingConfig().getAutoApproveSubscriptions());
255 |             log.info("使用平台级别自动审批配置: portalId={}, autoApprove={}", consumer.getPortalId(), autoApprove);
256 |         }
257 | 
258 |         if (autoApprove) {
259 |             // 如果autoApprove为true,立即授权并设置为APPROVED状态
260 |             ConsumerAuthConfig consumerAuthConfig = authorizeConsumer(consumer, credential, productRef);
261 |             subscription.setConsumerAuthConfig(consumerAuthConfig);
262 |             subscription.setStatus(SubscriptionStatus.APPROVED);
263 |         } else {
264 |             // 如果autoApprove为false,暂时不授权,设置为PENDING状态
265 |             subscription.setStatus(SubscriptionStatus.PENDING);
266 |         }
267 | 
268 |         subscriptionRepository.save(subscription);
269 | 
270 |         SubscriptionResult r = new SubscriptionResult().convertFrom(subscription);
271 |         r.setProductName(product.getName());
272 |         r.setProductType(product.getType());
273 | 
274 |         return r;
275 |     }
276 | 
277 |     @Override
278 |     public void unsubscribeProduct(String consumerId, String productId) {
279 |         existsConsumer(consumerId);
280 | 
281 |         ProductSubscription subscription = subscriptionRepository
282 |                 .findByConsumerIdAndProductId(consumerId, productId)
283 |                 .orElse(null);
284 |         if (subscription == null) {
285 |             return;
286 |         }
287 | 
288 |         if (subscription.getConsumerAuthConfig() != null) {
289 |             ProductRefResult productRef = productService.getProductRef(productId);
290 |             GatewayConfig gatewayConfig = gatewayService.getGatewayConfig(productRef.getGatewayId());
291 | 
292 |             // 取消网关上的Consumer授权
293 |             Optional.ofNullable(matchConsumerRef(consumerId, gatewayConfig))
294 |                     .ifPresent(consumerRef ->
295 |                             gatewayService.revokeConsumerAuthorization(productRef.getGatewayId(), consumerRef.getGwConsumerId(), subscription.getConsumerAuthConfig())
296 |                     );
297 |         }
298 | 
299 |         subscriptionRepository.deleteByConsumerIdAndProductId(consumerId, productId);
300 |     }
301 | 
302 |     @Override
303 |     public PageResult<SubscriptionResult> listSubscriptions(String consumerId, QuerySubscriptionParam param, Pageable pageable) {
304 |         existsConsumer(consumerId);
305 | 
306 |         Page<ProductSubscription> subscriptions = subscriptionRepository.findAll(buildCredentialSpec(consumerId, param), pageable);
307 | 
308 |         List<String> productIds = subscriptions.getContent().stream()
309 |                 .map(ProductSubscription::getProductId)
310 |                 .collect(Collectors.toList());
311 |         Map<String, ProductResult> products = productService.getProducts(productIds);
312 |         return new PageResult<SubscriptionResult>().convertFrom(subscriptions, s -> {
313 |             SubscriptionResult r = new SubscriptionResult().convertFrom(s);
314 |             ProductResult product = products.get(r.getProductId());
315 |             if (product != null) {
316 |                 r.setProductType(product.getType());
317 |                 r.setProductName(product.getName());
318 |             }
319 |             return r;
320 |         });
321 |     }
322 | 
323 |     @Override
324 |     public void deleteSubscription(String consumerId, String productId) {
325 |         existsConsumer(consumerId);
326 | 
327 |         subscriptionRepository.findByConsumerIdAndProductId(consumerId, productId)
328 |                 .ifPresent(subscriptionRepository::delete);
329 |     }
330 | 
331 |     @Override
332 |     public SubscriptionResult approveSubscription(String consumerId, String productId) {
333 |         existsConsumer(consumerId);
334 | 
335 |         ProductSubscription subscription = subscriptionRepository.findByConsumerIdAndProductId(consumerId, productId)
336 |                 .orElseThrow(() -> new BusinessException(ErrorCode.NOT_FOUND, Resources.SUBSCRIPTION, StrUtil.format("{}:{}", productId, consumerId)));
337 | 
338 |         // 检查订阅状态,只有PENDING状态的订阅才能被审批
339 |         if (subscription.getStatus() != SubscriptionStatus.PENDING) {
340 |             throw new BusinessException(ErrorCode.INVALID_REQUEST, "订阅已审批");
341 |         }
342 | 
343 |         // 获取消费者和凭证信息
344 |         Consumer consumer = contextHolder.isDeveloper() ?
345 |                 findDevConsumer(consumerId) : findConsumer(consumerId);
346 |         ConsumerCredential credential = credentialRepository.findByConsumerId(consumerId)
347 |                 .orElseThrow(() -> new BusinessException(ErrorCode.NOT_FOUND, Resources.CONSUMER_CREDENTIAL, consumerId));
348 | 
349 |         // 获取产品引用信息
350 |         ProductRefResult productRef = productService.getProductRef(productId);
351 |         if (productRef == null) {
352 |             throw new BusinessException(ErrorCode.INTERNAL_ERROR, "API产品未关联API");
353 |         }
354 | 
355 |         // 执行授权操作
356 |         ConsumerAuthConfig consumerAuthConfig = authorizeConsumer(consumer, credential, productRef);
357 | 
358 |         // 更新订阅状态和授权配置
359 |         subscription.setConsumerAuthConfig(consumerAuthConfig);
360 |         subscription.setStatus(SubscriptionStatus.APPROVED);
361 |         subscriptionRepository.saveAndFlush(subscription);
362 | 
363 |         ProductResult product = productService.getProduct(productId);
364 |         SubscriptionResult result = new SubscriptionResult().convertFrom(subscription);
365 |         if (product != null) {
366 |             result.setProductName(product.getName());
367 |             result.setProductType(product.getType());
368 |         }
369 |         return result;
370 |     }
371 | 
372 |     private Consumer findConsumer(String consumerId) {
373 |         return consumerRepository.findByConsumerId(consumerId)
374 |                 .orElseThrow(() -> new BusinessException(ErrorCode.NOT_FOUND, Resources.CONSUMER, consumerId));
375 |     }
376 | 
377 |     private Consumer findDevConsumer(String consumerId) {
378 |         return consumerRepository.findByDeveloperIdAndConsumerId(contextHolder.getUser(), consumerId)
379 |                 .orElseThrow(() -> new BusinessException(ErrorCode.NOT_FOUND, Resources.CONSUMER, consumerId));
380 |     }
381 | 
382 |     private void existsConsumer(String consumerId) {
383 |         (contextHolder.isDeveloper() ?
384 |                 consumerRepository.findByDeveloperIdAndConsumerId(contextHolder.getUser(), consumerId) :
385 |                 consumerRepository.findByConsumerId(consumerId))
386 |                 .orElseThrow(() -> new BusinessException(ErrorCode.NOT_FOUND, Resources.CONSUMER, consumerId));
387 |     }
388 | 
389 |     private Specification<Consumer> buildConsumerSpec(QueryConsumerParam param) {
390 |         return (root, query, cb) -> {
391 |             List<Predicate> predicates = new ArrayList<>();
392 | 
393 |             if (contextHolder.isDeveloper()) {
394 |                 param.setDeveloperId(contextHolder.getUser());
395 |             }
396 | 
397 |             if (StrUtil.isNotBlank(param.getDeveloperId())) {
398 |                 predicates.add(cb.equal(root.get("developerId"), param.getDeveloperId()));
399 |             }
400 | 
401 |             if (StrUtil.isNotBlank(param.getPortalId())) {
402 |                 predicates.add(cb.equal(root.get("portalId"), param.getPortalId()));
403 |             }
404 | 
405 |             if (StrUtil.isNotBlank(param.getName())) {
406 |                 String likePattern = "%" + param.getName() + "%";
407 |                 predicates.add(cb.like(cb.lower(root.get("name")), likePattern));
408 |             }
409 | 
410 |             return cb.and(predicates.toArray(new Predicate[0]));
411 |         };
412 |     }
413 | 
414 |     private Specification<ProductSubscription> buildCredentialSpec(String consumerId, QuerySubscriptionParam param) {
415 |         return (root, query, cb) -> {
416 |             List<Predicate> predicates = new ArrayList<>();
417 |             predicates.add(cb.equal(root.get("consumerId"), consumerId));
418 |             if (param.getStatus() != null) {
419 |                 predicates.add(cb.equal(root.get("status"), param.getStatus()));
420 |             }
421 |             if (StrUtil.isNotBlank(param.getProductName())) {
422 |                 // 使用子查询
423 |                 Subquery<String> productSubquery = query.subquery(String.class);
424 |                 Root<Product> productRoot = productSubquery.from(Product.class);
425 | 
426 |                 productSubquery.select(productRoot.get("productId"))
427 |                         .where(cb.like(
428 |                                 cb.lower(productRoot.get("name")),
429 |                                 "%" + param.getProductName().toLowerCase() + "%"
430 |                         ));
431 | 
432 |                 predicates.add(root.get("productId").in(productSubquery));
433 |             }
434 |             return cb.and(predicates.toArray(new Predicate[0]));
435 |         };
436 |     }
437 | 
438 |     /**
439 |      * 补充Credentials
440 |      *
441 |      * @param credential
442 |      */
443 |     private void complementCredentials(ConsumerCredential credential) {
444 |         if (credential == null) {
445 |             return;
446 |         }
447 | 
448 |         // ApiKey
449 |         if (credential.getApiKeyConfig() != null) {
450 |             List<ApiKeyConfig.ApiKeyCredential> apiKeyCredentials = credential.getApiKeyConfig().getCredentials();
451 |             if (apiKeyCredentials != null) {
452 |                 for (ApiKeyConfig.ApiKeyCredential cred : apiKeyCredentials) {
453 |                     if (cred.getMode() == CredentialMode.SYSTEM && StrUtil.isBlank(cred.getApiKey())) {
454 |                         cred.setApiKey(IdGenerator.genIdWithPrefix("apikey-"));
455 |                     }
456 |                 }
457 |             }
458 |         }
459 | 
460 |         // HMAC
461 |         if (credential.getHmacConfig() != null) {
462 |             List<HmacConfig.HmacCredential> hmacCredentials = credential.getHmacConfig().getCredentials();
463 |             if (hmacCredentials != null) {
464 |                 for (HmacConfig.HmacCredential cred : hmacCredentials) {
465 |                     if (cred.getMode() == CredentialMode.SYSTEM &&
466 |                             (StrUtil.isBlank(cred.getAk()) || StrUtil.isBlank(cred.getSk()))) {
467 |                         cred.setAk(IdGenerator.genIdWithPrefix("ak-"));
468 |                         cred.setSk(IdGenerator.genIdWithPrefix("sk-"));
469 |                     }
470 |                 }
471 |             }
472 |         }
473 |     }
474 | 
475 |     private ConsumerAuthConfig authorizeConsumer(Consumer consumer, ConsumerCredential credential, ProductRefResult productRef) {
476 |         GatewayConfig gatewayConfig = gatewayService.getGatewayConfig(productRef.getGatewayId());
477 | 
478 |         // 检查是否在网关上有对应的Consumer
479 |         ConsumerRef existingConsumerRef = matchConsumerRef(consumer.getConsumerId(), gatewayConfig);
480 |         String gwConsumerId;
481 |         
482 |         if (existingConsumerRef != null) {
483 |             // 如果存在ConsumerRef记录,需要检查实际网关中是否还存在该消费者
484 |             gwConsumerId = existingConsumerRef.getGwConsumerId();
485 |             
486 |             // 检查实际网关中是否还存在该消费者
487 |             if (!isConsumerExistsInGateway(gwConsumerId, gatewayConfig)) {
488 |                 log.warn("网关中的消费者已被删除,需要重新创建: gwConsumerId={}, gatewayType={}", 
489 |                     gwConsumerId, gatewayConfig.getGatewayType());
490 |                 
491 |                 // 删除过期的ConsumerRef记录
492 |                 consumerRefRepository.delete(existingConsumerRef);
493 |                 
494 |                 // 重新创建消费者
495 |                 gwConsumerId = gatewayService.createConsumer(consumer, credential, gatewayConfig);
496 |                 consumerRefRepository.save(ConsumerRef.builder()
497 |                         .consumerId(consumer.getConsumerId())
498 |                         .gwConsumerId(gwConsumerId)
499 |                         .gatewayType(gatewayConfig.getGatewayType())
500 |                         .gatewayConfig(gatewayConfig)
501 |                         .build());
502 |             }
503 |         } else {
504 |             // 如果不存在ConsumerRef记录,直接创建新的消费者
505 |             gwConsumerId = gatewayService.createConsumer(consumer, credential, gatewayConfig);
506 |             consumerRefRepository.save(ConsumerRef.builder()
507 |                     .consumerId(consumer.getConsumerId())
508 |                     .gwConsumerId(gwConsumerId)
509 |                     .gatewayType(gatewayConfig.getGatewayType())
510 |                     .gatewayConfig(gatewayConfig)
511 |                     .build());
512 |         }
513 | 
514 |         // 授权
515 |         return gatewayService.authorizeConsumer(productRef.getGatewayId(), gwConsumerId, productRef);
516 |     }
517 | 
518 |     /**
519 |      * 检查消费者是否在实际网关中存在
520 |      */
521 |     private boolean isConsumerExistsInGateway(String gwConsumerId, GatewayConfig gatewayConfig) {
522 |         try {
523 |             return gatewayService.isConsumerExists(gwConsumerId, gatewayConfig);
524 |         } catch (Exception e) {
525 |             log.warn("检查网关消费者存在性失败: gwConsumerId={}, gatewayType={}", 
526 |                 gwConsumerId, gatewayConfig.getGatewayType(), e);
527 |             // 如果检查失败,默认认为存在,避免无谓的重新创建
528 |             return true;
529 |         }
530 |     }
531 | 
532 |     @EventListener
533 |     @Async("taskExecutor")
534 |     public void handleDeveloperDeletion(DeveloperDeletingEvent event) {
535 |         String developerId = event.getDeveloperId();
536 |         log.info("Cleaning consumers for developer {}", developerId);
537 | 
538 |         List<Consumer> consumers = consumerRepository.findAllByDeveloperId(developerId);
539 |         consumers.forEach(consumer -> {
540 |             try {
541 |                 deleteConsumer(consumer.getConsumerId());
542 |             } catch (Exception e) {
543 |                 log.error("Failed to delete consumer {}", consumer.getConsumerId(), e);
544 |             }
545 |         });
546 |     }
547 | 
548 |     @EventListener
549 |     @Async("taskExecutor")
550 |     public void handleProductDeletion(ProductDeletingEvent event) {
551 |         String productId = event.getProductId();
552 |         log.info("Cleaning subscriptions for product {}", productId);
553 | 
554 |         subscriptionRepository.deleteAllByProductId(productId);
555 | 
556 |         List<ProductSubscription> subscriptions = subscriptionRepository.findAllByProductId(productId);
557 | 
558 |         subscriptions.forEach(subscription -> {
559 |             try {
560 |                 unsubscribeProduct(subscription.getConsumerId(), subscription.getProductId());
561 |             } catch (Exception e) {
562 |                 log.error("Failed to unsubscribe product {} for consumer {}", productId, subscription.getConsumerId(), e);
563 |             }
564 |         });
565 |     }
566 | 
567 |     private ConsumerRef matchConsumerRef(String consumerId, GatewayConfig gatewayConfig) {
568 |         List<ConsumerRef> consumeRefs = consumerRefRepository.findAllByConsumerIdAndGatewayType(consumerId, gatewayConfig.getGatewayType());
569 |         if (consumeRefs.isEmpty()) {
570 |             return null;
571 |         }
572 | 
573 |         for (ConsumerRef ref : consumeRefs) {
574 |             // 网关配置相同
575 |             if (StrUtil.equals(JSONUtil.toJsonStr(ref.getGatewayConfig()), JSONUtil.toJsonStr(gatewayConfig))) {
576 |                 return ref;
577 |             }
578 |         }
579 |         return null;
580 |     }
581 | }
582 | 
```

--------------------------------------------------------------------------------
/portal-server/src/main/java/com/alibaba/apiopenplatform/service/gateway/APIGOperator.java:
--------------------------------------------------------------------------------

```java
  1 | /*
  2 |  * Licensed to the Apache Software Foundation (ASF) under one
  3 |  * or more contributor license agreements.  See the NOTICE file
  4 |  * distributed with this work for additional information
  5 |  * regarding copyright ownership.  The ASF licenses this file
  6 |  * to you under the Apache License, Version 2.0 (the
  7 |  * "License"); you may not use this file except in compliance
  8 |  * with the License.  You may obtain a copy of the License at
  9 |  *
 10 |  *   http://www.apache.org/licenses/LICENSE-2.0
 11 |  *
 12 |  * Unless required by applicable law or agreed to in writing,
 13 |  * software distributed under the License is distributed on an
 14 |  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 15 |  * KIND, either express or implied.  See the License for the
 16 |  * specific language governing permissions and limitations
 17 |  * under the License.
 18 |  */
 19 | 
 20 | package com.alibaba.apiopenplatform.service.gateway;
 21 | 
 22 | import cn.hutool.core.codec.Base64;
 23 | import cn.hutool.core.collection.CollUtil;
 24 | import cn.hutool.core.util.StrUtil;
 25 | import cn.hutool.json.JSONUtil;
 26 | import com.alibaba.apiopenplatform.dto.params.gateway.QueryAPIGParam;
 27 | import com.alibaba.apiopenplatform.dto.result.*;
 28 | import com.alibaba.apiopenplatform.support.consumer.APIGAuthConfig;
 29 | import com.alibaba.apiopenplatform.support.consumer.ApiKeyConfig;
 30 | import com.alibaba.apiopenplatform.support.consumer.ConsumerAuthConfig;
 31 | import com.alibaba.apiopenplatform.support.consumer.HmacConfig;
 32 | import com.alibaba.apiopenplatform.support.enums.APIGAPIType;
 33 | import com.alibaba.apiopenplatform.core.exception.BusinessException;
 34 | import com.alibaba.apiopenplatform.core.exception.ErrorCode;
 35 | import com.alibaba.apiopenplatform.entity.Gateway;
 36 | import com.alibaba.apiopenplatform.entity.Consumer;
 37 | import com.alibaba.apiopenplatform.entity.ConsumerCredential;
 38 | import com.alibaba.apiopenplatform.service.gateway.client.APIGClient;
 39 | import com.alibaba.apiopenplatform.service.gateway.client.SLSClient;
 40 | import com.alibaba.apiopenplatform.support.enums.GatewayType;
 41 | import com.alibaba.apiopenplatform.support.gateway.GatewayConfig;
 42 | import com.alibaba.apiopenplatform.support.product.APIGRefConfig;
 43 | import com.aliyun.sdk.gateway.pop.exception.PopClientException;
 44 | import com.aliyun.sdk.service.apig20240327.models.*;
 45 | import com.aliyun.sdk.service.apig20240327.models.CreateConsumerAuthorizationRulesRequest.AuthorizationRules;
 46 | import com.aliyun.sdk.service.apig20240327.models.CreateConsumerAuthorizationRulesRequest.ResourceIdentifier;
 47 | import com.aliyun.sdk.service.sls20201230.models.*;
 48 | import lombok.RequiredArgsConstructor;
 49 | import lombok.extern.slf4j.Slf4j;
 50 | import org.springframework.context.annotation.Primary;
 51 | import org.springframework.stereotype.Service;
 52 | 
 53 | import java.util.ArrayList;
 54 | import java.util.List;
 55 | import java.util.concurrent.CompletableFuture;
 56 | import java.util.concurrent.ExecutionException;
 57 | import java.util.stream.Collectors;
 58 | 
 59 | @RequiredArgsConstructor
 60 | @Service
 61 | @Slf4j
 62 | @Primary
 63 | public class APIGOperator extends GatewayOperator<APIGClient> {
 64 | 
 65 |     @Override
 66 |     public PageResult<APIResult> fetchHTTPAPIs(Gateway gateway, int page, int size) {
 67 |         return fetchAPIs(gateway, APIGAPIType.HTTP, page, size);
 68 |     }
 69 | 
 70 |     public PageResult<APIResult> fetchRESTAPIs(Gateway gateway, int page, int size) {
 71 |         return fetchAPIs(gateway, APIGAPIType.REST, page, size);
 72 |     }
 73 | 
 74 |     @Override
 75 |     public PageResult<? extends GatewayMCPServerResult> fetchMcpServers(Gateway gateway, int page, int size) {
 76 |         throw new UnsupportedOperationException("APIG does not support MCP Servers");
 77 |     }
 78 | 
 79 |     @Override
 80 |     public String fetchAPIConfig(Gateway gateway, Object config) {
 81 |         APIGClient client = getClient(gateway);
 82 | 
 83 |         try {
 84 |             APIGRefConfig apigRefConfig = (APIGRefConfig) config;
 85 |             CompletableFuture<ExportHttpApiResponse> f = client.execute(c -> {
 86 |                 ExportHttpApiRequest request = ExportHttpApiRequest.builder()
 87 |                         .httpApiId(apigRefConfig.getApiId())
 88 |                         .build();
 89 |                 return c.exportHttpApi(request);
 90 |             });
 91 | 
 92 |             ExportHttpApiResponse response = f.join();
 93 |             if (response.getStatusCode() != 200) {
 94 |                 throw new BusinessException(ErrorCode.GATEWAY_ERROR, response.getBody().getMessage());
 95 |             }
 96 | 
 97 |             String contentBase64 = response.getBody().getData().getSpecContentBase64();
 98 | 
 99 |             APIConfigResult configResult = new APIConfigResult();
100 |             // spec
101 |             String apiSpec = Base64.decodeStr(contentBase64);
102 |             configResult.setSpec(apiSpec);
103 | 
104 |             // meta
105 |             APIConfigResult.APIMetadata meta = new APIConfigResult.APIMetadata();
106 |             meta.setSource(GatewayType.APIG_API.name());
107 |             meta.setType("REST");
108 |             configResult.setMeta(meta);
109 | 
110 |             return JSONUtil.toJsonStr(configResult);
111 |         } catch (Exception e) {
112 |             log.error("Error fetching API Spec", e);
113 |             throw new BusinessException(ErrorCode.INTERNAL_ERROR, "Error fetching API Spec,Cause:" + e.getMessage());
114 |         }
115 |     }
116 | 
117 |     @Override
118 |     public String fetchMcpConfig(Gateway gateway, Object conf) {
119 |         throw new UnsupportedOperationException("APIG does not support MCP Servers");
120 |     }
121 | 
122 |     @Override
123 |     public PageResult<GatewayResult> fetchGateways(Object param, int page, int size) {
124 |         return fetchGateways((QueryAPIGParam) param, page, size);
125 |     }
126 | 
127 |     public PageResult<GatewayResult> fetchGateways(QueryAPIGParam param, int page, int size) {
128 |         APIGClient client = new APIGClient(param.convertTo());
129 | 
130 |         List<GatewayResult> gateways = new ArrayList<>();
131 |         try {
132 |             CompletableFuture<ListGatewaysResponse> f = client.execute(c -> {
133 |                 ListGatewaysRequest request = ListGatewaysRequest.builder()
134 |                         .gatewayType(param.getGatewayType().getType())
135 |                         .pageNumber(page)
136 |                         .pageSize(size)
137 |                         .build();
138 | 
139 |                 return c.listGateways(request);
140 |             });
141 | 
142 |             ListGatewaysResponse response = f.join();
143 |             if (response.getStatusCode() != 200) {
144 |                 throw new BusinessException(ErrorCode.GATEWAY_ERROR, response.getBody().getMessage());
145 |             }
146 | 
147 |             for (ListGatewaysResponseBody.Items item : response.getBody().getData().getItems()) {
148 |                 gateways.add(GatewayResult.builder()
149 |                         .gatewayName(item.getName())
150 |                         .gatewayId(item.getGatewayId())
151 |                         .gatewayType(param.getGatewayType())
152 |                         .build());
153 |             }
154 | 
155 |             int total = Math.toIntExact(response.getBody().getData().getTotalSize());
156 |             return PageResult.of(gateways, page, size, total);
157 |         } catch (Exception e) {
158 |             log.error("Error fetching Gateways", e);
159 |             throw new BusinessException(ErrorCode.INTERNAL_ERROR, "Error fetching Gateways,Cause:" + e.getMessage());
160 |         }
161 |     }
162 | 
163 |     protected String fetchGatewayEnv(Gateway gateway) {
164 |         APIGClient client = getClient(gateway);
165 |         try {
166 |             CompletableFuture<GetGatewayResponse> f = client.execute(c -> {
167 |                 GetGatewayRequest request = GetGatewayRequest.builder()
168 |                         .gatewayId(gateway.getGatewayId())
169 |                         .build();
170 | 
171 |                 return c.getGateway(request);
172 | 
173 |             });
174 | 
175 |             GetGatewayResponse response = f.join();
176 |             if (response.getStatusCode() != 200) {
177 |                 throw new BusinessException(ErrorCode.GATEWAY_ERROR, response.getBody().getMessage());
178 |             }
179 | 
180 |             List<GetGatewayResponseBody.Environments> environments = response.getBody().getData().getEnvironments();
181 |             if (CollUtil.isEmpty(environments)) {
182 |                 return null;
183 |             }
184 | 
185 |             return environments.get(0).getEnvironmentId();
186 |         } catch (Exception e) {
187 |             log.error("Error fetching Gateway", e);
188 |             throw new BusinessException(ErrorCode.INTERNAL_ERROR, "Error fetching Gateway,Cause:" + e.getMessage());
189 |         }
190 |     }
191 | 
192 |     @Override
193 |     public String createConsumer(Consumer consumer, ConsumerCredential credential, GatewayConfig config) {
194 |         APIGClient client = new APIGClient(config.getApigConfig());
195 | 
196 |         String mark = consumer.getConsumerId().substring(Math.max(0, consumer.getConsumerId().length() - 8));
197 |         String gwConsumerName = StrUtil.format("{}-{}", consumer.getName(), mark);
198 |         try {
199 |             // ApiKey
200 |             ApiKeyIdentityConfig apikeyIdentityConfig = convertToApiKeyIdentityConfig(credential.getApiKeyConfig());
201 | 
202 |             // Hmac
203 |             List<AkSkIdentityConfig> akSkIdentityConfigs = convertToAkSkIdentityConfigs(credential.getHmacConfig());
204 | 
205 |             CreateConsumerRequest.Builder builder = CreateConsumerRequest.builder()
206 |                     .name(gwConsumerName)
207 |                     .description("Created by HiMarket")
208 |                     .gatewayType(config.getGatewayType().getType())
209 |                     .enable(true);
210 |             if (apikeyIdentityConfig != null) {
211 |                 builder.apikeyIdentityConfig(apikeyIdentityConfig);
212 |             }
213 |             if (akSkIdentityConfigs != null) {
214 |                 builder.akSkIdentityConfigs(akSkIdentityConfigs);
215 |             }
216 | 
217 |             CompletableFuture<CreateConsumerResponse> f = client.execute(c -> c.createConsumer(builder.build()));
218 | 
219 |             CreateConsumerResponse response = f.join();
220 |             if (response.getStatusCode() != 200) {
221 |                 throw new BusinessException(ErrorCode.GATEWAY_ERROR, response.getBody().getMessage());
222 |             }
223 | 
224 |             return response.getBody().getData().getConsumerId();
225 |         } catch (Exception e) {
226 |             Throwable cause = e.getCause();
227 |             // Consumer已经存在
228 |             if (cause instanceof PopClientException && "Conflict.ConsumerNameDuplicate".equals(((PopClientException) cause).getErrCode())) {
229 |                 return retrievalConsumer(gwConsumerName, config);
230 |             }
231 |             log.error("Error creating Consumer", e);
232 |             throw new BusinessException(ErrorCode.INTERNAL_ERROR, "Error creating Consumer,Cause:" + e.getMessage());
233 |         }
234 |     }
235 | 
236 |     private String retrievalConsumer(String name, GatewayConfig gatewayConfig) {
237 |         APIGClient client = new APIGClient(gatewayConfig.getApigConfig());
238 | 
239 |         try {
240 |             CompletableFuture<ListConsumersResponse> f = client.execute(c -> {
241 |                 ListConsumersRequest request = ListConsumersRequest.builder()
242 |                         .gatewayType(gatewayConfig.getGatewayType().getType())
243 |                         .nameLike(name)
244 |                         .pageNumber(1)
245 |                         .pageSize(10)
246 |                         .build();
247 | 
248 |                 return c.listConsumers(request);
249 |             });
250 |             ListConsumersResponse response = f.join();
251 |             if (response.getStatusCode() != 200) {
252 |                 throw new BusinessException(ErrorCode.GATEWAY_ERROR, response.getBody().getMessage());
253 |             }
254 | 
255 |             for (ListConsumersResponseBody.Items item : response.getBody().getData().getItems()) {
256 |                 if (StrUtil.equals(item.getName(), name)) {
257 |                     return item.getConsumerId();
258 |                 }
259 |             }
260 |         } catch (Exception e) {
261 |             log.error("Error fetching Consumer", e);
262 |             throw new BusinessException(ErrorCode.INTERNAL_ERROR, "Error fetching Consumer,Cause:" + e.getMessage());
263 |         }
264 |         return null;
265 |     }
266 | 
267 |     @Override
268 |     public void updateConsumer(String consumerId, ConsumerCredential credential, GatewayConfig config) {
269 |         APIGClient client = new APIGClient(config.getApigConfig());
270 |         try {
271 |             // ApiKey
272 |             ApiKeyIdentityConfig apikeyIdentityConfig = convertToApiKeyIdentityConfig(credential.getApiKeyConfig());
273 | 
274 |             // Hmac
275 |             List<AkSkIdentityConfig> akSkIdentityConfigs = convertToAkSkIdentityConfigs(credential.getHmacConfig());
276 | 
277 |             UpdateConsumerRequest.Builder builder = UpdateConsumerRequest.builder()
278 |                     .enable(true)
279 |                     .consumerId(consumerId);
280 | 
281 |             if (apikeyIdentityConfig != null) {
282 |                 builder.apikeyIdentityConfig(apikeyIdentityConfig);
283 |             }
284 | 
285 |             if (akSkIdentityConfigs != null) {
286 |                 builder.akSkIdentityConfigs(akSkIdentityConfigs);
287 |             }
288 | 
289 |             CompletableFuture<UpdateConsumerResponse> f = client.execute(c -> c.updateConsumer(builder.build()));
290 | 
291 |             UpdateConsumerResponse response = f.join();
292 |             if (response.getStatusCode() != 200) {
293 |                 throw new BusinessException(ErrorCode.GATEWAY_ERROR, response.getBody().getMessage());
294 |             }
295 |         } catch (Exception e) {
296 |             log.error("Error creating Consumer", e);
297 |             throw new BusinessException(ErrorCode.INTERNAL_ERROR, "Error creating Consumer,Cause:" + e.getMessage());
298 |         }
299 |     }
300 | 
301 |     @Override
302 |     public void deleteConsumer(String consumerId, GatewayConfig config) {
303 |         APIGClient client = new APIGClient(config.getApigConfig());
304 |         try {
305 |             DeleteConsumerRequest request = DeleteConsumerRequest.builder()
306 |                     .consumerId(consumerId)
307 |                     .build();
308 |             client.execute(c -> {
309 |                 c.deleteConsumer(request);
310 |                 return null;
311 |             });
312 |         } catch (Exception e) {
313 |             log.error("Error deleting Consumer", e);
314 |             throw new BusinessException(ErrorCode.INTERNAL_ERROR, "Error deleting Consumer,Cause:" + e.getMessage());
315 |         }
316 |     }
317 | 
318 |     @Override
319 |     public boolean isConsumerExists(String consumerId, GatewayConfig config) {
320 |         // TODO: 实现APIG网关消费者存在性检查
321 |         return true;
322 |     }
323 | 
324 |     @Override
325 |     public ConsumerAuthConfig authorizeConsumer(Gateway gateway, String consumerId, Object refConfig) {
326 |         APIGClient client = getClient(gateway);
327 | 
328 |         APIGRefConfig config = (APIGRefConfig) refConfig;
329 |         // REST API 授权
330 |         String apiId = config.getApiId();
331 | 
332 |         try {
333 |             List<HttpApiOperationInfo> operations = fetchRESTOperations(gateway, apiId);
334 |             if (CollUtil.isEmpty(operations)) {
335 |                 return null;
336 |             }
337 | 
338 |             // 确认Gateway的EnvId
339 |             String envId = fetchGatewayEnv(gateway);
340 | 
341 |             List<AuthorizationRules> rules = new ArrayList<>();
342 |             for (HttpApiOperationInfo operation : operations) {
343 |                 AuthorizationRules rule = AuthorizationRules.builder()
344 |                         .consumerId(consumerId)
345 |                         .expireMode("LongTerm")
346 |                         .resourceType("RestApiOperation")
347 |                         .resourceIdentifier(ResourceIdentifier.builder()
348 |                                 .resourceId(operation.getOperationId())
349 |                                 .environmentId(envId).build())
350 |                         .build();
351 |                 rules.add(rule);
352 |             }
353 | 
354 |             CompletableFuture<CreateConsumerAuthorizationRulesResponse> f = client.execute(c -> {
355 |                 CreateConsumerAuthorizationRulesRequest request = CreateConsumerAuthorizationRulesRequest.builder()
356 |                         .authorizationRules(rules)
357 |                         .build();
358 |                 return c.createConsumerAuthorizationRules(request);
359 |             });
360 | 
361 |             CreateConsumerAuthorizationRulesResponse response = f.join();
362 |             if (200 != response.getStatusCode()) {
363 |                 throw new BusinessException(ErrorCode.GATEWAY_ERROR, response.getBody().getMessage());
364 |             }
365 | 
366 |             APIGAuthConfig apigAuthConfig = APIGAuthConfig.builder()
367 |                     .authorizationRuleIds(response.getBody().getData().getConsumerAuthorizationRuleIds())
368 |                     .build();
369 | 
370 |             return ConsumerAuthConfig.builder()
371 |                     .apigAuthConfig(apigAuthConfig)
372 |                     .build();
373 |         } catch (Exception e) {
374 |             log.error("Error authorizing consumer {} to apiId {} in APIG gateway {}", consumerId, apiId, gateway.getGatewayId(), e);
375 |             throw new BusinessException(ErrorCode.GATEWAY_ERROR, "Failed to authorize consumer to apiId in APIG gateway: " + e.getMessage());
376 |         }
377 |     }
378 | 
379 |     @Override
380 |     public void revokeConsumerAuthorization(Gateway gateway, String consumerId, ConsumerAuthConfig authConfig) {
381 |         APIGAuthConfig apigAuthConfig = authConfig.getApigAuthConfig();
382 |         if (apigAuthConfig == null) {
383 |             return;
384 |         }
385 | 
386 |         APIGClient client = getClient(gateway);
387 | 
388 |         try {
389 |             BatchDeleteConsumerAuthorizationRuleRequest request = BatchDeleteConsumerAuthorizationRuleRequest.builder()
390 |                     .consumerAuthorizationRuleIds(StrUtil.join(",", apigAuthConfig.getAuthorizationRuleIds()))
391 |                     .build();
392 | 
393 |             CompletableFuture<BatchDeleteConsumerAuthorizationRuleResponse> f = client.execute(c -> c.batchDeleteConsumerAuthorizationRule(request));
394 | 
395 |             BatchDeleteConsumerAuthorizationRuleResponse response = f.join();
396 |             if (response.getStatusCode() != 200) {
397 |                 throw new BusinessException(ErrorCode.GATEWAY_ERROR, response.getBody().getMessage());
398 |             }
399 |         } catch (Exception e) {
400 |             Throwable cause = e.getCause();
401 |             if (cause instanceof PopClientException
402 |                     && "DatabaseError.RecordNotFound".equals(((PopClientException) cause).getErrCode())) {
403 |                 log.warn("Consumer authorization rules[{}] not found, ignore", apigAuthConfig.getAuthorizationRuleIds());
404 |                 return;
405 |             }
406 | 
407 |             log.error("Error deleting Consumer Authorization", e);
408 |             throw new BusinessException(ErrorCode.INTERNAL_ERROR, "Error deleting Consumer Authorization,Cause:" + e.getMessage());
409 |         }
410 |     }
411 | 
412 |     @Override
413 |     public GatewayType getGatewayType() {
414 |         return GatewayType.APIG_API;
415 |     }
416 | 
417 |     @Override
418 |     public String getDashboard(Gateway gateway, String type) {
419 |         SLSClient ticketClient = new SLSClient(gateway.getApigConfig(), true);
420 |         String ticket = null;
421 |         try {
422 |             CreateTicketResponse response = ticketClient.execute(c -> {
423 |                 CreateTicketRequest request = CreateTicketRequest.builder().build();
424 |                 try {
425 |                     return c.createTicket(request).get();
426 |                 } catch (InterruptedException | ExecutionException e) {
427 |                     throw new RuntimeException(e);
428 |                 }
429 |             });
430 |             ticket = response.getBody().getTicket();
431 |         } catch (Exception e) {
432 |             log.error("Error fetching API", e);
433 |             throw new BusinessException(ErrorCode.INTERNAL_ERROR, "Error fetching createTicker API,Cause:" + e.getMessage());
434 |         }
435 |         SLSClient client = new SLSClient(gateway.getApigConfig(), false);
436 |         String projectName = null;
437 |         try {
438 |             ListProjectResponse response = client.execute(c -> {
439 |                 ListProjectRequest request = ListProjectRequest.builder().projectName("product").build();
440 |                 try {
441 |                     return c.listProject(request).get();
442 |                 } catch (InterruptedException | ExecutionException e) {
443 |                     throw new RuntimeException(e);
444 |                 }
445 |             });
446 |             projectName = response.getBody().getProjects().get(0).getProjectName();
447 |         } catch (Exception e) {
448 |             log.error("Error fetching Project", e);
449 |             throw new BusinessException(ErrorCode.INTERNAL_ERROR, "Error fetching Project,Cause:" + e.getMessage());
450 |         }
451 |         String region = gateway.getApigConfig().getRegion();
452 |         String gatewayId = gateway.getGatewayId();
453 |         String dashboardId = "";
454 |         if (type.equals("Portal")) {
455 |             dashboardId = "dashboard-1758009692051-393998";
456 |         } else if (type.equals("MCP")) {
457 |             dashboardId = "dashboard-1757483808537-433375";
458 |         } else if (type.equals("API")) {
459 |             dashboardId = "dashboard-1756276497392-966932";
460 |         }
461 |         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);
462 |         log.info("Dashboard URL: {}", dashboardUrl);
463 |         return dashboardUrl;
464 |     }
465 | 
466 |     public APIResult fetchAPI(Gateway gateway, String apiId) {
467 |         APIGClient client = getClient(gateway);
468 |         try {
469 |             CompletableFuture<GetHttpApiResponse> f = client.execute(c -> {
470 |                 GetHttpApiRequest request = GetHttpApiRequest.builder()
471 |                         .httpApiId(apiId)
472 |                         .build();
473 | 
474 |                 return c.getHttpApi(request);
475 | 
476 |             });
477 | 
478 |             GetHttpApiResponse response = f.join();
479 |             if (response.getStatusCode() != 200) {
480 |                 throw new BusinessException(ErrorCode.GATEWAY_ERROR, response.getBody().getMessage());
481 |             }
482 | 
483 |             HttpApiApiInfo apiInfo = response.getBody().getData();
484 |             return new APIResult().convertFrom(apiInfo);
485 |         } catch (Exception e) {
486 |             log.error("Error fetching API", e);
487 |             throw new BusinessException(ErrorCode.INTERNAL_ERROR, "Error fetching API,Cause:" + e.getMessage());
488 |         }
489 |     }
490 | 
491 |     protected HttpRoute fetchHTTPRoute(Gateway gateway, String apiId, String routeId) {
492 |         APIGClient client = getClient(gateway);
493 | 
494 |         try {
495 |             CompletableFuture<GetHttpApiRouteResponse> f = client.execute(c -> {
496 |                 GetHttpApiRouteRequest request = GetHttpApiRouteRequest.builder()
497 |                         .httpApiId(apiId)
498 |                         .routeId(routeId)
499 |                         .build();
500 | 
501 |                 return c.getHttpApiRoute(request);
502 | 
503 |             });
504 | 
505 |             GetHttpApiRouteResponse response = f.join();
506 |             if (response.getStatusCode() != 200) {
507 |                 throw new BusinessException(ErrorCode.GATEWAY_ERROR, response.getBody().getMessage());
508 |             }
509 | 
510 |             return response.getBody().getData();
511 | 
512 |         } catch (Exception e) {
513 |             log.error("Error fetching HTTP Route", e);
514 |             throw new BusinessException(ErrorCode.INTERNAL_ERROR, "Error fetching HTTP Route,Cause:" + e.getMessage());
515 |         }
516 |     }
517 | 
518 |     protected PageResult<APIResult> fetchAPIs(Gateway gateway, APIGAPIType type, int page, int size) {
519 |         APIGClient client = getClient(gateway);
520 |         try {
521 |             List<APIResult> apis = new ArrayList<>();
522 |             CompletableFuture<ListHttpApisResponse> f = client.execute(c -> {
523 |                 ListHttpApisRequest request = ListHttpApisRequest.builder()
524 |                         .gatewayId(gateway.getGatewayId())
525 |                         .gatewayType(gateway.getGatewayType().getType())
526 |                         .types(type.getType())
527 |                         .pageNumber(page)
528 |                         .pageSize(size)
529 |                         .build();
530 | 
531 |                 return c.listHttpApis(request);
532 |             });
533 | 
534 |             ListHttpApisResponse response = f.join();
535 |             if (response.getStatusCode() != 200) {
536 |                 throw new BusinessException(ErrorCode.GATEWAY_ERROR, response.getBody().getMessage());
537 |             }
538 | 
539 |             for (HttpApiInfoByName item : response.getBody().getData().getItems()) {
540 |                 for (HttpApiApiInfo apiInfo : item.getVersionedHttpApis()) {
541 |                     APIResult apiResult = new APIResult().convertFrom(apiInfo);
542 |                     apis.add(apiResult);
543 |                     break;
544 |                 }
545 |             }
546 | 
547 |             int total = response.getBody().getData().getTotalSize();
548 |             return PageResult.of(apis, page, size, total);
549 |         } catch (Exception e) {
550 |             log.error("Error fetching APIs", e);
551 |             throw new BusinessException(ErrorCode.INTERNAL_ERROR, "Error fetching APIs,Cause:" + e.getMessage());
552 |         }
553 |     }
554 | 
555 |     public PageResult<HttpRoute> fetchHttpRoutes(Gateway gateway, String apiId, int page, int size) {
556 |         APIGClient client = getClient(gateway);
557 |         try {
558 |             CompletableFuture<ListHttpApiRoutesResponse> f = client.execute(c -> {
559 |                 ListHttpApiRoutesRequest request = ListHttpApiRoutesRequest.builder()
560 |                         .gatewayId(gateway.getGatewayId())
561 |                         .httpApiId(apiId)
562 |                         .pageNumber(page)
563 |                         .pageSize(size)
564 |                         .build();
565 | 
566 |                 return c.listHttpApiRoutes(request);
567 | 
568 |             });
569 | 
570 |             ListHttpApiRoutesResponse response = f.join();
571 |             if (response.getStatusCode() != 200) {
572 |                 throw new BusinessException(ErrorCode.GATEWAY_ERROR, response.getBody().getMessage());
573 |             }
574 |             List<HttpRoute> httpRoutes = response.getBody().getData().getItems();
575 |             int total = response.getBody().getData().getTotalSize();
576 |             return PageResult.of(httpRoutes, page, size, total);
577 |         } catch (Exception e) {
578 |             log.error("Error fetching HTTP Roues", e);
579 |             throw new BusinessException(ErrorCode.INTERNAL_ERROR, "Error fetching HTTP Roues,Cause:" + e.getMessage());
580 |         }
581 |     }
582 | 
583 |     public List<HttpApiOperationInfo> fetchRESTOperations(Gateway gateway, String apiId) {
584 |         APIGClient client = getClient(gateway);
585 | 
586 |         try {
587 |             CompletableFuture<ListHttpApiOperationsResponse> f = client.execute(c -> {
588 |                 ListHttpApiOperationsRequest request = ListHttpApiOperationsRequest.builder()
589 |                         .gatewayId(gateway.getGatewayId())
590 |                         .httpApiId(apiId)
591 |                         .pageNumber(1)
592 |                         .pageSize(500)
593 |                         .build();
594 | 
595 |                 return c.listHttpApiOperations(request);
596 | 
597 |             });
598 | 
599 |             ListHttpApiOperationsResponse response = f.join();
600 |             if (response.getStatusCode() != 200) {
601 |                 throw new BusinessException(ErrorCode.GATEWAY_ERROR, response.getBody().getMessage());
602 |             }
603 | 
604 |             return response.getBody().getData().getItems();
605 |         } catch (Exception e) {
606 |             log.error("Error fetching REST operations", e);
607 |             throw new BusinessException(ErrorCode.INTERNAL_ERROR, "Error fetching REST operations,Cause:" + e.getMessage());
608 |         }
609 |     }
610 | 
611 |     protected ApiKeyIdentityConfig convertToApiKeyIdentityConfig(ApiKeyConfig config) {
612 |         if (config == null) {
613 |             return null;
614 |         }
615 | 
616 |         // ApikeySource
617 |         ApiKeyIdentityConfig.ApikeySource apikeySource = ApiKeyIdentityConfig.ApikeySource.builder()
618 |                 .source(config.getSource())
619 |                 .value(config.getKey())
620 |                 .build();
621 | 
622 |         // credentials
623 |         List<ApiKeyIdentityConfig.Credentials> credentials = config.getCredentials().stream()
624 |                 .map(cred -> ApiKeyIdentityConfig.Credentials.builder()
625 |                         .apikey(cred.getApiKey())
626 |                         .generateMode("Custom")
627 |                         .build())
628 |                 .collect(Collectors.toList());
629 | 
630 |         return ApiKeyIdentityConfig.builder()
631 |                 .apikeySource(apikeySource)
632 |                 .credentials(credentials)
633 |                 .type("Apikey")
634 |                 .build();
635 |     }
636 | 
637 |     protected List<AkSkIdentityConfig> convertToAkSkIdentityConfigs(HmacConfig hmacConfig) {
638 |         if (hmacConfig == null || hmacConfig.getCredentials() == null) {
639 |             return null;
640 |         }
641 | 
642 |         return hmacConfig.getCredentials().stream()
643 |                 .map(cred -> AkSkIdentityConfig.builder()
644 |                         .ak(cred.getAk())
645 |                         .sk(cred.getSk())
646 |                         .generateMode("Custom")
647 |                         .type("AkSk")
648 |                         .build())
649 |                 .collect(Collectors.toList());
650 |     }
651 | }
652 | 
653 | 
```

--------------------------------------------------------------------------------
/portal-web/api-portal-frontend/src/components/consumer/CredentialManager.tsx:
--------------------------------------------------------------------------------

```typescript
  1 | import {useState, useEffect} from "react";
  2 | import {
  3 |     Card,
  4 |     Button,
  5 |     message,
  6 |     Tabs,
  7 |     Modal,
  8 |     Radio,
  9 |     Input,
 10 |     Table,
 11 |     Popconfirm,
 12 |     Select,
 13 |     Form,
 14 | } from "antd";
 15 | import {
 16 |     PlusOutlined,
 17 |     InfoCircleOutlined,
 18 |     CopyOutlined,
 19 |     DeleteOutlined,
 20 |     EditOutlined
 21 | } from "@ant-design/icons";
 22 | import api from "../../lib/api";
 23 | import type {
 24 |     ConsumerCredentialResult,
 25 |     CreateCredentialParam,
 26 |     ConsumerCredential,
 27 |     HMACCredential,
 28 |     APIKeyCredential
 29 | } from "../../types/consumer";
 30 | import type {ApiResponse} from "../../types";
 31 | 
 32 | interface CredentialManagerProps {
 33 |     consumerId: string;
 34 | }
 35 | 
 36 | export function CredentialManager({consumerId}: CredentialManagerProps) {
 37 |     const [credentialType, setCredentialType] = useState<'API_KEY' | 'HMAC'>('API_KEY');
 38 |     const [credentialModalVisible, setCredentialModalVisible] = useState(false);
 39 |     const [credentialLoading, setCredentialLoading] = useState(false);
 40 | 
 41 |     const [sourceModalVisible, setSourceModalVisible] = useState(false);
 42 |     const [editingSource, setEditingSource] = useState<string>('Default');
 43 |     const [editingKey, setEditingKey] = useState<string>('Authorization');
 44 |     // 已保存(展示用)与编辑中的两套状态,取消时回滚到已保存值
 45 |     const [currentSource, setCurrentSource] = useState<string>('Default');
 46 |     const [currentKey, setCurrentKey] = useState<string>('Authorization');
 47 |     // 表单(编辑凭证来源)
 48 |     const [sourceForm] = Form.useForm();
 49 |     // 表单(创建凭证)
 50 |     const [credentialForm] = Form.useForm();
 51 |     // 当前完整配置(驱动表格数据源)
 52 |     const [currentConfig, setCurrentConfig] = useState<ConsumerCredentialResult | null>(null);
 53 | 
 54 |     // 初始化时获取当前配置
 55 |     const fetchCurrentConfig = async () => {
 56 |         try {
 57 |             const response: ApiResponse<ConsumerCredentialResult> = await api.get(`/consumers/${consumerId}/credentials`);
 58 |             if (response.code === "SUCCESS" && response.data) {
 59 |                 const config = response.data;
 60 |                 setCurrentConfig(config);
 61 |                 if (config.apiKeyConfig) {
 62 |                     setCurrentSource(config.apiKeyConfig.source || 'Default');
 63 |                     setCurrentKey(config.apiKeyConfig.key || 'Authorization');
 64 |                 }
 65 |             }
 66 |         } catch (error) {
 67 |             console.error('获取当前配置失败:', error);
 68 |         }
 69 |     };
 70 | 
 71 |     // 组件挂载时获取配置
 72 |     useEffect(() => {
 73 |         fetchCurrentConfig();
 74 |     }, [consumerId]);
 75 | 
 76 |     const handleCreateCredential = async () => {
 77 |         try {
 78 |             const values = await credentialForm.validateFields();
 79 |             setCredentialLoading(true);
 80 | 
 81 |             // 先获取当前的凭证配置
 82 |             const currentResponse: ApiResponse<ConsumerCredentialResult> = await api.get(`/consumers/${consumerId}/credentials`);
 83 |             let currentConfig: ConsumerCredentialResult = {};
 84 | 
 85 |             if (currentResponse.code === "SUCCESS" && currentResponse.data) {
 86 |                 currentConfig = currentResponse.data;
 87 |             }
 88 | 
 89 |             // 构建新的凭证配置
 90 |             const param: CreateCredentialParam = {
 91 |                 ...currentConfig,
 92 |             };
 93 | 
 94 |             if (credentialType === 'API_KEY') {
 95 |                 const newCredential: ConsumerCredential = {
 96 |                     apiKey: values.generationMethod === 'CUSTOM' ? values.customApiKey : generateRandomCredential('apiKey'),
 97 |                     mode: values.generationMethod
 98 |                 };
 99 |                 param.apiKeyConfig = {
100 |                     ...currentConfig.apiKeyConfig,
101 |                     credentials: [...(currentConfig.apiKeyConfig?.credentials || []), newCredential]
102 |                 };
103 |             } else if (credentialType === 'HMAC') {
104 |                 const newCredential: ConsumerCredential = {
105 |                     ak: values.generationMethod === 'CUSTOM' ? values.customAccessKey : generateRandomCredential('accessKey'),
106 |                     sk: values.generationMethod === 'CUSTOM' ? values.customSecretKey : generateRandomCredential('secretKey'),
107 |                     mode: values.generationMethod
108 |                 };
109 |                 param.hmacConfig = {
110 |                     ...currentConfig.hmacConfig,
111 |                     credentials: [...(currentConfig.hmacConfig?.credentials || []), newCredential]
112 |                 };
113 |             }
114 | 
115 |             const response: ApiResponse<ConsumerCredentialResult> = await api.put(`/consumers/${consumerId}/credentials`, param);
116 |             if (response?.code === "SUCCESS") {
117 |                 message.success('凭证添加成功');
118 |                 setCredentialModalVisible(false);
119 |                 resetCredentialForm();
120 |                 // 刷新当前配置以驱动表格
121 |                 await fetchCurrentConfig();
122 |             }
123 |         } catch (error) {
124 |             console.error('创建凭证失败:', error);
125 |             // message.error('创建凭证失败');
126 |         } finally {
127 |             setCredentialLoading(false);
128 |         }
129 |     };
130 | 
131 |     const handleDeleteCredential = async (credentialType: string, credential: ConsumerCredential) => {
132 |         try {
133 |             // 先获取当前的凭证配置
134 |             const currentResponse: ApiResponse<ConsumerCredentialResult> = await api.get(`/consumers/${consumerId}/credentials`);
135 |             let currentConfig: ConsumerCredentialResult = {};
136 | 
137 |             if (currentResponse.code === "SUCCESS" && currentResponse.data) {
138 |                 currentConfig = currentResponse.data;
139 |             }
140 | 
141 |             // 构建删除后的凭证配置,清空对应类型的凭证
142 |             const param: CreateCredentialParam = {
143 |                 ...currentConfig,
144 |             };
145 | 
146 |             if (credentialType === 'API_KEY') {
147 |                 param.apiKeyConfig = {
148 |                     credentials: currentConfig.apiKeyConfig?.credentials?.filter(cred => cred.apiKey !== (credential as APIKeyCredential).apiKey),
149 |                     source: currentConfig.apiKeyConfig?.source || 'Default',
150 |                     key: currentConfig.apiKeyConfig?.key || 'Authorization'
151 |                 };
152 |             } else if (credentialType === 'HMAC') {
153 |                 param.hmacConfig = {
154 |                     credentials: currentConfig.hmacConfig?.credentials?.filter(cred => cred.ak !== (credential as HMACCredential).ak),
155 |                 };
156 |             }
157 | 
158 |             const response: ApiResponse<ConsumerCredentialResult> = await api.put(`/consumers/${consumerId}/credentials`, param);
159 |             if (response?.code === "SUCCESS") {
160 |                 message.success('凭证删除成功');
161 |                 await fetchCurrentConfig();
162 |             }
163 |         } catch (error) {
164 |             console.error('删除凭证失败:', error);
165 |             // message.error('删除凭证失败');
166 |         }
167 |     };
168 |     const handleCopyCredential = (text: string) => {
169 |         const textArea = document.createElement('textarea');
170 |         textArea.value = text;
171 |         textArea.style.position = 'fixed';
172 |         textArea.style.left = '-9999px'; // 避免影响页面布局
173 |         document.body.appendChild(textArea);
174 |         textArea.focus();
175 |         textArea.select();
176 | 
177 |         try {
178 |             const success = document.execCommand('copy');
179 |             if (success) {
180 |                 message.success('已复制到剪贴板');
181 |             } else {
182 |                 // message.error('复制失败,请手动复制内容');
183 |             }
184 |         } catch (err) {
185 |             // message.error('复制失败,请手动复制内容');
186 |         } finally {
187 |             document.body.removeChild(textArea); // 清理 DOM
188 |         }
189 |     };
190 | 
191 | 
192 |     const resetCredentialForm = () => {
193 |         credentialForm.resetFields();
194 |     };
195 | 
196 |     const handleEditSource = async (source: string, key: string) => {
197 |         try {
198 |             // 先获取当前的凭证配置
199 |             const currentResponse: ApiResponse<ConsumerCredentialResult> = await api.get(`/consumers/${consumerId}/credentials`);
200 |             let currentConfig: ConsumerCredentialResult = {};
201 | 
202 |             if (currentResponse.code === "SUCCESS" && currentResponse.data) {
203 |                 currentConfig = currentResponse.data as ConsumerCredentialResult;
204 |             }
205 | 
206 |             // 构建新的凭证配置
207 |             const param: CreateCredentialParam = {};
208 | 
209 |             // 更新API Key配置的source和key
210 |             if (currentConfig.apiKeyConfig) {
211 |                 param.apiKeyConfig = {
212 |                     source: source,
213 |                     key: source === 'Default' ? 'Authorization' : key,
214 |                     credentials: currentConfig.apiKeyConfig.credentials
215 |                 };
216 |             } else {
217 |                 param.apiKeyConfig = {
218 |                     source: source,
219 |                     key: source === 'Default' ? 'Authorization' : key,
220 |                     credentials: []
221 |                 };
222 |             }
223 | 
224 | 
225 |             // 提交配置到后端
226 |             const response: ApiResponse<ConsumerCredentialResult> = await api.put(`/consumers/${consumerId}/credentials`, param);
227 |             if (response?.code === "SUCCESS") {
228 |                 message.success('凭证来源更新成功');
229 | 
230 |                 // 重新查询接口获取最新配置,确保数据落盘
231 |                 const updatedResponse: ApiResponse<ConsumerCredentialResult> = await api.get(`/consumers/${consumerId}/credentials`);
232 |                 if (updatedResponse.code === "SUCCESS" && updatedResponse.data) {
233 |                     const updatedConfig = updatedResponse.data;
234 |                     if (updatedConfig.apiKeyConfig) {
235 |                         setCurrentSource(updatedConfig.apiKeyConfig.source || 'Default');
236 |                         setCurrentKey(updatedConfig.apiKeyConfig.key || 'Authorization');
237 |                     }
238 |                 }
239 | 
240 |                 setSourceModalVisible(false);
241 |                 await fetchCurrentConfig();
242 |             }
243 |         } catch (error) {
244 |             console.error('更新凭证来源失败:', error);
245 |             // message.error('更新凭证来源失败');
246 |         }
247 |     };
248 | 
249 |     const openSourceModal = () => {
250 |         // 打开弹窗前将已保存值拷贝到编辑态和表单
251 |         const initSource = currentSource;
252 |         const initKey = initSource === 'Default' ? 'Authorization' : currentKey;
253 |         setEditingSource(initSource);
254 |         setEditingKey(initKey);
255 |         sourceForm.setFieldsValue({source: initSource, key: initKey});
256 |         setSourceModalVisible(true);
257 |     };
258 | 
259 |     const openCredentialModal = () => {
260 |         // 打开弹窗前重置表单并设置初始值
261 |         credentialForm.resetFields();
262 |         credentialForm.setFieldsValue({
263 |             generationMethod: 'SYSTEM',
264 |             customApiKey: '',
265 |             customAccessKey: '',
266 |             customSecretKey: ''
267 |         });
268 |         setCredentialModalVisible(true);
269 |     };
270 | 
271 |     // 生成随机凭证
272 |     const generateRandomCredential = (type: 'apiKey' | 'accessKey' | 'secretKey'): string => {
273 |         const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-';
274 | 
275 |         if (type === 'apiKey') {
276 |             // 生成32位API Key
277 |             const apiKey = Array.from({length: 32}, () => chars.charAt(Math.floor(Math.random() * chars.length))).join('');
278 | 
279 |             // 确保表单字段已经渲染并设置值
280 |             const setValue = () => {
281 |                 try {
282 |                     credentialForm.setFieldsValue({customApiKey: apiKey});
283 |                 } catch (error) {
284 |                     console.error('设置API Key失败:', error);
285 |                 }
286 |             };
287 | 
288 |             // 如果表单已经渲染,立即设置;否则延迟设置
289 |             if (credentialForm.getFieldValue('customApiKey') !== undefined) {
290 |                 setValue();
291 |             } else {
292 |                 setTimeout(setValue, 100);
293 |             }
294 | 
295 |             return apiKey;
296 |         } else {
297 |             // 生成32位Access Key和64位Secret Key
298 |             const ak = Array.from({length: 32}, () => chars.charAt(Math.floor(Math.random() * chars.length))).join('');
299 |             const sk = Array.from({length: 64}, () => chars.charAt(Math.floor(Math.random() * chars.length))).join('');
300 | 
301 |             // 确保表单字段已经渲染并设置值
302 |             const setValue = () => {
303 |                 try {
304 |                     credentialForm.setFieldsValue({
305 |                         customAccessKey: ak,
306 |                         customSecretKey: sk
307 |                     });
308 |                 } catch (error) {
309 |                     console.error('设置AK/SK失败:', error);
310 |                 }
311 |             };
312 | 
313 |             // 如果表单已经渲染,立即设置;否则延迟设置
314 |             if (credentialForm.getFieldValue('customAccessKey') !== undefined) {
315 |                 setValue();
316 |             } else {
317 |                 setTimeout(setValue, 100);
318 |             }
319 | 
320 |             // 根据类型返回对应的值
321 |             return type === 'accessKey' ? ak : sk;
322 |         }
323 |     };
324 | 
325 |     // API Key 列
326 |     const apiKeyColumns = [
327 |         {
328 |             title: 'API Key',
329 |             dataIndex: 'apiKey',
330 |             key: 'apiKey',
331 |             render: (apiKey: string) => (
332 |                 <div className="flex items-center space-x-2">
333 |                     <code className="text-sm bg-gray-100 px-2 py-1 rounded">{apiKey}</code>
334 |                     <Button type="text" size="small" icon={<CopyOutlined/>}
335 |                             onClick={() => handleCopyCredential(apiKey)}/>
336 |                 </div>
337 |             ),
338 |         },
339 |         {
340 |             title: '操作',
341 |             key: 'action',
342 |             render: (record: ConsumerCredential) => (
343 |                 <Popconfirm title="确定要删除该API Key凭证吗?"
344 |                             onConfirm={() => handleDeleteCredential('API_KEY', record)}>
345 |                     <Button type="link" danger size="small" icon={<DeleteOutlined/>}>删除</Button>
346 |                 </Popconfirm>
347 |             ),
348 |         },
349 |     ];
350 | 
351 |     // 脱敏函数
352 |     const maskSecretKey = (secretKey: string): string => {
353 |         if (!secretKey || secretKey.length < 8) return secretKey;
354 |         return secretKey.substring(0, 4) + '*'.repeat(secretKey.length - 8) + secretKey.substring(secretKey.length - 4);
355 |     };
356 | 
357 |     // HMAC 列
358 |     const hmacColumns = [
359 |         {
360 |             title: 'Access Key',
361 |             dataIndex: 'ak',
362 |             key: 'ak',
363 |             render: (ak: string) => (
364 |                 <div className="flex items-center space-x-2">
365 |                     <code className="text-sm bg-gray-100 px-2 py-1 rounded">{ak}</code>
366 |                     <Button type="text" size="small" icon={<CopyOutlined/>} onClick={() => handleCopyCredential(ak)}/>
367 |                 </div>
368 |             ),
369 |         },
370 |         {
371 |             title: 'Secret Key',
372 |             dataIndex: 'sk',
373 |             key: 'sk',
374 |             render: (sk: string) => (
375 |                 <div className="flex items-center space-x-2">
376 |                     <code className="text-sm bg-gray-100 px-2 py-1 rounded">{maskSecretKey(sk)}</code>
377 |                     <Button type="text" size="small" icon={<CopyOutlined/>} onClick={() => handleCopyCredential(sk)}/>
378 |                 </div>
379 |             ),
380 |         },
381 |         {
382 |             title: '操作',
383 |             key: 'action',
384 |             render: (record: ConsumerCredential) => (
385 |                 <Popconfirm title="确定要删除该AK/SK凭证吗?" onConfirm={() => handleDeleteCredential('HMAC', record)}>
386 |                     <Button type="link" danger size="small" icon={<DeleteOutlined/>}>删除</Button>
387 |                 </Popconfirm>
388 |             ),
389 |         },
390 |     ];
391 | 
392 |     return (
393 |         <>
394 |             <Card title="认证方式">
395 |                 <Tabs defaultActiveKey="API_KEY">
396 |                     <Tabs.TabPane tab="API Key" key="API_KEY">
397 |                         <div className="mb-4">
398 |                             <div className="flex items-start space-x-2 mb-4">
399 |                                 <InfoCircleOutlined className="text-blue-500 mt-1"/>
400 |                                 <div className="text-sm text-gray-600">
401 |                                     API Key是一种简单的认证方式,客户端需要在请求中添加凭证,网关会验证API Key的合法性和权限。
402 |                                     API Key常用于简单场景,不涉及敏感操作,安全性相对较低,请注意凭证的管理与保护。
403 |                                 </div>
404 |                             </div>
405 | 
406 |                             {/* 凭证来源配置(展示已保存值)*/}
407 |                             <div className="mb-4 p-3 bg-gray-50 rounded border">
408 |                                 <div className="flex items-center gap-2 mb-2">
409 |                                     <span className="text-sm font-medium text-gray-700">凭证来源</span>
410 |                                     <Button type="link" size="small" icon={<EditOutlined/>} onClick={openSourceModal}>
411 |                                         编辑
412 |                                     </Button>
413 |                                 </div>
414 |                                 {/* <div className="text-sm text-gray-600">
415 |                   {currentSource === 'Default' ? '' : `${currentSource}`}
416 |                 </div> */}
417 |                                 <div className="text-sm text-gray-600">
418 |                                     {currentSource === 'Default' ? 'Authorization: Bearer <token>' : `${currentSource}:${currentKey}`}
419 |                                 </div>
420 |                             </div>
421 | 
422 |                             <Button
423 |                                 type="primary"
424 |                                 icon={<PlusOutlined/>}
425 |                                 onClick={() => {
426 |                                     setCredentialType('API_KEY');
427 |                                     openCredentialModal();
428 |                                 }}
429 |                             >
430 |                                 添加凭证
431 |                             </Button>
432 |                         </div>
433 |                         <Table
434 |                             columns={apiKeyColumns}
435 |                             dataSource={currentConfig?.apiKeyConfig?.credentials || []}
436 |                             rowKey={(record) => record.apiKey || Math.random().toString()}
437 |                             pagination={false}
438 |                             size="small"
439 |                             locale={{emptyText: '暂无API Key凭证,请点击上方按钮创建'}}
440 |                         />
441 |                     </Tabs.TabPane>
442 | 
443 |                     <Tabs.TabPane tab="HMAC" key="HMAC">
444 |                         <div className="mb-4">
445 |                             <div className="flex items-start space-x-2 mb-4">
446 |                                 <InfoCircleOutlined className="text-blue-500 mt-1"/>
447 |                                 <div className="text-sm text-gray-600">
448 |                                     一种基于HMAC算法的AK/SK签名认证方式。客户端在调用API时,需要使用签名密钥对请求内容进行签名计算,
449 |                                     并将签名同步传输给服务器端进行签名验证。
450 |                                 </div>
451 |                             </div>
452 |                             <Button
453 |                                 type="primary"
454 |                                 icon={<PlusOutlined/>}
455 |                                 onClick={() => {
456 |                                     setCredentialType('HMAC');
457 |                                     openCredentialModal();
458 |                                 }}
459 |                             >
460 |                                 添加AK/SK
461 |                             </Button>
462 |                         </div>
463 |                         <Table
464 |                             columns={hmacColumns}
465 |                             dataSource={currentConfig?.hmacConfig?.credentials || []}
466 |                             rowKey={(record) => record.ak || record.sk || Math.random().toString()}
467 |                             pagination={false}
468 |                             size="small"
469 |                             locale={{emptyText: '暂无AK/SK凭证,请点击上方按钮创建'}}
470 |                         />
471 |                     </Tabs.TabPane>
472 | 
473 |                     <Tabs.TabPane tab="JWT" key="JWT" disabled>
474 |                         <div className="text-center py-8 text-gray-500">
475 |                             JWT功能暂未开放
476 |                         </div>
477 |                     </Tabs.TabPane>
478 |                 </Tabs>
479 |             </Card>
480 | 
481 |             {/* 创建凭证模态框 */}
482 |             <Modal
483 |                 title={`添加 ${credentialType === 'API_KEY' ? 'API Key' : 'AK/SK'}`}
484 |                 open={credentialModalVisible}
485 |                 onCancel={() => {
486 |                     setCredentialModalVisible(false);
487 |                     resetCredentialForm();
488 |                 }}
489 |                 onOk={handleCreateCredential}
490 |                 confirmLoading={credentialLoading}
491 |                 okText="添加"
492 |                 cancelText="取消"
493 |             >
494 |                 <Form form={credentialForm} initialValues={{
495 |                     generationMethod: 'SYSTEM',
496 |                     customApiKey: '',
497 |                     customAccessKey: '',
498 |                     customSecretKey: ''
499 |                 }}>
500 |                     <div className="mb-4">
501 |                         <div className="mb-2">
502 |                             <span className="text-red-500 mr-1">*</span>
503 |                             <span>生成方式</span>
504 |                         </div>
505 |                         <Form.Item
506 |                             name="generationMethod"
507 |                             rules={[{required: true, message: '请选择生成方式'}]}
508 |                             className="mb-0"
509 |                         >
510 |                             <Radio.Group>
511 |                                 <Radio value="SYSTEM">系统生成</Radio>
512 |                                 <Radio value="CUSTOM">自定义</Radio>
513 |                             </Radio.Group>
514 |                         </Form.Item>
515 |                     </div>
516 | 
517 |                     <Form.Item noStyle shouldUpdate={(prev, curr) => prev.generationMethod !== curr.generationMethod}>
518 |                         {({getFieldValue}) => {
519 |                             const method = getFieldValue('generationMethod');
520 |                             if (method === 'CUSTOM') {
521 |                                 return (
522 |                                     <>
523 |                                         {credentialType === 'API_KEY' && (
524 |                                             <div className="mb-4">
525 |                                                 <div className="mb-2">
526 |                                                     <span className="text-red-500 mr-1">*</span>
527 |                                                     <span>凭证</span>
528 |                                                 </div>
529 |                                                 <Form.Item
530 |                                                     name="customApiKey"
531 |                                                     rules={[
532 |                                                         {required: true, message: '请输入自定义API Key'},
533 |                                                         {
534 |                                                             pattern: /^[A-Za-z0-9_-]+$/,
535 |                                                             message: '支持英文、数字、下划线(_)和短横线(-)'
536 |                                                         },
537 |                                                         {min: 8, message: 'API Key长度至少8个字符'},
538 |                                                         {max: 128, message: 'API Key长度不能超过128个字符'}
539 |                                                     ]}
540 |                                                     className="mb-2"
541 |                                                 >
542 |                                                     <Input placeholder="请输入凭证" maxLength={128}/>
543 |                                                 </Form.Item>
544 |                                                 <div className="text-xs text-gray-500">
545 |                                                     长度为8-128个字符,可包含英文、数字、下划线(_)和短横线(-)
546 |                                                 </div>
547 |                                             </div>
548 |                                         )}
549 |                                         {credentialType === 'HMAC' && (
550 |                                             <>
551 |                                                 <div className="mb-4">
552 |                                                     <div className="mb-2">
553 |                                                         <span className="text-red-500 mr-1">*</span>
554 |                                                         <span>Access Key</span>
555 |                                                     </div>
556 |                                                     <Form.Item
557 |                                                         name="customAccessKey"
558 |                                                         rules={[
559 |                                                             {required: true, message: '请输入自定义Access Key'},
560 |                                                             {
561 |                                                                 pattern: /^[A-Za-z0-9_-]+$/,
562 |                                                                 message: '支持英文、数字、下划线(_)和短横线(-)'
563 |                                                             },
564 |                                                             {min: 8, message: 'Access Key长度至少8个字符'},
565 |                                                             {max: 128, message: 'Access Key长度不能超过128个字符'}
566 |                                                         ]}
567 |                                                         className="mb-2"
568 |                                                     >
569 |                                                         <Input placeholder="请输入Access Key" maxLength={128}/>
570 |                                                     </Form.Item>
571 |                                                     <div className="text-xs text-gray-500">
572 |                                                         长度为8-128个字符,可包含英文、数字、下划线(_)和短横线(-)
573 |                                                     </div>
574 |                                                 </div>
575 |                                                 <div className="mb-4">
576 |                                                     <div className="mb-2">
577 |                                                         <span className="text-red-500 mr-1">*</span>
578 |                                                         <span>Secret Key</span>
579 |                                                     </div>
580 |                                                     <Form.Item
581 |                                                         name="customSecretKey"
582 |                                                         rules={[
583 |                                                             {required: true, message: '请输入自定义Secret Key'},
584 |                                                             {
585 |                                                                 pattern: /^[A-Za-z0-9_-]+$/,
586 |                                                                 message: '支持英文、数字、下划线(_)和短横线(-)'
587 |                                                             },
588 |                                                             {min: 8, message: 'Secret Key长度至少8个字符'},
589 |                                                             {max: 128, message: 'Secret Key长度不能超过128个字符'}
590 |                                                         ]}
591 |                                                         className="mb-2"
592 |                                                     >
593 |                                                         <Input placeholder="请输入 Secret Key" maxLength={128}/>
594 |                                                     </Form.Item>
595 |                                                     <div className="text-xs text-gray-500">
596 |                                                         长度为8-128个字符,可包含英文、数字、下划线(_)和短横线(-)
597 |                                                     </div>
598 |                                                 </div>
599 |                                             </>
600 |                                         )}
601 |                                     </>
602 |                                 );
603 |                             } else if (method === 'SYSTEM') {
604 |                                 return (
605 |                                     <div>
606 |                                         <div className="flex items-center gap-2 text-sm text-gray-500">
607 |                                             <InfoCircleOutlined/>
608 |                                             <span>系统将自动生成符合规范的凭证</span>
609 |                                         </div>
610 |                                     </div>
611 |                                 );
612 |                             }
613 |                             return null;
614 |                         }}
615 |                     </Form.Item>
616 |                 </Form>
617 |             </Modal>
618 | 
619 |             {/* 编辑凭证来源模态框 */}
620 |             <Modal
621 |                 title="编辑凭证来源"
622 |                 open={sourceModalVisible}
623 |                 onCancel={() => {
624 |                     // 取消不落盘,回退到已保存值并重置表单
625 |                     const initSource = currentSource;
626 |                     const initKey = initSource === 'Default' ? 'Authorization' : currentKey;
627 |                     setEditingSource(initSource);
628 |                     setEditingKey(initKey);
629 |                     sourceForm.resetFields();
630 |                     setSourceModalVisible(false);
631 |                 }}
632 |                 onOk={async () => {
633 |                     try {
634 |                         const values = await sourceForm.validateFields();
635 |                         setEditingSource(values.source);
636 |                         setEditingKey(values.key);
637 |                         await handleEditSource(values.source, values.key);
638 |                     } catch {
639 |                         // 校验失败,不提交
640 |                     }
641 |                 }}
642 |                 okText="保存"
643 |                 cancelText="取消"
644 |             >
645 |                 <Form form={sourceForm} layout="vertical" initialValues={{source: editingSource, key: editingKey}}>
646 |                     <Form.Item
647 |                         label="凭证来源"
648 |                         name="source"
649 |                         rules={[{required: true, message: '请选择凭证来源'}]}
650 |                     >
651 |                         <Select
652 |                             onChange={(value) => {
653 |                                 const nextKey = value === 'Default' ? 'Authorization' : '';
654 |                                 sourceForm.setFieldsValue({key: nextKey});
655 |                             }}
656 |                             style={{width: '100%'}}
657 |                         >
658 |                             <Select.Option value="Header">Header</Select.Option>
659 |                             <Select.Option value="QueryString">QueryString</Select.Option>
660 |                             <Select.Option value="Default">默认</Select.Option>
661 |                         </Select>
662 |                     </Form.Item>
663 | 
664 |                     <Form.Item noStyle shouldUpdate={(prev, curr) => prev.source !== curr.source}>
665 |                         {({getFieldValue}) =>
666 |                             getFieldValue('source') !== 'Default' ? (
667 |                                 <Form.Item
668 |                                     label="键名"
669 |                                     name="key"
670 |                                     rules={[
671 |                                         {
672 |                                             required: true,
673 |                                             message: '请输入键名',
674 |                                         },
675 |                                         {
676 |                                             pattern: /^[A-Za-z0-9-_]+$/,
677 |                                             message: '仅支持字母/数字/-/_',
678 |                                         },
679 |                                     ]}
680 |                                 >
681 |                                     <Input placeholder="请输入键名"/>
682 |                                 </Form.Item>
683 |                             ) : null
684 |                         }
685 |                     </Form.Item>
686 |                     {/*
687 |           <div className="text-sm text-gray-500">
688 |             <div>说明:</div>
689 |             <div>• Header: 凭证放在HTTP请求头中</div>
690 |             <div>• QueryString: 凭证放在URL查询参数中</div>
691 |             <div>• Default: 使用标准的Authorization头</div>
692 |           </div> */}
693 |                 </Form>
694 |             </Modal>
695 |         </>
696 |     );
697 | } 
```

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

```typescript
  1 | import { Card, Button, Modal, Form, Select, message, Collapse, Tabs, Row, Col } from 'antd'
  2 | import { PlusOutlined, DeleteOutlined, ExclamationCircleOutlined, CopyOutlined } from '@ant-design/icons'
  3 | import { useState, useEffect } from 'react'
  4 | import type { ApiProduct, LinkedService, RestAPIItem, HigressMCPItem, NacosMCPItem, APIGAIMCPItem, ApiItem } from '@/types/api-product'
  5 | import type { Gateway, NacosInstance } from '@/types/gateway'
  6 | import { apiProductApi, gatewayApi, nacosApi } from '@/lib/api'
  7 | import { getGatewayTypeLabel } from '@/lib/constant'
  8 | import { copyToClipboard } from '@/lib/utils'
  9 | import * as yaml from 'js-yaml'
 10 | import { SwaggerUIWrapper } from './SwaggerUIWrapper'
 11 | 
 12 | interface ApiProductLinkApiProps {
 13 |   apiProduct: ApiProduct
 14 |   linkedService: LinkedService | null
 15 |   onLinkedServiceUpdate: (linkedService: LinkedService | null) => void
 16 |   handleRefresh: () => void
 17 | }
 18 | 
 19 | export function ApiProductLinkApi({ apiProduct, linkedService, onLinkedServiceUpdate, handleRefresh }: ApiProductLinkApiProps) {
 20 |   // 移除了内部的 linkedService 状态,现在从 props 接收
 21 |   const [isModalVisible, setIsModalVisible] = useState(false)
 22 |   const [form] = Form.useForm()
 23 |   const [gateways, setGateways] = useState<Gateway[]>([])
 24 |   const [nacosInstances, setNacosInstances] = useState<NacosInstance[]>([])
 25 |   const [gatewayLoading, setGatewayLoading] = useState(false)
 26 |   const [nacosLoading, setNacosLoading] = useState(false)
 27 |   const [selectedGateway, setSelectedGateway] = useState<Gateway | null>(null)
 28 |   const [selectedNacos, setSelectedNacos] = useState<NacosInstance | null>(null)
 29 |   const [nacosNamespaces, setNacosNamespaces] = useState<any[]>([])
 30 |   const [selectedNamespace, setSelectedNamespace] = useState<string | null>(null)
 31 |   const [apiList, setApiList] = useState<ApiItem[] | NacosMCPItem[]>([])
 32 |   const [apiLoading, setApiLoading] = useState(false)
 33 |   const [sourceType, setSourceType] = useState<'GATEWAY' | 'NACOS'>('GATEWAY')
 34 |   const [parsedTools, setParsedTools] = useState<Array<{
 35 |     name: string;
 36 |     description: string;
 37 |     args?: Array<{
 38 |       name: string;
 39 |       description: string;
 40 |       type: string;
 41 |       required: boolean;
 42 |       position: string;
 43 |       default?: string;
 44 |       enum?: string[];
 45 |     }>;
 46 |   }>>([])
 47 |   const [httpJson, setHttpJson] = useState('')
 48 |   const [sseJson, setSseJson] = useState('')
 49 |   const [localJson, setLocalJson] = useState('')
 50 | 
 51 |   useEffect(() => {    
 52 |     fetchGateways()
 53 |     fetchNacosInstances()
 54 |   }, [])
 55 | 
 56 |   // 解析MCP tools配置
 57 |   useEffect(() => {
 58 |     if (apiProduct.type === 'MCP_SERVER' && apiProduct.mcpConfig?.tools) {
 59 |       const parsedConfig = parseYamlConfig(apiProduct.mcpConfig.tools)
 60 |       if (parsedConfig && parsedConfig.tools && Array.isArray(parsedConfig.tools)) {
 61 |         setParsedTools(parsedConfig.tools)
 62 |       } else {
 63 |         // 如果tools字段存在但是空数组,也设置为空数组
 64 |         setParsedTools([])
 65 |       }
 66 |     } else {
 67 |       setParsedTools([])
 68 |     }
 69 |   }, [apiProduct])
 70 | 
 71 |   // 生成连接配置
 72 |   useEffect(() => {
 73 |     if (apiProduct.type === 'MCP_SERVER' && apiProduct.mcpConfig) {
 74 |       generateConnectionConfig(
 75 |         apiProduct.mcpConfig.mcpServerConfig.domains,
 76 |         apiProduct.mcpConfig.mcpServerConfig.path,
 77 |         apiProduct.mcpConfig.mcpServerName,
 78 |         apiProduct.mcpConfig.mcpServerConfig.rawConfig,
 79 |         apiProduct.mcpConfig.meta?.protocol
 80 |       )
 81 |     }
 82 |   }, [apiProduct])
 83 | 
 84 |   // 解析YAML配置的函数
 85 |   const parseYamlConfig = (yamlString: string): {
 86 |     tools?: Array<{
 87 |       name: string;
 88 |       description: string;
 89 |       args?: Array<{
 90 |         name: string;
 91 |         description: string;
 92 |         type: string;
 93 |         required: boolean;
 94 |         position: string;
 95 |         default?: string;
 96 |         enum?: string[];
 97 |       }>;
 98 |     }>;
 99 |   } | null => {
100 |     try {
101 |       const parsed = yaml.load(yamlString) as {
102 |         tools?: Array<{
103 |           name: string;
104 |           description: string;
105 |           args?: Array<{
106 |             name: string;
107 |             description: string;
108 |             type: string;
109 |             required: boolean;
110 |             position: string;
111 |             default?: string;
112 |             enum?: string[];
113 |           }>;
114 |         }>;
115 |       };
116 |       return parsed;
117 |     } catch (error) {
118 |       console.error('YAML解析失败:', error)
119 |       return null
120 |     }
121 |   }
122 | 
123 |   // 生成连接配置
124 |   const generateConnectionConfig = (
125 |     domains: Array<{ domain: string; protocol: string }> | null | undefined,
126 |     path: string | null | undefined,
127 |     serverName: string,
128 |     localConfig?: unknown,
129 |     protocolType?: string
130 |   ) => {
131 |     // 互斥:优先判断本地模式
132 |     if (localConfig) {
133 |       const localConfigJson = JSON.stringify(localConfig, null, 2);
134 |       setLocalJson(localConfigJson);
135 |       setHttpJson("");
136 |       setSseJson("");
137 |       return;
138 |     }
139 | 
140 |     // HTTP/SSE 模式
141 |     if (domains && domains.length > 0 && path) {
142 |       const domain = domains[0]
143 |       const fullUrl = `${domain.protocol}://${domain.domain}${path || '/'}`
144 | 
145 |       if (protocolType === 'SSE') {
146 |         // 仅生成SSE配置,不追加/sse
147 |         const sseConfig = {
148 |           mcpServers: {
149 |             [serverName]: {
150 |               type: "sse",
151 |               url: fullUrl
152 |             }
153 |           }
154 |         }
155 |         setSseJson(JSON.stringify(sseConfig, null, 2))
156 |         setHttpJson("")
157 |         setLocalJson("")
158 |         return;
159 |       } else if (protocolType === 'StreamableHTTP') {
160 |         // 仅生成HTTP配置
161 |         const httpConfig = {
162 |           mcpServers: {
163 |             [serverName]: {
164 |               url: fullUrl
165 |             }
166 |           }
167 |         }
168 |         setHttpJson(JSON.stringify(httpConfig, null, 2))
169 |         setSseJson("")
170 |         setLocalJson("")
171 |         return;
172 |       } else {
173 |         // protocol为null或其他值:生成两种配置
174 |         const sseConfig = {
175 |           mcpServers: {
176 |             [serverName]: {
177 |               type: "sse",
178 |               url: `${fullUrl}/sse`
179 |             }
180 |           }
181 |         }
182 | 
183 |         const httpConfig = {
184 |           mcpServers: {
185 |             [serverName]: {
186 |               url: fullUrl
187 |             }
188 |           }
189 |         }
190 | 
191 |         setSseJson(JSON.stringify(sseConfig, null, 2))
192 |         setHttpJson(JSON.stringify(httpConfig, null, 2))
193 |         setLocalJson("")
194 |         return;
195 |       }
196 |     }
197 | 
198 |     // 无有效配置
199 |     setHttpJson("");
200 |     setSseJson("");
201 |     setLocalJson("");
202 |   }
203 | 
204 |   const handleCopy = async (text: string) => {
205 |     try {
206 |       await copyToClipboard(text);
207 |       message.success("已复制到剪贴板");
208 |     } catch {
209 |       message.error("复制失败,请手动复制");
210 |     }
211 |   }
212 | 
213 |   const fetchGateways = async () => {
214 |     setGatewayLoading(true)
215 |     try {
216 |       const res = await gatewayApi.getGateways()
217 |       const result = apiProduct.type === 'REST_API' ?
218 |        res.data?.content?.filter?.((item: Gateway) => item.gatewayType === 'APIG_API') :
219 |        res.data?.content?.filter?.((item: Gateway) => item.gatewayType === 'HIGRESS' || item.gatewayType === 'APIG_AI' || item.gatewayType === 'ADP_AI_GATEWAY')
220 |       setGateways(result || [])
221 |     } catch (error) {
222 |       console.error('获取网关列表失败:', error)
223 |     } finally {
224 |       setGatewayLoading(false)
225 |     }
226 |   }
227 | 
228 |   const fetchNacosInstances = async () => {
229 |     setNacosLoading(true)
230 |     try {
231 |       const res = await nacosApi.getNacos({
232 |         page: 1,
233 |         size: 1000 // 获取所有 Nacos 实例
234 |       })
235 |       setNacosInstances(res.data.content || [])
236 |     } catch (error) {
237 |       console.error('获取Nacos实例列表失败:', error)
238 |     } finally {
239 |       setNacosLoading(false)
240 |     }
241 |   }
242 | 
243 |   const handleSourceTypeChange = (value: 'GATEWAY' | 'NACOS') => {
244 |     setSourceType(value)
245 |   setSelectedGateway(null)
246 |   setSelectedNacos(null)
247 |   setSelectedNamespace(null)
248 |   setNacosNamespaces([])
249 |     setApiList([])
250 |     form.setFieldsValue({
251 |       gatewayId: undefined,
252 |       nacosId: undefined,
253 |       apiId: undefined
254 |     })
255 |   }
256 | 
257 |   const handleGatewayChange = async (gatewayId: string) => {
258 |     const gateway = gateways.find(g => g.gatewayId === gatewayId)
259 |     setSelectedGateway(gateway || null)
260 |     
261 |     if (!gateway) return
262 | 
263 |     setApiLoading(true)
264 |     try {
265 |       if (gateway.gatewayType === 'APIG_API') {
266 |         // APIG_API类型:获取REST API列表
267 |         const restRes = await gatewayApi.getGatewayRestApis(gatewayId, {})
268 |         const restApis = (restRes.data?.content || []).map((api: any) => ({
269 |           apiId: api.apiId,
270 |           apiName: api.apiName,
271 |           type: 'REST API'
272 |         }))
273 |         setApiList(restApis)
274 |       } else if (gateway.gatewayType === 'HIGRESS') {
275 |         // HIGRESS类型:获取MCP Server列表
276 |         const res = await gatewayApi.getGatewayMcpServers(gatewayId, {
277 |           page: 1,
278 |           size: 1000 // 获取所有MCP Server
279 |         })
280 |         const mcpServers = (res.data?.content || []).map((api: any) => ({
281 |           mcpServerName: api.mcpServerName,
282 |           fromGatewayType: 'HIGRESS' as const,
283 |           type: 'MCP Server'
284 |         }))
285 |         setApiList(mcpServers)
286 |       } else if (gateway.gatewayType === 'APIG_AI') {
287 |         // APIG_AI类型:获取MCP Server列表
288 |         const res = await gatewayApi.getGatewayMcpServers(gatewayId, {
289 |           page: 1,
290 |           size: 500 // 获取所有MCP Server
291 |         })
292 |         const mcpServers = (res.data?.content || []).map((api: any) => ({
293 |           mcpServerName: api.mcpServerName,
294 |           fromGatewayType: 'APIG_AI' as const,
295 |           mcpRouteId: api.mcpRouteId,
296 |           apiId: api.apiId,
297 |           mcpServerId: api.mcpServerId,
298 |           type: 'MCP Server'
299 |         }))
300 |         setApiList(mcpServers)
301 |       } else if (gateway.gatewayType === 'ADP_AI_GATEWAY') {
302 |         // ADP_AI_GATEWAY类型:获取MCP Server列表
303 |         const res = await gatewayApi.getGatewayMcpServers(gatewayId, {
304 |           page: 1,
305 |           size: 500 // 获取所有MCP Server
306 |         })
307 |         const mcpServers = (res.data?.content || []).map((api: any) => ({
308 |           mcpServerName: api.mcpServerName || api.name,
309 |           fromGatewayType: 'ADP_AI_GATEWAY' as const,
310 |           mcpRouteId: api.mcpRouteId,
311 |           mcpServerId: api.mcpServerId,
312 |           type: 'MCP Server'
313 |         }))
314 |         setApiList(mcpServers)
315 |       }
316 |     } catch (error) {
317 |     } finally {
318 |       setApiLoading(false)
319 |     }
320 |   }
321 | 
322 |   const handleNacosChange = async (nacosId: string) => {
323 |     const nacos = nacosInstances.find(n => n.nacosId === nacosId)
324 |     setSelectedNacos(nacos || null)
325 |     setSelectedNamespace(null)
326 |     setApiList([])
327 |     setNacosNamespaces([])
328 |     if (!nacos) return
329 | 
330 |     // 获取命名空间列表
331 |     try {
332 |       const nsRes = await nacosApi.getNamespaces(nacosId, { page: 1, size: 1000 })
333 |       const namespaces = (nsRes.data?.content || []).map((ns: any) => ({
334 |         namespaceId: ns.namespaceId,
335 |         namespaceName: ns.namespaceName || ns.namespaceId,
336 |         namespaceDesc: ns.namespaceDesc
337 |       }))
338 |       setNacosNamespaces(namespaces)
339 |     } catch (e) {
340 |       console.error('获取命名空间失败', e)
341 |     }
342 |   }
343 | 
344 |   const handleNamespaceChange = async (namespaceId: string) => {
345 |     setSelectedNamespace(namespaceId)
346 |     setApiLoading(true)
347 |     try {
348 |       if (!selectedNacos) return
349 |       const res = await nacosApi.getNacosMcpServers(selectedNacos.nacosId, {
350 |         page: 1,
351 |         size: 1000,
352 |         namespaceId
353 |       })
354 |       const mcpServers = (res.data?.content || []).map((api: any) => ({
355 |         mcpServerName: api.mcpServerName,
356 |         fromGatewayType: 'NACOS' as const,
357 |         type: `MCP Server (${namespaceId})`
358 |       }))
359 |       setApiList(mcpServers)
360 |     } catch (e) {
361 |       console.error('获取Nacos MCP Server列表失败:', e)
362 |     } finally {
363 |       setApiLoading(false)
364 |     }
365 |   }
366 | 
367 | 
368 |   const handleModalOk = () => {
369 |     form.validateFields().then((values) => {
370 |       const { sourceType, gatewayId, nacosId, apiId } = values
371 |       const selectedApi = apiList.find(item => {
372 |         if ('apiId' in item) {
373 |           // mcp server 会返回apiId和mcpRouteId,此时mcpRouteId为唯一值,apiId不是
374 |           if ('mcpRouteId' in item) {
375 |             return item.mcpRouteId === apiId
376 |           } else {
377 |             return item.apiId === apiId
378 |           }
379 |         } else if ('mcpServerName' in item) {
380 |           return item.mcpServerName === apiId
381 |         }
382 |         return false
383 |       })
384 |       const newService: LinkedService = {
385 |         gatewayId: sourceType === 'GATEWAY' ? gatewayId : undefined, // 对于 Nacos,使用 nacosId 作为 gatewayId
386 |         nacosId: sourceType === 'NACOS' ? nacosId : undefined,
387 |         sourceType,
388 |         productId: apiProduct.productId,
389 |         apigRefConfig: selectedApi && 'apiId' in selectedApi ? selectedApi as RestAPIItem | APIGAIMCPItem : undefined,
390 |         higressRefConfig: selectedApi && 'mcpServerName' in selectedApi && 'fromGatewayType' in selectedApi && selectedApi.fromGatewayType === 'HIGRESS' ? selectedApi as HigressMCPItem : undefined,
391 |         nacosRefConfig: sourceType === 'NACOS' && selectedApi && 'fromGatewayType' in selectedApi && selectedApi.fromGatewayType === 'NACOS' ? {
392 |           ...selectedApi,
393 |           namespaceId: selectedNamespace || 'public'
394 |         } : undefined,
395 |         adpAIGatewayRefConfig: selectedApi && 'fromGatewayType' in selectedApi && selectedApi.fromGatewayType === 'ADP_AI_GATEWAY' ? selectedApi as APIGAIMCPItem : undefined,
396 |       }
397 |       apiProductApi.createApiProductRef(apiProduct.productId, newService).then(async () => {
398 |         message.success('关联成功')
399 |         setIsModalVisible(false)
400 |         
401 |         // 重新获取关联信息并更新
402 |         try {
403 |           const res = await apiProductApi.getApiProductRef(apiProduct.productId)
404 |           onLinkedServiceUpdate(res.data || null)
405 |         } catch (error) {
406 |           console.error('获取关联API失败:', error)
407 |           onLinkedServiceUpdate(null)
408 |         }
409 |         
410 |         // 重新获取产品详情(特别重要,因为关联API后apiProduct.apiConfig可能会更新)
411 |         handleRefresh()
412 |         
413 |         form.resetFields()
414 |         setSelectedGateway(null)
415 |         setSelectedNacos(null)
416 |         setApiList([])
417 |         setSourceType('GATEWAY')
418 |       }).catch(() => {
419 |         message.error('关联失败')
420 |       })
421 |     })
422 |   }
423 | 
424 |   const handleModalCancel = () => {
425 |     setIsModalVisible(false)
426 |     form.resetFields()
427 |     setSelectedGateway(null)
428 |     setSelectedNacos(null)
429 |     setApiList([])
430 |     setSourceType('GATEWAY')
431 |   }
432 | 
433 | 
434 |   const handleDelete = () => {
435 |     if (!linkedService) return
436 | 
437 |     Modal.confirm({
438 |       title: '确认解除关联',
439 |       content: '确定要解除与当前API的关联吗?',
440 |       icon: <ExclamationCircleOutlined />,
441 |       onOk() {
442 |         return apiProductApi.deleteApiProductRef(apiProduct.productId).then(() => {
443 |           message.success('解除关联成功')
444 |           onLinkedServiceUpdate(null)
445 |           // 重新获取产品详情(解除关联后apiProduct.apiConfig可能会更新)
446 |           handleRefresh()
447 |         }).catch(() => {
448 |           message.error('解除关联失败')
449 |         })
450 |       }
451 |     })
452 |   }
453 | 
454 |   const getServiceInfo = () => {
455 |     if (!linkedService) return null
456 | 
457 |     let apiName = ''
458 |     let apiType = ''
459 |     let sourceInfo = ''
460 |     let gatewayInfo = ''
461 | 
462 |     // 首先根据 Product 的 type 确定基本类型
463 |     if (apiProduct.type === 'REST_API') {
464 |       // REST API 类型产品 - 只能关联 API 网关上的 REST API
465 |       if (linkedService.sourceType === 'GATEWAY' && linkedService.apigRefConfig && 'apiName' in linkedService.apigRefConfig) {
466 |         apiName = linkedService.apigRefConfig.apiName || '未命名'
467 |         apiType = 'REST API'
468 |         sourceInfo = 'API网关'
469 |         gatewayInfo = linkedService.gatewayId || '未知'
470 |       }
471 |     } else if (apiProduct.type === 'MCP_SERVER') {
472 |       // MCP Server 类型产品 - 可以关联多种平台上的 MCP Server
473 |       apiType = 'MCP Server'
474 |       
475 |       if (linkedService.sourceType === 'GATEWAY' && linkedService.apigRefConfig && 'mcpServerName' in linkedService.apigRefConfig) {
476 |         // AI网关上的MCP Server
477 |         apiName = linkedService.apigRefConfig.mcpServerName || '未命名'
478 |         sourceInfo = 'AI网关'
479 |         gatewayInfo = linkedService.gatewayId || '未知'
480 |       } else if (linkedService.sourceType === 'GATEWAY' && linkedService.higressRefConfig) {
481 |         // Higress网关上的MCP Server
482 |         apiName = linkedService.higressRefConfig.mcpServerName || '未命名'
483 |         sourceInfo = 'Higress网关'
484 |         gatewayInfo = linkedService.gatewayId || '未知'
485 |       } else if (linkedService.sourceType === 'GATEWAY' && linkedService.adpAIGatewayRefConfig) {
486 |         // 专有云AI网关上的MCP Server
487 |         apiName = linkedService.adpAIGatewayRefConfig.mcpServerName || '未命名'
488 |         sourceInfo = '专有云AI网关'
489 |         gatewayInfo = linkedService.gatewayId || '未知'
490 |       } else if (linkedService.sourceType === 'NACOS' && linkedService.nacosRefConfig) {
491 |         // Nacos上的MCP Server
492 |         apiName = linkedService.nacosRefConfig.mcpServerName || '未命名'
493 |         sourceInfo = 'Nacos服务发现'
494 |         gatewayInfo = linkedService.nacosId || '未知'
495 |       }
496 |     }
497 | 
498 |     return {
499 |       apiName,
500 |       apiType,
501 |       sourceInfo,
502 |       gatewayInfo
503 |     }
504 |   }
505 | 
506 |   const renderLinkInfo = () => {
507 |     const serviceInfo = getServiceInfo()
508 |     
509 |     // 没有关联任何API
510 |     if (!linkedService || !serviceInfo) {
511 |       return (
512 |         <Card className="mb-6">
513 |           <div className="text-center py-8">
514 |             <div className="text-gray-500 mb-4">暂未关联任何API</div>
515 |             <Button type="primary" icon={<PlusOutlined />} onClick={() => setIsModalVisible(true)}>
516 |               关联API
517 |             </Button>
518 |           </div>
519 |         </Card>
520 |       )
521 |     }
522 | 
523 |     return (
524 |       <Card 
525 |         className="mb-6"
526 |         title="关联详情"
527 |         extra={
528 |           <Button type="primary" danger icon={<DeleteOutlined />} onClick={handleDelete}>
529 |             解除关联
530 |           </Button>
531 |         }
532 |       >
533 |         <div>
534 |           {/* 第一行:名称 + 类型 */}
535 |           <div className="grid grid-cols-6 gap-8 items-center pt-2 pb-2">
536 |             <span className="text-xs text-gray-600">名称:</span>
537 |             <span className="col-span-2 text-xs text-gray-900">{serviceInfo.apiName || '未命名'}</span>
538 |             <span className="text-xs text-gray-600">类型:</span>
539 |             <span className="col-span-2 text-xs text-gray-900">{serviceInfo.apiType}</span>
540 |           </div>
541 |           
542 |           {/* 第二行:来源 + ID */}
543 |           <div className="grid grid-cols-6 gap-8 items-center pt-2 pb-2">
544 |             <span className="text-xs text-gray-600">来源:</span>
545 |             <span className="col-span-2 text-xs text-gray-900">{serviceInfo.sourceInfo}</span>
546 |             <span className="text-xs text-gray-600">
547 |               {linkedService?.sourceType === 'NACOS' ? 'Nacos ID:' : '网关ID:'}
548 |             </span>
549 |             <span className="col-span-2 text-xs text-gray-700">{serviceInfo.gatewayInfo}</span>
550 |           </div>
551 |         </div>
552 |       </Card>
553 |     )
554 |   }
555 | 
556 |   const renderApiConfig = () => {
557 |     const isMcp = apiProduct.type === 'MCP_SERVER'
558 |     const isOpenApi = apiProduct.type === 'REST_API'
559 | 
560 |     // MCP Server类型:无论是否有linkedService都显示tools和连接点配置  
561 |     if (isMcp && apiProduct.mcpConfig) {
562 |       return (
563 |         <Card title="配置详情">
564 |           <Row gutter={24}>
565 |             {/* 左侧:工具列表 */}
566 |             <Col span={15}>
567 |               <Card>
568 |                 <Tabs
569 |                   defaultActiveKey="tools"
570 |                   items={[
571 |                     {
572 |                       key: "tools",
573 |                       label: `Tools (${parsedTools.length})`,
574 |                       children: parsedTools.length > 0 ? (
575 |                         <div className="border border-gray-200 rounded-lg bg-gray-50">
576 |                           {parsedTools.map((tool, idx) => (
577 |                             <div key={idx} className={idx < parsedTools.length - 1 ? "border-b border-gray-200" : ""}>
578 |                               <Collapse
579 |                                 ghost
580 |                                 expandIconPosition="end"
581 |                                 items={[{
582 |                                   key: idx.toString(),
583 |                                   label: tool.name,
584 |                                   children: (
585 |                                     <div className="px-4 pb-2">
586 |                                       <div className="text-gray-600 mb-4">{tool.description}</div>
587 |                                       
588 |                                       {tool.args && tool.args.length > 0 && (
589 |                                         <div>
590 |                                           <p className="font-medium text-gray-700 mb-3">输入参数:</p>
591 |                                           {tool.args.map((arg, argIdx) => (
592 |                                             <div key={argIdx} className="mb-3">
593 |                                               <div className="flex items-center mb-2">
594 |                                                 <span className="font-medium text-gray-800 mr-2">{arg.name}</span>
595 |                                                 <span className="text-xs bg-gray-200 text-gray-600 px-2 py-1 rounded mr-2">
596 |                                                   {arg.type}
597 |                                                 </span>
598 |                                                 {arg.required && (
599 |                                                   <span className="text-red-500 text-xs mr-2">*</span>
600 |                                                 )}
601 |                                                 {arg.description && (
602 |                                                   <span className="text-xs text-gray-500">
603 |                                                     {arg.description}
604 |                                                   </span>
605 |                                                 )}
606 |                                               </div>
607 |                                               <input
608 |                                                 type="text"
609 |                                                 placeholder={arg.description || `请输入${arg.name}`}
610 |                                                 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"
611 |                                                 defaultValue={arg.default !== undefined ? JSON.stringify(arg.default) : ''}
612 |                                               />
613 |                                               {arg.enum && (
614 |                                                 <div className="text-xs text-gray-500">
615 |                                                   可选值: {arg.enum.map(value => <code key={value} className="mr-1">{value}</code>)}
616 |                                                 </div>
617 |                                               )}
618 |                                             </div>
619 |                                           ))}
620 |                                         </div>
621 |                                       )}
622 |                                     </div>
623 |                                   )
624 |                                 }]}
625 |                               />
626 |                             </div>
627 |                           ))}
628 |                         </div>
629 |                       ) : (
630 |                         <div className="text-gray-500 text-center py-8">
631 |                           No tools available
632 |                         </div>
633 |                       ),
634 |                     },
635 |                   ]}
636 |                 />
637 |               </Card>
638 |             </Col>
639 | 
640 |             {/* 右侧:连接点配置 */}
641 |             <Col span={9}>
642 |               <Card>
643 |                 <div className="mb-4">
644 |                   <h3 className="text-sm font-semibold mb-3">连接点配置</h3>
645 |                   <Tabs
646 |                     size="small" 
647 |                     defaultActiveKey={localJson ? "local" : (sseJson ? "sse" : "http")}
648 |                     items={(() => {
649 |                       const tabs = [];
650 |                       
651 |                       if (localJson) {
652 |                         tabs.push({
653 |                           key: "local",
654 |                           label: "Stdio",
655 |                           children: (
656 |                             <div className="relative bg-gray-50 border border-gray-200 rounded-md p-3">
657 |                               <Button
658 |                                 size="small"
659 |                                 icon={<CopyOutlined />}
660 |                                 className="absolute top-2 right-2 z-10"
661 |                                 onClick={() => handleCopy(localJson)}
662 |                               >
663 |                               </Button>
664 |                               <div className="text-gray-800 font-mono text-xs overflow-x-auto">
665 |                                 <pre className="whitespace-pre-wrap">{localJson}</pre>
666 |                               </div>
667 |                             </div>
668 |                           ),
669 |                         });
670 |                       } else {
671 |                         if (sseJson) {
672 |                           tabs.push({
673 |                             key: "sse",
674 |                             label: "SSE",
675 |                             children: (
676 |                               <div className="relative bg-gray-50 border border-gray-200 rounded-md p-3">
677 |                                 <Button
678 |                                   size="small"
679 |                                   icon={<CopyOutlined />}
680 |                                   className="absolute top-2 right-2 z-10"
681 |                                   onClick={() => handleCopy(sseJson)}
682 |                                 >
683 |                                 </Button>
684 |                                 <div className="text-gray-800 font-mono text-xs overflow-x-auto">
685 |                                   <pre className="whitespace-pre-wrap">{sseJson}</pre>
686 |                                 </div>
687 |                               </div>
688 |                             ),
689 |                           });
690 |                         }
691 |                         
692 |                         if (httpJson) {
693 |                           tabs.push({
694 |                             key: "http",
695 |                             label: "Streaming HTTP",
696 |                             children: (
697 |                               <div className="relative bg-gray-50 border border-gray-200 rounded-md p-3">
698 |                                 <Button
699 |                                   size="small"
700 |                                   icon={<CopyOutlined />}
701 |                                   className="absolute top-2 right-2 z-10"
702 |                                   onClick={() => handleCopy(httpJson)}
703 |                                 >
704 |                                 </Button>
705 |                                 <div className="text-gray-800 font-mono text-xs overflow-x-auto">
706 |                                   <pre className="whitespace-pre-wrap">{httpJson}</pre>
707 |                                 </div>
708 |                               </div>
709 |                             ),
710 |                           });
711 |                         }
712 |                       }
713 |                       
714 |                       return tabs;
715 |                     })()}
716 |                   />
717 |                 </div>
718 |               </Card>
719 |             </Col>
720 |           </Row>
721 |         </Card>
722 |       )
723 |     }
724 | 
725 |     // REST API类型:需要linkedService才显示
726 |     if (!linkedService) {
727 |       return null
728 |     }
729 | 
730 |     return (
731 |       <Card title="配置详情">
732 | 
733 |         {isOpenApi && apiProduct.apiConfig && apiProduct.apiConfig.spec && (
734 |           <div>
735 |             <h4 className="text-base font-medium mb-4">REST API接口文档</h4>
736 |             <SwaggerUIWrapper apiSpec={apiProduct.apiConfig.spec} />
737 |           </div>
738 |         )}
739 |       </Card>
740 |     )
741 |   }
742 | 
743 |   return (
744 |     <div className="p-6 space-y-6">
745 |       <div className="mb-6">
746 |         <h1 className="text-2xl font-bold mb-2">API关联</h1>
747 |         <p className="text-gray-600">管理Product关联的API</p>
748 |       </div>
749 | 
750 |       {renderLinkInfo()}
751 |       {renderApiConfig()}
752 | 
753 |       <Modal
754 |         title={linkedService ? '重新关联API' : '关联新API'}
755 |         open={isModalVisible}
756 |         onOk={handleModalOk}
757 |         onCancel={handleModalCancel}
758 |         okText="关联"
759 |         cancelText="取消"
760 |         width={600}
761 |       >
762 |         <Form form={form} layout="vertical">
763 |           <Form.Item
764 |             name="sourceType"
765 |             label="来源类型"
766 |             initialValue="GATEWAY"
767 |             rules={[{ required: true, message: '请选择来源类型' }]}
768 |           >
769 |             <Select placeholder="请选择来源类型" onChange={handleSourceTypeChange}>
770 |               <Select.Option value="GATEWAY">网关</Select.Option>
771 |               <Select.Option value="NACOS" disabled={apiProduct.type === 'REST_API'}>Nacos</Select.Option>
772 |             </Select>
773 |           </Form.Item>
774 | 
775 |           {sourceType === 'GATEWAY' && (
776 |             <Form.Item
777 |               name="gatewayId"
778 |               label="网关实例"
779 |               rules={[{ required: true, message: '请选择网关' }]}
780 |             >
781 |               <Select 
782 |                 placeholder="请选择网关实例" 
783 |                 loading={gatewayLoading}
784 |                 showSearch
785 |                 filterOption={(input, option) =>
786 |                   (option?.value as unknown as string)?.toLowerCase().includes(input.toLowerCase())
787 |                 }
788 |                 onChange={handleGatewayChange}
789 |                 optionLabelProp="label"
790 |               >
791 |                 {gateways.map(gateway => (
792 |                   <Select.Option
793 |                     key={gateway.gatewayId}
794 |                     value={gateway.gatewayId}
795 |                     label={gateway.gatewayName}
796 |                   >
797 |                     <div>
798 |                       <div className="font-medium">{gateway.gatewayName}</div>
799 |                       <div className="text-sm text-gray-500">
800 |                         {gateway.gatewayId} - {getGatewayTypeLabel(gateway.gatewayType as any)}
801 |                       </div>
802 |                     </div>
803 |                   </Select.Option>
804 |                 ))}
805 |               </Select>
806 |             </Form.Item>
807 |           )}
808 | 
809 |           {sourceType === 'NACOS' && (
810 |             <Form.Item
811 |               name="nacosId"
812 |               label="Nacos实例"
813 |               rules={[{ required: true, message: '请选择Nacos实例' }]}
814 |             >
815 |               <Select
816 |                 placeholder="请选择Nacos实例"
817 |                 loading={nacosLoading}
818 |                 showSearch
819 |                 filterOption={(input, option) =>
820 |                   (option?.value as unknown as string)?.toLowerCase().includes(input.toLowerCase())
821 |                 }
822 |                 onChange={handleNacosChange}
823 |                 optionLabelProp="label"
824 |               >
825 |                 {nacosInstances.map(nacos => (
826 |                   <Select.Option 
827 |                     key={nacos.nacosId} 
828 |                     value={nacos.nacosId}
829 |                     label={nacos.nacosName}
830 |                   >
831 |                     <div>
832 |                       <div className="font-medium">{nacos.nacosName}</div>
833 |                       <div className="text-sm text-gray-500">
834 |                         {nacos.serverUrl}
835 |                       </div>
836 |                     </div>
837 |                   </Select.Option>
838 |                 ))}
839 |               </Select>
840 |             </Form.Item>
841 |           )}
842 | 
843 |           {sourceType === 'NACOS' && selectedNacos && (
844 |             <Form.Item
845 |               name="namespaceId"
846 |               label="命名空间"
847 |               rules={[{ required: true, message: '请选择命名空间' }]}
848 |             >
849 |               <Select
850 |                 placeholder="请选择命名空间"
851 |                 loading={apiLoading && nacosNamespaces.length === 0}
852 |                 onChange={handleNamespaceChange}
853 |                 showSearch
854 |                 filterOption={(input, option) => (option?.children as unknown as string)?.toLowerCase().includes(input.toLowerCase())}
855 |                 optionLabelProp="label"
856 |               >
857 |                 {nacosNamespaces.map(ns => (
858 |                   <Select.Option key={ns.namespaceId} value={ns.namespaceId} label={ns.namespaceName}>
859 |                     <div>
860 |                       <div className="font-medium">{ns.namespaceName}</div>
861 |                       <div className="text-sm text-gray-500">{ns.namespaceId}</div>
862 |                     </div>
863 |                   </Select.Option>
864 |                 ))}
865 |               </Select>
866 |             </Form.Item>
867 |           )}
868 |           
869 |           {(selectedGateway || (selectedNacos && selectedNamespace)) && (
870 |             <Form.Item
871 |               name="apiId"
872 |               label={apiProduct.type === 'REST_API' ? '选择REST API' : '选择MCP Server'}
873 |               rules={[{ required: true, message: apiProduct.type === 'REST_API' ? '请选择REST API' : '请选择MCP Server' }]}
874 |             >
875 |               <Select 
876 |                 placeholder={apiProduct.type === 'REST_API' ? '请选择REST API' : '请选择MCP Server'} 
877 |                 loading={apiLoading}
878 |                 showSearch
879 |                 filterOption={(input, option) =>
880 |                   (option?.value as unknown as string)?.toLowerCase().includes(input.toLowerCase())
881 |                 }
882 |                 optionLabelProp="label"
883 |               >
884 |                 {apiList.map((api: any) => (
885 |                   <Select.Option 
886 |                     key={apiProduct.type === 'REST_API' ? api.apiId : (api.mcpRouteId || api.mcpServerName || api.name)} 
887 |                     value={apiProduct.type === 'REST_API' ? api.apiId : (api.mcpRouteId || api.mcpServerName || api.name)}
888 |                     label={api.apiName || api.mcpServerName || api.name}
889 |                   >
890 |                     <div>
891 |                       <div className="font-medium">{api.apiName || api.mcpServerName || api.name}</div>
892 |                       <div className="text-sm text-gray-500">
893 |                         {api.type} - {apiProduct.type === 'REST_API' ? api.apiId : (api.mcpRouteId || api.mcpServerName || api.name)}
894 |                       </div>
895 |                     </div>
896 |                   </Select.Option>
897 |                 ))}
898 |               </Select>
899 |             </Form.Item>
900 |           )}
901 |         </Form>
902 |       </Modal>
903 |     </div>
904 |   )
905 | } 
```
Page 8/9FirstPrevNextLast