Remove desktop bridge legacy chain

This commit is contained in:
Haitao Pan 2026-04-13 11:00:31 +08:00
parent add6d0eb95
commit 895c34dd9d
10 changed files with 192 additions and 106 deletions

View File

@ -12,7 +12,7 @@ mobile:
description: Mobile assistant destination
ui_surface: mobile_shell
tasks:
enabled: true
enabled: false
release_tier: stable
build_modes: [debug, profile, release]
description: Mobile tasks destination
@ -37,25 +37,25 @@ mobile:
ui_surface: mobile_shell
workspace:
skills:
enabled: true
enabled: false
release_tier: stable
build_modes: [debug, profile, release]
description: Mobile workspace skills launcher
ui_surface: mobile_workspace_hub
nodes:
enabled: true
enabled: false
release_tier: stable
build_modes: [debug, profile, release]
description: Mobile workspace nodes launcher
ui_surface: mobile_workspace_hub
agents:
enabled: true
enabled: false
release_tier: stable
build_modes: [debug, profile, release]
description: Mobile workspace agents launcher
ui_surface: mobile_workspace_hub
mcp_server:
enabled: true
enabled: false
release_tier: experimental
build_modes: [debug, profile, release]
description: Mobile workspace MCP launcher
@ -264,13 +264,13 @@ desktop:
description: Desktop ClawHub destination
ui_surface: sidebar_navigation
secrets:
enabled: true
enabled: false
release_tier: stable
build_modes: [debug, profile, release]
description: Desktop secrets destination
ui_surface: sidebar_navigation
ai_gateway:
enabled: true
enabled: false
release_tier: stable
build_modes: [debug, profile, release]
description: Desktop AI Gateway destination
@ -282,7 +282,7 @@ desktop:
description: Desktop settings destination
ui_surface: sidebar_navigation
account:
enabled: true
enabled: false
release_tier: stable
build_modes: [debug, profile, release]
description: Desktop account destination

View File

