fix: prioritize managed bridge sync state

This commit is contained in:
Haitao Pan 2026-06-16 06:20:13 +08:00
parent 57f1cbc02a
commit 7e4b2a756a
6 changed files with 105 additions and 23 deletions

View File

@ -101,14 +101,14 @@ flowchart TD
D --> C
C --> E["bridge runtime"]
note1["Priority order\n1. selfHosted when explicitly configured\n2. cloudSynced when account sync is ready and token exists\n3. disconnected"] --> C
note1["Priority order\n1. cloudSynced when account sync is ready and token exists\n2. selfHosted when explicitly configured\n3. disconnected or managed default"] --> C
```
### Runtime Invariants
- `selfHosted` always wins when it is configured.
- `cloudSynced` is valid only when account sync is ready and the managed bridge token exists.
- Signed-out state is disconnected: runtime must not use a default managed endpoint, stale managed secret, gateway profile token, or loopback ACP endpoint.
- `cloudSynced` wins when account sync is ready and the managed bridge token exists.
- `selfHosted` wins only when cloud sync is not ready.
- Signed-out state is disconnected for authorization decisions: runtime must not use a stale managed secret, gateway profile token, or loopback ACP endpoint.
- Missing `BRIDGE_AUTH_TOKEN` is disconnected for the managed cloud-sync path.
- `BRIDGE_SERVER_URL` may be retained in `AccountSyncState.syncedDefaults.bridgeServerUrl`, but it is metadata only.
- `BRIDGE_AUTH_TOKEN` is written to secure storage only, never to normal settings.

View File

@ -1248,6 +1248,17 @@ extension AppControllerDesktopRuntimeHelpers on AppController {
}
Uri? resolveBridgeAcpEndpointInternal() {
final accountSyncState = settingsControllerInternal.accountSyncState;
final managedBridgeReady =
settingsControllerInternal.accountSessionTokenInternal
.trim()
.isNotEmpty &&
accountSyncState?.syncState.trim().toLowerCase() == 'ready' &&
accountSyncState?.tokenConfigured.bridge == true;
if (managedBridgeReady) {
return Uri.parse(kManagedBridgeServerUrl);
}
final selfHosted = settingsControllerInternal
.snapshot
.acpBridgeServerModeConfig
@ -1260,8 +1271,7 @@ extension AppControllerDesktopRuntimeHelpers on AppController {
}
}
final uri = Uri.parse(kManagedBridgeServerUrl);
return uri.replace(query: null, fragment: null);
return Uri.parse(kManagedBridgeServerUrl);
}
Uri? resolveExternalAcpEndpointForTargetInternal(AssistantExecutionTarget _) {
@ -1281,12 +1291,16 @@ extension AppControllerDesktopRuntimeHelpers on AppController {
return true;
}
final accountSyncState = settingsControllerInternal.accountSyncState;
if (settingsControllerInternal.accountSignedIn &&
if (settingsControllerInternal.accountSessionTokenInternal
.trim()
.isNotEmpty &&
accountSyncState?.syncState.trim().toLowerCase() == 'ready' &&
accountSyncState?.tokenConfigured.bridge == true) {
return true;
}
if (settingsControllerInternal.accountSignedIn) {
if (settingsControllerInternal.accountSessionTokenInternal
.trim()
.isNotEmpty) {
return false;
}
final envToken = runtimeEnvironmentValueInternal('BRIDGE_AUTH_TOKEN');
@ -1322,19 +1336,28 @@ extension AppControllerDesktopRuntimeHelpers on AppController {
final bridgeEndpoint = resolveBridgeAcpEndpointInternal();
final bridgeHost = bridgeEndpoint?.host.trim().toLowerCase() ?? '';
final bridgePort = bridgeEndpoint?.port ?? 0;
final accountSyncState = settingsControllerInternal.accountSyncState;
final managedBridgeReady =
settingsControllerInternal.accountSessionTokenInternal
.trim()
.isNotEmpty &&
accountSyncState?.syncState.trim().toLowerCase() == 'ready' &&
accountSyncState?.tokenConfigured.bridge == true;
final matchesBridgeEndpoint =
bridgeHost.isNotEmpty &&
normalizedHost == bridgeHost &&
(bridgePort <= 0 || endpoint.port == bridgePort);
if (matchesBridgeEndpoint) {
if (managedBridgeReady) {
final bridgeToken = await _resolveManagedBridgeAuthTokenInternal();
if (bridgeToken != null && bridgeToken.isNotEmpty) {
return bridgeToken;
}
}
final manualBridgeToken = await _resolveManualBridgeAuthTokenInternal();
if (manualBridgeToken != null && manualBridgeToken.isNotEmpty) {
return manualBridgeToken;
}
final bridgeToken = await _resolveManagedBridgeAuthTokenInternal();
if (bridgeToken != null && bridgeToken.isNotEmpty) {
return bridgeToken;
}
}
return null;
}
@ -1345,18 +1368,28 @@ extension AppControllerDesktopRuntimeHelpers on AppController {
final normalizedHost = endpoint.host.trim().toLowerCase();
final bridgeEndpoint = resolveBridgeAcpEndpointInternal();
final bridgeHost = bridgeEndpoint?.host.trim().toLowerCase() ?? '';
final accountSyncState = settingsControllerInternal.accountSyncState;
final managedBridgeReady =
settingsControllerInternal.accountSessionTokenInternal
.trim()
.isNotEmpty &&
accountSyncState?.syncState.trim().toLowerCase() == 'ready' &&
accountSyncState?.tokenConfigured.bridge == true;
if (bridgeHost.isEmpty || normalizedHost != bridgeHost) {
return null;
}
if (managedBridgeReady) {
final bridgeToken = await _resolveManagedBridgeAuthTokenInternal();
if (bridgeToken != null && bridgeToken.isNotEmpty) {
return _normalizeAuthorizationHeaderInternal(bridgeToken);
}
}
final manualBridgeToken = await _resolveManualBridgeAuthTokenInternal();
if (manualBridgeToken != null && manualBridgeToken.isNotEmpty) {
return _normalizeAuthorizationHeaderInternal(manualBridgeToken);
}
final bridgeToken = await _resolveManagedBridgeAuthTokenInternal();
if (bridgeToken != null && bridgeToken.isNotEmpty) {
return _normalizeAuthorizationHeaderInternal(bridgeToken);
}
return null;
}
@ -1379,15 +1412,14 @@ extension AppControllerDesktopRuntimeHelpers on AppController {
}
Future<String?> _resolveManagedBridgeAuthTokenInternal() async {
if (settingsControllerInternal.accountSignedIn) {
if (settingsControllerInternal.accountSessionTokenInternal
.trim()
.isNotEmpty) {
final bridgeToken = (await storeInternal.loadAccountManagedSecret(
target: kAccountManagedSecretTargetBridgeAuthToken,
))?.trim();
return bridgeToken?.isNotEmpty == true ? bridgeToken : null;
}
if (settingsControllerInternal.accountSignedIn) {
return null;
}
final envToken = runtimeEnvironmentValueInternal('BRIDGE_AUTH_TOKEN');
return envToken?.isNotEmpty == true ? envToken : null;

View File

@ -49,8 +49,8 @@ class _DesktopViewState extends State<DesktopView> {
text: '2000',
);
bool _useGpu = false;
bool _adaptiveResolution = false;
final bool _useGpu = false;
final bool _adaptiveResolution = false;
bool _showControlPanel = true;
String _connectionState = 'disconnected';
bool _hasStream = false;

View File

@ -603,6 +603,20 @@ AcpBridgeServerEffectiveConfig resolveAcpBridgeServerEffectiveConfigInternal(
SettingsController controller, {
required AcpBridgeServerModeConfig config,
}) {
final accountSyncState = controller.accountSyncState;
final managedBridgeReady =
controller.accountSessionTokenInternal.trim().isNotEmpty &&
accountSyncState?.syncState.trim().toLowerCase() == 'ready' &&
accountSyncState?.tokenConfigured.bridge == true;
if (managedBridgeReady) {
return const AcpBridgeServerEffectiveConfig(
endpoint: kManagedBridgeServerUrl,
tokenRef: '',
source: 'cloud',
reason: 'Account sync is ready and the managed bridge token is available',
);
}
if (config.selfHosted.isConfigured) {
return AcpBridgeServerEffectiveConfig(
endpoint: config.selfHosted.serverUrl,

View File

@ -13,7 +13,7 @@ import 'package:xworkmate/runtime/secure_config_store.dart';
void main() {
group('Bridge runtime cleanup', () {
test(
'keeps the managed bridge endpoint fixed even when account sync carries a bridge URL',
'keeps the managed bridge endpoint fixed even when account sync carries a bridge URL and stale manual bridge config exists',
() async {
final storeRoot = await Directory.systemTemp.createTemp(
'xworkmate-bridge-runtime-cleanup-',
@ -48,6 +48,28 @@ void main() {
),
),
);
await store.saveAccountSessionToken('session-token');
await store.saveAccountSessionSummary(
const AccountSessionSummary(
userId: 'user-1',
email: 'review@svc.plus',
name: 'Review User',
role: 'reviewer',
mfaEnabled: true,
),
);
await store.saveSettingsSnapshot(
SettingsSnapshot.defaults().copyWith(
acpBridgeServerModeConfig: AcpBridgeServerModeConfig.defaults()
.copyWith(
selfHosted: AcpBridgeServerModeConfig.defaults().selfHosted
.copyWith(
serverUrl: 'https://acp-bridge.onwalk.net',
username: 'admin',
),
),
),
);
await store.saveAccountManagedSecret(
target: kAccountManagedSecretTargetBridgeAuthToken,
value: 'bridge-token',
@ -66,6 +88,12 @@ void main() {
controller.resolveBridgeAcpEndpointInternal()?.toString(),
kManagedBridgeServerUrl,
);
expect(
await controller.resolveGatewayAcpAuthorizationHeaderInternal(
Uri.parse('$kManagedBridgeServerUrl/acp/rpc'),
),
'bridge-token',
);
expect(
controller
.resolveExternalAcpEndpointForTargetInternal(

View File

@ -490,6 +490,14 @@ void main() {
),
'fresh-bridge-token',
);
expect(
controller.snapshot.acpBridgeServerModeConfig.effective.source,
'cloud',
);
expect(
controller.snapshot.acpBridgeServerModeConfig.effective.endpoint,
kManagedBridgeServerUrl,
);
expect(
controller.accountSyncState!.syncedDefaults.bridgeServerUrl,
'https://xworkmate-bridge-new.svc.plus',