This is page 7 of 7. Use http://codebase.md/higress-group/himarket?lines=false&page={x} to view the full context. # Directory Structure ``` ├── .cursor │ └── rules │ ├── api-style.mdc │ └── project-architecture.mdc ├── .gitignore ├── build.sh ├── deploy │ ├── docker │ │ ├── docker-compose.yml │ │ └── Docker部署说明.md │ └── helm │ ├── Chart.yaml │ ├── Helm部署说明.md │ ├── templates │ │ ├── _helpers.tpl │ │ ├── himarket-admin-cm.yaml │ │ ├── himarket-admin-deployment.yaml │ │ ├── himarket-admin-service.yaml │ │ ├── himarket-frontend-cm.yaml │ │ ├── himarket-frontend-deployment.yaml │ │ ├── himarket-frontend-service.yaml │ │ ├── himarket-server-cm.yaml │ │ ├── himarket-server-deployment.yaml │ │ ├── himarket-server-service.yaml │ │ ├── mysql.yaml │ │ └── serviceaccount.yaml │ └── values.yaml ├── LICENSE ├── NOTICE ├── pom.xml ├── portal-bootstrap │ ├── Dockerfile │ ├── pom.xml │ └── src │ ├── main │ │ ├── java │ │ │ └── com │ │ │ └── alibaba │ │ │ └── apiopenplatform │ │ │ ├── config │ │ │ │ ├── AsyncConfig.java │ │ │ │ ├── FilterConfig.java │ │ │ │ ├── PageConfig.java │ │ │ │ ├── RestTemplateConfig.java │ │ │ │ ├── SecurityConfig.java │ │ │ │ └── SwaggerConfig.java │ │ │ ├── filter │ │ │ │ └── PortalResolvingFilter.java │ │ │ └── PortalApplication.java │ │ └── resources │ │ └── application.yaml │ └── test │ └── java │ └── com │ └── alibaba │ └── apiopenplatform │ └── integration │ └── AdministratorAuthIntegrationTest.java ├── portal-dal │ ├── pom.xml │ └── src │ └── main │ └── java │ └── com │ └── alibaba │ └── apiopenplatform │ ├── converter │ │ ├── AdpAIGatewayConfigConverter.java │ │ ├── APIGConfigConverter.java │ │ ├── APIGRefConfigConverter.java │ │ ├── ApiKeyConfigConverter.java │ │ ├── ConsumerAuthConfigConverter.java │ │ ├── GatewayConfigConverter.java │ │ ├── HigressConfigConverter.java │ │ ├── HigressRefConfigConverter.java │ │ ├── HmacConfigConverter.java │ │ ├── JsonConverter.java │ │ ├── JwtConfigConverter.java │ │ ├── NacosRefConfigConverter.java │ │ ├── PortalSettingConfigConverter.java │ │ ├── PortalUiConfigConverter.java │ │ └── ProductIconConverter.java │ ├── entity │ │ ├── Administrator.java │ │ ├── BaseEntity.java │ │ ├── Consumer.java │ │ ├── ConsumerCredential.java │ │ ├── ConsumerRef.java │ │ ├── Developer.java │ │ ├── DeveloperExternalIdentity.java │ │ ├── Gateway.java │ │ ├── NacosInstance.java │ │ ├── Portal.java │ │ ├── PortalDomain.java │ │ ├── Product.java │ │ ├── ProductPublication.java │ │ ├── ProductRef.java │ │ └── ProductSubscription.java │ ├── repository │ │ ├── AdministratorRepository.java │ │ ├── BaseRepository.java │ │ ├── ConsumerCredentialRepository.java │ │ ├── ConsumerRefRepository.java │ │ ├── ConsumerRepository.java │ │ ├── DeveloperExternalIdentityRepository.java │ │ ├── DeveloperRepository.java │ │ ├── GatewayRepository.java │ │ ├── NacosInstanceRepository.java │ │ ├── PortalDomainRepository.java │ │ ├── PortalRepository.java │ │ ├── ProductPublicationRepository.java │ │ ├── ProductRefRepository.java │ │ ├── ProductRepository.java │ │ └── SubscriptionRepository.java │ └── support │ ├── common │ │ ├── Encrypted.java │ │ ├── Encryptor.java │ │ └── User.java │ ├── consumer │ │ ├── AdpAIAuthConfig.java │ │ ├── APIGAuthConfig.java │ │ ├── ApiKeyConfig.java │ │ ├── ConsumerAuthConfig.java │ │ ├── HigressAuthConfig.java │ │ ├── HmacConfig.java │ │ └── JwtConfig.java │ ├── enums │ │ ├── APIGAPIType.java │ │ ├── ConsumerAuthType.java │ │ ├── ConsumerStatus.java │ │ ├── CredentialMode.java │ │ ├── DeveloperAuthType.java │ │ ├── DeveloperStatus.java │ │ ├── DomainType.java │ │ ├── GatewayType.java │ │ ├── GrantType.java │ │ ├── HigressAPIType.java │ │ ├── JwtAlgorithm.java │ │ ├── ProductIconType.java │ │ ├── ProductStatus.java │ │ ├── ProductType.java │ │ ├── ProtocolType.java │ │ ├── PublicKeyFormat.java │ │ ├── SourceType.java │ │ ├── SubscriptionStatus.java │ │ └── UserType.java │ ├── gateway │ │ ├── AdpAIGatewayConfig.java │ │ ├── APIGConfig.java │ │ ├── GatewayConfig.java │ │ └── HigressConfig.java │ ├── portal │ │ ├── AuthCodeConfig.java │ │ ├── IdentityMapping.java │ │ ├── JwtBearerConfig.java │ │ ├── OAuth2Config.java │ │ ├── OidcConfig.java │ │ ├── PortalSettingConfig.java │ │ ├── PortalUiConfig.java │ │ └── PublicKeyConfig.java │ └── product │ ├── APIGRefConfig.java │ ├── HigressRefConfig.java │ ├── NacosRefConfig.java │ └── ProductIcon.java ├── portal-server │ ├── pom.xml │ └── src │ └── main │ └── java │ └── com │ └── alibaba │ └── apiopenplatform │ ├── controller │ │ ├── AdministratorController.java │ │ ├── ConsumerController.java │ │ ├── DeveloperController.java │ │ ├── GatewayController.java │ │ ├── NacosController.java │ │ ├── OAuth2Controller.java │ │ ├── OidcController.java │ │ ├── PortalController.java │ │ └── ProductController.java │ ├── core │ │ ├── advice │ │ │ ├── ExceptionAdvice.java │ │ │ └── ResponseAdvice.java │ │ ├── annotation │ │ │ ├── AdminAuth.java │ │ │ ├── AdminOrDeveloperAuth.java │ │ │ └── DeveloperAuth.java │ │ ├── constant │ │ │ ├── CommonConstants.java │ │ │ ├── IdpConstants.java │ │ │ ├── JwtConstants.java │ │ │ └── Resources.java │ │ ├── event │ │ │ ├── DeveloperDeletingEvent.java │ │ │ ├── PortalDeletingEvent.java │ │ │ └── ProductDeletingEvent.java │ │ ├── exception │ │ │ ├── BusinessException.java │ │ │ └── ErrorCode.java │ │ ├── response │ │ │ └── Response.java │ │ ├── security │ │ │ ├── ContextHolder.java │ │ │ ├── DeveloperAuthenticationProvider.java │ │ │ └── JwtAuthenticationFilter.java │ │ └── utils │ │ ├── IdGenerator.java │ │ ├── PasswordHasher.java │ │ └── TokenUtil.java │ ├── dto │ │ ├── converter │ │ │ ├── InputConverter.java │ │ │ ├── NacosToGatewayToolsConverter.java │ │ │ └── OutputConverter.java │ │ ├── params │ │ │ ├── admin │ │ │ │ ├── AdminCreateParam.java │ │ │ │ ├── AdminLoginParam.java │ │ │ │ └── ResetPasswordParam.java │ │ │ ├── consumer │ │ │ │ ├── CreateConsumerParam.java │ │ │ │ ├── CreateCredentialParam.java │ │ │ │ ├── CreateSubscriptionParam.java │ │ │ │ ├── QueryConsumerParam.java │ │ │ │ ├── QuerySubscriptionParam.java │ │ │ │ └── UpdateCredentialParam.java │ │ │ ├── developer │ │ │ │ ├── CreateDeveloperParam.java │ │ │ │ ├── CreateExternalDeveloperParam.java │ │ │ │ ├── DeveloperLoginParam.java │ │ │ │ ├── QueryDeveloperParam.java │ │ │ │ ├── UnbindExternalIdentityParam.java │ │ │ │ ├── UpdateDeveloperParam.java │ │ │ │ └── UpdateDeveloperStatusParam.java │ │ │ ├── gateway │ │ │ │ ├── ImportGatewayParam.java │ │ │ │ ├── QueryAdpAIGatewayParam.java │ │ │ │ ├── QueryAPIGParam.java │ │ │ │ └── QueryGatewayParam.java │ │ │ ├── nacos │ │ │ │ ├── CreateNacosParam.java │ │ │ │ ├── QueryNacosNamespaceParam.java │ │ │ │ ├── QueryNacosParam.java │ │ │ │ └── UpdateNacosParam.java │ │ │ ├── portal │ │ │ │ ├── BindDomainParam.java │ │ │ │ ├── CreatePortalParam.java │ │ │ │ └── UpdatePortalParam.java │ │ │ └── product │ │ │ ├── CreateProductParam.java │ │ │ ├── CreateProductRefParam.java │ │ │ ├── PublishProductParam.java │ │ │ ├── QueryProductParam.java │ │ │ ├── QueryProductSubscriptionParam.java │ │ │ ├── UnPublishProductParam.java │ │ │ └── UpdateProductParam.java │ │ └── result │ │ ├── AdminResult.java │ │ ├── AdpGatewayInstanceResult.java │ │ ├── AdpMcpServerListResult.java │ │ ├── AdpMCPServerResult.java │ │ ├── APIConfigResult.java │ │ ├── APIGMCPServerResult.java │ │ ├── APIResult.java │ │ ├── AuthResult.java │ │ ├── ConsumerCredentialResult.java │ │ ├── ConsumerResult.java │ │ ├── DeveloperResult.java │ │ ├── GatewayMCPServerResult.java │ │ ├── GatewayResult.java │ │ ├── HigressMCPServerResult.java │ │ ├── IdpResult.java │ │ ├── IdpState.java │ │ ├── IdpTokenResult.java │ │ ├── MCPConfigResult.java │ │ ├── MCPServerResult.java │ │ ├── MseNacosResult.java │ │ ├── NacosMCPServerResult.java │ │ ├── NacosNamespaceResult.java │ │ ├── NacosResult.java │ │ ├── PageResult.java │ │ ├── PortalResult.java │ │ ├── ProductPublicationResult.java │ │ ├── ProductRefResult.java │ │ ├── ProductResult.java │ │ └── SubscriptionResult.java │ └── service │ ├── AdministratorService.java │ ├── AdpAIGatewayService.java │ ├── ConsumerService.java │ ├── DeveloperService.java │ ├── gateway │ │ ├── AdpAIGatewayOperator.java │ │ ├── AIGatewayOperator.java │ │ ├── APIGOperator.java │ │ ├── client │ │ │ ├── AdpAIGatewayClient.java │ │ │ ├── APIGClient.java │ │ │ ├── GatewayClient.java │ │ │ ├── HigressClient.java │ │ │ ├── PopGatewayClient.java │ │ │ └── SLSClient.java │ │ ├── factory │ │ │ └── HTTPClientFactory.java │ │ ├── GatewayOperator.java │ │ └── HigressOperator.java │ ├── GatewayService.java │ ├── IdpService.java │ ├── impl │ │ ├── AdministratorServiceImpl.java │ │ ├── ConsumerServiceImpl.java │ │ ├── DeveloperServiceImpl.java │ │ ├── GatewayServiceImpl.java │ │ ├── IdpServiceImpl.java │ │ ├── NacosServiceImpl.java │ │ ├── OAuth2ServiceImpl.java │ │ ├── OidcServiceImpl.java │ │ ├── PortalServiceImpl.java │ │ └── ProductServiceImpl.java │ ├── NacosService.java │ ├── OAuth2Service.java │ ├── OidcService.java │ ├── PortalService.java │ └── ProductService.java ├── portal-web │ ├── api-portal-admin │ │ ├── .env │ │ ├── .gitignore │ │ ├── bin │ │ │ ├── replace_var.py │ │ │ └── start.sh │ │ ├── Dockerfile │ │ ├── eslint.config.js │ │ ├── index.html │ │ ├── nginx.conf │ │ ├── package.json │ │ ├── postcss.config.js │ │ ├── proxy.conf │ │ ├── public │ │ │ ├── logo.png │ │ │ └── vite.svg │ │ ├── README.md │ │ ├── src │ │ │ ├── aliyunThemeToken.ts │ │ │ ├── App.css │ │ │ ├── App.tsx │ │ │ ├── assets │ │ │ │ └── react.svg │ │ │ ├── components │ │ │ │ ├── api-product │ │ │ │ │ ├── ApiProductApiDocs.tsx │ │ │ │ │ ├── ApiProductDashboard.tsx │ │ │ │ │ ├── ApiProductFormModal.tsx │ │ │ │ │ ├── ApiProductLinkApi.tsx │ │ │ │ │ ├── ApiProductOverview.tsx │ │ │ │ │ ├── ApiProductPolicy.tsx │ │ │ │ │ ├── ApiProductPortal.tsx │ │ │ │ │ ├── ApiProductUsageGuide.tsx │ │ │ │ │ ├── SwaggerUIWrapper.css │ │ │ │ │ └── SwaggerUIWrapper.tsx │ │ │ │ ├── common │ │ │ │ │ ├── AdvancedSearch.tsx │ │ │ │ │ └── index.ts │ │ │ │ ├── console │ │ │ │ │ ├── GatewayTypeSelector.tsx │ │ │ │ │ ├── ImportGatewayModal.tsx │ │ │ │ │ ├── ImportHigressModal.tsx │ │ │ │ │ ├── ImportMseNacosModal.tsx │ │ │ │ │ └── NacosTypeSelector.tsx │ │ │ │ ├── icons │ │ │ │ │ └── McpServerIcon.tsx │ │ │ │ ├── Layout.tsx │ │ │ │ ├── LayoutWrapper.tsx │ │ │ │ ├── portal │ │ │ │ │ ├── PortalConsumers.tsx │ │ │ │ │ ├── PortalDashboard.tsx │ │ │ │ │ ├── PortalDevelopers.tsx │ │ │ │ │ ├── PortalDomain.tsx │ │ │ │ │ ├── PortalFormModal.tsx │ │ │ │ │ ├── PortalOverview.tsx │ │ │ │ │ ├── PortalPublishedApis.tsx │ │ │ │ │ ├── PortalSecurity.tsx │ │ │ │ │ ├── PortalSettings.tsx │ │ │ │ │ ├── PublicKeyManager.tsx │ │ │ │ │ └── ThirdPartyAuthManager.tsx │ │ │ │ └── subscription │ │ │ │ └── SubscriptionListModal.tsx │ │ │ ├── contexts │ │ │ │ └── LoadingContext.tsx │ │ │ ├── index.css │ │ │ ├── lib │ │ │ │ ├── api.ts │ │ │ │ ├── constant.ts │ │ │ │ └── utils.ts │ │ │ ├── main.tsx │ │ │ ├── pages │ │ │ │ ├── ApiProductDetail.tsx │ │ │ │ ├── ApiProducts.tsx │ │ │ │ ├── Dashboard.tsx │ │ │ │ ├── GatewayConsoles.tsx │ │ │ │ ├── Login.tsx │ │ │ │ ├── NacosConsoles.tsx │ │ │ │ ├── PortalDetail.tsx │ │ │ │ ├── Portals.tsx │ │ │ │ └── Register.tsx │ │ │ ├── routes │ │ │ │ └── index.tsx │ │ │ ├── types │ │ │ │ ├── api-product.ts │ │ │ │ ├── consumer.ts │ │ │ │ ├── gateway.ts │ │ │ │ ├── index.ts │ │ │ │ ├── portal.ts │ │ │ │ ├── shims-js-yaml.d.ts │ │ │ │ └── subscription.ts │ │ │ └── vite-env.d.ts │ │ ├── tailwind.config.js │ │ ├── tsconfig.json │ │ ├── tsconfig.node.json │ │ └── vite.config.ts │ └── api-portal-frontend │ ├── .env │ ├── .gitignore │ ├── .husky │ │ └── pre-commit │ ├── bin │ │ ├── replace_var.py │ │ └── start.sh │ ├── Dockerfile │ ├── eslint.config.js │ ├── index.html │ ├── nginx.conf │ ├── package.json │ ├── postcss.config.js │ ├── proxy.conf │ ├── public │ │ ├── favicon.ico │ │ ├── logo.png │ │ ├── logo.svg │ │ ├── MCP.png │ │ ├── MCP.svg │ │ └── vite.svg │ ├── README.md │ ├── src │ │ ├── aliyunThemeToken.ts │ │ ├── App.css │ │ ├── App.tsx │ │ ├── assets │ │ │ ├── aliyun.png │ │ │ ├── github.png │ │ │ ├── google.png │ │ │ └── react.svg │ │ ├── components │ │ │ ├── consumer │ │ │ │ ├── ConsumerBasicInfo.tsx │ │ │ │ ├── CredentialManager.tsx │ │ │ │ ├── index.ts │ │ │ │ └── SubscriptionManager.tsx │ │ │ ├── Layout.tsx │ │ │ ├── Navigation.tsx │ │ │ ├── ProductHeader.tsx │ │ │ ├── SwaggerUIWrapper.css │ │ │ ├── SwaggerUIWrapper.tsx │ │ │ └── UserInfo.tsx │ │ ├── index.css │ │ ├── lib │ │ │ ├── api.ts │ │ │ ├── statusUtils.ts │ │ │ └── utils.ts │ │ ├── main.tsx │ │ ├── pages │ │ │ ├── ApiDetail.tsx │ │ │ ├── Apis.tsx │ │ │ ├── Callback.tsx │ │ │ ├── ConsumerDetail.tsx │ │ │ ├── Consumers.tsx │ │ │ ├── GettingStarted.tsx │ │ │ ├── Home.tsx │ │ │ ├── Login.tsx │ │ │ ├── Mcp.tsx │ │ │ ├── McpDetail.tsx │ │ │ ├── OidcCallback.tsx │ │ │ ├── Profile.tsx │ │ │ ├── Register.tsx │ │ │ └── Test.css │ │ ├── router.tsx │ │ ├── types │ │ │ ├── consumer.ts │ │ │ └── index.ts │ │ └── vite-env.d.ts │ ├── tailwind.config.js │ ├── tsconfig.app.json │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts └── README.md ``` # Files -------------------------------------------------------------------------------- /portal-web/api-portal-admin/src/components/portal/ThirdPartyAuthManager.tsx: -------------------------------------------------------------------------------- ```typescript import {useState} from 'react' import {Button, Form, Input, Select, Switch, Table, Modal, Space, message, Divider, Steps, Card, Tabs, Collapse, Radio} from 'antd' import {PlusOutlined, EditOutlined, DeleteOutlined, ExclamationCircleOutlined, MinusCircleOutlined, KeyOutlined, CheckCircleFilled, MinusCircleFilled} from '@ant-design/icons' import {ThirdPartyAuthConfig, AuthenticationType, GrantType, AuthCodeConfig, OAuth2Config, OidcConfig, PublicKeyFormat} from '@/types' interface ThirdPartyAuthManagerProps { configs: ThirdPartyAuthConfig[] onSave: (configs: ThirdPartyAuthConfig[]) => Promise<void> } export function ThirdPartyAuthManager({configs, onSave}: ThirdPartyAuthManagerProps) { const [form] = Form.useForm() const [modalVisible, setModalVisible] = useState(false) const [loading, setLoading] = useState(false) const [editingConfig, setEditingConfig] = useState<ThirdPartyAuthConfig | null>(null) const [currentStep, setCurrentStep] = useState(0) const [selectedType, setSelectedType] = useState<AuthenticationType | null>(null) // 添加新配置 const handleAdd = () => { setEditingConfig(null) setSelectedType(null) setCurrentStep(0) setModalVisible(true) form.resetFields() } // 编辑配置 const handleEdit = (config: ThirdPartyAuthConfig) => { setEditingConfig(config) setSelectedType(config.type) setCurrentStep(1) // 直接进入配置步骤 setModalVisible(true) // 根据类型设置表单值 if (config.type === AuthenticationType.OIDC) { // OIDC配置:直接使用OidcConfig的字段 const oidcConfig = config as (OidcConfig & { type: AuthenticationType.OIDC }) // 检查是否是手动配置模式(有具体的端点地址) const hasManualEndpoints = !!(oidcConfig.authCodeConfig?.authorizationEndpoint && oidcConfig.authCodeConfig?.tokenEndpoint && oidcConfig.authCodeConfig?.userInfoEndpoint) form.setFieldsValue({ provider: oidcConfig.provider, name: oidcConfig.name, enabled: oidcConfig.enabled, type: oidcConfig.type, configMode: hasManualEndpoints ? 'manual' : 'auto', ...oidcConfig.authCodeConfig, // 身份映射字段可能在根级别或authCodeConfig中 userIdField: oidcConfig.identityMapping?.userIdField || oidcConfig.authCodeConfig?.identityMapping?.userIdField, userNameField: oidcConfig.identityMapping?.userNameField || oidcConfig.authCodeConfig?.identityMapping?.userNameField, emailField: oidcConfig.identityMapping?.emailField || oidcConfig.authCodeConfig?.identityMapping?.emailField }) } else if (config.type === AuthenticationType.OAUTH2) { // OAuth2配置:直接使用OAuth2Config的字段 const oauth2Config = config as (OAuth2Config & { type: AuthenticationType.OAUTH2 }) form.setFieldsValue({ provider: oauth2Config.provider, name: oauth2Config.name, enabled: oauth2Config.enabled, type: oauth2Config.type, grantType: oauth2Config.grantType || GrantType.JWT_BEARER, // 确保有默认值 userIdField: oauth2Config.identityMapping?.userIdField, userNameField: oauth2Config.identityMapping?.userNameField, emailField: oauth2Config.identityMapping?.emailField, publicKeys: oauth2Config.jwtBearerConfig?.publicKeys || [] }) } } // 删除配置 const handleDelete = async (provider: string, name: string) => { Modal.confirm({ title: '确认删除', icon: <ExclamationCircleOutlined/>, content: `确定要删除第三方认证配置 "${name}" 吗?此操作不可恢复。`, okText: '确认删除', okType: 'danger', cancelText: '取消', async onOk() { try { const updatedConfigs = configs.filter(config => config.provider !== provider) await onSave(updatedConfigs) message.success('第三方认证配置删除成功') } catch (error) { message.error('删除第三方认证配置失败') } }, }) } // 下一步 const handleNext = async () => { if (currentStep === 0) { try { const values = await form.validateFields(['type']) setSelectedType(values.type) setCurrentStep(1) // 为不同类型设置默认值 if (values.type === AuthenticationType.OAUTH2) { form.setFieldsValue({ grantType: GrantType.JWT_BEARER, enabled: true }) } else if (values.type === AuthenticationType.OIDC) { form.setFieldsValue({ enabled: true, configMode: 'auto' }) } } catch (error) { // 验证失败 } } } // 上一步 const handlePrevious = () => { setCurrentStep(0) } // 保存配置 const handleSave = async () => { try { setLoading(true) const values = await form.validateFields() let newConfig: ThirdPartyAuthConfig if (selectedType === AuthenticationType.OIDC) { // OIDC配置:根据配置模式创建不同的authCodeConfig let authCodeConfig: AuthCodeConfig if (values.configMode === 'auto') { // 自动发现模式:只保存issuer,端点置空(后端会通过issuer自动发现) authCodeConfig = { clientId: values.clientId, clientSecret: values.clientSecret, scopes: values.scopes, issuer: values.issuer, authorizationEndpoint: '', // 自动发现模式下端点为空 tokenEndpoint: '', userInfoEndpoint: '', jwkSetUri: '', // 可选的身份映射配置 identityMapping: (values.userIdField || values.userNameField || values.emailField) ? { userIdField: values.userIdField || null, userNameField: values.userNameField || null, emailField: values.emailField || null } : undefined } } else { // 手动配置模式:保存具体的端点地址 authCodeConfig = { clientId: values.clientId, clientSecret: values.clientSecret, scopes: values.scopes, issuer: values.issuer || '', // 手动配置模式下issuer可选 authorizationEndpoint: values.authorizationEndpoint, tokenEndpoint: values.tokenEndpoint, userInfoEndpoint: values.userInfoEndpoint, jwkSetUri: values.jwkSetUri || '', // 可选的身份映射配置 identityMapping: (values.userIdField || values.userNameField || values.emailField) ? { userIdField: values.userIdField || null, userNameField: values.userNameField || null, emailField: values.emailField || null } : undefined } } newConfig = { provider: values.provider, name: values.name, logoUrl: null, enabled: values.enabled ?? true, grantType: 'AUTHORIZATION_CODE' as const, authCodeConfig, // 根级别的身份映射(为兼容后端格式) identityMapping: authCodeConfig.identityMapping, type: AuthenticationType.OIDC } as (OidcConfig & { type: AuthenticationType.OIDC }) } else { // OAuth2配置:直接创建OAuth2Config格式 const grantType = values.grantType || GrantType.JWT_BEARER // 确保有默认值 newConfig = { provider: values.provider, name: values.name, enabled: values.enabled ?? true, grantType: grantType, jwtBearerConfig: grantType === GrantType.JWT_BEARER ? { publicKeys: values.publicKeys || [] } : undefined, identityMapping: { userIdField: values.userIdField || null, userNameField: values.userNameField || null, emailField: values.emailField || null }, type: AuthenticationType.OAUTH2 } as (OAuth2Config & { type: AuthenticationType.OAUTH2 }) } let updatedConfigs if (editingConfig) { updatedConfigs = configs.map(config => config.provider === editingConfig.provider ? newConfig : config ) } else { updatedConfigs = [...configs, newConfig] } await onSave(updatedConfigs) message.success(editingConfig ? '第三方认证配置更新成功' : '第三方认证配置添加成功') setModalVisible(false) } catch (error) { message.error('保存第三方认证配置失败') } finally { setLoading(false) } } // 取消 const handleCancel = () => { setModalVisible(false) setEditingConfig(null) setSelectedType(null) setCurrentStep(0) form.resetFields() } // OIDC表格列定义(不包含类型列) const oidcColumns = [ { title: '提供商', dataIndex: 'provider', key: 'provider', width: 120, render: (provider: string) => ( <span className="font-medium text-gray-700">{provider}</span> ) }, { title: '名称', dataIndex: 'name', key: 'name', width: 150, }, { title: '授权模式', key: 'grantType', width: 120, render: () => <span className="text-gray-600">授权码模式</span> }, { title: '状态', dataIndex: 'enabled', key: 'enabled', width: 80, render: (enabled: boolean) => ( <div className="flex items-center"> {enabled ? ( <CheckCircleFilled className="text-green-500 mr-2" style={{fontSize: '12px'}} /> ) : ( <MinusCircleFilled className="text-gray-500 mr-2" style={{fontSize: '12px'}} /> )} <span className="text-gray-700"> {enabled ? '已启用' : '已停用'} </span> </div> ) }, { title: '操作', key: 'action', width: 150, render: (_: any, record: ThirdPartyAuthConfig) => ( <Space> <Button type="link" icon={<EditOutlined/>} onClick={() => handleEdit(record)} > 编辑 </Button> <Button type="link" danger icon={<DeleteOutlined/>} onClick={() => handleDelete(record.provider, record.name)} > 删除 </Button> </Space> ) } ] // OAuth2表格列定义(不包含类型列) const oauth2Columns = [ { title: '提供商', dataIndex: 'provider', key: 'provider', width: 120, render: (provider: string) => ( <span className="font-medium text-gray-700">{provider}</span> ) }, { title: '名称', dataIndex: 'name', key: 'name', width: 150, }, { title: '授权模式', key: 'grantType', width: 120, render: (record: ThirdPartyAuthConfig) => { if (record.type === AuthenticationType.OAUTH2) { const oauth2Config = record as (OAuth2Config & { type: AuthenticationType.OAUTH2 }) return ( <span className="text-gray-600"> {oauth2Config.grantType === GrantType.JWT_BEARER ? 'JWT断言' : '授权码模式'} </span> ) } return <span className="text-gray-600">授权码模式</span> } }, { title: '状态', dataIndex: 'enabled', key: 'enabled', width: 80, render: (enabled: boolean) => ( <div className="flex items-center"> {enabled ? ( <CheckCircleFilled className="text-green-500 mr-2" style={{fontSize: '12px'}} /> ) : ( <MinusCircleFilled className="text-gray-500 mr-2" style={{fontSize: '12px'}} /> )} <span className="text-gray-700"> {enabled ? '已启用' : '已停用'} </span> </div> ) }, { title: '操作', key: 'action', width: 150, render: (_: any, record: ThirdPartyAuthConfig) => ( <Space> <Button type="link" icon={<EditOutlined/>} onClick={() => handleEdit(record)} > 编辑 </Button> <Button type="link" danger icon={<DeleteOutlined/>} onClick={() => handleDelete(record.provider, record.name)} > 删除 </Button> </Space> ) } ] // 渲染OIDC配置表单 const renderOidcForm = () => ( <div className="space-y-6"> <Form.Item name="grantType" label="授权模式" initialValue="AUTHORIZATION_CODE" > <Select disabled> <Select.Option value="AUTHORIZATION_CODE">授权码模式</Select.Option> </Select> </Form.Item> <div className="grid grid-cols-2 gap-4"> <Form.Item name="clientId" label="Client ID" rules={[{required: true, message: '请输入 Client ID'}]} > <Input placeholder="Client ID"/> </Form.Item> <Form.Item name="clientSecret" label="Client Secret" rules={[{required: true, message: '请输入 Client Secret'}]} > <Input.Password placeholder="Client Secret"/> </Form.Item> </div> <div className="grid grid-cols-2 gap-4"> <Form.Item name="scopes" label="授权范围" rules={[{required: true, message: '请输入授权范围'}]} > <Input placeholder="如: openid profile email"/> </Form.Item> <div></div> </div> <Divider /> {/* 配置模式选择 */} <Form.Item name="configMode" label="端点配置" initialValue="auto" > <Radio.Group> <Radio value="auto">自动发现</Radio> <Radio value="manual">手动配置</Radio> </Radio.Group> </Form.Item> {/* 根据配置模式显示不同字段 */} <Form.Item noStyle shouldUpdate={(prevValues, curValues) => prevValues.configMode !== curValues.configMode} > {({ getFieldValue }) => { const configMode = getFieldValue('configMode') || 'auto' if (configMode === 'auto') { // 自动发现模式:只需要Issuer地址 return ( <Form.Item name="issuer" label="Issuer" rules={[ { required: true, message: '请输入Issuer地址' }, { type: 'url', message: '请输入有效的URL' } ]} > <Input placeholder="如: https://accounts.google.com" /> </Form.Item> ) } else { // 手动配置模式:需要各个端点 return ( <div className="space-y-4"> <div className="grid grid-cols-2 gap-4"> <Form.Item name="authorizationEndpoint" label="授权端点" rules={[{ required: true, message: '请输入授权端点' }]} > <Input placeholder="Authorization 授权端点"/> </Form.Item> <Form.Item name="tokenEndpoint" label="令牌端点" rules={[{ required: true, message: '请输入令牌端点' }]} > <Input placeholder="Token 令牌端点"/> </Form.Item> </div> <div className="grid grid-cols-2 gap-4"> <Form.Item name="userInfoEndpoint" label="用户信息端点" rules={[{ required: true, message: '请输入用户信息端点' }]} > <Input placeholder="UserInfo 端点"/> </Form.Item> <Form.Item name="jwkSetUri" label="公钥端点" > <Input placeholder="可选"/> </Form.Item> </div> </div> ) } }} </Form.Item> <div className="-ml-3"> <Collapse size="small" ghost expandIcon={({ isActive }) => ( <svg className={`w-4 h-4 transition-transform ${isActive ? 'rotate-90' : ''}`} fill="currentColor" viewBox="0 0 20 20" > <path fillRule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clipRule="evenodd" /> </svg> )} items={[ { key: 'advanced', label: ( <div className="flex items-center text-gray-600"> <svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20"> <path fillRule="evenodd" d="M11.49 3.17c-.38-1.56-2.6-1.56-2.98 0a1.532 1.532 0 01-2.286.948c-1.372-.836-2.942.734-2.106 2.106.54.886.061 2.042-.947 2.287-1.561.379-1.561 2.6 0 2.978a1.532 1.532 0 01.947 2.287c-.836 1.372.734 2.942 2.106 2.106a1.532 1.532 0 012.287.947c.379 1.561 2.6 1.561 2.978 0a1.533 1.533 0 012.287-.947c1.372.836 2.942-.734 2.106-2.106a1.533 1.533 0 01.947-2.287c1.561-.379 1.561-2.6 0-2.978a1.532 1.532 0 01-.947-2.287c.836-1.372-.734-2.942-2.106-2.106a1.532 1.532 0 01-2.287-.947z" clipRule="evenodd" /> <path fillRule="evenodd" d="M10 13a3 3 0 100-6 3 3 0 000 6z" clipRule="evenodd" /> </svg> <span className="ml-2">高级配置</span> <span className="text-xs text-gray-400 ml-2">身份映射</span> </div> ), children: ( <div className="space-y-4 pt-2 ml-3"> <div className="grid grid-cols-3 gap-4"> <Form.Item name="userIdField" label="开发者ID" > <Input placeholder="默认: sub"/> </Form.Item> <Form.Item name="userNameField" label="开发者名称" > <Input placeholder="默认: name"/> </Form.Item> <Form.Item name="emailField" label="邮箱" > <Input placeholder="默认: email"/> </Form.Item> </div> <div className="bg-blue-50 p-3 rounded-lg"> <div className="flex items-start space-x-2"> <div className="text-blue-600 mt-0.5"> <svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20"> <path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clipRule="evenodd" /> </svg> </div> <div> <h4 className="text-blue-800 font-medium text-sm">配置说明</h4> <p className="text-blue-700 text-xs mt-1"> 身份映射用于从OIDC令牌中提取用户信息。如果不填写,系统将使用OIDC标准字段。 </p> </div> </div> </div> </div> ) } ]} /> </div> </div> ) // 渲染OAuth2配置表单 const renderOAuth2Form = () => ( <div className="space-y-6"> <Form.Item name="grantType" label="授权模式" initialValue={GrantType.JWT_BEARER} rules={[{required: true}]} > <Select disabled> <Select.Option value={GrantType.JWT_BEARER}>JWT断言</Select.Option> </Select> </Form.Item> <Form.List name="publicKeys"> {(fields, { add, remove }) => ( <div className="space-y-4"> {fields.length > 0 && ( <Collapse size="small" items={fields.map(({ key, name, ...restField }) => ({ key: key, label: ( <div className="flex items-center"> <KeyOutlined className="mr-2" /> <span>公钥 {name + 1}</span> </div> ), extra: ( <Button type="link" danger size="small" icon={<MinusCircleOutlined />} onClick={(e) => { e.stopPropagation() remove(name) }} > 删除 </Button> ), children: ( <div className="space-y-4 px-4"> <div className="grid grid-cols-3 gap-4"> <Form.Item {...restField} name={[name, 'kid']} label="Key ID" rules={[{ required: true, message: '请输入Key ID' }]} > <Input placeholder="公钥标识符" size="small" /> </Form.Item> <Form.Item {...restField} name={[name, 'algorithm']} label="签名算法" rules={[{ required: true, message: '请选择签名算法' }]} > <Select placeholder="选择签名算法" size="small"> <Select.Option value="RS256">RS256</Select.Option> <Select.Option value="RS384">RS384</Select.Option> <Select.Option value="RS512">RS512</Select.Option> <Select.Option value="ES256">ES256</Select.Option> <Select.Option value="ES384">ES384</Select.Option> <Select.Option value="ES512">ES512</Select.Option> </Select> </Form.Item> <Form.Item {...restField} name={[name, 'format']} label="公钥格式" rules={[{ required: true, message: '请选择公钥格式' }]} > <Select placeholder="选择公钥格式" size="small"> <Select.Option value={PublicKeyFormat.PEM}>PEM</Select.Option> <Select.Option value={PublicKeyFormat.JWK}>JWK</Select.Option> </Select> </Form.Item> </div> <Form.Item noStyle shouldUpdate={(prevValues, curValues) => { const prevFormat = prevValues?.publicKeys?.[name]?.format const curFormat = curValues?.publicKeys?.[name]?.format return prevFormat !== curFormat }} > {({ getFieldValue }) => { const format = getFieldValue(['publicKeys', name, 'format']) return ( <Form.Item {...restField} name={[name, 'value']} label="公钥内容" rules={[{ required: true, message: '请输入公钥内容' }]} > <Input.TextArea rows={6} placeholder={ format === PublicKeyFormat.JWK ? 'JWK格式公钥,例如:\n{\n "kty": "RSA",\n "kid": "key1",\n "n": "...",\n "e": "AQAB"\n}' : 'PEM格式公钥,例如:\n-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A...\n-----END PUBLIC KEY-----' } style={{ fontFamily: 'monospace', fontSize: '12px' }} /> </Form.Item> ) }} </Form.Item> </div> ) }))} /> )} <Button type="dashed" onClick={() => add()} block icon={<PlusOutlined />} size="small"> 添加公钥 </Button> </div> )} </Form.List> <div className="-ml-3"> <Collapse size="small" ghost expandIcon={({ isActive }) => ( <svg className={`w-4 h-4 transition-transform ${isActive ? 'rotate-90' : ''}`} fill="currentColor" viewBox="0 0 20 20" > <path fillRule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clipRule="evenodd" /> </svg> )} items={[ { key: 'advanced', label: ( <div className="flex items-center text-gray-600"> <svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20"> <path fillRule="evenodd" d="M11.49 3.17c-.38-1.56-2.6-1.56-2.98 0a1.532 1.532 0 01-2.286.948c-1.372-.836-2.942.734-2.106 2.106.54.886.061 2.042-.947 2.287-1.561.379-1.561 2.6 0 2.978a1.532 1.532 0 01.947 2.287c-.836 1.372.734 2.942 2.106 2.106a1.532 1.532 0 012.287.947c.379 1.561 2.6 1.561 2.978 0a1.533 1.533 0 012.287-.947c1.372.836 2.942-.734 2.106-2.106a1.533 1.533 0 01.947-2.287c1.561-.379 1.561-2.6 0-2.978a1.532 1.532 0 01-.947-2.287c.836-1.372-.734-2.942-2.106-2.106a1.532 1.532 0 01-2.287-.947z" clipRule="evenodd" /> <path fillRule="evenodd" d="M10 13a3 3 0 100-6 3 3 0 000 6z" clipRule="evenodd" /> </svg> <span className="ml-2">高级配置</span> <span className="text-xs text-gray-400 ml-2">身份映射</span> </div> ), children: ( <div className="space-y-4 pt-2 ml-3"> <div className="grid grid-cols-3 gap-4"> <Form.Item name="userIdField" label="开发者ID" > <Input placeholder="默认: userId"/> </Form.Item> <Form.Item name="userNameField" label="开发者名称" > <Input placeholder="默认: name"/> </Form.Item> <Form.Item name="emailField" label="邮箱" > <Input placeholder="默认: email"/> </Form.Item> </div> <div className="bg-blue-50 p-3 rounded-lg"> <div className="flex items-start space-x-2"> <div className="text-blue-600 mt-0.5"> <svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20"> <path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 0 100-2v-3a1 1 0 00-1-1H9z" clipRule="evenodd" /> </svg> </div> <div> <h4 className="text-blue-800 font-medium text-sm">配置说明</h4> <p className="text-blue-700 text-xs mt-1"> 身份映射用于从JWT载荷中提取用户信息。如果不填写,系统将使用默认字段名。 </p> </div> </div> </div> </div> ) } ]} /> </div> </div> ) // 按类型分组配置 const oidcConfigs = configs.filter(config => config.type === AuthenticationType.OIDC) const oauth2Configs = configs.filter(config => config.type === AuthenticationType.OAUTH2) return ( <div className="space-y-6"> <div className="flex justify-between items-center"> <div> <h3 className="text-lg font-medium">第三方认证</h3> <p className="text-sm text-gray-500">管理外部身份认证配置</p> </div> <Button type="primary" icon={<PlusOutlined/>} onClick={handleAdd} > 添加配置 </Button> </div> <Tabs defaultActiveKey="oidc" items={[ { key: 'oidc', label: 'OIDC配置', children: ( <div className="bg-white rounded-lg"> <div className="py-4 border-b border-gray-200"> <h4 className="text-lg font-medium text-gray-900">OIDC配置</h4> <p className="text-sm text-gray-500 mt-1">支持OpenID Connect标准协议的身份提供商</p> </div> <Table columns={oidcColumns} dataSource={oidcConfigs} rowKey="provider" pagination={false} size="small" locale={{ emptyText: '暂无OIDC配置' }} /> </div> ), }, { key: 'oauth2', label: 'OAuth2配置', children: ( <div className="bg-white rounded-lg"> <div className="py-4 border-b border-gray-200"> <h4 className="text-lg font-medium text-gray-900">OAuth2配置</h4> <p className="text-sm text-gray-500 mt-1">支持OAuth 2.0标准协议的身份提供商</p> </div> <Table columns={oauth2Columns} dataSource={oauth2Configs} rowKey="provider" pagination={false} size="small" locale={{ emptyText: '暂无OAuth2配置' }} /> </div> ), }, ]} /> {/* 添加/编辑配置模态框 */} <Modal title={editingConfig ? '编辑第三方认证配置' : '添加第三方认证配置'} open={modalVisible} onCancel={handleCancel} width={800} footer={null} > <Steps current={currentStep} className="mb-6" items={[ { title: '选择类型', description: '选择认证协议类型' }, { title: '配置认证', description: '填写认证参数' } ]} /> <Form form={form} layout="vertical" > {currentStep === 0 ? ( // 第一步:选择类型 <Card> <Form.Item name="type" label="认证类型" rules={[{required: true, message: '请选择认证类型'}]} > <Select placeholder="请选择认证方式" size="large"> <Select.Option value={AuthenticationType.OIDC}> <div className="py-2"> <div className="font-medium">OIDC(适用于支持OpenID Connect的身份提供商认证)</div> </div> </Select.Option> <Select.Option value={AuthenticationType.OAUTH2}> <div className="py-2"> <div className="font-medium">OAuth2(适用于服务间集成)</div> </div> </Select.Option> </Select> </Form.Item> <div className="flex justify-end"> <Button type="primary" onClick={handleNext}> 下一步 </Button> </div> </Card> ) : ( // 第二步:配置详情 <div> <div className="grid grid-cols-2 gap-4"> <Form.Item name="provider" label="提供商标识" rules={[ {required: true, message: '请输入提供商标识'}, { validator: (_, value) => { if (!value) return Promise.resolve() // 检查provider唯一性 const isDuplicate = configs.some(config => config.provider === value && (!editingConfig || editingConfig.provider !== value) ) if (isDuplicate) { return Promise.reject(new Error('该提供商标识已存在,请使用不同的标识')) } return Promise.resolve() } } ]} > <Input placeholder="如: google, company-sso" disabled={editingConfig !== null} /> </Form.Item> <Form.Item name="name" label="显示名称" rules={[{required: true, message: '请输入显示名称'}]} > <Input placeholder="如: Google登录、公司SSO"/> </Form.Item> </div> <Form.Item name="enabled" label="启用状态" valuePropName="checked" > <Switch/> </Form.Item> <Divider /> {/* 根据类型显示不同的配置表单 */} {selectedType === AuthenticationType.OIDC ? renderOidcForm() : renderOAuth2Form()} <div className="flex justify-between mt-6"> <Button onClick={handlePrevious}> 上一步 </Button> <Space> <Button onClick={handleCancel}> 取消 </Button> <Button type="primary" loading={loading} onClick={handleSave}> {editingConfig ? '更新' : '添加'} </Button> </Space> </div> </div> )} </Form> </Modal> </div> ) } ``` -------------------------------------------------------------------------------- /portal-server/src/main/java/com/alibaba/apiopenplatform/service/gateway/AdpAIGatewayOperator.java: -------------------------------------------------------------------------------- ```java package com.alibaba.apiopenplatform.service.gateway; import com.alibaba.apiopenplatform.core.exception.BusinessException; import com.alibaba.apiopenplatform.core.exception.ErrorCode; import com.alibaba.apiopenplatform.dto.params.gateway.QueryAdpAIGatewayParam; import com.alibaba.apiopenplatform.dto.result.*; import com.alibaba.apiopenplatform.dto.result.AdpGatewayInstanceResult; import com.alibaba.apiopenplatform.entity.Consumer; import com.alibaba.apiopenplatform.entity.ConsumerCredential; import com.alibaba.apiopenplatform.entity.Gateway; import com.alibaba.apiopenplatform.support.consumer.AdpAIAuthConfig; import com.alibaba.apiopenplatform.support.consumer.ConsumerAuthConfig; import com.alibaba.apiopenplatform.support.enums.GatewayType; import com.alibaba.apiopenplatform.support.gateway.AdpAIGatewayConfig; import com.alibaba.apiopenplatform.service.gateway.client.AdpAIGatewayClient; import com.alibaba.apiopenplatform.support.gateway.GatewayConfig; import com.alibaba.apiopenplatform.support.product.APIGRefConfig; import com.alibaba.apiopenplatform.dto.result.MCPConfigResult; import cn.hutool.json.JSONUtil; import lombok.Data; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpEntity; import org.springframework.http.HttpMethod; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.stream.Collectors; /** * ADP AI网关操作器 */ @Service @Slf4j public class AdpAIGatewayOperator extends GatewayOperator { @Override public PageResult<APIResult> fetchHTTPAPIs(Gateway gateway, int page, int size) { return null; } @Override public PageResult<APIResult> fetchRESTAPIs(Gateway gateway, int page, int size) { return null; } @Override public PageResult<? extends GatewayMCPServerResult> fetchMcpServers(Gateway gateway, int page, int size) { AdpAIGatewayConfig config = gateway.getAdpAIGatewayConfig(); if (config == null) { throw new BusinessException(ErrorCode.INVALID_PARAMETER, "ADP AI Gateway 配置缺失"); } AdpAIGatewayClient client = new AdpAIGatewayClient(config); try { String url = client.getFullUrl("/mcpServer/listMcpServers"); // 修复:添加必需的 gwInstanceId 参数 String requestBody = String.format( "{\"current\": %d, \"size\": %d, \"gwInstanceId\": \"%s\"}", page, size, gateway.getGatewayId() ); HttpEntity<String> requestEntity = client.createRequestEntity(requestBody); ResponseEntity<AdpMcpServerListResult> response = client.getRestTemplate().exchange( url, HttpMethod.POST, requestEntity, AdpMcpServerListResult.class); if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) { AdpMcpServerListResult result = response.getBody(); if (result.getCode() != null && result.getCode() == 200 && result.getData() != null) { List<GatewayMCPServerResult> items = new ArrayList<>(); if (result.getData().getRecords() != null) { items.addAll(result.getData().getRecords()); } int total = result.getData().getTotal() != null ? result.getData().getTotal() : 0; return PageResult.of(items, page, size, total); } String msg = result.getMessage() != null ? result.getMessage() : result.getMsg(); throw new BusinessException(ErrorCode.GATEWAY_ERROR, msg); } throw new BusinessException(ErrorCode.GATEWAY_ERROR, "调用 ADP /mcpServer/listMcpServers 失败"); } catch (Exception e) { log.error("Error fetching ADP MCP servers", e); throw new BusinessException(ErrorCode.INTERNAL_ERROR, e.getMessage()); } finally { client.close(); } } @Override public String fetchAPIConfig(Gateway gateway, Object config) { return ""; } @Override public String fetchMcpConfig(Gateway gateway, Object conf) { AdpAIGatewayConfig config = gateway.getAdpAIGatewayConfig(); if (config == null) { throw new BusinessException(ErrorCode.INVALID_PARAMETER, "ADP AI Gateway 配置缺失"); } // 从 conf 参数中获取 APIGRefConfig APIGRefConfig apigRefConfig = (APIGRefConfig) conf; if (apigRefConfig == null || apigRefConfig.getMcpServerName() == null) { throw new BusinessException(ErrorCode.INVALID_PARAMETER, "MCP Server 名称缺失"); } AdpAIGatewayClient client = new AdpAIGatewayClient(config); try { String url = client.getFullUrl("/mcpServer/getMcpServer"); // 构建请求体,包含 gwInstanceId 和 mcpServerName String requestBody = String.format( "{\"gwInstanceId\": \"%s\", \"mcpServerName\": \"%s\"}", gateway.getGatewayId(), apigRefConfig.getMcpServerName() ); HttpEntity<String> requestEntity = client.createRequestEntity(requestBody); ResponseEntity<AdpMcpServerDetailResult> response = client.getRestTemplate().exchange( url, HttpMethod.POST, requestEntity, AdpMcpServerDetailResult.class); if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) { AdpMcpServerDetailResult result = response.getBody(); if (result.getCode() != null && result.getCode() == 200 && result.getData() != null) { return convertToMCPConfig(result.getData(), config); } String msg = result.getMessage() != null ? result.getMessage() : result.getMsg(); throw new BusinessException(ErrorCode.GATEWAY_ERROR, msg); } throw new BusinessException(ErrorCode.GATEWAY_ERROR, "调用 ADP /mcpServer/getMcpServer 失败"); } catch (Exception e) { log.error("Error fetching ADP MCP config for server: {}", apigRefConfig.getMcpServerName(), e); throw new BusinessException(ErrorCode.INTERNAL_ERROR, e.getMessage()); } finally { client.close(); } } /** * 将 ADP MCP Server 详情转换为 MCPConfigResult 格式 */ private String convertToMCPConfig(AdpMcpServerDetailResult.AdpMcpServerDetail data, AdpAIGatewayConfig config) { MCPConfigResult mcpConfig = new MCPConfigResult(); mcpConfig.setMcpServerName(data.getName()); // 设置 MCP Server 配置 MCPConfigResult.MCPServerConfig serverConfig = new MCPConfigResult.MCPServerConfig(); serverConfig.setPath("/" + data.getName()); // 获取网关实例访问信息并设置域名信息 List<MCPConfigResult.Domain> domains = getGatewayAccessDomains(data.getGwInstanceId(), config); if (domains != null && !domains.isEmpty()) { serverConfig.setDomains(domains); } else { // 如果无法获取网关访问信息,则使用原有的services信息作为备选 if (data.getServices() != null && !data.getServices().isEmpty()) { List<MCPConfigResult.Domain> fallbackDomains = data.getServices().stream() .map(domain -> MCPConfigResult.Domain.builder() .domain(domain.getName() + ":" + domain.getPort()) .protocol("http") .build()) .collect(Collectors.toList()); serverConfig.setDomains(fallbackDomains); } } mcpConfig.setMcpServerConfig(serverConfig); // 设置工具配置 mcpConfig.setTools(data.getRawConfigurations()); // 设置元数据 MCPConfigResult.McpMetadata meta = new MCPConfigResult.McpMetadata(); meta.setSource(GatewayType.ADP_AI_GATEWAY.name()); meta.setCreateFromType(data.getType()); mcpConfig.setMeta(meta); return JSONUtil.toJsonStr(mcpConfig); } /** * 获取网关实例的访问信息并构建域名列表 */ private List<MCPConfigResult.Domain> getGatewayAccessDomains(String gwInstanceId, AdpAIGatewayConfig config) { AdpAIGatewayClient client = new AdpAIGatewayClient(config); try { String url = client.getFullUrl("/gatewayInstance/getInstanceInfo"); String requestBody = String.format("{\"gwInstanceId\": \"%s\"}", gwInstanceId); HttpEntity<String> requestEntity = client.createRequestEntity(requestBody); // 注意:getInstanceInfo 返回的 data 是单个实例对象(无 records 字段),直接从 data.accessMode 读取 ResponseEntity<String> response = client.getRestTemplate().exchange( url, HttpMethod.POST, requestEntity, String.class); if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) { cn.hutool.json.JSONObject root = JSONUtil.parseObj(response.getBody()); Integer code = root.getInt("code"); if (code != null && code == 200 && root.containsKey("data")) { cn.hutool.json.JSONObject dataObj = root.getJSONObject("data"); if (dataObj != null && dataObj.containsKey("accessMode")) { cn.hutool.json.JSONArray arr = dataObj.getJSONArray("accessMode"); List<AdpGatewayInstanceResult.AccessMode> accessModes = JSONUtil.toList(arr, AdpGatewayInstanceResult.AccessMode.class); return buildDomainsFromAccessModes(accessModes); } log.warn("Gateway instance has no accessMode, instanceId={}", gwInstanceId); return null; } String message = root.getStr("message", root.getStr("msg")); log.warn("Failed to get gateway instance access info: {}", message); return null; } log.warn("Failed to call gateway instance access API"); return null; } catch (Exception e) { log.error("Error fetching gateway access info for instance: {}", gwInstanceId, e); return null; } finally { client.close(); } } /** * 根据网关实例访问信息构建域名列表 */ private List<MCPConfigResult.Domain> buildDomainsFromAccessInfo(AdpGatewayInstanceResult.AdpGatewayInstanceData data) { // 兼容 listInstances 调用:取第一条记录的 accessMode if (data != null && data.getRecords() != null && !data.getRecords().isEmpty()) { AdpGatewayInstanceResult.AdpGatewayInstance instance = data.getRecords().get(0); if (instance.getAccessMode() != null) { return buildDomainsFromAccessModes(instance.getAccessMode()); } } return new ArrayList<>(); } private List<MCPConfigResult.Domain> buildDomainsFromAccessModes(List<AdpGatewayInstanceResult.AccessMode> accessModes) { List<MCPConfigResult.Domain> domains = new ArrayList<>(); if (accessModes == null || accessModes.isEmpty()) { return domains; } AdpGatewayInstanceResult.AccessMode accessMode = accessModes.get(0); // 1) LoadBalancer: externalIps:80 if ("LoadBalancer".equalsIgnoreCase(accessMode.getAccessModeType())) { if (accessMode.getExternalIps() != null && !accessMode.getExternalIps().isEmpty()) { for (String externalIp : accessMode.getExternalIps()) { if (externalIp == null || externalIp.isEmpty()) { continue; } MCPConfigResult.Domain domain = MCPConfigResult.Domain.builder() .domain(externalIp + ":80") .protocol("http") .build(); domains.add(domain); } } } // 2) NodePort: ips + ports → ip:nodePort if (domains.isEmpty() && "NodePort".equalsIgnoreCase(accessMode.getAccessModeType())) { List<String> ips = accessMode.getIps(); List<String> ports = accessMode.getPorts(); if (ips != null && !ips.isEmpty() && ports != null && !ports.isEmpty()) { for (String ip : ips) { if (ip == null || ip.isEmpty()) { continue; } for (String portMapping : ports) { if (portMapping == null || portMapping.isEmpty()) { continue; } String[] parts = portMapping.split(":"); if (parts.length >= 2) { String nodePort = parts[1].split("/")[0]; MCPConfigResult.Domain domain = MCPConfigResult.Domain.builder() .domain(ip + ":" + nodePort) .protocol("http") .build(); domains.add(domain); } } } } } // 3) fallback: only externalIps → :80 if (domains.isEmpty() && accessMode.getExternalIps() != null && !accessMode.getExternalIps().isEmpty()) { for (String externalIp : accessMode.getExternalIps()) { if (externalIp == null || externalIp.isEmpty()) { continue; } MCPConfigResult.Domain domain = MCPConfigResult.Domain.builder() .domain(externalIp + ":80") .protocol("http") .build(); domains.add(domain); } } return domains; } @Override public String createConsumer(Consumer consumer, ConsumerCredential credential, GatewayConfig config) { AdpAIGatewayConfig adpConfig = config.getAdpAIGatewayConfig(); if (adpConfig == null) { throw new BusinessException(ErrorCode.INVALID_PARAMETER, "ADP AI Gateway配置缺失"); } AdpAIGatewayClient client = new AdpAIGatewayClient(adpConfig); try { // 构建请求参数 cn.hutool.json.JSONObject requestData = JSONUtil.createObj(); requestData.set("authType", 5); // 从凭证中获取key if (credential.getApiKeyConfig() != null && credential.getApiKeyConfig().getCredentials() != null && !credential.getApiKeyConfig().getCredentials().isEmpty()) { String key = credential.getApiKeyConfig().getCredentials().get(0).getApiKey(); requestData.set("key", key); } requestData.set("appName", consumer.getName()); // 从 GatewayConfig 中获取 Gateway 实体,与 fetchMcpConfig 方法保持一致 Gateway gateway = config.getGateway(); if (gateway == null || gateway.getGatewayId() == null) { throw new BusinessException(ErrorCode.INVALID_PARAMETER, "网关实例ID缺失"); } requestData.set("gwInstanceId", gateway.getGatewayId()); String url = client.getFullUrl("/application/createApp"); String requestBody = requestData.toString(); HttpEntity<String> requestEntity = client.createRequestEntity(requestBody); log.info("Creating consumer in ADP gateway: url={}, requestBody={}", url, requestBody); ResponseEntity<String> response = client.getRestTemplate().exchange( url, HttpMethod.POST, requestEntity, String.class); if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) { log.info("ADP gateway response: {}", response.getBody()); // 对于ADP AI网关,返回的data就是appName,可以直接用于后续的MCP授权 return extractConsumerIdFromResponse(response.getBody(), consumer.getName()); } throw new BusinessException(ErrorCode.GATEWAY_ERROR, "Failed to create consumer in ADP gateway"); } catch (BusinessException e) { log.error("Business error creating consumer in ADP gateway", e); throw e; } catch (Exception e) { log.error("Error creating consumer in ADP gateway", e); throw new BusinessException(ErrorCode.INTERNAL_ERROR, "Error creating consumer in ADP gateway: " + e.getMessage()); } finally { client.close(); } } /** * 从响应中提取消费者ID * 对于ADP AI网关,/application/createApp接口返回的data就是appName(应用名称) * 我们直接返回appName,这样在授权时可以直接使用 */ private String extractConsumerIdFromResponse(String responseBody, String defaultConsumerId) { try { cn.hutool.json.JSONObject responseJson = JSONUtil.parseObj(responseBody); // ADP AI网关的/application/createApp接口,成功时返回格式: {"code": 200, "data": "appName"} if (responseJson.getInt("code", 0) == 200 && responseJson.containsKey("data")) { Object dataObj = responseJson.get("data"); if (dataObj != null) { // data字段就是appName,直接返回 if (dataObj instanceof String) { return (String) dataObj; } // 如果data是对象类型,则按原逻辑处理(兼容性考虑) if (dataObj instanceof cn.hutool.json.JSONObject) { cn.hutool.json.JSONObject data = (cn.hutool.json.JSONObject) dataObj; if (data.containsKey("applicationId")) { return data.getStr("applicationId"); } // 如果没有applicationId字段,将整个data对象转为字符串 return data.toString(); } // 其他类型直接转为字符串 return dataObj.toString(); } } // 如果无法解析,使用应用名称作为fallback return defaultConsumerId; // 这里传入的是consumer.getName() } catch (Exception e) { log.warn("Failed to parse response body, using consumer name as fallback", e); return defaultConsumerId; } } @Override public void updateConsumer(String consumerId, ConsumerCredential credential, GatewayConfig config) { AdpAIGatewayConfig adpConfig = config.getAdpAIGatewayConfig(); if (adpConfig == null) { throw new BusinessException(ErrorCode.INVALID_PARAMETER, "ADP AI Gateway配置缺失"); } Gateway gateway = config.getGateway(); if (gateway == null || gateway.getGatewayId() == null) { throw new BusinessException(ErrorCode.INVALID_PARAMETER, "网关实例ID缺失"); } AdpAIGatewayClient client = new AdpAIGatewayClient(adpConfig); try { // 从凭据中提取API Key String apiKey = null; if (credential != null && credential.getApiKeyConfig() != null && credential.getApiKeyConfig().getCredentials() != null && !credential.getApiKeyConfig().getCredentials().isEmpty()) { apiKey = credential.getApiKeyConfig().getCredentials().get(0).getApiKey(); } String url = client.getFullUrl("/application/modifyApp"); // 构建请求体 cn.hutool.json.JSONObject requestData = JSONUtil.createObj(); requestData.set("appId", consumerId); requestData.set("appName", consumerId); requestData.set("authType", 5); // 固定参数 requestData.set("authTypeName", "API_KEY"); requestData.set("description", consumerId); requestData.set("enable", true); // 固定参数 if (apiKey != null) { requestData.set("key", apiKey); } requestData.set("groups", Collections.singletonList("true")); // 固定参数 requestData.set("gwInstanceId", gateway.getGatewayId()); String requestBody = requestData.toString(); HttpEntity<String> requestEntity = client.createRequestEntity(requestBody); log.info("Updating consumer in ADP gateway: url={}, requestBody={}", url, requestBody); ResponseEntity<String> response = client.getRestTemplate().exchange( url, HttpMethod.POST, requestEntity, String.class); if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) { cn.hutool.json.JSONObject responseJson = JSONUtil.parseObj(response.getBody()); Integer code = responseJson.getInt("code", 0); if (code != null && code == 200) { log.info("Successfully updated consumer {} in ADP gateway instance {}", consumerId, gateway.getGatewayId()); return; } String message = responseJson.getStr("message", responseJson.getStr("msg", "Unknown error")); throw new BusinessException(ErrorCode.GATEWAY_ERROR, "更新ADP网关消费者失败: " + message); } throw new BusinessException(ErrorCode.GATEWAY_ERROR, "调用 ADP /application/modifyApp 失败"); } catch (BusinessException e) { throw e; } catch (Exception e) { log.error("Error updating consumer {} in ADP gateway instance {}", consumerId, gateway != null ? gateway.getGatewayId() : "unknown", e); throw new BusinessException(ErrorCode.INTERNAL_ERROR, "更新ADP网关消费者异常: " + e.getMessage()); } finally { client.close(); } } @Override public void deleteConsumer(String consumerId, GatewayConfig config) { AdpAIGatewayConfig adpConfig = config.getAdpAIGatewayConfig(); if (adpConfig == null) { throw new BusinessException(ErrorCode.INVALID_PARAMETER, "ADP AI Gateway配置缺失"); } Gateway gateway = config.getGateway(); if (gateway == null || gateway.getGatewayId() == null) { throw new BusinessException(ErrorCode.INVALID_PARAMETER, "网关实例ID缺失"); } AdpAIGatewayClient client = new AdpAIGatewayClient(adpConfig); try { String url = client.getFullUrl("/application/deleteApp"); String requestBody = String.format( "{\"appId\": \"%s\", \"gwInstanceId\": \"%s\"}", consumerId, gateway.getGatewayId() ); HttpEntity<String> requestEntity = client.createRequestEntity(requestBody); log.info("Deleting consumer in ADP gateway: url={}, requestBody={}", url, requestBody); ResponseEntity<String> response = client.getRestTemplate().exchange( url, HttpMethod.POST, requestEntity, String.class); if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) { cn.hutool.json.JSONObject responseJson = JSONUtil.parseObj(response.getBody()); Integer code = responseJson.getInt("code", 0); if (code != null && code == 200) { log.info("Successfully deleted consumer {} from ADP gateway instance {}", consumerId, gateway.getGatewayId()); return; } String message = responseJson.getStr("message", responseJson.getStr("msg", "Unknown error")); throw new BusinessException(ErrorCode.GATEWAY_ERROR, "删除ADP网关消费者失败: " + message); } throw new BusinessException(ErrorCode.GATEWAY_ERROR, "调用 ADP /application/deleteApp 失败"); } catch (BusinessException e) { throw e; } catch (Exception e) { log.error("Error deleting consumer {} from ADP gateway instance {}", consumerId, gateway != null ? gateway.getGatewayId() : "unknown", e); throw new BusinessException(ErrorCode.INTERNAL_ERROR, "删除ADP网关消费者异常: " + e.getMessage()); } finally { client.close(); } } @Override public boolean isConsumerExists(String consumerId, GatewayConfig config) { AdpAIGatewayConfig adpConfig = config.getAdpAIGatewayConfig(); if (adpConfig == null) { log.warn("ADP AI Gateway配置缺失,无法检查消费者存在性"); return false; } AdpAIGatewayClient client = new AdpAIGatewayClient(adpConfig); try { // 从 GatewayConfig 中获取 Gateway 实体 Gateway gateway = config.getGateway(); if (gateway == null || gateway.getGatewayId() == null) { log.warn("网关实例ID缺失,无法检查消费者存在性"); return false; } String url = client.getFullUrl("/application/getApp"); String requestBody = String.format( "{\"%s\": \"%s\", \"%s\": \"%s\"}", "gwInstanceId", gateway.getGatewayId(), "appId", consumerId ); HttpEntity<String> requestEntity = client.createRequestEntity(requestBody); ResponseEntity<String> response = client.getRestTemplate().exchange( url, HttpMethod.POST, requestEntity, String.class); if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) { cn.hutool.json.JSONObject responseJson = JSONUtil.parseObj(response.getBody()); Integer code = responseJson.getInt("code", 0); // 如果返回200且有data,说明消费者存在 return code == 200 && responseJson.containsKey("data") && responseJson.get("data") != null; } return false; } catch (Exception e) { log.warn("检查ADP网关消费者存在性失败: consumerId={}", consumerId, e); return false; } finally { client.close(); } } @Override public ConsumerAuthConfig authorizeConsumer(Gateway gateway, String consumerId, Object refConfig) { AdpAIGatewayConfig adpConfig = gateway.getAdpAIGatewayConfig(); if (adpConfig == null) { throw new BusinessException(ErrorCode.INVALID_PARAMETER, "ADP AI Gateway配置缺失"); } // 解析MCP Server配置 APIGRefConfig apigRefConfig = (APIGRefConfig) refConfig; if (apigRefConfig == null || apigRefConfig.getMcpServerName() == null) { throw new BusinessException(ErrorCode.INVALID_PARAMETER, "MCP Server名称缺失"); } AdpAIGatewayClient client = new AdpAIGatewayClient(adpConfig); try { // 构建授权请求参数 // 由于createConsumer返回的就是appName,所以consumerId就是应用名称 cn.hutool.json.JSONObject requestData = JSONUtil.createObj(); requestData.set("mcpServerName", apigRefConfig.getMcpServerName()); requestData.set("consumers", Collections.singletonList(consumerId)); // consumerId就是appName requestData.set("gwInstanceId", gateway.getGatewayId()); String url = client.getFullUrl("/mcpServer/addMcpServerConsumers"); String requestBody = requestData.toString(); HttpEntity<String> requestEntity = client.createRequestEntity(requestBody); log.info("Authorizing consumer to MCP server: url={}, requestBody={}", url, requestBody); ResponseEntity<String> response = client.getRestTemplate().exchange( url, HttpMethod.POST, requestEntity, String.class); if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) { cn.hutool.json.JSONObject responseJson = JSONUtil.parseObj(response.getBody()); Integer code = responseJson.getInt("code", 0); if (code == 200) { log.info("Successfully authorized consumer {} to MCP server {}", consumerId, apigRefConfig.getMcpServerName()); // 构建授权配置返回结果 AdpAIAuthConfig authConfig = AdpAIAuthConfig.builder() .mcpServerName(apigRefConfig.getMcpServerName()) .consumerId(consumerId) .gwInstanceId(gateway.getGatewayId()) .build(); return ConsumerAuthConfig.builder() .adpAIAuthConfig(authConfig) .build(); } else { String message = responseJson.getStr("message", responseJson.getStr("msg", "Unknown error")); throw new BusinessException(ErrorCode.GATEWAY_ERROR, "Failed to authorize consumer to MCP server: " + message); } } throw new BusinessException(ErrorCode.GATEWAY_ERROR, "Failed to authorize consumer to MCP server"); } catch (BusinessException e) { log.error("Business error authorizing consumer to MCP server", e); throw e; } catch (Exception e) { log.error("Error authorizing consumer {} to MCP server {}", consumerId, apigRefConfig.getMcpServerName(), e); throw new BusinessException(ErrorCode.INTERNAL_ERROR, "Error authorizing consumer to MCP server: " + e.getMessage()); } finally { client.close(); } } @Override public void revokeConsumerAuthorization(Gateway gateway, String consumerId, ConsumerAuthConfig authConfig) { AdpAIAuthConfig adpAIAuthConfig = authConfig.getAdpAIAuthConfig(); if (adpAIAuthConfig == null) { log.warn("ADP AI 授权配置为空,无法撤销授权"); return; } AdpAIGatewayConfig adpConfig = gateway.getAdpAIGatewayConfig(); if (adpConfig == null) { throw new BusinessException(ErrorCode.INVALID_PARAMETER, "ADP AI Gateway配置缺失"); } AdpAIGatewayClient client = new AdpAIGatewayClient(adpConfig); try { // 构建撤销授权请求参数 // 由于createConsumer返回的就是appName,所以consumerId就是应用名称 cn.hutool.json.JSONObject requestData = JSONUtil.createObj(); requestData.set("mcpServerName", adpAIAuthConfig.getMcpServerName()); requestData.set("consumers", Collections.singletonList(consumerId)); // consumerId就是appName requestData.set("gwInstanceId", gateway.getGatewayId()); String url = client.getFullUrl("/mcpServer/deleteMcpServerConsumers"); String requestBody = requestData.toString(); HttpEntity<String> requestEntity = client.createRequestEntity(requestBody); log.info("Revoking consumer authorization from MCP server: url={}, requestBody={}", url, requestBody); ResponseEntity<String> response = client.getRestTemplate().exchange( url, HttpMethod.POST, requestEntity, String.class); if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) { cn.hutool.json.JSONObject responseJson = JSONUtil.parseObj(response.getBody()); Integer code = responseJson.getInt("code", 0); if (code == 200) { log.info("Successfully revoked consumer {} authorization from MCP server {}", consumerId, adpAIAuthConfig.getMcpServerName()); } else { String message = responseJson.getStr("message", responseJson.getStr("msg", "Unknown error")); log.warn("Failed to revoke consumer authorization from MCP server: {}", message); // 撤销授权失败不抛异常,只记录日志 } } else { log.warn("Failed to revoke consumer authorization from MCP server, HTTP status: {}", response.getStatusCode()); } } catch (Exception e) { log.error("Error revoking consumer {} authorization from MCP server {}", consumerId, adpAIAuthConfig.getMcpServerName(), e); // 撤销授权失败不抛异常,只记录日志 } finally { client.close(); } } @Override public APIResult fetchAPI(Gateway gateway, String apiId) { return null; } @Override public GatewayType getGatewayType() { return GatewayType.ADP_AI_GATEWAY; } @Override public String getDashboard(Gateway gateway,String type) { return null; } @Override public PageResult<GatewayResult> fetchGateways(Object param, int page, int size) { if (!(param instanceof QueryAdpAIGatewayParam)) { throw new BusinessException(ErrorCode.INVALID_PARAMETER, "param"); } return fetchGateways((QueryAdpAIGatewayParam) param, page, size); } public PageResult<GatewayResult> fetchGateways(QueryAdpAIGatewayParam param, int page, int size) { AdpAIGatewayConfig config = new AdpAIGatewayConfig(); config.setBaseUrl(param.getBaseUrl()); config.setPort(param.getPort()); // 根据认证类型设置不同的认证信息 if ("Seed".equals(param.getAuthType())) { if (param.getAuthSeed() == null || param.getAuthSeed().trim().isEmpty()) { throw new BusinessException(ErrorCode.INVALID_PARAMETER, "Seed认证方式下authSeed不能为空"); } config.setAuthSeed(param.getAuthSeed()); } else if ("Header".equals(param.getAuthType())) { if (param.getAuthHeaders() == null || param.getAuthHeaders().isEmpty()) { throw new BusinessException(ErrorCode.INVALID_PARAMETER, "Header认证方式下authHeaders不能为空"); } // 将authHeaders转换为配置 List<AdpAIGatewayConfig.AuthHeader> configHeaders = new ArrayList<>(); for (QueryAdpAIGatewayParam.AuthHeader paramHeader : param.getAuthHeaders()) { AdpAIGatewayConfig.AuthHeader configHeader = new AdpAIGatewayConfig.AuthHeader(); configHeader.setKey(paramHeader.getKey()); configHeader.setValue(paramHeader.getValue()); configHeaders.add(configHeader); } config.setAuthHeaders(configHeaders); } else { throw new BusinessException(ErrorCode.INVALID_PARAMETER, "不支持的认证类型: " + param.getAuthType()); } AdpAIGatewayClient client = new AdpAIGatewayClient(config); try { String url = client.getFullUrl("/gatewayInstance/listInstances"); String requestBody = String.format("{\"current\": %d, \"size\": %d}", page, size); HttpEntity<String> requestEntity = client.createRequestEntity(requestBody); ResponseEntity<AdpGatewayInstanceResult> response = client.getRestTemplate().exchange( url, HttpMethod.POST, requestEntity, AdpGatewayInstanceResult.class); if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) { AdpGatewayInstanceResult result = response.getBody(); if (result.getCode() == 200 && result.getData() != null) { return convertToGatewayResult(result.getData(), page, size); } String msg = result.getMessage() != null ? result.getMessage() : result.getMsg(); throw new BusinessException(ErrorCode.GATEWAY_ERROR, msg); } throw new BusinessException(ErrorCode.GATEWAY_ERROR, "Failed to call ADP gateway API"); } catch (Exception e) { log.error("Error fetching ADP gateways", e); throw new BusinessException(ErrorCode.INTERNAL_ERROR, e.getMessage()); } finally { client.close(); } } private PageResult<GatewayResult> convertToGatewayResult(AdpGatewayInstanceResult.AdpGatewayInstanceData data, int page, int size) { List<GatewayResult> gateways = new ArrayList<>(); DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); if (data.getRecords() != null) { for (AdpGatewayInstanceResult.AdpGatewayInstance instance : data.getRecords()) { LocalDateTime createTime = null; try { if (instance.getCreateTime() != null) { createTime = LocalDateTime.parse(instance.getCreateTime(), formatter); } } catch (Exception e) { log.warn("Failed to parse create time: {}", instance.getCreateTime(), e); } GatewayResult gateway = GatewayResult.builder() .gatewayId(instance.getGwInstanceId()) .gatewayName(instance.getName()) .gatewayType(GatewayType.ADP_AI_GATEWAY) .createAt(createTime) .build(); gateways.add(gateway); } } return PageResult.of(gateways, page, size, data.getTotal() != null ? data.getTotal() : 0); } @Data public static class AdpMcpServerDetailResult { private Integer code; private String msg; private String message; private AdpMcpServerDetail data; @Data public static class AdpMcpServerDetail { private String gwInstanceId; private String name; private String description; private List<String> domains; private List<Service> services; private ConsumerAuthInfo consumerAuthInfo; private String rawConfigurations; private String type; private String dsn; private String dbType; private String upstreamPathPrefix; @Data public static class Service { private String name; private Integer port; private String version; private Integer weight; } @Data public static class ConsumerAuthInfo { private String type; private Boolean enable; private List<String> allowedConsumers; } } } } ```