Browse Source

add

newdev_shunjiawei
wolfplus 2 years ago
parent
commit
2335647957
  1. 6
      android/app/build.gradle.kts
  2. 27
      android/app/src/main/AndroidManifest.xml
  3. 30
      ios/Runner/Info.plist
  4. 14
      lib/core/bindings/initial_binding.dart
  5. 6
      lib/core/controllers/app_controller.dart
  6. 66
      lib/data/services/audio_service.dart
  7. 95
      lib/data/services/background_agent_service.dart
  8. 83
      lib/data/services/my_audio_handler.dart
  9. 147
      lib/data/services/notification_service.dart
  10. 52
      lib/data/services/volcano_tts_service.dart
  11. 20
      lib/modules/chat/bindings/chat_binding.dart
  12. 271
      lib/modules/chat/controllers/chat_controller.dart
  13. 28
      lib/modules/explore/views/explore_view.dart
  14. 5
      pubspec.yaml

6
android/app/build.gradle.kts

@ -13,6 +13,7 @@ android {
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
isCoreLibraryDesugaringEnabled = true
}
kotlinOptions {
@ -28,6 +29,7 @@ android {
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
multiDexEnabled = true
}
buildTypes {
@ -42,3 +44,7 @@ android {
flutter {
source = "../.."
}
dependencies {
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.4")
}

27
android/app/src/main/AndroidManifest.xml

@ -9,10 +9,14 @@
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
<uses-permission android:name="android.permission.MEDIA_CONTENT_CONTROL"/>
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.VIBRATE"/>
<application
android:label="deep_voice"
android:name="${applicationName}"
android:name="android.app.Application"
android:icon="@mipmap/ic_launcher"
android:usesCleartextTraffic="true"
android:networkSecurityConfig="@xml/network_security_config">
@ -44,7 +48,9 @@
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
android:windowSoftInputMode="adjustResize"
android:showWhenLocked="true"
android:turnScreenOn="true">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
@ -63,6 +69,23 @@
<meta-data
android:name="flutterEmbedding"
android:value="2" />
<!-- 添加通知图标 -->
<meta-data
android:name="com.dexterous.flutterlocalnotifications.default_notification_icon"
android:resource="@mipmap/ic_launcher" />
<receiver android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationReceiver"
android:exported="false" />
<receiver android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationBootReceiver"
android:exported="false">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/>
<action android:name="android.intent.action.MY_PACKAGE_REPLACED"/>
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
<action android:name="com.htc.intent.action.QUICKBOOT_POWERON"/>
</intent-filter>
</receiver>
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and

30
ios/Runner/Info.plist

@ -24,6 +24,14 @@
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSMicrophoneUsageDescription</key>
<string>需要使用麦克风来进行语音交互</string>
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
<string>fetch</string>
<string>remote-notification</string>
</array>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
@ -31,24 +39,10 @@
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
<key>NSUserNotificationsUsageDescription</key>
<string>需要通知权限来保持与AI助手的实时对话</string>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
</dict>
</plist>

14
lib/core/bindings/initial_binding.dart

@ -6,14 +6,14 @@ import '../../modules/explore/controllers/explore_controller.dart';
import '../../modules/profile/controllers/profile_controller.dart';
import '../../data/services/volcano_tts_service.dart';
import '../../data/services/audio_service.dart';
import '../../data/services/notification_service.dart';
/// 初始绑定,用于管理全局依赖
class InitialBinding extends Bindings {
@override
void dependencies() {
// 初始化服务
Get.put(AudioServiceManager(), permanent: true);
Get.put(VolcanoTtsService(), permanent: true);
final notificationService = Get.put(NotificationService(), permanent: true);
// 注册 Controllers
Get.put(AppController(), permanent: true);
@ -22,11 +22,17 @@ class InitialBinding extends Bindings {
Get.put(ProfileController(), permanent: true);
Get.put(PermissionController(), permanent: true);
// 初始化 AudioService
Get.put(AudioServiceManager(), permanent: true);
Get.put(VolcanoTtsService(), permanent: true);
// 异步初始化
Future.wait([
notificationService.init(),
AudioServiceManager.to.init().then((_) {
print('AudioService初始化完成');
}).catchError((error) {
print('AudioService初始化失败: $error');
});
}),
]);
}
}

6
lib/core/controllers/app_controller.dart

@ -31,4 +31,10 @@ class AppController extends GetxController {
void changeIndex(int index) {
_currentIndex.value = index;
}
@override
void onClose() {
// 在这里清理全局资源
super.onClose();
}
}

66
lib/data/services/audio_service.dart

@ -2,20 +2,30 @@ import 'dart:async';
import 'package:audio_service/audio_service.dart';
import 'package:get/get.dart';
import 'package:flutter/material.dart';
import 'package:synchronized/synchronized.dart';
import 'my_audio_handler.dart';
import '../../modules/chat/controllers/chat_controller.dart';
import '../../data/providers/agent_provider.dart';
import 'volcano_tts_service.dart';
class AudioServiceManager extends GetxService {
static AudioServiceManager get to => Get.find();
late MyAudioHandler _audioHandler;
StreamSubscription? _customEventSubscription;
MyAudioHandler? _audioHandler;
bool _isInitialized = false;
final _lock = Lock();
Future<AudioServiceManager> init() async {
print('初始化 AudioService');
if (_isInitialized) {
print('AudioService 已经初始化');
return this;
}
await _cleanupExistingService();
try {
// 使用锁确保初始化的原子性
await _lock.synchronized(() async {
if (_isInitialized) return;
// 初始化 AudioService
_audioHandler = await AudioService.init(
builder: () => MyAudioHandler(),
@ -30,50 +40,40 @@ class AudioServiceManager extends GetxService {
),
);
// 监听自定义事件
_customEventSubscription = _audioHandler.customEventStream.listen((event) {
if (event == 'activate_app') {
print('收到激活应用事件');
_activateAppWithPersonalAssistant();
}
_isInitialized = true;
});
return this;
} catch (e) {
print('AudioService初始化失败: $e');
await _cleanupExistingService();
rethrow;
}
}
void _activateAppWithPersonalAssistant() {
try {
// 获取 personal_assistant agent
final agent = AgentProvider.getAgentById('personal_assistant');
if (agent == null) {
print('未找到 personal_assistant agent');
return;
}
Future<void> _cleanupExistingService() async {
_isInitialized = false;
// 导航到聊天页面,传递 agent 参数
Get.toNamed('/chat', arguments: {
'agentId': agent.id,
'agentName': agent.name,
'agentAvatar': agent.avatarUrl,
'agentSubtitle': agent.description,
'systemPrompt': agent.systemPrompt,
'welcomeMessage': agent.welcomeMessage,
});
// 停止现有的音频处理器
if (_audioHandler != null) {
await _audioHandler!.stop();
_audioHandler = null;
}
// ChatController 会自动处理欢迎消息和问候语的显示与播放
// 停止 AudioService
try {
await AudioService.stop();
} catch (e) {
print('激活应用失败: $e');
print('停止 AudioService 时出错: $e');
}
// 等待一小段时间确保清理完成
await Future.delayed(const Duration(milliseconds: 100));
}
@override
void onClose() {
_customEventSubscription?.cancel();
_audioHandler.stop();
void onClose() async {
await _cleanupExistingService();
super.onClose();
}
}

95
lib/data/services/background_agent_service.dart

@ -0,0 +1,95 @@
import 'package:get/get.dart';
import 'dart:async';
import 'volcano_ai_service.dart';
import 'volcano_tts_service.dart';
import '../../modules/chat/models/message_model.dart';
class BackgroundAgentService extends GetxService {
static BackgroundAgentService get to => Get.find();
final VolcanoAIService _aiService;
final VolcanoTtsService _ttsService;
final List<Message> _messageHistory = [];
String _pendingTtsText = '';
static const int _minTtsLength = 20;
bool _isProcessing = false;
BackgroundAgentService()
: _aiService = VolcanoAIService(),
_ttsService = Get.find<VolcanoTtsService>();
Future<void> handleAgentInteraction(String systemPrompt) async {
if (_isProcessing) return;
_isProcessing = true;
try {
// 构建用于生成回应的消息列表
final messages = [
{'role': 'system', 'content': systemPrompt},
{'role': 'user', 'content': '请用一句简短的话回应我,要体现你的特点,不要超过12个字。'},
];
String fullResponse = '';
await for (final chunk in _aiService.sendMessageStream(
messages: messages,
systemPrompt: systemPrompt,
)) {
fullResponse += chunk;
// 累积文本并处理TTS
_pendingTtsText += chunk;
if (_ttsService.isEnabled.value) {
if (_pendingTtsText.length >= _minTtsLength) {
int lastSentenceEnd = _findLastSentenceEnd(_pendingTtsText);
if (lastSentenceEnd > 0) {
String textToSpeak = _pendingTtsText.substring(0, lastSentenceEnd + 1);
await _ttsService.speak(textToSpeak);
_pendingTtsText = _pendingTtsText.substring(lastSentenceEnd + 1);
}
}
}
}
// 处理剩余的文本
if (_ttsService.isEnabled.value && _pendingTtsText.isNotEmpty) {
await _ttsService.speak(_pendingTtsText);
}
_pendingTtsText = '';
// 保存对话历史
_messageHistory.add(Message(
role: 'assistant',
content: fullResponse,
timestamp: DateTime.now(),
));
// 限制历史记录长度
if (_messageHistory.length > 10) {
_messageHistory.removeAt(0);
}
} catch (e) {
print('Background agent interaction failed: $e');
} finally {
_isProcessing = false;
}
}
int _findLastSentenceEnd(String text) {
final sentenceEnds = [
text.lastIndexOf('。'),
text.lastIndexOf('!'),
text.lastIndexOf('?'),
text.lastIndexOf('.'),
text.lastIndexOf('!'),
text.lastIndexOf('?'),
];
return sentenceEnds.reduce((max, pos) => pos > max ? pos : max);
}
List<Message> get messageHistory => List.unmodifiable(_messageHistory);
void clearHistory() {
_messageHistory.clear();
}
}

83
lib/data/services/my_audio_handler.dart

@ -1,17 +1,44 @@
import 'dart:async';
import 'package:audio_service/audio_service.dart';
import 'package:get/get.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'background_agent_service.dart';
import '../../data/providers/agent_provider.dart';
/// 自定义 AudioHandler,用于捕获媒体按键事件(例如蓝牙耳机双击)
class MyAudioHandler extends BaseAudioHandler {
// 定义一个 StreamController 用于向 UI 发送自定义事件
final StreamController<dynamic> _customEventController = StreamController.broadcast();
final StreamController<dynamic> _customEventController =
StreamController.broadcast();
bool _isDisposed = false;
late final BackgroundAgentService _backgroundAgent;
late final String _systemPrompt;
Stream<dynamic> get customEventStream => _customEventController.stream;
MyAudioHandler() {
_initializeHandler();
_initializeBackgroundAgent();
_initializeSystemPrompt();
}
void _initializeSystemPrompt() {
try {
final agent = AgentProvider.getAgentById('personal_assistant');
_systemPrompt = agent?.systemPrompt ?? '';
} catch (e) {
print('初始化 SystemPrompt 失败: $e');
_systemPrompt = '';
}
}
void _initializeBackgroundAgent() {
try {
_backgroundAgent = Get.put(BackgroundAgentService(), permanent: true);
} catch (e) {
print('初始化 BackgroundAgent 失败: $e');
}
}
void _initializeHandler() {
@ -60,21 +87,6 @@ class MyAudioHandler extends BaseAudioHandler {
}
}
// 激活应用
Future<void> _activateApp() async {
if (_isDisposed) return;
try {
print('收到激活应用事件');
_customEventController.add('activate_app');
// 尝试将应用带到前台
await SystemChannels.platform.invokeMethod('SystemNavigator.routeInformationUpdated');
} catch (e) {
print('激活应用失败: $e');
}
}
// 更新播放状态
void _updatePlaybackState({
required bool playing,
@ -93,10 +105,35 @@ class MyAudioHandler extends BaseAudioHandler {
}
}
Future<void> _handleInteraction() async {
if (_systemPrompt.isEmpty) return;
if (Get.context != null &&
WidgetsBinding.instance.lifecycleState == AppLifecycleState.resumed) {
// 应用在前台,导航到聊天页面
final agent = AgentProvider.getAgentById('personal_assistant');
if (agent != null) {
// 导航到聊天页面,并标记需要在进入时播放语音应答
Get.toNamed('/chat', arguments: {
'agentId': agent.id,
'agentName': agent.name,
'agentAvatar': agent.avatarUrl,
'agentSubtitle': agent.description,
'systemPrompt': agent.systemPrompt,
'welcomeMessage': agent.welcomeMessage,
'playVoiceOnEnter': true,
});
}
} else {
// 应用在后台,使用 BackgroundAgent 处理交互
await _backgroundAgent.handleAgentInteraction(_systemPrompt);
}
}
@override
Future<void> play() async {
print('收到播放命令');
await _activateApp();
await _handleInteraction();
_updatePlaybackState(
playing: true,
processingState: AudioProcessingState.ready,
@ -106,6 +143,8 @@ class MyAudioHandler extends BaseAudioHandler {
@override
Future<void> pause() async {
print('收到暂停命令');
await _handleInteraction();
_updatePlaybackState(
playing: false,
processingState: AudioProcessingState.ready,
@ -131,26 +170,26 @@ class MyAudioHandler extends BaseAudioHandler {
@override
Future<void> skipToPrevious() async {
print('收到上一曲命令');
await _activateApp();
await _handleInteraction();
}
@override
Future<void> skipToNext() async {
print('收到下一曲命令');
await _activateApp();
await _handleInteraction();
}
@override
Future<void> seek(Duration position) async {
print('收到跳转命令: $position');
await _activateApp();
}
@override
Future<dynamic> customAction(String name, [Map<String, dynamic>? extras]) async {
Future<dynamic> customAction(String name,
[Map<String, dynamic>? extras]) async {
print('收到自定义命令: $name');
if (name == 'media_button') {
await _activateApp();
await _handleInteraction();
}
}

147
lib/data/services/notification_service.dart

@ -0,0 +1,147 @@
import 'dart:io';
import 'package:get/get.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:permission_handler/permission_handler.dart';
import '../providers/agent_provider.dart';
class NotificationService extends GetxService {
static NotificationService get to => Get.find();
final FlutterLocalNotificationsPlugin _notificationsPlugin = FlutterLocalNotificationsPlugin();
bool _notificationActive = false;
bool _isRequestingPermission = false;
String get _defaultAgentName {
final agent = AgentProvider.getAgentById('personal_assistant');
return agent?.name ?? 'Deep Voice';
}
Future<void> init() async {
const androidSettings = AndroidInitializationSettings('@mipmap/ic_launcher');
const iosSettings = DarwinInitializationSettings(
requestAlertPermission: false, // 初始化时不请求权限
requestBadgePermission: false,
requestSoundPermission: false,
);
const initSettings = InitializationSettings(
android: androidSettings,
iOS: iosSettings,
);
await _notificationsPlugin.initialize(
initSettings,
onDidReceiveNotificationResponse: (details) {
if (Get.currentRoute != '/chat') {
Get.toNamed('/chat');
}
},
);
// 创建通知渠道
if (Platform.isAndroid) {
await _notificationsPlugin.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin>()?.createNotificationChannel(
const AndroidNotificationChannel(
'foreground_service', // id
'Foreground Service', // name
description: 'Used for the foreground service notification', // description
importance: Importance.min,
playSound: false,
enableVibration: false,
showBadge: false,
),
);
}
await _requestPermissionIfNeeded();
// 在初始化完成后显示前台服务通知
await showForegroundNotification();
}
Future<void> showForegroundNotification() async {
if (_notificationActive) return;
// 在显示通知前确保有权限
await _requestPermissionIfNeeded();
final androidDetails = AndroidNotificationDetails(
'foreground_service',
'Foreground Service',
channelDescription: 'Used for the foreground service notification',
importance: Importance.min,
priority: Priority.min,
ongoing: true,
autoCancel: false,
showWhen: false,
enableVibration: false,
playSound: false,
icon: '@mipmap/ic_launcher',
category: AndroidNotificationCategory.service,
actions: [
const AndroidNotificationAction(
'chat',
'开始对话',
showsUserInterface: true,
),
],
);
const iosDetails = DarwinNotificationDetails(
presentAlert: false,
presentBadge: false,
presentSound: false,
interruptionLevel: InterruptionLevel.passive,
);
final notificationDetails = NotificationDetails(
android: androidDetails,
iOS: iosDetails,
);
await _notificationsPlugin.show(
0,
_defaultAgentName,
'点击开始对话',
notificationDetails,
);
_notificationActive = true;
}
Future<void> cancelNotification() async {
if (!_notificationActive) return;
await _notificationsPlugin.cancel(0);
_notificationActive = false;
}
Future<void> _requestPermissionIfNeeded() async {
if (_isRequestingPermission) return;
_isRequestingPermission = true;
try {
if (Platform.isAndroid) {
final androidInfo = await DeviceInfoPlugin().androidInfo;
if (androidInfo.version.sdkInt >= 33) {
final status = await Permission.notification.status;
if (status.isDenied) {
await Permission.notification.request();
}
}
} else if (Platform.isIOS) {
await _notificationsPlugin
.resolvePlatformSpecificImplementation<
IOSFlutterLocalNotificationsPlugin>()
?.requestPermissions(
alert: true,
badge: true,
sound: true,
);
}
} finally {
_isRequestingPermission = false;
}
}
}

52
lib/data/services/volcano_tts_service.dart

@ -228,7 +228,7 @@ class VolcanoTtsService extends GetxService {
}
int lastPos = 0;
String currentSegment = '';
String currentBatch = '';
for (final match in matches) {
final end = match.end;
@ -239,33 +239,42 @@ class VolcanoTtsService extends GetxService {
continue;
}
// 如果当前段落加上新句子不超过最佳长度,则合并
if (currentSegment.isEmpty) {
currentSegment = sentence;
} else if ((currentSegment + sentence).length <= _optimalSegmentLength) {
currentSegment += sentence;
} else {
// 当前段落已经有内容,直接加入队列
if (currentSegment.isNotEmpty) {
_sentenceQueue.add(currentSegment);
// 如果当前批次为空,直接添加句子
if (currentBatch.isEmpty) {
currentBatch = sentence;
}
// 如果当前批次加上新句子不超过30字符,则合并
else if ((currentBatch + sentence).length <= 30) {
currentBatch += sentence;
}
currentSegment = sentence;
// 如果超过30字符,将当前批次加入队列,开始新批次
else {
if (currentBatch.isNotEmpty) {
_sentenceQueue.add(currentBatch);
}
currentBatch = sentence;
}
lastPos = end;
}
// 处理当前累积的段落
if (currentSegment.isNotEmpty) {
_sentenceQueue.add(currentSegment);
// 处理最后一个批次
if (currentBatch.isNotEmpty) {
_sentenceQueue.add(currentBatch);
}
// 处理剩余的文本
if (lastPos < _pendingText.length) {
final remaining = _pendingText.substring(lastPos).trim();
if (remaining.isNotEmpty) {
if (remaining.length <= _maxSegmentLength) {
if (remaining.length <= 30) {
// 尝试与最后一个批次合并
if (_sentenceQueue.isNotEmpty &&
(_sentenceQueue.last + remaining).length <= 30) {
_sentenceQueue[_sentenceQueue.length - 1] += remaining;
} else {
_sentenceQueue.add(remaining);
}
} else {
_splitByLength(remaining);
}
@ -280,12 +289,9 @@ class VolcanoTtsService extends GetxService {
int start = 0;
while (start < text.length) {
int end = start + _maxSegmentLength;
if (end > text.length) {
end = text.length;
}
int end = math.min(start + 30, text.length);
// 尝试在单词或字符边界处分割
// 尝试在标点或空格处分割
if (end < text.length) {
final spacePos = text.lastIndexOf(' ', end);
final breakPos = _sentenceBreaks.firstMatch(
@ -301,8 +307,14 @@ class VolcanoTtsService extends GetxService {
final segment = text.substring(start, end).trim();
if (segment.isNotEmpty) {
// 尝试与前一个批次合并
if (_sentenceQueue.isNotEmpty &&
(_sentenceQueue.last + segment).length <= 30) {
_sentenceQueue[_sentenceQueue.length - 1] += segment;
} else {
_sentenceQueue.add(segment);
}
}
start = end;
}
}

20
lib/modules/chat/bindings/chat_binding.dart

@ -1,25 +1,9 @@
import 'package:get/get.dart';
import '../controllers/chat_controller.dart';
class ChatBinding extends Bindings {
class ChatBinding implements Bindings {
@override
void dependencies() {
// 获取传递的参数
final arguments = Get.arguments;
if (arguments == null || !(arguments is Map<String, dynamic>)) {
print('Chat binding: 未提供有效的参数');
Get.back();
return;
}
// 创建并注入控制器
Get.put(ChatController(
agentId: arguments['agentId'],
agentName: arguments['agentName'],
agentAvatar: arguments['agentAvatar'],
agentSubtitle: arguments['agentSubtitle'],
systemPrompt: arguments['systemPrompt'],
welcomeMessage: arguments['welcomeMessage'],
));
Get.put(ChatController());
}
}

271
lib/modules/chat/controllers/chat_controller.dart

@ -7,20 +7,21 @@ import '../../../data/services/volcano_tts_service.dart';
import '../models/message_model.dart';
class ChatController extends GetxController {
final String agentId;
final String agentName;
final String agentAvatar;
final String? agentSubtitle;
final String systemPrompt;
final String welcomeMessage;
final VolcanoAIService _aiService;
late final String agentId;
late final String agentName;
late final String agentAvatar;
late final String? agentSubtitle;
late final String systemPrompt;
late final String welcomeMessage;
late final bool playVoiceOnEnter;
final VolcanoAIService _aiService = VolcanoAIService();
late final VolcanoTtsService _ttsService;
static const String _storagePrefix = 'chat_history_';
final messages = <Message>[].obs;
final isLoading = false.obs;
final textController = TextEditingController();
final scrollController = ScrollController();
late final TextEditingController textController;
late final ScrollController scrollController;
final _storage = GetStorage();
// 用于存储当前正在流式生成的消息
@ -30,25 +31,34 @@ class ChatController extends GetxController {
static const int _minTtsLength = 20; // TTS最小文本长度
bool _isDisposed = false; // 添加销毁标记
ChatController({
required this.agentId,
required this.agentName,
required this.agentAvatar,
this.agentSubtitle,
required this.systemPrompt,
required this.welcomeMessage,
}) : _aiService = VolcanoAIService() {
ChatController() {
_initControllers();
_initTtsService();
_initFromArguments();
}
void _initFromArguments() {
final arguments = Get.arguments as Map<String, dynamic>;
agentId = arguments['agentId'] as String;
agentName = arguments['agentName'] as String;
agentAvatar = arguments['agentAvatar'] as String;
agentSubtitle = arguments['agentSubtitle'] as String?;
systemPrompt = arguments['systemPrompt'] as String;
welcomeMessage = arguments['welcomeMessage'] as String;
playVoiceOnEnter = arguments['playVoiceOnEnter'] as bool? ?? false;
}
void _initControllers() {
textController = TextEditingController();
scrollController = ScrollController();
}
void _initTtsService() {
try {
_ttsService = Get.find<VolcanoTtsService>();
} catch (_) {
// 如果找不到实例,创建一个新的
_ttsService = Get.put(VolcanoTtsService(), permanent: true);
}
// 默认开启TTS
_ttsService.isEnabled.value = true;
}
@ -56,23 +66,51 @@ class ChatController extends GetxController {
void onInit() async {
super.onInit();
await _initializeController();
// 如果需要在进入时播放语音,延迟一下以确保页面已加载
if (playVoiceOnEnter) {
Future.delayed(const Duration(milliseconds: 500), () {
generateSimpleGreeting();
});
}
}
Future<void> _initializeController() async {
try {
await _loadChatHistory();
// 如果没有聊天记录,显示欢迎消息并生成个性化问候语
if (messages.isEmpty) {
// 添加欢迎消息
final initialMessage = Message(
role: 'assistant',
content: welcomeMessage,
timestamp: DateTime.now(),
await _showWelcomeMessages();
} catch (e) {
Get.snackbar(
'Error',
'Failed to initialize chat: $e',
snackPosition: SnackPosition.BOTTOM,
);
messages.add(initialMessage);
}
}
// 生成并添加个性化问候语
Future<void> _showWelcomeMessages() async {
try {
if (messages.isEmpty && agentName.isNotEmpty) {
// 生成动态问候语
await _generatePersonalizedGreeting();
}
// 保存聊天记录并滚动到底部
await _saveChatHistory();
Future.delayed(const Duration(milliseconds: 100), _scrollToBottom);
} catch (e) {
print('显示欢迎消息失败: $e');
// 如果生成问候语失败,确保至少显示静态欢迎消息
if (_ttsService.isEnabled.value) {
_ttsService.speak(welcomeMessage);
}
}
}
Future<void> _generatePersonalizedGreeting() async {
if (_isDisposed) return;
String fullGreeting = '';
try {
currentStreamMessage.value = '';
_currentAssistantMessage = Message(
@ -82,36 +120,39 @@ class ChatController extends GetxController {
);
messages.add(_currentAssistantMessage!);
await for (final chunk in _aiService.generateGreeting(
agentName: agentName,
// 构建用于生成个性化问候语的消息列表
final greetingMessages = [
{'role': 'system', 'content': systemPrompt},
{
'role': 'user',
'content': '请根据你的身份和性格,用一句简短的话向我打个招呼,要体现你的特点,不要超过18个字。'
},
];
await for (final chunk in _aiService.sendMessageStream(
messages: greetingMessages,
systemPrompt: systemPrompt,
)) {
if (_isDisposed) break;
// 更新消息内容
currentStreamMessage.value += chunk;
_updateAssistantMessage(currentStreamMessage.value);
// 累积文本用于TTS
if (!_isDisposed && _ttsService.isEnabled.value) {
_ttsService.speak(chunk);
}
fullGreeting += chunk;
}
// 保存聊天记录
if (!_isDisposed) {
await _saveChatHistory();
// 延迟滚动以确保布局完成
Future.delayed(const Duration(milliseconds: 100), _scrollToBottom);
// 完整问候语生成完毕后,一次性播放
if (!_isDisposed &&
_ttsService.isEnabled.value &&
fullGreeting.isNotEmpty) {
await _ttsService.speak(fullGreeting);
}
} catch (e) {
print('生成问候语失败: $e');
// 如果生成问候语失败,移除空消息并只播放欢迎消息
// 移除未完成的消息
if (_currentAssistantMessage != null) {
messages.remove(_currentAssistantMessage);
}
if (_ttsService.isEnabled.value) {
_ttsService.speak(welcomeMessage);
}
} finally {
if (!_isDisposed) {
_currentAssistantMessage = null;
@ -119,22 +160,50 @@ class ChatController extends GetxController {
}
}
}
} catch (e) {
Get.snackbar(
'Error',
'Failed to initialize chat: $e',
snackPosition: SnackPosition.BOTTOM,
);
}
}
@override
void onClose() {
_isDisposed = true; // 标记为已销毁
_cleanupResources();
super.onClose();
}
@override
void dispose() {
_cleanupResources();
super.dispose();
}
void _cleanupResources() {
if (_isDisposed) return; // 防止重复清理
_isDisposed = true;
try {
if (textController.hasListeners) {
textController.dispose();
}
} catch (e) {
print('Disposing textController failed: $e');
}
try {
if (scrollController.hasClients) {
scrollController.dispose();
_ttsService.stop(); // 停止当前播放
super.onClose();
}
} catch (e) {
print('Disposing scrollController failed: $e');
}
try {
_ttsService.stop();
} catch (e) {
print('Stopping TTS service failed: $e');
}
// 清理其他资源
currentStreamMessage.value = '';
_currentAssistantMessage = null;
_pendingTtsText = '';
}
Future<void> _loadChatHistory() async {
@ -142,7 +211,8 @@ class ChatController extends GetxController {
final historyJson = _storage.read('${_storagePrefix}${agentId}');
if (historyJson != null) {
final List<dynamic> historyList = json.decode(historyJson);
messages.value = historyList.map((item) => Message.fromJson(item)).toList();
messages.value =
historyList.map((item) => Message.fromJson(item)).toList();
}
} catch (e) {
Get.snackbar(
@ -155,7 +225,8 @@ class ChatController extends GetxController {
Future<void> _saveChatHistory() async {
try {
final historyJson = json.encode(messages.map((msg) => msg.toJson()).toList());
final historyJson =
json.encode(messages.map((msg) => msg!.toJson()).toList());
await _storage.write('${_storagePrefix}${agentId}', historyJson);
} catch (e) {
Get.snackbar(
@ -179,9 +250,8 @@ class ChatController extends GetxController {
void _updateAssistantMessage(String content) {
if (_currentAssistantMessage != null) {
final index = messages.indexWhere((msg) =>
msg.timestamp == _currentAssistantMessage!.timestamp &&
msg.role == 'assistant'
);
msg.timestamp == _currentAssistantMessage?.timestamp &&
msg.role == _currentAssistantMessage?.role);
if (index != -1) {
messages[index] = Message(
@ -219,7 +289,12 @@ class ChatController extends GetxController {
_pendingTtsText = '';
await for (final chunk in _aiService.sendMessageStream(
messages: messages.map((m) => {'role': m.role, 'content': m.content}).toList(),
messages: messages
.map((m) => {
'role': m.role,
'content': m.content,
})
.toList(),
systemPrompt: systemPrompt,
)) {
if (_isDisposed) break;
@ -231,12 +306,13 @@ class ChatController extends GetxController {
_pendingTtsText += chunk;
if (!_isDisposed && _ttsService.isEnabled.value) {
// 检查是否达到最小长度
if (_pendingTtsText.length >= _minTtsLength ) {
if (_pendingTtsText.length >= _minTtsLength) {
// 找到最后一个句子结束的位置
int lastSentenceEnd = _findLastSentenceEnd(_pendingTtsText);
if (lastSentenceEnd > 0) {
// 播放到最后一个句子结束的位置
String textToSpeak = _pendingTtsText.substring(0, lastSentenceEnd + 1);
String textToSpeak =
_pendingTtsText.substring(0, lastSentenceEnd + 1);
_ttsService.speak(textToSpeak);
// 保留剩余的文本
_pendingTtsText = _pendingTtsText.substring(lastSentenceEnd + 1);
@ -246,12 +322,13 @@ class ChatController extends GetxController {
}
// 处理剩余的文本
if (!_isDisposed && _ttsService.isEnabled.value && _pendingTtsText.isNotEmpty) {
if (!_isDisposed &&
_ttsService.isEnabled.value &&
_pendingTtsText.isNotEmpty) {
_ttsService.speak(_pendingTtsText);
}
_pendingTtsText = '';
// 流式传输完成后保存聊天记录
if (!_isDisposed) {
await _saveChatHistory();
}
@ -292,20 +369,7 @@ class ChatController extends GetxController {
try {
messages.clear();
await _storage.remove('${_storagePrefix}${agentId}');
// 重新添加欢迎消息
final initialMessage = Message(
role: 'assistant',
content: welcomeMessage,
timestamp: DateTime.now(),
);
messages.add(initialMessage);
await _saveChatHistory();
// 播放欢迎消息
if (_ttsService.isEnabled.value) {
_ttsService.speak(welcomeMessage);
}
await _showWelcomeMessages();
} catch (e) {
Get.snackbar(
'Error',
@ -318,4 +382,53 @@ class ChatController extends GetxController {
void toggleTTS() {
_ttsService.toggleEnabled();
}
/// 生成简单的问候语
Future<void> generateSimpleGreeting() async {
try {
currentStreamMessage.value = '';
_currentAssistantMessage = Message(
role: 'assistant',
content: '',
timestamp: DateTime.now(),
);
messages.add(_currentAssistantMessage!);
// 构建用于生成问候语的消息列表
final greetingMessages = [
{'role': 'system', 'content': systemPrompt},
{'role': 'user', 'content': '请用一句简短的话向我打个招呼,要体现你的特点,不要超过12个字。'},
];
String fullGreeting = '';
await for (final chunk in _aiService.sendMessageStream(
messages: greetingMessages,
systemPrompt: systemPrompt,
)) {
if (_isDisposed) break;
// 更新消息内容
currentStreamMessage.value += chunk;
_updateAssistantMessage(currentStreamMessage.value);
fullGreeting += chunk;
}
// 完整问候语生成完毕后,一次性播放
if (!_isDisposed && _ttsService.isEnabled.value && fullGreeting.isNotEmpty) {
await _ttsService.speak(fullGreeting);
}
} catch (e) {
print('生成问候语失败: $e');
// 移除未完成的消息
if (_currentAssistantMessage != null) {
messages.remove(_currentAssistantMessage);
}
} finally {
if (!_isDisposed) {
_currentAssistantMessage = null;
currentStreamMessage.value = '';
}
}
}
}

28
lib/modules/explore/views/explore_view.dart

@ -21,26 +21,16 @@ class ExploreView extends GetView<ExploreController> {
return;
}
// 初始化聊天控制器
final chatController = ChatController(
agentId: agent.id,
agentName: agent.name,
agentAvatar: agent.avatarUrl,
agentSubtitle: agent.description,
systemPrompt: agent.systemPrompt,
welcomeMessage: agent.welcomeMessage,
);
// 注入控制器
Get.delete<ChatController>(); // 确保删除之前的控制器
Get.put(chatController);
// 导航到聊天页面
Get.to(
() => const ChatView(),
transition: Transition.rightToLeft,
duration: const Duration(milliseconds: 300),
);
Get.toNamed('/chat', arguments: {
'agentId': agent.id,
'agentName': agent.name,
'agentAvatar': agent.avatarUrl,
'agentSubtitle': agent.description,
'systemPrompt': agent.systemPrompt,
'welcomeMessage': agent.welcomeMessage,
'playVoiceOnEnter': false,
});
}
Widget _buildChatItem(ChatItem item) {

5
pubspec.yaml

@ -20,15 +20,18 @@ dependencies:
audio_session: ^0.1.18
flutter_markdown: ^0.6.18+2
markdown: ^7.1.1
permission_handler: ^11.3.0
permission_handler: ^11.4.0
audio_service: ^0.18.12
web_socket_channel: ^2.4.4
flutter_dotenv: ^5.1.0
synchronized: ^3.1.0
window_size:
git:
url: https://github.com/google/flutter-desktop-embedding.git
path: plugins/window_size
ref: 6c66ad23ee79749f30a8eece542cf54eaf157ed8
flutter_local_notifications: ^18.0.1
device_info_plus: ^10.1.2
dev_dependencies:
flutter_test:

Loading…
Cancel
Save