add-home-widget-selector
为Flutter插件添加可配置的选择器小组件(HomeWidget),支持用户点击配置、数据选择和动态数据渲染。核心特性:(1) 配置dataSelector保存必要数据,(2) 通过controller传递id获取最新数据,(3) 支持导航到详情页
Best use case
add-home-widget-selector is best used when you need a repeatable AI agent workflow instead of a one-off prompt.
为Flutter插件添加可配置的选择器小组件(HomeWidget),支持用户点击配置、数据选择和动态数据渲染。核心特性:(1) 配置dataSelector保存必要数据,(2) 通过controller传递id获取最新数据,(3) 支持导航到详情页
Teams using add-home-widget-selector should expect a more consistent output, faster repeated execution, less prompt rewriting.
When to use this skill
- You want a reusable workflow that can be run more than once with consistent structure.
When not to use this skill
- You only need a quick one-off answer and do not need a reusable workflow.
- You cannot install or maintain the underlying files, dependencies, or repository context.
Installation
Claude Code / Cursor / Codex
Manual Installation
- Download SKILL.md from GitHub
- Place it in
.claude/skills/add-home-widget-selector/SKILL.mdinside your project - Restart your AI agent — it will auto-discover the skill
How add-home-widget-selector Compares
| Feature / Agent | add-home-widget-selector | Standard Approach |
|---|---|---|
| Platform Support | Not specified | Limited / Varies |
| Context Awareness | High | Baseline |
| Installation Complexity | Unknown | N/A |
Frequently Asked Questions
What does this skill do?
为Flutter插件添加可配置的选择器小组件(HomeWidget),支持用户点击配置、数据选择和动态数据渲染。核心特性:(1) 配置dataSelector保存必要数据,(2) 通过controller传递id获取最新数据,(3) 支持导航到详情页
Where can I find the source code?
You can find the source code on GitHub using the link provided at the top of the page.
SKILL.md Source
# Add HomeWidget Selector
为 Flutter 插件添加可配置的选择器类型 HomeWidget,让用户可以在首页添加小组件并自定义选择要显示的数据。
## Usage
```bash
# 基础用法 - 为插件添加选择器小组件
/add-home-widget-selector <plugin-path> --widget-id <widget-id> --selector-id <selector-id>
# 完整参数
/add-home-widget-selector lib/plugins/todo \
--widget-id todo_quick_access \
--selector-id todo.task \
--name "todo_quickAccessWidget".tr \
--icon Icons.check_circle \
--category "home_categoryTask".tr
```
**Examples:**
```bash
# 为待办插件添加任务快捷访问小组件
/add-home-widget-selector lib/plugins/todo \
--widget-id todo_quick_access \
--selector-id todo.task \
--name "todo_quickAccessWidget".tr \
--icon Icons.task_alt \
--category "home_categoryTask".tr
# 为日记插件添加日记入口小组件
/add-home-widget-selector lib/plugins/diary \
--widget-id diary_entry_selector \
--selector-id diary.entry \
--name "diary_quickAccessWidget".tr \
--icon Icons.auto_stories \
--category "home_categoryContent".tr
# 为聊天插件添加会话快捷小组件
/add-home-widget-selector lib/plugins/agent_chat \
--widget-id chat_conversation_selector \
--selector-id chat.conversation \
--name "chat_quickAccessWidget".tr \
--icon Icons.chat_bubble \
--category "home_categoryChat".tr
```
## Arguments
- `<plugin-path>`: 插件根目录路径(包含 `home_widgets.dart` 或需要创建的文件)
- `--widget-id <id>`: 小组件唯一 ID(格式:`plugin_id_widget_name`)
- `--selector-id <id>`: 数据选择器 ID(格式:`plugin_id.selector_name`,需先注册)
- `--name <name>`: 小组件显示名称(国际化键值,如 `"todo_quickAccessWidget".tr`)
- `--icon <icon>`: 小组件图标(格式:`Icons.icon_name`)
- `--category <category>`: 小组件分类(国际化键值)
### 可选参数
- `--size <sizes>`: 支持的尺寸(逗号分隔,默认:`medium,large`)
- `--default-size <size>`: 默认尺寸(默认:`large`)
- `--data-extractor <method>`: 自定义数据提取方法名(默认:`_extractWidgetData`)
- `--data-renderer <method>`: 自定义渲染方法名(默认:`_renderWidgetData`)
- `--navigation <method>`: 自定义导航方法名(默认:`_navigateToDetail`)
## Workflow
### 1. Analyze Plugin Structure
读取插件目录并识别:
- 插件主文件(`[plugin_name]_plugin.dart`)
- 是否已存在 `home_widgets.dart`
- 插件的 controller 类和方法
- 国际化文件位置
- 路由配置文件
### 2. Register Data Selector (如果尚未注册)
在插件主文件中注册数据选择器:
```dart
// 在 [plugin_name]_plugin.dart 中
void _registerDataSelectors() {
pluginDataSelectorService.registerSelector(
SelectorDefinition(
id: '[plugin_id].[selector_name]', // 例如: 'todo.task', 'diary.entry'
pluginId: '[plugin_id]',
name: '[翻译键]'.tr,
selectionMode: SelectionMode.single, // 或 SelectionMode.multiple
steps: [
SelectorStep(
id: 'select_[item]',
title: '[选择步骤标题]'.tr,
viewType: SelectorViewType.list, // 或 .grid, .tree
dataLoader: (previousSelections) async {
// 加载可选数据
final items = await _loadSelectableItems();
return items.map((item) => SelectableItem(
id: item.id,
title: item.title,
subtitle: item.subtitle,
icon: Icons.[icon_name],
rawData: item.toJson(), // 重要:保存完整数据供后续使用
)).toList();
},
isFinalStep: true,
),
],
),
);
}
```
### 3. Create/Update home_widgets.dart
在 `home_widgets.dart` 中注册选择器小组件:
```dart
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:Memento/screens/home_screen/models/home_widget_size.dart';
import 'package:Memento/screens/home_screen/widgets/home_widget.dart';
import 'package:Memento/screens/home_screen/widgets/generic_plugin_widget.dart';
import 'package:Memento/screens/home_screen/widgets/generic_selector_widget.dart';
import 'package:Memento/screens/home_screen/managers/home_widget_registry.dart';
import 'package:Memento/core/plugin_manager.dart';
import 'package:Memento/core/navigation/navigation_helper.dart';
import 'package:Memento/core/services/plugin_data_selector/models/selector_result.dart';
/// [PluginName] 插件的主页小组件注册
class [PluginName]HomeWidgets {
/// 注册所有小组件
static void register(HomeWidgetRegistry registry) {
// 注册选择器小组件
registry.register(
HomeWidget(
id: '[widget_id]', // 例如: 'todo_quick_access'
pluginId: '[plugin_id]',
name: '[小组件名称]'.tr,
description: '[小组件描述]'.tr,
icon: Icons.[icon_name],
color: Colors.[primary_color],
defaultSize: HomeWidgetSize.[default_size],
supportedSizes: [
HomeWidgetSize.[size1],
HomeWidgetSize.[size2],
],
category: '[分类]'.tr,
// === 选择器特定字段 ===
selectorId: '[plugin_id].[selector_name]', // 与选择器注册时的 id 一致
dataRenderer: _[render_method], // 自定义渲染函数
navigationHandler: _[navigate_method], // 导航处理函数
builder: (context, config) {
return GenericSelectorWidget(
widgetDefinition: registry.getWidget('[widget_id]')!,
config: config,
);
},
),
);
}
/// 从选择器数据中提取小组件需要的数据(保存到本地存储)
static Map<String, dynamic> _[extract_method](List<dynamic> dataArray) {
final itemData = dataArray[0] as Map<String, dynamic>;
return {
'id': itemData['id'] as String,
'title': itemData['title'] as String?,
'subtitle': itemData['subtitle'] as String?,
// 只保存必要的数据,避免存储冗余信息
};
}
/// 渲染小组件数据(从 controller 获取最新数据)
static Widget _[render_method](
BuildContext context,
SelectorResult result,
Map<String, dynamic> config,
) {
// 从 result.data 获取已保存的数据(包含 id)
final savedData = result.data is Map<String, dynamic>
? result.data as Map<String, dynamic>
: {};
final itemId = savedData['id'] as String? ?? '';
return FutureBuilder<dynamic>(
// ✅ 关键:通过 controller 传递 id 获取最新数据
future: _loadLatestData(itemId),
builder: (context, snapshot) {
final latestData = snapshot.data ?? savedData;
final title = latestData['title'] ?? savedData['title'] ?? 'Unknown';
final subtitle = latestData['subtitle'] ?? savedData['subtitle'] ?? '';
return _buildWidgetUI(context, title, subtitle, latestData);
},
);
}
/// 从 controller 加载最新数据
static Future<dynamic> _loadLatestData(String itemId) async {
try {
final plugin = PluginManager.instance.getPlugin('[plugin_id]') as [PluginClass]?;
if (plugin == null) return null;
// ✅ 关键:通过 controller 传递 id 获取最新数据
return await plugin.controller.getItemById(itemId);
} catch (e) {
debugPrint('加载最新数据失败: $e');
return null;
}
}
/// 构建小组件 UI
static Widget _buildWidgetUI(
BuildContext context,
String title,
String subtitle,
dynamic data,
) {
final theme = Theme.of(context);
return Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(16),
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: theme.colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(16),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.[icon_name], size: 20),
const SizedBox(width: 8),
Expanded(
child: Text(
title,
style: theme.textTheme.titleMedium,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
),
if (subtitle.isNotEmpty) ...[
const SizedBox(height: 8),
Text(
subtitle,
style: theme.textTheme.bodySmall,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
const Spacer(),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Text(
'home_clickToView'.tr,
style: theme.textTheme.labelSmall?.copyWith(
color: theme.colorScheme.primary,
),
),
const SizedBox(width: 4),
Icon(
Icons.arrow_forward,
size: 16,
color: theme.colorScheme.primary,
),
],
),
],
),
),
),
);
}
/// 导航到详情页
static void _[navigate_method](
BuildContext context,
SelectorResult result,
) {
final data = result.data is Map<String, dynamic>
? result.data as Map<String, dynamic>
: {};
final itemId = data['id'] as String?;
// 使用 navigatorKey.currentContext 确保导航正常工作
final navContext = NavigationHelper.getCurrentContext() ?? context;
NavigationHelper.pushNamed(
navContext,
'/[plugin_id]/[detail_screen]',
arguments: {
'id': itemId,
// 可以传递其他必要参数
},
);
}
}
```
### 4. Update Plugin Registration
在插件主文件的 `initialize()` 方法中调用小组件注册:
```dart
// 在 [plugin_name]_plugin.dart
@override
Future<void> initialize() async {
// ... 其他初始化代码 ...
// 注册选择器(如果还没有)
_registerDataSelectors();
}
@override
Future<void> registerToApp(
PluginManager pluginManager,
ConfigManager configManager,
) async {
// 在应用级别注册小组件
final homeWidgetRegistry = pluginManager.getService<HomeWidgetRegistry>();
if (homeWidgetRegistry != null) {
[PluginName]HomeWidgets.register(homeWidgetRegistry);
}
}
```
### 5. Update Route Configuration
在 `lib/screens/route.dart` 中添加详情页路由:
```dart
// 在 generateRoute 方法中添加
case '/[plugin_id]/[detail_screen]':
case '[plugin_id]/[detail_screen]':
String? id;
if (settings.arguments is Map<String, dynamic>) {
final args = settings.arguments as Map<String, dynamic>;
id = args['id'] as String?;
}
debugPrint('打开详情页: id=$id');
return _createRoute(
[DetailScreen](id: id),
);
```
### 6. Add Internationalization Strings
在插件的国际化文件中添加字符串:
**中文 (zh):**
```dart
'[plugin_id]_quickAccessWidget': '快捷访问',
'[plugin_id]_widgetDescription': '点击选择要显示的项目',
'[plugin_id]_selectTitle': '选择项目',
'[plugin_id]_clickToConfigure': '点击配置',
'[plugin_id]_clickToView': '点击查看详情',
```
**英文 (en):**
```dart
'[plugin_id]_quickAccessWidget': 'Quick Access',
'[plugin_id]_widgetDescription': 'Tap to select an item to display',
'[plugin_id]_selectTitle': 'Select Item',
'[plugin_id]_clickToConfigure': 'Tap to configure',
'[plugin_id]_clickToView': 'Tap to view details',
```
## Key Concepts
### 1. dataSelector - 数据提取函数
`dataSelector` 函数负责从选择器返回的完整数据中提取**必要字段**并保存到本地存储:
```dart
// ✅ 推荐:只保存必要数据
static Map<String, dynamic> _extractWidgetData(List<dynamic> dataArray) {
final itemData = dataArray[0] as Map<String, dynamic>;
return {
'id': itemData['id'] as String, // 必需:用于后续获取最新数据
'title': itemData['title'] as String?,
// 不要保存大型数据(如 content、description 等)
};
}
// ❌ 避免:保存过多数据
static Map<String, dynamic> _extractWidgetData(List<dynamic> dataArray) {
return dataArray[0] as Map<String, dynamic>; // 保存完整数据
}
```
**为什么只保存必要数据?**
- 减少存储空间
- 数据变化时不需要更新小组件配置
- 通过 id 可以随时获取最新数据
### 2. dataRenderer - 动态数据获取
`dataRenderer` 函数必须通过插件的 controller 传递保存的 id 来获取**最新数据**:
```dart
static Widget _renderWidgetData(
BuildContext context,
SelectorResult result,
Map<String, dynamic> config,
) {
final savedData = result.data as Map<String, dynamic>;
final itemId = savedData['id'] as String? ?? '';
return FutureBuilder<dynamic>(
// ✅ 关键:传递 id 获取最新数据
future: _loadLatestData(itemId),
builder: (context, snapshot) {
final data = snapshot.data ?? savedData;
return _buildWidgetUI(context, data);
},
);
}
static Future<dynamic> _loadLatestData(String itemId) async {
final plugin = PluginManager.instance.getPlugin('[plugin_id]') as PluginClass?;
// ✅ 关键:通过 controller 获取最新数据
return await plugin.controller.getItemById(itemId);
}
```
**为什么必须通过 controller 获取最新数据?**
- 保持小组件显示的信息与实际数据一致
- 用户的操作(如修改标题)能即时反映在小组件上
- 遵循单一数据源原则
### 3. navigationHandler - 导航处理
导航函数负责跳转到详情页:
```dart
static void _navigateToDetail(BuildContext context, SelectorResult result) {
final data = result.data as Map<String, dynamic>;
final itemId = data['id'] as String?;
NavigationHelper.pushNamed(
context,
'/[plugin_id]/detail',
arguments: {'id': itemId},
);
}
```
## Complete Example: Todo Plugin
### 1. 注册选择器 (`todo_plugin.dart`)
```dart
void _registerDataSelectors() {
pluginDataSelectorService.registerSelector(
SelectorDefinition(
id: 'todo.task',
pluginId: 'todo',
name: 'todo_selectTask'.tr,
selectionMode: SelectionMode.single,
steps: [
SelectorStep(
id: 'select_task',
title: 'todo_selectTask'.tr,
viewType: SelectorViewType.list,
dataLoader: (previousSelections) async {
final tasks = await taskController.getAllTasks();
return tasks.map((task) => SelectableItem(
id: task.id,
title: task.title,
subtitle: task.description,
icon: task.completed ? Icons.check_circle : Icons.radio_button_unchecked,
rawData: {
'id': task.id,
'title': task.title,
'description': task.description,
'completed': task.completed,
'priority': task.priority.index,
},
)).toList();
},
isFinalStep: true,
),
],
),
);
}
```
### 2. 小组件注册 (`home_widgets.dart`)
```dart
class TodoHomeWidgets {
static void register(HomeWidgetRegistry registry) {
registry.register(
HomeWidget(
id: 'todo_quick_access',
pluginId: 'todo',
name: 'todo_quickAccessWidget'.tr,
description: 'todo_quickAccessDescription'.tr,
icon: Icons.check_circle,
color: Colors.blue,
defaultSize: HomeWidgetSize.large,
supportedSizes: [HomeWidgetSize.medium, HomeWidgetSize.large],
category: 'home_categoryTask'.tr,
selectorId: 'todo.task',
dataRenderer: _renderTaskData,
navigationHandler: _navigateToTask,
builder: (context, config) {
return GenericSelectorWidget(
widgetDefinition: registry.getWidget('todo_quick_access')!,
config: config,
);
},
),
);
}
static Map<String, dynamic> _extractTaskData(List<dynamic> dataArray) {
final taskData = dataArray[0] as Map<String, dynamic>;
return {
'id': taskData['id'] as String,
'title': taskData['title'] as String?,
};
}
static Widget _renderTaskData(
BuildContext context,
SelectorResult result,
Map<String, dynamic> config,
) {
final savedData = result.data is Map<String, dynamic>
? result.data as Map<String, dynamic>
: {};
final taskId = savedData['id'] as String? ?? '';
return FutureBuilder<Task?>(
future: _loadLatestTask(taskId),
builder: (context, snapshot) {
final task = snapshot.data;
final title = task?.title ?? savedData['title'] ?? 'Unknown';
final description = task?.description ?? '';
final completed = task?.completed ?? false;
return _buildTaskWidget(context, title, description, completed);
},
);
}
static Future<Task?> _loadLatestTask(String taskId) async {
try {
final plugin = PluginManager.instance.getPlugin('todo') as TodoPlugin?;
return await plugin?.taskController.getTaskById(taskId);
} catch (e) {
debugPrint('加载任务失败: $e');
return null;
}
}
static Widget _buildTaskWidget(
BuildContext context,
String title,
String description,
bool completed,
) {
final theme = Theme.of(context);
return Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(16),
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: completed
? theme.colorScheme.surfaceContainerHighest
: theme.colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(16),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
completed ? Icons.check_circle : Icons.radio_button_unchecked,
color: completed ? Colors.green : theme.colorScheme.primary,
),
const SizedBox(width: 8),
Expanded(
child: Text(
title,
style: theme.textTheme.titleMedium?.copyWith(
decoration: completed ? TextDecoration.lineThrough : null,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
),
if (description.isNotEmpty) ...[
const SizedBox(height: 8),
Text(
description,
style: theme.textTheme.bodySmall,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
],
),
),
),
);
}
static void _navigateToTask(BuildContext context, SelectorResult result) {
final data = result.data is Map<String, dynamic>
? result.data as Map<String, dynamic>
: {};
final taskId = data['id'] as String?;
NavigationHelper.pushNamed(
context,
'/todo/task',
arguments: {'taskId': taskId},
);
}
}
```
### 3. 路由注册 (`route.dart`)
```dart
case '/todo/task':
case 'todo/task':
String? taskId;
if (settings.arguments is Map<String, dynamic>) {
final args = settings.arguments as Map<String, dynamic>;
taskId = args['taskId'] as String?;
}
return _createRoute(
TodoTaskDetailScreen(taskId: taskId),
);
```
## Best Practices
### 1. 数据提取
```dart
// ✅ 推荐:只提取必要字段
static Map<String, dynamic> _extractData(List<dynamic> dataArray) {
final item = dataArray[0] as Map<String, dynamic>;
return {
'id': item['id'] as String, // 必须保存 id
'title': item['title'] as String?,
};
}
// ❌ 避免:保存完整数据
static Map<String, dynamic> _extractData(List<dynamic> dataArray) {
return dataArray[0] as Map<String, dynamic>;
}
```
### 2. 动态数据加载
```dart
// ✅ 推荐:使用 FutureBuilder 获取最新数据
static Widget _renderData(BuildContext context, SelectorResult result, ...) {
final savedData = result.data as Map<String, dynamic>;
final id = savedData['id'] as String?;
return FutureBuilder<Item?>(
future: _loadLatestData(id),
builder: (context, snapshot) {
final data = snapshot.data ?? savedData;
return _buildWidgetUI(context, data);
},
);
}
// ❌ 避免:只使用保存的数据,不获取最新
static Widget _renderData(BuildContext context, SelectorResult result, ...) {
final data = result.data as Map<String, dynamic>;
return _buildWidgetUI(context, data); // 不获取最新数据
}
```
### 3. 空值处理
```dart
// ✅ 推荐:安全的空值处理
final title = itemData['title'] as String? ?? 'Unknown';
final id = itemData['id'] as String? ?? '';
// ❌ 避免:直接访问可能为 null 的值
final title = itemData['title'] as String; // 可能抛出异常
```
### 4. 错误处理
```dart
static Future<Item?> _loadLatestData(String id) async {
try {
final plugin = PluginManager.instance.getPlugin('[plugin_id]') as PluginClass?;
if (plugin == null || id.isEmpty) return null;
return await plugin.controller.getItemById(id);
} catch (e) {
debugPrint('加载数据失败: $e');
return null;
}
}
```
## Testing Checklist
完成后验证:
- [ ] `flutter analyze` 无错误
- [ ] 选择器能正常显示可选数据列表
- [ ] 选择后小组件正确显示配置状态
- [ ] 小组件点击后能正常导航到详情页
- [ ] 小组件显示的数据是最新的(通过 controller 获取)
- [ ] 重新选择后小组件正确更新
- [ ] 国际化字符串完整
- [ ] 删除/修改原始数据后小组件正确显示最新状态
## Troubleshooting
### 问题 1: 点击小组件没有反应
**检查清单**:
- [ ] `selectorId` 是否与 `SelectorDefinition.id` 完全一致?
- [ ] `dataRenderer` 和 `navigationHandler` 是否都已实现?
- [ ] `GenericSelectorWidget` 的 `widgetDefinition` 是否正确获取?
### 问题 2: 小组件显示的数据不是最新的
**原因**: `dataRenderer` 没有通过 controller 获取最新数据
**解决**:
```dart
// ✅ 确保这样写
return FutureBuilder<Item?>(
future: plugin.controller.getItemById(itemId), // 通过 controller 获取
builder: (context, snapshot) {
// ...
},
);
```
### 问题 3: 选择后数据丢失
**原因**: `dataSelector` 函数返回空或格式错误
**解决**:
- 确保 `dataSelector` 返回 `Map<String, dynamic>`
- 确保返回的数据包含 `id` 字段
### 问题 4: 导航到详情页失败
**检查**:
1. **路由配置检查** - 最常见问题!
`route.dart` 中有两套路由机制:
- `routes` Map(静态路由定义)
- `onGenerateRoute`(动态路由处理)
如果 `routes` 中定义了路由,`onGenerateRoute` 不会被调用!
```dart
// ❌ 错误:routes 中定义了 /tracker,导致 onGenerateRoute 不会执行
static Map<String, WidgetBuilder> get routes => {
// ...
tracker: (context) => const TrackerMainView(), // 移除这行!
};
// ✅ 正确:让 onGenerateRoute 处理(可接收参数)
case '/tracker':
case 'tracker':
String? goalId;
if (settings.arguments is Map<String, dynamic>) {
goalId = (settings.arguments as Map<String, dynamic>)['goalId'] as String?;
}
if (goalId != null) {
return _createRoute(GoalDetailScreen(goalId: goalId));
}
return _createRoute(const TrackerMainView());
```
2. **导航上下文检查**
小组件回调中的 context 可能不在导航树中,使用 `navigatorKey.currentContext`:
```dart
// ✅ 推荐:使用 navigatorKey
final navContext = navigatorKey.currentContext ?? context;
NavigationHelper.pushNamed(navContext, '/route', arguments: {...});
```
3. **参数类型检查**
id 可能是 `int` 或 `String`:
```dart
final id = data['id']?.toString(); // 安全处理
```
4. **详情页 Provider 依赖检查**
如果详情页使用 `Consumer<Controller>` 或 `context.read<Controller>()`:
- 需要在路由中用 `ChangeNotifierProvider.value` 包裹
- 或让详情页直接使用 `Plugin.instance.controller` 单例
## Route Configuration Checklist
为新小组件添加路由时检查:
- [ ] 从 `routes` Map 中移除该路由(如果有)
- [ ] 在 `onGenerateRoute` 的 switch case 中添加路由处理
- [ ] 正确解析 `settings.arguments` 中的参数
- [ ] 使用 `navigatorKey.currentContext` 进行导航
- [ ] 详情页能正确获取 controller(使用单例或 Provider)
## Notes
- 使用中文注释与现有代码库保持一致
- 优先使用 `GenericSelectorWidget` 作为小组件构建器
- 通过 controller 获取最新数据而不是使用保存的静态数据
- 使用 `flutter analyze` 验证代码
- 参考 `lib/plugins/bill/home_widgets.dart` 获取完整实现示例
- 参考 `docs/SELECTOR_WIDGET_GUIDE.md` 获取详细指南Related Skills
gboy-character-selector
Select characters from the G*BOY universe for your OpenCLAW agent personality.
corrosion-materials-selector
Materials selection skill for corrosion resistance based on process conditions and industry standards
homebrew-release
Create a new version release for a Homebrew formula in the homebrew-tools tap. Handles git tagging, GitHub releases, SHA256 computation, and formula updates. Use when the user wants to publish a new version of a tool in their Homebrew tap.
esphome-config-helper
This skill should be used when the user asks to "create an esphome config", "set up an esp32 device", "configure esphome yaml", "add a sensor to esphome", "fix esphome compile error", or mentions "gpio pin assignment", "wifi setup", "ota update", or error messages like "Unknown platform", "GPIO already in use", "Could not compile", or "WiFi connection failed". Provides rapid ESPHome configuration generation, troubleshooting, and validation.
adguard-home
Manage, troubleshoot, configure, analyze, and review AdGuard Home DNS server. Use when working with AdGuard Home, DNS blocking, ad blocking, network-wide filtering, DNS queries, blocklists, client management, DHCP, or DNS rewrites. Supports REST API and SSH access. Triggers on adguard, DNS blocking, ad blocking, network filtering, DNS server, blocklist, pi-hole alternative.
chatgpt-app:add-widget
Add a new inline widget to your ChatGPT App with Tailwind CSS and Apps SDK integration.
bgo
Automates the complete Blender build-go workflow, from building and packaging your extension/add-on to removing old versions, installing, enabling, and launching Blender for quick testing and iteration.
humanizer-ko
Detects and corrects Korean AI writing patterns to transform text into natural human writing. Based on scientific linguistic research (KatFishNet paper with 94.88% AUC accuracy). Analyzes 19 patterns including comma overuse, spacing rigidity, POS diversity, AI vocabulary overuse, and structural monotony. Use when humanizing Korean text from ChatGPT/Claude/Gemini or removing AI traces from Korean LLM output.
huggingface-accelerate
Simplest distributed training API. 4 lines to add distributed support to any PyTorch script. Unified API for DeepSpeed/FSDP/Megatron/DDP. Automatic device placement, mixed precision (FP16/BF16/FP8). Interactive config, single launch command. HuggingFace ecosystem standard.
hr-pro
Professional, ethical HR partner for hiring, onboarding/offboarding, PTO and leave, performance, compliant policies, and employee relations. Ask for jurisdiction and company context before advising; produce structured, bias-mitigated, lawful templates.
hive-mind-advanced
Advanced Hive Mind collective intelligence system for queen-led multi-agent coordination with consensus mechanisms and persistent memory
hire
Interactive hiring wizard to set up a new AI team member. Guides the user through role design via conversation, generates agent identity files, and optionally sets up performance reviews. Use when the user wants to hire, add, or set up a new AI agent, team member, or assistant. Triggers on phrases like "hire", "add an agent", "I need help with X" (implying a new role), or "/hire".