From 23d75d45fa119d873dfb2ea9f99b51f8210da156 Mon Sep 17 00:00:00 2001 From: Haitao Pan Date: Fri, 13 Mar 2026 21:19:43 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E9=87=8D=E6=9E=84=E4=BE=A7=E8=BE=B9?= =?UTF-8?q?=E6=A0=8F=E5=AF=BC=E8=88=AA=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 更新 WorkspaceDestination 枚举,添加 skills/nodes/agents/clawHub/aiGateway - 调整侧边栏顺序:助手→任务→技能→节点→代理 | ClawHub→密钥→AI Gateway - 新建 SkillsPage 展示已安装技能列表 - ModulesPage 添加 initialTab 参数支持直接跳转 - 修复 ai_gateway_page 和 claw_hub_page 的编译错误 - 更新测试文件适配新的导航结构 --- lib/app/app_shell.dart | 33 ++-- lib/features/ai_gateway/ai_gateway_page.dart | 40 ++-- lib/features/claw_hub/claw_hub_page.dart | 22 ++- lib/features/modules/modules_page.dart | 14 +- lib/features/skills/skills_page.dart | 191 +++++++++++++++++++ lib/models/app_models.dart | 36 +++- lib/widgets/sidebar_navigation.dart | 18 +- test/features/modules_page_test.dart | 2 +- 8 files changed, 302 insertions(+), 54 deletions(-) create mode 100644 lib/features/skills/skills_page.dart diff --git a/lib/app/app_shell.dart b/lib/app/app_shell.dart index d92a7b21..ce9ea36b 100644 --- a/lib/app/app_shell.dart +++ b/lib/app/app_shell.dart @@ -1,14 +1,15 @@ import 'package:flutter/material.dart'; -import '../features/account/account_page.dart'; -import '../features/ai_gateway/ai_gateway_page.dart'; -import '../features/assistant/assistant_page.dart'; -import '../features/claw_hub/claw_hub_page.dart'; -import '../features/mobile/ios_mobile_shell.dart'; -import '../features/modules/modules_page.dart'; -import '../features/secrets/secrets_page.dart'; -import '../features/settings/settings_page.dart'; -import '../features/tasks/tasks_page.dart'; + import '../features/account/account_page.dart'; + import '../features/ai_gateway/ai_gateway_page.dart'; + import '../features/assistant/assistant_page.dart'; + import '../features/claw_hub/claw_hub_page.dart'; + import '../features/mobile/ios_mobile_shell.dart'; + import '../features/modules/modules_page.dart'; + import '../features/secrets/secrets_page.dart'; + import '../features/settings/settings_page.dart'; + import '../features/skills/skills_page.dart'; + import '../features/tasks/tasks_page.dart'; import '../i18n/app_language.dart'; import '../models/app_models.dart'; import '../theme/app_palette.dart'; @@ -33,7 +34,7 @@ class _AppShellState extends State { static const _mobileDestinations = [ WorkspaceDestination.assistant, WorkspaceDestination.tasks, - WorkspaceDestination.modules, + WorkspaceDestination.skills, WorkspaceDestination.secrets, WorkspaceDestination.settings, ]; @@ -325,10 +326,20 @@ class _AppShellState extends State { controller: widget.controller, onOpenDetail: onOpenDetail, ), - WorkspaceDestination.modules => ModulesPage( + WorkspaceDestination.skills => SkillsPage( controller: widget.controller, onOpenDetail: onOpenDetail, ), + WorkspaceDestination.nodes => ModulesPage( + controller: widget.controller, + onOpenDetail: onOpenDetail, + initialTab: ModulesTab.nodes, + ), + WorkspaceDestination.agents => ModulesPage( + controller: widget.controller, + onOpenDetail: onOpenDetail, + initialTab: ModulesTab.agents, + ), WorkspaceDestination.clawHub => ClawHubPage( controller: widget.controller, onOpenDetail: onOpenDetail, diff --git a/lib/features/ai_gateway/ai_gateway_page.dart b/lib/features/ai_gateway/ai_gateway_page.dart index 9ec09066..312691db 100644 --- a/lib/features/ai_gateway/ai_gateway_page.dart +++ b/lib/features/ai_gateway/ai_gateway_page.dart @@ -1,13 +1,16 @@ -import 'package:flutter/material.dart'; - -import '../../app/app_controller.dart'; -import '../../i18n/app_language.dart'; -import '../../models/app_models.dart'; -import '../../widgets/metric_card.dart'; -import '../../widgets/section_header.dart'; -import '../../widgets/section_tabs.dart'; -import '../../widgets/surface_card.dart'; -import '../../widgets/top_bar.dart'; + import 'package:flutter/material.dart'; + + import '../../app/app_controller.dart'; + import '../../i18n/app_language.dart'; + import '../../models/app_models.dart'; + import '../../runtime/runtime_models.dart'; + import '../../theme/app_palette.dart'; + import '../../theme/app_theme.dart'; + import '../../widgets/metric_card.dart'; + import '../../widgets/section_header.dart'; + import '../../widgets/section_tabs.dart'; + import '../../widgets/surface_card.dart'; + import '../../widgets/top_bar.dart'; class AiGatewayPage extends StatefulWidget { const AiGatewayPage({ @@ -77,11 +80,12 @@ class _AiGatewayPageState extends State { children: metrics.map((m) => MetricCard(metric: m)).toList(), ), const SizedBox(height: 24), - SectionTabs( - tabs: AiGatewayTab.values, - selected: _tab, - onSelect: (t) => setState(() => _tab = t), - labelFor: (t) => t.label, + SectionTabs( + items: AiGatewayTab.values.map((t) => t.label).toList(), + value: _tab.label, + onChanged: (label) => setState( + () => _tab = AiGatewayTab.values.firstWhere((t) => t.label == label), + ), ), const SizedBox(height: 16), _buildTabContent(context, _tab, controller), @@ -201,7 +205,7 @@ class _AiGatewayPageState extends State { children: [ Row( children: [ - Icon(Icons.endpoint_rounded, color: palette.accent, size: 20), + Icon(Icons.device_hub_rounded, color: palette.accent, size: 20), const SizedBox(width: 8), Text( appText('端点配置', 'Endpoint Configuration'), @@ -238,7 +242,7 @@ class _AiGatewayPageState extends State { return switch (status) { RuntimeConnectionStatus.connected => const StatusInfo('Connected', StatusTone.success), RuntimeConnectionStatus.connecting => const StatusInfo('Connecting', StatusTone.accent), - RuntimeConnectionStatus.disconnected => const StatusInfo('Disconnected', StatusTone.neutral), + RuntimeConnectionStatus.offline => const StatusInfo('Offline', StatusTone.neutral), RuntimeConnectionStatus.error => const StatusInfo('Error', StatusTone.danger), }; } @@ -362,7 +366,7 @@ class _EndpointCard extends StatelessWidget { child: ListTile( onTap: onTap, leading: Icon( - Icons.endpoint_rounded, + Icons.device_hub_rounded, color: isConnected ? palette.accent : palette.textMuted, ), title: Text(name, style: TextStyle(color: palette.textPrimary)), diff --git a/lib/features/claw_hub/claw_hub_page.dart b/lib/features/claw_hub/claw_hub_page.dart index ac18606f..26960a35 100644 --- a/lib/features/claw_hub/claw_hub_page.dart +++ b/lib/features/claw_hub/claw_hub_page.dart @@ -1,11 +1,13 @@ -import 'package:flutter/material.dart'; - -import '../../app/app_controller.dart'; -import '../../i18n/app_language.dart'; -import '../../models/app_models.dart'; -import '../../widgets/section_header.dart'; -import '../../widgets/surface_card.dart'; -import '../../widgets/top_bar.dart'; + import 'package:flutter/material.dart'; + + import '../../app/app_controller.dart'; + import '../../i18n/app_language.dart'; + import '../../models/app_models.dart'; + import '../../theme/app_palette.dart'; + import '../../theme/app_theme.dart'; + import '../../widgets/section_header.dart'; + import '../../widgets/surface_card.dart'; + import '../../widgets/top_bar.dart'; class ClawHubPage extends StatefulWidget { const ClawHubPage({ @@ -191,7 +193,7 @@ class _ClawHubPageState extends State { const SizedBox(height: 24), SectionHeader( title: appText('终端', 'Terminal'), - icon: Icons.terminal_rounded, + subtitle: appText('执行终端命令', 'Execute terminal commands'), ), const SizedBox(height: 12), SurfaceCard( @@ -327,7 +329,7 @@ class _ClawHubPageState extends State { const SizedBox(height: 24), SectionHeader( title: appText('快速操作', 'Quick Actions'), - icon: Icons.bolt_rounded, + subtitle: appText('常用操作快捷入口', 'Quick access to common actions'), ), const SizedBox(height: 12), Wrap( diff --git a/lib/features/modules/modules_page.dart b/lib/features/modules/modules_page.dart index d11a76da..859bcc33 100644 --- a/lib/features/modules/modules_page.dart +++ b/lib/features/modules/modules_page.dart @@ -13,23 +13,33 @@ import '../../widgets/status_badge.dart'; import '../../widgets/surface_card.dart'; import '../../widgets/top_bar.dart'; -class ModulesPage extends StatefulWidget { + class ModulesPage extends StatefulWidget { const ModulesPage({ super.key, required this.controller, required this.onOpenDetail, + this.initialTab, }); final AppController controller; final ValueChanged onOpenDetail; + final ModulesTab? initialTab; @override State createState() => _ModulesPageState(); } -class _ModulesPageState extends State { + class _ModulesPageState extends State { ModulesTab _tab = ModulesTab.gateway; + @override + void initState() { + super.initState(); + if (widget.initialTab != null) { + _tab = widget.initialTab!; + } + } + @override Widget build(BuildContext context) { final controller = widget.controller; diff --git a/lib/features/skills/skills_page.dart b/lib/features/skills/skills_page.dart new file mode 100644 index 00000000..26fb3c06 --- /dev/null +++ b/lib/features/skills/skills_page.dart @@ -0,0 +1,191 @@ + import 'package:flutter/material.dart'; + + import '../../app/app_controller.dart'; + import '../../i18n/app_language.dart'; + import '../../models/app_models.dart'; + import '../../runtime/runtime_models.dart'; + import '../../widgets/status_badge.dart'; + import '../../widgets/surface_card.dart'; + import '../../widgets/top_bar.dart'; + +class SkillsPage extends StatelessWidget { + const SkillsPage({ + super.key, + required this.controller, + required this.onOpenDetail, + }); + + final AppController controller; + final ValueChanged onOpenDetail; + + @override + Widget build(BuildContext context) { + final items = controller.skills; + + return AnimatedBuilder( + animation: controller, + builder: (context, _) { + return SingleChildScrollView( + padding: const EdgeInsets.fromLTRB(32, 32, 32, 8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TopBar( + title: appText('技能', 'Skills'), + subtitle: appText( + '管理已安装的技能包,查看技能状态与依赖。', + 'Manage installed skill packages, view status and dependencies.', + ), + trailing: Wrap( + spacing: 12, + runSpacing: 12, + children: [ + SizedBox( + width: 220, + child: TextField( + decoration: InputDecoration( + hintText: appText('搜索技能', 'Search skills'), + prefixIcon: Icon(Icons.search_rounded), + ), + ), + ), + IconButton( + onPressed: () async { + await controller.skillsController.refresh( + agentId: controller.selectedAgentId.isEmpty + ? null + : controller.selectedAgentId, + ); + }, + icon: const Icon(Icons.refresh_rounded), + ), + ], + ), + ), + const SizedBox(height: 24), + if (items.isEmpty) + SurfaceCard( + child: Text( + controller.connection.status == + RuntimeConnectionStatus.connected + ? appText( + '当前网关或代理没有加载技能。', + 'No skills loaded for the active gateway / agent.', + ) + : appText( + '连接 Gateway 后可加载技能。', + 'Connect a gateway to load skills.', + ), + ), + ) + else + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: items + .map( + (skill) => Padding( + padding: const EdgeInsets.only(bottom: 12), + child: SurfaceCard( + onTap: () => onOpenDetail( + DetailPanelData( + title: skill.name, + subtitle: appText('技能', 'Skill'), + icon: Icons.extension_rounded, + status: skill.disabled + ? StatusInfo( + appText('已禁用', 'Disabled'), + StatusTone.warning, + ) + : StatusInfo( + appText('已启用', 'Enabled'), + StatusTone.success, + ), + description: skill.description, + meta: [skill.source, skill.skillKey], + actions: [appText('刷新', 'Refresh')], + sections: [ + DetailSection( + title: appText('依赖要求', 'Requirements'), + items: [ + DetailItem( + label: appText( + '缺失二进制', 'Missing bins'), + value: skill.missingBins.isEmpty + ? appText('无', 'None') + : skill.missingBins.join(', '), + ), + DetailItem( + label: appText( + '缺失环境变量', 'Missing env'), + value: skill.missingEnv.isEmpty + ? appText('无', 'None') + : skill.missingEnv.join(', '), + ), + DetailItem( + label: appText('缺失配置', 'Missing config'), + value: skill.missingConfig.isEmpty + ? appText('无', 'None') + : skill.missingConfig.join(', '), + ), + ], + ), + ], + ), + ), + child: Row( + children: [ + Expanded( + flex: 4, + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + skill.name, + style: Theme.of(context) + .textTheme + .titleMedium, + ), + const SizedBox(height: 6), + Text( + skill.description, + style: + Theme.of(context).textTheme.bodySmall, + ), + ], + ), + ), + Expanded( + flex: 2, + child: StatusBadge( + status: skill.disabled + ? StatusInfo( + appText('已禁用', 'Disabled'), + StatusTone.warning, + ) + : StatusInfo( + appText('已启用', 'Enabled'), + StatusTone.success, + ), + ), + ), + Expanded(flex: 2, child: Text(skill.source)), + Expanded( + flex: 2, + child: Text(skill.primaryEnv ?? 'workspace'), + ), + const Icon(Icons.chevron_right_rounded), + ], + ), + ), + ), + ) + .toList(), + ), + ], + ), + ); + }, + ); + } +} diff --git a/lib/models/app_models.dart b/lib/models/app_models.dart index be9a1e1f..4fdb80e6 100644 --- a/lib/models/app_models.dart +++ b/lib/models/app_models.dart @@ -5,8 +5,12 @@ import '../i18n/app_language.dart'; enum WorkspaceDestination { assistant, tasks, - modules, + skills, + nodes, + agents, + clawHub, secrets, + aiGateway, settings, account, } @@ -15,8 +19,12 @@ extension WorkspaceDestinationCopy on WorkspaceDestination { String get label => switch (this) { WorkspaceDestination.assistant => appText('助手', 'Assistant'), WorkspaceDestination.tasks => appText('任务', 'Tasks'), - WorkspaceDestination.modules => appText('模块', 'Modules'), + WorkspaceDestination.skills => appText('技能', 'Skills'), + WorkspaceDestination.nodes => appText('节点', 'Nodes'), + WorkspaceDestination.agents => appText('代理', 'Agents'), + WorkspaceDestination.clawHub => 'ClawHub', WorkspaceDestination.secrets => appText('密钥', 'Secrets'), + WorkspaceDestination.aiGateway => 'AI Gateway', WorkspaceDestination.settings => appText('设置', 'Settings'), WorkspaceDestination.account => appText('账号', 'Account'), }; @@ -24,8 +32,12 @@ extension WorkspaceDestinationCopy on WorkspaceDestination { IconData get icon => switch (this) { WorkspaceDestination.assistant => Icons.chat_bubble_outline_rounded, WorkspaceDestination.tasks => Icons.layers_rounded, - WorkspaceDestination.modules => Icons.extension_rounded, + WorkspaceDestination.skills => Icons.auto_awesome_rounded, + WorkspaceDestination.nodes => Icons.developer_board_rounded, + WorkspaceDestination.agents => Icons.hub_rounded, + WorkspaceDestination.clawHub => Icons.extension_rounded, WorkspaceDestination.secrets => Icons.key_rounded, + WorkspaceDestination.aiGateway => Icons.smart_toy_rounded, WorkspaceDestination.settings => Icons.tune_rounded, WorkspaceDestination.account => Icons.account_circle_rounded, }; @@ -39,9 +51,21 @@ extension WorkspaceDestinationCopy on WorkspaceDestination { '任务队列、运行态、失败项和调度历史的统一视图。', 'Unified view for queue, running, failed, and history.', ), - WorkspaceDestination.modules => appText( - '平台能力中心,管理 Gateway、Nodes、Agents、Skills 与 Connectors。', - 'Capability center for gateway, nodes, agents, skills, and connectors.', + WorkspaceDestination.skills => appText( + '管理技能包与能力扩展,浏览和安装 ClawHub 技能。', + 'Manage skill packages and extensions, browse and install from ClawHub.', + ), + WorkspaceDestination.nodes => appText( + '管理边缘节点与实例,监控运行状态与负载。', + 'Manage edge nodes and instances, monitor status and load.', + ), + WorkspaceDestination.agents => appText( + '管理代理实例,配置行为与能力。', + 'Manage agent instances, configure behaviors and capabilities.', + ), + WorkspaceDestination.clawHub => appText( + '浏览和安装技能包、代理模板与连接器。', + 'Browse and install skill packages, agent templates and connectors.', ), WorkspaceDestination.secrets => appText( 'Vault 密码保险箱,安全存储密钥、凭证与审计信息。', diff --git a/lib/widgets/sidebar_navigation.dart b/lib/widgets/sidebar_navigation.dart index ddd2f8c6..9f1891f5 100644 --- a/lib/widgets/sidebar_navigation.dart +++ b/lib/widgets/sidebar_navigation.dart @@ -40,7 +40,9 @@ class SidebarNavigation extends StatelessWidget { static const _mainSections = [ WorkspaceDestination.assistant, WorkspaceDestination.tasks, - WorkspaceDestination.modules, + WorkspaceDestination.skills, + WorkspaceDestination.nodes, + WorkspaceDestination.agents, WorkspaceDestination.clawHub, WorkspaceDestination.secrets, WorkspaceDestination.aiGateway, @@ -253,9 +255,11 @@ class _SidebarNavItemState extends State { return switch (section) { WorkspaceDestination.assistant => Icons.auto_awesome_rounded, WorkspaceDestination.tasks => Icons.task_alt_rounded, - WorkspaceDestination.modules => Icons.extension_rounded, - WorkspaceDestination.clawHub => Icons.hub_rounded, - WorkspaceDestination.secrets => Icons.security_rounded, + WorkspaceDestination.skills => Icons.auto_awesome_rounded, + WorkspaceDestination.nodes => Icons.developer_board_rounded, + WorkspaceDestination.agents => Icons.hub_rounded, + WorkspaceDestination.clawHub => Icons.extension_rounded, + WorkspaceDestination.secrets => Icons.key_rounded, WorkspaceDestination.aiGateway => Icons.smart_toy_rounded, WorkspaceDestination.settings => Icons.tune_rounded, WorkspaceDestination.account => Icons.account_circle_rounded, @@ -266,9 +270,11 @@ class _SidebarNavItemState extends State { return switch (section) { WorkspaceDestination.assistant => appText('助手', 'Assistant'), WorkspaceDestination.tasks => appText('任务', 'Tasks'), - WorkspaceDestination.modules => appText('模块', 'Modules'), + WorkspaceDestination.skills => appText('技能', 'Skills'), + WorkspaceDestination.nodes => appText('节点', 'Nodes'), + WorkspaceDestination.agents => appText('代理', 'Agents'), WorkspaceDestination.clawHub => 'ClawHub', - WorkspaceDestination.secrets => appText('密钥 / Vault', 'Secrets / Vault'), + WorkspaceDestination.secrets => appText('密钥', 'Secrets'), WorkspaceDestination.aiGateway => 'AI Gateway', WorkspaceDestination.settings => appText('设置', 'Settings'), WorkspaceDestination.account => appText('账户', 'Account'), diff --git a/test/features/modules_page_test.dart b/test/features/modules_page_test.dart index 7171b71d..fa221ccd 100644 --- a/test/features/modules_page_test.dart +++ b/test/features/modules_page_test.dart @@ -9,7 +9,7 @@ void main() { 'ModulesPage switches connectors tab and routes module actions to settings', (WidgetTester tester) async { final controller = await createTestController(tester); - controller.navigateTo(WorkspaceDestination.modules); + controller.navigateTo(WorkspaceDestination.skills); await pumpPage( tester,