Merge account and ACP settings into advanced config
This commit is contained in:
parent
5e5965be63
commit
c3ef4bbb60
@ -1,6 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../features/account/account_page.dart';
|
||||
import '../features/mobile/mobile_shell.dart';
|
||||
import '../i18n/app_language.dart';
|
||||
import '../models/app_models.dart';
|
||||
@ -189,7 +188,7 @@ class _AppShellState extends State<AppShell> {
|
||||
constraints.maxWidth > 1280;
|
||||
final mobileDestination =
|
||||
controller.destination == WorkspaceDestination.account
|
||||
? WorkspaceDestination.assistant
|
||||
? WorkspaceDestination.settings
|
||||
: controller.destination;
|
||||
final availableMobileDestinations = _mobileDestinations
|
||||
.where(controller.capabilities.supportsDestination)
|
||||
@ -220,33 +219,6 @@ class _AppShellState extends State<AppShell> {
|
||||
);
|
||||
}
|
||||
|
||||
void openAccountSheet() {
|
||||
showModalBottomSheet<void>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (sheetContext) {
|
||||
return Container(
|
||||
margin: EdgeInsets.fromLTRB(
|
||||
12,
|
||||
MediaQuery.of(sheetContext).padding.top + 12,
|
||||
12,
|
||||
12,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: palette.surfacePrimary,
|
||||
borderRadius: BorderRadius.circular(28),
|
||||
border: Border.all(color: palette.strokeSoft),
|
||||
),
|
||||
child: SafeArea(
|
||||
top: false,
|
||||
child: AccountPage(controller: controller),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (isCompactMobile) {
|
||||
return MobileShell(controller: controller);
|
||||
}
|
||||
@ -313,21 +285,7 @@ class _AppShellState extends State<AppShell> {
|
||||
),
|
||||
],
|
||||
),
|
||||
Positioned(
|
||||
right: 24,
|
||||
bottom: 96,
|
||||
child:
|
||||
controller.capabilities.supportsDestination(
|
||||
WorkspaceDestination.account,
|
||||
)
|
||||
? FloatingActionButton.small(
|
||||
onPressed: openAccountSheet,
|
||||
child: const Icon(
|
||||
Icons.account_circle_rounded,
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
const SizedBox.shrink(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../features/account/account_page.dart';
|
||||
import '../features/assistant/assistant_page.dart';
|
||||
import '../features/claw_hub/claw_hub_page.dart';
|
||||
import '../features/mcp_server/mcp_server_page.dart';
|
||||
@ -121,22 +120,22 @@ workspacePageSpecsInternal = <WorkspaceDestination, WorkspacePageSpec>{
|
||||
initialTab: controller.settingsTab,
|
||||
initialDetail: controller.settingsDetail,
|
||||
navigationContext: controller.settingsNavigationContext,
|
||||
showSectionTabs: false,
|
||||
showSectionTabs: true,
|
||||
),
|
||||
mobileBuilder: (controller, onOpenDetail) => SettingsPage(
|
||||
controller: controller,
|
||||
initialTab: controller.settingsTab,
|
||||
initialDetail: controller.settingsDetail,
|
||||
navigationContext: controller.settingsNavigationContext,
|
||||
showSectionTabs: false,
|
||||
showSectionTabs: true,
|
||||
),
|
||||
),
|
||||
WorkspaceDestination.account: WorkspacePageSpec(
|
||||
destination: WorkspaceDestination.account,
|
||||
desktopBuilder: (controller, onOpenDetail) =>
|
||||
AccountPage(controller: controller),
|
||||
SettingsPage(controller: controller, initialTab: SettingsTab.gateway),
|
||||
mobileBuilder: (controller, onOpenDetail) =>
|
||||
AccountPage(controller: controller),
|
||||
SettingsPage(controller: controller, initialTab: SettingsTab.gateway),
|
||||
),
|
||||
};
|
||||
|
||||
|
||||
@ -101,10 +101,10 @@ class MobileShellStateInternal extends State<MobileShell> {
|
||||
WorkspaceDestination.agents ||
|
||||
WorkspaceDestination.mcpServer ||
|
||||
WorkspaceDestination.clawHub ||
|
||||
WorkspaceDestination.aiGateway ||
|
||||
WorkspaceDestination.account => MobileShellTab.workspace,
|
||||
WorkspaceDestination.aiGateway => MobileShellTab.workspace,
|
||||
WorkspaceDestination.secrets => MobileShellTab.secrets,
|
||||
WorkspaceDestination.settings => MobileShellTab.settings,
|
||||
WorkspaceDestination.account => MobileShellTab.settings,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -72,15 +72,6 @@ class MobileWorkspaceLauncherInternal extends StatelessWidget {
|
||||
iconColor: palette.accent,
|
||||
iconBackground: palette.accentMuted,
|
||||
),
|
||||
WorkspaceEntryInternal(
|
||||
destination: WorkspaceDestination.account,
|
||||
subtitle: appText(
|
||||
'身份、工作区与会话',
|
||||
'Identity, workspace and sessions',
|
||||
),
|
||||
iconColor: palette.success,
|
||||
iconBackground: palette.success.withValues(alpha: 0.12),
|
||||
),
|
||||
]
|
||||
.where(
|
||||
(entry) =>
|
||||
|
||||
@ -60,6 +60,10 @@ class SettingsPageStateInternal extends State<SettingsPage> {
|
||||
late final TextEditingController aiGatewayApiKeyRefControllerInternal;
|
||||
late final TextEditingController aiGatewayApiKeyControllerInternal;
|
||||
late final TextEditingController aiGatewayModelSearchControllerInternal;
|
||||
late final TextEditingController accountBaseUrlControllerInternal;
|
||||
late final TextEditingController accountUsernameControllerInternal;
|
||||
late final TextEditingController accountPasswordControllerInternal;
|
||||
late final TextEditingController accountMfaCodeControllerInternal;
|
||||
late final TextEditingController gatewaySetupCodeControllerInternal;
|
||||
late final TextEditingController gatewayHostControllerInternal;
|
||||
late final TextEditingController gatewayPortControllerInternal;
|
||||
@ -73,6 +77,8 @@ class SettingsPageStateInternal extends State<SettingsPage> {
|
||||
late final TextEditingController acpBridgeServerUrlControllerInternal;
|
||||
late final TextEditingController acpBridgeServerUsernameControllerInternal;
|
||||
late final TextEditingController acpBridgeServerPasswordControllerInternal;
|
||||
String accountBaseUrlSyncedValueInternal = '';
|
||||
String accountUsernameSyncedValueInternal = '';
|
||||
late final Map<String, TextEditingController>
|
||||
externalAcpLabelControllersInternal;
|
||||
late final Map<String, TextEditingController>
|
||||
@ -135,6 +141,10 @@ class SettingsPageStateInternal extends State<SettingsPage> {
|
||||
aiGatewayApiKeyRefControllerInternal = TextEditingController();
|
||||
aiGatewayApiKeyControllerInternal = TextEditingController();
|
||||
aiGatewayModelSearchControllerInternal = TextEditingController();
|
||||
accountBaseUrlControllerInternal = TextEditingController();
|
||||
accountUsernameControllerInternal = TextEditingController();
|
||||
accountPasswordControllerInternal = TextEditingController();
|
||||
accountMfaCodeControllerInternal = TextEditingController();
|
||||
gatewaySetupCodeControllerInternal = TextEditingController();
|
||||
gatewayHostControllerInternal = TextEditingController();
|
||||
gatewayPortControllerInternal = TextEditingController();
|
||||
@ -244,6 +254,10 @@ class SettingsPageStateInternal extends State<SettingsPage> {
|
||||
aiGatewayApiKeyRefControllerInternal.dispose();
|
||||
aiGatewayApiKeyControllerInternal.dispose();
|
||||
aiGatewayModelSearchControllerInternal.dispose();
|
||||
accountBaseUrlControllerInternal.dispose();
|
||||
accountUsernameControllerInternal.dispose();
|
||||
accountPasswordControllerInternal.dispose();
|
||||
accountMfaCodeControllerInternal.dispose();
|
||||
gatewaySetupCodeControllerInternal.dispose();
|
||||
gatewayHostControllerInternal.dispose();
|
||||
gatewayPortControllerInternal.dispose();
|
||||
|
||||
@ -64,10 +64,12 @@ extension SettingsPageGatewayMixinInternal on SettingsPageStateInternal {
|
||||
'SKILLS 目录授权',
|
||||
'SKILLS Directory Authorization',
|
||||
),
|
||||
GatewayIntegrationSubTabInternal.advancedConfig => appText(
|
||||
'高级自定义配置',
|
||||
'Advanced Custom Configuration',
|
||||
),
|
||||
};
|
||||
return [
|
||||
buildAcpBridgeServerModeCardInternal(context, controller, settings),
|
||||
const SizedBox(height: 16),
|
||||
SectionTabs(
|
||||
items: <String>[
|
||||
'OpenClaw Gateway',
|
||||
@ -75,6 +77,7 @@ extension SettingsPageGatewayMixinInternal on SettingsPageStateInternal {
|
||||
appText('LLM 接入点', 'LLM Endpoints'),
|
||||
appText('ACP 外部接入', 'External ACP'),
|
||||
appText('SKILLS 目录授权', 'SKILLS Directory Authorization'),
|
||||
appText('高级自定义配置', 'Advanced Custom Configuration'),
|
||||
],
|
||||
value: tabLabel,
|
||||
onChanged: (value) => setStateInternal(() {
|
||||
@ -86,7 +89,11 @@ extension SettingsPageGatewayMixinInternal on SettingsPageStateInternal {
|
||||
GatewayIntegrationSubTabInternal.llm,
|
||||
_ when value == appText('ACP 外部接入', 'External ACP') =>
|
||||
GatewayIntegrationSubTabInternal.acp,
|
||||
_ => GatewayIntegrationSubTabInternal.skills,
|
||||
_
|
||||
when value ==
|
||||
appText('SKILLS 目录授权', 'SKILLS Directory Authorization') =>
|
||||
GatewayIntegrationSubTabInternal.skills,
|
||||
_ => GatewayIntegrationSubTabInternal.advancedConfig,
|
||||
};
|
||||
}),
|
||||
),
|
||||
@ -210,6 +217,11 @@ extension SettingsPageGatewayMixinInternal on SettingsPageStateInternal {
|
||||
),
|
||||
),
|
||||
],
|
||||
GatewayIntegrationSubTabInternal.advancedConfig => <Widget>[
|
||||
buildOnlineAccountCardInternal(context, controller, settings),
|
||||
const SizedBox(height: 16),
|
||||
buildAcpBridgeServerModeCardInternal(context, controller, settings),
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
@ -224,8 +236,6 @@ extension SettingsPageGatewayMixinInternal on SettingsPageStateInternal {
|
||||
settings.acpBridgeServerModeConfig.mode ==
|
||||
AcpBridgeServerMode.advancedCustom;
|
||||
return [
|
||||
buildAcpBridgeServerModeCardInternal(context, controller, settings),
|
||||
const SizedBox(height: 16),
|
||||
Opacity(
|
||||
opacity: advancedEditable ? 1 : 0.72,
|
||||
child: IgnorePointer(
|
||||
@ -237,7 +247,11 @@ extension SettingsPageGatewayMixinInternal on SettingsPageStateInternal {
|
||||
onChanged: (value) => setStateInternal(() {
|
||||
openClawGatewayExpandedInternal = value;
|
||||
}),
|
||||
child: buildOpenClawGatewayCardInternal(context, controller, settings),
|
||||
child: buildOpenClawGatewayCardInternal(
|
||||
context,
|
||||
controller,
|
||||
settings,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -254,7 +268,11 @@ extension SettingsPageGatewayMixinInternal on SettingsPageStateInternal {
|
||||
onChanged: (value) => setStateInternal(() {
|
||||
vaultServerExpandedInternal = value;
|
||||
}),
|
||||
child: buildVaultProviderCardInternal(context, controller, settings),
|
||||
child: buildVaultProviderCardInternal(
|
||||
context,
|
||||
controller,
|
||||
settings,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
@ -280,7 +298,11 @@ extension SettingsPageGatewayMixinInternal on SettingsPageStateInternal {
|
||||
onChanged: (value) => setStateInternal(() {
|
||||
aiGatewayExpandedInternal = value;
|
||||
}),
|
||||
child: buildLlmEndpointManagerInternal(context, controller, settings),
|
||||
child: buildLlmEndpointManagerInternal(
|
||||
context,
|
||||
controller,
|
||||
settings,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@ -39,17 +39,17 @@ String describeExternalAcpTestFailure(Object error, {Uri? endpoint}) {
|
||||
final bodyRead = detailMap['bodyRead'] == true ? 'yes' : 'no';
|
||||
return appText(
|
||||
'连接不稳定:服务端在响应体接收完成前提前关闭了连接。'
|
||||
'${requestUrl.isEmpty ? '' : '\nURL: $requestUrl'}'
|
||||
'\nHTTP: $statusCode'
|
||||
'\ncontent-type: ${contentType == null || contentType.isEmpty ? 'n/a' : contentType}'
|
||||
'\nbody received: $bodyRead'
|
||||
'\n应用会对这类瞬时错误自动重试一次;如果仍失败,请检查上游服务或反向代理是否提前断流。',
|
||||
'${requestUrl.isEmpty ? '' : '\nURL: $requestUrl'}'
|
||||
'\nHTTP: $statusCode'
|
||||
'\ncontent-type: ${contentType == null || contentType.isEmpty ? 'n/a' : contentType}'
|
||||
'\nbody received: $bodyRead'
|
||||
'\n应用会对这类瞬时错误自动重试一次;如果仍失败,请检查上游服务或反向代理是否提前断流。',
|
||||
'Connection was interrupted before the response body finished arriving.'
|
||||
'${requestUrl.isEmpty ? '' : '\nURL: $requestUrl'}'
|
||||
'\nHTTP: $statusCode'
|
||||
'\ncontent-type: ${contentType == null || contentType.isEmpty ? 'n/a' : contentType}'
|
||||
'\nbody received: $bodyRead'
|
||||
'\nThe app retries this transient error once automatically. If it still fails, inspect the upstream service or reverse proxy for early connection termination.',
|
||||
'${requestUrl.isEmpty ? '' : '\nURL: $requestUrl'}'
|
||||
'\nHTTP: $statusCode'
|
||||
'\ncontent-type: ${contentType == null || contentType.isEmpty ? 'n/a' : contentType}'
|
||||
'\nbody received: $bodyRead'
|
||||
'\nThe app retries this transient error once automatically. If it still fails, inspect the upstream service or reverse proxy for early connection termination.',
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -150,6 +150,299 @@ bool shouldRetryExternalAcpTestFailure(Object error) {
|
||||
}
|
||||
|
||||
extension SettingsPageGatewayAcpMixinInternal on SettingsPageStateInternal {
|
||||
void syncAccountDraftControllersInternal(SettingsSnapshot settings) {
|
||||
if (accountBaseUrlControllerInternal.text ==
|
||||
accountBaseUrlSyncedValueInternal &&
|
||||
settings.accountBaseUrl != accountBaseUrlSyncedValueInternal) {
|
||||
accountBaseUrlControllerInternal.text = settings.accountBaseUrl;
|
||||
}
|
||||
if (accountUsernameControllerInternal.text ==
|
||||
accountUsernameSyncedValueInternal &&
|
||||
settings.accountUsername != accountUsernameSyncedValueInternal) {
|
||||
accountUsernameControllerInternal.text = settings.accountUsername;
|
||||
}
|
||||
accountBaseUrlSyncedValueInternal = settings.accountBaseUrl;
|
||||
accountUsernameSyncedValueInternal = settings.accountUsername;
|
||||
}
|
||||
|
||||
Future<void> saveAccountProfileInternal(SettingsSnapshot settings) async {
|
||||
final nextSettings = settings.copyWith(
|
||||
accountBaseUrl: accountBaseUrlControllerInternal.text.trim(),
|
||||
accountUsername: accountUsernameControllerInternal.text.trim(),
|
||||
);
|
||||
await saveSettingsInternal(widget.controller, nextSettings);
|
||||
accountBaseUrlSyncedValueInternal = nextSettings.accountBaseUrl;
|
||||
accountUsernameSyncedValueInternal = nextSettings.accountUsername;
|
||||
}
|
||||
|
||||
Future<void> loginAccountInternal(SettingsSnapshot settings) async {
|
||||
await saveAccountProfileInternal(settings);
|
||||
try {
|
||||
await widget.controller.settingsController.loginAccount(
|
||||
baseUrl: accountBaseUrlControllerInternal.text.trim(),
|
||||
identifier: accountUsernameControllerInternal.text.trim(),
|
||||
password: accountPasswordControllerInternal.text,
|
||||
);
|
||||
} finally {
|
||||
accountPasswordControllerInternal.clear();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> verifyAccountMfaInternal() async {
|
||||
try {
|
||||
await widget.controller.settingsController.verifyAccountMfa(
|
||||
baseUrl: accountBaseUrlControllerInternal.text.trim(),
|
||||
code: accountMfaCodeControllerInternal.text.trim(),
|
||||
);
|
||||
} finally {
|
||||
accountMfaCodeControllerInternal.clear();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> syncAccountSettingsInternal(SettingsSnapshot settings) async {
|
||||
await saveAccountProfileInternal(settings);
|
||||
await widget.controller.settingsController.syncAccountSettings(
|
||||
baseUrl: accountBaseUrlControllerInternal.text.trim(),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> logoutAccountInternal() async {
|
||||
await widget.controller.settingsController.logoutAccount();
|
||||
accountPasswordControllerInternal.clear();
|
||||
accountMfaCodeControllerInternal.clear();
|
||||
}
|
||||
|
||||
Future<void> cancelAccountMfaInternal() async {
|
||||
await widget.controller.settingsController.cancelAccountMfaChallenge();
|
||||
accountPasswordControllerInternal.clear();
|
||||
accountMfaCodeControllerInternal.clear();
|
||||
}
|
||||
|
||||
Widget buildOnlineAccountCardInternal(
|
||||
BuildContext context,
|
||||
AppController controller,
|
||||
SettingsSnapshot settings,
|
||||
) {
|
||||
syncAccountDraftControllersInternal(settings);
|
||||
final accountController = controller.settingsController;
|
||||
final accountSession = accountController.accountSession;
|
||||
final accountSyncState = accountController.accountSyncState;
|
||||
final accountBusy = accountController.accountBusy;
|
||||
final accountSignedIn = accountController.accountSignedIn;
|
||||
final accountMfaRequired = accountController.accountMfaRequired;
|
||||
final signedInLabel = accountSession?.email.trim().isNotEmpty == true
|
||||
? accountSession!.email.trim()
|
||||
: accountSession?.name.trim().isNotEmpty == true
|
||||
? accountSession!.name.trim()
|
||||
: appText('在线账户', 'Online Account');
|
||||
final sessionStatusText = accountSignedIn
|
||||
? appText('已登录:$signedInLabel', 'Signed in: $signedInLabel')
|
||||
: accountMfaRequired
|
||||
? appText('等待双重验证', 'Waiting for MFA verification')
|
||||
: appText('未登录', 'Signed out');
|
||||
final syncStatusText = accountSyncState == null
|
||||
? appText('idle · 尚未同步远程配置', 'idle · Remote config not synced yet')
|
||||
: '${accountSyncState.syncState} · ${accountSyncState.syncMessage}';
|
||||
|
||||
Widget buildSignedOutLoginCard() {
|
||||
final theme = Theme.of(context);
|
||||
return Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 840),
|
||||
child: SurfaceCard(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 36),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.cloud_outlined,
|
||||
size: 72,
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
appText('在线账户', 'Online Account'),
|
||||
style: theme.textTheme.headlineMedium,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
appText(
|
||||
'请先登录 ACP Bridge Server',
|
||||
'Please sign in to ACP Bridge Server',
|
||||
),
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
color: theme.textTheme.bodyMedium?.color?.withValues(
|
||||
alpha: 0.8,
|
||||
),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 28),
|
||||
TextFormField(
|
||||
key: const ValueKey('account-base-url-field'),
|
||||
controller: accountBaseUrlControllerInternal,
|
||||
decoration: InputDecoration(
|
||||
labelText: appText('服务地址', 'Service URL'),
|
||||
prefixIcon: const Icon(Icons.dns_outlined),
|
||||
),
|
||||
onFieldSubmitted: (_) => saveAccountProfileInternal(settings),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
key: const ValueKey('account-username-field'),
|
||||
controller: accountUsernameControllerInternal,
|
||||
decoration: InputDecoration(
|
||||
labelText: appText('邮箱或账号', 'Email or Username'),
|
||||
prefixIcon: const Icon(Icons.person_outline_rounded),
|
||||
),
|
||||
onFieldSubmitted: (_) => saveAccountProfileInternal(settings),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
key: const ValueKey('account-password-field'),
|
||||
controller: accountPasswordControllerInternal,
|
||||
obscureText: true,
|
||||
decoration: InputDecoration(
|
||||
labelText: appText('密码', 'Password'),
|
||||
prefixIcon: const Icon(Icons.lock_outline_rounded),
|
||||
),
|
||||
onFieldSubmitted: (_) => loginAccountInternal(settings),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: FilledButton(
|
||||
key: const ValueKey('account-login-button'),
|
||||
onPressed: accountBusy
|
||||
? null
|
||||
: () => loginAccountInternal(settings),
|
||||
child: Text(appText('登录', 'Sign In')),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildSignedInProfileCard() {
|
||||
return SurfaceCard(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
accountSignedIn
|
||||
? signedInLabel
|
||||
: settings.accountUsername.trim().isEmpty
|
||||
? appText('本地操作员', 'Local Operator')
|
||||
: settings.accountUsername,
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
appText(
|
||||
'这里继续只负责在线账户身份、MFA、工作区与同步摘要。ACP Bridge Server 的本地连接与高级配置在下面统一收口。',
|
||||
'This card focuses on online account identity, MFA, workspace, and sync summary. Local ACP Bridge Server connection and advanced config are unified below.',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
sessionStatusText,
|
||||
key: const ValueKey('account-session-status'),
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
syncStatusText,
|
||||
key: const ValueKey('account-sync-status'),
|
||||
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('在线账户同步摘要', 'Online account sync summary'),
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'${appText('服务地址', 'Service URL')}: ${settings.accountBaseUrl.trim().isEmpty ? appText('未填写', 'Not set') : settings.accountBaseUrl}',
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
'${appText('在线账户', 'Online Account')}: ${settings.accountUsername.trim().isEmpty ? appText('未填写', 'Not set') : settings.accountUsername}',
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
'${appText('最近同步', 'Last Sync')}: ${accountSyncState == null || settings.acpBridgeServerModeConfig.cloudSynced.lastSyncAt <= 0 ? appText('尚未同步', 'Not synced yet') : DateTime.fromMillisecondsSinceEpoch(settings.acpBridgeServerModeConfig.cloudSynced.lastSyncAt).toLocal().toIso8601String()}',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
if (accountMfaRequired) ...[
|
||||
TextFormField(
|
||||
key: const ValueKey('account-mfa-code-field'),
|
||||
controller: accountMfaCodeControllerInternal,
|
||||
decoration: InputDecoration(
|
||||
labelText: appText('双重验证代码', 'MFA Code'),
|
||||
),
|
||||
onFieldSubmitted: (_) => verifyAccountMfaInternal(),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
Wrap(
|
||||
spacing: 12,
|
||||
runSpacing: 12,
|
||||
children: [
|
||||
if (accountMfaRequired)
|
||||
FilledButton.tonal(
|
||||
key: const ValueKey('account-verify-mfa-button'),
|
||||
onPressed: accountBusy ? null : verifyAccountMfaInternal,
|
||||
child: Text(appText('验证并同步', 'Verify & Sync')),
|
||||
),
|
||||
if (accountMfaRequired)
|
||||
FilledButton.tonal(
|
||||
key: const ValueKey('account-edit-button'),
|
||||
onPressed: accountBusy ? null : cancelAccountMfaInternal,
|
||||
child: Text(appText('返回编辑', 'Back to Edit')),
|
||||
),
|
||||
if (accountSignedIn)
|
||||
FilledButton.tonal(
|
||||
key: const ValueKey('account-sync-button'),
|
||||
onPressed: accountBusy
|
||||
? null
|
||||
: () => syncAccountSettingsInternal(settings),
|
||||
child: Text(appText('重新同步', 'Sync Again')),
|
||||
),
|
||||
if (accountSignedIn)
|
||||
FilledButton.tonal(
|
||||
key: const ValueKey('account-logout-button'),
|
||||
onPressed: accountBusy ? null : logoutAccountInternal,
|
||||
child: Text(appText('退出登录', 'Log Out')),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return accountSignedIn || accountMfaRequired
|
||||
? buildSignedInProfileCard()
|
||||
: buildSignedOutLoginCard();
|
||||
}
|
||||
|
||||
Widget buildAcpBridgeServerModeCardInternal(
|
||||
BuildContext context,
|
||||
AppController controller,
|
||||
@ -164,9 +457,9 @@ extension SettingsPageGatewayAcpMixinInternal on SettingsPageStateInternal {
|
||||
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'),
|
||||
'cloudSynced' => appText('在线账户', 'Online Account'),
|
||||
'selfHosted' => appText('本地账户', 'Local Account'),
|
||||
_ => appText('高级模式', 'Advanced Mode'),
|
||||
};
|
||||
final syncStatus = accountSyncState?.syncState.trim().isNotEmpty == true
|
||||
? accountSyncState!.syncState
|
||||
@ -181,14 +474,17 @@ extension SettingsPageGatewayAcpMixinInternal on SettingsPageStateInternal {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'XWorkmate ACP Bridge Server',
|
||||
appText(
|
||||
'ACP Bridge Server 连接模式',
|
||||
'ACP Bridge Server Connection Mode',
|
||||
),
|
||||
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.',
|
||||
'在线账户负责云端同步,本地账户负责连接 ACP Bridge Server,高级模式是在本地账户基础上再叠加 advanced config 覆盖层。App 只负责配置、会话、安全存储与连接编排,不承载服务端逻辑。',
|
||||
'Online account handles cloud sync, local account connects to ACP Bridge Server, and advanced mode layers advanced config on top of the local account. The app stays a pure client for configuration, session handling, secure storage, and connection orchestration.',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
@ -198,7 +494,7 @@ extension SettingsPageGatewayAcpMixinInternal on SettingsPageStateInternal {
|
||||
children: [
|
||||
ChoiceChip(
|
||||
key: const ValueKey('acp-bridge-mode-cloud'),
|
||||
label: Text(appText('在线同步配置', 'Cloud Sync')),
|
||||
label: Text(appText('在线账户', 'Online Account')),
|
||||
selected: modeConfig.mode == AcpBridgeServerMode.cloudSynced,
|
||||
onSelected: (_) => saveSettingsInternal(
|
||||
controller,
|
||||
@ -212,7 +508,7 @@ extension SettingsPageGatewayAcpMixinInternal on SettingsPageStateInternal {
|
||||
),
|
||||
ChoiceChip(
|
||||
key: const ValueKey('acp-bridge-mode-self-hosted'),
|
||||
label: Text(appText('本地模式', 'Self-hosted')),
|
||||
label: Text(appText('本地账户', 'Local Account')),
|
||||
selected: modeConfig.mode == AcpBridgeServerMode.selfHosted,
|
||||
onSelected: (_) => saveSettingsInternal(
|
||||
controller,
|
||||
@ -226,7 +522,7 @@ extension SettingsPageGatewayAcpMixinInternal on SettingsPageStateInternal {
|
||||
),
|
||||
ChoiceChip(
|
||||
key: const ValueKey('acp-bridge-mode-advanced'),
|
||||
label: Text(appText('高级自定义', 'Advanced Custom')),
|
||||
label: Text(appText('高级模式', 'Advanced Mode')),
|
||||
selected: modeConfig.mode == AcpBridgeServerMode.advancedCustom,
|
||||
onSelected: (_) => saveSettingsInternal(
|
||||
controller,
|
||||
@ -246,7 +542,7 @@ extension SettingsPageGatewayAcpMixinInternal on SettingsPageStateInternal {
|
||||
runSpacing: 12,
|
||||
children: [
|
||||
StatusChipInternal(
|
||||
label: '${appText('当前来源', 'Source')}: $currentSource',
|
||||
label: '${appText('当前模式', 'Mode')}: $currentSource',
|
||||
tone: StatusChipToneInternal.ready,
|
||||
),
|
||||
StatusChipInternal(
|
||||
@ -263,23 +559,14 @@ extension SettingsPageGatewayAcpMixinInternal on SettingsPageStateInternal {
|
||||
Text(
|
||||
accountSignedIn
|
||||
? appText(
|
||||
'已登录云账户,可直接同步远端 ACP Bridge Server 配置。',
|
||||
'Signed in to the cloud account. You can sync the remote ACP Bridge Server configuration directly.',
|
||||
'已登录在线账户,可直接同步云端 ACP Bridge Server 默认配置。',
|
||||
'Signed in to the online account. You can sync the cloud ACP Bridge Server defaults directly.',
|
||||
)
|
||||
: appText(
|
||||
'当前未登录云账户。普通用户建议先登录,再从云端同步默认配置。',
|
||||
'No cloud account is signed in. For most users, sign in first and sync the default configuration from the cloud.',
|
||||
'当前未登录在线账户。建议先登录,再从云端同步默认配置。',
|
||||
'No online account is signed in. Sign in first, then 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}',
|
||||
),
|
||||
@ -297,43 +584,29 @@ extension SettingsPageGatewayAcpMixinInternal on SettingsPageStateInternal {
|
||||
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,
|
||||
),
|
||||
: () => syncAccountSettingsInternal(settings),
|
||||
child: Text(appText('重新同步', 'Sync Again')),
|
||||
),
|
||||
FilledButton.tonal(
|
||||
key: const ValueKey('acp-bridge-cloud-disconnect'),
|
||||
onPressed: accountBusy || !accountSignedIn
|
||||
? null
|
||||
: accountController.logoutAccount,
|
||||
: logoutAccountInternal,
|
||||
child: Text(appText('断开', 'Disconnect')),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
AcpBridgeServerMode.selfHosted => <Widget>[
|
||||
buildAcpBridgeServerSelfHostedPanelInternal(
|
||||
context,
|
||||
controller,
|
||||
settings,
|
||||
),
|
||||
],
|
||||
AcpBridgeServerMode.selfHosted => <Widget>[],
|
||||
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.',
|
||||
'高级模式 = 本地账户 + advanced config。下面先保留本地 ACP Bridge Server 连接,再把 OpenClaw Gateway / Vault Server / LLM Endpoint / 外部 ACP Server endpoint / SKILLS 目录 当作覆盖层。未覆盖的值继续继承当前基础模式。',
|
||||
'Advanced mode = local account + advanced config. Keep the local ACP Bridge Server connection below, then treat the OpenClaw Gateway / Vault Server / LLM Endpoint / external ACP server endpoint / SKILLS directory as overrides. Fields you do not override keep inheriting from the current base mode.',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
@ -347,6 +620,15 @@ extension SettingsPageGatewayAcpMixinInternal on SettingsPageStateInternal {
|
||||
),
|
||||
],
|
||||
},
|
||||
const SizedBox(height: 16),
|
||||
buildAcpBridgeServerSelfHostedPanelInternal(
|
||||
context,
|
||||
controller,
|
||||
settings,
|
||||
targetMode: modeConfig.mode == AcpBridgeServerMode.advancedCustom
|
||||
? AcpBridgeServerMode.advancedCustom
|
||||
: AcpBridgeServerMode.selfHosted,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
@ -355,12 +637,25 @@ extension SettingsPageGatewayAcpMixinInternal on SettingsPageStateInternal {
|
||||
Widget buildAcpBridgeServerSelfHostedPanelInternal(
|
||||
BuildContext context,
|
||||
AppController controller,
|
||||
SettingsSnapshot settings,
|
||||
) {
|
||||
SettingsSnapshot settings, {
|
||||
AcpBridgeServerMode targetMode = AcpBridgeServerMode.selfHosted,
|
||||
}) {
|
||||
final selfHosted = settings.acpBridgeServerModeConfig.selfHosted;
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
appText('连接 ACP Bridge Server', 'Connect to ACP Bridge Server'),
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
appText(
|
||||
'填写本地或私有部署的 ACP Bridge Server 地址、用户名和密码,然后测试连接并保存到安全存储。',
|
||||
'Enter the URL, username, and password for your local or private ACP Bridge Server, then test the connection and save it into secure storage.',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
key: const ValueKey('acp-bridge-self-hosted-url'),
|
||||
controller: acpBridgeServerUrlControllerInternal,
|
||||
@ -375,9 +670,7 @@ extension SettingsPageGatewayAcpMixinInternal on SettingsPageStateInternal {
|
||||
TextField(
|
||||
key: const ValueKey('acp-bridge-self-hosted-username'),
|
||||
controller: acpBridgeServerUsernameControllerInternal,
|
||||
decoration: InputDecoration(
|
||||
labelText: appText('用户', 'Username'),
|
||||
),
|
||||
decoration: InputDecoration(labelText: appText('用户', 'Username')),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
@ -393,9 +686,7 @@ extension SettingsPageGatewayAcpMixinInternal on SettingsPageStateInternal {
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'${appText('密码引用', 'Password Ref')}: ${selfHosted.passwordRef}',
|
||||
),
|
||||
Text('${appText('密码引用', 'Password Ref')}: ${selfHosted.passwordRef}'),
|
||||
const SizedBox(height: 14),
|
||||
Wrap(
|
||||
spacing: 10,
|
||||
@ -417,6 +708,7 @@ extension SettingsPageGatewayAcpMixinInternal on SettingsPageStateInternal {
|
||||
onPressed: () => saveAcpBridgeServerSelfHostedInternal(
|
||||
controller,
|
||||
settings,
|
||||
targetMode: targetMode,
|
||||
),
|
||||
child: Text(appText('保存', 'Save')),
|
||||
),
|
||||
@ -425,6 +717,7 @@ extension SettingsPageGatewayAcpMixinInternal on SettingsPageStateInternal {
|
||||
onPressed: () => connectAcpBridgeServerSelfHostedInternal(
|
||||
controller,
|
||||
settings,
|
||||
targetMode: targetMode,
|
||||
),
|
||||
child: Text(appText('连接', 'Connect')),
|
||||
),
|
||||
@ -488,8 +781,9 @@ extension SettingsPageGatewayAcpMixinInternal on SettingsPageStateInternal {
|
||||
|
||||
Future<void> saveAcpBridgeServerSelfHostedInternal(
|
||||
AppController controller,
|
||||
SettingsSnapshot settings,
|
||||
) async {
|
||||
SettingsSnapshot settings, {
|
||||
AcpBridgeServerMode targetMode = AcpBridgeServerMode.selfHosted,
|
||||
}) async {
|
||||
final modeConfig = settings.acpBridgeServerModeConfig;
|
||||
final nextSelfHosted = modeConfig.selfHosted.copyWith(
|
||||
serverUrl: acpBridgeServerUrlControllerInternal.text,
|
||||
@ -504,16 +798,15 @@ extension SettingsPageGatewayAcpMixinInternal on SettingsPageStateInternal {
|
||||
module: 'Settings',
|
||||
);
|
||||
}
|
||||
final nextSettings = settings.captureAcpBridgeServerAdvancedOverrides().copyWith(
|
||||
accountLocalMode: true,
|
||||
acpBridgeServerModeConfig: settings
|
||||
.captureAcpBridgeServerAdvancedOverrides()
|
||||
.acpBridgeServerModeConfig
|
||||
.copyWith(
|
||||
mode: AcpBridgeServerMode.selfHosted,
|
||||
selfHosted: nextSelfHosted,
|
||||
),
|
||||
);
|
||||
final nextSettings = settings
|
||||
.captureAcpBridgeServerAdvancedOverrides()
|
||||
.copyWith(
|
||||
accountLocalMode: true,
|
||||
acpBridgeServerModeConfig: settings
|
||||
.captureAcpBridgeServerAdvancedOverrides()
|
||||
.acpBridgeServerModeConfig
|
||||
.copyWith(mode: targetMode, selfHosted: nextSelfHosted),
|
||||
);
|
||||
await saveSettingsInternal(controller, nextSettings);
|
||||
if (!mounted) {
|
||||
return;
|
||||
@ -521,17 +814,22 @@ extension SettingsPageGatewayAcpMixinInternal on SettingsPageStateInternal {
|
||||
acpBridgeServerPasswordControllerInternal.clear();
|
||||
setStateInternal(() {
|
||||
acpBridgeServerSelfHostedMessageInternal = appText(
|
||||
'Self-hosted 配置已保存,密码已进入 secure storage。',
|
||||
'The self-hosted configuration was saved and the password is now in secure storage.',
|
||||
'ACP Bridge Server 配置已保存,密码已进入 secure storage。',
|
||||
'The ACP Bridge Server configuration was saved and the password is now in secure storage.',
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> connectAcpBridgeServerSelfHostedInternal(
|
||||
AppController controller,
|
||||
SettingsSnapshot settings,
|
||||
) async {
|
||||
await saveAcpBridgeServerSelfHostedInternal(controller, settings);
|
||||
SettingsSnapshot settings, {
|
||||
AcpBridgeServerMode targetMode = AcpBridgeServerMode.selfHosted,
|
||||
}) async {
|
||||
await saveAcpBridgeServerSelfHostedInternal(
|
||||
controller,
|
||||
settings,
|
||||
targetMode: targetMode,
|
||||
);
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
@ -566,7 +864,11 @@ extension SettingsPageGatewayAcpMixinInternal on SettingsPageStateInternal {
|
||||
acpBridgeServerPasswordControllerInternal.text.trim().isNotEmpty
|
||||
? acpBridgeServerPasswordControllerInternal.text.trim()
|
||||
: await controller.settingsController.loadSecretValueByRef(
|
||||
controller.settings.acpBridgeServerModeConfig.selfHosted.passwordRef,
|
||||
controller
|
||||
.settings
|
||||
.acpBridgeServerModeConfig
|
||||
.selfHosted
|
||||
.passwordRef,
|
||||
);
|
||||
final authorization = password.isEmpty
|
||||
? ''
|
||||
@ -836,20 +1138,22 @@ extension SettingsPageGatewayAcpMixinInternal on SettingsPageStateInternal {
|
||||
);
|
||||
GatewayAcpCapabilities capabilities;
|
||||
try {
|
||||
capabilities = await controller.gatewayAcpClientInternal.loadCapabilities(
|
||||
forceRefresh: true,
|
||||
endpointOverride: endpoint,
|
||||
authorizationOverride: authorization,
|
||||
);
|
||||
capabilities = await controller.gatewayAcpClientInternal
|
||||
.loadCapabilities(
|
||||
forceRefresh: true,
|
||||
endpointOverride: endpoint,
|
||||
authorizationOverride: authorization,
|
||||
);
|
||||
} catch (error) {
|
||||
if (!shouldRetryExternalAcpTestFailure(error)) {
|
||||
rethrow;
|
||||
}
|
||||
capabilities = await controller.gatewayAcpClientInternal.loadCapabilities(
|
||||
forceRefresh: true,
|
||||
endpointOverride: endpoint,
|
||||
authorizationOverride: authorization,
|
||||
);
|
||||
capabilities = await controller.gatewayAcpClientInternal
|
||||
.loadCapabilities(
|
||||
forceRefresh: true,
|
||||
endpointOverride: endpoint,
|
||||
authorizationOverride: authorization,
|
||||
);
|
||||
}
|
||||
if (!mounted) {
|
||||
return;
|
||||
|
||||
@ -68,9 +68,16 @@ extension SettingsPageSectionsMixinInternal on SettingsPageStateInternal {
|
||||
settings,
|
||||
uiFeatures,
|
||||
),
|
||||
SettingsTab.agents => buildAgentsInternal(context, controller, settings),
|
||||
SettingsTab.agents => buildAgentsInternal(
|
||||
context,
|
||||
controller,
|
||||
settings,
|
||||
),
|
||||
SettingsTab.appearance => buildAppearanceInternal(context, controller),
|
||||
SettingsTab.diagnostics => buildDiagnosticsInternal(context, controller),
|
||||
SettingsTab.diagnostics => buildDiagnosticsInternal(
|
||||
context,
|
||||
controller,
|
||||
),
|
||||
SettingsTab.experimental => buildExperimentalInternal(
|
||||
context,
|
||||
controller,
|
||||
@ -256,10 +263,7 @@ extension SettingsPageSectionsMixinInternal on SettingsPageStateInternal {
|
||||
'当前存在待生效更改。保存并生效:立即按当前配置更新。',
|
||||
'There are saved changes waiting to be applied. Save & apply updates the current configuration immediately.',
|
||||
)
|
||||
: appText(
|
||||
'当前没有待提交更改。',
|
||||
'There are no pending settings changes.',
|
||||
),
|
||||
: appText('当前没有待提交更改。', 'There are no pending settings changes.'),
|
||||
applyLabel: appText('保存并生效', 'Save & apply'),
|
||||
onApply: (!hasDraft && !hasPendingApply)
|
||||
? null
|
||||
@ -311,43 +315,6 @@ extension SettingsPageSectionsMixinInternal on SettingsPageStateInternal {
|
||||
),
|
||||
if (controller.supportsDesktopIntegration)
|
||||
buildLinuxDesktopIntegrationInternal(context, controller, settings),
|
||||
if (uiFeatures.supportsAccountAccess)
|
||||
SurfaceCard(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
appText('账号访问', 'Account Access'),
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
EditableFieldInternal(
|
||||
label: appText('账号服务地址', 'Account Base URL'),
|
||||
value: settings.accountBaseUrl,
|
||||
onSubmitted: (value) => saveSettingsInternal(
|
||||
controller,
|
||||
settings.copyWith(accountBaseUrl: value),
|
||||
),
|
||||
),
|
||||
EditableFieldInternal(
|
||||
label: appText('账号用户名', 'Account Username'),
|
||||
value: settings.accountUsername,
|
||||
onSubmitted: (value) => saveSettingsInternal(
|
||||
controller,
|
||||
settings.copyWith(accountUsername: value),
|
||||
),
|
||||
),
|
||||
EditableFieldInternal(
|
||||
label: appText('工作区名称', 'Workspace Label'),
|
||||
value: settings.accountWorkspace,
|
||||
onSubmitted: (value) => saveSettingsInternal(
|
||||
controller,
|
||||
settings.copyWith(accountWorkspace: value),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@ -498,7 +498,14 @@ class WorkflowStepInternal extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
enum GatewayIntegrationSubTabInternal { gateway, vault, llm, acp, skills }
|
||||
enum GatewayIntegrationSubTabInternal {
|
||||
gateway,
|
||||
vault,
|
||||
llm,
|
||||
acp,
|
||||
skills,
|
||||
advancedConfig,
|
||||
}
|
||||
|
||||
enum LlmEndpointSlotInternal { aiGateway, ollamaLocal, ollamaCloud }
|
||||
|
||||
|
||||
@ -28,7 +28,7 @@ extension WorkspaceDestinationCopy on WorkspaceDestination {
|
||||
WorkspaceDestination.secrets => appText('密钥', 'Secrets'),
|
||||
WorkspaceDestination.aiGateway => 'LLM API',
|
||||
WorkspaceDestination.settings => appText('设置', 'Settings'),
|
||||
WorkspaceDestination.account => appText('账号', 'Account'),
|
||||
WorkspaceDestination.account => appText('在线账户', 'Online Account'),
|
||||
};
|
||||
|
||||
IconData get icon => switch (this) {
|
||||
@ -87,8 +87,8 @@ extension WorkspaceDestinationCopy on WorkspaceDestination {
|
||||
'Global settings and diagnostics, separated from business modules.',
|
||||
),
|
||||
WorkspaceDestination.account => appText(
|
||||
'用户身份、工作区切换与登录会话管理。',
|
||||
'Identity, workspace switching, and session management.',
|
||||
'在线账户、工作区切换、登录会话与 ACP Bridge Server 同步管理。',
|
||||
'Online account, workspace switching, login sessions, and ACP Bridge Server sync.',
|
||||
),
|
||||
};
|
||||
|
||||
|
||||
@ -113,10 +113,7 @@ class SidebarNavigation extends StatelessWidget {
|
||||
children: [
|
||||
const SizedBox(height: 4),
|
||||
if (isCollapsed && showCollapseControl) ...[
|
||||
SidebarHeader(
|
||||
isCollapsed: true,
|
||||
onTap: onExpandFromCollapsed,
|
||||
),
|
||||
SidebarHeader(isCollapsed: true, onTap: onExpandFromCollapsed),
|
||||
const SizedBox(height: AppSpacing.xs),
|
||||
],
|
||||
Expanded(
|
||||
@ -158,13 +155,8 @@ class SidebarNavigation extends StatelessWidget {
|
||||
sidebarState: sidebarState,
|
||||
onCycleSidebarState: onCycleSidebarState,
|
||||
onOpenAccount: onOpenAccount,
|
||||
showAccountButton:
|
||||
availableDestinations == null ||
|
||||
availableDestinations!.contains(
|
||||
WorkspaceDestination.account,
|
||||
),
|
||||
accountSelected:
|
||||
currentSection == WorkspaceDestination.account,
|
||||
showAccountButton: false,
|
||||
accountSelected: false,
|
||||
showCollapseControl: false,
|
||||
),
|
||||
],
|
||||
|
||||
@ -9,29 +9,45 @@ 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);
|
||||
testWidgets(
|
||||
'SettingsPage shows ACP bridge server mode card in advanced custom config',
|
||||
(WidgetTester tester) async {
|
||||
final controller = await createTestController(tester);
|
||||
controller.openSettings(tab: SettingsTab.gateway);
|
||||
|
||||
await pumpPage(
|
||||
tester,
|
||||
child: SettingsPage(
|
||||
controller: controller,
|
||||
initialTab: SettingsTab.gateway,
|
||||
),
|
||||
);
|
||||
await pumpPage(
|
||||
tester,
|
||||
child: SettingsPage(
|
||||
controller: controller,
|
||||
initialTab: SettingsTab.gateway,
|
||||
showSectionTabs: true,
|
||||
),
|
||||
);
|
||||
|
||||
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,
|
||||
);
|
||||
});
|
||||
await tester.tap(find.byKey(const ValueKey('section-tab-高级自定义配置')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('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,
|
||||
);
|
||||
expect(
|
||||
find.byKey(const ValueKey('acp-bridge-self-hosted-url')),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(
|
||||
find.byKey(const ValueKey('acp-bridge-self-hosted-connect')),
|
||||
findsOneWidget,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@ -196,47 +196,50 @@ void main() {
|
||||
expect(controller.themeMode, ThemeMode.light);
|
||||
});
|
||||
|
||||
testWidgets('SettingsPage hides account access controls by default', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
final controller = await createTestController(tester);
|
||||
testWidgets(
|
||||
'SettingsPage gateway advanced config tab merges online account and ACP Bridge Server',
|
||||
(WidgetTester tester) async {
|
||||
final controller = await createTestController(tester);
|
||||
controller.setSettingsTab(SettingsTab.gateway);
|
||||
|
||||
await pumpPage(
|
||||
tester,
|
||||
child: SettingsPage(controller: controller),
|
||||
platform: TargetPlatform.macOS,
|
||||
);
|
||||
await pumpPage(
|
||||
tester,
|
||||
child: SettingsPage(
|
||||
controller: controller,
|
||||
initialTab: SettingsTab.gateway,
|
||||
showSectionTabs: true,
|
||||
),
|
||||
platform: TargetPlatform.macOS,
|
||||
);
|
||||
|
||||
expect(find.text('账号访问'), findsNothing);
|
||||
expect(find.text('Account Access'), findsNothing);
|
||||
expect(find.text('账号本地模式'), findsNothing);
|
||||
expect(find.text('Account local mode'), findsNothing);
|
||||
});
|
||||
expect(
|
||||
find.byKey(const ValueKey('account-base-url-field')),
|
||||
findsNothing,
|
||||
);
|
||||
expect(find.byKey(const ValueKey('acp-bridge-mode-cloud')), findsNothing);
|
||||
|
||||
testWidgets('SettingsPage can expose account access when feature enabled', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
final manifest = UiFeatureManifest.fallback().copyWithFeature(
|
||||
platform: UiFeaturePlatform.desktop,
|
||||
module: 'settings',
|
||||
feature: 'account_access',
|
||||
enabled: true,
|
||||
releaseTier: UiFeatureReleaseTier.experimental,
|
||||
);
|
||||
final controller = await createTestController(
|
||||
tester,
|
||||
uiFeatureManifest: manifest,
|
||||
);
|
||||
await tester.tap(find.byKey(const ValueKey('section-tab-高级自定义配置')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await pumpPage(
|
||||
tester,
|
||||
child: SettingsPage(controller: controller),
|
||||
platform: TargetPlatform.macOS,
|
||||
);
|
||||
|
||||
expect(find.text('账号访问'), findsOneWidget);
|
||||
expect(find.text('账号本地模式'), findsOneWidget);
|
||||
});
|
||||
expect(find.text('ACP Bridge Server 连接模式'), findsOneWidget);
|
||||
expect(
|
||||
find.byKey(const ValueKey('account-base-url-field')),
|
||||
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,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'SettingsPage workspace tab no longer exposes remote project root',
|
||||
@ -860,15 +863,16 @@ paths:
|
||||
final remoteProfile = controller.settings.primaryRemoteGatewayProfile;
|
||||
setGatewaySnapshotForTest(
|
||||
controller,
|
||||
GatewayConnectionSnapshot.initial(mode: RuntimeConnectionMode.remote)
|
||||
.copyWith(
|
||||
status: RuntimeConnectionStatus.connected,
|
||||
statusText: 'Connected',
|
||||
remoteAddress: '${remoteProfile.host}:${remoteProfile.port}',
|
||||
lastError: 'NOT_PAIRED: pairing required',
|
||||
lastErrorCode: 'NOT_PAIRED',
|
||||
lastErrorDetailCode: 'PAIRING_REQUIRED',
|
||||
),
|
||||
GatewayConnectionSnapshot.initial(
|
||||
mode: RuntimeConnectionMode.remote,
|
||||
).copyWith(
|
||||
status: RuntimeConnectionStatus.connected,
|
||||
statusText: 'Connected',
|
||||
remoteAddress: '${remoteProfile.host}:${remoteProfile.port}',
|
||||
lastError: 'NOT_PAIRED: pairing required',
|
||||
lastErrorCode: 'NOT_PAIRED',
|
||||
lastErrorDetailCode: 'PAIRING_REQUIRED',
|
||||
),
|
||||
);
|
||||
|
||||
await _pumpWithoutSettling(
|
||||
@ -876,7 +880,10 @@ paths:
|
||||
child: ConnectionChipInternal(controller: controller),
|
||||
);
|
||||
|
||||
expect(find.byKey(const Key('assistant-connection-chip')), findsOneWidget);
|
||||
expect(
|
||||
find.byKey(const Key('assistant-connection-chip')),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(
|
||||
find.textContaining(
|
||||
'已连接 · ${remoteProfile.host}:${remoteProfile.port}',
|
||||
|
||||
@ -187,38 +187,44 @@ class _TestFakeGatewayRuntime extends GatewayRuntime {
|
||||
case 'device.pair.list':
|
||||
return <String, dynamic>{
|
||||
'pending': _pairingList.pending
|
||||
.map((item) => <String, dynamic>{
|
||||
'requestId': item.requestId,
|
||||
'deviceId': item.deviceId,
|
||||
'label': item.label,
|
||||
'role': item.role,
|
||||
'scopes': item.scopes,
|
||||
'remoteIp': item.remoteIp,
|
||||
'requestedAtMs': item.requestedAtMs,
|
||||
'repair': item.isRepair,
|
||||
})
|
||||
.map(
|
||||
(item) => <String, dynamic>{
|
||||
'requestId': item.requestId,
|
||||
'deviceId': item.deviceId,
|
||||
'label': item.label,
|
||||
'role': item.role,
|
||||
'scopes': item.scopes,
|
||||
'remoteIp': item.remoteIp,
|
||||
'requestedAtMs': item.requestedAtMs,
|
||||
'repair': item.isRepair,
|
||||
},
|
||||
)
|
||||
.toList(growable: false),
|
||||
'paired': _pairingList.paired
|
||||
.map((item) => <String, dynamic>{
|
||||
'deviceId': item.deviceId,
|
||||
'displayName': item.displayName,
|
||||
'roles': item.roles,
|
||||
'scopes': item.scopes,
|
||||
'remoteIp': item.remoteIp,
|
||||
'tokens': item.tokens
|
||||
.map((token) => <String, dynamic>{
|
||||
'role': token.role,
|
||||
'scopes': token.scopes,
|
||||
'createdAtMs': token.createdAtMs,
|
||||
'rotatedAtMs': token.rotatedAtMs,
|
||||
'revokedAtMs': token.revokedAtMs,
|
||||
'lastUsedAtMs': token.lastUsedAtMs,
|
||||
})
|
||||
.toList(growable: false),
|
||||
'createdAtMs': item.createdAtMs,
|
||||
'approvedAtMs': item.approvedAtMs,
|
||||
'currentDevice': item.currentDevice,
|
||||
})
|
||||
.map(
|
||||
(item) => <String, dynamic>{
|
||||
'deviceId': item.deviceId,
|
||||
'displayName': item.displayName,
|
||||
'roles': item.roles,
|
||||
'scopes': item.scopes,
|
||||
'remoteIp': item.remoteIp,
|
||||
'tokens': item.tokens
|
||||
.map(
|
||||
(token) => <String, dynamic>{
|
||||
'role': token.role,
|
||||
'scopes': token.scopes,
|
||||
'createdAtMs': token.createdAtMs,
|
||||
'rotatedAtMs': token.rotatedAtMs,
|
||||
'revokedAtMs': token.revokedAtMs,
|
||||
'lastUsedAtMs': token.lastUsedAtMs,
|
||||
},
|
||||
)
|
||||
.toList(growable: false),
|
||||
'createdAtMs': item.createdAtMs,
|
||||
'approvedAtMs': item.approvedAtMs,
|
||||
'currentDevice': item.currentDevice,
|
||||
},
|
||||
)
|
||||
.toList(growable: false),
|
||||
};
|
||||
case 'system-presence':
|
||||
@ -235,7 +241,9 @@ void setGatewaySnapshotForTest(
|
||||
) {
|
||||
final runtime = controller.runtime;
|
||||
if (runtime is! _TestFakeGatewayRuntime) {
|
||||
throw StateError('createTestController() runtime does not support mutation');
|
||||
throw StateError(
|
||||
'createTestController() runtime does not support mutation',
|
||||
);
|
||||
}
|
||||
runtime.setSnapshotForTest(snapshot);
|
||||
}
|
||||
@ -246,7 +254,9 @@ void setGatewayPairingListForTest(
|
||||
) {
|
||||
final runtime = controller.runtime;
|
||||
if (runtime is! _TestFakeGatewayRuntime) {
|
||||
throw StateError('createTestController() runtime does not support mutation');
|
||||
throw StateError(
|
||||
'createTestController() runtime does not support mutation',
|
||||
);
|
||||
}
|
||||
runtime.setDevicePairingForTest(pairingList);
|
||||
}
|
||||
@ -262,7 +272,7 @@ class _TestFakeCodexRuntime extends CodexRuntime {
|
||||
Future<void> pumpPage(
|
||||
WidgetTester tester, {
|
||||
required Widget child,
|
||||
Size size = const Size(1600, 1000),
|
||||
Size size = const Size(1600, 4000),
|
||||
TargetPlatform? platform,
|
||||
}) async {
|
||||
tester.view.devicePixelRatio = 1;
|
||||
|
||||
@ -85,7 +85,7 @@ void main() {
|
||||
expect(find.text('ClawHub'), findsNothing);
|
||||
expect(find.text('回到 APP首页'), findsNothing);
|
||||
expect(find.text('设置'), findsOneWidget);
|
||||
expect(find.text('账户'), findsOneWidget);
|
||||
expect(find.text('账户'), findsNothing);
|
||||
expect(find.text('语言'), findsOneWidget);
|
||||
expect(find.text('主题'), findsOneWidget);
|
||||
|
||||
@ -107,11 +107,11 @@ void main() {
|
||||
await tester.pumpAndSettle();
|
||||
expect(themeToggled, 1);
|
||||
|
||||
await tester.tap(
|
||||
expect(
|
||||
find.byKey(const ValueKey<String>('sidebar-footer-account')),
|
||||
findsNothing,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(accountOpened, 1);
|
||||
expect(accountOpened, 0);
|
||||
|
||||
await tester.tap(
|
||||
find.byKey(const Key('workspace-sidebar-collapse-button')),
|
||||
|
||||
Loading…
Reference in New Issue
Block a user