@ -655,7 +655,12 @@ extension AppControllerDesktopRuntimeHelpers on AppController {
}
Uri? resolveBridgeAcpEndpointInternal() {
final uri = Uri.tryParse(kManagedBridgeServerUrl);
final syncedBridgeServerUrl =
settingsControllerInternal.accountProfile?.bridgeServerUrl.trim() ?? '';
final candidate = isSupportedExternalAcpEndpoint(syncedBridgeServerUrl)
? syncedBridgeServerUrl
: kManagedBridgeServerUrl;
final uri = Uri.tryParse(candidate);
final scheme = uri?.scheme.trim().toLowerCase() ?? '';
if (uri == null || !kSupportedExternalAcpEndpointSchemes.contains(scheme)) {
return null;

View File

@ -26,6 +26,10 @@ class _AppShellState extends State<AppShell> {
static const _sidebarViewportPadding = 72.0;
static const _mainContentMinWidth = 640.0;
static const _sidebarExpandedBaseWidth = 336.0;
static const _desktopDestinations = <WorkspaceDestination>[
WorkspaceDestination.assistant,
WorkspaceDestination.settings,
];
double? _sidebarExpandedWidth;
static const _mobileDestinations = [
@ -482,14 +486,29 @@ class _AppShellState extends State<AppShell> {
}
Widget _buildCurrentPage(ValueChanged<DetailPanelData> onOpenDetail) {
final currentDestination = _resolveDesktopDestination(
widget.controller.destination,
);
return IndexedStack(
index: widget.controller.destination.index,
children: WorkspaceDestination.values
index: _desktopDestinations.indexOf(currentDestination),
children: _desktopDestinations
.map((destination) => _pageForDestination(destination, onOpenDetail))
.toList(),
);
}
WorkspaceDestination _resolveDesktopDestination(
WorkspaceDestination destination,
) {
if (destination == WorkspaceDestination.account) {
return WorkspaceDestination.settings;
}
if (_desktopDestinations.contains(destination)) {
return destination;
}
return WorkspaceDestination.assistant;
}
Widget _pageForDestination(
WorkspaceDestination destination,
ValueChanged<DetailPanelData> onOpenDetail,

View File

@ -399,16 +399,7 @@ class UiFeatureAccess {
},
UiFeaturePlatform.desktop: <String, WorkspaceDestination>{
UiFeatureKeys.navigationAssistant: WorkspaceDestination.assistant,
UiFeatureKeys.navigationTasks: WorkspaceDestination.tasks,
UiFeatureKeys.navigationSkills: WorkspaceDestination.skills,
UiFeatureKeys.navigationNodes: WorkspaceDestination.nodes,
UiFeatureKeys.navigationAgents: WorkspaceDestination.agents,
UiFeatureKeys.navigationMcpServer: WorkspaceDestination.mcpServer,
UiFeatureKeys.navigationClawHub: WorkspaceDestination.clawHub,
UiFeatureKeys.navigationSecrets: WorkspaceDestination.secrets,
UiFeatureKeys.navigationAiGateway: WorkspaceDestination.aiGateway,
UiFeatureKeys.navigationSettings: WorkspaceDestination.settings,
UiFeatureKeys.navigationAccount: WorkspaceDestination.account,
},
UiFeaturePlatform.web: <String, WorkspaceDestination>{
UiFeatureKeys.navigationAssistant: WorkspaceDestination.assistant,

View File

@ -23,7 +23,7 @@ mobile:
description: Mobile assistant destination
ui_surface: mobile_shell
tasks:
enabled: true
enabled: false
release_tier: stable
build_modes: [debug, profile, release]
description: Mobile tasks destination
@ -48,25 +48,25 @@ mobile:
ui_surface: mobile_shell
workspace:
skills:
enabled: true
enabled: false
release_tier: stable
build_modes: [debug, profile, release]
description: Mobile workspace skills launcher
ui_surface: mobile_workspace_hub
nodes:
enabled: true
enabled: false
release_tier: stable
build_modes: [debug, profile, release]
description: Mobile workspace nodes launcher
ui_surface: mobile_workspace_hub
agents:
enabled: true
enabled: false
release_tier: stable
build_modes: [debug, profile, release]
description: Mobile workspace agents launcher
ui_surface: mobile_workspace_hub
mcp_server:
enabled: true
enabled: false
release_tier: stable
build_modes: [debug, profile, release]
description: Mobile workspace MCP launcher
@ -275,13 +275,13 @@ desktop:
description: Desktop ClawHub destination
ui_surface: sidebar_navigation
secrets:
enabled: true
enabled: false
release_tier: stable
build_modes: [debug, profile, release]
description: Desktop secrets destination
ui_surface: sidebar_navigation
ai_gateway:
enabled: true
enabled: false
release_tier: stable
build_modes: [debug, profile, release]
description: Desktop LLM API destination
@ -293,7 +293,7 @@ desktop:
description: Desktop settings destination
ui_surface: sidebar_navigation
account:
enabled: true
enabled: false
release_tier: stable
build_modes: [debug, profile, release]
description: Desktop account destination

View File

@ -136,10 +136,7 @@ extension SettingsControllerAccountExtension on SettingsController {
accountSessionTokenInternal =
(await storeInternal.loadAccountSessionToken())?.trim() ?? '';
accountSessionInternal = await storeInternal.loadAccountSessionSummary();
accountSyncStateInternal = await recoverBridgeAccountSyncStateInternal(
this,
await storeInternal.loadAccountSyncState(),
);
accountSyncStateInternal = await storeInternal.loadAccountSyncState();
if (!accountBusyInternal) {
if (accountSignedIn) {
final email = accountSessionInternal?.email.trim() ?? '';

View File

@ -146,6 +146,7 @@ Future<void> completeAccountSignInSettingsInternal(
controller,
baseUrl: baseUrl,
bridgeTokenOverride: _resolveBridgeAuthorizationToken(payload),
bridgeServerUrlOverride: _resolveBridgeServerUrl(payload),
quiet: true,
);
await controller.reloadDerivedStateInternal();
@ -223,6 +224,7 @@ Future<AccountSyncResult> syncAccountSettingsInternal(
String baseUrl = '',
bool quiet = false,
String bridgeTokenOverride = '',
String bridgeServerUrlOverride = '',
}) async {
final sessionToken =
(await controller.storeInternal.loadAccountSessionToken())?.trim() ?? '';
@ -286,7 +288,10 @@ Future<AccountSyncResult> syncAccountSettingsInternal(
target: kAccountManagedSecretTargetBridgeAuthToken,
value: bridgeToken,
);
const resolvedBridgeServerUrl = kManagedBridgeServerUrl;
final resolvedBridgeServerUrl = _resolveCurrentBridgeServerUrl(
controller,
bridgeServerUrlOverride: bridgeServerUrlOverride,
);
await controller.storeInternal.clearAccountManagedSecret(
target: kAccountManagedSecretTargetAIGatewayAccessToken,
);
@ -346,55 +351,6 @@ Future<AccountSyncResult> syncAccountSettingsInternal(
);
}
Future<AccountSyncState?> recoverBridgeAccountSyncStateInternal(
SettingsController controller,
AccountSyncState? currentState,
) async {
final currentBridgeServerUrl =
currentState?.syncedDefaults.bridgeServerUrl.trim() ?? '';
if (currentBridgeServerUrl.isNotEmpty) {
return currentState;
}
final cloudSynced =
controller.snapshotInternal.acpBridgeServerModeConfig.cloudSynced;
final legacyBridgeServerUrl = cloudSynced.remoteServerSummary.endpoint.trim();
if (!isSupportedExternalAcpEndpoint(legacyBridgeServerUrl)) {
return currentState;
}
final defaults = AccountSyncState.defaults();
final baseline = currentState ?? defaults;
final hasBridgeToken = controller.secureRefsInternal.containsKey(
kAccountManagedSecretTargetBridgeAuthToken,
);
final recoveredState = baseline.copyWith(
syncedDefaults: baseline.syncedDefaults.copyWith(
bridgeServerUrl: legacyBridgeServerUrl,
),
syncState: baseline.syncState == defaults.syncState
? 'ready'
: baseline.syncState,
syncMessage: baseline.syncMessage == defaults.syncMessage
? 'Bridge access synced'
: baseline.syncMessage,
lastSyncAtMs: baseline.lastSyncAtMs > 0
? baseline.lastSyncAtMs
: cloudSynced.lastSyncAt,
lastSyncSource: baseline.lastSyncSource.trim().isNotEmpty
? baseline.lastSyncSource
: legacyBridgeServerUrl,
profileScope: baseline.profileScope.trim().isNotEmpty
? baseline.profileScope
: 'bridge',
tokenConfigured: baseline.tokenConfigured.copyWith(
bridge: baseline.tokenConfigured.bridge || hasBridgeToken,
),
);
await controller.storeInternal.saveAccountSyncState(recoveredState);
return recoveredState;
}
Future<void> logoutAccountSettingsInternal(
SettingsController controller, {
String statusMessage = 'Signed out',
@ -518,6 +474,36 @@ String _resolveBridgeAuthorizationToken(Map<String, dynamic> payload) {
return '';
}
String _resolveBridgeServerUrl(Map<String, dynamic> payload) {
final explicit = _stringValue(payload['BRIDGE_SERVER_URL']);
if (explicit.isNotEmpty) {
return explicit;
}
final camelCase = _stringValue(payload['bridgeServerUrl']);
if (camelCase.isNotEmpty) {
return camelCase;
}
return '';
}
String _resolveCurrentBridgeServerUrl(
SettingsController controller, {
String bridgeServerUrlOverride = '',
}) {
final explicit = bridgeServerUrlOverride.trim();
if (isSupportedExternalAcpEndpoint(explicit)) {
return explicit;
}
final syncedBridgeServerUrl =
controller.accountSyncStateInternal?.syncedDefaults.bridgeServerUrl
.trim() ??
'';
if (isSupportedExternalAcpEndpoint(syncedBridgeServerUrl)) {
return syncedBridgeServerUrl;
}
return kManagedBridgeServerUrl;
}
int _parseExpiresAtMs(Object? value) {
if (value is int) {
return value;

View File

@ -1,11 +1,63 @@
import 'dart:io';
import 'package:flutter_test/flutter_test.dart';
import 'package:xworkmate/app/app_controller.dart';
import 'package:xworkmate/runtime/mode_switcher.dart';
import 'package:xworkmate/runtime/runtime_models.dart';
import 'package:xworkmate/runtime/secure_config_store.dart';
void main() {
group('Bridge runtime cleanup', () {
test('resolves the managed bridge endpoint without BRIDGE_SERVER_URL', () {
test('resolves the current synced bridge endpoint before env leftovers', () async {
final storeRoot = await Directory.systemTemp.createTemp(
'xworkmate-bridge-runtime-cleanup-',
);
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.saveAccountSyncState(
AccountSyncState.defaults().copyWith(
syncedDefaults: AccountRemoteProfile.defaults().copyWith(
bridgeServerUrl: 'https://xworkmate-bridge-alt.svc.plus',
),
syncState: 'ready',
),
);
final controller = AppController(
store: store,
environmentOverride: const <String, String>{
'BRIDGE_SERVER_URL': 'https://stale.example.invalid',
},
);
addTearDown(controller.dispose);
await controller.settingsControllerInternal.initialize();
expect(
controller.resolveBridgeAcpEndpointInternal()?.toString(),
'https://xworkmate-bridge-alt.svc.plus',
);
expect(
controller
.resolveExternalAcpEndpointForTargetInternal(
AssistantExecutionTarget.gateway,
)
?.toString(),
'https://xworkmate-bridge-alt.svc.plus',
);
});
test('falls back to the managed bridge endpoint without BRIDGE_SERVER_URL', () {
final controller = AppController(
environmentOverride: const <String, String>{
'BRIDGE_SERVER_URL': 'https://stale.example.invalid',
@ -17,14 +69,6 @@ void main() {
controller.resolveBridgeAcpEndpointInternal()?.toString(),
kManagedBridgeServerUrl,
);
expect(
controller
.resolveExternalAcpEndpointForTargetInternal(
AssistantExecutionTarget.gateway,
)
?.toString(),
kManagedBridgeServerUrl,
);
});
test(

View File

@ -60,7 +60,7 @@ void main() {
);
test(
'login sync accepts INTERNAL_SERVICE_TOKEN payload for managed bridge auth',
'login sync stores the current bridge contract from login payload',
() async {
final storeRoot = await Directory.systemTemp.createTemp(
'xworkmate-account-sync-uppercase-token-',
@ -90,6 +90,7 @@ void main() {
loginPayload: <String, dynamic>{
'token': 'session-token',
'INTERNAL_SERVICE_TOKEN': 'bridge-token-from-login',
'BRIDGE_SERVER_URL': 'https://xworkmate-bridge-alt.svc.plus',
'user': <String, dynamic>{
'id': 'user-1',
'email': 'review@svc.plus',
@ -108,6 +109,19 @@ void main() {
expect(controller.accountSyncState, isNotNull);
expect(controller.accountSyncState!.syncState, 'ready');
expect(
controller.accountSyncState!.syncedDefaults.bridgeServerUrl,
'https://xworkmate-bridge-alt.svc.plus',
);
expect(
controller
.snapshot
.acpBridgeServerModeConfig
.cloudSynced
.remoteServerSummary
.endpoint,
'https://xworkmate-bridge-alt.svc.plus',
);
expect(
await store.loadAccountManagedSecret(
target: kAccountManagedSecretTargetBridgeAuthToken,
@ -172,7 +186,7 @@ void main() {
});
test(
'recovers bridge sync state from cloud-synced snapshot when support state is missing',
'does not recover bridge sync state from stale cloud-synced snapshot state',
() async {
final storeRoot = await Directory.systemTemp.createTemp(
'xworkmate-account-recover-',
@ -220,20 +234,9 @@ void main() {
addTearDown(controller.dispose);
await controller.initialize();
expect(controller.accountSyncState, isNotNull);
expect(
controller.accountSyncState!.syncedDefaults.bridgeServerUrl,
'https://bridge.svc.plus',
);
expect(controller.accountSyncState!.syncState, 'ready');
expect(controller.accountSyncState!.profileScope, 'bridge');
expect(controller.accountSyncState, isNull);
final persisted = await store.loadAccountSyncState();
expect(persisted, isNotNull);
expect(
persisted!.syncedDefaults.bridgeServerUrl,
'https://bridge.svc.plus',
);
expect(persisted, isNull);
},
);
});

View File

@ -0,0 +1,41 @@
import 'dart:io';
import 'package:flutter_test/flutter_test.dart';
import 'package:xworkmate/app/ui_feature_manifest.dart';
import 'package:xworkmate/models/app_models.dart';
void main() {
group('Desktop feature manifest cleanup', () {
test('repo config only exposes assistant and settings on desktop', () {
final raw = File('config/feature_flags.yaml').readAsStringSync();
final manifest = UiFeatureManifest.fromYamlString(raw);
final desktop = manifest.forPlatform(
UiFeaturePlatform.desktop,
buildMode: UiFeatureBuildMode.debug,
);
expect(
desktop.allowedDestinations,
<WorkspaceDestination>{
WorkspaceDestination.assistant,
WorkspaceDestination.settings,
},
);
});
test('fallback manifest only exposes assistant and settings on desktop', () {
final desktop = UiFeatureManifest.fallback().forPlatform(
UiFeaturePlatform.desktop,
buildMode: UiFeatureBuildMode.debug,
);
expect(
desktop.allowedDestinations,
<WorkspaceDestination>{
WorkspaceDestination.assistant,
WorkspaceDestination.settings,
},
);
});
});
}