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
@@ -0,0 +1,64 @@
import 'dart:io';
import 'package:flutter/services.dart';
import 'package:record/record.dart';
abstract class VoiceRecorder {
Future<void> start();
Future<String?> stop();
Future<void> dispose();
}
class RecordVoiceRecorder implements VoiceRecorder {
final AudioRecorder _recorder;
String? _currentPath;
RecordVoiceRecorder({AudioRecorder? recorder})
: _recorder = recorder ?? AudioRecorder();
@override
Future<void> start() async {
bool hasPermission;
try {
hasPermission = await _recorder.hasPermission();
} on MissingPluginException catch (_) {
throw StateError('录音组件未加载,请完全重启 App 后重试');
}
if (!hasPermission) {
throw StateError('录音权限未授权');
}
final fileName =
'voice_${DateTime.now().millisecondsSinceEpoch}_${DateTime.now().microsecond}.wav';
final path = '${Directory.systemTemp.path}/$fileName';
_currentPath = path;
try {
await _recorder.start(
const RecordConfig(
encoder: AudioEncoder.wav,
sampleRate: 16000,
numChannels: 1,
),
path: path,
);
} on MissingPluginException catch (_) {
throw StateError('录音组件未加载,请完全重启 App 后重试');
}
}
@override
Future<String?> stop() async {
String? stoppedPath;
try {
stoppedPath = await _recorder.stop();
} on MissingPluginException catch (_) {
throw StateError('录音组件未加载,请完全重启 App 后重试');
}
return stoppedPath ?? _currentPath;
}
@override
Future<void> dispose() async {
await _recorder.dispose();
}
}