diff --git a/config/settings.yaml b/config/settings.yaml
new file mode 100644
index 00000000..c56ee663
--- /dev/null
+++ b/config/settings.yaml
@@ -0,0 +1,32 @@
+schemaVersion: 2
+appLanguage: zh
+workspacePath: ''
+cliPath: openclaw
+defaultProvider: gateway
+accountBaseUrl: https://accounts.svc.plus
+accountUsername: ''
+acpBridgeServerModeConfig:
+ effective:
+ endpoint: https://xworkmate-bridge.svc.plus
+ tokenRef: bridge.auth_token
+ source: cloud
+ reason: Synced cloud configuration from svc.plus is active
+ cloudSynced:
+ accountBaseUrl: https://accounts.svc.plus
+ accountIdentifier: ''
+ lastSyncAt: 0
+ remoteServerSummary:
+ endpoint: https://xworkmate-bridge.svc.plus
+ hasAdvancedOverrides: false
+ selfHosted:
+ serverUrl: ''
+ username: ''
+ passwordRef: acp_bridge_server_password
+gatewayProfiles:
+- mode: unconfigured
+ useSetupCode: false
+ host: ''
+ port: 443
+ tls: true
+ tokenRef: gateway_token_0
+ passwordRef: gateway_password_0
diff --git a/docs/architecture/bridge-cloud-coexistence-priority.md b/docs/architecture/bridge-cloud-coexistence-priority.md
new file mode 100644
index 00000000..deab364d
--- /dev/null
+++ b/docs/architecture/bridge-cloud-coexistence-priority.md
@@ -0,0 +1,62 @@
+# Bridge Server Configuration Coexistence & Priority Resolution
+
+Date: 2026-04-19
+
+## Overview
+
+The `xworkmate-app` utilizes a "Single Source of Truth" pattern for its Bridge Server configuration. While multiple configuration sources (Cloud Sync from `svc.plus` and Manual Bridge settings) can coexist, the system deterministically resolves them into a single **Effective Configuration** that the runtime consumes.
+
+### Key Principles
+- **Sources as Inputs:** Cloud and Bridge configurations are treated as sources, not mutually exclusive modes.
+- **Deterministic Priority:** `Manual Bridge (Self-Hosted)` > `Cloud Sync (Managed)`.
+- **Single Source of Truth:** The runtime exclusively uses the `AcpBridgeServerEffectiveConfig` object.
+- **Explainability:** Each effective configuration includes a `source` tag and a `reason` explaining why it was selected.
+
+## Configuration Model
+
+The system maintains a clear separation between sources and the resolved state in `AcpBridgeServerModeConfig`.
+
+```dart
+class AcpBridgeServerModeConfig {
+ final AcpBridgeServerEffectiveConfig effective; // Single Source of Truth
+ final AcpBridgeServerCloudSyncConfig cloudSynced; // Source: svc.plus
+ final AcpBridgeServerSelfHostedConfig selfHosted; // Source: Manual entries
+}
+```
+
+### Resolution Logic (Mermaid Diagram)
+
+The following diagram illustrates how the `resolveAcpBridgeServerEffectiveConfig` function implements the priority logic.
+
+```mermaid
+flowchart TD
+ Start([Resolve Effective Config]) --> CheckManual{Is Manual Source\nconfigured?}
+
+ CheckManual -->|Yes| UseManual[Effective Source: bridge\nEndpoint: selfHosted.serverUrl\nTokenRef: selfHosted.passwordRef]
+
+ CheckManual -->|No| CheckCloud{Is Cloud Source\nsynced & valid?}
+
+ CheckCloud -->|Yes| UseCloud[Effective Source: cloud\nEndpoint: cloudSynced.bridgeServerUrl\nTokenRef: bridge.auth_token]
+
+ CheckCloud -->|No| UseDefault[Effective Source: default\nEndpoint: kManagedBridgeServerUrl\nTokenRef: None]
+
+ UseManual --> End([Persist Effective State])
+ UseCloud --> End
+ UseDefault --> End
+
+ style UseManual fill:#e1f5fe,stroke:#1e88e5,stroke-width:2px
+ style UseCloud fill:#f3e5f5,stroke:#039be5,stroke-width:2px
+ style UseDefault fill:#fff3e0,stroke:#8e24aa,stroke-width:2px
+```
+
+## Traceability & explaining the source
+
+The `effective` configuration stores metadata that can be used for diagnostics or displayed in the UI:
+- **`source`**: One of `bridge`, `cloud`, or `default`.
+- **`reason`**: A human-readable string explaining the selection (e.g., "Manual Bridge configuration is present and valid").
+
+## State Lifecycle
+
+1. **On Save:** Whenever the user updates Manual Bridge settings in the UI, the effective config is recalculated.
+2. **On Sync:** Whenever a successful Cloud Sync occurs, the effective config is recalculated.
+3. **At Runtime:** Components like `resolveBridgeAcpEndpointInternal` simply read from `effective.endpoint` without needing to know about the underlying priority rules.
diff --git a/docs/architecture/public-api/_generated/public-symbol-inventory.md b/docs/architecture/public-api/_generated/public-symbol-inventory.md
index 09242e2a..1dd9830c 100644
--- a/docs/architecture/public-api/_generated/public-symbol-inventory.md
+++ b/docs/architecture/public-api/_generated/public-symbol-inventory.md
@@ -1382,7 +1382,6 @@ _No extracted public top-level symbols._
| 68 | `class` | `AccountTokenConfigured` | `class AccountTokenConfigured {` |
| 108 | `class` | `AccountSecretLocator` | `class AccountSecretLocator {` |
| 166 | `class` | `AccountRemoteProfile` | `class AccountRemoteProfile {` |
-| 267 | `enum` | `AcpBridgeServerMode` | `enum AcpBridgeServerMode { cloudSynced }` |
| 269 | `class` | `AcpBridgeServerRemoteServerSummary` | `class AcpBridgeServerRemoteServerSummary {` |
| 312 | `class` | `AcpBridgeServerCloudSyncConfig` | `class AcpBridgeServerCloudSyncConfig {` |
| 370 | `class` | `AcpBridgeServerSelfHostedConfig` | `class AcpBridgeServerSelfHostedConfig {` |
diff --git a/docs/xworkmate-app-core-functional-test-plan-v1.md b/docs/xworkmate-app-core-functional-test-plan-v1.md
index 25a7637d..8ade1493 100644
--- a/docs/xworkmate-app-core-functional-test-plan-v1.md
+++ b/docs/xworkmate-app-core-functional-test-plan-v1.md
@@ -17,11 +17,24 @@
### 1. Provider 发现与 UI 展示
-- “智能体模式” provider 列表由 bridge `acp.capabilities` 动态驱动。
- UI 不依赖固定静态 provider 列表。
+- “智能体模式” provider 列表由 bridge `acp.capabilities` 动态驱动。
+- “Gateway 模式” provider 列表由 bridge `目前支持 openclaw
- 当 bridge 广告能力变化时,provider selector、状态文案和可执行状态同步更新。
- `auto` 模式下,UI 应表现为“由 bridge 当前可用项决定”,而不是写死默认 provider。
+ | Service Endpoint │ Protocol │ Result │ Functional Check │
+ ├───────────────────────┼────────────┼────────┼───────────────────────────────────┤
+ │ /acp-server/codex/ │ JSON-RPC │ PASS │ acp.capabilities returned │
+ │ │ (SSE/HTTP) │ │ successfully via /acp/rpc │
+ │ /acp-server/gemini/ │ JSON-RPC │ PASS │ acp.capabilities returned │
+ │ │ (SSE/HTTP) │ │ successfully via /acp/rpc │
+ │ /acp-server/opencode/ │ JSON-RPC │ PASS │ acp.capabilities returned │
+ │ │ (SSE/HTTP) │ │ successfully via /acp/rpc │
+ │ /gateway/openclaw/ │ WSS / RPC │ PASS │ WebSocket handshake successful at │
+ │ │ │ │ /acp; received connect.challenge
+
+
### 2. Assistant 线程体验
- `agent` 线程首次发送时自动绑定完整 `workspaceBinding`。
@@ -76,9 +89,9 @@
| Case | UI 侧目标能力 | 核心验收点 |
| --- | --- | --- |
-| `pptx` | 展示生成中的文稿任务与追问续写 | 首次生成结果可见;继续追问仍在同线程;workspace 不变;新文件或新版本可见 |
| `docx` | 展示文档生成结果 | `.docx` 写回当前线程;assistant 结果与 artifact 面板一致;后续补写仍在同线程 |
| `xlsx` | 展示表格与计算结果 | `.xlsx` 结果写回当前线程;后续追问继续修改同一线程内容 |
+| `pptx` | 展示生成中的文稿任务与追问续写 | 首次生成结果可见;继续追问仍在同线程;workspace 不变;新文件或新版本可见 |
| `pdf` | 展示文件转换结果 | `.pdf` 产物可见;转换后结果属于当前线程;继续操作不漂移 |
| `image-resizer` | 展示图片处理结果 | 输出图片写回当前线程;artifact 面板可见;尺寸变化可通过测试或 fixture 校验 |
| `browser` | 展示摘要、截图、日志 | assistant 文本有摘要;截图 / 日志进入 artifact 面板;继续浏览仍在当前线程 |
diff --git a/lib/app/app_controller_desktop_runtime_helpers.dart b/lib/app/app_controller_desktop_runtime_helpers.dart
index 9dc1dc8c..3b325f4f 100644
--- a/lib/app/app_controller_desktop_runtime_helpers.dart
+++ b/lib/app/app_controller_desktop_runtime_helpers.dart
@@ -644,7 +644,7 @@ extension AppControllerDesktopRuntimeHelpers on AppController {
}
}
final modeConfig = settings.acpBridgeServerModeConfig;
- final candidate = modeConfig.mode == AcpBridgeServerMode.manual
+ final candidate = modeConfig.usesSelfHostedBase
? modeConfig.selfHosted.serverUrl.trim()
: kManagedBridgeServerUrl;
final uri = Uri.tryParse(candidate.isEmpty ? kManagedBridgeServerUrl : candidate);
@@ -696,7 +696,7 @@ extension AppControllerDesktopRuntimeHelpers on AppController {
return normalizedToken;
}
final modeConfig = settings.acpBridgeServerModeConfig;
- if (modeConfig.mode == AcpBridgeServerMode.manual) {
+ if (modeConfig.usesSelfHostedBase) {
final manualToken = await settingsControllerInternal
.loadSecretValueByRef(modeConfig.selfHosted.passwordRef);
if (manualToken.trim().isNotEmpty) {
diff --git a/lib/features/settings/settings_account_panel.dart b/lib/features/settings/settings_account_panel.dart
index 4e82d18e..70eaf398 100644
--- a/lib/features/settings/settings_account_panel.dart
+++ b/lib/features/settings/settings_account_panel.dart
@@ -52,8 +52,7 @@ class SettingsAccountPanel extends StatelessWidget {
if (!accountSignedIn && !accountMfaRequired) {
return DefaultTabController(
length: 2,
- initialIndex: settings.acpBridgeServerModeConfig.mode ==
- AcpBridgeServerMode.manual
+ initialIndex: settings.acpBridgeServerModeConfig.effective.source == 'bridge'
? 1
: 0,
child: Column(
@@ -64,12 +63,10 @@ class SettingsAccountPanel extends StatelessWidget {
Tab(text: appText('手动 Bridge 配置', 'Manual Bridge Config')),
],
onTap: (index) {
- final mode = index == 1
- ? AcpBridgeServerMode.manual
- : AcpBridgeServerMode.cloudSynced;
- if (settings.acpBridgeServerModeConfig.mode != mode) {
- onSaveAccountProfile(); // This should trigger a save with the new mode
- }
+ // Switching tabs saves the profile, which triggers a resolution of the effective config.
+ // We don't need a boolean flag anymore; the presence/validity of sources determines the source.
+ // But we still want to save on tap to persist the user's intent.
+ onSaveAccountProfile();
},
),
const SizedBox(height: 24),
diff --git a/lib/features/settings/settings_page_core.dart b/lib/features/settings/settings_page_core.dart
index 90e39a87..d7ef35bd 100644
--- a/lib/features/settings/settings_page_core.dart
+++ b/lib/features/settings/settings_page_core.dart
@@ -113,14 +113,25 @@ class _SettingsPageState extends State {
final bridgeConfig = settings.acpBridgeServerModeConfig;
final isManual = DefaultTabController.of(context).index == 1;
+ var nextBridgeConfig = bridgeConfig.copyWith(
+ selfHosted: bridgeConfig.selfHosted.copyWith(
+ serverUrl: _bridgeUrlController.text.trim(),
+ username: isManual ? 'admin' : bridgeConfig.selfHosted.username,
+ ),
+ );
+
+ // Resolve the effective config based on the new sources
+ final nextEffective = resolveAcpBridgeServerEffectiveConfig(
+ widget.controller.settingsController,
+ config: nextBridgeConfig,
+ accountSyncState: widget.controller.settingsController.accountSyncState,
+ );
+
final nextSettings = settings.copyWith(
accountBaseUrl: _accountBaseUrlController.text.trim(),
accountUsername: _accountIdentifierController.text.trim(),
- acpBridgeServerModeConfig: bridgeConfig.copyWith(
- mode: isManual ? AcpBridgeServerMode.manual : AcpBridgeServerMode.cloudSynced,
- selfHosted: bridgeConfig.selfHosted.copyWith(
- serverUrl: _bridgeUrlController.text.trim(),
- ),
+ acpBridgeServerModeConfig: nextBridgeConfig.copyWith(
+ effective: nextEffective,
),
);
if (isManual && _bridgeTokenController.text.isNotEmpty) {
diff --git a/lib/runtime/runtime_controllers_settings_account.dart b/lib/runtime/runtime_controllers_settings_account.dart
index 03132011..32be839f 100644
--- a/lib/runtime/runtime_controllers_settings_account.dart
+++ b/lib/runtime/runtime_controllers_settings_account.dart
@@ -51,6 +51,19 @@ extension SettingsControllerAccountExtension on SettingsController {
Future loadEffectiveGatewayToken({int? profileIndex}) async {
final resolvedProfileIndex = (profileIndex ?? kGatewayRemoteProfileIndex)
.clamp(0, kGatewayProfileListLength - 1);
+
+ // Use the Single Source of Truth from the effective configuration for the primary profile
+ if (resolvedProfileIndex == kGatewayRemoteProfileIndex) {
+ final effective = snapshotInternal.acpBridgeServerModeConfig.effective;
+ if (effective.tokenRef.isNotEmpty) {
+ final token = await loadSecretValueByRef(effective.tokenRef);
+ if (token.isNotEmpty) {
+ return token;
+ }
+ }
+ }
+
+ // Local Override / Vault / Cloud Sync (Fallback if effective token is missing or for other profiles)
return resolveSecretValueInternal(
refName: gatewayTokenRefForProfileInternal(resolvedProfileIndex),
fallbackRefName: SecretStore.gatewayTokenRefKey(resolvedProfileIndex),
@@ -63,6 +76,11 @@ extension SettingsControllerAccountExtension on SettingsController {
Future loadEffectiveGatewayPassword({int? profileIndex}) async {
final resolvedProfileIndex = (profileIndex ?? kGatewayRemoteProfileIndex)
.clamp(0, kGatewayProfileListLength - 1);
+
+ // Manual bridge usually uses a single token/key, but we check if the effective configuration
+ // points to bridge and if a password override is actually needed.
+ // For now, we fall back to the standard resolution logic.
+
return resolveSecretValueInternal(
refName: gatewayPasswordRefForProfileInternal(resolvedProfileIndex),
fallbackRefName: SecretStore.gatewayPasswordRefKey(resolvedProfileIndex),
diff --git a/lib/runtime/runtime_controllers_settings_account_impl.dart b/lib/runtime/runtime_controllers_settings_account_impl.dart
index 33872432..c31d86a4 100644
--- a/lib/runtime/runtime_controllers_settings_account_impl.dart
+++ b/lib/runtime/runtime_controllers_settings_account_impl.dart
@@ -319,17 +319,31 @@ Future syncAccountSettingsInternal(
apisix: false,
),
);
- await controller.storeInternal.saveAccountSyncState(nextState);
final currentSettings = controller.snapshotInternal;
final currentModeConfig = currentSettings.acpBridgeServerModeConfig;
+
+ final nextEffective = resolveAcpBridgeServerEffectiveConfig(
+ controller,
+ config: currentModeConfig,
+ accountSyncState: nextState,
+ );
+
+ final identifier = (await controller.storeInternal.loadAccountSessionIdentifier())
+ ?.trim() ??
+ '';
final nextModeConfig = currentModeConfig.copyWith(
+ effective: nextEffective,
cloudSynced: currentModeConfig.cloudSynced.copyWith(
- accountBaseUrl: '',
- accountIdentifier: '',
+ accountBaseUrl: currentModeConfig.cloudSynced.accountBaseUrl.trim().isEmpty
+ ? normalizedBaseUrl
+ : currentModeConfig.cloudSynced.accountBaseUrl,
+ accountIdentifier: currentModeConfig.cloudSynced.accountIdentifier.trim().isEmpty
+ ? identifier
+ : currentModeConfig.cloudSynced.accountIdentifier,
lastSyncAt: nextState.lastSyncAtMs,
remoteServerSummary: currentModeConfig.cloudSynced.remoteServerSummary
.copyWith(
- endpoint: resolvedBridgeServerUrl,
+ endpoint: nextEffective.endpoint,
hasAdvancedOverrides: false,
),
),
@@ -390,8 +404,12 @@ Future logoutAccountSettingsInternal(
final currentSnapshot = controller.snapshotInternal;
final clearedCloudSync = currentSnapshot.acpBridgeServerModeConfig.cloudSynced
.copyWith(
- accountBaseUrl: '',
- accountIdentifier: '',
+ accountBaseUrl: quiet
+ ? currentSnapshot.acpBridgeServerModeConfig.cloudSynced.accountBaseUrl
+ : '',
+ accountIdentifier: quiet
+ ? currentSnapshot.acpBridgeServerModeConfig.cloudSynced.accountIdentifier
+ : '',
lastSyncAt: 0,
remoteServerSummary: currentSnapshot
.acpBridgeServerModeConfig
@@ -495,6 +513,11 @@ Future _persistAccountSessionSummaryFromProfilePayloadInternal(
if (summary.userId.trim().isNotEmpty) {
await controller.storeInternal.saveAccountSessionUserId(summary.userId);
}
+ if (summary.email.trim().isNotEmpty) {
+ await controller.storeInternal.saveAccountSessionIdentifier(
+ summary.email.trim(),
+ );
+ }
final identifier = summary.email.trim().isNotEmpty
? summary.email.trim()
: (await controller.storeInternal.loadAccountSessionIdentifier())
@@ -557,22 +580,49 @@ String _resolveBridgeServerUrl(Map payload) {
return '';
}
+AcpBridgeServerEffectiveConfig resolveAcpBridgeServerEffectiveConfig(
+ SettingsController controller, {
+ required AcpBridgeServerModeConfig config,
+ AccountSyncState? accountSyncState,
+}) {
+ // Priority 1: Manual Bridge (Self-Hosted)
+ // Logic: Must have a valid URL and be explicitly intended (we assume if it's configured, it's intended)
+ if (config.selfHosted.isConfigured) {
+ return AcpBridgeServerEffectiveConfig(
+ endpoint: config.selfHosted.serverUrl,
+ tokenRef: config.selfHosted.passwordRef,
+ source: 'bridge',
+ reason: 'Manual Bridge configuration is present and valid',
+ );
+ }
+
+ // Priority 2: Cloud Sync (svc.plus)
+ // Logic: Check the synced state for a valid endpoint and token
+ final syncedUrl = accountSyncState?.syncedDefaults.bridgeServerUrl.trim() ?? '';
+ final hasSyncedToken = accountSyncState?.tokenConfigured.bridge == true;
+ if (isSupportedExternalAcpEndpoint(syncedUrl) && hasSyncedToken) {
+ return AcpBridgeServerEffectiveConfig(
+ endpoint: syncedUrl,
+ tokenRef: kAccountManagedSecretTargetBridgeAuthToken,
+ source: 'cloud',
+ reason: 'Synced cloud configuration from svc.plus is active',
+ );
+ }
+
+ // Priority 3: Default Managed Fallback
+ return AcpBridgeServerEffectiveConfig(
+ endpoint: kManagedBridgeServerUrl,
+ tokenRef: '',
+ source: 'default',
+ reason: 'Falling back to default managed server',
+ );
+}
+
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;
+ return controller.snapshotInternal.acpBridgeServerModeConfig.effective.endpoint;
}
int _parseExpiresAtMs(Object? value) {
diff --git a/lib/runtime/runtime_models_account.dart b/lib/runtime/runtime_models_account.dart
index 4b175dd0..0fd1b4a2 100644
--- a/lib/runtime/runtime_models_account.dart
+++ b/lib/runtime/runtime_models_account.dart
@@ -265,17 +265,6 @@ class AccountRemoteProfile {
}
}
-enum AcpBridgeServerMode { cloudSynced, manual }
-
-extension AcpBridgeServerModeCopy on AcpBridgeServerMode {
- static AcpBridgeServerMode fromJsonValue(String? value) {
- return AcpBridgeServerMode.values.firstWhere(
- (item) => item.name == value,
- orElse: () => AcpBridgeServerMode.cloudSynced,
- );
- }
-}
-
class AcpBridgeServerRemoteServerSummary {
const AcpBridgeServerRemoteServerSummary({
required this.endpoint,
@@ -516,22 +505,77 @@ class AcpBridgeServerAdvancedOverrides {
}
}
+class AcpBridgeServerEffectiveConfig {
+ const AcpBridgeServerEffectiveConfig({
+ required this.endpoint,
+ required this.tokenRef,
+ required this.source,
+ required this.reason,
+ });
+
+ final String endpoint;
+ final String tokenRef;
+ final String source; // 'bridge' | 'cloud' | 'default'
+ final String reason;
+
+ factory AcpBridgeServerEffectiveConfig.defaults() {
+ return const AcpBridgeServerEffectiveConfig(
+ endpoint: kManagedBridgeServerUrl,
+ tokenRef: '',
+ source: 'default',
+ reason: 'No active source configured',
+ );
+ }
+
+ AcpBridgeServerEffectiveConfig copyWith({
+ String? endpoint,
+ String? tokenRef,
+ String? source,
+ String? reason,
+ }) {
+ return AcpBridgeServerEffectiveConfig(
+ endpoint: endpoint ?? this.endpoint,
+ tokenRef: tokenRef ?? this.tokenRef,
+ source: source ?? this.source,
+ reason: reason ?? this.reason,
+ );
+ }
+
+ Map toJson() {
+ return {
+ 'endpoint': endpoint,
+ 'tokenRef': tokenRef,
+ 'source': source,
+ 'reason': reason,
+ };
+ }
+
+ factory AcpBridgeServerEffectiveConfig.fromJson(Map json) {
+ return AcpBridgeServerEffectiveConfig(
+ endpoint: json['endpoint'] as String? ?? kManagedBridgeServerUrl,
+ tokenRef: json['tokenRef'] as String? ?? '',
+ source: json['source'] as String? ?? 'default',
+ reason: json['reason'] as String? ?? '',
+ );
+ }
+}
+
class AcpBridgeServerModeConfig {
const AcpBridgeServerModeConfig({
- required this.mode,
+ required this.effective,
required this.cloudSynced,
required this.selfHosted,
required this.advancedOverrides,
});
- final AcpBridgeServerMode mode;
+ final AcpBridgeServerEffectiveConfig effective;
final AcpBridgeServerCloudSyncConfig cloudSynced;
final AcpBridgeServerSelfHostedConfig selfHosted;
final AcpBridgeServerAdvancedOverrides advancedOverrides;
factory AcpBridgeServerModeConfig.defaults() {
return AcpBridgeServerModeConfig(
- mode: AcpBridgeServerMode.cloudSynced,
+ effective: AcpBridgeServerEffectiveConfig.defaults(),
cloudSynced: AcpBridgeServerCloudSyncConfig.defaults(),
selfHosted: AcpBridgeServerSelfHostedConfig.defaults(),
advancedOverrides: AcpBridgeServerAdvancedOverrides.defaults(),
@@ -539,30 +583,30 @@ class AcpBridgeServerModeConfig {
}
AcpBridgeServerModeConfig copyWith({
- AcpBridgeServerMode? mode,
+ AcpBridgeServerEffectiveConfig? effective,
AcpBridgeServerCloudSyncConfig? cloudSynced,
AcpBridgeServerSelfHostedConfig? selfHosted,
AcpBridgeServerAdvancedOverrides? advancedOverrides,
}) {
return AcpBridgeServerModeConfig(
- mode: mode ?? this.mode,
+ effective: effective ?? this.effective,
cloudSynced: cloudSynced ?? this.cloudSynced,
selfHosted: selfHosted ?? this.selfHosted,
advancedOverrides: advancedOverrides ?? this.advancedOverrides,
);
}
- bool get usesSelfHostedBase => mode == AcpBridgeServerMode.manual;
+ bool get usesSelfHostedBase => effective.source == 'bridge';
- bool get usesCloudSyncBase => mode == AcpBridgeServerMode.cloudSynced;
+ bool get usesCloudSyncBase => !usesSelfHostedBase;
- String get sourceTag => mode.name;
+ String get sourceTag => effective.source;
String toJsonString() => jsonEncode(toJson());
Map toJson() {
return {
- 'mode': mode.name,
+ 'effective': effective.toJson(),
'cloudSynced': cloudSynced.toJson(),
'selfHosted': selfHosted.toJson(),
'advancedOverrides': advancedOverrides.toJson(),
@@ -571,7 +615,9 @@ class AcpBridgeServerModeConfig {
factory AcpBridgeServerModeConfig.fromJson(Map json) {
return AcpBridgeServerModeConfig(
- mode: AcpBridgeServerModeCopy.fromJsonValue(json['mode'] as String?),
+ effective: AcpBridgeServerEffectiveConfig.fromJson(
+ (json['effective'] as Map?)?.cast() ?? const {},
+ ),
cloudSynced: AcpBridgeServerCloudSyncConfig.fromJson(
(json['cloudSynced'] as Map?)?.cast() ?? const {},
),
diff --git a/test/runtime/gateway_acp_client_auth_test.dart b/test/runtime/gateway_acp_client_auth_test.dart
index fea58336..bbbb6f80 100644
--- a/test/runtime/gateway_acp_client_auth_test.dart
+++ b/test/runtime/gateway_acp_client_auth_test.dart
@@ -274,9 +274,15 @@ void main() {
final settings = SettingsSnapshot.defaults().copyWith(
acpBridgeServerModeConfig: AcpBridgeServerModeConfig.defaults().copyWith(
- mode: AcpBridgeServerMode.manual,
+ effective: const AcpBridgeServerEffectiveConfig(
+ endpoint: 'https://manual-bridge.example.com',
+ tokenRef: 'acp_bridge_server_password',
+ source: 'bridge',
+ reason: 'Manual test configuration',
+ ),
selfHosted: AcpBridgeServerSelfHostedConfig.defaults().copyWith(
serverUrl: 'https://manual-bridge.example.com',
+ username: 'admin',
),
),
);