diff --git a/lib/app/app_controller_desktop_thread_sessions.dart b/lib/app/app_controller_desktop_thread_sessions.dart index 65b1a760..2c3091a5 100644 --- a/lib/app/app_controller_desktop_thread_sessions.dart +++ b/lib/app/app_controller_desktop_thread_sessions.dart @@ -55,14 +55,20 @@ AssistantThreadConnectionState resolveGatewayThreadConnectionStateInternal({ final bridgeAddress = connection.remoteAddress?.trim() ?? ''; final rawStatus = connection.status; final gatewayTokenMissing = connection.gatewayTokenMissing; + final missingEndpoint = + (connection.lastErrorCode?.trim().toUpperCase() ?? '') == + 'MISSING_ENDPOINT'; final hasFailureEvidence = - rawStatus == RuntimeConnectionStatus.error || - (connection.lastError?.trim().isNotEmpty ?? false) || - (connection.lastErrorCode?.trim().isNotEmpty ?? false) || - (connection.lastErrorDetailCode?.trim().isNotEmpty ?? false); + !missingEndpoint && + (rawStatus == RuntimeConnectionStatus.error || + (connection.lastError?.trim().isNotEmpty ?? false) || + (connection.lastErrorCode?.trim().isNotEmpty ?? false) || + (connection.lastErrorDetailCode?.trim().isNotEmpty ?? false)); final genericFailure = !gatewayTokenMissing && hasFailureEvidence; final status = gatewayTokenMissing || genericFailure ? RuntimeConnectionStatus.error + : missingEndpoint + ? RuntimeConnectionStatus.offline : rawStatus; final primaryLabel = gatewayTokenMissing ? appText('缺少令牌', 'Missing Token') diff --git a/lib/runtime/gateway_runtime_core.dart b/lib/runtime/gateway_runtime_core.dart index ae484d22..8129ec31 100644 --- a/lib/runtime/gateway_runtime_core.dart +++ b/lib/runtime/gateway_runtime_core.dart @@ -104,7 +104,8 @@ class GatewayRuntime extends ChangeNotifier with GatewayRuntimeHelpersInternal { bool get usesSessionClient => sessionClientInternal != null; @visibleForTesting - GatewayRuntimeSessionClient? get sessionClientForTest => sessionClientInternal; + GatewayRuntimeSessionClient? get sessionClientForTest => + sessionClientInternal; Future initialize() async { sessionUpdatesInternal ??= sessionClientInternal?.updates.listen( @@ -234,9 +235,6 @@ class GatewayRuntime extends ChangeNotifier with GatewayRuntimeHelpersInternal { ); snapshotInternal = GatewayConnectionSnapshot.initial(mode: profile.mode) .copyWith( - statusText: 'Missing gateway endpoint', - lastError: 'Configure setup code or manual host / port first.', - lastErrorCode: 'MISSING_ENDPOINT', deviceId: identity.deviceId, connectAuthMode: connectAuthMode, connectAuthFields: connectAuthFields, diff --git a/lib/runtime/runtime_controllers_settings_account_impl.dart b/lib/runtime/runtime_controllers_settings_account_impl.dart index 35313de5..d98c426f 100644 --- a/lib/runtime/runtime_controllers_settings_account_impl.dart +++ b/lib/runtime/runtime_controllers_settings_account_impl.dart @@ -505,6 +505,12 @@ String _resolveBridgeAuthorizationToken(Map payload) { if (explicit.isNotEmpty) { return explicit; } + final uppercaseInternalServiceToken = _stringValue( + payload['INTERNAL_SERVICE_TOKEN'], + ); + if (uppercaseInternalServiceToken.isNotEmpty) { + return uppercaseInternalServiceToken; + } final internalServiceToken = _stringValue(payload['internalServiceToken']); if (internalServiceToken.isNotEmpty) { return internalServiceToken; diff --git a/test/features/assistant/assistant_connection_status_test.dart b/test/features/assistant/assistant_connection_status_test.dart index 47fe9fb5..be5bcf4b 100644 --- a/test/features/assistant/assistant_connection_status_test.dart +++ b/test/features/assistant/assistant_connection_status_test.dart @@ -110,6 +110,45 @@ void main() { findsOneWidget, ); }); + + testWidgets( + 'treats missing endpoint as offline and hides stale english setup copy', + (tester) async { + final controller = AppController(); + addTearDown(controller.dispose); + + await controller.sessionsController.switchSession('session-1'); + controller.runtimeInternal.snapshotInternal = controller + .runtimeInternal + .snapshot + .copyWith( + status: RuntimeConnectionStatus.error, + statusText: 'Missing gateway endpoint', + lastError: 'Configure setup code or manual host / port first.', + lastErrorCode: 'MISSING_ENDPOINT', + clearRemoteAddress: true, + ); + + await tester.binding.setSurfaceSize(const Size(1440, 960)); + addTearDown(() async { + await tester.binding.setSurfaceSize(null); + }); + + await tester.pumpWidget(_buildAssistantPage(controller)); + await tester.pump(const Duration(milliseconds: 100)); + + expect(find.text('Bridge 连接失败'), findsNothing); + expect(find.text('先连接 Bridge'), findsOneWidget); + expect( + find.text('Configure setup code or manual host / port first.'), + findsNothing, + ); + expect( + find.text('当前 xworkmate-bridge 尚未连接。请先恢复 bridge 连接,再继续当前任务。'), + findsOneWidget, + ); + }, + ); }); } diff --git a/test/runtime/assistant_connection_state_test.dart b/test/runtime/assistant_connection_state_test.dart index c0b54d8c..a76aeed8 100644 --- a/test/runtime/assistant_connection_state_test.dart +++ b/test/runtime/assistant_connection_state_test.dart @@ -111,6 +111,35 @@ void main() { }, ); + test( + 'treats missing endpoint as true offline instead of bridge failure', + () async { + final controller = AppController(); + addTearDown(controller.dispose); + + await controller.sessionsController.switchSession('session-1'); + await controller.setAssistantExecutionTarget( + AssistantExecutionTarget.gateway, + ); + + controller.runtimeInternal.snapshotInternal = controller + .runtimeInternal + .snapshot + .copyWith( + status: RuntimeConnectionStatus.error, + statusText: 'Missing gateway endpoint', + lastError: 'Configure setup code or manual host / port first.', + lastErrorCode: 'MISSING_ENDPOINT', + clearRemoteAddress: true, + ); + + final state = controller.currentAssistantConnectionState; + expect(state.status, RuntimeConnectionStatus.offline); + expect(state.primaryLabel, '离线'); + expect(state.detailLabel, 'xworkmate-bridge 未连接'); + }, + ); + test('desktop snapshot uses derived assistant connection labels', () async { final controller = AppController(); addTearDown(controller.dispose); diff --git a/test/runtime/runtime_controllers_settings_account_test.dart b/test/runtime/runtime_controllers_settings_account_test.dart index b050a136..e9b1f5cb 100644 --- a/test/runtime/runtime_controllers_settings_account_test.dart +++ b/test/runtime/runtime_controllers_settings_account_test.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'package:flutter_test/flutter_test.dart'; +import 'package:xworkmate/runtime/account_runtime_client.dart'; import 'package:xworkmate/runtime/runtime_controllers.dart'; import 'package:xworkmate/runtime/runtime_models.dart'; import 'package:xworkmate/runtime/secure_config_store.dart'; @@ -58,6 +59,64 @@ void main() { }, ); + test( + 'login sync accepts INTERNAL_SERVICE_TOKEN payload for managed bridge auth', + () async { + final storeRoot = await Directory.systemTemp.createTemp( + 'xworkmate-account-sync-uppercase-token-', + ); + addTearDown(() async { + if (await storeRoot.exists()) { + await storeRoot.delete(recursive: true); + } + }); + + final store = SecureConfigStore( + secretRootPathResolver: () async => '${storeRoot.path}/secrets', + appDataRootPathResolver: () async => '${storeRoot.path}/app-data', + supportRootPathResolver: () async => '${storeRoot.path}/support', + enableSecureStorage: false, + ); + await store.initialize(); + await store.saveSettingsSnapshot( + SettingsSnapshot.defaults().copyWith( + accountBaseUrl: 'https://accounts.svc.plus', + ), + ); + + final controller = SettingsController( + store, + accountClientFactory: (_) => _FakeAccountRuntimeClient( + loginPayload: { + 'token': 'session-token', + 'INTERNAL_SERVICE_TOKEN': 'bridge-token-from-login', + 'user': { + 'id': 'user-1', + 'email': 'review@svc.plus', + }, + }, + ), + ); + addTearDown(controller.dispose); + await controller.initialize(); + + await controller.loginAccount( + baseUrl: 'https://accounts.svc.plus', + identifier: 'review@svc.plus', + password: 'password', + ); + + expect(controller.accountSyncState, isNotNull); + expect(controller.accountSyncState!.syncState, 'ready'); + expect( + await store.loadAccountManagedSecret( + target: kAccountManagedSecretTargetBridgeAuthToken, + ), + 'bridge-token-from-login', + ); + }, + ); + test('syncAccountSettings pins the managed bridge cloud entry', () async { final storeRoot = await Directory.systemTemp.createTemp( 'xworkmate-account-managed-bridge-', @@ -179,3 +238,18 @@ void main() { ); }); } + +class _FakeAccountRuntimeClient extends AccountRuntimeClient { + _FakeAccountRuntimeClient({required this.loginPayload}) + : super(baseUrl: 'https://accounts.svc.plus'); + + final Map loginPayload; + + @override + Future> login({ + required String identifier, + required String password, + }) async { + return loginPayload; + } +}