Merge branch 'codex/bridge-only-routing-cleanup'

This commit is contained in:
Haitao Pan 2026-04-11 13:50:19 +08:00
commit 55a80d2891
18 changed files with 150 additions and 310 deletions

View File

@ -5,8 +5,7 @@ with the provider catalog aligned to the bridge-only design.
## Current Rule ## Current Rule
- Settings only manages bridge connection parameters and upstream sync - Settings only manages bridge connection parameters and account sync metadata.
definitions.
- The provider picker is not derived from local endpoint presets. - The provider picker is not derived from local endpoint presets.
- `xworkmate-bridge` is the only source of truth for the provider catalog. - `xworkmate-bridge` is the only source of truth for the provider catalog.
@ -16,15 +15,7 @@ with the provider catalog aligned to the bridge-only design.
flowchart TD flowchart TD
A["Settings UI A["Settings UI
仅管理 Bridge 连接参数 仅管理 Bridge 连接参数
与自定义 upstream sync 定义"] --> B["SettingsSnapshot.externalAcpEndpoints 与账号同步元数据"] --> G["acp.capabilities"]
仅作为 sync 输入"]
B --> C["buildExternalAcpSyncedProvidersInternal()"]
C --> D["syncExternalAcpProvidersInternal()"]
D --> E["xworkmate.providers.sync"]
E --> F["xworkmate-bridge providerCatalog"]
F --> G["acp.capabilities"]
G --> H["providerCatalog[] G --> H["providerCatalog[]
singleAgent / multiAgent"] singleAgent / multiAgent"]
@ -77,7 +68,7 @@ flowchart TD
## Notes ## Notes
- `externalAcpEndpoints` still matters, but only as bridge sync input. - Production cloud mode does not use app-side provider sync.
- Provider visibility and picker contents come from - Provider visibility and picker contents come from
`acp.capabilities.providerCatalog`. `acp.capabilities.providerCatalog`.
- Auto-provider resolution and unavailable messaging come from - Auto-provider resolution and unavailable messaging come from

View File

@ -53,15 +53,7 @@ Single-agent provider catalog and availability are owned by
flowchart TD flowchart TD
A["Settings UI A["Settings UI
仅管理 Bridge 连接参数 仅管理 Bridge 连接参数
与自定义 upstream sync 定义"] --> B["SettingsSnapshot.externalAcpEndpoints 与账号同步元数据"] --> G["acp.capabilities"]
仅作为 sync 输入"]
B --> C["buildExternalAcpSyncedProvidersInternal()"]
C --> D["syncExternalAcpProvidersInternal()"]
D --> E["xworkmate.providers.sync"]
E --> F["xworkmate-bridge providerCatalog"]
F --> G["acp.capabilities"]
G --> H["providerCatalog[] G --> H["providerCatalog[]
singleAgent / multiAgent"] singleAgent / multiAgent"]
@ -118,6 +110,8 @@ flowchart TD
- Desktop App 直接桥接 Go 代码 - Desktop App 直接桥接 Go 代码
- Desktop 正常执行链路不以“先启动一个本地 HTTP server再由 Desktop 自己回连”作为目标架构 - Desktop 正常执行链路不以“先启动一个本地 HTTP server再由 Desktop 自己回连”作为目标架构
- Desktop 的 `sendMessage -> GoTaskService.executeTask -> ACP` 应理解为进程内或直接桥接语义 - Desktop 的 `sendMessage -> GoTaskService.executeTask -> ACP` 应理解为进程内或直接桥接语义
- Production cloud mode does not call `xworkmate.providers.sync`
- Production provider upstreams are bridge-owned, not app-owned
- 对 app 来说bridge 是 discovery / config / connect / dialogue 的统一枢纽 - 对 app 来说bridge 是 discovery / config / connect / dialogue 的统一枢纽
### Web / Mobile ### Web / Mobile

View File

@ -6,96 +6,108 @@ Date: 2026-04-11
Scope: Scope:
- `xworkmate-app` - `xworkmate-app`
- Settings / account sync / local UI state / task thread persistence - settings / account sync / cloud runtime state
## V1 Decision ## V1 Decision
This worktree implements the first app-side simplification: Production cloud mode is bridge-only:
- keep a single persisted config file: `config/settings.yaml` - app-facing cloud endpoint is fixed to `https://xworkmate-bridge.svc.plus`
- move local recoverable UI state to `ui/state.json` - production provider catalog is bridge-owned
- keep task title/archive in `tasks/*.json` - production gateway upstream is bridge-owned
- make account sync one-way overwrite for sync-owned fields - account sync is metadata-only for session state, status, and managed secret references
- keep bridge provider catalog / runtime capabilities runtime-only - account sync does not own executable ACP or gateway upstream endpoints
## Overview Workflow ## Production Routing Truth
The app does not define or sync production upstreams.
Bridge-owned production routing is:
- `codex` -> `https://acp-server.svc.plus/codex/acp/rpc`
- `opencode` -> `https://acp-server.svc.plus/opencode/acp/rpc`
- `gemini` -> `https://acp-server.svc.plus/gemini/acp/rpc`
- gateway -> `wss://openclaw.svc.plus`
The app only talks to:
- `https://xworkmate-bridge.svc.plus`
## App Responsibilities
- sign in to `accounts.svc.plus`
- persist account session and sync metadata
- call bridge runtime methods:
- `acp.capabilities`
- `xworkmate.routing.resolve`
- `session.start`
- `session.message`
- `session.cancel`
- `session.close`
- bridge-owned gateway methods
- render bridge/provider/gateway status from bridge runtime results
## Removed Responsibilities
- no app-side direct-connect cloud path
- no production `xworkmate.providers.sync`
- no production provider catalog from `providerSyncDefinitions`
- no execution-time use of account-synced `openclawUrl`
- no execution-time use of account-synced `apisixUrl`
- no direct app calls to `acp-server.svc.plus/*`
- no direct app calls to `openclaw.svc.plus`
## State Rules
`settings.yaml`
- stores current user settings and local editing state
- does not own production ACP upstream definitions
- does not get executable provider endpoints from account sync
`account/sync_state.json`
- stores synced account metadata only
- may retain `openclawUrl` / `apisixUrl` as account profile metadata
- does not overwrite executable cloud routing targets
`acpBridgeServerModeConfig.cloudSynced.remoteServerSummary.endpoint`
- represents bridge cloud entry only
- fixed to `https://xworkmate-bridge.svc.plus` while signed in and synced
- is not an upstream provider URL
- is not a gateway upstream URL
## Workflow
```mermaid ```mermaid
flowchart TD flowchart TD
UI["Settings UI / App Startup"] --> INIT["SettingsController.initialize()"] UI["Settings UI / App Startup"] --> INIT["SettingsController.initialize()"]
INIT --> LOAD["load settings + UI state + task state"]
subgraph LocalStores["APP Local Stores"]
YAML["config/settings.yaml"]
UISTATE["ui/state.json"]
SYNCJSON["account/sync_state.json"]
SECRET["secrets/*.secret\naccount session token / managed secrets"]
TASKS["tasks/*.json\nthread title / archived / thread-owned state"]
end
INIT --> LOAD["SecureConfigStore.loadSettingsSnapshot()"]
LOAD --> YAML
INIT --> LOADUI["SecureConfigStore.loadAppUiState()"]
LOADUI --> UISTATE
INIT --> LOADTHREADS["loadTaskThreads()"]
LOADTHREADS --> TASKS
INIT --> RESTORE["restoreAccountSession()"] INIT --> RESTORE["restoreAccountSession()"]
RESTORE --> TOKEN["loadAccountSessionToken()"] RESTORE --> CHECK{"account session ready?"}
TOKEN --> SECRET CHECK -->|no| BLOCK["blocked"]
TOKEN --> CHECK{"baseUrl + session token ready?"}
CHECK -->|no| BLOCK["blocked\nAccount session is unavailable"]
CHECK -->|yes| SYNC["syncAccountSettingsInternal(baseUrl)"] CHECK -->|yes| SYNC["syncAccountSettingsInternal(baseUrl)"]
SYNC --> API["AccountRuntimeClient.loadProfile(token)"] SYNC --> API["AccountRuntimeClient.loadProfile(token)"]
API --> SAVE_SYNC["saveAccountSyncState(nextState)"] API --> SAVE_SYNC["save account sync metadata"]
SAVE_SYNC --> SYNCJSON API --> SAVE_SUMMARY["set cloud summary endpoint = bridge base URL"]
API --> MODECFG["saveSnapshot(\naccountLocalMode=false,\nacpBridgeServerModeConfig.cloudSynced=remote summary\n)"]
MODECFG --> YAML
API --> APPLY["applyAccountSyncedDefaultsSettingsInternal(state)"] API --> APPLY["applyAccountSyncedDefaultsSettingsInternal(state)"]
APPLY --> O1["overwrite remote gateway endpoint"] APPLY --> KEEP1["keep vault metadata"]
APPLY --> O2["overwrite gateway tokenRef"] APPLY --> KEEP2["keep managed secret refs"]
APPLY --> O3["overwrite vault address / namespace"] APPLY --> SKIP1["do not overwrite gateway executable endpoint"]
APPLY --> O4["overwrite aiGateway baseUrl / apiKeyRef"] APPLY --> SKIP2["do not overwrite ACP executable endpoint"]
APPLY --> O5["overwrite ollamaCloud apiKeyRef"]
APPLY --> O6["update cloudSynced metadata"]
O1 --> SAVE["saveSnapshot(next settings)"] UI --> BRIDGE_CAPS["acp.capabilities via bridge"]
O2 --> SAVE UI --> BRIDGE_ROUTE["xworkmate.routing.resolve via bridge"]
O3 --> SAVE UI --> BRIDGE_RUN["session.* via bridge"]
O4 --> SAVE UI --> BRIDGE_GATEWAY["xworkmate.gateway.* via bridge"]
O5 --> SAVE
O6 --> SAVE
SAVE --> YAML
SAVE --> DERIVED["reloadDerivedStateInternal()"]
DERIVED --> VIEW["Settings / Runtime ViewModel"]
VIEW --> NOTE1["does not auto-connect gateway"]
APPLY -. not touched .-> NOTE2["providerSyncDefinitions\n(sync payload definitions)\nnot overwritten here"]
UI --> LOCAL_EDIT["local settings edit"]
LOCAL_EDIT --> SAVE_LOCAL["saveSnapshot()"]
SAVE_LOCAL --> YAML
UI --> UI_EDIT["local ui restore edit"]
UI_EDIT --> SAVE_UI["saveAppUiState()"]
SAVE_UI --> UISTATE
UI --> THREAD_EDIT["rename / archive / restore thread"]
THREAD_EDIT --> SAVE_THREAD["saveTaskThreads()"]
SAVE_THREAD --> TASKS
``` ```
## V1 Boundaries ## Invariants
- `settings.yaml` only stores current schema V1 config intent and sync-owned local snapshots. - `providerSyncDefinitions` is not a production truth source.
- `ui/state.json` stores `assistantLastSessionKey`, `assistantNavigationDestinations`, and `savedGatewayTargets`. - account sync may update metadata, but not production execution targets.
- `tasks/*.json` stores thread-owned display facts such as `title` and `archived`. - gateway runtime status shown in the app must come from bridge runtime results.
- `account/sync_state.json` stores sync metadata only, not local override policy. - bridge capability/provider availability shown in the app must come from `acp.capabilities`.
- bridge-advertised providers and ACP capability state stay runtime-only.

View File

@ -300,7 +300,8 @@ class AppController extends ChangeNotifier {
late final MultiAgentOrchestrator multiAgentOrchestratorInternal; late final MultiAgentOrchestrator multiAgentOrchestratorInternal;
late final MultiAgentMountManager multiAgentMountManagerInternal; late final MultiAgentMountManager multiAgentMountManagerInternal;
GoTaskServiceClient get goTaskServiceClientForTest => goTaskServiceClientInternal; GoTaskServiceClient get goTaskServiceClientForTest =>
goTaskServiceClientInternal;
Map<SingleAgentProvider, SingleAgentCapabilities> Map<SingleAgentProvider, SingleAgentCapabilities>
singleAgentCapabilitiesByProviderInternal = singleAgentCapabilitiesByProviderInternal =
@ -654,10 +655,7 @@ class AppController extends ChangeNotifier {
if (selection == SingleAgentProvider.auto) { if (selection == SingleAgentProvider.auto) {
return null; return null;
} }
final resolvedSelection = settings.resolveSingleAgentProvider(selection); return canUseSingleAgentProviderInternal(selection) ? selection : null;
return canUseSingleAgentProviderInternal(resolvedSelection)
? resolvedSelection
: null;
} }
List<String> get aiGatewayConversationModelChoices { List<String> get aiGatewayConversationModelChoices {

View File

@ -57,7 +57,6 @@ Future<void> refreshAcpCapabilitiesRuntimeInternal(
controller.sessionsControllerInternal.currentSessionKey, controller.sessionsControllerInternal.currentSessionKey,
); );
if (target == AssistantExecutionTarget.singleAgent) { if (target == AssistantExecutionTarget.singleAgent) {
await controller.syncExternalAcpProvidersInternal();
await controller.goTaskServiceClientInternal.loadExternalAcpCapabilities( await controller.goTaskServiceClientInternal.loadExternalAcpCapabilities(
target: AssistantExecutionTarget.singleAgent, target: AssistantExecutionTarget.singleAgent,
forceRefresh: forceRefresh, forceRefresh: forceRefresh,
@ -94,7 +93,6 @@ Future<void> refreshSingleAgentCapabilitiesRuntimeInternal(
AppController controller, { AppController controller, {
bool forceRefresh = false, bool forceRefresh = false,
}) async { }) async {
await controller.syncExternalAcpProvidersInternal();
final capabilities = await controller.goTaskServiceClientInternal final capabilities = await controller.goTaskServiceClientInternal
.loadExternalAcpCapabilities( .loadExternalAcpCapabilities(
target: AssistantExecutionTarget.singleAgent, target: AssistantExecutionTarget.singleAgent,
@ -221,40 +219,15 @@ String? resolveSingleAgentWorkingDirectoryForSessionRuntimeInternal(
} }
bool singleAgentProviderRequiresLocalPathRuntimeInternal( bool singleAgentProviderRequiresLocalPathRuntimeInternal(
AppController controller, AppController _,
SingleAgentProvider provider, SingleAgentProvider provider,
) { ) {
final configuredEndpoint = controller.settings if (provider == SingleAgentProvider.codex ||
.providerSyncDefinitionForProvider(provider) provider == SingleAgentProvider.opencode ||
.endpoint provider == SingleAgentProvider.gemini) {
.trim();
if (configuredEndpoint.isEmpty) {
return true;
}
final normalizedInput = configuredEndpoint.contains('://')
? configuredEndpoint
: 'ws://$configuredEndpoint';
final endpoint = Uri.tryParse(normalizedInput);
if (endpoint == null) {
return true;
}
final scheme = endpoint.scheme.trim().toLowerCase();
if (scheme == 'wss' || scheme == 'https') {
return false; return false;
} }
final host = endpoint.host.trim(); return true;
if (host.isEmpty) {
return true;
}
final address = InternetAddress.tryParse(host);
if (address != null) {
return !(address.isLoopback || address.type == InternetAddressType.unix);
}
final normalizedHost = host.toLowerCase();
if (normalizedHost == 'localhost') {
return true;
}
return false;
} }
CodeAgentNodeState buildCodeAgentNodeStateRuntimeInternal( CodeAgentNodeState buildCodeAgentNodeStateRuntimeInternal(

View File

@ -661,42 +661,6 @@ extension AppControllerDesktopRuntimeHelpers on AppController {
notifyListeners(); notifyListeners();
} }
Future<List<ExternalCodeAgentAcpSyncedProvider>>
buildExternalAcpSyncedProvidersInternal() async {
final providers = <ExternalCodeAgentAcpSyncedProvider>[];
for (final profile in settings.providerSyncDefinitions) {
final provider = settings.singleAgentProviderForId(profile.providerKey);
if (provider == SingleAgentProvider.auto) {
continue;
}
final endpoint = profile.endpoint.trim();
if (!profile.enabled || endpoint.isEmpty) {
continue;
}
final authorizationHeader = profile.authRef.trim().isEmpty
? ''
: await settingsControllerInternal.resolveSecretValueInternal(
refName: profile.authRef.trim(),
);
providers.add(
ExternalCodeAgentAcpSyncedProvider(
providerId: provider.providerId,
label: provider.label,
endpoint: endpoint,
authorizationHeader: authorizationHeader,
enabled: true,
),
);
}
return providers;
}
Future<void> syncExternalAcpProvidersInternal() async {
await goTaskServiceClientInternal.syncExternalProviders(
await buildExternalAcpSyncedProvidersInternal(),
);
}
Future<void> persistGoTaskArtifactsForSessionInternal( Future<void> persistGoTaskArtifactsForSessionInternal(
String sessionKey, String sessionKey,
GoTaskServiceResult result, GoTaskServiceResult result,

View File

@ -152,7 +152,6 @@ Future<void> sendSingleAgentMessageDesktopGoTaskFlowInternal(
.map((item) => item.label.trim().isNotEmpty ? item.label : item.key) .map((item) => item.label.trim().isNotEmpty ? item.label : item.key)
.where((item) => item.trim().isNotEmpty) .where((item) => item.trim().isNotEmpty)
.toList(growable: false); .toList(growable: false);
await controller.syncExternalAcpProvidersInternal();
final result = await controller.goTaskServiceClientInternal.executeTask( final result = await controller.goTaskServiceClientInternal.executeTask(
GoTaskServiceRequest( GoTaskServiceRequest(
sessionId: sessionKey, sessionId: sessionKey,

View File

@ -306,7 +306,6 @@ extension AppControllerDesktopThreadActions on AppController {
try { try {
final dispatch = await codeAgentNodeOrchestratorInternal final dispatch = await codeAgentNodeOrchestratorInternal
.buildGatewayDispatch(buildCodeAgentNodeStateInternal()); .buildGatewayDispatch(buildCodeAgentNodeStateInternal());
await syncExternalAcpProvidersInternal();
final result = await goTaskServiceClientInternal.executeTask( final result = await goTaskServiceClientInternal.executeTask(
GoTaskServiceRequest( GoTaskServiceRequest(
sessionId: sessionKey, sessionId: sessionKey,

View File

@ -160,7 +160,6 @@ Future<void> runMultiAgentCollaborationThreadSessionInternal(
); );
controller.recomputeTasksInternal(); controller.recomputeTasksInternal();
try { try {
await controller.syncExternalAcpProvidersInternal();
final result = await controller.goTaskServiceClientInternal.executeTask( final result = await controller.goTaskServiceClientInternal.executeTask(
GoTaskServiceRequest( GoTaskServiceRequest(
sessionId: sessionKey, sessionId: sessionKey,

View File

@ -13,8 +13,6 @@ class ExternalCodeAgentAcpDesktopTransport
: _bridge = bridge ?? GoAcpStdioBridge(); : _bridge = bridge ?? GoAcpStdioBridge();
final GoAcpStdioBridge _bridge; final GoAcpStdioBridge _bridge;
List<ExternalCodeAgentAcpSyncedProvider> _syncedProviders =
const <ExternalCodeAgentAcpSyncedProvider>[];
@visibleForTesting @visibleForTesting
GoAcpStdioBridge get bridgeForTest => _bridge; GoAcpStdioBridge get bridgeForTest => _bridge;
@ -22,19 +20,13 @@ class ExternalCodeAgentAcpDesktopTransport
@override @override
Future<void> syncExternalProviders( Future<void> syncExternalProviders(
List<ExternalCodeAgentAcpSyncedProvider> providers, List<ExternalCodeAgentAcpSyncedProvider> providers,
) async { ) async {}
_syncedProviders = List<ExternalCodeAgentAcpSyncedProvider>.unmodifiable(
providers,
);
await _syncProviders();
}
@override @override
Future<ExternalCodeAgentAcpCapabilities> loadExternalAcpCapabilities({ Future<ExternalCodeAgentAcpCapabilities> loadExternalAcpCapabilities({
required AssistantExecutionTarget target, required AssistantExecutionTarget target,
bool forceRefresh = false, bool forceRefresh = false,
}) async { }) async {
await _syncProviders();
final response = await _bridge.request( final response = await _bridge.request(
method: 'acp.capabilities', method: 'acp.capabilities',
params: const <String, dynamic>{}, params: const <String, dynamic>{},
@ -66,7 +58,6 @@ class ExternalCodeAgentAcpDesktopTransport
String aiGatewayBaseUrl = '', String aiGatewayBaseUrl = '',
String aiGatewayApiKey = '', String aiGatewayApiKey = '',
}) async { }) async {
await _syncProviders();
final response = await _bridge.request( final response = await _bridge.request(
method: 'xworkmate.routing.resolve', method: 'xworkmate.routing.resolve',
params: <String, dynamic>{ params: <String, dynamic>{
@ -89,7 +80,6 @@ class ExternalCodeAgentAcpDesktopTransport
GoTaskServiceRequest request, { GoTaskServiceRequest request, {
required void Function(GoTaskServiceUpdate update) onUpdate, required void Function(GoTaskServiceUpdate update) onUpdate,
}) async { }) async {
await _syncProviders();
late final StreamSubscription<Map<String, dynamic>> subscription; late final StreamSubscription<Map<String, dynamic>> subscription;
var streamedText = ''; var streamedText = '';
String? completedMessage; String? completedMessage;
@ -158,28 +148,6 @@ class ExternalCodeAgentAcpDesktopTransport
@override @override
Future<void> dispose() => _bridge.dispose(); Future<void> dispose() => _bridge.dispose();
Future<void> _syncProviders() async {
if (_syncedProviders.isEmpty) {
return;
}
await _bridge.request(
method: 'xworkmate.providers.sync',
params: <String, dynamic>{
'providers': _syncedProviders
.map(
(item) => <String, dynamic>{
'providerId': item.providerId,
'endpoint': item.endpoint,
'label': item.label,
'authorizationHeader': item.authorizationHeader,
'enabled': item.enabled,
},
)
.toList(growable: false),
},
);
}
Map<String, dynamic> _castMap(Object? value) { Map<String, dynamic> _castMap(Object? value) {
if (value is Map<String, dynamic>) { if (value is Map<String, dynamic>) {
return value; return value;

View File

@ -25,10 +25,7 @@ extension SettingsControllerAccountExtension on SettingsController {
if (local.isNotEmpty) { if (local.isNotEmpty) {
return local; return local;
} }
if (!snapshotInternal.acpBridgeServerModeConfig.usesCloudSyncBase) { return '';
return '';
}
return accountSyncStateInternal?.syncedDefaults.apisixUrl.trim() ?? '';
} }
List<String> get effectiveAiGatewayAvailableModels { List<String> get effectiveAiGatewayAvailableModels {

View File

@ -2,6 +2,8 @@ import 'account_runtime_client.dart';
import 'runtime_controllers_settings.dart'; import 'runtime_controllers_settings.dart';
import 'runtime_models.dart'; import 'runtime_models.dart';
const _kProductionBridgeEndpoint = 'https://xworkmate-bridge.svc.plus';
Future<void> loginAccountSettingsInternal( Future<void> loginAccountSettingsInternal(
SettingsController controller, { SettingsController controller, {
required String baseUrl, required String baseUrl,
@ -270,9 +272,7 @@ Future<AccountSyncResult> syncAccountSettingsInternal(
lastSyncAt: nextState.lastSyncAtMs, lastSyncAt: nextState.lastSyncAtMs,
remoteServerSummary: currentModeConfig.cloudSynced.remoteServerSummary remoteServerSummary: currentModeConfig.cloudSynced.remoteServerSummary
.copyWith( .copyWith(
endpoint: response.profile.openclawUrl.trim().isNotEmpty endpoint: _kProductionBridgeEndpoint,
? response.profile.openclawUrl.trim()
: response.profile.apisixUrl.trim(),
hasAdvancedOverrides: false, hasAdvancedOverrides: false,
), ),
), ),
@ -352,37 +352,6 @@ Future<void> applyAccountSyncedDefaultsSettingsInternal(
final previous = controller.snapshotInternal; final previous = controller.snapshotInternal;
var next = previous; var next = previous;
final defaults = state.syncedDefaults; final defaults = state.syncedDefaults;
if (defaults.openclawUrl.trim().isNotEmpty) {
final remoteProfile = previous.gatewayProfiles[kGatewayRemoteProfileIndex];
final normalized = normalizeGatewayManualEndpointInternal(
host: defaults.openclawUrl,
port: remoteProfile.port,
tls: remoteProfile.tls,
);
next = next.copyWithGatewayProfileAt(
kGatewayRemoteProfileIndex,
remoteProfile.copyWith(
mode: RuntimeConnectionMode.remote,
useSetupCode: false,
setupCode: '',
host: normalized.host,
port: normalized.port,
tls: normalized.tls,
),
);
}
final gatewayTokenLocator = defaults.locatorForTarget(
kAccountManagedSecretTargetOpenclawGatewayToken,
);
if (gatewayTokenLocator != null) {
final remoteProfile = next.gatewayProfiles[kGatewayRemoteProfileIndex];
next = next.copyWithGatewayProfileAt(
kGatewayRemoteProfileIndex,
remoteProfile.copyWith(tokenRef: gatewayTokenLocator.target),
);
}
if (defaults.vaultUrl.trim().isNotEmpty) { if (defaults.vaultUrl.trim().isNotEmpty) {
next = next.copyWith( next = next.copyWith(
vault: next.vault.copyWith(address: defaults.vaultUrl.trim()), vault: next.vault.copyWith(address: defaults.vaultUrl.trim()),
@ -395,12 +364,6 @@ Future<void> applyAccountSyncedDefaultsSettingsInternal(
); );
} }
if (defaults.apisixUrl.trim().isNotEmpty) {
next = next.copyWith(
aiGateway: next.aiGateway.copyWith(baseUrl: defaults.apisixUrl.trim()),
);
}
final aiGatewayLocator = defaults.locatorForTarget( final aiGatewayLocator = defaults.locatorForTarget(
kAccountManagedSecretTargetAIGatewayAccessToken, kAccountManagedSecretTargetAIGatewayAccessToken,
); );
@ -433,9 +396,7 @@ Future<void> applyAccountSyncedDefaultsSettingsInternal(
.cloudSynced .cloudSynced
.remoteServerSummary .remoteServerSummary
.copyWith( .copyWith(
endpoint: defaults.openclawUrl.trim().isNotEmpty endpoint: _kProductionBridgeEndpoint,
? defaults.openclawUrl.trim()
: defaults.apisixUrl.trim(),
hasAdvancedOverrides: false, hasAdvancedOverrides: false,
), ),
), ),

View File

@ -208,7 +208,7 @@ SettingsSnapshot _buildCanonicalSettings() {
accountIdentifier: 'canonical@svc.plus', accountIdentifier: 'canonical@svc.plus',
lastSyncAt: 123456789, lastSyncAt: 123456789,
remoteServerSummary: const AcpBridgeServerRemoteServerSummary( remoteServerSummary: const AcpBridgeServerRemoteServerSummary(
endpoint: 'wss://gateway.svc.plus', endpoint: 'https://xworkmate-bridge.svc.plus',
hasAdvancedOverrides: false, hasAdvancedOverrides: false,
), ),
), ),

View File

@ -73,20 +73,20 @@ void main() {
expect(first.state, 'ready'); expect(first.state, 'ready');
expect( expect(
controller.snapshot.gatewayProfiles[kGatewayRemoteProfileIndex].host, controller.snapshot.gatewayProfiles[kGatewayRemoteProfileIndex].host,
'remote.gateway.svc.plus', 'local.example.com',
); );
expect( expect(
controller controller
.snapshot .snapshot
.gatewayProfiles[kGatewayRemoteProfileIndex] .gatewayProfiles[kGatewayRemoteProfileIndex]
.tokenRef, .tokenRef,
kAccountManagedSecretTargetOpenclawGatewayToken, 'local_ref',
); );
expect(controller.snapshot.vault.address, 'https://vault.svc.plus'); expect(controller.snapshot.vault.address, 'https://vault.svc.plus');
expect(controller.snapshot.vault.namespace, 'prod'); expect(controller.snapshot.vault.namespace, 'prod');
expect( expect(
controller.snapshot.aiGateway.baseUrl, controller.snapshot.aiGateway.baseUrl,
'https://apisix.svc.plus', 'https://local-apisix.example.com',
); );
expect( expect(
controller.snapshot.aiGateway.apiKeyRef, controller.snapshot.aiGateway.apiKeyRef,
@ -116,7 +116,16 @@ void main() {
expect(controller.snapshot.vault.address, 'https://vault.svc.plus'); expect(controller.snapshot.vault.address, 'https://vault.svc.plus');
expect( expect(
controller.snapshot.aiGateway.baseUrl, controller.snapshot.aiGateway.baseUrl,
'https://apisix.svc.plus', 'https://edited-apisix.example.com',
);
expect(
controller
.snapshot
.acpBridgeServerModeConfig
.cloudSynced
.remoteServerSummary
.endpoint,
'https://xworkmate-bridge.svc.plus',
); );
final rawSyncState = await store.loadSupportJson( final rawSyncState = await store.loadSupportJson(

View File

@ -46,48 +46,22 @@ class _FakeGoAcpStdioBridgeWithSyncOrder extends GoAcpStdioBridge {
} }
void main() { void main() {
group('External ACP bridge sync order', () { group('External ACP bridge routing order', () {
test('syncs providers before capabilities requests', () async { test('loads capabilities without app-side provider sync', () async {
final bridge = _FakeGoAcpStdioBridgeWithSyncOrder(); final bridge = _FakeGoAcpStdioBridgeWithSyncOrder();
final transport = ExternalCodeAgentAcpDesktopTransport(bridge: bridge); final transport = ExternalCodeAgentAcpDesktopTransport(bridge: bridge);
await transport
.syncExternalProviders(const <ExternalCodeAgentAcpSyncedProvider>[
ExternalCodeAgentAcpSyncedProvider(
providerId: 'codex',
label: 'Codex',
endpoint: 'https://acp-server.svc.plus/codex/acp/rpc',
authorizationHeader: '',
enabled: true,
),
]);
await transport.loadExternalAcpCapabilities( await transport.loadExternalAcpCapabilities(
target: AssistantExecutionTarget.singleAgent, target: AssistantExecutionTarget.singleAgent,
); );
expect(bridge.methods, <String>[ expect(bridge.methods, <String>['acp.capabilities']);
'xworkmate.providers.sync',
'xworkmate.providers.sync',
'acp.capabilities',
]);
}); });
test('syncs providers before session start requests', () async { test('starts sessions without app-side provider sync', () async {
final bridge = _FakeGoAcpStdioBridgeWithSyncOrder(); final bridge = _FakeGoAcpStdioBridgeWithSyncOrder();
final transport = ExternalCodeAgentAcpDesktopTransport(bridge: bridge); final transport = ExternalCodeAgentAcpDesktopTransport(bridge: bridge);
await transport
.syncExternalProviders(const <ExternalCodeAgentAcpSyncedProvider>[
ExternalCodeAgentAcpSyncedProvider(
providerId: 'codex',
label: 'Codex',
endpoint: 'https://acp-server.svc.plus/codex/acp/rpc',
authorizationHeader: '',
enabled: true,
),
]);
await transport.executeTask( await transport.executeTask(
const GoTaskServiceRequest( const GoTaskServiceRequest(
sessionId: 's1', sessionId: 's1',
@ -108,11 +82,7 @@ void main() {
onUpdate: (_) {}, onUpdate: (_) {},
); );
expect(bridge.methods, <String>[ expect(bridge.methods, <String>['session.start']);
'xworkmate.providers.sync',
'xworkmate.providers.sync',
'session.start',
]);
}); });
}); });
} }

View File

@ -77,26 +77,23 @@ void main() {
}, },
); );
test( test('ignores app-side provider sync in bridge-only mode', () async {
'only syncs when app has explicit provider overrides to send', final bridge = _FakeGoAcpStdioBridge();
() async { final transport = ExternalCodeAgentAcpDesktopTransport(bridge: bridge);
final bridge = _FakeGoAcpStdioBridge();
final transport = ExternalCodeAgentAcpDesktopTransport(bridge: bridge);
await transport await transport
.syncExternalProviders(const <ExternalCodeAgentAcpSyncedProvider>[ .syncExternalProviders(const <ExternalCodeAgentAcpSyncedProvider>[
ExternalCodeAgentAcpSyncedProvider( ExternalCodeAgentAcpSyncedProvider(
providerId: 'codex', providerId: 'codex',
label: 'Codex', label: 'Codex',
endpoint: 'https://acp-server.svc.plus/codex/acp/rpc', endpoint: 'https://acp-server.svc.plus/codex/acp/rpc',
authorizationHeader: '', authorizationHeader: '',
enabled: true, enabled: true,
), ),
]); ]);
expect(bridge.methods, <String>['xworkmate.providers.sync']); expect(bridge.methods, isEmpty);
}, });
);
test( test(
'uses bridge routing resolve for preflight provider selection', 'uses bridge routing resolve for preflight provider selection',

View File

@ -79,7 +79,7 @@ void main() {
lastSyncAt: 123456789, lastSyncAt: 123456789,
remoteServerSummary: remoteServerSummary:
const AcpBridgeServerRemoteServerSummary( const AcpBridgeServerRemoteServerSummary(
endpoint: 'wss://gateway.svc.plus', endpoint: 'https://xworkmate-bridge.svc.plus',
hasAdvancedOverrides: false, hasAdvancedOverrides: false,
), ),
), ),

View File

@ -58,6 +58,15 @@ void main() {
expect(controller.accountSyncState?.profileScope, 'tenant-shared'); expect(controller.accountSyncState?.profileScope, 'tenant-shared');
expect(controller.accountSyncState?.tokenConfigured.apisix, isTrue); expect(controller.accountSyncState?.tokenConfigured.apisix, isTrue);
expect(await store.loadAccountSessionToken(), 'session-token'); expect(await store.loadAccountSessionToken(), 'session-token');
expect(
controller
.snapshot
.acpBridgeServerModeConfig
.cloudSynced
.remoteServerSummary
.endpoint,
'https://xworkmate-bridge.svc.plus',
);
}, },
); );