refactor(ui): eliminate unowned helper sprawl in assistant skill picker
This commit is contained in:
parent
008b97b170
commit
f30b1e2eda
@ -6,7 +6,6 @@ export 'assistant_page_composer_support.dart';
|
||||
export 'assistant_page_tooltip_labels.dart';
|
||||
export 'assistant_page_message_widgets.dart';
|
||||
export 'assistant_page_task_models.dart';
|
||||
export 'assistant_page_composer_skill_models.dart';
|
||||
export 'assistant_page_composer_skill_picker.dart';
|
||||
export 'assistant_page_composer_clipboard.dart';
|
||||
export 'assistant_page_components_core.dart';
|
||||
|
||||
@ -32,7 +32,6 @@ import 'assistant_page_composer_support.dart';
|
||||
import 'assistant_page_tooltip_labels.dart';
|
||||
import 'assistant_page_message_widgets.dart';
|
||||
import 'assistant_page_task_models.dart';
|
||||
import 'assistant_page_composer_skill_models.dart';
|
||||
import 'assistant_page_composer_skill_picker.dart';
|
||||
import 'assistant_page_composer_clipboard.dart';
|
||||
import 'assistant_page_components_core.dart';
|
||||
|
||||
@ -33,7 +33,6 @@ import 'assistant_page_composer_support.dart';
|
||||
import 'assistant_page_tooltip_labels.dart';
|
||||
import 'assistant_page_message_widgets.dart';
|
||||
import 'assistant_page_task_models.dart';
|
||||
import 'assistant_page_composer_skill_models.dart';
|
||||
import 'assistant_page_composer_skill_picker.dart';
|
||||
import 'assistant_page_composer_clipboard.dart';
|
||||
|
||||
|
||||
@ -31,7 +31,6 @@ import 'assistant_page_composer_support.dart';
|
||||
import 'assistant_page_tooltip_labels.dart';
|
||||
import 'assistant_page_message_widgets.dart';
|
||||
import 'assistant_page_task_models.dart';
|
||||
import 'assistant_page_composer_skill_models.dart';
|
||||
import 'assistant_page_composer_skill_picker.dart';
|
||||
import 'assistant_page_composer_clipboard.dart';
|
||||
import 'assistant_page_components_core.dart';
|
||||
|
||||
@ -33,7 +33,6 @@ import 'assistant_page_composer_support.dart';
|
||||
import 'assistant_page_tooltip_labels.dart';
|
||||
import 'assistant_page_message_widgets.dart';
|
||||
import 'assistant_page_task_models.dart';
|
||||
import 'assistant_page_composer_skill_models.dart';
|
||||
import 'assistant_page_composer_skill_picker.dart';
|
||||
import 'assistant_page_components_core.dart';
|
||||
|
||||
|
||||
@ -1,124 +0,0 @@
|
||||
// ignore_for_file: unused_import, unnecessary_import
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:math' as math;
|
||||
import 'package:file_selector/file_selector.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_markdown/flutter_markdown.dart';
|
||||
import 'package:markdown/markdown.dart' as md;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:super_clipboard/super_clipboard.dart';
|
||||
import '../../app/app_controller.dart';
|
||||
import '../../app/app_metadata.dart';
|
||||
import '../../app/ui_feature_manifest.dart';
|
||||
import '../../i18n/app_language.dart';
|
||||
import '../../models/app_models.dart';
|
||||
import '../../runtime/multi_agent_orchestrator.dart';
|
||||
import '../../runtime/runtime_models.dart';
|
||||
import '../../theme/app_palette.dart';
|
||||
import '../../theme/app_theme.dart';
|
||||
import '../../widgets/assistant_focus_panel.dart';
|
||||
import '../../widgets/assistant_artifact_sidebar.dart';
|
||||
import '../../widgets/desktop_workspace_scaffold.dart';
|
||||
import '../../widgets/pane_resize_handle.dart';
|
||||
import '../../widgets/surface_card.dart';
|
||||
import 'assistant_page_main.dart';
|
||||
import 'assistant_page_components.dart';
|
||||
import 'assistant_page_composer_bar.dart';
|
||||
import 'assistant_page_composer_state_helpers.dart';
|
||||
import 'assistant_page_composer_support.dart';
|
||||
import 'assistant_page_tooltip_labels.dart';
|
||||
import 'assistant_page_message_widgets.dart';
|
||||
import 'assistant_page_task_models.dart';
|
||||
import 'assistant_page_composer_skill_picker.dart';
|
||||
import 'assistant_page_composer_clipboard.dart';
|
||||
import 'assistant_page_components_core.dart';
|
||||
|
||||
ComposerSkillOptionInternal skillOptionFromGatewayInternal(
|
||||
GatewaySkillSummary skill,
|
||||
) {
|
||||
final key = skill.skillKey.trim().isEmpty
|
||||
? skill.name.trim().toLowerCase()
|
||||
: skill.skillKey.trim();
|
||||
final label = skill.name.trim().isEmpty ? key : skill.name.trim();
|
||||
final sourceLabel = skill.source.trim().isEmpty ? 'Gateway' : skill.source;
|
||||
final group = skillGroupForSourceInternal(skill.source);
|
||||
final description = skill.description.trim().isEmpty
|
||||
? appText('可在当前任务中调用的技能。', 'Skill available in the current task.')
|
||||
: skill.description.trim();
|
||||
|
||||
return ComposerSkillOptionInternal(
|
||||
key: key,
|
||||
label: label,
|
||||
description: description,
|
||||
sourceLabel: sourceLabel,
|
||||
groupLabel: group.label,
|
||||
groupSortOrder: group.sortOrder,
|
||||
icon: Icons.key_rounded,
|
||||
);
|
||||
}
|
||||
|
||||
ComposerSkillGroupInternal skillGroupForSourceInternal(String source) {
|
||||
final normalized = source.trim().toLowerCase();
|
||||
if (normalized == 'openclaw-workspace') {
|
||||
return const ComposerSkillGroupInternal(
|
||||
label: 'Workspace Skills',
|
||||
sortOrder: 0,
|
||||
);
|
||||
}
|
||||
if (normalized.startsWith('agents-skills-') ||
|
||||
normalized == 'agent' ||
|
||||
normalized.startsWith('agent-') ||
|
||||
normalized.contains('personal')) {
|
||||
return const ComposerSkillGroupInternal(
|
||||
label: 'Agent Skills',
|
||||
sortOrder: 1,
|
||||
);
|
||||
}
|
||||
if (normalized == 'bridge' || normalized == 'gateway') {
|
||||
return const ComposerSkillGroupInternal(
|
||||
label: 'Gateway Skills',
|
||||
sortOrder: 2,
|
||||
);
|
||||
}
|
||||
if (normalized.isEmpty) {
|
||||
return const ComposerSkillGroupInternal(
|
||||
label: 'Gateway Skills',
|
||||
sortOrder: 2,
|
||||
);
|
||||
}
|
||||
return const ComposerSkillGroupInternal(label: 'Other Skills', sortOrder: 3);
|
||||
}
|
||||
|
||||
class ComposerSkillGroupInternal {
|
||||
const ComposerSkillGroupInternal({
|
||||
required this.label,
|
||||
required this.sortOrder,
|
||||
});
|
||||
|
||||
final String label;
|
||||
final int sortOrder;
|
||||
}
|
||||
|
||||
class ComposerSkillOptionInternal {
|
||||
const ComposerSkillOptionInternal({
|
||||
required this.key,
|
||||
required this.label,
|
||||
required this.description,
|
||||
required this.sourceLabel,
|
||||
required this.groupLabel,
|
||||
required this.groupSortOrder,
|
||||
required this.icon,
|
||||
});
|
||||
|
||||
final String key;
|
||||
final String label;
|
||||
final String description;
|
||||
final String sourceLabel;
|
||||
final String groupLabel;
|
||||
final int groupSortOrder;
|
||||
final IconData icon;
|
||||
}
|
||||
@ -33,7 +33,6 @@ import 'assistant_page_composer_support.dart';
|
||||
import 'assistant_page_tooltip_labels.dart';
|
||||
import 'assistant_page_message_widgets.dart';
|
||||
import 'assistant_page_task_models.dart';
|
||||
import 'assistant_page_composer_skill_models.dart';
|
||||
import 'assistant_page_composer_clipboard.dart';
|
||||
import 'assistant_page_components_core.dart';
|
||||
|
||||
@ -349,3 +348,89 @@ class SkillPickerTileInternal extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ComposerSkillOptionInternal skillOptionFromGatewayInternal(
|
||||
GatewaySkillSummary skill,
|
||||
) {
|
||||
final key = skill.skillKey.trim().isEmpty
|
||||
? skill.name.trim().toLowerCase()
|
||||
: skill.skillKey.trim();
|
||||
final label = skill.name.trim().isEmpty ? key : skill.name.trim();
|
||||
final sourceLabel = skill.source.trim().isEmpty ? 'Gateway' : skill.source;
|
||||
final group = skillGroupForSourceInternal(skill.source);
|
||||
final description = skill.description.trim().isEmpty
|
||||
? appText('可在当前任务中调用的技能。', 'Skill available in the current task.')
|
||||
: skill.description.trim();
|
||||
|
||||
return ComposerSkillOptionInternal(
|
||||
key: key,
|
||||
label: label,
|
||||
description: description,
|
||||
sourceLabel: sourceLabel,
|
||||
groupLabel: group.label,
|
||||
groupSortOrder: group.sortOrder,
|
||||
icon: Icons.key_rounded,
|
||||
);
|
||||
}
|
||||
|
||||
ComposerSkillGroupInternal skillGroupForSourceInternal(String source) {
|
||||
final normalized = source.trim().toLowerCase();
|
||||
if (normalized == 'openclaw-workspace') {
|
||||
return const ComposerSkillGroupInternal(
|
||||
label: 'Workspace Skills',
|
||||
sortOrder: 0,
|
||||
);
|
||||
}
|
||||
if (normalized.startsWith('agents-skills-') ||
|
||||
normalized == 'agent' ||
|
||||
normalized.startsWith('agent-') ||
|
||||
normalized.contains('personal')) {
|
||||
return const ComposerSkillGroupInternal(
|
||||
label: 'Agent Skills',
|
||||
sortOrder: 1,
|
||||
);
|
||||
}
|
||||
if (normalized == 'bridge' || normalized == 'gateway') {
|
||||
return const ComposerSkillGroupInternal(
|
||||
label: 'Gateway Skills',
|
||||
sortOrder: 2,
|
||||
);
|
||||
}
|
||||
if (normalized.isEmpty) {
|
||||
return const ComposerSkillGroupInternal(
|
||||
label: 'Gateway Skills',
|
||||
sortOrder: 2,
|
||||
);
|
||||
}
|
||||
return const ComposerSkillGroupInternal(label: 'Other Skills', sortOrder: 3);
|
||||
}
|
||||
|
||||
class ComposerSkillGroupInternal {
|
||||
const ComposerSkillGroupInternal({
|
||||
required this.label,
|
||||
required this.sortOrder,
|
||||
});
|
||||
|
||||
final String label;
|
||||
final int sortOrder;
|
||||
}
|
||||
|
||||
class ComposerSkillOptionInternal {
|
||||
const ComposerSkillOptionInternal({
|
||||
required this.key,
|
||||
required this.label,
|
||||
required this.description,
|
||||
required this.sourceLabel,
|
||||
required this.groupLabel,
|
||||
required this.groupSortOrder,
|
||||
required this.icon,
|
||||
});
|
||||
|
||||
final String key;
|
||||
final String label;
|
||||
final String description;
|
||||
final String sourceLabel;
|
||||
final String groupLabel;
|
||||
final int groupSortOrder;
|
||||
final IconData icon;
|
||||
}
|
||||
|
||||
@ -32,7 +32,6 @@ import 'assistant_page_composer_support.dart';
|
||||
import 'assistant_page_tooltip_labels.dart';
|
||||
import 'assistant_page_message_widgets.dart';
|
||||
import 'assistant_page_task_models.dart';
|
||||
import 'assistant_page_composer_skill_models.dart';
|
||||
import 'assistant_page_composer_skill_picker.dart';
|
||||
import 'assistant_page_composer_clipboard.dart';
|
||||
import 'assistant_page_components_core.dart';
|
||||
|
||||
@ -32,7 +32,6 @@ import 'assistant_page_composer_state_helpers.dart';
|
||||
import 'assistant_page_tooltip_labels.dart';
|
||||
import 'assistant_page_message_widgets.dart';
|
||||
import 'assistant_page_task_models.dart';
|
||||
import 'assistant_page_composer_skill_models.dart';
|
||||
import 'assistant_page_composer_skill_picker.dart';
|
||||
import 'assistant_page_composer_clipboard.dart';
|
||||
import 'assistant_page_components_core.dart';
|
||||
|
||||
@ -32,7 +32,6 @@ import 'assistant_page_composer_support.dart';
|
||||
import 'assistant_page_tooltip_labels.dart';
|
||||
import 'assistant_page_message_widgets.dart';
|
||||
import 'assistant_page_task_models.dart';
|
||||
import 'assistant_page_composer_skill_models.dart';
|
||||
import 'assistant_page_composer_skill_picker.dart';
|
||||
import 'assistant_page_composer_clipboard.dart';
|
||||
import 'assistant_page_components_core.dart';
|
||||
|
||||
@ -32,7 +32,6 @@ import 'assistant_page_composer_state_helpers.dart';
|
||||
import 'assistant_page_composer_support.dart';
|
||||
import 'assistant_page_tooltip_labels.dart';
|
||||
import 'assistant_page_task_models.dart';
|
||||
import 'assistant_page_composer_skill_models.dart';
|
||||
import 'assistant_page_composer_skill_picker.dart';
|
||||
import 'assistant_page_composer_clipboard.dart';
|
||||
import 'assistant_page_components_core.dart';
|
||||
|
||||
@ -32,7 +32,6 @@ import 'assistant_page_composer_support.dart';
|
||||
import 'assistant_page_tooltip_labels.dart';
|
||||
import 'assistant_page_message_widgets.dart';
|
||||
import 'assistant_page_task_models.dart';
|
||||
import 'assistant_page_composer_skill_models.dart';
|
||||
import 'assistant_page_composer_skill_picker.dart';
|
||||
import 'assistant_page_composer_clipboard.dart';
|
||||
import 'assistant_attachment_payloads.dart';
|
||||
|
||||
@ -35,7 +35,6 @@ import 'assistant_page_composer_support.dart';
|
||||
import 'assistant_page_tooltip_labels.dart';
|
||||
import 'assistant_page_message_widgets.dart';
|
||||
import 'assistant_page_task_models.dart';
|
||||
import 'assistant_page_composer_skill_models.dart';
|
||||
import 'assistant_page_composer_skill_picker.dart';
|
||||
import 'assistant_page_composer_clipboard.dart';
|
||||
import 'assistant_page_components_core.dart';
|
||||
|
||||
@ -32,7 +32,6 @@ import 'assistant_page_composer_state_helpers.dart';
|
||||
import 'assistant_page_composer_support.dart';
|
||||
import 'assistant_page_tooltip_labels.dart';
|
||||
import 'assistant_page_message_widgets.dart';
|
||||
import 'assistant_page_composer_skill_models.dart';
|
||||
import 'assistant_page_composer_skill_picker.dart';
|
||||
import 'assistant_page_composer_clipboard.dart';
|
||||
import 'assistant_page_components_core.dart';
|
||||
|
||||
@ -32,7 +32,6 @@ import 'assistant_page_composer_state_helpers.dart';
|
||||
import 'assistant_page_composer_support.dart';
|
||||
import 'assistant_page_message_widgets.dart';
|
||||
import 'assistant_page_task_models.dart';
|
||||
import 'assistant_page_composer_skill_models.dart';
|
||||
import 'assistant_page_composer_skill_picker.dart';
|
||||
import 'assistant_page_composer_clipboard.dart';
|
||||
import 'assistant_page_components_core.dart';
|
||||
|
||||
@ -6,6 +6,7 @@ import 'desktop_client.dart';
|
||||
import 'desktop_input_handler.dart';
|
||||
import '../../app/app_controller.dart';
|
||||
import '../../widgets/surface_card.dart';
|
||||
import '../../i18n/app_language.dart';
|
||||
|
||||
class DesktopView extends StatefulWidget {
|
||||
const DesktopView({
|
||||
@ -141,7 +142,9 @@ class _DesktopViewState extends State<DesktopView> {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Failed to connect remote desktop: $e'),
|
||||
content: Text(
|
||||
appText('连接AI工作空间失败: $e', 'Failed to connect AI Workspace: $e'),
|
||||
),
|
||||
backgroundColor: Colors.redAccent,
|
||||
),
|
||||
);
|
||||
@ -204,10 +207,10 @@ class _DesktopViewState extends State<DesktopView> {
|
||||
),
|
||||
label: Text(
|
||||
_connectionState == 'connected'
|
||||
? '断开连接'
|
||||
? appText('断开连接', 'Disconnect')
|
||||
: (_connectionState == 'connecting'
|
||||
? '正在连接...'
|
||||
: '连接桌面'),
|
||||
? appText('正在连接...', 'Connecting...')
|
||||
: appText('连接AI工作空间', 'Connect AI Workspace')),
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
@ -251,10 +254,10 @@ class _DesktopViewState extends State<DesktopView> {
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
_connectionState == 'connected'
|
||||
? '已连接'
|
||||
? appText('已连接', 'Connected')
|
||||
: (_connectionState == 'connecting'
|
||||
? '连接中'
|
||||
: '未连接'),
|
||||
? appText('连接中', 'Connecting')
|
||||
: appText('已断开', 'Disconnected')),
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
@ -508,8 +511,8 @@ class _DesktopViewState extends State<DesktopView> {
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
_connectionState == 'connecting'
|
||||
? '正在建立 WebRTC 连接,请稍候...'
|
||||
: '未开启远程桌面流。点击“连接桌面”启动视频流。',
|
||||
? appText('正在建立 WebRTC 连接,请稍候...', 'Establishing WebRTC connection, please wait...')
|
||||
: appText('未开启 AI 工作空间流。点击“连接AI工作空间”启动视频流。', 'AI Workspace stream not enabled. Click "Connect AI Workspace" to start the video stream.'),
|
||||
style: TextStyle(
|
||||
color: theme.colorScheme.onSurface
|
||||
.withValues(alpha: 0.6),
|
||||
|
||||
@ -59,7 +59,7 @@ class _SettingsRemoteDesktopPanelState extends State<SettingsRemoteDesktopPanel>
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Text(
|
||||
appText('远程桌面', 'Remote Desktop'),
|
||||
appText('AI工作空间', 'AI Workspace'),
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
|
||||
@ -155,7 +155,7 @@ extension SettingsTabCopy on SettingsTab {
|
||||
String get label => switch (this) {
|
||||
SettingsTab.gateway => appText('集成', 'Integrations'),
|
||||
SettingsTab.archivedTasks => appText('归档任务', 'Archived tasks'),
|
||||
SettingsTab.remoteDesktop => appText('远程桌面', 'Remote Desktop'),
|
||||
SettingsTab.remoteDesktop => appText('AI工作空间', 'AI Workspace'),
|
||||
SettingsTab.logs => appText('运行日志', 'Runtime Logs'),
|
||||
};
|
||||
}
|
||||
|
||||
@ -37,9 +37,6 @@ class SkillsController extends ChangeNotifier {
|
||||
errorInternal = null;
|
||||
notifyListeners();
|
||||
try {
|
||||
await runtimeInternal.ensureBridgeSessionConnected(
|
||||
selectedAgentId: agentId?.trim() ?? '',
|
||||
);
|
||||
itemsInternal = await runtimeInternal.listSkills(agentId: agentId);
|
||||
} catch (error) {
|
||||
errorInternal = error.toString();
|
||||
|
||||
@ -5,7 +5,6 @@ import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:xworkmate/app/app_controller.dart';
|
||||
import 'package:xworkmate/app/ui_feature_manifest.dart';
|
||||
import 'package:xworkmate/features/assistant/assistant_page_composer_clipboard.dart';
|
||||
import 'package:xworkmate/features/assistant/assistant_page_composer_skill_models.dart';
|
||||
import 'package:xworkmate/features/assistant/assistant_page_composer_skill_picker.dart';
|
||||
import 'package:xworkmate/features/assistant/assistant_page_main.dart';
|
||||
import 'package:xworkmate/runtime/runtime_models.dart';
|
||||
|
||||
@ -29,8 +29,8 @@ void main() {
|
||||
);
|
||||
|
||||
// Verify the panel headers and titles
|
||||
expect(find.text('远程桌面'), findsOneWidget);
|
||||
expect(find.text('连接桌面'), findsOneWidget);
|
||||
expect(find.text('AI工作空间'), findsOneWidget);
|
||||
expect(find.text('连接AI工作空间'), findsOneWidget);
|
||||
|
||||
// Verify advanced options are hidden initially
|
||||
expect(find.text('GPU 加速'), findsNothing);
|
||||
|
||||
@ -8,7 +8,7 @@ import 'package:xworkmate/app/app_controller.dart';
|
||||
import 'package:xworkmate/app/app_controller_desktop_external_acp_routing.dart';
|
||||
import 'package:xworkmate/app/app_controller_openclaw_task_queue.dart';
|
||||
import 'package:xworkmate/app/ui_feature_manifest.dart';
|
||||
import 'package:xworkmate/features/assistant/assistant_page_composer_skill_models.dart';
|
||||
import 'package:xworkmate/features/assistant/assistant_page_composer_skill_picker.dart';
|
||||
import 'package:xworkmate/runtime/gateway_acp_client.dart';
|
||||
import 'package:xworkmate/runtime/go_task_service_client.dart';
|
||||
import 'package:xworkmate/runtime/runtime_models.dart';
|
||||
|
||||
@ -11,7 +11,7 @@ import 'package:xworkmate/runtime/secure_config_store.dart';
|
||||
|
||||
void main() {
|
||||
test(
|
||||
'SkillsController lazily connects and loads OpenClaw skills through bridge gateway request',
|
||||
'SkillsController loads OpenClaw skills through bridge request without legacy gateway connect',
|
||||
() async {
|
||||
final observedMethods = <String>[];
|
||||
final observedGatewayRequests = <Map<String, dynamic>>[];
|
||||
@ -23,31 +23,6 @@ void main() {
|
||||
observedMethods.add(method);
|
||||
request.response.headers.contentType = ContentType.json;
|
||||
|
||||
if (method == 'xworkmate.gateway.connect') {
|
||||
request.response.write(
|
||||
jsonEncode(<String, dynamic>{
|
||||
'jsonrpc': '2.0',
|
||||
'id': rpc['id'],
|
||||
'result': <String, dynamic>{
|
||||
'ok': true,
|
||||
'snapshot': <String, dynamic>{
|
||||
'status': 'connected',
|
||||
'mode': 'remote',
|
||||
'statusText': 'Connected',
|
||||
'mainSessionKey': 'main',
|
||||
},
|
||||
'auth': <String, dynamic>{
|
||||
'role': 'operator',
|
||||
'scopes': <String>['operator.read', 'operator.write'],
|
||||
},
|
||||
'returnedDeviceToken': '',
|
||||
},
|
||||
}),
|
||||
);
|
||||
await request.response.close();
|
||||
return;
|
||||
}
|
||||
|
||||
if (method == 'xworkmate.gateway.request') {
|
||||
final params = (rpc['params'] as Map).cast<String, dynamic>();
|
||||
observedGatewayRequests.add(params);
|
||||
@ -159,10 +134,7 @@ void main() {
|
||||
final controller = SkillsController(runtime);
|
||||
await controller.refresh(agentId: 'main');
|
||||
|
||||
expect(observedMethods, <String>[
|
||||
'xworkmate.gateway.connect',
|
||||
'xworkmate.gateway.request',
|
||||
]);
|
||||
expect(observedMethods, const <String>['xworkmate.gateway.request']);
|
||||
expect(observedGatewayRequests.single['method'], 'skills.status');
|
||||
expect(
|
||||
(observedGatewayRequests.single['params'] as Map)['agentId'],
|
||||
@ -176,4 +148,92 @@ void main() {
|
||||
expect(controller.items.first.eligible, isTrue);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'SkillsController keeps bridge skill payload when OpenClaw gateway is offline',
|
||||
() async {
|
||||
final observedGatewayRequests = <Map<String, dynamic>>[];
|
||||
final server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0);
|
||||
final subscription = server.listen((request) async {
|
||||
final body = await utf8.decoder.bind(request).join();
|
||||
final rpc = jsonDecode(body) as Map<String, dynamic>;
|
||||
request.response.headers.contentType = ContentType.json;
|
||||
|
||||
if (rpc['method'] == 'xworkmate.gateway.request') {
|
||||
final params = (rpc['params'] as Map).cast<String, dynamic>();
|
||||
observedGatewayRequests.add(params);
|
||||
request.response.write(
|
||||
jsonEncode(<String, dynamic>{
|
||||
'jsonrpc': '2.0',
|
||||
'id': rpc['id'],
|
||||
'result': <String, dynamic>{
|
||||
'ok': false,
|
||||
'error': <String, dynamic>{
|
||||
'code': 'OFFLINE',
|
||||
'message': 'gateway not connected',
|
||||
},
|
||||
'payload': <String, dynamic>{
|
||||
'skills': <Map<String, dynamic>>[
|
||||
<String, dynamic>{
|
||||
'name': 'PDF Writer',
|
||||
'description': 'Write PDF documents',
|
||||
'source': 'openclaw-workspace',
|
||||
'skillKey': 'pdf',
|
||||
'eligible': false,
|
||||
'disabled': false,
|
||||
'missing': <String, dynamic>{
|
||||
'bins': <String>[],
|
||||
'env': <String>[],
|
||||
'config': <String>[],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
await request.response.close();
|
||||
return;
|
||||
}
|
||||
|
||||
request.response.statusCode = HttpStatus.badRequest;
|
||||
await request.response.close();
|
||||
});
|
||||
|
||||
final tempDir = await Directory.systemTemp.createTemp(
|
||||
'xworkmate-bridge-skills-offline-test-',
|
||||
);
|
||||
final store = SecureConfigStore(
|
||||
enableSecureStorage: false,
|
||||
appDataRootPathResolver: () async => '${tempDir.path}/settings.sqlite3',
|
||||
secretRootPathResolver: () async => tempDir.path,
|
||||
);
|
||||
final acpClient = GatewayAcpClient(
|
||||
endpointResolver: () => Uri.parse('http://127.0.0.1:${server.port}'),
|
||||
authorizationResolver: (_) async => 'bridge-token',
|
||||
);
|
||||
final runtime = GatewayRuntime(
|
||||
store: store,
|
||||
identityStore: DeviceIdentityStore(store),
|
||||
sessionClient: GatewayAcpRuntimeSessionClient(client: acpClient),
|
||||
);
|
||||
await runtime.initialize();
|
||||
addTearDown(() async {
|
||||
runtime.dispose();
|
||||
await subscription.cancel();
|
||||
await server.close(force: true);
|
||||
await tempDir.delete(recursive: true);
|
||||
});
|
||||
|
||||
final controller = SkillsController(runtime);
|
||||
await controller.refresh(agentId: 'main');
|
||||
|
||||
expect(observedGatewayRequests.single['method'], 'skills.status');
|
||||
expect(controller.error, isNull);
|
||||
expect(controller.items.map((item) => item.skillKey), const <String>[
|
||||
'pdf',
|
||||
]);
|
||||
expect(controller.items.single.eligible, isFalse);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user