Unify task dialog gateway modes
This commit is contained in:
parent
09ef2ea4f5
commit
c679d6a13f
@ -534,7 +534,12 @@ class AppController extends ChangeNotifier {
|
||||
mode: _modeFromHost(decoded?.host ?? settings.gateway.host),
|
||||
);
|
||||
await saveSettings(
|
||||
settings.copyWith(gateway: nextProfile),
|
||||
settings.copyWith(
|
||||
gateway: nextProfile,
|
||||
assistantExecutionTarget: _assistantExecutionTargetForMode(
|
||||
nextProfile.mode,
|
||||
),
|
||||
),
|
||||
refreshAfterSave: false,
|
||||
);
|
||||
await _connectProfile(
|
||||
@ -572,7 +577,12 @@ class AppController extends ChangeNotifier {
|
||||
tls: mode == RuntimeConnectionMode.local ? false : tls,
|
||||
);
|
||||
await saveSettings(
|
||||
settings.copyWith(gateway: nextProfile),
|
||||
settings.copyWith(
|
||||
gateway: nextProfile,
|
||||
assistantExecutionTarget: _assistantExecutionTargetForMode(
|
||||
nextProfile.mode,
|
||||
),
|
||||
),
|
||||
refreshAfterSave: false,
|
||||
);
|
||||
await _connectProfile(
|
||||
@ -739,6 +749,30 @@ class AppController extends ChangeNotifier {
|
||||
if (settings.assistantExecutionTarget == target) {
|
||||
return;
|
||||
}
|
||||
if (target == AssistantExecutionTarget.aiGatewayOnly) {
|
||||
final nextGatewayProfile = settings.gateway.copyWith(
|
||||
mode: RuntimeConnectionMode.unconfigured,
|
||||
useSetupCode: false,
|
||||
setupCode: '',
|
||||
);
|
||||
await saveSettings(
|
||||
settings.copyWith(
|
||||
assistantExecutionTarget: target,
|
||||
gateway: nextGatewayProfile,
|
||||
),
|
||||
refreshAfterSave: false,
|
||||
);
|
||||
if (_runtime.isConnected) {
|
||||
try {
|
||||
await disconnectGateway();
|
||||
} catch (_) {
|
||||
// Preserve the selected AI Gateway-only mode even if the active
|
||||
// gateway session does not close cleanly on the first attempt.
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
await saveSettings(
|
||||
settings.copyWith(assistantExecutionTarget: target),
|
||||
refreshAfterSave: false,
|
||||
@ -1460,10 +1494,31 @@ class AppController extends ChangeNotifier {
|
||||
return RuntimeConnectionMode.remote;
|
||||
}
|
||||
|
||||
AssistantExecutionTarget _assistantExecutionTargetForMode(
|
||||
RuntimeConnectionMode mode,
|
||||
) {
|
||||
return switch (mode) {
|
||||
RuntimeConnectionMode.unconfigured =>
|
||||
AssistantExecutionTarget.aiGatewayOnly,
|
||||
RuntimeConnectionMode.local => AssistantExecutionTarget.local,
|
||||
RuntimeConnectionMode.remote => AssistantExecutionTarget.remote,
|
||||
};
|
||||
}
|
||||
|
||||
GatewayConnectionProfile _gatewayProfileForAssistantExecutionTarget(
|
||||
AssistantExecutionTarget target,
|
||||
) {
|
||||
if (target == AssistantExecutionTarget.aiGatewayOnly) {
|
||||
return settings.gateway.copyWith(
|
||||
mode: RuntimeConnectionMode.unconfigured,
|
||||
useSetupCode: false,
|
||||
setupCode: '',
|
||||
);
|
||||
}
|
||||
|
||||
final desiredMode = switch (target) {
|
||||
AssistantExecutionTarget.aiGatewayOnly =>
|
||||
RuntimeConnectionMode.unconfigured,
|
||||
AssistantExecutionTarget.local => RuntimeConnectionMode.local,
|
||||
AssistantExecutionTarget.remote => RuntimeConnectionMode.remote,
|
||||
};
|
||||
@ -1484,13 +1539,19 @@ class AppController extends ChangeNotifier {
|
||||
}
|
||||
|
||||
final defaults = GatewayConnectionProfile.defaults();
|
||||
final savedHost = savedProfile.host.trim().isEmpty
|
||||
? defaults.host
|
||||
: savedProfile.host.trim();
|
||||
final savedPort = savedProfile.port <= 0
|
||||
? defaults.port
|
||||
: savedProfile.port;
|
||||
return savedProfile.copyWith(
|
||||
mode: RuntimeConnectionMode.remote,
|
||||
useSetupCode: false,
|
||||
setupCode: '',
|
||||
host: defaults.host,
|
||||
port: defaults.port,
|
||||
tls: defaults.tls,
|
||||
host: savedHost,
|
||||
port: savedPort,
|
||||
tls: savedProfile.tls,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -2073,7 +2073,7 @@ class _ComposerBar extends StatelessWidget {
|
||||
const SizedBox(width: 6),
|
||||
PopupMenuButton<AssistantExecutionTarget>(
|
||||
key: const Key('assistant-execution-target-button'),
|
||||
tooltip: appText('本地或远程', 'Local or remote'),
|
||||
tooltip: appText('任务对话模式', 'Task Dialog Mode'),
|
||||
onSelected: (value) {
|
||||
controller.setAssistantExecutionTarget(value);
|
||||
},
|
||||
@ -2608,6 +2608,7 @@ class _ComposerToolbarChipState extends State<_ComposerToolbarChip> {
|
||||
|
||||
extension on AssistantExecutionTarget {
|
||||
IconData get icon => switch (this) {
|
||||
AssistantExecutionTarget.aiGatewayOnly => Icons.hub_outlined,
|
||||
AssistantExecutionTarget.local => Icons.computer_outlined,
|
||||
AssistantExecutionTarget.remote => Icons.cloud_outlined,
|
||||
};
|
||||
|
||||
@ -31,15 +31,26 @@ extension RuntimeConnectionStatusCopy on RuntimeConnectionStatus {
|
||||
};
|
||||
}
|
||||
|
||||
enum AssistantExecutionTarget { local, remote }
|
||||
enum AssistantExecutionTarget { aiGatewayOnly, local, remote }
|
||||
|
||||
extension AssistantExecutionTargetCopy on AssistantExecutionTarget {
|
||||
String get label => switch (this) {
|
||||
AssistantExecutionTarget.local => appText('本地', 'Local'),
|
||||
AssistantExecutionTarget.remote => appText('远程', 'Remote'),
|
||||
AssistantExecutionTarget.aiGatewayOnly => appText(
|
||||
'仅 AI Gateway',
|
||||
'AI Gateway Only',
|
||||
),
|
||||
AssistantExecutionTarget.local => appText(
|
||||
'本地 OpenClaw Gateway',
|
||||
'Local OpenClaw Gateway',
|
||||
),
|
||||
AssistantExecutionTarget.remote => appText(
|
||||
'远程 OpenClaw Gateway',
|
||||
'Remote OpenClaw Gateway',
|
||||
),
|
||||
};
|
||||
|
||||
String get promptValue => switch (this) {
|
||||
AssistantExecutionTarget.aiGatewayOnly => 'ai-gateway-only',
|
||||
AssistantExecutionTarget.local => 'local',
|
||||
AssistantExecutionTarget.remote => 'remote',
|
||||
};
|
||||
|
||||
@ -38,6 +38,32 @@ class _GatewayConnectDialogState extends State<GatewayConnectDialog> {
|
||||
RuntimeConnectionMode _connectionMode = RuntimeConnectionMode.remote;
|
||||
bool _submitting = false;
|
||||
|
||||
bool get _isAiGatewayOnlyMode =>
|
||||
_mode == 'manual' &&
|
||||
_connectionMode == RuntimeConnectionMode.unconfigured;
|
||||
|
||||
bool get _manualGatewayFieldsEnabled => !_isAiGatewayOnlyMode;
|
||||
|
||||
bool get _credentialFieldsEnabled =>
|
||||
_mode == 'setup' || _manualGatewayFieldsEnabled;
|
||||
|
||||
String _connectionModeLabel(RuntimeConnectionMode mode) {
|
||||
return switch (mode) {
|
||||
RuntimeConnectionMode.unconfigured => appText(
|
||||
'仅 AI Gateway',
|
||||
'AI Gateway Only',
|
||||
),
|
||||
RuntimeConnectionMode.local => appText(
|
||||
'本地 OpenClaw Gateway',
|
||||
'Local OpenClaw Gateway',
|
||||
),
|
||||
RuntimeConnectionMode.remote => appText(
|
||||
'远程 OpenClaw Gateway',
|
||||
'Remote OpenClaw Gateway',
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@ -93,6 +119,9 @@ class _GatewayConnectDialogState extends State<GatewayConnectDialog> {
|
||||
final typedGatewayToken = _tokenController.text.trim();
|
||||
final willUseStoredGatewayToken =
|
||||
typedGatewayToken.isEmpty && hasStoredGatewayToken;
|
||||
final showSharedTokenStatusCard =
|
||||
_credentialFieldsEnabled &&
|
||||
(willUseStoredGatewayToken || typedGatewayToken.isNotEmpty);
|
||||
final body = Theme(
|
||||
data: theme.copyWith(
|
||||
inputDecorationTheme: theme.inputDecorationTheme.copyWith(
|
||||
@ -119,8 +148,8 @@ class _GatewayConnectDialogState extends State<GatewayConnectDialog> {
|
||||
const SizedBox(height: AppSpacing.section),
|
||||
Text(
|
||||
appText(
|
||||
'通过配置码或手动 Host / TLS 将 XWorkmate 连接到 OpenClaw Gateway。',
|
||||
'Connect XWorkmate to an OpenClaw gateway with setup code or manual host / TLS.',
|
||||
'通过配置码或手动 Host / TLS 将 XWorkmate 连接到 OpenClaw Gateway。也可切换到仅 AI Gateway 模式,仅使用模型路由而不建立 Gateway 会话。',
|
||||
'Connect XWorkmate to an OpenClaw gateway with setup code or manual host / TLS. You can also switch to AI Gateway Only mode to use model routing without opening a gateway session.',
|
||||
),
|
||||
style: supportingCopyStyle,
|
||||
),
|
||||
@ -159,13 +188,13 @@ class _GatewayConnectDialogState extends State<GatewayConnectDialog> {
|
||||
DropdownButtonFormField<RuntimeConnectionMode>(
|
||||
initialValue: _connectionMode,
|
||||
decoration: InputDecoration(
|
||||
labelText: appText('连接模式', 'Connection Mode'),
|
||||
labelText: appText('工作模式', 'Work Mode'),
|
||||
),
|
||||
items: RuntimeConnectionMode.values
|
||||
.map(
|
||||
(mode) => DropdownMenuItem<RuntimeConnectionMode>(
|
||||
value: mode,
|
||||
child: Text(mode.label),
|
||||
child: Text(_connectionModeLabel(mode)),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
@ -183,9 +212,24 @@ class _GatewayConnectDialogState extends State<GatewayConnectDialog> {
|
||||
});
|
||||
},
|
||||
),
|
||||
if (_isAiGatewayOnlyMode) ...[
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
appText(
|
||||
'当前模式仅通过 AI Gateway 处理任务,不会建立 OpenClaw Gateway 会话。',
|
||||
'This mode routes tasks through AI Gateway only and does not establish an OpenClaw Gateway session.',
|
||||
),
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
fontSize: 12,
|
||||
height: 16 / 12,
|
||||
color: palette.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
controller: _hostController,
|
||||
enabled: _manualGatewayFieldsEnabled,
|
||||
decoration: InputDecoration(labelText: appText('主机', 'Host')),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
@ -196,6 +240,7 @@ class _GatewayConnectDialogState extends State<GatewayConnectDialog> {
|
||||
flex: 3,
|
||||
child: TextField(
|
||||
controller: _portController,
|
||||
enabled: _manualGatewayFieldsEnabled,
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: InputDecoration(
|
||||
labelText: appText('端口', 'Port'),
|
||||
@ -208,8 +253,12 @@ class _GatewayConnectDialogState extends State<GatewayConnectDialog> {
|
||||
child: _TlsToggleCard(
|
||||
value: _tls,
|
||||
label: appText('TLS', 'TLS'),
|
||||
enabled: _connectionMode != RuntimeConnectionMode.local,
|
||||
onChanged: _connectionMode == RuntimeConnectionMode.local
|
||||
enabled:
|
||||
_manualGatewayFieldsEnabled &&
|
||||
_connectionMode != RuntimeConnectionMode.local,
|
||||
onChanged:
|
||||
!_manualGatewayFieldsEnabled ||
|
||||
_connectionMode == RuntimeConnectionMode.local
|
||||
? null
|
||||
: (value) => setState(() => _tls = value),
|
||||
),
|
||||
@ -222,6 +271,7 @@ class _GatewayConnectDialogState extends State<GatewayConnectDialog> {
|
||||
const SizedBox(height: 8),
|
||||
TextField(
|
||||
controller: _tokenController,
|
||||
enabled: _credentialFieldsEnabled,
|
||||
obscureText: _obscureSharedToken,
|
||||
enableSuggestions: false,
|
||||
autocorrect: false,
|
||||
@ -235,9 +285,11 @@ class _GatewayConnectDialogState extends State<GatewayConnectDialog> {
|
||||
tooltip: _obscureSharedToken
|
||||
? appText('显示 Token', 'Show token')
|
||||
: appText('隐藏 Token', 'Hide token'),
|
||||
onPressed: () => setState(
|
||||
() => _obscureSharedToken = !_obscureSharedToken,
|
||||
),
|
||||
onPressed: !_credentialFieldsEnabled
|
||||
? null
|
||||
: () => setState(
|
||||
() => _obscureSharedToken = !_obscureSharedToken,
|
||||
),
|
||||
icon: Icon(
|
||||
_obscureSharedToken
|
||||
? Icons.visibility_off_rounded
|
||||
@ -247,7 +299,7 @@ class _GatewayConnectDialogState extends State<GatewayConnectDialog> {
|
||||
),
|
||||
onChanged: (_) => setState(() {}),
|
||||
),
|
||||
if (willUseStoredGatewayToken || typedGatewayToken.isNotEmpty) ...[
|
||||
if (showSharedTokenStatusCard) ...[
|
||||
const SizedBox(height: 10),
|
||||
_SharedTokenStatusCard(
|
||||
hasStoredGatewayToken: hasStoredGatewayToken,
|
||||
@ -268,6 +320,7 @@ class _GatewayConnectDialogState extends State<GatewayConnectDialog> {
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
controller: _passwordController,
|
||||
enabled: _credentialFieldsEnabled,
|
||||
obscureText: true,
|
||||
decoration: InputDecoration(
|
||||
labelText: appText('密码', 'Password'),
|
||||
@ -300,8 +353,12 @@ class _GatewayConnectDialogState extends State<GatewayConnectDialog> {
|
||||
icon: const Icon(Icons.wifi_tethering_rounded),
|
||||
label: Text(
|
||||
_submitting
|
||||
? appText('连接中…', 'Connecting…')
|
||||
: appText('连接', 'Connect'),
|
||||
? (_isAiGatewayOnlyMode
|
||||
? appText('应用中…', 'Applying…')
|
||||
: appText('连接中…', 'Connecting…'))
|
||||
: (_isAiGatewayOnlyMode
|
||||
? appText('应用模式', 'Apply Mode')
|
||||
: appText('连接', 'Connect')),
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -342,7 +399,9 @@ class _GatewayConnectDialogState extends State<GatewayConnectDialog> {
|
||||
profile.port == defaults.port;
|
||||
setState(() {
|
||||
if (shouldPrefillEndpoint) {
|
||||
_connectionMode = preferred.mode;
|
||||
if (_connectionMode != RuntimeConnectionMode.unconfigured) {
|
||||
_connectionMode = preferred.mode;
|
||||
}
|
||||
_hostController.text = preferred.host;
|
||||
_portController.text = '${preferred.port}';
|
||||
_tls = preferred.tls;
|
||||
@ -368,6 +427,33 @@ class _GatewayConnectDialogState extends State<GatewayConnectDialog> {
|
||||
token: resolvedToken,
|
||||
password: _passwordController.text,
|
||||
);
|
||||
} else if (_connectionMode == RuntimeConnectionMode.unconfigured) {
|
||||
final currentSettings = widget.controller.settings;
|
||||
final currentProfile = currentSettings.gateway;
|
||||
final resolvedHost = _hostController.text.trim().isEmpty
|
||||
? currentProfile.host
|
||||
: _hostController.text.trim();
|
||||
final resolvedPort =
|
||||
int.tryParse(_portController.text.trim()) ?? currentProfile.port;
|
||||
final nextProfile = currentProfile.copyWith(
|
||||
mode: RuntimeConnectionMode.unconfigured,
|
||||
useSetupCode: false,
|
||||
setupCode: '',
|
||||
host: resolvedHost,
|
||||
port: resolvedPort <= 0 ? currentProfile.port : resolvedPort,
|
||||
tls: _tls,
|
||||
);
|
||||
await widget.controller.saveSettings(
|
||||
currentSettings.copyWith(
|
||||
gateway: nextProfile,
|
||||
assistantExecutionTarget: AssistantExecutionTarget.aiGatewayOnly,
|
||||
),
|
||||
refreshAfterSave: false,
|
||||
);
|
||||
if (widget.controller.connection.status ==
|
||||
RuntimeConnectionStatus.connected) {
|
||||
await widget.controller.disconnectGateway();
|
||||
}
|
||||
} else {
|
||||
await widget.controller.connectManual(
|
||||
host: _hostController.text,
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:xworkmate/features/assistant/assistant_page.dart';
|
||||
import 'package:xworkmate/runtime/runtime_models.dart';
|
||||
|
||||
import '../test_support.dart';
|
||||
|
||||
@ -174,16 +175,27 @@ void main() {
|
||||
child: AssistantPage(controller: controller, onOpenDetail: (_) {}),
|
||||
);
|
||||
|
||||
expect(find.textContaining('Claw'), findsNothing);
|
||||
expect(find.text('幻灯片'), findsNothing);
|
||||
expect(find.text('视频生成'), findsNothing);
|
||||
expect(find.text('深度研究'), findsNothing);
|
||||
expect(find.text('自动化'), findsNothing);
|
||||
expect(find.textContaining('输入需求、补充上下文、继续追问'), findsOneWidget);
|
||||
expect(find.byKey(const Key('assistant-attachment-menu-button')), findsOneWidget);
|
||||
expect(find.byKey(const Key('assistant-execution-target-button')), findsOneWidget);
|
||||
expect(find.byKey(const Key('assistant-skill-picker-button')), findsOneWidget);
|
||||
expect(find.byKey(const Key('assistant-permission-button')), findsOneWidget);
|
||||
expect(
|
||||
find.byKey(const Key('assistant-attachment-menu-button')),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(
|
||||
find.byKey(const Key('assistant-execution-target-button')),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(
|
||||
find.byKey(const Key('assistant-skill-picker-button')),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(
|
||||
find.byKey(const Key('assistant-permission-button')),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(find.byKey(const Key('assistant-model-button')), findsOneWidget);
|
||||
expect(find.byKey(const Key('assistant-thinking-button')), findsOneWidget);
|
||||
expect(find.byTooltip('模式'), findsNothing);
|
||||
@ -199,11 +211,22 @@ void main() {
|
||||
await tester.tapAt(const Offset(24, 24));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.byKey(const Key('assistant-execution-target-button')));
|
||||
await tester.tap(
|
||||
find.byKey(const Key('assistant-execution-target-button')),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('本地'), findsWidgets);
|
||||
expect(find.text('远程'), findsOneWidget);
|
||||
expect(find.text('仅 AI Gateway'), findsOneWidget);
|
||||
expect(find.text('本地 OpenClaw Gateway'), findsWidgets);
|
||||
expect(find.text('远程 OpenClaw Gateway'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.text('仅 AI Gateway').last);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(
|
||||
controller.assistantExecutionTarget,
|
||||
AssistantExecutionTarget.aiGatewayOnly,
|
||||
);
|
||||
|
||||
await tester.tapAt(const Offset(24, 24));
|
||||
await tester.pumpAndSettle();
|
||||
@ -214,8 +237,14 @@ void main() {
|
||||
await tester.tap(find.byKey(const Key('assistant-skill-picker-button')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byKey(const Key('assistant-skill-picker-dialog')), findsOneWidget);
|
||||
expect(find.byKey(const Key('assistant-skill-picker-search')), findsOneWidget);
|
||||
expect(
|
||||
find.byKey(const Key('assistant-skill-picker-dialog')),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(
|
||||
find.byKey(const Key('assistant-skill-picker-search')),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(find.text('1password'), findsOneWidget);
|
||||
expect(find.text('xlsx'), findsOneWidget);
|
||||
expect(find.text('网页处理'), findsOneWidget);
|
||||
|
||||
@ -17,6 +17,7 @@ class _FakeGatewayRuntime extends GatewayRuntime {
|
||||
|
||||
final List<GatewayConnectionProfile> connectedProfiles =
|
||||
<GatewayConnectionProfile>[];
|
||||
int disconnectCount = 0;
|
||||
GatewayConnectionSnapshot _snapshot = GatewayConnectionSnapshot.initial();
|
||||
|
||||
@override
|
||||
@ -35,18 +36,18 @@ class _FakeGatewayRuntime extends GatewayRuntime {
|
||||
String authPasswordOverride = '',
|
||||
}) async {
|
||||
connectedProfiles.add(profile);
|
||||
_snapshot =
|
||||
GatewayConnectionSnapshot.initial(mode: profile.mode).copyWith(
|
||||
status: RuntimeConnectionStatus.connected,
|
||||
statusText: 'Connected',
|
||||
remoteAddress: '${profile.host}:${profile.port}',
|
||||
connectAuthMode: 'none',
|
||||
);
|
||||
_snapshot = GatewayConnectionSnapshot.initial(mode: profile.mode).copyWith(
|
||||
status: RuntimeConnectionStatus.connected,
|
||||
statusText: 'Connected',
|
||||
remoteAddress: '${profile.host}:${profile.port}',
|
||||
connectAuthMode: 'none',
|
||||
);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> disconnect({bool clearDesiredProfile = true}) async {
|
||||
disconnectCount += 1;
|
||||
_snapshot = _snapshot.copyWith(
|
||||
status: RuntimeConnectionStatus.offline,
|
||||
statusText: 'Offline',
|
||||
@ -65,10 +66,7 @@ class _FakeGatewayRuntime extends GatewayRuntime {
|
||||
case 'status':
|
||||
return <String, dynamic>{'ok': true};
|
||||
case 'agents.list':
|
||||
return <String, dynamic>{
|
||||
'agents': const <Object>[],
|
||||
'mainKey': 'main',
|
||||
};
|
||||
return <String, dynamic>{'agents': const <Object>[], 'mainKey': 'main'};
|
||||
case 'sessions.list':
|
||||
return <String, dynamic>{'sessions': const <Object>[]};
|
||||
case 'chat.history':
|
||||
@ -200,6 +198,53 @@ void main() {
|
||||
);
|
||||
expect(controller.settings.gateway.port, 9443);
|
||||
expect(controller.settings.gateway.mode, RuntimeConnectionMode.remote);
|
||||
|
||||
await controller.setAssistantExecutionTarget(
|
||||
AssistantExecutionTarget.aiGatewayOnly,
|
||||
);
|
||||
|
||||
expect(
|
||||
controller.settings.assistantExecutionTarget,
|
||||
AssistantExecutionTarget.aiGatewayOnly,
|
||||
);
|
||||
expect(
|
||||
controller.settings.gateway.mode,
|
||||
RuntimeConnectionMode.unconfigured,
|
||||
);
|
||||
expect(controller.settings.gateway.useSetupCode, isFalse);
|
||||
expect(controller.settings.gateway.setupCode, isEmpty);
|
||||
expect(
|
||||
controller.settings.gateway.host,
|
||||
'gateway.example.com',
|
||||
reason:
|
||||
'AI Gateway-only mode should preserve the saved remote endpoint.',
|
||||
);
|
||||
expect(controller.settings.gateway.port, 9443);
|
||||
expect(controller.settings.gateway.tls, isTrue);
|
||||
expect(gateway.disconnectCount, 1);
|
||||
expect(
|
||||
gateway.connectedProfiles,
|
||||
hasLength(2),
|
||||
reason: 'AI Gateway-only mode should not open another gateway session.',
|
||||
);
|
||||
|
||||
await controller.setAssistantExecutionTarget(
|
||||
AssistantExecutionTarget.remote,
|
||||
);
|
||||
|
||||
expect(
|
||||
gateway.connectedProfiles.last,
|
||||
isA<GatewayConnectionProfile>()
|
||||
.having((item) => item.mode, 'mode', RuntimeConnectionMode.remote)
|
||||
.having((item) => item.host, 'host', 'gateway.example.com')
|
||||
.having((item) => item.port, 'port', 9443)
|
||||
.having((item) => item.tls, 'tls', isTrue)
|
||||
.having(
|
||||
(item) => item.selectedAgentId,
|
||||
'selectedAgentId',
|
||||
'assistant-main',
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:xworkmate/runtime/runtime_models.dart';
|
||||
import 'package:xworkmate/widgets/gateway_connect_dialog.dart';
|
||||
|
||||
import '../test_support.dart';
|
||||
@ -20,7 +22,7 @@ void main() {
|
||||
await tester.tap(find.text('手动配置'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('连接模式'), findsOneWidget);
|
||||
expect(find.text('工作模式'), findsOneWidget);
|
||||
expect(find.text('主机'), findsOneWidget);
|
||||
expect(find.text('端口'), findsOneWidget);
|
||||
expect(find.text('TLS'), findsOneWidget);
|
||||
@ -28,6 +30,34 @@ void main() {
|
||||
expect(find.text('认证诊断'), findsOneWidget);
|
||||
expect(find.textContaining('fields: none'), findsOneWidget);
|
||||
expect(find.textContaining('开发预填 token'), findsNothing);
|
||||
|
||||
await tester.tap(
|
||||
find.byType(DropdownButtonFormField<RuntimeConnectionMode>),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('仅 AI Gateway'), findsWidgets);
|
||||
expect(find.text('本地 OpenClaw Gateway'), findsWidgets);
|
||||
expect(find.text('远程 OpenClaw Gateway'), findsWidgets);
|
||||
|
||||
await tester.tap(find.text('仅 AI Gateway').last);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('应用模式'), findsOneWidget);
|
||||
expect(
|
||||
find.text('当前模式仅通过 AI Gateway 处理任务,不会建立 OpenClaw Gateway 会话。'),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(_textFieldByLabel(tester, '主机').enabled, isFalse);
|
||||
expect(_textFieldByLabel(tester, '端口').enabled, isFalse);
|
||||
expect(_textFieldByLabel(tester, '共享 Token').enabled, isFalse);
|
||||
expect(_textFieldByLabel(tester, '密码').enabled, isFalse);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
TextField _textFieldByLabel(WidgetTester tester, String label) {
|
||||
return tester
|
||||
.widgetList<TextField>(find.byType(TextField))
|
||||
.firstWhere((field) => field.decoration?.labelText == label);
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user