diff --git a/lib/app/app_controller_desktop_runtime_helpers.dart b/lib/app/app_controller_desktop_runtime_helpers.dart index 0ac73a61..94261002 100644 --- a/lib/app/app_controller_desktop_runtime_helpers.dart +++ b/lib/app/app_controller_desktop_runtime_helpers.dart @@ -736,16 +736,7 @@ extension AppControllerDesktopRuntimeHelpers on AppController { } Uri? resolveBridgeAcpEndpointInternal() { - final rawEndpoint = settings - .acpBridgeServerModeConfig - .cloudSynced - .remoteServerSummary - .endpoint - .trim(); - if (rawEndpoint.isEmpty) { - return null; - } - final uri = Uri.tryParse(rawEndpoint); + final uri = Uri.tryParse(kCanonicalBridgeAcpEndpoint); final scheme = uri?.scheme.trim().toLowerCase() ?? ''; if (uri == null || !kSupportedExternalAcpEndpointSchemes.contains(scheme)) { 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 cca798b5..f69f421f 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 @@ -102,8 +102,8 @@ Future sendSingleAgentMessageDesktopGoTaskFlowInternal( ) == null ? appText( - '当前线程还没有同步到 Bridge Server。请先登录账号并在设置里完成同步后再重试。', - 'This thread does not have a synced bridge server yet. Sign in and complete Settings sync before trying again.', + 'Bridge ACP 入口当前不可用。', + 'The bridge ACP entrypoint is currently unavailable.', ) : null; if (unavailableReason != null) { 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 6d24c288..f9eeb1d9 100644 --- a/lib/app/app_controller_desktop_single_agent_status_messages.dart +++ b/lib/app/app_controller_desktop_single_agent_status_messages.dart @@ -96,12 +96,12 @@ String singleAgentUnavailableLabelDesktopInternal( )) { return detail.isEmpty ? appText( - '当前没有可用的 Bridge Provider。请先在设置里配置并同步外部 Agent 连接。', - 'No bridge provider is available. Configure and sync an external agent connection in Settings first.', + 'Bridge 当前没有可用 Provider。', + 'The bridge does not currently advertise any available providers.', ) : appText( - '$detail 当前没有可用的 Bridge Provider。请先在设置里配置并同步外部 Agent 连接。', - '$detail No bridge provider is available. Configure and sync an external agent connection in Settings first.', + '$detail Bridge 当前没有可用 Provider。', + '$detail The bridge does not currently advertise any available providers.', ); } return detail.isEmpty diff --git a/lib/app/app_controller_desktop_thread_sessions.dart b/lib/app/app_controller_desktop_thread_sessions.dart index 80e13b04..6409511a 100644 --- a/lib/app/app_controller_desktop_thread_sessions.dart +++ b/lib/app/app_controller_desktop_thread_sessions.dart @@ -423,8 +423,8 @@ extension AppControllerDesktopThreadSessions on AppController { normalizedSessionKey, ) ? appText( - '当前没有可用的 Bridge Provider。请先在设置里配置并同步可用连接。', - 'No bridge provider is currently available. Configure and sync an available upstream connection in Settings first.', + 'Bridge 当前没有可用 Provider。', + 'The bridge does not currently advertise any available providers.', ) : appText( '当前线程的 Bridge Provider 尚未就绪。', diff --git a/lib/app/app_controller_desktop_workspace_execution.dart b/lib/app/app_controller_desktop_workspace_execution.dart index c86efa47..53b8d9b1 100644 --- a/lib/app/app_controller_desktop_workspace_execution.dart +++ b/lib/app/app_controller_desktop_workspace_execution.dart @@ -360,7 +360,14 @@ extension AppControllerDesktopWorkspaceExecution on AppController { assistantMessageViewModeForSession(currentSessionKey), singleAgentProvider: singleAgentProvider ?? - singleAgentProviderForSession(currentSessionKey), + settings.sanitizeSingleAgentProviderSelection( + SingleAgentProviderCopy.fromJsonValue( + assistantThreadRecordsInternal[normalizedSessionKey] + ?.executionBinding + .providerId ?? + '', + ), + ), singleAgentProviderSource: ThreadSelectionSource.inherited, updatedAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(), ); diff --git a/lib/app/app_shell_desktop.dart b/lib/app/app_shell_desktop.dart index 57aa0cfc..9b833bb0 100644 --- a/lib/app/app_shell_desktop.dart +++ b/lib/app/app_shell_desktop.dart @@ -94,7 +94,7 @@ class _AppShellState extends State { title: appText('新对话', 'New conversation'), executionTarget: target, messageViewMode: controller.currentAssistantMessageViewMode, - singleAgentProvider: controller.currentSingleAgentProvider, + singleAgentProvider: SingleAgentProvider.unspecified, ); controller.navigateTo(WorkspaceDestination.assistant); await controller.switchSession(sessionKey); diff --git a/lib/runtime/runtime_controllers_settings_account_impl.dart b/lib/runtime/runtime_controllers_settings_account_impl.dart index 3336452d..dd08100f 100644 --- a/lib/runtime/runtime_controllers_settings_account_impl.dart +++ b/lib/runtime/runtime_controllers_settings_account_impl.dart @@ -304,35 +304,10 @@ Future syncAccountSettingsInternal( .endpoint .trim() : ''; - if (bridgeServerUrl.isEmpty || - !isSupportedExternalAcpEndpoint(bridgeServerUrl)) { - const result = AccountSyncResult( - state: 'blocked', - message: 'Bridge server is unavailable', - ); - await controller.storeInternal.saveAccountSyncState( - AccountSyncState.defaults().copyWith( - syncedDefaults: AccountRemoteProfile.defaults(), - syncState: result.state, - syncMessage: result.message, - lastSyncAtMs: DateTime.now().millisecondsSinceEpoch, - lastSyncError: result.message, - profileScope: 'bridge', - tokenConfigured: const AccountTokenConfigured( - bridge: true, - vault: false, - apisix: false, - ), - ), - ); - await controller.reloadDerivedStateInternal(); - controller.accountStatusInternal = result.message; - if (!quiet) { - controller.accountBusyInternal = false; - controller.notifyListeners(); - } - return result; - } + final resolvedBridgeServerUrl = + isSupportedExternalAcpEndpoint(bridgeServerUrl) + ? bridgeServerUrl + : kCanonicalBridgeAcpEndpoint; await controller.storeInternal.clearAccountManagedSecret( target: kAccountManagedSecretTargetAIGatewayAccessToken, ); @@ -342,12 +317,12 @@ Future syncAccountSettingsInternal( final nextState = AccountSyncState.defaults().copyWith( syncedDefaults: AccountRemoteProfile.defaults().copyWith( - bridgeServerUrl: bridgeServerUrl, + bridgeServerUrl: resolvedBridgeServerUrl, ), syncState: 'ready', syncMessage: 'Bridge access synced', lastSyncAtMs: DateTime.now().millisecondsSinceEpoch, - lastSyncSource: bridgeServerUrl, + lastSyncSource: resolvedBridgeServerUrl, lastSyncError: '', profileScope: 'bridge', tokenConfigured: const AccountTokenConfigured( @@ -365,7 +340,10 @@ Future syncAccountSettingsInternal( accountIdentifier: '', lastSyncAt: nextState.lastSyncAtMs, remoteServerSummary: currentModeConfig.cloudSynced.remoteServerSummary - .copyWith(endpoint: bridgeServerUrl, hasAdvancedOverrides: false), + .copyWith( + endpoint: resolvedBridgeServerUrl, + hasAdvancedOverrides: false, + ), ), ); final sanitizedSettings = _sanitizeBridgeOnlyAccountSyncSettings( diff --git a/lib/runtime/runtime_models_connection.dart b/lib/runtime/runtime_models_connection.dart index a68e84a3..56b296a2 100644 --- a/lib/runtime/runtime_models_connection.dart +++ b/lib/runtime/runtime_models_connection.dart @@ -335,6 +335,7 @@ const List kPresetExternalAcpProviders = const String kCanonicalGatewayProviderId = 'openclaw'; const String kCanonicalGatewayProviderLabel = 'OpenClaw'; +const String kCanonicalBridgeAcpEndpoint = 'https://xworkmate-bridge.svc.plus'; const List kKnownSingleAgentProviders = [ diff --git a/test/app_controller_desktop_thread_binding_test.dart b/test/app_controller_desktop_thread_binding_test.dart index 8c202707..5a7cf37a 100644 --- a/test/app_controller_desktop_thread_binding_test.dart +++ b/test/app_controller_desktop_thread_binding_test.dart @@ -333,16 +333,19 @@ void main() { }); group('resolveGatewayAcpAuthorizationHeaderInternal', () { - test('requires synced bridge endpoint before ACP endpoint can resolve', () { + test('resolves ACP endpoint through the canonical bridge entry', () { final controller = AppController(); addTearDown(controller.dispose); - expect(controller.resolveBridgeAcpEndpointInternal(), isNull); + expect( + controller.resolveBridgeAcpEndpointInternal(), + Uri.parse(kCanonicalBridgeAcpEndpoint), + ); expect( controller.resolveExternalAcpEndpointForTargetInternal( AssistantExecutionTarget.singleAgent, ), - isNull, + Uri.parse(kCanonicalBridgeAcpEndpoint), ); controller.settingsController.snapshotInternal = controller.settings @@ -367,19 +370,19 @@ void main() { expect( controller.resolveBridgeAcpEndpointInternal(), - Uri.parse('https://bridge.customer.example/acp'), + Uri.parse(kCanonicalBridgeAcpEndpoint), ); expect( controller.resolveExternalAcpEndpointForTargetInternal( AssistantExecutionTarget.singleAgent, ), - Uri.parse('https://bridge.customer.example/acp'), + Uri.parse(kCanonicalBridgeAcpEndpoint), ); expect( controller.resolveExternalAcpEndpointForTargetInternal( AssistantExecutionTarget.gateway, ), - Uri.parse('https://bridge.customer.example/acp'), + Uri.parse(kCanonicalBridgeAcpEndpoint), ); }); diff --git a/test/app_controller_desktop_working_directory_dispatch_test.dart b/test/app_controller_desktop_working_directory_dispatch_test.dart index b648eb29..ca577374 100644 --- a/test/app_controller_desktop_working_directory_dispatch_test.dart +++ b/test/app_controller_desktop_working_directory_dispatch_test.dart @@ -101,7 +101,7 @@ void main() { ); test( - 'single-agent turns stay blocked until bridge server has been synced', + 'single-agent turns go through the canonical bridge entry without synced endpoint state', () async { final client = _CapturingGoTaskServiceClient(); final controller = AppController( @@ -121,12 +121,9 @@ void main() { await controller.sendChatMessage('first turn'); - expect(client.requests, isEmpty); - final messages = controller - .requireTaskThreadForSessionInternal(sessionKey) - .messages; - expect(messages, isNotEmpty); - expect(messages.last.text, contains('Bridge Server')); + expect(client.requests, hasLength(1)); + expect(client.requests.single.sessionId, sessionKey); + expect(client.requests.single.threadId, sessionKey); }, ); @@ -167,6 +164,38 @@ void main() { isNot(recordB.workspaceBinding.workspacePath), ); }); + + test('new task threads do not inherit another thread provider choice', () { + final controller = AppController( + availableSingleAgentProvidersOverride: const [ + SingleAgentProvider.codex, + SingleAgentProvider.gemini, + ], + ); + addTearDown(controller.dispose); + + const firstSessionKey = 'draft:thread-provider-a'; + const secondSessionKey = 'draft:thread-provider-b'; + + controller.initializeAssistantThreadContext( + firstSessionKey, + executionTarget: AssistantExecutionTarget.singleAgent, + singleAgentProvider: SingleAgentProvider.gemini, + ); + controller.initializeAssistantThreadContext( + secondSessionKey, + executionTarget: AssistantExecutionTarget.singleAgent, + ); + + expect( + controller.singleAgentProviderForSession(firstSessionKey), + SingleAgentProvider.gemini, + ); + expect( + controller.singleAgentProviderForSession(secondSessionKey), + SingleAgentProvider.codex, + ); + }); }); } diff --git a/test/assistant_execution_target_picker_widget_test.dart b/test/assistant_execution_target_picker_widget_test.dart index 47cbf496..584413a7 100644 --- a/test/assistant_execution_target_picker_widget_test.dart +++ b/test/assistant_execution_target_picker_widget_test.dart @@ -220,6 +220,19 @@ void main() { find.byKey(const Key('assistant-gateway-provider-button')), findsOneWidget, ); + final gatewayButton = tester.widget>( + find.byKey(const Key('assistant-gateway-provider-button')), + ); + final items = gatewayButton.itemBuilder( + tester.element( + find.byKey(const Key('assistant-gateway-provider-button')), + ), + ); + expect(items, hasLength(1)); + expect( + items.whereType>().single.value, + kCanonicalGatewayProviderId, + ); await tester.pumpWidget(const SizedBox.shrink()); await tester.pump(); diff --git a/test/runtime/settings_account_auth_flow_test.dart b/test/runtime/settings_account_auth_flow_test.dart index 699e0239..393d973a 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 stays blocked when bridge server is not included in sync data', + 'login still syncs bridge access when sync data omits bridge server', () 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, 'blocked'); + expect(controller.accountSyncState?.syncState, 'ready'); expect( controller.accountSyncState?.syncMessage, - 'Bridge server is unavailable', + 'Bridge access synced', ); expect( controller @@ -191,7 +191,7 @@ void main() { .cloudSynced .remoteServerSummary .endpoint, - isEmpty, + kCanonicalBridgeAcpEndpoint, ); expect( await store.loadAccountManagedSecret(