feat(agent): add voice input capability and standardize tool naming

- Add voice recording with transcribe endpoint (ASR) for multimodal input
- Android: add RECORD_AUDIO and INTERNET permissions
- Refactor tool naming: frontend tools use 'front.' prefix, backend tools use 'back.'
- Migrate calendar tools: create_calendar_event -> back.mutate/list/delete events
- Add calendar_event_list.v1 and calendar_operation.v1 UI card types
- Update all Flutter and Python tests to match new tool naming conventions
- Add record package dependency for voice recording
This commit is contained in:
zl-q
2026-03-09 00:10:09 +08:00
parent 6c83e35a69
commit 3ac09475ad
30 changed files with 1593 additions and 438 deletions
@@ -4,14 +4,19 @@ import '../../data/models/tool_result.dart';
/// 卡片类型常量
const _calendarCardType = 'calendar_card.v1';
const _calendarListType = 'calendar_event_list.v1';
const _calendarOperationType = 'calendar_operation.v1';
const _errorCardType = 'error_card.v1';
const _aiGeneratedSource = 'ai_generated';
const _agentGeneratedSource = 'agent_generated';
const _primaryActionType = 'primary';
class UiSchemaRenderer {
static Widget render(UiCard card) {
return switch (card.cardType) {
_calendarCardType => _renderCalendarCard(card),
_calendarListType => _renderCalendarList(card),
_calendarOperationType => _renderCalendarOperation(card),
_errorCardType => _renderErrorCard(card),
_ => _renderUnknownCard(card),
};
@@ -22,7 +27,9 @@ class UiSchemaRenderer {
final color = data.color != null
? Color(int.parse(data.color!.replaceFirst('#', '0xFF')))
: AppColors.blue500;
final isAiGenerated = data.sourceType == _aiGeneratedSource;
final isAiGenerated =
data.sourceType == _aiGeneratedSource ||
data.sourceType == _agentGeneratedSource;
return Container(
decoration: BoxDecoration(
@@ -152,6 +159,103 @@ class UiSchemaRenderer {
);
}
static Widget _renderCalendarList(UiCard card) {
final rawItems = card.data['items'];
final items = rawItems is List ? rawItems : const [];
final paginationRaw = card.data['pagination'];
final pagination = paginationRaw is Map<String, dynamic>
? paginationRaw
: const <String, dynamic>{};
final page = pagination['page'];
final total = pagination['total'];
return Container(
padding: EdgeInsets.all(AppSpacing.lg),
decoration: BoxDecoration(
color: AppColors.messageCardBg,
borderRadius: BorderRadius.circular(AppRadius.lg),
border: Border.all(color: AppColors.messageCardBorder),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'日程列表',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppColors.slate900,
),
),
if (page != null || total != null) ...[
SizedBox(height: AppSpacing.xs),
Text(
'${page ?? '-'}页 · 共${total ?? '-'}',
style: TextStyle(fontSize: 12, color: AppColors.slate500),
),
],
SizedBox(height: AppSpacing.sm),
if (items.isEmpty)
Text(
'暂无日程',
style: TextStyle(fontSize: 14, color: AppColors.slate500),
),
for (final item in items)
if (item is Map<String, dynamic>)
Padding(
padding: EdgeInsets.only(bottom: AppSpacing.xs),
child: Text(
item['title']?.toString() ?? '未命名日程',
style: TextStyle(fontSize: 14, color: AppColors.slate700),
),
),
],
),
);
}
static Widget _renderCalendarOperation(UiCard card) {
final ok = card.data['ok'] == true;
final operation = card.data['operation']?.toString() ?? 'operation';
final message = card.data['message']?.toString() ?? (ok ? '操作成功' : '操作失败');
return Container(
padding: EdgeInsets.all(AppSpacing.lg),
decoration: BoxDecoration(
color: ok ? AppColors.messageCardBg : AppColors.warningBackground,
borderRadius: BorderRadius.circular(AppRadius.lg),
border: Border.all(
color: ok ? AppColors.messageCardBorder : AppColors.red400,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'日程$operation结果',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: ok ? AppColors.slate900 : AppColors.red600,
),
),
SizedBox(height: AppSpacing.xs),
Text(
message,
style: TextStyle(
fontSize: 13,
color: ok ? AppColors.slate600 : AppColors.red600,
),
),
if (card.actions != null && card.actions!.isNotEmpty) ...[
SizedBox(height: AppSpacing.md),
_buildActions(card.actions!),
],
],
),
);
}
static Widget _renderErrorCard(UiCard card) {
final message = card.data['message'] as String? ?? '发生错误';