Merge branch 'codex/acp-bridge-modes'
This commit is contained in:
commit
adf3f6af83
@ -1,3 +1,4 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import '../test/helpers/test_keys.dart';
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import '../test/helpers/test_keys.dart';
|
||||
|
||||
@ -229,7 +229,13 @@ class _AccountPageState extends State<AccountPage> {
|
||||
String profileDescription,
|
||||
String sessionStatusText,
|
||||
String syncStatusText,
|
||||
AccountSyncState? accountSyncState,
|
||||
) {
|
||||
final cloudSync = settings.acpBridgeServerModeConfig.cloudSynced;
|
||||
final remoteSummary = cloudSync.remoteServerSummary;
|
||||
final syncSummaryText = remoteSummary.endpoint.trim().isEmpty
|
||||
? appText('还没有云端 ACP Bridge Server 摘要。', 'No cloud ACP Bridge Server summary yet.')
|
||||
: remoteSummary.endpoint;
|
||||
return SurfaceCard(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@ -259,6 +265,51 @@ class _AccountPageState extends State<AccountPage> {
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
appText('云端 ACP Bridge Server 摘要', 'Cloud ACP Bridge Server Summary'),
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'${appText('服务地址', 'Service URL')}: ${cloudSync.accountBaseUrl.trim().isEmpty ? settings.accountBaseUrl : cloudSync.accountBaseUrl}',
|
||||
key: const ValueKey('account-acp-sync-summary-url'),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
'${appText('同步目标', 'Synced Target')}: $syncSummaryText',
|
||||
key: const ValueKey('account-acp-sync-summary-endpoint'),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
'${appText('最近同步', 'Last Sync')}: ${accountSyncState == null || cloudSync.lastSyncAt <= 0 ? appText('尚未同步', 'Not synced yet') : DateTime.fromMillisecondsSinceEpoch(cloudSync.lastSyncAt).toLocal().toIso8601String()}',
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
FilledButton.tonal(
|
||||
key: const ValueKey('account-open-settings-acp'),
|
||||
onPressed: () => widget.controller.openSettings(
|
||||
tab: SettingsTab.gateway,
|
||||
),
|
||||
child: Text(
|
||||
appText(
|
||||
'前往设置中的 ACP Bridge Server',
|
||||
'Open ACP Bridge Server in Settings',
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
key: const ValueKey('account-base-url-field'),
|
||||
controller: _accountBaseUrlController,
|
||||
@ -364,22 +415,17 @@ class _AccountPageState extends State<AccountPage> {
|
||||
: '${accountSyncState.syncState} · ${accountSyncState.syncMessage}';
|
||||
final profileDescription = accountSignedIn
|
||||
? appText(
|
||||
'已登录,远端配置作为默认值;本地保存项优先',
|
||||
'Signed in. Remote defaults apply first, and local saved values win.',
|
||||
'这里继续只负责账号身份、MFA、工作区与同步摘要。ACP Bridge Server 的三模式配置已统一收口到设置页。',
|
||||
'This page now focuses on identity, MFA, workspace, and sync summary only. ACP Bridge Server mode configuration now lives in Settings.',
|
||||
)
|
||||
: accountMfaRequired
|
||||
? appText(
|
||||
'请输入 MFA 验证码完成同步,也可以返回编辑账号信息。',
|
||||
'Enter the MFA code to finish sync, or return to edit account details.',
|
||||
)
|
||||
: settings.accountLocalMode
|
||||
? appText(
|
||||
'本地模式 · 仅保存工作区偏好',
|
||||
'Local mode · saves workspace preferences only',
|
||||
)
|
||||
: appText(
|
||||
'登录后会同步远端默认配置,本地保存项可以覆盖远端默认值。',
|
||||
'Signing in syncs remote defaults, and local saved values can override them.',
|
||||
'登录后会同步云端默认配置;更细粒度的 Bridge Server、自托管和高级自定义请前往设置页。',
|
||||
'Signing in syncs the cloud defaults. For bridge server self-hosting and advanced overrides, use the Settings page.',
|
||||
);
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.fromLTRB(32, 32, 32, 8),
|
||||
@ -427,6 +473,7 @@ class _AccountPageState extends State<AccountPage> {
|
||||
profileDescription,
|
||||
sessionStatusText,
|
||||
syncStatusText,
|
||||
accountSyncState,
|
||||
),
|
||||
if (_tab == AccountTab.workspace)
|
||||
SurfaceCard(
|
||||
|
||||
@ -70,6 +70,9 @@ class SettingsPageStateInternal extends State<SettingsPage> {
|
||||
late final TextEditingController vaultTokenControllerInternal;
|
||||
late final TextEditingController ollamaApiKeyControllerInternal;
|
||||
late final TextEditingController runtimeLogFilterControllerInternal;
|
||||
late final TextEditingController acpBridgeServerUrlControllerInternal;
|
||||
late final TextEditingController acpBridgeServerUsernameControllerInternal;
|
||||
late final TextEditingController acpBridgeServerPasswordControllerInternal;
|
||||
late final Map<String, TextEditingController>
|
||||
externalAcpLabelControllersInternal;
|
||||
late final Map<String, TextEditingController>
|
||||
@ -102,6 +105,11 @@ class SettingsPageStateInternal extends State<SettingsPage> {
|
||||
String aiGatewayTestStateInternal = 'idle';
|
||||
String aiGatewayTestMessageInternal = '';
|
||||
String aiGatewayTestEndpointInternal = '';
|
||||
String acpBridgeServerUrlSyncedValueInternal = '';
|
||||
String acpBridgeServerUsernameSyncedValueInternal = '';
|
||||
String acpBridgeServerPasswordRefSyncedValueInternal = '';
|
||||
bool acpBridgeServerSelfHostedTestingInternal = false;
|
||||
String acpBridgeServerSelfHostedMessageInternal = '';
|
||||
GatewayIntegrationSubTabInternal integrationSubTabInternal =
|
||||
GatewayIntegrationSubTabInternal.gateway;
|
||||
int llmEndpointSlotLimitInternal = 1;
|
||||
@ -174,6 +182,9 @@ class SettingsPageStateInternal extends State<SettingsPage> {
|
||||
vaultTokenControllerInternal = TextEditingController();
|
||||
ollamaApiKeyControllerInternal = TextEditingController();
|
||||
runtimeLogFilterControllerInternal = TextEditingController();
|
||||
acpBridgeServerUrlControllerInternal = TextEditingController();
|
||||
acpBridgeServerUsernameControllerInternal = TextEditingController();
|
||||
acpBridgeServerPasswordControllerInternal = TextEditingController();
|
||||
externalAcpLabelControllersInternal = <String, TextEditingController>{};
|
||||
externalAcpEndpointControllersInternal = <String, TextEditingController>{};
|
||||
externalAcpAuthControllersInternal = <String, TextEditingController>{};
|
||||
@ -251,6 +262,9 @@ class SettingsPageStateInternal extends State<SettingsPage> {
|
||||
vaultTokenControllerInternal.dispose();
|
||||
ollamaApiKeyControllerInternal.dispose();
|
||||
runtimeLogFilterControllerInternal.dispose();
|
||||
acpBridgeServerUrlControllerInternal.dispose();
|
||||
acpBridgeServerUsernameControllerInternal.dispose();
|
||||
acpBridgeServerPasswordControllerInternal.dispose();
|
||||
for (final controller in externalAcpLabelControllersInternal.values) {
|
||||
controller.dispose();
|
||||
}
|
||||
|
||||
@ -43,6 +43,9 @@ extension SettingsPageGatewayMixinInternal on SettingsPageStateInternal {
|
||||
uiFeatures,
|
||||
);
|
||||
}
|
||||
final advancedEditable =
|
||||
settings.acpBridgeServerModeConfig.mode ==
|
||||
AcpBridgeServerMode.advancedCustom;
|
||||
final tabLabel = switch (integrationSubTabInternal) {
|
||||
GatewayIntegrationSubTabInternal.gateway => 'OpenClaw Gateway',
|
||||
GatewayIntegrationSubTabInternal.vault => appText(
|
||||
@ -63,6 +66,8 @@ extension SettingsPageGatewayMixinInternal on SettingsPageStateInternal {
|
||||
),
|
||||
};
|
||||
return [
|
||||
buildAcpBridgeServerModeCardInternal(context, controller, settings),
|
||||
const SizedBox(height: 16),
|
||||
SectionTabs(
|
||||
items: <String>[
|
||||
'OpenClaw Gateway',
|
||||
@ -88,33 +93,45 @@ extension SettingsPageGatewayMixinInternal on SettingsPageStateInternal {
|
||||
const SizedBox(height: 16),
|
||||
...switch (integrationSubTabInternal) {
|
||||
GatewayIntegrationSubTabInternal.gateway => <Widget>[
|
||||
buildCollapsibleGatewaySectionInternal(
|
||||
context: context,
|
||||
title: 'OpenClaw Gateway',
|
||||
expanded: openClawGatewayExpandedInternal,
|
||||
onChanged: (value) => setStateInternal(() {
|
||||
openClawGatewayExpandedInternal = value;
|
||||
}),
|
||||
child: buildOpenClawGatewayCardInternal(
|
||||
context,
|
||||
controller,
|
||||
settings,
|
||||
Opacity(
|
||||
opacity: advancedEditable ? 1 : 0.72,
|
||||
child: IgnorePointer(
|
||||
ignoring: !advancedEditable,
|
||||
child: buildCollapsibleGatewaySectionInternal(
|
||||
context: context,
|
||||
title: 'OpenClaw Gateway',
|
||||
expanded: openClawGatewayExpandedInternal,
|
||||
onChanged: (value) => setStateInternal(() {
|
||||
openClawGatewayExpandedInternal = value;
|
||||
}),
|
||||
child: buildOpenClawGatewayCardInternal(
|
||||
context,
|
||||
controller,
|
||||
settings,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
GatewayIntegrationSubTabInternal.vault => <Widget>[
|
||||
if (uiFeatures.supportsVaultServer)
|
||||
buildCollapsibleGatewaySectionInternal(
|
||||
context: context,
|
||||
title: appText('Vault Server', 'Vault Server'),
|
||||
expanded: vaultServerExpandedInternal,
|
||||
onChanged: (value) => setStateInternal(() {
|
||||
vaultServerExpandedInternal = value;
|
||||
}),
|
||||
child: buildVaultProviderCardInternal(
|
||||
context,
|
||||
controller,
|
||||
settings,
|
||||
Opacity(
|
||||
opacity: advancedEditable ? 1 : 0.72,
|
||||
child: IgnorePointer(
|
||||
ignoring: !advancedEditable,
|
||||
child: buildCollapsibleGatewaySectionInternal(
|
||||
context: context,
|
||||
title: appText('Vault Server', 'Vault Server'),
|
||||
expanded: vaultServerExpandedInternal,
|
||||
onChanged: (value) => setStateInternal(() {
|
||||
vaultServerExpandedInternal = value;
|
||||
}),
|
||||
child: buildVaultProviderCardInternal(
|
||||
context,
|
||||
controller,
|
||||
settings,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
@ -129,22 +146,150 @@ extension SettingsPageGatewayMixinInternal on SettingsPageStateInternal {
|
||||
),
|
||||
],
|
||||
GatewayIntegrationSubTabInternal.llm => <Widget>[
|
||||
buildCollapsibleGatewaySectionInternal(
|
||||
Opacity(
|
||||
opacity: advancedEditable ? 1 : 0.72,
|
||||
child: IgnorePointer(
|
||||
ignoring: !advancedEditable,
|
||||
child: buildCollapsibleGatewaySectionInternal(
|
||||
context: context,
|
||||
title: appText('LLM 接入点', 'LLM Endpoints'),
|
||||
expanded: aiGatewayExpandedInternal,
|
||||
onChanged: (value) => setStateInternal(() {
|
||||
aiGatewayExpandedInternal = value;
|
||||
}),
|
||||
child: buildLlmEndpointManagerInternal(
|
||||
context,
|
||||
controller,
|
||||
settings,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
GatewayIntegrationSubTabInternal.acp => <Widget>[
|
||||
Opacity(
|
||||
opacity: advancedEditable ? 1 : 0.72,
|
||||
child: IgnorePointer(
|
||||
ignoring: !advancedEditable,
|
||||
child: buildCollapsibleGatewaySectionInternal(
|
||||
context: context,
|
||||
title: appText(
|
||||
'外部 ACP Server Endpoint',
|
||||
'External ACP Server Endpoints',
|
||||
),
|
||||
expanded: externalAcpExpandedInternal,
|
||||
onChanged: (value) => setStateInternal(() {
|
||||
externalAcpExpandedInternal = value;
|
||||
}),
|
||||
child: buildExternalAcpEndpointManagerInternal(
|
||||
context,
|
||||
controller,
|
||||
settings,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
GatewayIntegrationSubTabInternal.skills => <Widget>[
|
||||
Opacity(
|
||||
opacity: advancedEditable ? 1 : 0.72,
|
||||
child: IgnorePointer(
|
||||
ignoring: !advancedEditable,
|
||||
child: buildCollapsibleGatewaySectionInternal(
|
||||
context: context,
|
||||
title: appText('SKILLS 目录授权', 'SKILLS Directory Authorization'),
|
||||
expanded: skillsDirectoryAuthorizationExpandedInternal,
|
||||
onChanged: (value) => setStateInternal(() {
|
||||
skillsDirectoryAuthorizationExpandedInternal = value;
|
||||
}),
|
||||
child: SkillDirectoryAuthorizationCard(
|
||||
controller: controller,
|
||||
showHeader: false,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
List<Widget> buildUnifiedGatewaySectionsInternal(
|
||||
BuildContext context,
|
||||
AppController controller,
|
||||
SettingsSnapshot settings,
|
||||
UiFeatureAccess uiFeatures,
|
||||
) {
|
||||
final advancedEditable =
|
||||
settings.acpBridgeServerModeConfig.mode ==
|
||||
AcpBridgeServerMode.advancedCustom;
|
||||
return [
|
||||
buildAcpBridgeServerModeCardInternal(context, controller, settings),
|
||||
const SizedBox(height: 16),
|
||||
Opacity(
|
||||
opacity: advancedEditable ? 1 : 0.72,
|
||||
child: IgnorePointer(
|
||||
ignoring: !advancedEditable,
|
||||
child: buildCollapsibleGatewaySectionInternal(
|
||||
context: context,
|
||||
title: 'OpenClaw Gateway',
|
||||
expanded: openClawGatewayExpandedInternal,
|
||||
onChanged: (value) => setStateInternal(() {
|
||||
openClawGatewayExpandedInternal = value;
|
||||
}),
|
||||
child: buildOpenClawGatewayCardInternal(context, controller, settings),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
if (uiFeatures.supportsVaultServer)
|
||||
Opacity(
|
||||
opacity: advancedEditable ? 1 : 0.72,
|
||||
child: IgnorePointer(
|
||||
ignoring: !advancedEditable,
|
||||
child: buildCollapsibleGatewaySectionInternal(
|
||||
context: context,
|
||||
title: appText('Vault Server', 'Vault Server'),
|
||||
expanded: vaultServerExpandedInternal,
|
||||
onChanged: (value) => setStateInternal(() {
|
||||
vaultServerExpandedInternal = value;
|
||||
}),
|
||||
child: buildVaultProviderCardInternal(context, controller, settings),
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
SurfaceCard(
|
||||
borderWidth: settingsHairlineBorderWidthInternal,
|
||||
child: Text(
|
||||
appText(
|
||||
'当前发布配置未开放 Vault Server 参数。',
|
||||
'Vault Server settings are disabled in this release configuration.',
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Opacity(
|
||||
opacity: advancedEditable ? 1 : 0.72,
|
||||
child: IgnorePointer(
|
||||
ignoring: !advancedEditable,
|
||||
child: buildCollapsibleGatewaySectionInternal(
|
||||
context: context,
|
||||
title: appText('LLM 接入点', 'LLM Endpoints'),
|
||||
expanded: aiGatewayExpandedInternal,
|
||||
onChanged: (value) => setStateInternal(() {
|
||||
aiGatewayExpandedInternal = value;
|
||||
}),
|
||||
child: buildLlmEndpointManagerInternal(
|
||||
context,
|
||||
controller,
|
||||
settings,
|
||||
),
|
||||
child: buildLlmEndpointManagerInternal(context, controller, settings),
|
||||
),
|
||||
],
|
||||
GatewayIntegrationSubTabInternal.acp => <Widget>[
|
||||
buildCollapsibleGatewaySectionInternal(
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Opacity(
|
||||
opacity: advancedEditable ? 1 : 0.72,
|
||||
child: IgnorePointer(
|
||||
ignoring: !advancedEditable,
|
||||
child: buildCollapsibleGatewaySectionInternal(
|
||||
context: context,
|
||||
title: appText(
|
||||
'外部 ACP Server Endpoint',
|
||||
@ -160,9 +305,14 @@ extension SettingsPageGatewayMixinInternal on SettingsPageStateInternal {
|
||||
settings,
|
||||
),
|
||||
),
|
||||
],
|
||||
GatewayIntegrationSubTabInternal.skills => <Widget>[
|
||||
buildCollapsibleGatewaySectionInternal(
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Opacity(
|
||||
opacity: advancedEditable ? 1 : 0.72,
|
||||
child: IgnorePointer(
|
||||
ignoring: !advancedEditable,
|
||||
child: buildCollapsibleGatewaySectionInternal(
|
||||
context: context,
|
||||
title: appText('SKILLS 目录授权', 'SKILLS Directory Authorization'),
|
||||
expanded: skillsDirectoryAuthorizationExpandedInternal,
|
||||
@ -174,86 +324,6 @@ extension SettingsPageGatewayMixinInternal on SettingsPageStateInternal {
|
||||
showHeader: false,
|
||||
),
|
||||
),
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
List<Widget> buildUnifiedGatewaySectionsInternal(
|
||||
BuildContext context,
|
||||
AppController controller,
|
||||
SettingsSnapshot settings,
|
||||
UiFeatureAccess uiFeatures,
|
||||
) {
|
||||
return [
|
||||
buildCollapsibleGatewaySectionInternal(
|
||||
context: context,
|
||||
title: 'OpenClaw Gateway',
|
||||
expanded: openClawGatewayExpandedInternal,
|
||||
onChanged: (value) => setStateInternal(() {
|
||||
openClawGatewayExpandedInternal = value;
|
||||
}),
|
||||
child: buildOpenClawGatewayCardInternal(context, controller, settings),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
if (uiFeatures.supportsVaultServer)
|
||||
buildCollapsibleGatewaySectionInternal(
|
||||
context: context,
|
||||
title: appText('Vault Server', 'Vault Server'),
|
||||
expanded: vaultServerExpandedInternal,
|
||||
onChanged: (value) => setStateInternal(() {
|
||||
vaultServerExpandedInternal = value;
|
||||
}),
|
||||
child: buildVaultProviderCardInternal(context, controller, settings),
|
||||
)
|
||||
else
|
||||
SurfaceCard(
|
||||
borderWidth: settingsHairlineBorderWidthInternal,
|
||||
child: Text(
|
||||
appText(
|
||||
'当前发布配置未开放 Vault Server 参数。',
|
||||
'Vault Server settings are disabled in this release configuration.',
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
buildCollapsibleGatewaySectionInternal(
|
||||
context: context,
|
||||
title: appText('LLM 接入点', 'LLM Endpoints'),
|
||||
expanded: aiGatewayExpandedInternal,
|
||||
onChanged: (value) => setStateInternal(() {
|
||||
aiGatewayExpandedInternal = value;
|
||||
}),
|
||||
child: buildLlmEndpointManagerInternal(context, controller, settings),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
buildCollapsibleGatewaySectionInternal(
|
||||
context: context,
|
||||
title: appText(
|
||||
'外部 ACP Server Endpoint',
|
||||
'External ACP Server Endpoints',
|
||||
),
|
||||
expanded: externalAcpExpandedInternal,
|
||||
onChanged: (value) => setStateInternal(() {
|
||||
externalAcpExpandedInternal = value;
|
||||
}),
|
||||
child: buildExternalAcpEndpointManagerInternal(
|
||||
context,
|
||||
controller,
|
||||
settings,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
buildCollapsibleGatewaySectionInternal(
|
||||
context: context,
|
||||
title: appText('SKILLS 目录授权', 'SKILLS Directory Authorization'),
|
||||
expanded: skillsDirectoryAuthorizationExpandedInternal,
|
||||
onChanged: (value) => setStateInternal(() {
|
||||
skillsDirectoryAuthorizationExpandedInternal = value;
|
||||
}),
|
||||
child: SkillDirectoryAuthorizationCard(
|
||||
controller: controller,
|
||||
showHeader: false,
|
||||
),
|
||||
),
|
||||
];
|
||||
|
||||
@ -1,13 +1,17 @@
|
||||
// ignore_for_file: unused_import, unnecessary_import
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../app/app_controller.dart';
|
||||
import '../../i18n/app_language.dart';
|
||||
import '../../runtime/gateway_acp_client.dart';
|
||||
import '../../runtime/runtime_controllers.dart';
|
||||
import '../../runtime/runtime_models.dart';
|
||||
import '../../models/app_models.dart';
|
||||
import '../../widgets/surface_card.dart';
|
||||
import 'settings_page_core.dart';
|
||||
import 'settings_page_support.dart';
|
||||
import 'settings_page_widgets.dart';
|
||||
@ -146,6 +150,294 @@ bool shouldRetryExternalAcpTestFailure(Object error) {
|
||||
}
|
||||
|
||||
extension SettingsPageGatewayAcpMixinInternal on SettingsPageStateInternal {
|
||||
Widget buildAcpBridgeServerModeCardInternal(
|
||||
BuildContext context,
|
||||
AppController controller,
|
||||
SettingsSnapshot settings,
|
||||
) {
|
||||
syncAcpBridgeServerModeDraftControllersInternal(settings);
|
||||
final modeConfig = settings.acpBridgeServerModeConfig;
|
||||
final accountController = controller.settingsController;
|
||||
final accountSyncState = accountController.accountSyncState;
|
||||
final accountSignedIn = accountController.accountSignedIn;
|
||||
final accountBusy = accountController.accountBusy;
|
||||
final cloudSync = modeConfig.cloudSynced;
|
||||
final remoteSummary = cloudSync.remoteServerSummary;
|
||||
final currentSource = switch (modeConfig.sourceTag) {
|
||||
'cloudSynced' => appText('云端同步', 'Cloud Sync'),
|
||||
'selfHosted' => appText('本地 Server', 'Self-hosted Server'),
|
||||
_ => appText('高级覆盖', 'Advanced Override'),
|
||||
};
|
||||
final syncStatus = accountSyncState?.syncState.trim().isNotEmpty == true
|
||||
? accountSyncState!.syncState
|
||||
: appText('未同步', 'Not synced');
|
||||
final lastSyncLabel = cloudSync.lastSyncAt <= 0
|
||||
? appText('尚未同步', 'Not synced yet')
|
||||
: DateTime.fromMillisecondsSinceEpoch(
|
||||
cloudSync.lastSyncAt,
|
||||
).toLocal().toIso8601String();
|
||||
return SurfaceCard(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'XWorkmate ACP Bridge Server',
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
appText(
|
||||
'XWorkmate App 继续只承担纯客户端职责:配置、会话、安全存储与连接编排。云端、自托管和高级自定义都通过这里统一收口,不在 App 内承载服务端逻辑。',
|
||||
'XWorkmate App remains a pure client: configuration, session, secure storage, and connection orchestration only. Cloud, self-hosted, and advanced custom flows are unified here without embedding server responsibilities into the app.',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Wrap(
|
||||
spacing: 10,
|
||||
runSpacing: 10,
|
||||
children: [
|
||||
ChoiceChip(
|
||||
key: const ValueKey('acp-bridge-mode-cloud'),
|
||||
label: Text(appText('在线同步配置', 'Cloud Sync')),
|
||||
selected: modeConfig.mode == AcpBridgeServerMode.cloudSynced,
|
||||
onSelected: (_) => saveSettingsInternal(
|
||||
controller,
|
||||
settings.copyWith(
|
||||
accountLocalMode: false,
|
||||
acpBridgeServerModeConfig: modeConfig.copyWith(
|
||||
mode: AcpBridgeServerMode.cloudSynced,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
ChoiceChip(
|
||||
key: const ValueKey('acp-bridge-mode-self-hosted'),
|
||||
label: Text(appText('本地模式', 'Self-hosted')),
|
||||
selected: modeConfig.mode == AcpBridgeServerMode.selfHosted,
|
||||
onSelected: (_) => saveSettingsInternal(
|
||||
controller,
|
||||
settings.copyWith(
|
||||
accountLocalMode: true,
|
||||
acpBridgeServerModeConfig: modeConfig.copyWith(
|
||||
mode: AcpBridgeServerMode.selfHosted,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
ChoiceChip(
|
||||
key: const ValueKey('acp-bridge-mode-advanced'),
|
||||
label: Text(appText('高级自定义', 'Advanced Custom')),
|
||||
selected: modeConfig.mode == AcpBridgeServerMode.advancedCustom,
|
||||
onSelected: (_) => saveSettingsInternal(
|
||||
controller,
|
||||
settings.captureAcpBridgeServerAdvancedOverrides().copyWith(
|
||||
acpBridgeServerModeConfig: settings
|
||||
.captureAcpBridgeServerAdvancedOverrides()
|
||||
.acpBridgeServerModeConfig
|
||||
.copyWith(mode: AcpBridgeServerMode.advancedCustom),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Wrap(
|
||||
spacing: 12,
|
||||
runSpacing: 12,
|
||||
children: [
|
||||
StatusChipInternal(
|
||||
label: '${appText('当前来源', 'Source')}: $currentSource',
|
||||
tone: StatusChipToneInternal.ready,
|
||||
),
|
||||
StatusChipInternal(
|
||||
label: '${appText('同步状态', 'Sync')}: $syncStatus',
|
||||
tone: accountSignedIn
|
||||
? StatusChipToneInternal.ready
|
||||
: StatusChipToneInternal.idle,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
...switch (modeConfig.mode) {
|
||||
AcpBridgeServerMode.cloudSynced => <Widget>[
|
||||
Text(
|
||||
accountSignedIn
|
||||
? appText(
|
||||
'已登录云账户,可直接同步远端 ACP Bridge Server 配置。',
|
||||
'Signed in to the cloud account. You can sync the remote ACP Bridge Server configuration directly.',
|
||||
)
|
||||
: appText(
|
||||
'当前未登录云账户。普通用户建议先登录,再从云端同步默认配置。',
|
||||
'No cloud account is signed in. For most users, sign in first and sync the default configuration from the cloud.',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
'${appText('服务地址', 'Service URL')}: ${cloudSync.accountBaseUrl.trim().isEmpty ? settings.accountBaseUrl : cloudSync.accountBaseUrl}',
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
'${appText('账号', 'Account')}: ${cloudSync.accountIdentifier.trim().isEmpty ? settings.accountUsername : cloudSync.accountIdentifier}',
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
'${appText('远端摘要', 'Remote Summary')}: ${remoteSummary.endpoint.trim().isEmpty ? appText('待同步', 'Pending sync') : remoteSummary.endpoint}',
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
'${appText('最近同步', 'Last Sync')}: $lastSyncLabel',
|
||||
key: const ValueKey('acp-bridge-cloud-last-sync'),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
'${appText('高级覆盖', 'Advanced Override')}: ${remoteSummary.hasAdvancedOverrides ? appText('存在', 'Present') : appText('无', 'None')}',
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
Wrap(
|
||||
spacing: 10,
|
||||
runSpacing: 10,
|
||||
children: [
|
||||
FilledButton.tonal(
|
||||
key: const ValueKey('acp-bridge-cloud-open-account'),
|
||||
onPressed: () =>
|
||||
controller.navigateTo(WorkspaceDestination.account),
|
||||
child: Text(appText('登录 / 管理账号', 'Open Account')),
|
||||
),
|
||||
FilledButton.tonal(
|
||||
key: const ValueKey('acp-bridge-cloud-sync'),
|
||||
onPressed: accountBusy || !accountSignedIn
|
||||
? null
|
||||
: () => accountController.syncAccountSettings(
|
||||
baseUrl: settings.accountBaseUrl,
|
||||
),
|
||||
child: Text(appText('重新同步', 'Sync Again')),
|
||||
),
|
||||
FilledButton.tonal(
|
||||
key: const ValueKey('acp-bridge-cloud-disconnect'),
|
||||
onPressed: accountBusy || !accountSignedIn
|
||||
? null
|
||||
: accountController.logoutAccount,
|
||||
child: Text(appText('断开', 'Disconnect')),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
AcpBridgeServerMode.selfHosted => <Widget>[
|
||||
buildAcpBridgeServerSelfHostedPanelInternal(
|
||||
context,
|
||||
controller,
|
||||
settings,
|
||||
),
|
||||
],
|
||||
AcpBridgeServerMode.advancedCustom => <Widget>[
|
||||
Text(
|
||||
appText(
|
||||
'高级自定义会把下面的 OpenClaw Gateway / Vault Server / LLM Endpoint / 外部 ACP Server endpoint / SKILLS 目录 当作覆盖层。未覆盖的值继续继承当前基础模式。',
|
||||
'Advanced custom mode treats the OpenClaw Gateway / Vault Server / LLM Endpoint / external ACP server endpoint / SKILLS directory below as overrides. Fields you do not override keep inheriting from the current base mode.',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
FilledButton.tonal(
|
||||
key: const ValueKey('acp-bridge-advanced-reset'),
|
||||
onPressed: () => resetAcpBridgeServerAdvancedOverridesInternal(
|
||||
controller,
|
||||
settings,
|
||||
),
|
||||
child: Text(appText('清空高级覆盖', 'Clear Advanced Overrides')),
|
||||
),
|
||||
],
|
||||
},
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildAcpBridgeServerSelfHostedPanelInternal(
|
||||
BuildContext context,
|
||||
AppController controller,
|
||||
SettingsSnapshot settings,
|
||||
) {
|
||||
final selfHosted = settings.acpBridgeServerModeConfig.selfHosted;
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TextField(
|
||||
key: const ValueKey('acp-bridge-self-hosted-url'),
|
||||
controller: acpBridgeServerUrlControllerInternal,
|
||||
decoration: InputDecoration(
|
||||
labelText: appText(
|
||||
'ACP Bridge Server URL',
|
||||
'ACP Bridge Server URL',
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
key: const ValueKey('acp-bridge-self-hosted-username'),
|
||||
controller: acpBridgeServerUsernameControllerInternal,
|
||||
decoration: InputDecoration(
|
||||
labelText: appText('用户', 'Username'),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
key: const ValueKey('acp-bridge-self-hosted-password'),
|
||||
controller: acpBridgeServerPasswordControllerInternal,
|
||||
obscureText: true,
|
||||
decoration: InputDecoration(
|
||||
labelText: appText('密码', 'Password'),
|
||||
helperText: appText(
|
||||
'密码只进入平台 secure storage,不写入普通 settings。',
|
||||
'The password is stored only in platform secure storage and never in plain settings.',
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'${appText('密码引用', 'Password Ref')}: ${selfHosted.passwordRef}',
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
Wrap(
|
||||
spacing: 10,
|
||||
runSpacing: 10,
|
||||
children: [
|
||||
OutlinedButton(
|
||||
key: const ValueKey('acp-bridge-self-hosted-test'),
|
||||
onPressed: acpBridgeServerSelfHostedTestingInternal
|
||||
? null
|
||||
: () => testAcpBridgeServerSelfHostedInternal(controller),
|
||||
child: Text(
|
||||
acpBridgeServerSelfHostedTestingInternal
|
||||
? appText('测试中...', 'Testing...')
|
||||
: appText('测试连接', 'Test Connection'),
|
||||
),
|
||||
),
|
||||
FilledButton.tonal(
|
||||
key: const ValueKey('acp-bridge-self-hosted-save'),
|
||||
onPressed: () => saveAcpBridgeServerSelfHostedInternal(
|
||||
controller,
|
||||
settings,
|
||||
),
|
||||
child: Text(appText('保存', 'Save')),
|
||||
),
|
||||
FilledButton(
|
||||
key: const ValueKey('acp-bridge-self-hosted-connect'),
|
||||
onPressed: () => connectAcpBridgeServerSelfHostedInternal(
|
||||
controller,
|
||||
settings,
|
||||
),
|
||||
child: Text(appText('连接', 'Connect')),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (acpBridgeServerSelfHostedMessageInternal.trim().isNotEmpty) ...[
|
||||
const SizedBox(height: 10),
|
||||
Text(acpBridgeServerSelfHostedMessageInternal),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildExternalAcpEndpointManagerInternal(
|
||||
BuildContext context,
|
||||
AppController controller,
|
||||
@ -194,6 +486,152 @@ extension SettingsPageGatewayAcpMixinInternal on SettingsPageStateInternal {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> saveAcpBridgeServerSelfHostedInternal(
|
||||
AppController controller,
|
||||
SettingsSnapshot settings,
|
||||
) async {
|
||||
final modeConfig = settings.acpBridgeServerModeConfig;
|
||||
final nextSelfHosted = modeConfig.selfHosted.copyWith(
|
||||
serverUrl: acpBridgeServerUrlControllerInternal.text,
|
||||
username: acpBridgeServerUsernameControllerInternal.text,
|
||||
);
|
||||
final password = acpBridgeServerPasswordControllerInternal.text.trim();
|
||||
if (password.isNotEmpty) {
|
||||
await controller.settingsController.saveSecretValueByRef(
|
||||
nextSelfHosted.passwordRef,
|
||||
password,
|
||||
provider: 'ACP Bridge Server',
|
||||
module: 'Settings',
|
||||
);
|
||||
}
|
||||
final nextSettings = settings.captureAcpBridgeServerAdvancedOverrides().copyWith(
|
||||
accountLocalMode: true,
|
||||
acpBridgeServerModeConfig: settings
|
||||
.captureAcpBridgeServerAdvancedOverrides()
|
||||
.acpBridgeServerModeConfig
|
||||
.copyWith(
|
||||
mode: AcpBridgeServerMode.selfHosted,
|
||||
selfHosted: nextSelfHosted,
|
||||
),
|
||||
);
|
||||
await saveSettingsInternal(controller, nextSettings);
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
acpBridgeServerPasswordControllerInternal.clear();
|
||||
setStateInternal(() {
|
||||
acpBridgeServerSelfHostedMessageInternal = appText(
|
||||
'Self-hosted 配置已保存,密码已进入 secure storage。',
|
||||
'The self-hosted configuration was saved and the password is now in secure storage.',
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> connectAcpBridgeServerSelfHostedInternal(
|
||||
AppController controller,
|
||||
SettingsSnapshot settings,
|
||||
) async {
|
||||
await saveAcpBridgeServerSelfHostedInternal(controller, settings);
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
await testAcpBridgeServerSelfHostedInternal(controller);
|
||||
}
|
||||
|
||||
Future<void> testAcpBridgeServerSelfHostedInternal(
|
||||
AppController controller,
|
||||
) async {
|
||||
final endpointText = acpBridgeServerUrlControllerInternal.text.trim();
|
||||
final username = acpBridgeServerUsernameControllerInternal.text.trim();
|
||||
if (endpointText.isEmpty || username.isEmpty) {
|
||||
setStateInternal(() {
|
||||
acpBridgeServerSelfHostedMessageInternal = appText(
|
||||
'请先填写 URL 和用户。',
|
||||
'Enter the URL and username first.',
|
||||
);
|
||||
});
|
||||
return;
|
||||
}
|
||||
final endpoint = Uri.tryParse(endpointText);
|
||||
if (endpoint == null || endpoint.host.trim().isEmpty) {
|
||||
setStateInternal(() {
|
||||
acpBridgeServerSelfHostedMessageInternal = appText(
|
||||
'请输入有效的 ACP Bridge Server URL。',
|
||||
'Enter a valid ACP Bridge Server URL.',
|
||||
);
|
||||
});
|
||||
return;
|
||||
}
|
||||
final password =
|
||||
acpBridgeServerPasswordControllerInternal.text.trim().isNotEmpty
|
||||
? acpBridgeServerPasswordControllerInternal.text.trim()
|
||||
: await controller.settingsController.loadSecretValueByRef(
|
||||
controller.settings.acpBridgeServerModeConfig.selfHosted.passwordRef,
|
||||
);
|
||||
final authorization = password.isEmpty
|
||||
? ''
|
||||
: 'Basic ${base64Encode(utf8.encode('$username:$password'))}';
|
||||
setStateInternal(() {
|
||||
acpBridgeServerSelfHostedTestingInternal = true;
|
||||
acpBridgeServerSelfHostedMessageInternal = '';
|
||||
});
|
||||
try {
|
||||
final capabilities = await controller.gatewayAcpClientInternal
|
||||
.loadCapabilities(
|
||||
forceRefresh: true,
|
||||
endpointOverride: endpoint,
|
||||
authorizationOverride: authorization,
|
||||
);
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
setStateInternal(() {
|
||||
acpBridgeServerSelfHostedMessageInternal =
|
||||
describeExternalAcpTestSuccess(capabilities);
|
||||
});
|
||||
} catch (error) {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
setStateInternal(() {
|
||||
acpBridgeServerSelfHostedMessageInternal =
|
||||
describeExternalAcpTestFailure(error, endpoint: endpoint);
|
||||
});
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setStateInternal(() {
|
||||
acpBridgeServerSelfHostedTestingInternal = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> resetAcpBridgeServerAdvancedOverridesInternal(
|
||||
AppController controller,
|
||||
SettingsSnapshot settings,
|
||||
) async {
|
||||
var next = settings.copyWith(
|
||||
gatewayProfiles: SettingsSnapshot.defaults().gatewayProfiles,
|
||||
vault: VaultConfig.defaults(),
|
||||
aiGateway: AiGatewayProfile.defaults(),
|
||||
externalAcpEndpoints: SettingsSnapshot.defaults().externalAcpEndpoints,
|
||||
authorizedSkillDirectories:
|
||||
SettingsSnapshot.defaults().authorizedSkillDirectories,
|
||||
acpBridgeServerModeConfig: settings.acpBridgeServerModeConfig.copyWith(
|
||||
mode: settings.acpBridgeServerModeConfig.usesSelfHostedBase
|
||||
? AcpBridgeServerMode.selfHosted
|
||||
: AcpBridgeServerMode.cloudSynced,
|
||||
),
|
||||
);
|
||||
await saveSettingsInternal(controller, next);
|
||||
if (controller.settingsController.accountSignedIn &&
|
||||
next.acpBridgeServerModeConfig.usesCloudSyncBase) {
|
||||
await controller.settingsController.syncAccountSettings(
|
||||
baseUrl: next.accountBaseUrl,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget buildExternalAcpProviderCardInternal(
|
||||
BuildContext context,
|
||||
AppController controller,
|
||||
|
||||
@ -306,15 +306,6 @@ extension SettingsPageSectionsMixinInternal on SettingsPageStateInternal {
|
||||
settings.copyWith(showDockIcon: value),
|
||||
),
|
||||
),
|
||||
if (uiFeatures.supportsAccountAccess)
|
||||
SwitchRowInternal(
|
||||
label: appText('账号本地模式', 'Account local mode'),
|
||||
value: settings.accountLocalMode,
|
||||
onChanged: (value) => saveSettingsInternal(
|
||||
controller,
|
||||
settings.copyWith(accountLocalMode: value),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@ -407,6 +407,27 @@ XWorkmate Privacy Policy
|
||||
);
|
||||
}
|
||||
|
||||
void syncAcpBridgeServerModeDraftControllersInternal(
|
||||
SettingsSnapshot settings,
|
||||
) {
|
||||
final selfHosted = settings.acpBridgeServerModeConfig.selfHosted;
|
||||
syncDraftControllerValueInternal(
|
||||
acpBridgeServerUrlControllerInternal,
|
||||
selfHosted.serverUrl,
|
||||
syncedValue: acpBridgeServerUrlSyncedValueInternal,
|
||||
onSyncedValueChanged: (value) =>
|
||||
acpBridgeServerUrlSyncedValueInternal = value,
|
||||
);
|
||||
syncDraftControllerValueInternal(
|
||||
acpBridgeServerUsernameControllerInternal,
|
||||
selfHosted.username,
|
||||
syncedValue: acpBridgeServerUsernameSyncedValueInternal,
|
||||
onSyncedValueChanged: (value) =>
|
||||
acpBridgeServerUsernameSyncedValueInternal = value,
|
||||
);
|
||||
acpBridgeServerPasswordRefSyncedValueInternal = selfHosted.passwordRef;
|
||||
}
|
||||
|
||||
void disposeRemovedExternalAcpDraftsInternal(
|
||||
Map<String, TextEditingController> controllers,
|
||||
Set<String> activeKeys,
|
||||
|
||||
@ -25,7 +25,7 @@ extension SettingsControllerAccountExtension on SettingsController {
|
||||
if (local.isNotEmpty) {
|
||||
return local;
|
||||
}
|
||||
if (snapshotInternal.accountLocalMode) {
|
||||
if (!snapshotInternal.acpBridgeServerModeConfig.usesCloudSyncBase) {
|
||||
return '';
|
||||
}
|
||||
return accountSyncStateInternal?.syncedDefaults.apisixUrl.trim() ?? '';
|
||||
|
||||
@ -268,6 +268,35 @@ Future<AccountSyncResult> syncAccountSettingsInternal(
|
||||
);
|
||||
await controller.storeInternal.saveAccountSyncState(nextState);
|
||||
await controller.storeInternal.clearAccountProfile();
|
||||
final currentSettings = controller.snapshotInternal;
|
||||
final currentModeConfig = currentSettings.acpBridgeServerModeConfig;
|
||||
final nextModeConfig = currentModeConfig.copyWith(
|
||||
cloudSynced: currentModeConfig.cloudSynced.copyWith(
|
||||
accountBaseUrl: normalizedBaseUrl,
|
||||
accountIdentifier: currentSettings.accountUsername.trim().isNotEmpty
|
||||
? currentSettings.accountUsername.trim()
|
||||
: controller.accountSessionInternal?.email.trim() ?? '',
|
||||
lastSyncAt: nextState.lastSyncAtMs,
|
||||
remoteServerSummary: currentModeConfig.cloudSynced.remoteServerSummary
|
||||
.copyWith(
|
||||
endpoint: response.profile.openclawUrl.trim().isNotEmpty
|
||||
? response.profile.openclawUrl.trim()
|
||||
: response.profile.apisixUrl.trim(),
|
||||
hasAdvancedOverrides:
|
||||
currentModeConfig.mode == AcpBridgeServerMode.advancedCustom,
|
||||
),
|
||||
),
|
||||
);
|
||||
if (nextModeConfig.toJson().toString() !=
|
||||
currentModeConfig.toJson().toString()) {
|
||||
await controller.saveSnapshot(
|
||||
currentSettings.copyWith(
|
||||
accountLocalMode: false,
|
||||
acpBridgeServerModeConfig: nextModeConfig,
|
||||
),
|
||||
recordAccountOverrides: false,
|
||||
);
|
||||
}
|
||||
await applyAccountSyncedDefaultsSettingsInternal(
|
||||
controller,
|
||||
state: nextState,
|
||||
@ -400,6 +429,25 @@ Future<void> applyAccountSyncedDefaultsSettingsInternal(
|
||||
if (next.accountLocalMode) {
|
||||
next = next.copyWith(accountLocalMode: false);
|
||||
}
|
||||
next = next.copyWith(
|
||||
acpBridgeServerModeConfig: next.acpBridgeServerModeConfig.copyWith(
|
||||
cloudSynced: next.acpBridgeServerModeConfig.cloudSynced.copyWith(
|
||||
accountBaseUrl: next.accountBaseUrl,
|
||||
accountIdentifier: next.accountUsername,
|
||||
lastSyncAt: state.lastSyncAtMs,
|
||||
remoteServerSummary:
|
||||
next.acpBridgeServerModeConfig.cloudSynced.remoteServerSummary
|
||||
.copyWith(
|
||||
endpoint: defaults.openclawUrl.trim().isNotEmpty
|
||||
? defaults.openclawUrl.trim()
|
||||
: defaults.apisixUrl.trim(),
|
||||
hasAdvancedOverrides:
|
||||
next.acpBridgeServerModeConfig.mode ==
|
||||
AcpBridgeServerMode.advancedCustom,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if (next.toJsonString() != previous.toJsonString()) {
|
||||
await controller.saveSnapshot(next, recordAccountOverrides: false);
|
||||
@ -424,7 +472,17 @@ Future<void> logoutAccountSettingsInternal(
|
||||
await controller.storeInternal.clearAccountSessionSummary();
|
||||
if (!controller.snapshotInternal.accountLocalMode) {
|
||||
await controller.saveSnapshot(
|
||||
controller.snapshotInternal.copyWith(accountLocalMode: true),
|
||||
controller.snapshotInternal.copyWith(
|
||||
accountLocalMode: true,
|
||||
acpBridgeServerModeConfig:
|
||||
controller.snapshotInternal.acpBridgeServerModeConfig.copyWith(
|
||||
cloudSynced: controller
|
||||
.snapshotInternal
|
||||
.acpBridgeServerModeConfig
|
||||
.cloudSynced
|
||||
.copyWith(accountIdentifier: ''),
|
||||
),
|
||||
),
|
||||
recordAccountOverrides: false,
|
||||
);
|
||||
} else {
|
||||
|
||||
@ -1,3 +1,6 @@
|
||||
import 'runtime_models_configs.dart';
|
||||
import 'runtime_models_profiles.dart';
|
||||
|
||||
class AccountSessionSummary {
|
||||
const AccountSessionSummary({
|
||||
required this.userId,
|
||||
@ -255,6 +258,346 @@ class AccountRemoteProfile {
|
||||
}
|
||||
}
|
||||
|
||||
enum AcpBridgeServerMode { cloudSynced, selfHosted, advancedCustom }
|
||||
|
||||
class AcpBridgeServerRemoteServerSummary {
|
||||
const AcpBridgeServerRemoteServerSummary({
|
||||
required this.endpoint,
|
||||
required this.hasAdvancedOverrides,
|
||||
});
|
||||
|
||||
final String endpoint;
|
||||
final bool hasAdvancedOverrides;
|
||||
|
||||
factory AcpBridgeServerRemoteServerSummary.defaults() {
|
||||
return const AcpBridgeServerRemoteServerSummary(
|
||||
endpoint: '',
|
||||
hasAdvancedOverrides: false,
|
||||
);
|
||||
}
|
||||
|
||||
AcpBridgeServerRemoteServerSummary copyWith({
|
||||
String? endpoint,
|
||||
bool? hasAdvancedOverrides,
|
||||
}) {
|
||||
return AcpBridgeServerRemoteServerSummary(
|
||||
endpoint: endpoint ?? this.endpoint,
|
||||
hasAdvancedOverrides: hasAdvancedOverrides ?? this.hasAdvancedOverrides,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return <String, dynamic>{
|
||||
'endpoint': endpoint,
|
||||
'hasAdvancedOverrides': hasAdvancedOverrides,
|
||||
};
|
||||
}
|
||||
|
||||
factory AcpBridgeServerRemoteServerSummary.fromJson(
|
||||
Map<String, dynamic> json,
|
||||
) {
|
||||
return AcpBridgeServerRemoteServerSummary(
|
||||
endpoint: json['endpoint'] as String? ?? '',
|
||||
hasAdvancedOverrides: json['hasAdvancedOverrides'] as bool? ?? false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AcpBridgeServerCloudSyncConfig {
|
||||
const AcpBridgeServerCloudSyncConfig({
|
||||
required this.accountBaseUrl,
|
||||
required this.accountIdentifier,
|
||||
required this.lastSyncAt,
|
||||
required this.remoteServerSummary,
|
||||
});
|
||||
|
||||
final String accountBaseUrl;
|
||||
final String accountIdentifier;
|
||||
final int lastSyncAt;
|
||||
final AcpBridgeServerRemoteServerSummary remoteServerSummary;
|
||||
|
||||
factory AcpBridgeServerCloudSyncConfig.defaults() {
|
||||
return AcpBridgeServerCloudSyncConfig(
|
||||
accountBaseUrl: '',
|
||||
accountIdentifier: '',
|
||||
lastSyncAt: 0,
|
||||
remoteServerSummary: AcpBridgeServerRemoteServerSummary.defaults(),
|
||||
);
|
||||
}
|
||||
|
||||
AcpBridgeServerCloudSyncConfig copyWith({
|
||||
String? accountBaseUrl,
|
||||
String? accountIdentifier,
|
||||
int? lastSyncAt,
|
||||
AcpBridgeServerRemoteServerSummary? remoteServerSummary,
|
||||
}) {
|
||||
return AcpBridgeServerCloudSyncConfig(
|
||||
accountBaseUrl: accountBaseUrl ?? this.accountBaseUrl,
|
||||
accountIdentifier: accountIdentifier ?? this.accountIdentifier,
|
||||
lastSyncAt: lastSyncAt ?? this.lastSyncAt,
|
||||
remoteServerSummary: remoteServerSummary ?? this.remoteServerSummary,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return <String, dynamic>{
|
||||
'accountBaseUrl': accountBaseUrl,
|
||||
'accountIdentifier': accountIdentifier,
|
||||
'lastSyncAt': lastSyncAt,
|
||||
'remoteServerSummary': remoteServerSummary.toJson(),
|
||||
};
|
||||
}
|
||||
|
||||
factory AcpBridgeServerCloudSyncConfig.fromJson(Map<String, dynamic> json) {
|
||||
return AcpBridgeServerCloudSyncConfig(
|
||||
accountBaseUrl: json['accountBaseUrl'] as String? ?? '',
|
||||
accountIdentifier: json['accountIdentifier'] as String? ?? '',
|
||||
lastSyncAt: (json['lastSyncAt'] as num?)?.toInt() ?? 0,
|
||||
remoteServerSummary: AcpBridgeServerRemoteServerSummary.fromJson(
|
||||
(json['remoteServerSummary'] as Map?)?.cast<String, dynamic>() ??
|
||||
const <String, dynamic>{},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AcpBridgeServerSelfHostedConfig {
|
||||
const AcpBridgeServerSelfHostedConfig({
|
||||
required this.serverUrl,
|
||||
required this.username,
|
||||
required this.passwordRef,
|
||||
});
|
||||
|
||||
final String serverUrl;
|
||||
final String username;
|
||||
final String passwordRef;
|
||||
|
||||
factory AcpBridgeServerSelfHostedConfig.defaults() {
|
||||
return const AcpBridgeServerSelfHostedConfig(
|
||||
serverUrl: '',
|
||||
username: '',
|
||||
passwordRef: 'acp_bridge_server_password',
|
||||
);
|
||||
}
|
||||
|
||||
AcpBridgeServerSelfHostedConfig copyWith({
|
||||
String? serverUrl,
|
||||
String? username,
|
||||
String? passwordRef,
|
||||
}) {
|
||||
return AcpBridgeServerSelfHostedConfig(
|
||||
serverUrl: (serverUrl ?? this.serverUrl).trim(),
|
||||
username: (username ?? this.username).trim(),
|
||||
passwordRef: (passwordRef ?? this.passwordRef).trim(),
|
||||
);
|
||||
}
|
||||
|
||||
bool get isConfigured =>
|
||||
serverUrl.trim().isNotEmpty && username.trim().isNotEmpty;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return <String, dynamic>{
|
||||
'serverUrl': serverUrl,
|
||||
'username': username,
|
||||
'passwordRef': passwordRef,
|
||||
};
|
||||
}
|
||||
|
||||
factory AcpBridgeServerSelfHostedConfig.fromJson(Map<String, dynamic> json) {
|
||||
return AcpBridgeServerSelfHostedConfig(
|
||||
serverUrl: json['serverUrl'] as String? ?? '',
|
||||
username: json['username'] as String? ?? '',
|
||||
passwordRef:
|
||||
json['passwordRef'] as String? ??
|
||||
AcpBridgeServerSelfHostedConfig.defaults().passwordRef,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AcpBridgeServerAdvancedOverrides {
|
||||
const AcpBridgeServerAdvancedOverrides({
|
||||
required this.gatewayProfiles,
|
||||
required this.vault,
|
||||
required this.aiGateway,
|
||||
required this.acpBridgeServerProfiles,
|
||||
required this.authorizedSkillDirectories,
|
||||
});
|
||||
|
||||
final List<GatewayConnectionProfile> gatewayProfiles;
|
||||
final VaultConfig vault;
|
||||
final AiGatewayProfile aiGateway;
|
||||
final List<ExternalAcpEndpointProfile> acpBridgeServerProfiles;
|
||||
final List<AuthorizedSkillDirectory> authorizedSkillDirectories;
|
||||
|
||||
factory AcpBridgeServerAdvancedOverrides.defaults() {
|
||||
return AcpBridgeServerAdvancedOverrides(
|
||||
gatewayProfiles: normalizeGatewayProfiles(),
|
||||
vault: VaultConfig.defaults(),
|
||||
aiGateway: AiGatewayProfile.defaults(),
|
||||
acpBridgeServerProfiles: normalizeExternalAcpEndpoints(),
|
||||
authorizedSkillDirectories: normalizeAuthorizedSkillDirectories(),
|
||||
);
|
||||
}
|
||||
|
||||
AcpBridgeServerAdvancedOverrides copyWith({
|
||||
List<GatewayConnectionProfile>? gatewayProfiles,
|
||||
VaultConfig? vault,
|
||||
AiGatewayProfile? aiGateway,
|
||||
List<ExternalAcpEndpointProfile>? acpBridgeServerProfiles,
|
||||
List<AuthorizedSkillDirectory>? authorizedSkillDirectories,
|
||||
}) {
|
||||
return AcpBridgeServerAdvancedOverrides(
|
||||
gatewayProfiles: gatewayProfiles != null
|
||||
? normalizeGatewayProfiles(profiles: gatewayProfiles)
|
||||
: this.gatewayProfiles,
|
||||
vault: vault ?? this.vault,
|
||||
aiGateway: aiGateway ?? this.aiGateway,
|
||||
acpBridgeServerProfiles: acpBridgeServerProfiles != null
|
||||
? normalizeExternalAcpEndpoints(profiles: acpBridgeServerProfiles)
|
||||
: this.acpBridgeServerProfiles,
|
||||
authorizedSkillDirectories: authorizedSkillDirectories != null
|
||||
? normalizeAuthorizedSkillDirectories(
|
||||
directories: authorizedSkillDirectories,
|
||||
)
|
||||
: this.authorizedSkillDirectories,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return <String, dynamic>{
|
||||
'gatewayProfiles': gatewayProfiles
|
||||
.map((item) => item.toJson())
|
||||
.toList(growable: false),
|
||||
'vault': vault.toJson(),
|
||||
'aiGateway': aiGateway.toJson(),
|
||||
'acpBridgeServerProfiles': acpBridgeServerProfiles
|
||||
.map((item) => item.toJson())
|
||||
.toList(growable: false),
|
||||
'authorizedSkillDirectories': authorizedSkillDirectories
|
||||
.map((item) => item.toJson())
|
||||
.toList(growable: false),
|
||||
};
|
||||
}
|
||||
|
||||
factory AcpBridgeServerAdvancedOverrides.fromJson(
|
||||
Map<String, dynamic> json,
|
||||
) {
|
||||
return AcpBridgeServerAdvancedOverrides(
|
||||
gatewayProfiles: normalizeGatewayProfiles(
|
||||
profiles: ((json['gatewayProfiles'] as List?) ?? const <Object>[])
|
||||
.whereType<Map>()
|
||||
.map(
|
||||
(item) => GatewayConnectionProfile.fromJson(
|
||||
item.cast<String, dynamic>(),
|
||||
),
|
||||
),
|
||||
),
|
||||
vault: VaultConfig.fromJson(
|
||||
(json['vault'] as Map?)?.cast<String, dynamic>() ?? const {},
|
||||
),
|
||||
aiGateway: AiGatewayProfile.fromJson(
|
||||
(json['aiGateway'] as Map?)?.cast<String, dynamic>() ?? const {},
|
||||
),
|
||||
acpBridgeServerProfiles: normalizeExternalAcpEndpoints(
|
||||
profiles:
|
||||
((json['acpBridgeServerProfiles'] as List?) ?? const <Object>[])
|
||||
.whereType<Map>()
|
||||
.map(
|
||||
(item) => ExternalAcpEndpointProfile.fromJson(
|
||||
item.cast<String, dynamic>(),
|
||||
),
|
||||
),
|
||||
),
|
||||
authorizedSkillDirectories: normalizeAuthorizedSkillDirectories(
|
||||
directories:
|
||||
((json['authorizedSkillDirectories'] as List?) ?? const <Object>[])
|
||||
.whereType<Map>()
|
||||
.map(
|
||||
(item) => AuthorizedSkillDirectory.fromJson(
|
||||
item.cast<String, dynamic>(),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AcpBridgeServerModeConfig {
|
||||
const AcpBridgeServerModeConfig({
|
||||
required this.mode,
|
||||
required this.cloudSynced,
|
||||
required this.selfHosted,
|
||||
required this.advancedOverrides,
|
||||
});
|
||||
|
||||
final AcpBridgeServerMode mode;
|
||||
final AcpBridgeServerCloudSyncConfig cloudSynced;
|
||||
final AcpBridgeServerSelfHostedConfig selfHosted;
|
||||
final AcpBridgeServerAdvancedOverrides advancedOverrides;
|
||||
|
||||
factory AcpBridgeServerModeConfig.defaults() {
|
||||
return AcpBridgeServerModeConfig(
|
||||
mode: AcpBridgeServerMode.cloudSynced,
|
||||
cloudSynced: AcpBridgeServerCloudSyncConfig.defaults(),
|
||||
selfHosted: AcpBridgeServerSelfHostedConfig.defaults(),
|
||||
advancedOverrides: AcpBridgeServerAdvancedOverrides.defaults(),
|
||||
);
|
||||
}
|
||||
|
||||
AcpBridgeServerModeConfig copyWith({
|
||||
AcpBridgeServerMode? mode,
|
||||
AcpBridgeServerCloudSyncConfig? cloudSynced,
|
||||
AcpBridgeServerSelfHostedConfig? selfHosted,
|
||||
AcpBridgeServerAdvancedOverrides? advancedOverrides,
|
||||
}) {
|
||||
return AcpBridgeServerModeConfig(
|
||||
mode: mode ?? this.mode,
|
||||
cloudSynced: cloudSynced ?? this.cloudSynced,
|
||||
selfHosted: selfHosted ?? this.selfHosted,
|
||||
advancedOverrides: advancedOverrides ?? this.advancedOverrides,
|
||||
);
|
||||
}
|
||||
|
||||
bool get usesSelfHostedBase =>
|
||||
mode == AcpBridgeServerMode.selfHosted ||
|
||||
(mode == AcpBridgeServerMode.advancedCustom && selfHosted.isConfigured);
|
||||
|
||||
bool get usesCloudSyncBase => !usesSelfHostedBase;
|
||||
|
||||
String get sourceTag => switch (mode) {
|
||||
AcpBridgeServerMode.cloudSynced => 'cloudSynced',
|
||||
AcpBridgeServerMode.selfHosted => 'selfHosted',
|
||||
AcpBridgeServerMode.advancedCustom => 'advancedOverride',
|
||||
};
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return <String, dynamic>{
|
||||
'mode': mode.name,
|
||||
'cloudSynced': cloudSynced.toJson(),
|
||||
'selfHosted': selfHosted.toJson(),
|
||||
'advancedOverrides': advancedOverrides.toJson(),
|
||||
};
|
||||
}
|
||||
|
||||
factory AcpBridgeServerModeConfig.fromJson(Map<String, dynamic> json) {
|
||||
return AcpBridgeServerModeConfig(
|
||||
mode: AcpBridgeServerMode.values.firstWhere(
|
||||
(item) => item.name == json['mode'],
|
||||
orElse: () => AcpBridgeServerMode.cloudSynced,
|
||||
),
|
||||
cloudSynced: AcpBridgeServerCloudSyncConfig.fromJson(
|
||||
(json['cloudSynced'] as Map?)?.cast<String, dynamic>() ?? const {},
|
||||
),
|
||||
selfHosted: AcpBridgeServerSelfHostedConfig.fromJson(
|
||||
(json['selfHosted'] as Map?)?.cast<String, dynamic>() ?? const {},
|
||||
),
|
||||
advancedOverrides: AcpBridgeServerAdvancedOverrides.fromJson(
|
||||
(json['advancedOverrides'] as Map?)?.cast<String, dynamic>() ?? const {},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AccountProfileResponse {
|
||||
const AccountProfileResponse({
|
||||
required this.profile,
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
import 'dart:convert';
|
||||
import '../i18n/app_language.dart';
|
||||
import '../models/app_models.dart';
|
||||
import 'runtime_models_account.dart';
|
||||
import 'runtime_models_connection.dart';
|
||||
import 'runtime_models_profiles.dart';
|
||||
import 'runtime_models_configs.dart';
|
||||
@ -40,6 +41,7 @@ class SettingsSnapshot {
|
||||
required this.accountWorkspace,
|
||||
required this.accountWorkspaceFollowed,
|
||||
required this.accountLocalMode,
|
||||
required this.acpBridgeServerModeConfig,
|
||||
required this.linuxDesktop,
|
||||
required this.assistantExecutionTarget,
|
||||
required this.assistantPermissionLevel,
|
||||
@ -78,6 +80,7 @@ class SettingsSnapshot {
|
||||
final String accountWorkspace;
|
||||
final bool accountWorkspaceFollowed;
|
||||
final bool accountLocalMode;
|
||||
final AcpBridgeServerModeConfig acpBridgeServerModeConfig;
|
||||
final LinuxDesktopConfig linuxDesktop;
|
||||
final AssistantExecutionTarget assistantExecutionTarget;
|
||||
final AssistantPermissionLevel assistantPermissionLevel;
|
||||
@ -117,6 +120,7 @@ class SettingsSnapshot {
|
||||
accountWorkspace: 'Default Workspace',
|
||||
accountWorkspaceFollowed: false,
|
||||
accountLocalMode: true,
|
||||
acpBridgeServerModeConfig: AcpBridgeServerModeConfig.defaults(),
|
||||
linuxDesktop: LinuxDesktopConfig.defaults(),
|
||||
assistantExecutionTarget: AssistantExecutionTarget.singleAgent,
|
||||
assistantPermissionLevel: AssistantPermissionLevel.defaultAccess,
|
||||
@ -157,6 +161,7 @@ class SettingsSnapshot {
|
||||
String? accountWorkspace,
|
||||
bool? accountWorkspaceFollowed,
|
||||
bool? accountLocalMode,
|
||||
AcpBridgeServerModeConfig? acpBridgeServerModeConfig,
|
||||
LinuxDesktopConfig? linuxDesktop,
|
||||
AssistantExecutionTarget? assistantExecutionTarget,
|
||||
AssistantPermissionLevel? assistantPermissionLevel,
|
||||
@ -209,6 +214,8 @@ class SettingsSnapshot {
|
||||
accountWorkspaceFollowed:
|
||||
accountWorkspaceFollowed ?? this.accountWorkspaceFollowed,
|
||||
accountLocalMode: accountLocalMode ?? this.accountLocalMode,
|
||||
acpBridgeServerModeConfig:
|
||||
acpBridgeServerModeConfig ?? this.acpBridgeServerModeConfig,
|
||||
linuxDesktop: linuxDesktop ?? this.linuxDesktop,
|
||||
assistantExecutionTarget:
|
||||
assistantExecutionTarget ?? this.assistantExecutionTarget,
|
||||
@ -265,6 +272,7 @@ class SettingsSnapshot {
|
||||
'accountWorkspace': accountWorkspace,
|
||||
'accountWorkspaceFollowed': accountWorkspaceFollowed,
|
||||
'accountLocalMode': accountLocalMode,
|
||||
'acpBridgeServerModeConfig': acpBridgeServerModeConfig.toJson(),
|
||||
'linuxDesktop': linuxDesktop.toJson(),
|
||||
'assistantExecutionTarget': assistantExecutionTarget.name,
|
||||
'assistantPermissionLevel': assistantPermissionLevel.name,
|
||||
@ -424,6 +432,10 @@ class SettingsSnapshot {
|
||||
accountWorkspaceFollowed:
|
||||
json['accountWorkspaceFollowed'] as bool? ?? false,
|
||||
accountLocalMode: json['accountLocalMode'] as bool? ?? true,
|
||||
acpBridgeServerModeConfig: AcpBridgeServerModeConfig.fromJson(
|
||||
(json['acpBridgeServerModeConfig'] as Map?)?.cast<String, dynamic>() ??
|
||||
const {},
|
||||
),
|
||||
linuxDesktop: LinuxDesktopConfig.fromJson(
|
||||
(json['linuxDesktop'] as Map?)?.cast<String, dynamic>() ?? const {},
|
||||
),
|
||||
@ -653,6 +665,20 @@ class SettingsSnapshot {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
SettingsSnapshot captureAcpBridgeServerAdvancedOverrides() {
|
||||
return copyWith(
|
||||
acpBridgeServerModeConfig: acpBridgeServerModeConfig.copyWith(
|
||||
advancedOverrides: AcpBridgeServerAdvancedOverrides(
|
||||
gatewayProfiles: gatewayProfiles,
|
||||
vault: vault,
|
||||
aiGateway: aiGateway,
|
||||
acpBridgeServerProfiles: externalAcpEndpoints,
|
||||
authorizedSkillDirectories: authorizedSkillDirectories,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
List<String> normalizeSavedGatewayTargets(Iterable<String> rawTargets) {
|
||||
|
||||
@ -27,6 +27,10 @@ void main() {
|
||||
expect(find.byKey(const ValueKey('account-password-field')), findsOneWidget);
|
||||
expect(find.widgetWithText(FilledButton, '登录'), findsOneWidget);
|
||||
expect(find.text('保存本地入口'), findsNothing);
|
||||
expect(
|
||||
find.byKey(const ValueKey('account-open-settings-acp')),
|
||||
findsNothing,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('AccountPage logs in and shows remote sync status inline', (
|
||||
@ -76,6 +80,11 @@ void main() {
|
||||
expect(find.byKey(const ValueKey('account-login-button')), findsNothing);
|
||||
expect(find.byKey(const ValueKey('account-sync-button')), findsOneWidget);
|
||||
expect(find.byKey(const ValueKey('account-logout-button')), findsOneWidget);
|
||||
expect(find.byKey(const ValueKey('account-open-settings-acp')), findsOneWidget);
|
||||
expect(
|
||||
find.byKey(const ValueKey('account-acp-sync-summary-endpoint')),
|
||||
findsOneWidget,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('AccountPage completes MFA verification and can log out', (
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
@ -21,7 +20,7 @@ Future<void> _waitForText(
|
||||
final deadline = DateTime.now().add(timeout);
|
||||
while (finder.evaluate().isEmpty) {
|
||||
if (DateTime.now().isAfter(deadline)) {
|
||||
fail('Timed out waiting for ${finder.description}');
|
||||
fail('Timed out waiting for expected widget.');
|
||||
}
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
}
|
||||
|
||||
@ -456,6 +456,7 @@ SettingsSnapshot buildAssistantPageTestSettingsSnapshotInternal(
|
||||
accountWorkspace: defaults.accountWorkspace,
|
||||
accountWorkspaceFollowed: defaults.accountWorkspaceFollowed,
|
||||
accountLocalMode: defaults.accountLocalMode,
|
||||
acpBridgeServerModeConfig: defaults.acpBridgeServerModeConfig,
|
||||
linuxDesktop: defaults.linuxDesktop,
|
||||
assistantExecutionTarget: assistantExecutionTarget,
|
||||
assistantPermissionLevel: defaults.assistantPermissionLevel,
|
||||
|
||||
37
test/features/settings_page_acp_bridge_mode_suite.dart
Normal file
37
test/features/settings_page_acp_bridge_mode_suite.dart
Normal file
@ -0,0 +1,37 @@
|
||||
@TestOn('vm')
|
||||
library;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:xworkmate/features/settings/settings_page_core.dart';
|
||||
import 'package:xworkmate/models/app_models.dart';
|
||||
|
||||
import '../test_support.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('SettingsPage shows ACP bridge server mode card on integrations', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
final controller = await createTestController(tester);
|
||||
controller.openSettings(tab: SettingsTab.gateway);
|
||||
|
||||
await pumpPage(
|
||||
tester,
|
||||
child: SettingsPage(
|
||||
controller: controller,
|
||||
initialTab: SettingsTab.gateway,
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.text('XWorkmate ACP Bridge Server'), findsOneWidget);
|
||||
expect(find.byKey(const ValueKey('acp-bridge-mode-cloud')), findsOneWidget);
|
||||
expect(
|
||||
find.byKey(const ValueKey('acp-bridge-mode-self-hosted')),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(
|
||||
find.byKey(const ValueKey('acp-bridge-mode-advanced')),
|
||||
findsOneWidget,
|
||||
);
|
||||
});
|
||||
}
|
||||
@ -19,7 +19,7 @@ Future<void> _waitForText(
|
||||
final deadline = DateTime.now().add(timeout);
|
||||
while (finder.evaluate().isEmpty) {
|
||||
if (DateTime.now().isAfter(deadline)) {
|
||||
fail('Timed out waiting for ${finder.description}');
|
||||
fail('Timed out waiting for expected widget.');
|
||||
}
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
}
|
||||
|
||||
77
test/runtime/acp_bridge_server_mode_config_suite.dart
Normal file
77
test/runtime/acp_bridge_server_mode_config_suite.dart
Normal file
@ -0,0 +1,77 @@
|
||||
@TestOn('vm')
|
||||
library;
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:xworkmate/runtime/runtime_models.dart';
|
||||
|
||||
void main() {
|
||||
test('AcpBridgeServerModeConfig defaults to cloud synced mode', () {
|
||||
final config = AcpBridgeServerModeConfig.defaults();
|
||||
|
||||
expect(config.mode, AcpBridgeServerMode.cloudSynced);
|
||||
expect(config.usesCloudSyncBase, isTrue);
|
||||
expect(config.usesSelfHostedBase, isFalse);
|
||||
expect(config.selfHosted.passwordRef, 'acp_bridge_server_password');
|
||||
});
|
||||
|
||||
test('advanced custom mode can inherit self hosted base when configured', () {
|
||||
final config = AcpBridgeServerModeConfig.defaults().copyWith(
|
||||
mode: AcpBridgeServerMode.advancedCustom,
|
||||
selfHosted: AcpBridgeServerSelfHostedConfig.defaults().copyWith(
|
||||
serverUrl: 'https://bridge.example.com',
|
||||
username: 'review',
|
||||
),
|
||||
);
|
||||
|
||||
expect(config.usesSelfHostedBase, isTrue);
|
||||
expect(config.usesCloudSyncBase, isFalse);
|
||||
expect(config.sourceTag, 'advancedOverride');
|
||||
});
|
||||
|
||||
test('SettingsSnapshot captures current advanced overrides into mode config', () {
|
||||
final snapshot = SettingsSnapshot.defaults().copyWith(
|
||||
gatewayProfiles: SettingsSnapshot.defaults().gatewayProfiles,
|
||||
vault: VaultConfig.defaults().copyWith(address: 'https://vault.example'),
|
||||
aiGateway: AiGatewayProfile.defaults().copyWith(
|
||||
baseUrl: 'https://llm.example.com/v1',
|
||||
),
|
||||
externalAcpEndpoints: <ExternalAcpEndpointProfile>[
|
||||
ExternalAcpEndpointProfile.defaultsForProvider(
|
||||
SingleAgentProvider.codex,
|
||||
).copyWith(endpoint: 'https://agent.example.com'),
|
||||
],
|
||||
authorizedSkillDirectories: const <AuthorizedSkillDirectory>[
|
||||
AuthorizedSkillDirectory(path: '/tmp/skills'),
|
||||
],
|
||||
);
|
||||
|
||||
final captured = snapshot.captureAcpBridgeServerAdvancedOverrides();
|
||||
|
||||
expect(
|
||||
captured.acpBridgeServerModeConfig.advancedOverrides.vault.address,
|
||||
'https://vault.example',
|
||||
);
|
||||
expect(
|
||||
captured.acpBridgeServerModeConfig.advancedOverrides.aiGateway.baseUrl,
|
||||
'https://llm.example.com/v1',
|
||||
);
|
||||
expect(
|
||||
captured
|
||||
.acpBridgeServerModeConfig
|
||||
.advancedOverrides
|
||||
.acpBridgeServerProfiles
|
||||
.firstWhere((item) => item.providerKey == 'codex')
|
||||
.endpoint,
|
||||
'https://agent.example.com',
|
||||
);
|
||||
expect(
|
||||
captured
|
||||
.acpBridgeServerModeConfig
|
||||
.advancedOverrides
|
||||
.authorizedSkillDirectories
|
||||
.single
|
||||
.path,
|
||||
'/tmp/skills',
|
||||
);
|
||||
});
|
||||
}
|
||||
55
test/runtime/acp_bridge_server_self_hosted_secret_suite.dart
Normal file
55
test/runtime/acp_bridge_server_self_hosted_secret_suite.dart
Normal file
@ -0,0 +1,55 @@
|
||||
@TestOn('vm')
|
||||
library;
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:xworkmate/runtime/runtime_models.dart';
|
||||
import 'package:xworkmate/runtime/secure_config_store.dart';
|
||||
|
||||
void main() {
|
||||
test('self hosted ACP bridge password stays in secure storage, not settings snapshot', () async {
|
||||
SharedPreferences.setMockInitialValues(<String, Object>{});
|
||||
final tempDirectory = await Directory.systemTemp.createTemp(
|
||||
'xworkmate-acp-bridge-self-hosted-',
|
||||
);
|
||||
addTearDown(() async {
|
||||
if (await tempDirectory.exists()) {
|
||||
await tempDirectory.delete(recursive: true);
|
||||
}
|
||||
});
|
||||
final store = SecureConfigStore(
|
||||
enableSecureStorage: false,
|
||||
databasePathResolver: () async => '${tempDirectory.path}/settings.sqlite3',
|
||||
fallbackDirectoryPathResolver: () async => tempDirectory.path,
|
||||
defaultSupportDirectoryPathResolver: () async => tempDirectory.path,
|
||||
);
|
||||
addTearDown(store.dispose);
|
||||
await store.initialize();
|
||||
|
||||
final snapshot = SettingsSnapshot.defaults().copyWith(
|
||||
acpBridgeServerModeConfig: AcpBridgeServerModeConfig.defaults().copyWith(
|
||||
mode: AcpBridgeServerMode.selfHosted,
|
||||
selfHosted: AcpBridgeServerSelfHostedConfig.defaults().copyWith(
|
||||
serverUrl: 'https://bridge.example.com',
|
||||
username: 'review@example.com',
|
||||
),
|
||||
),
|
||||
);
|
||||
await store.saveSettingsSnapshot(snapshot);
|
||||
await store.saveSecretValueByRef('acp_bridge_server_password', 'top-secret');
|
||||
|
||||
final loadedSnapshot = await store.loadSettingsSnapshot();
|
||||
|
||||
expect(
|
||||
loadedSnapshot.acpBridgeServerModeConfig.selfHosted.passwordRef,
|
||||
'acp_bridge_server_password',
|
||||
);
|
||||
expect(loadedSnapshot.toJsonString(), isNot(contains('top-secret')));
|
||||
expect(
|
||||
await store.loadSecretValueByRef('acp_bridge_server_password'),
|
||||
'top-secret',
|
||||
);
|
||||
});
|
||||
}
|
||||
@ -84,6 +84,19 @@ void main() {
|
||||
kAccountManagedSecretTargetOllamaCloudApiKey,
|
||||
);
|
||||
expect(controller.snapshot.accountLocalMode, isFalse);
|
||||
expect(
|
||||
controller.snapshot.acpBridgeServerModeConfig.cloudSynced.accountBaseUrl,
|
||||
server.accountBaseUrl,
|
||||
);
|
||||
expect(
|
||||
controller
|
||||
.snapshot
|
||||
.acpBridgeServerModeConfig
|
||||
.cloudSynced
|
||||
.remoteServerSummary
|
||||
.endpoint,
|
||||
'https://openclaw.account.example',
|
||||
);
|
||||
expect(
|
||||
(await store.loadSettingsSnapshot()).toJsonString(),
|
||||
allOf(
|
||||
@ -262,6 +275,10 @@ void main() {
|
||||
expect(await store.loadAccountSyncState(), isNotNull);
|
||||
expect(controller.snapshot.aiGateway.baseUrl, 'https://local-ai.example.com/v1');
|
||||
expect(controller.snapshot.accountLocalMode, isTrue);
|
||||
expect(
|
||||
controller.snapshot.acpBridgeServerModeConfig.cloudSynced.accountIdentifier,
|
||||
'',
|
||||
);
|
||||
expect(
|
||||
(await store.loadAccountSyncState())
|
||||
?.overrideFlags[kAccountOverrideAiGatewayBaseUrl],
|
||||
|
||||
Loading…
Reference in New Issue
Block a user