Fix managed bridge auth sync and offline connection state

This commit is contained in:
Haitao Pan 2026-04-13 10:07:46 +08:00
parent 29db450352
commit 05a346e372
6 changed files with 160 additions and 8 deletions

View File

@ -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')

View File

@ -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<void> 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,

View File

@ -505,6 +505,12 @@ String _resolveBridgeAuthorizationToken(Map<String, dynamic> 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;

View File

@ -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,
);
},
);
});
}

View File

@ -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);

View File

@ -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: <String, dynamic>{
'token': 'session-token',
'INTERNAL_SERVICE_TOKEN': 'bridge-token-from-login',
'user': <String, dynamic>{
'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<String, dynamic> loginPayload;
@override
Future<Map<String, dynamic>> login({
required String identifier,
required String password,
}) async {
return loginPayload;
}
}