From 4d200b6faa3b0bcb119c32f4ff7dc216e4921c90 Mon Sep 17 00:00:00 2001 From: Haitao Pan Date: Thu, 9 Apr 2026 09:30:54 +0800 Subject: [PATCH] Hide experimental gateway modes by default --- config/feature_flags.yaml | 36 ++ ...ettings-integration-configuration-model.md | 50 +++ lib/app/ui_feature_manifest_core.dart | 10 + lib/app/ui_feature_manifest_fallback.dart | 36 ++ .../settings/settings_page_gateway.dart | 389 ++++++++++-------- .../settings/settings_page_gateway_acp.dart | 158 ++++--- test/features/ai_gateway_page_suite.dart | 13 +- test/features/secrets_page_suite.dart | 14 +- .../settings_page_acp_bridge_mode_suite.dart | 22 +- test/features/settings_page_suite.dart | 245 +++++++---- 10 files changed, 646 insertions(+), 327 deletions(-) create mode 100644 docs/architecture/settings-integration-configuration-model.md diff --git a/config/feature_flags.yaml b/config/feature_flags.yaml index 0a94e944..38d45966 100644 --- a/config/feature_flags.yaml +++ b/config/feature_flags.yaml @@ -152,6 +152,18 @@ mobile: build_modes: [debug, profile, release] description: Mobile Vault server integration section ui_surface: settings_page + gateway_self_hosted_base: + enabled: false + release_tier: experimental + build_modes: [debug, profile, release] + description: Mobile self-hosted base connection controls + ui_surface: settings_page + gateway_advanced_custom_mode: + enabled: false + release_tier: experimental + build_modes: [debug, profile, release] + description: Mobile advanced custom override mode + ui_surface: settings_page gateway_setup_code: enabled: false release_tier: experimental @@ -356,6 +368,18 @@ desktop: build_modes: [debug, profile, release] description: Desktop Vault server integration section ui_surface: settings_page + gateway_self_hosted_base: + enabled: false + release_tier: experimental + build_modes: [debug, profile, release] + description: Desktop self-hosted base connection controls + ui_surface: settings_page + gateway_advanced_custom_mode: + enabled: false + release_tier: experimental + build_modes: [debug, profile, release] + description: Desktop advanced custom override mode + ui_surface: settings_page gateway_setup_code: enabled: false release_tier: experimental @@ -517,6 +541,18 @@ web: build_modes: [] description: Web does not expose vault server integration ui_surface: web_settings_page + gateway_self_hosted_base: + enabled: false + release_tier: experimental + build_modes: [] + description: Web does not expose self-hosted base connection controls + ui_surface: web_settings_page + gateway_advanced_custom_mode: + enabled: false + release_tier: experimental + build_modes: [] + description: Web does not expose advanced custom override mode + ui_surface: web_settings_page gateway_setup_code: enabled: false release_tier: experimental diff --git a/docs/architecture/settings-integration-configuration-model.md b/docs/architecture/settings-integration-configuration-model.md new file mode 100644 index 00000000..eb6b2db7 --- /dev/null +++ b/docs/architecture/settings-integration-configuration-model.md @@ -0,0 +1,50 @@ +# Settings Integration Configuration Model + +This document records the logical model behind the Settings -> Integrations page. + +The page is organized into three layers: + +- User login state +- Base connection configuration +- Advanced custom mode + +The base connection layer is the default configuration surface. It represents the connection identity that can come from either `svc.plus` or a self-hosted service. Advanced custom mode does not replace the base layer; it overrides selected defaults on top of it. + +```mermaid +flowchart TD + A[Settings Integrations Page] --> B[User Login State] + A --> C[Base Connection Configuration] + A --> D[Advanced Custom Mode] + + B --> B1[Signed out] + B --> B2[Signed in] + B --> B3[MFA pending] + B --> B4[Signing in] + + C --> C1[Account / Email] + C --> C2[Password] + C --> C3[Service URL] + C --> C4[User] + C --> C5[Sync] + C --> C6[Default connection source] + C6 --> C7[svc.plus provided] + C6 --> C8[Self-hosted] + + D --> D1[Override OpenClaw Gateway] + D --> D2[Override Vault Server] + D --> D3[Override LLM Endpoint] + D --> D4[Override External ACP Server endpoint] + D --> D5[Override SKILLS directories] + + B2 --> C + C --> D + D --> E[Final effective configuration] +``` + +## Notes + +- User login state describes authentication only. +- Base connection configuration describes the default connection path and identity. +- Advanced custom mode is a layered override mechanism. +- The effective runtime configuration is computed from the base layer plus any advanced overrides. + diff --git a/lib/app/ui_feature_manifest_core.dart b/lib/app/ui_feature_manifest_core.dart index 2a4f7b3d..0eefbe11 100644 --- a/lib/app/ui_feature_manifest_core.dart +++ b/lib/app/ui_feature_manifest_core.dart @@ -70,6 +70,10 @@ abstract final class UiFeatureKeys { static const settingsGateway = 'settings.gateway'; static const settingsAccountAccess = 'settings.account_access'; static const settingsVaultServer = 'settings.vault_server'; + static const settingsGatewaySelfHostedBase = + 'settings.gateway_self_hosted_base'; + static const settingsGatewayAdvancedCustomMode = + 'settings.gateway_advanced_custom_mode'; static const settingsGatewaySetupCode = 'settings.gateway_setup_code'; static const settingsAgents = 'settings.agents'; static const settingsAppearance = 'settings.appearance'; @@ -494,6 +498,12 @@ class UiFeatureAccess { bool get supportsVaultServer => isEnabledPath(UiFeatureKeys.settingsVaultServer); + bool get supportsGatewaySelfHostedBase => + isEnabledPath(UiFeatureKeys.settingsGatewaySelfHostedBase); + + bool get supportsGatewayAdvancedCustomMode => + isEnabledPath(UiFeatureKeys.settingsGatewayAdvancedCustomMode); + List get availableSettingsTabs { return SettingsTab.values .where( diff --git a/lib/app/ui_feature_manifest_fallback.dart b/lib/app/ui_feature_manifest_fallback.dart index 94f9f983..58898133 100644 --- a/lib/app/ui_feature_manifest_fallback.dart +++ b/lib/app/ui_feature_manifest_fallback.dart @@ -163,6 +163,18 @@ mobile: build_modes: [debug, profile, release] description: Mobile Vault server integration section ui_surface: settings_page + gateway_self_hosted_base: + enabled: false + release_tier: experimental + build_modes: [debug, profile, release] + description: Mobile self-hosted base connection controls + ui_surface: settings_page + gateway_advanced_custom_mode: + enabled: false + release_tier: experimental + build_modes: [debug, profile, release] + description: Mobile advanced custom override mode + ui_surface: settings_page gateway_setup_code: enabled: true release_tier: experimental @@ -367,6 +379,18 @@ desktop: build_modes: [debug, profile, release] description: Desktop Vault server integration section ui_surface: settings_page + gateway_self_hosted_base: + enabled: false + release_tier: experimental + build_modes: [debug, profile, release] + description: Desktop self-hosted base connection controls + ui_surface: settings_page + gateway_advanced_custom_mode: + enabled: false + release_tier: experimental + build_modes: [debug, profile, release] + description: Desktop advanced custom override mode + ui_surface: settings_page gateway_setup_code: enabled: false release_tier: experimental @@ -528,6 +552,18 @@ web: build_modes: [] description: Web does not expose vault server integration ui_surface: web_settings_page + gateway_self_hosted_base: + enabled: false + release_tier: experimental + build_modes: [] + description: Web does not expose self-hosted base connection controls + ui_surface: web_settings_page + gateway_advanced_custom_mode: + enabled: false + release_tier: experimental + build_modes: [] + description: Web does not expose advanced custom override mode + ui_surface: web_settings_page gateway_setup_code: enabled: false release_tier: experimental diff --git a/lib/features/settings/settings_page_gateway.dart b/lib/features/settings/settings_page_gateway.dart index fa187c18..7dadd61c 100644 --- a/lib/features/settings/settings_page_gateway.dart +++ b/lib/features/settings/settings_page_gateway.dart @@ -36,197 +36,210 @@ extension SettingsPageGatewayMixinInternal on SettingsPageStateInternal { UiFeatureAccess uiFeatures, ) { if (!widget.showSectionTabs) { - return buildUnifiedGatewaySectionsInternal( + return [ + buildGatewayOverviewCardInternal( + context, + controller, + settings, + uiFeatures, + ), + const SizedBox(height: 16), + buildOnlineAccountCardInternal(context, controller, settings), + const SizedBox(height: 16), + buildAcpBridgeServerModeCardInternal( + context, + controller, + settings, + uiFeatures: uiFeatures, + ), + if (uiFeatures.supportsGatewayAdvancedCustomMode) ...[ + const SizedBox(height: 16), + ...buildGatewayAdvancedSectionsInternal( + context, + controller, + settings, + uiFeatures, + ), + ], + ]; + } + final selectedSubTab = + !uiFeatures.supportsGatewayAdvancedCustomMode && + integrationSubTabInternal == GatewayIntegrationSubTabInternal.advancedConfig + ? GatewayIntegrationSubTabInternal.vault + : integrationSubTabInternal; + final effectiveTabLabel = switch (selectedSubTab) { + GatewayIntegrationSubTabInternal.gateway => appText( + '用户登录状态', + 'User Login State', + ), + GatewayIntegrationSubTabInternal.vault => appText( + '基础连接配置', + 'Base Connection Configuration', + ), + GatewayIntegrationSubTabInternal.llm => appText( + '高级自定义模式', + 'Advanced Custom Mode', + ), + GatewayIntegrationSubTabInternal.acp => appText( + '高级自定义模式', + 'Advanced Custom Mode', + ), + GatewayIntegrationSubTabInternal.skills => appText( + '高级自定义模式', + 'Advanced Custom Mode', + ), + GatewayIntegrationSubTabInternal.advancedConfig => appText( + '高级自定义模式', + 'Advanced Custom Mode', + ), + }; + return [ + buildGatewayOverviewCardInternal( context, controller, settings, uiFeatures, - ); - } - final advancedEditable = - settings.acpBridgeServerModeConfig.mode == - AcpBridgeServerMode.advancedCustom; - final tabLabel = switch (integrationSubTabInternal) { - GatewayIntegrationSubTabInternal.gateway => 'OpenClaw Gateway', - GatewayIntegrationSubTabInternal.vault => appText( - 'Vault Server', - 'Vault Server', ), - GatewayIntegrationSubTabInternal.llm => appText( - 'LLM 接入点', - 'LLM Endpoints', - ), - GatewayIntegrationSubTabInternal.acp => appText( - 'ACP 外部接入', - 'External ACP', - ), - GatewayIntegrationSubTabInternal.skills => appText( - 'SKILLS 目录授权', - 'SKILLS Directory Authorization', - ), - GatewayIntegrationSubTabInternal.advancedConfig => appText( - '高级自定义配置', - 'Advanced Custom Configuration', - ), - }; - return [ + const SizedBox(height: 16), SectionTabs( items: [ - 'OpenClaw Gateway', - appText('Vault Server', 'Vault Server'), - appText('LLM 接入点', 'LLM Endpoints'), - appText('ACP 外部接入', 'External ACP'), - appText('SKILLS 目录授权', 'SKILLS Directory Authorization'), - appText('高级自定义配置', 'Advanced Custom Configuration'), + appText('用户登录状态', 'User Login State'), + appText('基础连接配置', 'Base Connection Configuration'), + if (uiFeatures.supportsGatewayAdvancedCustomMode) + appText('高级自定义模式', 'Advanced Custom Mode'), ], - value: tabLabel, + value: effectiveTabLabel, onChanged: (value) => setStateInternal(() { integrationSubTabInternal = switch (value) { - 'OpenClaw Gateway' => GatewayIntegrationSubTabInternal.gateway, - _ when value == appText('Vault Server', 'Vault Server') => - GatewayIntegrationSubTabInternal.vault, - _ when value == appText('LLM 接入点', 'LLM Endpoints') => - GatewayIntegrationSubTabInternal.llm, - _ when value == appText('ACP 外部接入', 'External ACP') => - GatewayIntegrationSubTabInternal.acp, + _ when value == appText('用户登录状态', 'User Login State') => + GatewayIntegrationSubTabInternal.gateway, _ when value == - appText('SKILLS 目录授权', 'SKILLS Directory Authorization') => - GatewayIntegrationSubTabInternal.skills, + appText( + '基础连接配置', + 'Base Connection Configuration', + ) => + GatewayIntegrationSubTabInternal.vault, + _ + when value == + appText('高级自定义模式', 'Advanced Custom Mode') => + GatewayIntegrationSubTabInternal.advancedConfig, _ => GatewayIntegrationSubTabInternal.advancedConfig, }; }), ), const SizedBox(height: 16), - ...switch (integrationSubTabInternal) { + ...switch (selectedSubTab) { GatewayIntegrationSubTabInternal.gateway => [ - Opacity( - opacity: advancedEditable ? 1 : 0.72, - child: IgnorePointer( - ignoring: !advancedEditable, - child: buildCollapsibleGatewaySectionInternal( - context: context, - title: 'OpenClaw Gateway', - expanded: openClawGatewayExpandedInternal, - onChanged: (value) => setStateInternal(() { - openClawGatewayExpandedInternal = value; - }), - child: buildOpenClawGatewayCardInternal( - context, - controller, - settings, - ), - ), - ), - ), + buildOnlineAccountCardInternal(context, controller, settings), ], GatewayIntegrationSubTabInternal.vault => [ - if (uiFeatures.supportsVaultServer) - Opacity( - opacity: advancedEditable ? 1 : 0.72, - child: IgnorePointer( - ignoring: !advancedEditable, - child: buildCollapsibleGatewaySectionInternal( - context: context, - title: appText('Vault Server', 'Vault Server'), - expanded: vaultServerExpandedInternal, - onChanged: (value) => setStateInternal(() { - vaultServerExpandedInternal = value; - }), - child: buildVaultProviderCardInternal( - context, - controller, - settings, - ), - ), - ), - ) - else - SurfaceCard( - borderWidth: settingsHairlineBorderWidthInternal, - child: Text( - appText( - '当前发布配置未开放 Vault Server 参数。', - 'Vault Server settings are disabled in this release configuration.', - ), - ), - ), - ], - GatewayIntegrationSubTabInternal.llm => [ - Opacity( - opacity: advancedEditable ? 1 : 0.72, - child: IgnorePointer( - ignoring: !advancedEditable, - child: buildCollapsibleGatewaySectionInternal( - context: context, - title: appText('LLM 接入点', 'LLM Endpoints'), - expanded: aiGatewayExpandedInternal, - onChanged: (value) => setStateInternal(() { - aiGatewayExpandedInternal = value; - }), - child: buildLlmEndpointManagerInternal( - context, - controller, - settings, - ), - ), - ), + buildAcpBridgeServerModeCardInternal( + context, + controller, + settings, + uiFeatures: uiFeatures, ), ], - GatewayIntegrationSubTabInternal.acp => [ - Opacity( - opacity: advancedEditable ? 1 : 0.72, - child: IgnorePointer( - ignoring: !advancedEditable, - child: buildCollapsibleGatewaySectionInternal( - context: context, - title: appText( - '外部 ACP Server Endpoint', - 'External ACP Server Endpoints', - ), - expanded: externalAcpExpandedInternal, - onChanged: (value) => setStateInternal(() { - externalAcpExpandedInternal = value; - }), - child: buildExternalAcpEndpointManagerInternal( - context, - controller, - settings, - ), + GatewayIntegrationSubTabInternal.llm => const [], + GatewayIntegrationSubTabInternal.acp => const [], + GatewayIntegrationSubTabInternal.skills => const [], + GatewayIntegrationSubTabInternal.advancedConfig => + uiFeatures.supportsGatewayAdvancedCustomMode + ? [ + ...buildGatewayAdvancedSectionsInternal( + context, + controller, + settings, + uiFeatures, ), - ), - ), - ], - GatewayIntegrationSubTabInternal.skills => [ - Opacity( - opacity: advancedEditable ? 1 : 0.72, - child: IgnorePointer( - ignoring: !advancedEditable, - child: buildCollapsibleGatewaySectionInternal( - context: context, - title: appText('SKILLS 目录授权', 'SKILLS Directory Authorization'), - expanded: skillsDirectoryAuthorizationExpandedInternal, - onChanged: (value) => setStateInternal(() { - skillsDirectoryAuthorizationExpandedInternal = value; - }), - child: SkillDirectoryAuthorizationCard( - controller: controller, - showHeader: false, - ), - ), - ), - ), - ], - GatewayIntegrationSubTabInternal.advancedConfig => [ - buildOnlineAccountCardInternal(context, controller, settings), - const SizedBox(height: 16), - buildAcpBridgeServerModeCardInternal(context, controller, settings), - ], + ] + : [], }, ]; } - List buildUnifiedGatewaySectionsInternal( + Widget buildGatewayOverviewCardInternal( + BuildContext context, + AppController controller, + SettingsSnapshot settings, + UiFeatureAccess uiFeatures, + ) { + final accountController = controller.settingsController; + final modeConfig = settings.acpBridgeServerModeConfig; + final supportsSelfHosted = uiFeatures.supportsGatewaySelfHostedBase; + final supportsAdvancedOverrides = + uiFeatures.supportsGatewayAdvancedCustomMode; + final loginStatus = accountController.accountMfaRequired + ? appText('MFA 待验证', 'MFA pending') + : accountController.accountBusy + ? appText('登录中', 'Signing in') + : accountController.accountSignedIn + ? appText('已登录', 'Signed in') + : appText('未登录', 'Signed out'); + final defaultSource = supportsSelfHosted && modeConfig.usesSelfHostedBase + ? appText('自建服务', 'Self-hosted') + : appText('svc.plus 提供', 'svc.plus provided'); + final hasAdvancedOverrides = + supportsAdvancedOverrides && + modeConfig.mode == AcpBridgeServerMode.advancedCustom; + return SurfaceCard( + key: const ValueKey('gateway-configuration-overview-card'), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + appText('最终生效配置概览', 'Effective Configuration Overview'), + style: Theme.of(context).textTheme.titleLarge, + ), + const SizedBox(height: 8), + Text( + supportsAdvancedOverrides + ? appText( + '先确认登录状态,再确定默认连接来源,最后按需用高级自定义模式覆盖默认配置。', + 'Confirm login state first, choose the default connection source second, then apply advanced custom overrides only where needed.', + ) + : appText( + '先确认登录状态,再查看当前默认连接来源。', + 'Confirm login state first, then review the current default connection source.', + ), + ), + const SizedBox(height: 16), + Wrap( + spacing: 12, + runSpacing: 12, + children: [ + StatusChipInternal( + key: const ValueKey('gateway-overview-login-status'), + label: '${appText('登录状态', 'Login')}: $loginStatus', + tone: accountController.accountSignedIn + ? StatusChipToneInternal.ready + : StatusChipToneInternal.idle, + ), + StatusChipInternal( + key: const ValueKey('gateway-overview-default-source'), + label: + '${appText('默认连接来源', 'Default Source')}: $defaultSource', + tone: StatusChipToneInternal.ready, + ), + if (supportsAdvancedOverrides) + StatusChipInternal( + key: const ValueKey('gateway-overview-advanced-override'), + label: + '${appText('高级覆盖', 'Advanced Override')}: ${hasAdvancedOverrides ? appText('已启用', 'Enabled') : appText('未启用', 'Disabled')}', + tone: hasAdvancedOverrides + ? StatusChipToneInternal.ready + : StatusChipToneInternal.idle, + ), + ], + ), + ], + ), + ); + } + + List buildGatewayAdvancedSectionsInternal( BuildContext context, AppController controller, SettingsSnapshot settings, @@ -235,7 +248,38 @@ extension SettingsPageGatewayMixinInternal on SettingsPageStateInternal { final advancedEditable = settings.acpBridgeServerModeConfig.mode == AcpBridgeServerMode.advancedCustom; - return [ + final sections = [ + SurfaceCard( + key: const ValueKey('gateway-advanced-override-intro'), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + appText('高级自定义模式', 'Advanced Custom Mode'), + style: Theme.of(context).textTheme.titleLarge, + ), + const SizedBox(height: 8), + Text( + appText( + '这里的配置只负责覆盖默认配置,不会把基础连接配置替换成另一套平行模式。未覆盖的字段继续继承当前默认连接来源。', + 'These settings only override the default configuration. They do not replace the base connection model with a parallel mode. Any field you do not override keeps inheriting from the current default source.', + ), + ), + const SizedBox(height: 12), + FilledButton.tonal( + key: const ValueKey('acp-bridge-advanced-reset'), + onPressed: advancedEditable + ? () => resetAcpBridgeServerAdvancedOverridesInternal( + controller, + settings, + ) + : null, + child: Text(appText('清空高级覆盖', 'Clear Advanced Overrides')), + ), + ], + ), + ), + const SizedBox(height: 16), Opacity( opacity: advancedEditable ? 1 : 0.72, child: IgnorePointer( @@ -349,6 +393,21 @@ extension SettingsPageGatewayMixinInternal on SettingsPageStateInternal { ), ), ]; + return sections; + } + + List buildUnifiedGatewaySectionsInternal( + BuildContext context, + AppController controller, + SettingsSnapshot settings, + UiFeatureAccess uiFeatures, + ) { + return buildGatewayAdvancedSectionsInternal( + context, + controller, + settings, + uiFeatures, + ); } Widget buildLlmEndpointManagerInternal( diff --git a/lib/features/settings/settings_page_gateway_acp.dart b/lib/features/settings/settings_page_gateway_acp.dart index b094cb8d..5d1fd000 100644 --- a/lib/features/settings/settings_page_gateway_acp.dart +++ b/lib/features/settings/settings_page_gateway_acp.dart @@ -6,6 +6,7 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import '../../app/app_controller.dart'; +import '../../app/ui_feature_manifest.dart'; import '../../i18n/app_language.dart'; import '../../runtime/gateway_acp_client.dart'; import '../../runtime/runtime_controllers.dart'; @@ -234,7 +235,7 @@ extension SettingsPageGatewayAcpMixinInternal on SettingsPageStateInternal { ? accountSession!.email.trim() : accountSession?.name.trim().isNotEmpty == true ? accountSession!.name.trim() - : appText('在线账户', 'Online Account'); + : appText('用户登录状态', 'User Login State'); final sessionStatusText = accountSignedIn ? appText('已登录:$signedInLabel', 'Signed in: $signedInLabel') : accountMfaRequired @@ -261,15 +262,15 @@ extension SettingsPageGatewayAcpMixinInternal on SettingsPageStateInternal { ), const SizedBox(height: 16), Text( - appText('在线账户', 'Online Account'), + appText('用户登录状态', 'User Login State'), style: theme.textTheme.headlineMedium, textAlign: TextAlign.center, ), const SizedBox(height: 10), Text( appText( - '请先登录 ACP Bridge Server', - 'Please sign in to ACP Bridge Server', + '先完成账户登录,再同步或校验默认连接配置。', + 'Sign in first, then sync or verify the default connection configuration.', ), style: theme.textTheme.titleMedium?.copyWith( color: theme.textTheme.bodyMedium?.color?.withValues( @@ -343,8 +344,8 @@ extension SettingsPageGatewayAcpMixinInternal on SettingsPageStateInternal { const SizedBox(height: 8), Text( appText( - '这里继续只负责在线账户身份、MFA、工作区与同步摘要。ACP Bridge Server 的本地连接与高级配置在下面统一收口。', - 'This card focuses on online account identity, MFA, workspace, and sync summary. Local ACP Bridge Server connection and advanced config are unified below.', + '这里仅描述认证状态本身:登录、MFA、同步状态与当前账户身份。默认连接来源和高级覆盖在下面分别配置。', + 'This card describes authentication only: sign-in, MFA, sync state, and current account identity. The default connection source and advanced overrides are configured below.', ), ), const SizedBox(height: 16), @@ -371,7 +372,7 @@ extension SettingsPageGatewayAcpMixinInternal on SettingsPageStateInternal { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - appText('在线账户同步摘要', 'Online account sync summary'), + appText('登录状态摘要', 'Login state summary'), style: Theme.of(context).textTheme.titleSmall, ), const SizedBox(height: 8), @@ -380,7 +381,7 @@ extension SettingsPageGatewayAcpMixinInternal on SettingsPageStateInternal { ), const SizedBox(height: 6), Text( - '${appText('在线账户', 'Online Account')}: ${settings.accountUsername.trim().isEmpty ? appText('未填写', 'Not set') : settings.accountUsername}', + '${appText('账户标识', 'Account')}: ${settings.accountUsername.trim().isEmpty ? appText('未填写', 'Not set') : settings.accountUsername}', ), const SizedBox(height: 6), Text( @@ -446,21 +447,24 @@ extension SettingsPageGatewayAcpMixinInternal on SettingsPageStateInternal { Widget buildAcpBridgeServerModeCardInternal( BuildContext context, AppController controller, - SettingsSnapshot settings, - ) { + SettingsSnapshot settings, { + required UiFeatureAccess uiFeatures, + }) { syncAcpBridgeServerModeDraftControllersInternal(settings); final modeConfig = settings.acpBridgeServerModeConfig; + final supportsSelfHosted = uiFeatures.supportsGatewaySelfHostedBase; + final effectiveUsesSelfHosted = + supportsSelfHosted && modeConfig.usesSelfHostedBase; + final effectiveUsesCloudSync = !effectiveUsesSelfHosted; final accountController = controller.settingsController; final accountSyncState = accountController.accountSyncState; final accountSignedIn = accountController.accountSignedIn; final accountBusy = accountController.accountBusy; final cloudSync = modeConfig.cloudSynced; final remoteSummary = cloudSync.remoteServerSummary; - final currentSource = switch (modeConfig.sourceTag) { - 'cloudSynced' => appText('在线账户', 'Online Account'), - 'selfHosted' => appText('本地账户', 'Local Account'), - _ => appText('高级模式', 'Advanced Mode'), - }; + final currentSource = effectiveUsesSelfHosted + ? appText('自建服务', 'Self-hosted') + : appText('svc.plus 提供', 'svc.plus provided'); final syncStatus = accountSyncState?.syncState.trim().isNotEmpty == true ? accountSyncState!.syncState : appText('未同步', 'Not synced'); @@ -475,17 +479,22 @@ extension SettingsPageGatewayAcpMixinInternal on SettingsPageStateInternal { children: [ Text( appText( - 'ACP Bridge Server 连接模式', - 'ACP Bridge Server Connection Mode', + '基础连接配置', + 'Base Connection Configuration', ), style: Theme.of(context).textTheme.titleLarge, ), const SizedBox(height: 8), Text( - appText( - '在线账户负责云端同步,本地账户负责连接 ACP Bridge Server,高级模式是在本地账户基础上再叠加 advanced config 覆盖层。App 只负责配置、会话、安全存储与连接编排,不承载服务端逻辑。', - 'Online account handles cloud sync, local account connects to ACP Bridge Server, and advanced mode layers advanced config on top of the local account. The app stays a pure client for configuration, session handling, secure storage, and connection orchestration.', - ), + supportsSelfHosted + ? appText( + '这里维护默认连接来源与默认凭据。默认来源可以是 svc.plus 提供的托管配置,也可以是自建 ACP Bridge Server。高级自定义模式只在这层默认配置上做覆盖。', + 'This section maintains the default connection source and default credentials. The default source can come from svc.plus managed configuration or from a self-hosted ACP Bridge Server. Advanced custom mode only overrides this base layer.', + ) + : appText( + '这里维护默认连接来源与默认凭据。当前默认 UI 仅展示 svc.plus 提供的托管配置入口;实验性的自建与高级覆盖能力保留在代码模块中。', + 'This section maintains the default connection source and default credentials. The default UI currently exposes only the svc.plus managed entry point, while experimental self-hosted and advanced override capabilities remain in the codebase.', + ), ), const SizedBox(height: 16), Wrap( @@ -494,8 +503,8 @@ extension SettingsPageGatewayAcpMixinInternal on SettingsPageStateInternal { children: [ ChoiceChip( key: const ValueKey('acp-bridge-mode-cloud'), - label: Text(appText('在线账户', 'Online Account')), - selected: modeConfig.mode == AcpBridgeServerMode.cloudSynced, + label: Text(appText('svc.plus 提供', 'svc.plus provided')), + selected: effectiveUsesCloudSync, onSelected: (_) => saveSettingsInternal( controller, settings.copyWith( @@ -506,34 +515,21 @@ extension SettingsPageGatewayAcpMixinInternal on SettingsPageStateInternal { ), ), ), - ChoiceChip( - key: const ValueKey('acp-bridge-mode-self-hosted'), - label: Text(appText('本地账户', 'Local Account')), - selected: modeConfig.mode == AcpBridgeServerMode.selfHosted, - onSelected: (_) => saveSettingsInternal( - controller, - settings.copyWith( - accountLocalMode: true, - acpBridgeServerModeConfig: modeConfig.copyWith( - mode: AcpBridgeServerMode.selfHosted, + if (supportsSelfHosted) + ChoiceChip( + key: const ValueKey('acp-bridge-mode-self-hosted'), + label: Text(appText('自建服务', 'Self-hosted')), + selected: effectiveUsesSelfHosted, + onSelected: (_) => saveSettingsInternal( + controller, + settings.copyWith( + accountLocalMode: true, + acpBridgeServerModeConfig: modeConfig.copyWith( + mode: AcpBridgeServerMode.selfHosted, + ), ), ), ), - ), - ChoiceChip( - key: const ValueKey('acp-bridge-mode-advanced'), - label: Text(appText('高级模式', 'Advanced Mode')), - selected: modeConfig.mode == AcpBridgeServerMode.advancedCustom, - onSelected: (_) => saveSettingsInternal( - controller, - settings.captureAcpBridgeServerAdvancedOverrides().copyWith( - acpBridgeServerModeConfig: settings - .captureAcpBridgeServerAdvancedOverrides() - .acpBridgeServerModeConfig - .copyWith(mode: AcpBridgeServerMode.advancedCustom), - ), - ), - ), ], ), const SizedBox(height: 16), @@ -542,7 +538,8 @@ extension SettingsPageGatewayAcpMixinInternal on SettingsPageStateInternal { runSpacing: 12, children: [ StatusChipInternal( - label: '${appText('当前模式', 'Mode')}: $currentSource', + label: + '${appText('默认连接来源', 'Default Source')}: $currentSource', tone: StatusChipToneInternal.ready, ), StatusChipInternal( @@ -551,20 +548,28 @@ extension SettingsPageGatewayAcpMixinInternal on SettingsPageStateInternal { ? StatusChipToneInternal.ready : StatusChipToneInternal.idle, ), + if (uiFeatures.supportsGatewayAdvancedCustomMode) + StatusChipInternal( + label: + '${appText('高级覆盖', 'Advanced Override')}: ${modeConfig.mode == AcpBridgeServerMode.advancedCustom ? appText('已启用', 'Enabled') : appText('未启用', 'Disabled')}', + tone: modeConfig.mode == AcpBridgeServerMode.advancedCustom + ? StatusChipToneInternal.ready + : StatusChipToneInternal.idle, + ), ], ), const SizedBox(height: 16), - ...switch (modeConfig.mode) { - AcpBridgeServerMode.cloudSynced => [ + ...switch (effectiveUsesCloudSync) { + true => [ Text( accountSignedIn ? appText( - '已登录在线账户,可直接同步云端 ACP Bridge Server 默认配置。', - 'Signed in to the online account. You can sync the cloud ACP Bridge Server defaults directly.', + '当前默认来源为 svc.plus 提供的托管配置。你可以直接同步远端默认配置。', + 'The current default source is managed by svc.plus. You can sync the remote defaults directly.', ) : appText( - '当前未登录在线账户。建议先登录,再从云端同步默认配置。', - 'No online account is signed in. Sign in first, then sync the default configuration from the cloud.', + '当前默认来源为 svc.plus 提供的托管配置,但你还没有登录。建议先完成登录,再同步默认配置。', + 'The current default source is managed by svc.plus, but no account is signed in yet. Sign in first, then sync the default configuration.', ), ), Text( @@ -576,9 +581,10 @@ extension SettingsPageGatewayAcpMixinInternal on SettingsPageStateInternal { key: const ValueKey('acp-bridge-cloud-last-sync'), ), const SizedBox(height: 6), - Text( - '${appText('高级覆盖', 'Advanced Override')}: ${remoteSummary.hasAdvancedOverrides ? appText('存在', 'Present') : appText('无', 'None')}', - ), + if (uiFeatures.supportsGatewayAdvancedCustomMode) + Text( + '${appText('高级覆盖', 'Advanced Override')}: ${remoteSummary.hasAdvancedOverrides ? appText('存在', 'Present') : appText('无', 'None')}', + ), const SizedBox(height: 14), Wrap( spacing: 10, @@ -601,34 +607,26 @@ extension SettingsPageGatewayAcpMixinInternal on SettingsPageStateInternal { ], ), ], - AcpBridgeServerMode.selfHosted => [], - AcpBridgeServerMode.advancedCustom => [ + false => [ Text( appText( - '高级模式 = 本地账户 + advanced config。下面先保留本地 ACP Bridge Server 连接,再把 OpenClaw Gateway / Vault Server / LLM Endpoint / 外部 ACP Server endpoint / SKILLS 目录 当作覆盖层。未覆盖的值继续继承当前基础模式。', - 'Advanced mode = local account + advanced config. Keep the local ACP Bridge Server connection below, then treat the OpenClaw Gateway / Vault Server / LLM Endpoint / external ACP server endpoint / SKILLS directory as overrides. Fields you do not override keep inheriting from the current base mode.', + '当前默认来源为自建服务。下面的 ACP Bridge Server URL、用户和密码会作为默认连接配置使用;如果启用了高级自定义模式,其它集成项只会覆盖这份默认配置。', + 'The current default source is self-hosted. The ACP Bridge Server URL, username, and password below act as the default connection configuration. If advanced custom mode is enabled, the other integrations only override this base layer.', ), ), - const SizedBox(height: 12), - FilledButton.tonal( - key: const ValueKey('acp-bridge-advanced-reset'), - onPressed: () => resetAcpBridgeServerAdvancedOverridesInternal( - controller, - settings, - ), - child: Text(appText('清空高级覆盖', 'Clear Advanced Overrides')), - ), ], }, - const SizedBox(height: 16), - buildAcpBridgeServerSelfHostedPanelInternal( - context, - controller, - settings, - targetMode: modeConfig.mode == AcpBridgeServerMode.advancedCustom - ? AcpBridgeServerMode.advancedCustom - : AcpBridgeServerMode.selfHosted, - ), + if (supportsSelfHosted) ...[ + const SizedBox(height: 16), + buildAcpBridgeServerSelfHostedPanelInternal( + context, + controller, + settings, + targetMode: modeConfig.mode == AcpBridgeServerMode.advancedCustom + ? AcpBridgeServerMode.advancedCustom + : AcpBridgeServerMode.selfHosted, + ), + ], ], ), ); diff --git a/test/features/ai_gateway_page_suite.dart b/test/features/ai_gateway_page_suite.dart index 8bac5932..037329dc 100644 --- a/test/features/ai_gateway_page_suite.dart +++ b/test/features/ai_gateway_page_suite.dart @@ -91,8 +91,17 @@ void main() { ), ); - expect(find.text('OpenClaw Gateway'), findsWidgets); - expect(find.text('LLM 接入点'), findsWidgets); + expect(find.text('用户登录状态'), findsWidgets); + expect(find.text('基础连接配置'), findsWidgets); + expect(find.text('高级自定义模式'), findsNothing); + expect( + find.byKey(const ValueKey('gateway-configuration-overview-card')), + findsOneWidget, + ); + expect( + find.byKey(const ValueKey('gateway-overview-advanced-override')), + findsNothing, + ); }); testWidgets( diff --git a/test/features/secrets_page_suite.dart b/test/features/secrets_page_suite.dart index 48d5ea73..c7e20099 100644 --- a/test/features/secrets_page_suite.dart +++ b/test/features/secrets_page_suite.dart @@ -2,6 +2,7 @@ library; import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter/material.dart'; import 'package:xworkmate/features/settings/settings_page.dart'; import 'package:xworkmate/models/app_models.dart'; @@ -27,7 +28,16 @@ void main() { ), ); - expect(find.text('OpenClaw Gateway'), findsWidgets); - expect(find.text('Vault Server'), findsOneWidget); + expect(find.text('用户登录状态'), findsWidgets); + expect(find.text('基础连接配置'), findsWidgets); + expect(find.text('高级自定义模式'), findsNothing); + expect( + find.byKey(const ValueKey('gateway-configuration-overview-card')), + findsOneWidget, + ); + expect( + find.byKey(const ValueKey('gateway-overview-advanced-override')), + findsNothing, + ); }); } diff --git a/test/features/settings_page_acp_bridge_mode_suite.dart b/test/features/settings_page_acp_bridge_mode_suite.dart index bcb03f3f..90fcc51c 100644 --- a/test/features/settings_page_acp_bridge_mode_suite.dart +++ b/test/features/settings_page_acp_bridge_mode_suite.dart @@ -3,6 +3,7 @@ library; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:xworkmate/app/ui_feature_manifest.dart'; import 'package:xworkmate/features/settings/settings_page_core.dart'; import 'package:xworkmate/models/app_models.dart'; @@ -10,9 +11,19 @@ import '../test_support.dart'; void main() { testWidgets( - 'SettingsPage shows ACP bridge server mode card in advanced custom config', + 'SettingsPage shows base connection card when self-hosted base is enabled', (WidgetTester tester) async { - final controller = await createTestController(tester); + final manifest = UiFeatureManifest.fallback().copyWithFeature( + platform: UiFeaturePlatform.desktop, + module: 'settings', + feature: 'gateway_self_hosted_base', + enabled: true, + releaseTier: UiFeatureReleaseTier.experimental, + ); + final controller = await createTestController( + tester, + uiFeatureManifest: manifest, + ); controller.openSettings(tab: SettingsTab.gateway); await pumpPage( @@ -22,12 +33,13 @@ void main() { initialTab: SettingsTab.gateway, showSectionTabs: true, ), + platform: TargetPlatform.macOS, ); - await tester.tap(find.byKey(const ValueKey('section-tab-高级自定义配置'))); + await tester.tap(find.byKey(const ValueKey('section-tab-基础连接配置'))); await tester.pumpAndSettle(); - expect(find.text('ACP Bridge Server 连接模式'), findsOneWidget); + expect(find.text('基础连接配置'), findsWidgets); expect( find.byKey(const ValueKey('acp-bridge-mode-cloud')), findsOneWidget, @@ -38,7 +50,7 @@ void main() { ); expect( find.byKey(const ValueKey('acp-bridge-mode-advanced')), - findsOneWidget, + findsNothing, ); expect( find.byKey(const ValueKey('acp-bridge-self-hosted-url')), diff --git a/test/features/settings_page_suite.dart b/test/features/settings_page_suite.dart index 0b2e01ef..42f515af 100644 --- a/test/features/settings_page_suite.dart +++ b/test/features/settings_page_suite.dart @@ -76,10 +76,12 @@ class _DesktopServiceStub implements DesktopPlatformService { Future _createControllerWithSkillAccessService( WidgetTester tester, SkillDirectoryAccessService skillDirectoryAccessService, + {UiFeatureManifest? uiFeatureManifest,} ) async { final controller = AppController( store: createIsolatedTestStore(enableSecureStorage: false), skillDirectoryAccessService: skillDirectoryAccessService, + uiFeatureManifest: uiFeatureManifest, ); addTearDown(controller.dispose); await tester.pump(const Duration(milliseconds: 100)); @@ -179,6 +181,24 @@ Future _ensureVisible(WidgetTester tester, Finder finder) async { await tester.pumpAndSettle(); } +UiFeatureManifest _gatewayAdvancedManifestInternal() { + return UiFeatureManifest.fallback() + .copyWithFeature( + platform: UiFeaturePlatform.desktop, + module: 'settings', + feature: 'gateway_self_hosted_base', + enabled: true, + releaseTier: UiFeatureReleaseTier.experimental, + ) + .copyWithFeature( + platform: UiFeaturePlatform.desktop, + module: 'settings', + feature: 'gateway_advanced_custom_mode', + enabled: true, + releaseTier: UiFeatureReleaseTier.experimental, + ); +} + void main() { testWidgets('SettingsPage theme chips update controller theme mode', ( WidgetTester tester, @@ -197,7 +217,7 @@ void main() { }); testWidgets( - 'SettingsPage gateway advanced config tab merges online account and ACP Bridge Server', + 'SettingsPage gateway home aligns with the architecture model and hides experimental controls by default', (WidgetTester tester) async { final controller = await createTestController(tester); controller.setSettingsTab(SettingsTab.gateway); @@ -213,31 +233,111 @@ void main() { ); expect( - find.byKey(const ValueKey('account-base-url-field')), - findsNothing, - ); - expect(find.byKey(const ValueKey('acp-bridge-mode-cloud')), findsNothing); - - await tester.tap(find.byKey(const ValueKey('section-tab-高级自定义配置'))); - await tester.pumpAndSettle(); - - expect(find.text('ACP Bridge Server 连接模式'), findsOneWidget); - expect( - find.byKey(const ValueKey('account-base-url-field')), + find.byKey(const ValueKey('gateway-configuration-overview-card')), findsOneWidget, ); + expect( + find.byKey(const ValueKey('gateway-overview-login-status')), + findsOneWidget, + ); + expect( + find.byKey(const ValueKey('gateway-overview-default-source')), + findsOneWidget, + ); + expect( + find.byKey(const ValueKey('gateway-overview-advanced-override')), + findsNothing, + ); + expect( + find.byKey(const ValueKey('section-tab-用户登录状态')), + findsOneWidget, + ); + expect( + find.byKey(const ValueKey('section-tab-基础连接配置')), + findsOneWidget, + ); + expect( + find.byKey(const ValueKey('section-tab-高级自定义模式')), + findsNothing, + ); + expect(find.byKey(const ValueKey('account-base-url-field')), findsOneWidget); + expect(find.byKey(const ValueKey('account-username-field')), findsOneWidget); + expect(find.byKey(const ValueKey('account-password-field')), findsOneWidget); + expect(find.byKey(const ValueKey('acp-bridge-mode-cloud')), findsNothing); + expect( + find.byKey(const ValueKey('acp-bridge-mode-self-hosted')), + findsNothing, + ); + expect(find.text('OpenClaw Gateway'), findsNothing); + expect(find.text('Vault Server'), findsNothing); + expect(find.text('LLM 接入点'), findsNothing); + expect(find.text('外部 ACP Server Endpoint'), findsNothing); + expect(find.text('SKILLS 目录授权'), findsNothing); + + await tester.tap(find.byKey(const ValueKey('section-tab-基础连接配置'))); + await tester.pumpAndSettle(); + + expect(find.text('基础连接配置'), findsWidgets); expect( find.byKey(const ValueKey('acp-bridge-mode-cloud')), findsOneWidget, ); expect( find.byKey(const ValueKey('acp-bridge-mode-self-hosted')), - findsOneWidget, + findsNothing, ); expect( find.byKey(const ValueKey('acp-bridge-mode-advanced')), + findsNothing, + ); + }, + ); + + testWidgets( + 'SettingsPage gateway can show self-hosted and advanced custom controls when feature flags are enabled', + (WidgetTester tester) async { + final manifest = _gatewayAdvancedManifestInternal(); + final controller = await createTestController( + tester, + uiFeatureManifest: manifest, + ); + controller.setSettingsTab(SettingsTab.gateway); + + await pumpPage( + tester, + child: SettingsPage( + controller: controller, + initialTab: SettingsTab.gateway, + showSectionTabs: true, + ), + platform: TargetPlatform.macOS, + ); + + expect( + find.byKey(const ValueKey('section-tab-高级自定义模式')), findsOneWidget, ); + + await tester.tap(find.byKey(const ValueKey('section-tab-基础连接配置'))); + await tester.pumpAndSettle(); + + expect( + find.byKey(const ValueKey('acp-bridge-mode-self-hosted')), + findsOneWidget, + ); + + await tester.tap(find.byKey(const ValueKey('section-tab-高级自定义模式'))); + await tester.pumpAndSettle(); + + expect( + find.byKey(const ValueKey('gateway-advanced-override-intro')), + findsOneWidget, + ); + expect(find.text('OpenClaw Gateway'), findsWidgets); + expect(find.text('Vault Server'), findsAtLeastNWidgets(1)); + expect(find.text('LLM 接入点'), findsOneWidget); + expect(find.text('外部 ACP Server Endpoint'), findsOneWidget); + expect(find.text('SKILLS 目录授权'), findsOneWidget); }, ); @@ -309,73 +409,37 @@ void main() { await _pumpSettingsPage(tester, controller, tab: SettingsTab.gateway); - expect(find.text('OpenClaw Gateway'), findsWidgets); - expect(find.text('LLM 接入点'), findsOneWidget); - expect(find.text('Vault Server'), findsAtLeastNWidgets(1)); - expect(find.byKey(const ValueKey('gateway-test-button')), findsOneWidget); - expect(find.byKey(const ValueKey('gateway-save-button')), findsNothing); - expect(find.byKey(const ValueKey('gateway-apply-button')), findsOneWidget); + expect(find.text('用户登录状态'), findsWidgets); + expect(find.text('基础连接配置'), findsWidgets); + expect(find.text('高级自定义模式'), findsNothing); + expect(find.byKey(const ValueKey('account-base-url-field')), findsOneWidget); + expect(find.byKey(const ValueKey('account-username-field')), findsOneWidget); + expect(find.byKey(const ValueKey('account-password-field')), findsOneWidget); + expect(find.byKey(const ValueKey('acp-bridge-mode-cloud')), findsNothing); expect( - find.byKey(const ValueKey('gateway-profile-chip-0')), - findsOneWidget, - ); - expect( - find.byKey(const ValueKey('gateway-profile-chip-1')), - findsOneWidget, - ); - expect( - find.byKey(const ValueKey('gateway-profile-chip-2')), - findsOneWidget, - ); - expect( - find.byKey(const ValueKey('gateway-profile-chip-3')), - findsOneWidget, - ); - expect( - find.byKey(const ValueKey('gateway-profile-chip-4')), - findsOneWidget, - ); - expect( - find.descendant( - of: find.byKey(const ValueKey('gateway-profile-chip-2')), - matching: find.text('连接源 1(空)'), - ), - findsOneWidget, - ); - expect(find.text('自定义连接源 1(空)'), findsNothing); - expect( - find.byKey(const ValueKey('gateway-device-security-card')), - findsOneWidget, - ); - - expect( - find.byKey(const ValueKey('vault-server-url-field')), - findsOneWidget, - ); - expect( - find.byKey(const ValueKey('vault-root-access-token-field')), - findsOneWidget, - ); - expect(find.byKey(const ValueKey('ai-gateway-url-field')), findsOneWidget); - expect(find.byKey(const ValueKey('gateway-mode-field')), findsNothing); - expect(find.text('认证诊断'), findsNothing); - expect( - find.byKey(const ValueKey('external-acp-provider-add-button')), - findsOneWidget, - ); - expect( - find.byKey(const ValueKey('settings-global-apply-button')), + find.byKey(const ValueKey('acp-bridge-mode-self-hosted')), findsNothing, ); + expect(find.text('OpenClaw Gateway'), findsNothing); + expect(find.text('Vault Server'), findsNothing); + expect(find.text('LLM 接入点'), findsNothing); + expect(find.text('外部 ACP Server Endpoint'), findsNothing); + expect(find.text('SKILLS 目录授权'), findsNothing); }); testWidgets('SettingsPage vault card exposes concrete K/V fields', ( WidgetTester tester, ) async { - final controller = await createTestController(tester); + final controller = await createTestController( + tester, + uiFeatureManifest: _gatewayAdvancedManifestInternal(), + ); await _pumpSettingsPage(tester, controller, tab: SettingsTab.gateway); + await tester.tap(find.byKey(const ValueKey('section-tab-高级自定义模式'))); + await tester.pumpAndSettle(); + expect(find.text('Vault Server'), findsAtLeastNWidgets(1)); expect(find.text('VAULT_SERVER_URL'), findsOneWidget); expect( @@ -389,10 +453,16 @@ void main() { testWidgets('SettingsPage integration tab exposes ACP provider endpoints', ( WidgetTester tester, ) async { - final controller = await createTestController(tester); + final controller = await createTestController( + tester, + uiFeatureManifest: _gatewayAdvancedManifestInternal(), + ); await _pumpSettingsPage(tester, controller, tab: SettingsTab.gateway); + await tester.tap(find.byKey(const ValueKey('section-tab-高级自定义模式'))); + await tester.pumpAndSettle(); + expect(find.text('外部 ACP Server Endpoint'), findsOneWidget); expect(find.textContaining('Codex'), findsWidgets); expect(find.textContaining('OpenCode'), findsWidgets); @@ -419,10 +489,16 @@ void main() { testWidgets('SettingsPage ACP wizard adds a custom provider card', ( WidgetTester tester, ) async { - final controller = await createTestController(tester); + final controller = await createTestController( + tester, + uiFeatureManifest: _gatewayAdvancedManifestInternal(), + ); await _pumpSettingsPage(tester, controller, tab: SettingsTab.gateway); + await tester.tap(find.byKey(const ValueKey('section-tab-高级自定义模式'))); + await tester.pumpAndSettle(); + await _ensureVisible( tester, find.byKey(const ValueKey('external-acp-provider-add-button')), @@ -455,10 +531,14 @@ void main() { final controller = await _createControllerWithSkillAccessService( tester, _FakeSkillDirectoryAccessService(userHomeDirectory: '/Users/tester'), + uiFeatureManifest: _gatewayAdvancedManifestInternal(), ); await _pumpSettingsPage(tester, controller, tab: SettingsTab.gateway); + await tester.tap(find.byKey(const ValueKey('section-tab-高级自定义模式'))); + await tester.pumpAndSettle(); + expect(find.text('~/.agents/skills'), findsOneWidget); expect(find.text('/Users/tester/.agents/skills'), findsOneWidget); expect(find.text('~/.codex/skills'), findsOneWidget); @@ -485,9 +565,12 @@ void main() { ), ], ), + uiFeatureManifest: _gatewayAdvancedManifestInternal(), ); await _pumpSettingsPage(tester, controller, tab: SettingsTab.gateway); + await tester.tap(find.byKey(const ValueKey('section-tab-高级自定义模式'))); + await tester.pumpAndSettle(); await _ensureVisible( tester, find.byKey(const ValueKey('skill-directory-batch-add-button')), @@ -542,9 +625,12 @@ paths: ), ], ), + uiFeatureManifest: _gatewayAdvancedManifestInternal(), ); await _pumpSettingsPage(tester, controller, tab: SettingsTab.gateway); + await tester.tap(find.byKey(const ValueKey('section-tab-高级自定义模式'))); + await tester.pumpAndSettle(); await _ensureVisible( tester, find.byKey(const ValueKey('skill-directory-batch-add-button')), @@ -582,9 +668,12 @@ paths: final controller = await _createControllerWithSkillAccessService( tester, _FakeSkillDirectoryAccessService(userHomeDirectory: '/Users/tester'), + uiFeatureManifest: _gatewayAdvancedManifestInternal(), ); await _pumpSettingsPage(tester, controller, tab: SettingsTab.gateway); + await tester.tap(find.byKey(const ValueKey('section-tab-高级自定义模式'))); + await tester.pumpAndSettle(); await _ensureVisible( tester, find.byKey(const ValueKey('skill-directory-batch-add-button')), @@ -651,9 +740,14 @@ paths: testWidgets('SettingsPage external ACP section can collapse independently', ( WidgetTester tester, ) async { - final controller = await createTestController(tester); + final controller = await createTestController( + tester, + uiFeatureManifest: _gatewayAdvancedManifestInternal(), + ); await _pumpSettingsPage(tester, controller, tab: SettingsTab.gateway); + await tester.tap(find.byKey(const ValueKey('section-tab-高级自定义模式'))); + await tester.pumpAndSettle(); await _ensureVisible(tester, find.text('外部 ACP Server Endpoint')); await tester.tap(find.text('外部 ACP Server Endpoint').first); @@ -678,7 +772,10 @@ paths: testWidgets('SettingsPage external ACP card supports continuous input', ( WidgetTester tester, ) async { - final controller = await createTestController(tester); + final controller = await createTestController( + tester, + uiFeatureManifest: _gatewayAdvancedManifestInternal(), + ); final customProfile = buildCustomExternalAcpEndpointProfile( controller.settingsDraft.externalAcpEndpoints, label: 'Initial Name', @@ -694,6 +791,8 @@ paths: ); await _pumpSettingsPage(tester, controller, tab: SettingsTab.gateway); + await tester.tap(find.byKey(const ValueKey('section-tab-高级自定义模式'))); + await tester.pumpAndSettle(); final labelField = find.byKey( ValueKey('external-acp-label-${customProfile.providerKey}'),