refactor(bridge): implement Single Source of Truth for bridge config and fix login persistence
- Refactor bridge configuration to resolve co-existing Manual and Cloud sources into a single persistent 'effective' state - Implement deterministic priority resolution: Manual Bridge > Cloud Sync > Default Fallback - Fix login issues by preserving account base URL and identifier during sync and session restoration - Streamline config/settings.yaml by removing redundant fields and adopting YAML format - Update documentation with new architecture guide for bridge-cloud coexistence and priority logic - Verify functional connectivity for codex, gemini, opencode, and openclaw bridge services
This commit is contained in:
parent
a04b22ec4a
commit
5851196fc7
32
config/settings.yaml
Normal file
32
config/settings.yaml
Normal file
@ -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
|
||||
62
docs/architecture/bridge-cloud-coexistence-priority.md
Normal file
62
docs/architecture/bridge-cloud-coexistence-priority.md
Normal file
@ -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[<b>Effective Source: bridge</b>\nEndpoint: selfHosted.serverUrl\nTokenRef: selfHosted.passwordRef]
|
||||
|
||||
CheckManual -->|No| CheckCloud{Is Cloud Source\nsynced & valid?}
|
||||
|
||||
CheckCloud -->|Yes| UseCloud[<b>Effective Source: cloud</b>\nEndpoint: cloudSynced.bridgeServerUrl\nTokenRef: bridge.auth_token]
|
||||
|
||||
CheckCloud -->|No| UseDefault[<b>Effective Source: default</b>\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.
|
||||
@ -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 {` |
|
||||
|
||||
@ -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 面板;继续浏览仍在当前线程 |
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -113,14 +113,25 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
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) {
|
||||
|
||||
@ -51,6 +51,19 @@ extension SettingsControllerAccountExtension on SettingsController {
|
||||
Future<String> 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<String> 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),
|
||||
|
||||
@ -319,17 +319,31 @@ Future<AccountSyncResult> 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<void> 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<void> _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<String, dynamic> 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) {
|
||||
|
||||
@ -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<String, dynamic> toJson() {
|
||||
return <String, dynamic>{
|
||||
'endpoint': endpoint,
|
||||
'tokenRef': tokenRef,
|
||||
'source': source,
|
||||
'reason': reason,
|
||||
};
|
||||
}
|
||||
|
||||
factory AcpBridgeServerEffectiveConfig.fromJson(Map<String, dynamic> 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<String, dynamic> toJson() {
|
||||
return <String, dynamic>{
|
||||
'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<String, dynamic> json) {
|
||||
return AcpBridgeServerModeConfig(
|
||||
mode: AcpBridgeServerModeCopy.fromJsonValue(json['mode'] as String?),
|
||||
effective: AcpBridgeServerEffectiveConfig.fromJson(
|
||||
(json['effective'] as Map?)?.cast<String, dynamic>() ?? const {},
|
||||
),
|
||||
cloudSynced: AcpBridgeServerCloudSyncConfig.fromJson(
|
||||
(json['cloudSynced'] as Map?)?.cast<String, dynamic>() ?? const {},
|
||||
),
|
||||
|
||||
@ -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',
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user