From 01bd9e25052ee2fd21bf8573f25a43bbddffd7d7 Mon Sep 17 00:00:00 2001 From: Haitao Pan Date: Sun, 12 Apr 2026 11:34:12 +0800 Subject: [PATCH] Remove gateway fallback from bridge task runtime --- lib/app/app_controller_desktop_core.dart | 26 ++----- ...pp_controller_desktop_runtime_helpers.dart | 35 +++++---- ...ler_desktop_single_agent_go_task_flow.dart | 7 +- ..._desktop_single_agent_status_messages.dart | 2 +- ...app_controller_desktop_thread_actions.dart | 11 ++- ...pp_controller_desktop_thread_sessions.dart | 8 +- ...op_thread_sessions_collaboration_impl.dart | 33 ++++++-- .../assistant/assistant_page_components.dart | 16 ++-- ...rnal_code_agent_acp_desktop_transport.dart | 8 +- lib/runtime/gateway_acp_client.dart | 8 +- lib/runtime/go_task_service_client.dart | 12 --- .../go_task_service_desktop_service.dart | 4 - .../multi_agent_orchestrator_core.dart | 12 --- .../multi_agent_orchestrator_support.dart | 36 +-------- .../multi_agent_orchestrator_workflow.dart | 72 ++---------------- ...ime_controllers_settings_account_impl.dart | 43 +++++++---- lib/runtime/runtime_models_connection.dart | 21 +++++- lib/runtime/runtime_models_profiles.dart | 13 +--- .../runtime_models_settings_snapshot.dart | 6 +- .../assistant_focus_panel_previews.dart | 2 +- ...ntroller_desktop_runtime_cleanup_test.dart | 51 ++++++++----- ...ontroller_desktop_thread_binding_test.dart | 17 ++--- ...sktop_working_directory_dispatch_test.dart | 75 +++++++++++++------ ...t_execution_target_picker_widget_test.dart | 30 ++++++-- .../assistant_page_composer_golden_test.dart | 24 ++++-- test/runtime/bridge_real_e2e_test.dart | 10 +-- .../external_acp_bridge_sync_order_test.dart | 2 - .../settings_account_auth_flow_test.dart | 8 +- 28 files changed, 281 insertions(+), 311 deletions(-) diff --git a/lib/app/app_controller_desktop_core.dart b/lib/app/app_controller_desktop_core.dart index ba5d6647..7585d6c6 100644 --- a/lib/app/app_controller_desktop_core.dart +++ b/lib/app/app_controller_desktop_core.dart @@ -124,7 +124,6 @@ class AppController extends ChangeNotifier { SkillDirectoryAccessService? skillDirectoryAccessService, AccountRuntimeClient Function(String baseUrl)? accountClientFactory, List? singleAgentSharedSkillScanRootOverrides, - List? availableSingleAgentProvidersOverride, ArisBundleRepository? arisBundleRepository, GoTaskServiceClient? goTaskServiceClient, MultiAgentMountManager? multiAgentMountManager, @@ -197,8 +196,6 @@ class AppController extends ChangeNotifier { endpointResolver: resolveGatewayAcpEndpointInternal, authorizationResolver: resolveGatewayAcpAuthorizationHeaderInternal, ); - availableSingleAgentProvidersOverrideInternal = - availableSingleAgentProvidersOverride; arisBundleRepositoryInternal = arisBundleRepository ?? ArisBundleRepository(); runtimeCoordinatorInternal.attachDispatchResolver( @@ -287,8 +284,6 @@ class AppController extends ChangeNotifier { late final SkillDirectoryAccessService skillDirectoryAccessServiceInternal; late final List? singleAgentSharedSkillScanRootOverridesInternal; late final GatewayAcpClient gatewayAcpClientInternal; - late final List? - availableSingleAgentProvidersOverrideInternal; late final ArisBundleRepository arisBundleRepositoryInternal; late final GoTaskServiceClient goTaskServiceClientInternal; late final MultiAgentOrchestrator multiAgentOrchestratorInternal; @@ -584,16 +579,14 @@ class AppController extends ChangeNotifier { ); List get configuredSingleAgentProviders => - normalizeSingleAgentProviderList(bridgeAdvertisedProvidersInternal); + normalizeBridgeOwnedSingleAgentProviderList( + bridgeAdvertisedProvidersInternal, + ); List get availableSingleAgentProviders => - availableSingleAgentProvidersOverrideInternal != null - ? normalizeSingleAgentProviderList( - availableSingleAgentProvidersOverrideInternal!, - ) - : configuredSingleAgentProviders - .where(canUseSingleAgentProviderInternal) - .toList(growable: false); + configuredSingleAgentProviders + .where(canUseSingleAgentProviderInternal) + .toList(growable: false); List visibleAssistantExecutionTargets( Iterable supportedTargets, @@ -604,8 +597,7 @@ class AppController extends ChangeNotifier { availableSingleAgentProviders.isNotEmpty) { visible.add(AssistantExecutionTarget.singleAgent); } - if (supported.contains(AssistantExecutionTarget.gateway) && - appUiState.isGatewayTargetSaved(AssistantExecutionTarget.gateway)) { + if (supported.contains(AssistantExecutionTarget.gateway)) { visible.add(AssistantExecutionTarget.gateway); } if (!supportedTargets.contains(AssistantExecutionTarget.singleAgent) || @@ -624,10 +616,6 @@ class AppController extends ChangeNotifier { availableSingleAgentProviders.isNotEmpty; bool canUseSingleAgentProviderInternal(SingleAgentProvider provider) { - final override = availableSingleAgentProvidersOverrideInternal; - if (override != null) { - return !provider.isUnspecified && override.contains(provider); - } if (provider.isUnspecified) { return false; } diff --git a/lib/app/app_controller_desktop_runtime_helpers.dart b/lib/app/app_controller_desktop_runtime_helpers.dart index 94261002..f722d7c9 100644 --- a/lib/app/app_controller_desktop_runtime_helpers.dart +++ b/lib/app/app_controller_desktop_runtime_helpers.dart @@ -736,7 +736,26 @@ extension AppControllerDesktopRuntimeHelpers on AppController { } Uri? resolveBridgeAcpEndpointInternal() { - final uri = Uri.tryParse(kCanonicalBridgeAcpEndpoint); + final endpoint = + settingsControllerInternal + .accountSyncState + ?.syncedDefaults + .bridgeServerUrl + .trim() + .isNotEmpty == + true + ? settingsControllerInternal + .accountSyncState! + .syncedDefaults + .bridgeServerUrl + .trim() + : settings + .acpBridgeServerModeConfig + .cloudSynced + .remoteServerSummary + .endpoint + .trim(); + final uri = Uri.tryParse(endpoint); final scheme = uri?.scheme.trim().toLowerCase() ?? ''; if (uri == null || !kSupportedExternalAcpEndpointSchemes.contains(scheme)) { return null; @@ -781,20 +800,6 @@ extension AppControllerDesktopRuntimeHelpers on AppController { return 'Bearer $bridgeToken'; } } - final profileIndex = - gatewayProfileIndexMatchingEndpointInternal(endpoint) ?? - kGatewayRemoteProfileIndex; - final gatewayToken = await settingsControllerInternal - .loadEffectiveGatewayToken(profileIndex: profileIndex); - if (gatewayToken.isNotEmpty) { - return 'Bearer $gatewayToken'; - } - final gatewayPassword = await settingsControllerInternal - .loadEffectiveGatewayPassword(profileIndex: profileIndex); - if (gatewayPassword.isNotEmpty) { - final encoded = base64Encode(utf8.encode('operator:$gatewayPassword')); - return 'Basic $encoded'; - } return null; } diff --git a/lib/app/app_controller_desktop_single_agent_go_task_flow.dart b/lib/app/app_controller_desktop_single_agent_go_task_flow.dart index f69f421f..b70487ed 100644 --- a/lib/app/app_controller_desktop_single_agent_go_task_flow.dart +++ b/lib/app/app_controller_desktop_single_agent_go_task_flow.dart @@ -86,9 +86,7 @@ Future sendSingleAgentMessageDesktopGoTaskFlowInternal( sessionKey, null, ) - : controller.singleAgentNeedsAiGatewayConfigurationForSession( - sessionKey, - ) + : controller.singleAgentNeedsBridgeProviderForSession(sessionKey) ? singleAgentUnavailableLabelDesktopInternal( controller, sessionKey, @@ -124,7 +122,6 @@ Future sendSingleAgentMessageDesktopGoTaskFlowInternal( return; } - final aiGatewayApiKey = await controller.loadAiGatewayApiKey(); if (!effectiveProvider.isUnspecified) { appendSingleAgentRuntimeStatusDesktopInternal( controller, @@ -161,8 +158,6 @@ Future sendSingleAgentMessageDesktopGoTaskFlowInternal( selectedSkills: selectedSkills, inlineAttachments: attachments, localAttachments: localAttachments, - aiGatewayBaseUrl: controller.aiGatewayUrl, - aiGatewayApiKey: aiGatewayApiKey, agentId: '', metadata: const {}, routing: routing, diff --git a/lib/app/app_controller_desktop_single_agent_status_messages.dart b/lib/app/app_controller_desktop_single_agent_status_messages.dart index f9eeb1d9..f86493bd 100644 --- a/lib/app/app_controller_desktop_single_agent_status_messages.dart +++ b/lib/app/app_controller_desktop_single_agent_status_messages.dart @@ -91,7 +91,7 @@ String singleAgentUnavailableLabelDesktopInternal( 'This thread is pinned to ${selection.label}: $detail XWorkmate will not reroute to another bridge provider automatically. Switch to an available provider manually.', ); } - if (controller.singleAgentNeedsAiGatewayConfigurationForSession( + if (controller.singleAgentNeedsBridgeProviderForSession( normalizedSessionKey, )) { return detail.isEmpty diff --git a/lib/app/app_controller_desktop_thread_actions.dart b/lib/app/app_controller_desktop_thread_actions.dart index aa9b5e36..b6123bcf 100644 --- a/lib/app/app_controller_desktop_thread_actions.dart +++ b/lib/app/app_controller_desktop_thread_actions.dart @@ -306,6 +306,15 @@ extension AppControllerDesktopThreadActions on AppController { recomputeTasksInternal(); notifyIfActiveInternal(); try { + if (resolveExternalAcpEndpointForTargetInternal(currentTarget) == + null) { + throw StateError( + appText( + 'BRIDGE_SERVER_URL 未配置,无法启动任务对话。', + 'BRIDGE_SERVER_URL is unavailable, so task chat cannot start.', + ), + ); + } final dispatch = await codeAgentNodeOrchestratorInternal .buildGatewayDispatch(buildCodeAgentNodeStateInternal()); final result = await goTaskServiceClientInternal.executeTask( @@ -320,8 +329,6 @@ extension AppControllerDesktopThreadActions on AppController { selectedSkills: selectedSkillLabels, inlineAttachments: attachments, localAttachments: localAttachments, - aiGatewayBaseUrl: aiGatewayUrl, - aiGatewayApiKey: await loadAiGatewayApiKey(), agentId: dispatch.agentId ?? '', metadata: dispatch.metadata, routing: buildExternalAcpRoutingForSessionInternal(sessionKey), diff --git a/lib/app/app_controller_desktop_thread_sessions.dart b/lib/app/app_controller_desktop_thread_sessions.dart index 6409511a..55e93f16 100644 --- a/lib/app/app_controller_desktop_thread_sessions.dart +++ b/lib/app/app_controller_desktop_thread_sessions.dart @@ -277,7 +277,7 @@ extension AppControllerDesktopThreadSessions on AppController { SingleAgentProvider? get currentSingleAgentResolvedProvider => singleAgentResolvedProviderForSession(currentSessionKey); - bool singleAgentNeedsAiGatewayConfigurationForSession(String sessionKey) { + bool singleAgentNeedsBridgeProviderForSession(String sessionKey) { final normalizedSessionKey = normalizedAssistantSessionKeyInternal( sessionKey, ); @@ -288,8 +288,8 @@ extension AppControllerDesktopThreadSessions on AppController { return !hasAnyAvailableSingleAgentProvider; } - bool get currentSingleAgentNeedsAiGatewayConfiguration => - singleAgentNeedsAiGatewayConfigurationForSession(currentSessionKey); + bool get currentSingleAgentNeedsBridgeProvider => + singleAgentNeedsBridgeProviderForSession(currentSessionKey); bool singleAgentHasResolvedProviderForSession(String sessionKey) { return singleAgentResolvedProviderForSession(sessionKey) != null; @@ -419,7 +419,7 @@ extension AppControllerDesktopThreadSessions on AppController { '${provider.label} 当前不可用,请改成 Bridge 当前可用的 Provider。', '${provider.label} is unavailable. Switch to a provider currently advertised by the bridge.', ) - : singleAgentNeedsAiGatewayConfigurationForSession( + : singleAgentNeedsBridgeProviderForSession( normalizedSessionKey, ) ? appText( diff --git a/lib/app/app_controller_desktop_thread_sessions_collaboration_impl.dart b/lib/app/app_controller_desktop_thread_sessions_collaboration_impl.dart index fc52f245..2e0ec450 100644 --- a/lib/app/app_controller_desktop_thread_sessions_collaboration_impl.dart +++ b/lib/app/app_controller_desktop_thread_sessions_collaboration_impl.dart @@ -107,9 +107,34 @@ Future runMultiAgentCollaborationThreadSessionInternal( ? 'main' : controller.currentSessionKey; await controller.enqueueThreadTurnInternal(sessionKey, () async { - final aiGatewayApiKey = await loadAiGatewayApiKeyThreadSessionInternal( - controller, - ); + if (controller.resolveExternalAcpEndpointForTargetInternal( + controller.assistantExecutionTargetForSession(sessionKey), + ) == + null) { + final error = StateError( + appText( + 'BRIDGE_SERVER_URL 未配置,无法启动任务对话。', + 'BRIDGE_SERVER_URL is unavailable, so task chat cannot start.', + ), + ); + controller.appendLocalSessionMessageInternal( + sessionKey, + GatewayChatMessage( + id: controller.nextLocalMessageIdInternal(), + role: 'assistant', + text: error.message.toString(), + timestampMs: DateTime.now().millisecondsSinceEpoch.toDouble(), + toolCallId: null, + toolName: 'Multi-Agent', + stopReason: null, + pending: false, + error: true, + ), + ); + controller.recomputeTasksInternal(); + controller.notifyIfActiveInternal(); + throw error; + } await controller.ensureDesktopTaskThreadBindingInternal( sessionKey, executionTarget: controller.assistantExecutionTargetForSession( @@ -172,8 +197,6 @@ Future runMultiAgentCollaborationThreadSessionInternal( selectedSkills: selectedSkillLabels, inlineAttachments: const [], localAttachments: attachments, - aiGatewayBaseUrl: controller.aiGatewayUrl, - aiGatewayApiKey: aiGatewayApiKey, agentId: '', metadata: const {}, routingHint: 'gateway', diff --git a/lib/features/assistant/assistant_page_components.dart b/lib/features/assistant/assistant_page_components.dart index 72732de7..07775259 100644 --- a/lib/features/assistant/assistant_page_components.dart +++ b/lib/features/assistant/assistant_page_components.dart @@ -502,8 +502,8 @@ class AssistantEmptyStateInternal extends StatelessWidget { final connectionState = controller.currentAssistantConnectionState; final singleAgent = connectionState.isSingleAgent; final connected = connectionState.connected; - final singleAgentNeedsAiGateway = - controller.currentSingleAgentNeedsAiGatewayConfiguration; + final singleAgentNeedsBridgeProvider = + controller.currentSingleAgentNeedsBridgeProvider; final singleAgentSuggestsAcpSwitch = controller.currentSingleAgentShouldSuggestAcpSwitch; final providerLabel = controller.currentSingleAgentProvider.label; @@ -511,7 +511,7 @@ class AssistantEmptyStateInternal extends StatelessWidget { final title = singleAgent ? connected ? appText('开始智能体任务', 'Start an agent task') - : singleAgentNeedsAiGateway + : singleAgentNeedsBridgeProvider ? appText( '先配置 Bridge Provider', 'Configure a bridge provider first', @@ -536,7 +536,7 @@ class AssistantEmptyStateInternal extends StatelessWidget { '当前线程固定为 $providerLabel,但它在这台设备上不可用。请改成 Bridge 当前可用的 Provider。', 'This thread is pinned to $providerLabel, but it is unavailable on this device. Switch to a provider currently advertised by the bridge.', ) - : singleAgentNeedsAiGateway + : singleAgentNeedsBridgeProvider ? appText( '请先在 设置 -> 集成 中配置并同步可用的外部 Agent 连接,然后再继续当前任务。', 'Configure and sync an available external agent connection in Settings -> Integrations before continuing this task.', @@ -602,7 +602,7 @@ class AssistantEmptyStateInternal extends StatelessWidget { onPressed: connected ? onFocusComposer : singleAgent - ? singleAgentNeedsAiGateway + ? singleAgentNeedsBridgeProvider ? onOpenAiGatewaySettings : onFocusComposer : reconnectAvailable @@ -614,7 +614,7 @@ class AssistantEmptyStateInternal extends StatelessWidget { connected ? Icons.edit_rounded : singleAgent - ? singleAgentNeedsAiGateway + ? singleAgentNeedsBridgeProvider ? Icons.tune_rounded : Icons.smart_toy_outlined : reconnectAvailable @@ -625,7 +625,7 @@ class AssistantEmptyStateInternal extends StatelessWidget { connected ? appText('开始输入', 'Start typing') : singleAgent - ? singleAgentNeedsAiGateway + ? singleAgentNeedsBridgeProvider ? appText('打开配置中心', 'Open settings') : appText('查看线程工具栏', 'Open toolbar') : reconnectAvailable @@ -644,7 +644,7 @@ class AssistantEmptyStateInternal extends StatelessWidget { ), ), if (!connected && - (!singleAgent || singleAgentNeedsAiGateway)) + (!singleAgent || singleAgentNeedsBridgeProvider)) OutlinedButton.icon( onPressed: singleAgent ? onOpenAiGatewaySettings diff --git a/lib/runtime/external_code_agent_acp_desktop_transport.dart b/lib/runtime/external_code_agent_acp_desktop_transport.dart index 14eadfd9..3dd0f290 100644 --- a/lib/runtime/external_code_agent_acp_desktop_transport.dart +++ b/lib/runtime/external_code_agent_acp_desktop_transport.dart @@ -63,18 +63,12 @@ class ExternalCodeAgentAcpDesktopTransport required String taskPrompt, required String workingDirectory, required ExternalCodeAgentAcpRoutingConfig routing, - String aiGatewayBaseUrl = '', - String aiGatewayApiKey = '', }) async { final response = await _client.request( method: 'xworkmate.routing.resolve', params: { 'taskPrompt': taskPrompt, 'workingDirectory': workingDirectory.trim(), - if (aiGatewayBaseUrl.trim().isNotEmpty) - 'aiGatewayBaseUrl': aiGatewayBaseUrl.trim(), - if (aiGatewayApiKey.trim().isNotEmpty) - 'aiGatewayApiKey': aiGatewayApiKey.trim(), 'routing': routing.toJson(), }, endpointOverride: _endpointResolver(AssistantExecutionTarget.singleAgent), @@ -218,6 +212,6 @@ class ExternalCodeAgentAcpDesktopTransport providers.add(provider); } } - return normalizeSingleAgentProviderList(providers); + return normalizeBridgeOwnedSingleAgentProviderList(providers); } } diff --git a/lib/runtime/gateway_acp_client.dart b/lib/runtime/gateway_acp_client.dart index 881f6da4..6346c819 100644 --- a/lib/runtime/gateway_acp_client.dart +++ b/lib/runtime/gateway_acp_client.dart @@ -69,8 +69,6 @@ class GatewayAcpMultiAgentRequest { required this.workingDirectory, required this.attachments, required this.selectedSkills, - required this.aiGatewayBaseUrl, - required this.aiGatewayApiKey, required this.resumeSession, }); @@ -80,8 +78,6 @@ class GatewayAcpMultiAgentRequest { final String workingDirectory; final List attachments; final List selectedSkills; - final String aiGatewayBaseUrl; - final String aiGatewayApiKey; final bool resumeSession; } @@ -162,7 +158,7 @@ class GatewayAcpClient { providers.add(provider); } } - return normalizeSingleAgentProviderList(providers); + return normalizeBridgeOwnedSingleAgentProviderList(providers); } Stream runMultiAgent( @@ -196,8 +192,6 @@ class GatewayAcpClient { ) .toList(growable: false), 'selectedSkills': request.selectedSkills, - 'aiGatewayBaseUrl': request.aiGatewayBaseUrl, - 'aiGatewayApiKey': request.aiGatewayApiKey, }, ); var lastSequence = -1; diff --git a/lib/runtime/go_task_service_client.dart b/lib/runtime/go_task_service_client.dart index eba577b7..7655fc65 100644 --- a/lib/runtime/go_task_service_client.dart +++ b/lib/runtime/go_task_service_client.dart @@ -219,8 +219,6 @@ class GoTaskServiceRequest { required this.selectedSkills, required this.inlineAttachments, required this.localAttachments, - required this.aiGatewayBaseUrl, - required this.aiGatewayApiKey, required this.agentId, required this.metadata, this.routing, @@ -242,8 +240,6 @@ class GoTaskServiceRequest { final List selectedSkills; final List inlineAttachments; final List localAttachments; - final String aiGatewayBaseUrl; - final String aiGatewayApiKey; final String agentId; final Map metadata; final ExternalCodeAgentAcpRoutingConfig? routing; @@ -328,10 +324,6 @@ class GoTaskServiceRequest { 'remoteWorkingDirectoryHint': remoteWorkingDirectoryHint.trim(), if (model.trim().isNotEmpty) 'model': model.trim(), if (thinking.trim().isNotEmpty) 'thinking': thinking.trim(), - if (aiGatewayBaseUrl.trim().isNotEmpty) - 'aiGatewayBaseUrl': aiGatewayBaseUrl.trim(), - if (aiGatewayApiKey.trim().isNotEmpty) - 'aiGatewayApiKey': aiGatewayApiKey.trim(), 'routing': resolvedRouting.toJson(), if (routingHint.trim().isNotEmpty) 'routingHint': routingHint.trim(), 'requestedExecutionTarget': normalizedTarget.promptValue, @@ -625,8 +617,6 @@ abstract class ExternalCodeAgentAcpTransport { required String taskPrompt, required String workingDirectory, required ExternalCodeAgentAcpRoutingConfig routing, - String aiGatewayBaseUrl = '', - String aiGatewayApiKey = '', }); Future executeTask( @@ -663,8 +653,6 @@ abstract class GoTaskServiceClient { required String taskPrompt, required String workingDirectory, required ExternalCodeAgentAcpRoutingConfig routing, - String aiGatewayBaseUrl = '', - String aiGatewayApiKey = '', }); Future executeTask( diff --git a/lib/runtime/go_task_service_desktop_service.dart b/lib/runtime/go_task_service_desktop_service.dart index 6d2e4b74..e79eed64 100644 --- a/lib/runtime/go_task_service_desktop_service.dart +++ b/lib/runtime/go_task_service_desktop_service.dart @@ -30,14 +30,10 @@ class DesktopGoTaskService implements GoTaskServiceClient { required String taskPrompt, required String workingDirectory, required ExternalCodeAgentAcpRoutingConfig routing, - String aiGatewayBaseUrl = '', - String aiGatewayApiKey = '', }) => _acpTransport.resolveExternalAcpRouting( taskPrompt: taskPrompt, workingDirectory: workingDirectory, routing: routing, - aiGatewayBaseUrl: aiGatewayBaseUrl, - aiGatewayApiKey: aiGatewayApiKey, ); @override diff --git a/lib/runtime/multi_agent_orchestrator_core.dart b/lib/runtime/multi_agent_orchestrator_core.dart index fde1ffdb..073f8e0b 100644 --- a/lib/runtime/multi_agent_orchestrator_core.dart +++ b/lib/runtime/multi_agent_orchestrator_core.dart @@ -147,8 +147,6 @@ class MultiAgentOrchestrator extends ChangeNotifier { required String workingDirectory, List attachments = const [], List selectedSkills = const [], - String aiGatewayBaseUrl = '', - String aiGatewayApiKey = '', void Function(MultiAgentRunEvent event)? onEvent, }) async { assertEmbeddedProcessesAllowedInternal(); @@ -192,8 +190,6 @@ class MultiAgentOrchestrator extends ChangeNotifier { taskPrompt, preset: preset, selectedSkills: selectedSkills, - aiGatewayBaseUrl: aiGatewayBaseUrl, - aiGatewayApiKey: aiGatewayApiKey, ); steps.add( CollaborationStep( @@ -242,8 +238,6 @@ class MultiAgentOrchestrator extends ChangeNotifier { attachments, preset: preset, selectedSkills: selectedSkills, - aiGatewayBaseUrl: aiGatewayBaseUrl, - aiGatewayApiKey: aiGatewayApiKey, ); steps.add( CollaborationStep( @@ -286,8 +280,6 @@ class MultiAgentOrchestrator extends ChangeNotifier { final testerResult = await runTesterInternal( engineerResult.codeOutput, preset: preset, - aiGatewayBaseUrl: aiGatewayBaseUrl, - aiGatewayApiKey: aiGatewayApiKey, ); steps.add( CollaborationStep( @@ -335,8 +327,6 @@ class MultiAgentOrchestrator extends ChangeNotifier { testerResult.feedback, workingDirectory, preset: preset, - aiGatewayBaseUrl: aiGatewayBaseUrl, - aiGatewayApiKey: aiGatewayApiKey, ); steps.add( CollaborationStep( @@ -352,8 +342,6 @@ class MultiAgentOrchestrator extends ChangeNotifier { final reReview = await runTesterInternal( fixedResult.codeOutput, preset: preset, - aiGatewayBaseUrl: aiGatewayBaseUrl, - aiGatewayApiKey: aiGatewayApiKey, ); steps.add( CollaborationStep( diff --git a/lib/runtime/multi_agent_orchestrator_support.dart b/lib/runtime/multi_agent_orchestrator_support.dart index 5e652d1b..ad7b4b13 100644 --- a/lib/runtime/multi_agent_orchestrator_support.dart +++ b/lib/runtime/multi_agent_orchestrator_support.dart @@ -15,23 +15,12 @@ import 'multi_agent_orchestrator_workflow.dart'; import 'multi_agent_orchestrator_core.dart'; extension MultiAgentOrchestratorSupportInternal on MultiAgentOrchestrator { - String openAiCompatibleBaseUrlInternal({required String aiGatewayBaseUrl}) { - if (configInternal.aiGatewayInjectionPolicy != - AiGatewayInjectionPolicy.disabled && - aiGatewayBaseUrl.trim().isNotEmpty) { - final normalized = aiGatewayBaseUrl.trim(); - return normalized.endsWith('/v1') ? normalized : '$normalized/v1'; - } + String openAiCompatibleBaseUrlInternal() { final normalized = configInternal.ollamaEndpoint.trim(); return normalized.endsWith('/v1') ? normalized : '$normalized/v1'; } - String openAiCompatibleApiKeyInternal({required String aiGatewayApiKey}) { - if (configInternal.aiGatewayInjectionPolicy != - AiGatewayInjectionPolicy.disabled && - aiGatewayApiKey.trim().isNotEmpty) { - return aiGatewayApiKey.trim(); - } + String openAiCompatibleApiKeyInternal() { return 'ollama'; } @@ -236,27 +225,8 @@ extension MultiAgentOrchestratorSupportInternal on MultiAgentOrchestrator { } /// 构建 Ollama 环境变量 - Map buildCliEnvVarsInternal({ - required String tool, - required String aiGatewayBaseUrl, - required String aiGatewayApiKey, - }) { + Map buildCliEnvVarsInternal({required String tool}) { final baseEnv = {...Platform.environment}; - if (configInternal.aiGatewayInjectionPolicy != - AiGatewayInjectionPolicy.disabled && - aiGatewayBaseUrl.trim().isNotEmpty && - aiGatewayApiKey.trim().isNotEmpty) { - baseEnv['OPENAI_BASE_URL'] = aiGatewayBaseUrl.trim(); - baseEnv['OPENAI_API_KEY'] = aiGatewayApiKey.trim(); - baseEnv['OLLAMA_BASE_URL'] = aiGatewayBaseUrl.trim(); - baseEnv['OLLAMA_HOST'] = aiGatewayBaseUrl.trim(); - if (tool == 'claude') { - baseEnv['ANTHROPIC_BASE_URL'] = aiGatewayBaseUrl.trim(); - baseEnv['ANTHROPIC_AUTH_TOKEN'] = aiGatewayApiKey.trim(); - baseEnv['ANTHROPIC_API_KEY'] = aiGatewayApiKey.trim(); - } - return baseEnv; - } final ollamaEndpoint = configInternal.ollamaEndpoint.trim(); if (ollamaEndpoint.isNotEmpty) { baseEnv['OLLAMA_BASE_URL'] = ollamaEndpoint; diff --git a/lib/runtime/multi_agent_orchestrator_workflow.dart b/lib/runtime/multi_agent_orchestrator_workflow.dart index b1a3f20f..5fe385ce 100644 --- a/lib/runtime/multi_agent_orchestrator_workflow.dart +++ b/lib/runtime/multi_agent_orchestrator_workflow.dart @@ -20,8 +20,6 @@ extension MultiAgentOrchestratorWorkflowInternal on MultiAgentOrchestrator { String task, { required FrameworkPreset preset, required List selectedSkills, - required String aiGatewayBaseUrl, - required String aiGatewayApiKey, }) async { final stopwatch = Stopwatch()..start(); @@ -50,8 +48,6 @@ extension MultiAgentOrchestratorWorkflowInternal on MultiAgentOrchestrator { instructionBlock, ), cwd: '', - aiGatewayBaseUrl: aiGatewayBaseUrl, - aiGatewayApiKey: aiGatewayApiKey, ); stopwatch.stop(); @@ -91,8 +87,6 @@ extension MultiAgentOrchestratorWorkflowInternal on MultiAgentOrchestrator { List attachments, { required FrameworkPreset preset, required List selectedSkills, - required String aiGatewayBaseUrl, - required String aiGatewayApiKey, }) async { final stopwatch = Stopwatch()..start(); final tool = await resolveToolForRoleInternal( @@ -139,8 +133,6 @@ ${selectedSkills.isEmpty ? '- 无' : selectedSkills.map((item) => '- $item').joi ), prompt: prompt, cwd: workingDirectory, - aiGatewayBaseUrl: aiGatewayBaseUrl, - aiGatewayApiKey: aiGatewayApiKey, ); stopwatch.stop(); @@ -156,8 +148,6 @@ ${selectedSkills.isEmpty ? '- 无' : selectedSkills.map((item) => '- $item').joi Future runTesterInternal( String codeOutput, { required FrameworkPreset preset, - required String aiGatewayBaseUrl, - required String aiGatewayApiKey, }) async { final stopwatch = Stopwatch()..start(); final tool = await resolveToolForRoleInternal( @@ -212,8 +202,6 @@ ${codeOutput.length > 4000 ? '${codeOutput.substring(0, 4000)}\n...[代码已截 model: testerModel, prompt: prompt, cwd: '', - aiGatewayBaseUrl: aiGatewayBaseUrl, - aiGatewayApiKey: aiGatewayApiKey, ); stopwatch.stop(); @@ -234,8 +222,6 @@ ${codeOutput.length > 4000 ? '${codeOutput.substring(0, 4000)}\n...[代码已截 String feedback, String workingDirectory, { required FrameworkPreset preset, - required String aiGatewayBaseUrl, - required String aiGatewayApiKey, }) async { final stopwatch = Stopwatch()..start(); final tool = await resolveToolForRoleInternal( @@ -272,8 +258,6 @@ $originalCode ), prompt: prompt, cwd: workingDirectory, - aiGatewayBaseUrl: aiGatewayBaseUrl, - aiGatewayApiKey: aiGatewayApiKey, ); stopwatch.stop(); @@ -292,8 +276,6 @@ $originalCode required String model, required String prompt, required String cwd, - required String aiGatewayBaseUrl, - required String aiGatewayApiKey, }) async { late final List args; late final String command; @@ -306,11 +288,7 @@ $originalCode switch (tool) { case 'claude': command = useOllamaLaunch ? 'ollama' : resolveCliPathInternal('claude'); - envVars = buildCliEnvVarsInternal( - tool: tool, - aiGatewayBaseUrl: aiGatewayBaseUrl, - aiGatewayApiKey: aiGatewayApiKey, - ); + envVars = buildCliEnvVarsInternal(tool: tool); if (useOllamaLaunch) { args = buildOllamaLaunchArgsInternal( tool: tool, @@ -327,11 +305,7 @@ $originalCode case 'codex': command = useOllamaLaunch ? 'ollama' : resolveCliPathInternal('codex'); - envVars = buildCliEnvVarsInternal( - tool: tool, - aiGatewayBaseUrl: aiGatewayBaseUrl, - aiGatewayApiKey: aiGatewayApiKey, - ); + envVars = buildCliEnvVarsInternal(tool: tool); if (useOllamaLaunch) { args = buildOllamaLaunchArgsInternal( tool: tool, @@ -364,11 +338,7 @@ $originalCode case 'gemini': command = resolveCliPathInternal('gemini'); - envVars = buildCliEnvVarsInternal( - tool: tool, - aiGatewayBaseUrl: aiGatewayBaseUrl, - aiGatewayApiKey: aiGatewayApiKey, - ); + envVars = buildCliEnvVarsInternal(tool: tool); if (model.isNotEmpty) { args = ['--model', model, '-p', prompt]; } else { @@ -380,11 +350,7 @@ $originalCode command = useOllamaLaunch ? 'ollama' : resolveCliPathInternal('opencode'); - envVars = buildCliEnvVarsInternal( - tool: tool, - aiGatewayBaseUrl: aiGatewayBaseUrl, - aiGatewayApiKey: aiGatewayApiKey, - ); + envVars = buildCliEnvVarsInternal(tool: tool); args = useOllamaLaunch ? buildOllamaLaunchArgsInternal( tool: tool, @@ -408,13 +374,7 @@ $originalCode final cliAvailable = await binaryExistsInternal(command); if (configInternal.usesAris && !cliAvailable) { - return runArisFallbackInternal( - role: role, - model: model, - prompt: prompt, - aiGatewayBaseUrl: aiGatewayBaseUrl, - aiGatewayApiKey: aiGatewayApiKey, - ); + return runArisFallbackInternal(role: role, model: model, prompt: prompt); } try { @@ -464,8 +424,6 @@ $originalCode role: role, model: model, prompt: prompt, - aiGatewayBaseUrl: aiGatewayBaseUrl, - aiGatewayApiKey: aiGatewayApiKey, ); } return cliResult; @@ -476,8 +434,6 @@ $originalCode role: role, model: model, prompt: prompt, - aiGatewayBaseUrl: aiGatewayBaseUrl, - aiGatewayApiKey: aiGatewayApiKey, ); } return CliResult(output: '', error: e.toString(), exitCode: -1); @@ -606,15 +562,11 @@ ${selectedSkills.isEmpty ? '- 无' : selectedSkills.map((item) => '- $item').joi required MultiAgentRole role, required String model, required String prompt, - required String aiGatewayBaseUrl, - required String aiGatewayApiKey, }) async { if (role == MultiAgentRole.testerDoc) { final viaLlmChat = await runArisTesterViaLlmChatInternal( model: model, prompt: prompt, - aiGatewayBaseUrl: aiGatewayBaseUrl, - aiGatewayApiKey: aiGatewayApiKey, ); if (viaLlmChat.success) { return viaLlmChat; @@ -624,23 +576,17 @@ ${selectedSkills.isEmpty ? '- 无' : selectedSkills.map((item) => '- $item').joi role: role, model: model, prompt: prompt, - aiGatewayBaseUrl: aiGatewayBaseUrl, - aiGatewayApiKey: aiGatewayApiKey, ); } Future runArisTesterViaLlmChatInternal({ required String model, required String prompt, - required String aiGatewayBaseUrl, - required String aiGatewayApiKey, }) async { return runOpenAiCompatiblePromptInternal( role: MultiAgentRole.testerDoc, model: model, prompt: prompt, - aiGatewayBaseUrl: aiGatewayBaseUrl, - aiGatewayApiKey: aiGatewayApiKey, ); } @@ -655,8 +601,6 @@ ${selectedSkills.isEmpty ? '- 无' : selectedSkills.map((item) => '- $item').joi model: model, prompt: prompt, cwd: '', - aiGatewayBaseUrl: '', - aiGatewayApiKey: '', ); } return CliResult( @@ -670,21 +614,19 @@ ${selectedSkills.isEmpty ? '- 无' : selectedSkills.map((item) => '- $item').joi required MultiAgentRole role, required String model, required String prompt, - required String aiGatewayBaseUrl, - required String aiGatewayApiKey, }) async { final client = httpClientFactoryInternal(); activeHttpClientInternal = client; try { final request = await client.postUrl( Uri.parse( - '${openAiCompatibleBaseUrlInternal(aiGatewayBaseUrl: aiGatewayBaseUrl).replaceAll(RegExp(r'/$'), '')}/chat/completions', + '${openAiCompatibleBaseUrlInternal().replaceAll(RegExp(r'/$'), '')}/chat/completions', ), ); request.headers.set(HttpHeaders.contentTypeHeader, 'application/json'); request.headers.set( HttpHeaders.authorizationHeader, - 'Bearer ${openAiCompatibleApiKeyInternal(aiGatewayApiKey: aiGatewayApiKey)}', + 'Bearer ${openAiCompatibleApiKeyInternal()}', ); request.add( utf8.encode( diff --git a/lib/runtime/runtime_controllers_settings_account_impl.dart b/lib/runtime/runtime_controllers_settings_account_impl.dart index dd08100f..56db4a3c 100644 --- a/lib/runtime/runtime_controllers_settings_account_impl.dart +++ b/lib/runtime/runtime_controllers_settings_account_impl.dart @@ -280,7 +280,7 @@ Future syncAccountSettingsInternal( value: bridgeToken, ); - final bridgeServerUrl = bridgeServerUrlOverride.trim().isNotEmpty + final resolvedBridgeServerUrl = bridgeServerUrlOverride.trim().isNotEmpty ? bridgeServerUrlOverride.trim() : controller.accountSyncStateInternal?.syncedDefaults.bridgeServerUrl .trim() @@ -294,20 +294,33 @@ Future syncAccountSettingsInternal( .cloudSynced .remoteServerSummary .endpoint - .trim() - .isNotEmpty - ? controller - .snapshotInternal - .acpBridgeServerModeConfig - .cloudSynced - .remoteServerSummary - .endpoint - .trim() - : ''; - final resolvedBridgeServerUrl = - isSupportedExternalAcpEndpoint(bridgeServerUrl) - ? bridgeServerUrl - : kCanonicalBridgeAcpEndpoint; + .trim(); + if (!isSupportedExternalAcpEndpoint(resolvedBridgeServerUrl)) { + const result = AccountSyncResult( + state: 'blocked', + message: 'BRIDGE_SERVER_URL is unavailable', + ); + await controller.storeInternal.saveAccountSyncState( + AccountSyncState.defaults().copyWith( + syncState: result.state, + syncMessage: result.message, + lastSyncAtMs: DateTime.now().millisecondsSinceEpoch, + lastSyncError: result.message, + profileScope: 'bridge', + tokenConfigured: const AccountTokenConfigured( + bridge: true, + vault: false, + apisix: false, + ), + ), + ); + controller.accountStatusInternal = result.message; + if (!quiet) { + controller.accountBusyInternal = false; + controller.notifyListeners(); + } + return result; + } await controller.storeInternal.clearAccountManagedSecret( target: kAccountManagedSecretTargetAIGatewayAccessToken, ); diff --git a/lib/runtime/runtime_models_connection.dart b/lib/runtime/runtime_models_connection.dart index 56b296a2..5955ea1b 100644 --- a/lib/runtime/runtime_models_connection.dart +++ b/lib/runtime/runtime_models_connection.dart @@ -335,14 +335,27 @@ const List kPresetExternalAcpProviders = const String kCanonicalGatewayProviderId = 'openclaw'; const String kCanonicalGatewayProviderLabel = 'OpenClaw'; -const String kCanonicalBridgeAcpEndpoint = 'https://xworkmate-bridge.svc.plus'; -const List kKnownSingleAgentProviders = +const List kBridgeOwnedSingleAgentProviders = [ SingleAgentProvider.codex, SingleAgentProvider.opencode, - SingleAgentProvider.claude, SingleAgentProvider.gemini, ]; -const Set kLegacyExternalAcpProviderIds = {'claude'}; +bool isBridgeOwnedSingleAgentProviderId(String providerId) { + final normalized = normalizeSingleAgentProviderId(providerId); + return kBridgeOwnedSingleAgentProviders.any( + (item) => item.providerId == normalized, + ); +} + +List normalizeBridgeOwnedSingleAgentProviderList( + Iterable providers, +) { + return normalizeSingleAgentProviderList( + providers.where( + (provider) => isBridgeOwnedSingleAgentProviderId(provider.providerId), + ), + ); +} diff --git a/lib/runtime/runtime_models_profiles.dart b/lib/runtime/runtime_models_profiles.dart index a45e7726..83d1a218 100644 --- a/lib/runtime/runtime_models_profiles.dart +++ b/lib/runtime/runtime_models_profiles.dart @@ -62,7 +62,7 @@ class ExternalAcpEndpointProfile { SingleAgentProvider? get builtinProvider { final normalized = providerKey.trim().toLowerCase(); - for (final provider in kKnownSingleAgentProviders) { + for (final provider in kPresetExternalAcpProviders) { if (provider.providerId == normalized) { return provider; } @@ -131,14 +131,14 @@ List normalizeExternalAcpEndpoints({ ExternalAcpEndpointProfile profile, ) { final key = profile.providerKey.trim().toLowerCase(); - for (final provider in kKnownSingleAgentProviders) { + for (final provider in kPresetExternalAcpProviders) { if (provider.providerId == key) { return provider; } } final label = profile.label.trim(); final badge = profile.badge.trim(); - for (final provider in kKnownSingleAgentProviders) { + for (final provider in kPresetExternalAcpProviders) { if (provider.label == label && provider.badge == badge) { return provider; } @@ -153,12 +153,7 @@ List normalizeExternalAcpEndpoints({ if (key.isEmpty) { continue; } - if (kLegacyExternalAcpProviderIds.contains(originalKey) && - item.endpoint.trim().isEmpty) { - continue; - } - if (originalKey.startsWith('custom-agent-') && - canonicalProvider != null && + if (!isBridgeOwnedSingleAgentProviderId(originalKey) && item.endpoint.trim().isEmpty) { continue; } diff --git a/lib/runtime/runtime_models_settings_snapshot.dart b/lib/runtime/runtime_models_settings_snapshot.dart index 27936ce4..aea010dc 100644 --- a/lib/runtime/runtime_models_settings_snapshot.dart +++ b/lib/runtime/runtime_models_settings_snapshot.dart @@ -474,12 +474,10 @@ class SettingsSnapshot { if (resolved.isUnspecified) { return SingleAgentProvider.unspecified; } - if (kKnownSingleAgentProviders.any( - (item) => item.providerId == resolved.providerId, - )) { + if (isBridgeOwnedSingleAgentProviderId(resolved.providerId)) { return resolved; } - return resolved; + return SingleAgentProvider.unspecified; } SettingsSnapshot copyWithProviderSyncDefinitionForProvider( diff --git a/lib/widgets/assistant_focus_panel_previews.dart b/lib/widgets/assistant_focus_panel_previews.dart index 9e60baab..516aa86b 100644 --- a/lib/widgets/assistant_focus_panel_previews.dart +++ b/lib/widgets/assistant_focus_panel_previews.dart @@ -114,7 +114,7 @@ class SkillsFocusPreviewInternal extends StatelessWidget { if (items.isEmpty) { return PreviewEmptyStateInternal( message: typedController.isSingleAgentMode - ? (typedController.currentSingleAgentNeedsAiGatewayConfiguration + ? (typedController.currentSingleAgentNeedsBridgeProvider ? appText( '当前没有可用的 Bridge Provider,请先在设置里配置并同步连接。', 'No bridge provider is available. Configure and sync a connection in Settings first.', diff --git a/test/app_controller_desktop_runtime_cleanup_test.dart b/test/app_controller_desktop_runtime_cleanup_test.dart index 7c5a88ac..74efb1df 100644 --- a/test/app_controller_desktop_runtime_cleanup_test.dart +++ b/test/app_controller_desktop_runtime_cleanup_test.dart @@ -10,6 +10,7 @@ import 'package:xworkmate/runtime/desktop_platform_service.dart'; import 'package:xworkmate/runtime/go_task_service_client.dart'; import 'package:xworkmate/runtime/runtime_models.dart'; import 'package:xworkmate/runtime/secure_config_store.dart'; +import 'package:xworkmate/runtime/single_agent_capabilities.dart'; import 'package:xworkmate/runtime/skill_directory_access.dart'; void main() { @@ -114,10 +115,10 @@ void main() { ), goTaskServiceClient: const _FakeGoTaskServiceClient(), singleAgentSharedSkillScanRootOverrides: const [], - availableSingleAgentProvidersOverride: const [ - SingleAgentProvider.codex, - ], ); + _seedBridgeProviders(controller, const [ + SingleAgentProvider.codex, + ]); addTearDown(() async { controller.dispose(); await server.close(force: true); @@ -196,10 +197,10 @@ void main() { ), goTaskServiceClient: const _FakeGoTaskServiceClient(), singleAgentSharedSkillScanRootOverrides: const [], - availableSingleAgentProvidersOverride: const [ - SingleAgentProvider.codex, - ], ); + _seedBridgeProviders(controller, const [ + SingleAgentProvider.codex, + ]); addTearDown(() async { controller.dispose(); if (root.existsSync()) { @@ -217,23 +218,41 @@ void main() { executionTarget: AssistantExecutionTarget.singleAgent, ); + expect( + controller.singleAgentProviderForSession('draft:bridge-default'), + SingleAgentProvider.codex, + ); + expect( + controller.singleAgentResolvedProviderForSession( + 'draft:bridge-default', + ), + SingleAgentProvider.codex, + ); + final thread = controller.taskThreadForSessionInternal( 'draft:bridge-default', ); expect(thread, isNotNull); - expect( - thread!.executionBinding.providerId, - SingleAgentProvider.codex.providerId, - ); - expect( - thread.executionBinding.providerSource, - ThreadSelectionSource.inherited, - ); - expect(thread.hasExplicitProviderSelection, isFalse); + expect(thread!.hasExplicitProviderSelection, isFalse); }, ); } +void _seedBridgeProviders( + AppController controller, + List providers, +) { + controller.bridgeAdvertisedProvidersInternal = providers; + controller.singleAgentCapabilitiesByProviderInternal = { + for (final provider in providers) + provider: SingleAgentCapabilities( + available: true, + supportedProviders: [provider], + endpoint: 'bridge', + ), + }; +} + class _FakeSkillDirectoryAccessService implements SkillDirectoryAccessService { const _FakeSkillDirectoryAccessService(this.homeDirectory); @@ -326,8 +345,6 @@ class _FakeGoTaskServiceClient implements GoTaskServiceClient { required String taskPrompt, required String workingDirectory, required ExternalCodeAgentAcpRoutingConfig routing, - String aiGatewayBaseUrl = '', - String aiGatewayApiKey = '', }) async { return const ExternalCodeAgentAcpRoutingResolution( raw: { diff --git a/test/app_controller_desktop_thread_binding_test.dart b/test/app_controller_desktop_thread_binding_test.dart index 5a7cf37a..0485d78a 100644 --- a/test/app_controller_desktop_thread_binding_test.dart +++ b/test/app_controller_desktop_thread_binding_test.dart @@ -333,19 +333,16 @@ void main() { }); group('resolveGatewayAcpAuthorizationHeaderInternal', () { - test('resolves ACP endpoint through the canonical bridge entry', () { + test('uses only synced or persisted BRIDGE_SERVER_URL values', () { final controller = AppController(); addTearDown(controller.dispose); - expect( - controller.resolveBridgeAcpEndpointInternal(), - Uri.parse(kCanonicalBridgeAcpEndpoint), - ); + expect(controller.resolveBridgeAcpEndpointInternal(), isNull); expect( controller.resolveExternalAcpEndpointForTargetInternal( AssistantExecutionTarget.singleAgent, ), - Uri.parse(kCanonicalBridgeAcpEndpoint), + isNull, ); controller.settingsController.snapshotInternal = controller.settings @@ -370,19 +367,19 @@ void main() { expect( controller.resolveBridgeAcpEndpointInternal(), - Uri.parse(kCanonicalBridgeAcpEndpoint), + Uri.parse('https://bridge.customer.example/acp'), ); expect( controller.resolveExternalAcpEndpointForTargetInternal( AssistantExecutionTarget.singleAgent, ), - Uri.parse(kCanonicalBridgeAcpEndpoint), + Uri.parse('https://bridge.customer.example/acp'), ); expect( controller.resolveExternalAcpEndpointForTargetInternal( AssistantExecutionTarget.gateway, ), - Uri.parse(kCanonicalBridgeAcpEndpoint), + Uri.parse('https://bridge.customer.example/acp'), ); }); @@ -442,7 +439,7 @@ void main() { ); expect(bridgeAuthorization, 'Bearer bridge-token'); - expect(nonBridgeAuthorization, 'Bearer local-token'); + expect(nonBridgeAuthorization, isNull); }, ); }); diff --git a/test/app_controller_desktop_working_directory_dispatch_test.dart b/test/app_controller_desktop_working_directory_dispatch_test.dart index ca577374..0b774205 100644 --- a/test/app_controller_desktop_working_directory_dispatch_test.dart +++ b/test/app_controller_desktop_working_directory_dispatch_test.dart @@ -9,6 +9,7 @@ import 'package:xworkmate/app/app_controller_desktop_workspace_execution.dart'; import 'package:xworkmate/runtime/go_task_service_client.dart'; import 'package:xworkmate/runtime/runtime_models.dart'; import 'package:xworkmate/runtime/secure_config_store.dart'; +import 'package:xworkmate/runtime/single_agent_capabilities.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -49,10 +50,10 @@ void main() { final controller = AppController( store: store, goTaskServiceClient: client, - availableSingleAgentProvidersOverride: const [ - SingleAgentProvider.codex, - ], ); + _seedBridgeProviders(controller, const [ + SingleAgentProvider.codex, + ]); addTearDown(() async { controller.dispose(); store.dispose(); @@ -101,16 +102,33 @@ void main() { ); test( - 'single-agent turns go through the canonical bridge entry without synced endpoint state', + 'single-agent turns stop before dispatch when BRIDGE_SERVER_URL is missing', () async { + final root = await Directory.systemTemp.createTemp( + 'xworkmate-missing-bridge-server-', + ); + final store = SecureConfigStore( + enableSecureStorage: false, + appDataRootPathResolver: () async => '${root.path}/settings.sqlite3', + secretRootPathResolver: () async => root.path, + supportRootPathResolver: () async => root.path, + ); + await store.initialize(); final client = _CapturingGoTaskServiceClient(); final controller = AppController( + store: store, goTaskServiceClient: client, - availableSingleAgentProvidersOverride: const [ - SingleAgentProvider.codex, - ], ); - addTearDown(controller.dispose); + _seedBridgeProviders(controller, const [ + SingleAgentProvider.codex, + ]); + addTearDown(() async { + controller.dispose(); + store.dispose(); + if (await root.exists()) { + await root.delete(recursive: true); + } + }); const sessionKey = 'draft:single-agent-missing-bridge-server'; controller.initializeAssistantThreadContext( @@ -121,18 +139,15 @@ void main() { await controller.sendChatMessage('first turn'); - expect(client.requests, hasLength(1)); - expect(client.requests.single.sessionId, sessionKey); - expect(client.requests.single.threadId, sessionKey); + expect(client.requests, isEmpty); }, ); test('each task thread keeps an independent workingDirectory', () async { - final controller = AppController( - availableSingleAgentProvidersOverride: const [ - SingleAgentProvider.codex, - ], - ); + final controller = AppController(); + _seedBridgeProviders(controller, const [ + SingleAgentProvider.codex, + ]); addTearDown(controller.dispose); const sessionKey = 'draft:thread-working-directory-a'; @@ -166,12 +181,11 @@ void main() { }); test('new task threads do not inherit another thread provider choice', () { - final controller = AppController( - availableSingleAgentProvidersOverride: const [ - SingleAgentProvider.codex, - SingleAgentProvider.gemini, - ], - ); + final controller = AppController(); + _seedBridgeProviders(controller, const [ + SingleAgentProvider.codex, + SingleAgentProvider.gemini, + ]); addTearDown(controller.dispose); const firstSessionKey = 'draft:thread-provider-a'; @@ -199,6 +213,21 @@ void main() { }); } +void _seedBridgeProviders( + AppController controller, + List providers, +) { + controller.bridgeAdvertisedProvidersInternal = providers; + controller.singleAgentCapabilitiesByProviderInternal = { + for (final provider in providers) + provider: SingleAgentCapabilities( + available: true, + supportedProviders: [provider], + endpoint: 'bridge', + ), + }; +} + class _CapturingGoTaskServiceClient implements GoTaskServiceClient { final List requests = []; int resolveExternalAcpRoutingCallCount = 0; @@ -269,8 +298,6 @@ class _CapturingGoTaskServiceClient implements GoTaskServiceClient { required String taskPrompt, required String workingDirectory, required ExternalCodeAgentAcpRoutingConfig routing, - String aiGatewayBaseUrl = '', - String aiGatewayApiKey = '', }) async { resolveExternalAcpRoutingCallCount += 1; return const ExternalCodeAgentAcpRoutingResolution( diff --git a/test/assistant_execution_target_picker_widget_test.dart b/test/assistant_execution_target_picker_widget_test.dart index 584413a7..d37ca2db 100644 --- a/test/assistant_execution_target_picker_widget_test.dart +++ b/test/assistant_execution_target_picker_widget_test.dart @@ -11,6 +11,7 @@ import 'package:xworkmate/runtime/desktop_platform_service.dart'; import 'package:xworkmate/runtime/go_task_service_client.dart'; import 'package:xworkmate/runtime/runtime_models.dart'; import 'package:xworkmate/runtime/secure_config_store.dart'; +import 'package:xworkmate/runtime/single_agent_capabilities.dart'; import 'package:xworkmate/runtime/skill_directory_access.dart'; import 'package:xworkmate/theme/app_theme.dart'; @@ -37,10 +38,10 @@ void main() { ), goTaskServiceClient: const _FakeGoTaskServiceClient(), singleAgentSharedSkillScanRootOverrides: const [], - availableSingleAgentProvidersOverride: const [ - SingleAgentProvider.codex, - ], ); + _seedBridgeProviders(controller, const [ + SingleAgentProvider.codex, + ]); final inputController = TextEditingController(); final focusNode = FocusNode(); addTearDown(() async { @@ -152,10 +153,10 @@ void main() { skillDirectoryAccessService: _FakeSkillDirectoryAccessService(root.path), goTaskServiceClient: const _FakeGoTaskServiceClient(), singleAgentSharedSkillScanRootOverrides: const [], - availableSingleAgentProvidersOverride: const [ - SingleAgentProvider.codex, - ], ); + _seedBridgeProviders(controller, const [ + SingleAgentProvider.codex, + ]); final inputController = TextEditingController(); final focusNode = FocusNode(); addTearDown(() async { @@ -239,6 +240,21 @@ void main() { }); } +void _seedBridgeProviders( + AppController controller, + List providers, +) { + controller.bridgeAdvertisedProvidersInternal = providers; + controller.singleAgentCapabilitiesByProviderInternal = { + for (final provider in providers) + provider: SingleAgentCapabilities( + available: true, + supportedProviders: [provider], + endpoint: 'bridge', + ), + }; +} + class _FakeSkillDirectoryAccessService implements SkillDirectoryAccessService { const _FakeSkillDirectoryAccessService(this.homeDirectory); @@ -317,8 +333,6 @@ class _FakeGoTaskServiceClient implements GoTaskServiceClient { required String taskPrompt, required String workingDirectory, required ExternalCodeAgentAcpRoutingConfig routing, - String aiGatewayBaseUrl = '', - String aiGatewayApiKey = '', }) async { return const ExternalCodeAgentAcpRoutingResolution( raw: { diff --git a/test/features/assistant/assistant_page_composer_golden_test.dart b/test/features/assistant/assistant_page_composer_golden_test.dart index 1b818e5d..6d6c66c9 100644 --- a/test/features/assistant/assistant_page_composer_golden_test.dart +++ b/test/features/assistant/assistant_page_composer_golden_test.dart @@ -10,6 +10,7 @@ import 'package:xworkmate/runtime/desktop_platform_service.dart'; import 'package:xworkmate/runtime/go_task_service_client.dart'; import 'package:xworkmate/runtime/runtime_models.dart'; import 'package:xworkmate/runtime/secure_config_store.dart'; +import 'package:xworkmate/runtime/single_agent_capabilities.dart'; import 'package:xworkmate/runtime/skill_directory_access.dart'; import 'package:xworkmate/theme/app_theme.dart'; @@ -39,10 +40,10 @@ void main() { ), goTaskServiceClient: const _GoldenGoTaskServiceClient(), singleAgentSharedSkillScanRootOverrides: const [], - availableSingleAgentProvidersOverride: const [ - SingleAgentProvider.codex, - ], ); + _seedBridgeProviders(controller, const [ + SingleAgentProvider.codex, + ]); final inputController = TextEditingController(text: '请整理今天的任务进展'); final focusNode = FocusNode(); @@ -112,6 +113,21 @@ void main() { }); } +void _seedBridgeProviders( + AppController controller, + List providers, +) { + controller.bridgeAdvertisedProvidersInternal = providers; + controller.singleAgentCapabilitiesByProviderInternal = { + for (final provider in providers) + provider: SingleAgentCapabilities( + available: true, + supportedProviders: [provider], + endpoint: 'bridge', + ), + }; +} + class _GoldenSkillDirectoryAccessService implements SkillDirectoryAccessService { const _GoldenSkillDirectoryAccessService(this.homeDirectory); @@ -205,8 +221,6 @@ class _GoldenGoTaskServiceClient implements GoTaskServiceClient { required String taskPrompt, required String workingDirectory, required ExternalCodeAgentAcpRoutingConfig routing, - String aiGatewayBaseUrl = '', - String aiGatewayApiKey = '', }) async { return const ExternalCodeAgentAcpRoutingResolution( raw: { diff --git a/test/runtime/bridge_real_e2e_test.dart b/test/runtime/bridge_real_e2e_test.dart index 328869d2..1719c440 100644 --- a/test/runtime/bridge_real_e2e_test.dart +++ b/test/runtime/bridge_real_e2e_test.dart @@ -32,9 +32,7 @@ void main() { late ExternalCodeAgentAcpDesktopTransport transport; setUpAll(() async { - if (!runRealE2E || - bridgeAuthToken.isEmpty || - bridgeAcpEndpoint.isEmpty) { + if (!runRealE2E || bridgeAuthToken.isEmpty || bridgeAcpEndpoint.isEmpty) { return; } final client = GatewayAcpClient( @@ -69,9 +67,7 @@ void main() { }); test('loads external ACP capabilities and provider catalog', () async { - if (!runRealE2E || - bridgeAuthToken.isEmpty || - bridgeAcpEndpoint.isEmpty) { + if (!runRealE2E || bridgeAuthToken.isEmpty || bridgeAcpEndpoint.isEmpty) { return; } final capabilities = await transport.loadExternalAcpCapabilities( @@ -350,8 +346,6 @@ GoTaskServiceRequest _buildRequest({ selectedSkills: selectedSkills, inlineAttachments: const [], localAttachments: const [], - aiGatewayBaseUrl: '', - aiGatewayApiKey: '', agentId: '', metadata: const {}, routing: ExternalCodeAgentAcpRoutingConfig( diff --git a/test/runtime/external_acp_bridge_sync_order_test.dart b/test/runtime/external_acp_bridge_sync_order_test.dart index 42aaf2d7..93c2a3a5 100644 --- a/test/runtime/external_acp_bridge_sync_order_test.dart +++ b/test/runtime/external_acp_bridge_sync_order_test.dart @@ -72,8 +72,6 @@ void main() { selectedSkills: [], inlineAttachments: [], localAttachments: [], - aiGatewayBaseUrl: '', - aiGatewayApiKey: '', agentId: '', metadata: {}, ), diff --git a/test/runtime/settings_account_auth_flow_test.dart b/test/runtime/settings_account_auth_flow_test.dart index 393d973a..79d3f7c8 100644 --- a/test/runtime/settings_account_auth_flow_test.dart +++ b/test/runtime/settings_account_auth_flow_test.dart @@ -135,7 +135,7 @@ void main() { }); test( - 'login still syncs bridge access when sync data omits bridge server', + 'login blocks bridge sync when sync data omits BRIDGE_SERVER_URL', () async { final root = await Directory.systemTemp.createTemp( 'xworkmate-account-auth-missing-bridge-server-', @@ -179,10 +179,10 @@ void main() { controller.accountStatus, 'Signed in as review@customer.example', ); - expect(controller.accountSyncState?.syncState, 'ready'); + expect(controller.accountSyncState?.syncState, 'blocked'); expect( controller.accountSyncState?.syncMessage, - 'Bridge access synced', + 'BRIDGE_SERVER_URL is unavailable', ); expect( controller @@ -191,7 +191,7 @@ void main() { .cloudSynced .remoteServerSummary .endpoint, - kCanonicalBridgeAcpEndpoint, + isEmpty, ); expect( await store.loadAccountManagedSecret(