From b4b872bd2a1fa5f5efdf3ba3e97dd7aa25d50aee Mon Sep 17 00:00:00 2001 From: tanlongsheng <252620078@qq.com> Date: Wed, 30 Jul 2025 19:03:26 +0800 Subject: [PATCH 01/20] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=96=B0=E6=89=8B?= =?UTF-8?q?=E6=8C=87=E5=BC=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/main.dart | 6 +- lib/modules/home/views/home_view.dart | 126 ++++++++++++++++---------- pubspec.yaml | 1 + 3 files changed, 82 insertions(+), 51 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 7c5818e36..b35b8380c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_easyloading/flutter_easyloading.dart'; +import 'package:flutter_intro/flutter_intro.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:get/get.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; @@ -92,7 +93,10 @@ class MainApp extends StatelessWidget { data: MediaQuery.of(context).copyWith( textScaler: TextScaler.noScaling, ), - child: widget!, + child: Intro( + padding: EdgeInsets.zero, + child: widget!, + ), ); }, ), diff --git a/lib/modules/home/views/home_view.dart b/lib/modules/home/views/home_view.dart index 8bcbac345..18b24b210 100644 --- a/lib/modules/home/views/home_view.dart +++ b/lib/modules/home/views/home_view.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; +import 'package:flutter_intro/flutter_intro.dart'; import 'package:get/get.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import '../controllers/home_controller.dart'; -import '../../../routes/app_pages.dart'; import '../../../routes/app_routes.dart'; import '../../../core/widgets/common_bottom_nav.dart'; @@ -623,23 +623,32 @@ class HomeView extends GetView { // 左侧大卡片 - AI助理 Expanded( flex: 1, - child: _buildFeatureCard( - title: 'aiAssistant'.tr, // AI全能助手 - description: 'aiAssistantInfo'.tr, // 24小时专属顾问,解答问题、提供建议 - icon: Icons.assistant, - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [ - Color(0xFF0066CC).withOpacity(0.3), - Color(0xFF5E5CE6).withOpacity(0.3), - ], + child: IntroStepBuilder( + order: 3, + text: 'AI全能助手', + onHighlightWidgetTap: () { + Intro.of(Get.context!).dispose(); + controller.exploreAIFeatures(); + }, + builder: (BuildContext context, GlobalKey key) => _buildFeatureCard( + key: key, + title: 'aiAssistant'.tr, // AI全能助手 + description: 'aiAssistantInfo'.tr, // 24小时专属顾问,解答问题、提供建议 + icon: Icons.assistant, + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Color(0xFF0066CC).withOpacity(0.3), + Color(0xFF5E5CE6).withOpacity(0.3), + ], + ), + onTap: controller.exploreAIFeatures, + iconOnTop: true, + height: 175.h, // 增加高度 + isDarkMode: isDarkMode, + iconColor: const Color(0xFF0066CC), ), - onTap: controller.exploreAIFeatures, - iconOnTop: true, - height: 175.h, // 增加高度 - isDarkMode: isDarkMode, - iconColor: const Color(0xFF0066CC), ), ), @@ -651,47 +660,62 @@ class HomeView extends GetView { child: Column( children: [ // 右上卡片 - 同声翻译 - _buildFeatureCard( - title: 'simultaneousInterpretation'.tr, // 同声传译 - description: - 'simultaneousInterpretationInfo'.tr, // 80+语言实时翻译,出国无忧 - icon: Icons.translate, - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [ - const Color(0xFF4CAF50).withOpacity(0.3), - const Color(0xFF009688).withOpacity(0.3), - ], + IntroStepBuilder( + order: 1, + text: '同声传译', + // onWidgetLoad: () { + // Intro.of(Get.context!).start(); + // }, + builder: (BuildContext context, GlobalKey key) => + _buildFeatureCard( + key: key, + title: 'simultaneousInterpretation'.tr, // 同声传译 + description: + 'simultaneousInterpretationInfo'.tr, // 80+语言实时翻译,出国无忧 + icon: Icons.translate, + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + const Color(0xFF4CAF50).withOpacity(0.3), + const Color(0xFF009688).withOpacity(0.3), + ], + ), + onTap: controller.openTranslation, + height: 83.h, // 增加高度 + isDarkMode: isDarkMode, + iconColor: const Color(0xFF4CAF50), ), - onTap: controller.openTranslation, - height: 83.h, // 增加高度 - isDarkMode: isDarkMode, - iconColor: const Color(0xFF4CAF50), ), SizedBox(height: 5.h), // 减少间距 // 右下卡片 - // 右下卡片 - 会议助手 - _buildFeatureCard( - title: 'conferenceAssistant'.tr, // 商务会议助手 - description: 'conferenceAssistantInfo'.tr, // 实时记录会议纪要,多语言翻译 - icon: Icons.people, - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [ - Color(0xFFab47bc).withOpacity(0.3), // 薰衣草紫 - Color(0xFFec407a).withOpacity(0.3), // 品红 - ], + IntroStepBuilder( + order: 2, + text: '商务会议助手', + builder: (BuildContext context, GlobalKey key) => + _buildFeatureCard( + key: key, + title: 'conferenceAssistant'.tr, // 商务会议助手 + description: 'conferenceAssistantInfo'.tr, // 实时记录会议纪要,多语言翻译 + icon: Icons.people, + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Color(0xFFab47bc).withOpacity(0.3), // 薰衣草紫 + Color(0xFFec407a).withOpacity(0.3), // 品红 + ], + ), + onTap: () { + Get.toNamed(Routes.meeting); + }, + height: 85.h, // 增加高度 + isDarkMode: isDarkMode, + iconColor: const Color(0xFFd81b60), // 品红 ), - onTap: () { - Get.toNamed(Routes.meeting); - }, - height: 85.h, // 增加高度 - isDarkMode: isDarkMode, - iconColor: const Color(0xFFd81b60), // 品红 ), ], ), @@ -702,6 +726,7 @@ class HomeView extends GetView { // 特性卡片构建方法 Widget _buildFeatureCard({ + GlobalKey? key, required String title, required String description, required IconData icon, @@ -713,6 +738,7 @@ class HomeView extends GetView { Color iconColor = Colors.white, }) { return GestureDetector( + key: key, onTap: onTap, child: Container( height: height ?? 175.h, diff --git a/pubspec.yaml b/pubspec.yaml index 64a6f01ac..fb92cb9b7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -27,6 +27,7 @@ dependencies: parchment: ^1.24.0 htmltopdfwidgets: ^1.1.0 wifi_iot: ^0.3.19+2 + flutter_intro: ^3.4.0 permission_handler: ^10.4.5 web_socket_channel: ^2.4.0 flutter_dotenv: ^5.1.0 From 079bf19b1fe58ba7ebd3c918728075379ae8040f Mon Sep 17 00:00:00 2001 From: lxm <1274844347@qq.com> Date: Wed, 30 Jul 2025 20:54:14 +0800 Subject: [PATCH 02/20] =?UTF-8?q?feat(agent):=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E7=94=B5=E8=AF=9D=E6=A8=A1=E5=BC=8FUI=E5=B9=B6=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0=E5=BD=95=E9=9F=B3=E5=88=87=E6=8D=A2=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将声音条组件居中显示 - 重构电话模式控制界面,移除冗余代码 - 增加电话模式专用的录音状态切换功能 - 改进错误处理逻辑,确保状态同步 --- .../agent/controllers/agent_controller.dart | 74 ++++-- lib/modules/agent/views/agent_view.dart | 219 +++--------------- .../agent/widgets/sound_bar_widget.dart | 2 +- 3 files changed, 95 insertions(+), 200 deletions(-) diff --git a/lib/modules/agent/controllers/agent_controller.dart b/lib/modules/agent/controllers/agent_controller.dart index dda9ed2c4..149c05b22 100644 --- a/lib/modules/agent/controllers/agent_controller.dart +++ b/lib/modules/agent/controllers/agent_controller.dart @@ -153,8 +153,6 @@ class AgentController extends GetxController { _setupMediaSessionCallbacks(); //设置媒体会话回调 _setupTextInputListener(); //设置文本输入监听器 - - startVoiceInput(); //开启语音输入 } @@ -1013,8 +1011,6 @@ class AgentController extends GetxController { } } - - // 开始语音输入 Future startVoiceInput() async { if (isListening.value) return; @@ -1309,29 +1305,71 @@ class AgentController extends GetxController { return; } - // 切换到语音模式并开始语音输入 + // 打电话模式 isTextInputMode.value = false; - // 打电话模式不使用按住说话模式,而是持续语音输入 - //isPushToTalkMode.value = false; - // 开始语音识别 - Logger.i(TAG, '开始打电话模式,开始语音输入'); + Logger.i(TAG, '打电话模式已启动,开始语音输入'); await AgentService.startConversation(); - //} } // 结束打电话模式 void endPhoneCall() async { Logger.i(TAG, '🔥 结束打电话模式'); - // 退出打电话模式 - isPhoneCallMode.value = false; + try { + // 如果正在录音,先停止录音 + if (isListening.value) { + final result = await AgentService.stopConversation(); - // 停止语音输入 - //await stopVoiceInput(); - // 开始语音识别 - await AgentService.stopConversation(); - // 恢复之前的输入模式,而不是强制切换到文字模式 - _restorePreviousInputMode(); + if (result) { + isListening.value = false; + } + } + + // 退出打电话模式 + isPhoneCallMode.value = false; + _restorePreviousInputMode(); + } catch (e) { + Logger.e(TAG, '结束打电话模式失败: $e'); + + // 即使出现异常,也要确保退出打电话模式 + isPhoneCallMode.value = false; + isListening.value = false; + _restorePreviousInputMode(); + } + } + + // 打电话模式专用:切换录音状态 + Future togglePhoneCallRecording() async { + Logger.i(TAG, '🎤 打电话模式切换录音状态'); + // 只有在打电话模式下才能使用此方法 + if (!isPhoneCallMode.value) { + Logger.w(TAG, '不在打电话模式,无法切换录音状态'); + return; + } + + try { + if (isListening.value) { + // 当前正在录音,停止录音 + Logger.i(TAG, '停止打电话模式录音'); + final result = await AgentService.stopConversation(); + + // 手动更新状态,确保UI同步 + if (result) { + isListening.value = false; + } + } else { + // 当前未录音,开始录音 + Logger.i(TAG, '开始打电话模式录音'); + final result = await AgentService.startConversation(); + + // 手动更新状态,确保UI同步 + if (result) { + isListening.value = true; + } + } + } catch (e) { + Logger.e(TAG, '打电话模式录音状态切换失败: $e'); + } } // 耳机唤醒通话模式(专用入口) diff --git a/lib/modules/agent/views/agent_view.dart b/lib/modules/agent/views/agent_view.dart index 3703ec3f4..0ef9a63ac 100644 --- a/lib/modules/agent/views/agent_view.dart +++ b/lib/modules/agent/views/agent_view.dart @@ -922,20 +922,12 @@ class _AgentViewState extends State with WidgetsBindingObserver { child: SafeArea( child: Column( children: [ - // 顶部标题栏 - 保持背景 - // Container( - // color: isDarkMode - // ? Colors.black.withOpacity(0.8) - // : Colors.white.withOpacity(0.8), - // child: _buildPhoneCallHeader(isDarkMode), - // ), - - // 中间音频可视化区域 - 完全透明 + // 中间空白区域 - 完全透明 const Expanded( child: SizedBox(), // 空的占位符,完全透明 ), - // 底部控制按钮 - 保持背景 + // 底部控制按钮 - 包含音频波纹 Container( color: isDarkMode ? Colors.black : Colors.white, child: _buildPhoneCallControls(isDarkMode), @@ -946,185 +938,43 @@ class _AgentViewState extends State with WidgetsBindingObserver { ); } - // 构建打电话模式标题栏 - Widget _buildPhoneCallHeader(bool isDarkMode) { - return Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - child: Row( - children: [ - // 返回按钮 - GestureDetector( - onTap: () => controller.endPhoneCall(), - child: Container( - width: 40, - height: 40, - decoration: BoxDecoration( - color: isDarkMode ? Colors.grey[800] : Colors.grey[200], - borderRadius: BorderRadius.circular(20), - ), - child: Icon( - Icons.arrow_back, - color: isDarkMode ? Colors.white : Colors.black, - size: 20, - ), - ), - ), - - const SizedBox(width: 16), - - // 标题 - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'aiVoiceCall'.tr, // AI语音通话 - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - color: isDarkMode ? Colors.white : Colors.black, - ), - ), - Obx(() { - final status = controller.isListening.value - ? 'inCall'.tr // 正在通话中... - : controller.isSpeaking.value - ? 'aiReplying'.tr // AI正在回复... - : 'waitingToStart'.tr; // 等待开始... - return Text( - status, - style: TextStyle( - fontSize: 14, - color: isDarkMode ? Colors.grey[400] : Colors.grey[600], - ), - ); - }), - ], - ), - ), - ], - ), - ); - } - - // 构建打电话模式音频可视化 - Widget _buildPhoneCallVisualization(bool isDarkMode) { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - // 音频可视化圆圈 - Obx(() { - final isActive = - controller.isListening.value || controller.isSpeaking.value; - return AnimatedContainer( - duration: const Duration(milliseconds: 300), - width: isActive ? 200 : 160, - height: isActive ? 200 : 160, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: isDarkMode - ? Colors.blue[900]?.withOpacity(0.3) - : Colors.blue[100], - border: Border.all( - color: Colors.blue, - width: isActive ? 3 : 2, - ), - ), - child: Icon( - Icons.phone, - size: isActive ? 80 : 60, - color: Colors.blue, - ), - ); - }), - - const SizedBox(height: 32), - - // 状态文字 - Obx(() { - String statusText = 'clickToStartCall'.tr; // 点击开始通话 - if (controller.isListening.value) { - statusText = 'listeningToYou'.tr; // 正在听您说话... - } else if (controller.isSpeaking.value) { - statusText = 'aiReplying'.tr; // AI正在回复... - } else if (controller.isProcessing.value) { - statusText = 'processing'.tr; // 正在处理... - } - - return Text( - statusText, - style: TextStyle( - fontSize: 16, - color: isDarkMode ? Colors.grey[300] : Colors.grey[700], - fontWeight: FontWeight.w500, - ), - ); - }), - ], - ), - ); - } - // 构建打电话模式控制按钮 Widget _buildPhoneCallControls(bool isDarkMode) { return Container( padding: EdgeInsets.symmetric(horizontal: 32.w, vertical: 24.h), child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, + crossAxisAlignment: CrossAxisAlignment.center, // 确保垂直居中对齐 children: [ - // 静音按钮 + // 录音控制按钮(左侧) - 控制录音输入功能 Obx(() => _buildPhoneControlButton( - icon: controller.isTtsEnabled.value - ? Icons.volume_up - : Icons.volume_off, - onTap: () => controller.toggleTts(), + icon: controller.isListening.value + ? Icons.mic // 录音中显示麦克风图标 + : Icons.mic_off, // 未录音显示麦克风关闭图标 + onTap: () => controller.togglePhoneCallRecording(), isDarkMode: isDarkMode, - isActive: controller.isTtsEnabled.value, + isActive: controller.isListening.value, + // backgroundColor: controller.isListening.value + // ? Colors.red // 录音中显示红色背景 + // : Theme.of(context).primaryColor, // 未录音显示主题色 )), - // 主要通话按钮 - Obx(() { - final isActive = controller.isListening.value; - return GestureDetector( - // onTap: () { - // if (isActive) { - // controller.stopVoiceInput(); - // } else { - // controller.startVoiceInput(); - // } - // }, - child: Container( - width: 80.w, - height: 80.h, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: isActive ? Colors.red : Colors.green, - boxShadow: [ - BoxShadow( - color: (isActive ? Colors.red : Colors.green) - .withOpacity(0.3), - blurRadius: 12.w, - spreadRadius: 2.w, - ), - ], - ), - child: Icon( - isActive ? Icons.call_end : Icons.call, - color: Colors.white, - size: 36.sp, - ), - ), - ); - }), + // 音频波纹组件(中间) - 仅显示动态波形效果,无其他UI元素 + Container( + width: 120.w, // 设置固定宽度 + height: 64.h, // 与按钮高度一致 + alignment: Alignment.center, // 确保波纹居中 + child: const AgentSoundBarsWidget(barCount: 12), // 适应宽度的波纹条数 + ), - // 结束通话按钮 + // 关闭打电话模式按钮(右侧) - 退出打电话模式 _buildPhoneControlButton( - icon: Icons.call_end, + icon: Icons.close, onTap: () => controller.endPhoneCall(), isDarkMode: isDarkMode, isActive: false, - backgroundColor: Colors.red, + //backgroundColor: Colors.red, + iconColor: Colors.redAccent, ), ], ), @@ -1138,24 +988,31 @@ class _AgentViewState extends State with WidgetsBindingObserver { required bool isDarkMode, required bool isActive, Color? backgroundColor, + Color? iconColor, }) { return GestureDetector( onTap: onTap, child: Container( - width: 56.w, - height: 56.h, + width: 64.w, // 按钮尺寸 + height: 64.h, decoration: BoxDecoration( shape: BoxShape.circle, color: backgroundColor ?? - (isDarkMode ? Colors.grey[800] : Colors.grey[200]), - border: isActive ? Border.all(color: Colors.blue, width: 2.w) : null, + (isDarkMode ? Colors.grey[800] : Colors.grey[250]), + border: Border.all(color: Colors.blue, width: 2.w), + boxShadow: [ + BoxShadow( + color: (backgroundColor ?? Colors.grey).withValues(alpha: 0.3), + blurRadius: 8.w, + spreadRadius: 1.w, + offset: Offset(0, 2.h), + ), + ], ), child: Icon( icon, - color: backgroundColor != null - ? Colors.white - : (isDarkMode ? Colors.white : Colors.black), - size: 24.sp, + color: iconColor ?? (isDarkMode ? Colors.white : Colors.black), + size: 28.sp, // 图标尺寸 ), ), ); diff --git a/lib/modules/agent/widgets/sound_bar_widget.dart b/lib/modules/agent/widgets/sound_bar_widget.dart index c108bcca2..11fb9ed86 100644 --- a/lib/modules/agent/widgets/sound_bar_widget.dart +++ b/lib/modules/agent/widgets/sound_bar_widget.dart @@ -106,7 +106,7 @@ class SoundBarsWidget extends StatelessWidget { @override Widget build(BuildContext context) { return Row( - mainAxisAlignment: MainAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, children: List.generate(barCount, (index) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 1), From 186df116ff28be150953129b8e45fc864cb19110 Mon Sep 17 00:00:00 2001 From: tanlongsheng <252620078@qq.com> Date: Thu, 31 Jul 2025 13:24:46 +0800 Subject: [PATCH 03/20] =?UTF-8?q?=E4=BF=AE=E6=94=B9iOS=E3=80=81iPad?= =?UTF-8?q?=E5=8F=AA=E8=83=BD=E7=AB=96=E5=B1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ios/Runner/Info.plist | 8 +++----- lib/main.dart | 3 +++ pubspec.yaml | 4 ++-- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index f1195f0fc..8ff8a78c2 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -122,17 +122,15 @@ UIDeviceFamily 1 + 2 UILaunchStoryboardName LaunchScreen UIMainStoryboardFile Main - UISupportedInterfaceOrientations~ipad + UISupportedInterfaceOrientations - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight + UIInterfaceOrientationPortrait UIViewControllerBasedStatusBarAppearance diff --git a/lib/main.dart b/lib/main.dart index b35b8380c..fd95d2863 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_easyloading/flutter_easyloading.dart'; import 'package:flutter_intro/flutter_intro.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; @@ -20,6 +21,8 @@ void main() async { // 确保Flutter绑定初始化 WidgetsFlutterBinding.ensureInitialized(); + await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); + // 初始化存储 - 在创建应用之前初始化 await GetStorage.init(); diff --git a/pubspec.yaml b/pubspec.yaml index bc5b4ea8c..2fa9c86e6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: voitrans description: "Voitrans - AI Voice Assistant." publish_to: "none" -version: 1.0.3+11 +version: 1.0.3+19 environment: sdk: ">=3.3.0 <4.0.0" @@ -60,7 +60,7 @@ dependencies: font_awesome_flutter: ^10.8.0 lottie: ^3.3.1 app_settings: 5.2.0 - fluwx: ^5.4.1 + fluwx: ^5.7.1 url_launcher: ^6.3.1 firebase_core: ^3.12.1 firebase_auth: ^5.5.1 From b50cdb1f78b70694e555be0142369fd98690823b Mon Sep 17 00:00:00 2001 From: lxm <1274844347@qq.com> Date: Thu, 31 Jul 2025 15:47:05 +0800 Subject: [PATCH 04/20] =?UTF-8?q?refactor(version):=20=E5=B0=86=E7=89=88?= =?UTF-8?q?=E6=9C=AC=E6=A3=80=E6=9F=A5=E9=80=BB=E8=BE=91=E4=BB=8Ehome?= =?UTF-8?q?=E8=BF=81=E7=A7=BB=E5=88=B0splash=E6=8E=A7=E5=88=B6=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将版本更新检查功能从HomeController迁移到SplashController,使其在应用启动时执行 移除HomeController中不再需要的相关代码和依赖 --- .../home/controllers/home_controller.dart | 116 --------------- .../splash/controllers/splash_controller.dart | 137 +++++++++++++++--- 2 files changed, 116 insertions(+), 137 deletions(-) diff --git a/lib/modules/home/controllers/home_controller.dart b/lib/modules/home/controllers/home_controller.dart index 31db67e15..3f952483e 100644 --- a/lib/modules/home/controllers/home_controller.dart +++ b/lib/modules/home/controllers/home_controller.dart @@ -5,9 +5,6 @@ import '../../../core/utils/logger.dart'; import 'package:flutter/material.dart'; import '../../../data/services/bluetooth_manager.dart'; import '../../../data/services/ble_manager.dart'; -import '../../../data/services/version_update_service.dart'; -import '../../common/widgets/version_update_dialog.dart'; -import 'package:get_storage/get_storage.dart'; /// 连接状态枚举 enum ConnectionStatus { @@ -53,14 +50,6 @@ class HomeController extends GetxController { final _bluetoothManager = Get.find(); final _bleManager = Get.find(); - final _storage = GetStorage(); - - // 会话级别的版本检查标记(静态变量,整个应用会话期间保持) - static bool _hasCheckedVersionInSession = false; - - // 版本检查相关常量 - static const String _lastVersionCheckKey = 'last_version_check_timestamp'; - static const int _versionCheckIntervalHours = 24; // 24小时间隔 @override void onInit() { super.onInit(); @@ -78,12 +67,6 @@ class HomeController extends GetxController { void _initWithCachedInfo() { _setupStateListeners(); _checkConnectionStatus(); - - // 延迟检查版本更新,避免影响页面加载 - // 混合检查机制:会话检查 + 24小时间隔检查 - Future.delayed(const Duration(seconds: 2), () { - _checkVersionUpdateIfNeeded(); - }); } void _setupStateListeners() { @@ -271,103 +254,4 @@ class HomeController extends GetxController { : 'unknown'.tr; } - /// 混合检查机制:判断是否需要进行版本检查 - /// 检查条件:1. 会话内未检查过 OR 2. 距离上次检查超过24小时 - void _checkVersionUpdateIfNeeded() { - try { - final now = DateTime.now(); - final lastCheckTimestamp = _storage.read(_lastVersionCheckKey); - - bool shouldCheck = false; - String reason = ''; - - // 条件1:会话内未检查过 - if (!_hasCheckedVersionInSession) { - shouldCheck = true; - reason = '会话内首次检查'; - } - // 条件2:距离上次检查超过24小时 - else if (lastCheckTimestamp != null) { - final lastCheckTime = DateTime.fromMillisecondsSinceEpoch(lastCheckTimestamp); - final timeDifference = now.difference(lastCheckTime); - - if (timeDifference.inHours >= _versionCheckIntervalHours) { - shouldCheck = true; - reason = '距离上次检查已超过${_versionCheckIntervalHours}小时'; - } - } - // 条件3:从未检查过(首次使用应用) - else { - shouldCheck = true; - reason = '首次使用应用'; - } - - if (shouldCheck) { - Logger.i(_tag, '开始版本检查 - 原因: $reason'); - _performVersionCheck(); - } else { - Logger.i(_tag, '跳过版本检查 - 会话内已检查且未超过时间间隔'); - } - } catch (e) { - Logger.e(_tag, '版本检查判断失败: $e'); - } - } - - /// 执行版本检查(在后台进行,不阻塞启动流程) - Future _performVersionCheck() async { - try { - final versionService = VersionUpdateService.to; - final updateInfo = await versionService.checkForUpdate(); - - if (updateInfo.hasUpdate) { - Logger.i(_tag, '发现新版本: ${updateInfo.latestVersion}'); - // 显示更新对话框 - await VersionUpdateDialog.show(updateInfo); - } else { - Logger.i(_tag, '当前已是最新版本: ${updateInfo.currentVersion}'); - } - - // 更新检查记录 - _updateVersionCheckRecord(); - } catch (e) { - Logger.e(_tag, '版本检查失败: $e'); - // 即使检查失败,也更新记录,避免频繁重试 - _updateVersionCheckRecord(); - } - } - - /// 更新版本检查记录 - void _updateVersionCheckRecord() { - try { - // 更新时间戳 - final now = DateTime.now(); - _storage.write(_lastVersionCheckKey, now.millisecondsSinceEpoch); - - // 标记本次会话已检查过版本 - _hasCheckedVersionInSession = true; - - Logger.i(_tag, '版本检查记录已更新: ${now.toIso8601String()}'); - } catch (e) { - Logger.e(_tag, '更新版本检查记录失败: $e'); - } - } - - /// 重置版本检查标记(可用于强制重新检查) - /// 注意:这会清除会话标记,但不会清除时间戳记录 - static void resetVersionCheckFlag() { - _hasCheckedVersionInSession = false; - Logger.i('HomeCtrl', '版本检查会话标记已重置'); - } - - /// 完全重置版本检查状态(包括时间戳) - /// 用于测试或特殊情况下强制重新检查 - void forceResetVersionCheck() { - try { - _hasCheckedVersionInSession = false; - _storage.remove(_lastVersionCheckKey); - Logger.i(_tag, '版本检查状态已完全重置'); - } catch (e) { - Logger.e(_tag, '重置版本检查状态失败: $e'); - } - } } diff --git a/lib/modules/splash/controllers/splash_controller.dart b/lib/modules/splash/controllers/splash_controller.dart index ba834cdc1..4badc4d43 100644 --- a/lib/modules/splash/controllers/splash_controller.dart +++ b/lib/modules/splash/controllers/splash_controller.dart @@ -14,10 +14,12 @@ import '../../../data/services/network/api.dart'; import '../../../data/services/version_update_service.dart'; import '../../../modules/common/widgets/version_update_dialog.dart'; import '../../../modules/common/widgets/privacy_policy_dialog.dart'; -import '../../common/webview_page.dart'; +import '../../../core/utils/logger.dart'; class SplashController extends GetxController { final GetStorage _storage = GetStorage(); + static const String _tag = 'SplashCtrl'; + // 进度条动画控制 final _loadingProgress = 0.0.obs; double get loadingProgress => _loadingProgress.value; @@ -34,6 +36,13 @@ class SplashController extends GetxController { // 主题服务 final ThemeService _themeService = ThemeService(); + // 会话级别的版本检查标记(静态变量,整个应用会话期间保持) + static bool _hasCheckedVersionInSession = false; + + // 版本检查相关常量 + static const String _lastVersionCheckKey = 'last_version_check_timestamp'; + static const int _versionCheckIntervalHours = 24; // 24小时间隔 + @override void onInit() { super.onInit(); @@ -50,7 +59,7 @@ class SplashController extends GetxController { } }); - //弹出用户追中问题 + //弹出用户追踪问题 WidgetsFlutterBinding.ensureInitialized().addPostFrameCallback((_) async { final status = await AppTrackingTransparency.requestTrackingAuthorization(); @@ -60,12 +69,17 @@ class SplashController extends GetxController { void _startNormalFlow() { _startLoadingAnimation(); _loadAppResources(); + + // 延迟执行版本检查,避免影响启动速度 + Future.delayed(const Duration(seconds: 1), () { + _checkVersionUpdateIfNeeded(); + }); } void _onPrivacyAccepted() { _storage.write('hasAcceptedPrivacy', true); Get.back(); // 关闭弹窗 - _startNormalFlow(); // 开始正常流程 + _startNormalFlow(); // 开始正常流程(已包含版本检查) } void _onPrivacyRejected() { @@ -205,22 +219,103 @@ class SplashController extends GetxController { } } - /// 检查版本更新(后台执行,不阻塞启动流程) - // void _checkVersionUpdate() { - // // 延迟执行版本检查,避免影响启动速度 - // Timer(const Duration(seconds: 1), () async { - // try { - // final versionService = VersionUpdateService.to; - // final updateInfo = await versionService.checkForUpdate(); - - // if (updateInfo.hasUpdate) { - // // 显示更新对话框 - // await VersionUpdateDialog.show(updateInfo); - // } - // } catch (e) { - // print('版本检查失败: $e'); - // // 版本检查失败不影响用户使用,静默处理 - // } - // }); - // } + /// 混合检查机制:判断是否需要进行版本检查 + /// 检查条件:1. 会话内未检查过 OR 2. 距离上次检查超过24小时 + void _checkVersionUpdateIfNeeded() { + try { + final now = DateTime.now(); + final lastCheckTimestamp = _storage.read(_lastVersionCheckKey); + + bool shouldCheck = false; + String reason = ''; + + // 条件1:会话内未检查过 + if (!_hasCheckedVersionInSession) { + shouldCheck = true; + reason = '会话内首次检查'; + } + // 条件2:距离上次检查超过24小时 + else if (lastCheckTimestamp != null) { + final lastCheckTime = DateTime.fromMillisecondsSinceEpoch(lastCheckTimestamp); + final timeDifference = now.difference(lastCheckTime); + + if (timeDifference.inHours >= _versionCheckIntervalHours) { + shouldCheck = true; + reason = '距离上次检查已超过${_versionCheckIntervalHours}小时'; + } + } + // 条件3:从未检查过(首次使用应用) + else { + shouldCheck = true; + reason = '首次使用应用'; + } + + if (shouldCheck) { + Logger.i(_tag, '开始版本检查 - 原因: $reason'); + _performVersionCheck(); + } else { + Logger.i(_tag, '跳过版本检查 - 会话内已检查且未超过时间间隔'); + } + } catch (e) { + Logger.e(_tag, '版本检查判断失败: $e'); + } + } + + /// 执行版本检查(在后台进行,不阻塞启动流程) + Future _performVersionCheck() async { + try { + final versionService = VersionUpdateService.to; + final updateInfo = await versionService.checkForUpdate(); + + if (updateInfo.hasUpdate) { + Logger.i(_tag, '发现新版本: ${updateInfo.latestVersion}'); + // 显示更新对话框 + await VersionUpdateDialog.show(updateInfo); + } else { + Logger.i(_tag, '当前已是最新版本: ${updateInfo.currentVersion}'); + } + + // 更新检查记录 + _updateVersionCheckRecord(); + } catch (e) { + Logger.e(_tag, '版本检查失败: $e'); + // 即使检查失败,也更新记录,避免频繁重试 + _updateVersionCheckRecord(); + } + } + + /// 更新版本检查记录 + void _updateVersionCheckRecord() { + try { + // 更新时间戳 + final now = DateTime.now(); + _storage.write(_lastVersionCheckKey, now.millisecondsSinceEpoch); + + // 标记本次会话已检查过版本 + _hasCheckedVersionInSession = true; + + Logger.i(_tag, '版本检查记录已更新: ${now.toIso8601String()}'); + } catch (e) { + Logger.e(_tag, '更新版本检查记录失败: $e'); + } + } + + /// 重置版本检查标记(可用于强制重新检查) + /// 注意:这会清除会话标记,但不会清除时间戳记录 + static void resetVersionCheckFlag() { + _hasCheckedVersionInSession = false; + Logger.i('SplashCtrl', '版本检查会话标记已重置'); + } + + /// 完全重置版本检查状态(包括时间戳) + /// 用于测试或特殊情况下强制重新检查 + void forceResetVersionCheck() { + try { + _hasCheckedVersionInSession = false; + _storage.remove(_lastVersionCheckKey); + Logger.i(_tag, '版本检查状态已完全重置'); + } catch (e) { + Logger.e(_tag, '重置版本检查状态失败: $e'); + } + } } From fac1d3c933c7ad818e3e1030357ec66451c735ec Mon Sep 17 00:00:00 2001 From: tanlongsheng <252620078@qq.com> Date: Thu, 31 Jul 2025 16:14:40 +0800 Subject: [PATCH 05/20] =?UTF-8?q?=E4=BF=AE=E6=94=B9iOS=E3=80=81iPad?= =?UTF-8?q?=E5=8F=AA=E8=83=BD=E7=AB=96=E5=B1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ios/Runner/Info.plist | 4 +++- pubspec.yaml | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 8ff8a78c2..05d554d39 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -122,7 +122,6 @@ UIDeviceFamily 1 - 2 UILaunchStoryboardName LaunchScreen @@ -131,6 +130,9 @@ UISupportedInterfaceOrientations UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight UIViewControllerBasedStatusBarAppearance diff --git a/pubspec.yaml b/pubspec.yaml index 2fa9c86e6..d59266ff8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: voitrans description: "Voitrans - AI Voice Assistant." publish_to: "none" -version: 1.0.3+19 +version: 1.0.4+20 environment: sdk: ">=3.3.0 <4.0.0" From cbcd4166aaa6e88e09c752bc7e49413c9cfa1e5c Mon Sep 17 00:00:00 2001 From: fdp <1286779656@qq.com> Date: Fri, 1 Aug 2025 10:41:17 +0800 Subject: [PATCH 06/20] =?UTF-8?q?=E4=BC=98=E5=8C=96=E6=8C=89=E4=BD=8F?= =?UTF-8?q?=E8=AF=B4=E8=AF=9D=E4=BD=93=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/azure_speech/AzureAsrHelper.swift | 440 ++++++++++-------- 1 file changed, 244 insertions(+), 196 deletions(-) diff --git a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift index 24b60e444..716ea30ae 100644 --- a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift +++ b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift @@ -32,6 +32,7 @@ public class AzureAsrHelper: NSObject { private var subscriptionKey = "" private var region = "" + // 音频源配置 public enum AudioSourceType { /** 使用设备麦克风 */ @@ -59,10 +60,14 @@ public class AzureAsrHelper: NSObject { * @param audioSourceType 音频源类型 * @return 初始化是否成功 */ + /** + * 初始化Azure语音服务 + * 预初始化关键组件以提升首次启动性能 + */ public func initialize( subscriptionKey: String, region: String, - supportedLanguages: [String] = ["zh-CN"], + supportedLanguages: [String] = ["zh-CN", "en-US"], audioSourceType: AudioSourceType = .microphone ) -> Bool { do { @@ -95,21 +100,23 @@ public class AzureAsrHelper: NSObject { // 创建语音配置 speechConfig = try SPXSpeechConfiguration(subscription: subscriptionKey, region: region) - speechConfig?.setPropertyTo("800", byName: "SpeechServiceConnection_EndSilenceTimeoutMs") - speechConfig?.setPropertyTo("800", byName: "Speech_SegmentationSilenceTimeoutMs") - speechConfig?.setPropertyTo("Time", byName: "Speech_SegmentationStrategy") - if isAutoDetectLanguage { - // 直接启用语言检测模式 - speechConfig?.setPropertyTo("Continuous", byName: "SpeechServiceConnection_LanguageIdMode") - os_log("启用语言检测模式", log: log, type: .info) + // 移除过时的属性设置方式 + // speechConfig?.setPropertyTo("Continuous", byName: "SpeechServiceConnection_LanguageIdMode") + speechConfig?.setPropertyTo(supportedLanguages.joined(separator: ","), byName: "SpeechServiceConnection_AutoDetectSourceLanguages") + + // 不在这里设置语言,让SPXAutoDetectSourceLanguageConfiguration处理 + os_log("启用语言检测模式,支持语言: %{public}@", log: log, type: .info, supportedLanguages.joined(separator: ",")) } else { - // 设置指定的识别语言 speechConfig?.speechRecognitionLanguage = currentLanguage } + // 录音文件类 recordfile = RecordFile() - // 创建识别器 + + // 【优化】预初始化音频流,避免首次启动延迟 + preInitializeAudioComponents() + return true } catch { os_log("初始化失败: %{public}@", log: log, type: .error, error.localizedDescription) @@ -117,6 +124,26 @@ public class AzureAsrHelper: NSObject { } } + /** + * 预初始化音频组件 + * 在初始化阶段就准备好音频流和相关组件,减少首次启动延迟 + */ + private func preInitializeAudioComponents() { + if audioStream == nil { + audioStream = AudioStream() + audioStream?.initAudioRecord() + audioStream?.onAudioData = { [weak self] data in + self?.recordfile?.saveAudioDataToWav(data) + } + + // 预创建音频配置 + if let pushStream = audioStream?.pushAudioStream { + audioConfig = SPXAudioConfiguration(streamInput: pushStream) + os_log("预初始化音频组件完成", log: log, type: .info) + } + } + } + /** * 开始连续语音识别 @@ -165,6 +192,28 @@ public class AzureAsrHelper: NSObject { return false } } + // /** + // * 获取检测到的语言 + // */ + // private func getDetectedLanguage(from result: SPXSpeechRecognitionResult) -> String { + // if !isAutoDetectLanguage { + // return "" + // } + + // // 尝试从属性中获取语言 + // if let properties = result.properties, + // let language = properties.getPropertyByName("SpeechServiceResponse_RecognitionLanguage") { + // return language + // } + + // // 尝试另一种方式获取语言 + // do { + // let langResult = try SPXAutoDetectSourceLanguageResult(result) + // return langResult.language ?? "" + // } catch { + // return "" + // } + // } /** * 停止连续语音识别 @@ -257,30 +306,32 @@ public class AzureAsrHelper: NSObject { /** * 设置识别器 */ + /** + * 设置识别器 + * 优化:避免重复创建音频流,复用已初始化的组件 + */ private func setupRecognizer() -> Bool { do { print("设置识别器${_isContinuousRecognitionActive}") + // 如果正在进行连续识别,先停止 if _isContinuousRecognitionActive { - // 直接停止,不等待结果 try? recognizer?.stopContinuousRecognition() _isContinuousRecognitionActive = false } - setupMicrophoneStream() + + // 【优化】只有在音频流未初始化时才创建,避免重复初始化 + if audioStream == nil || audioConfig == nil { + setupMicrophoneStream() + } + // 清理旧的识别器 recognizer = nil // 创建识别器 if isAutoDetectLanguage { - // 使用更精确的语言配置方式 - var sourceLanguageConfigs: [SPXSourceLanguageConfiguration] = [] - for language in supportedLanguages { - if let config = try? SPXSourceLanguageConfiguration(language) { - sourceLanguageConfigs.append(config) - } - } - - let autoDetectConfig = try SPXAutoDetectSourceLanguageConfiguration(sourceLanguageConfigurations: sourceLanguageConfigs) + // 使用正确的初始化方法 + let autoDetectConfig = try SPXAutoDetectSourceLanguageConfiguration(supportedLanguages) recognizer = try SPXSpeechRecognizer( speechConfiguration: speechConfig!, autoDetectSourceLanguageConfiguration: autoDetectConfig, @@ -296,7 +347,7 @@ public class AzureAsrHelper: NSObject { return true } catch { os_log("创建识别器失败: %{public}@", log: log, type: .error, error.localizedDescription) - try? recognizer?.stopContinuousRecognition() + try? recognizer?.stopContinuousRecognition() return false } } @@ -304,31 +355,76 @@ public class AzureAsrHelper: NSObject { /** * 设置麦克风流 - 使用推流方式 + * 优化:避免重复初始化已存在的音频流 */ private func setupMicrophoneStream() { do { - - if (audioStream == nil) { - // 创建外部音频拉流对象 - // 明确指定使用外部音频源(推流模式) + // 【优化】检查是否已经初始化,避免重复创建 + if audioStream == nil { audioStream = AudioStream() audioStream?.initAudioRecord() - audioStream?.onAudioData = { [weak self] data in - self?.recordfile?.saveAudioDataToWav(data) - } + audioStream?.onAudioData = { [weak self] data in + self?.recordfile?.saveAudioDataToWav(data) + } } - // 创建音频配置 - if let pushStream = audioStream?.pushAudioStream { // Safely unwrap optional + // 【优化】检查音频配置是否已存在 + if audioConfig == nil, let pushStream = audioStream?.pushAudioStream { audioConfig = SPXAudioConfiguration(streamInput: pushStream) - os_log("设置麦克风流:") + os_log("设置麦克风流完成", log: log, type: .info) } } catch { os_log("设置麦克风流失败: %{public}@", log: log, type: .error, error.localizedDescription) audioStream = nil } } - + /** + * 从语音识别结果中获取检测到的语言 + * @param result 语音识别结果 + * @return 检测到的语言代码,如果检测失败则返回当前语言 + */ + private func getDetectedLanguage(from result: SPXSpeechRecognitionResult) -> String { + guard isAutoDetectLanguage else { + return currentLanguage + } + + print("result: \(result)") + + // 备用方案:从属性中获取语言 + if let properties = result.properties, + let language = properties.getPropertyByName("SpeechServiceResponse_RecognitionLanguage"), + !language.isEmpty { + print("从属性中检测到语言: \(language)") + return language + } + + // 最后的备用方案:从JSON响应中解析 + if let properties = result.properties, + let jsonString = properties.getPropertyByName("SpeechServiceResponse_Json"), + let jsonData = jsonString.data(using: .utf8) { + do { + if let json = try JSONSerialization.jsonObject(with: jsonData, options: []) as? [String: Any], + let language = json["Language"] as? String { + print("从JSON中检测到语言: \(language)") + return language + } + } catch { + print("JSON解析失败: \(error)") + } + } + // 使用正确的SPXAutoDetectSourceLanguageResult初始化方法 + do { + let langResult = try SPXAutoDetectSourceLanguageResult(result) + if let language = langResult.language, !language.isEmpty { + print("SDK方法检测到语言: \(language)") + return language + } + } catch { + print("SDK语言检测失败: \(error.localizedDescription)") + } + print("无法检测语言,使用默认语言: \(currentLanguage)") + return currentLanguage + } /** * 设置事件监听器 */ @@ -342,68 +438,28 @@ public class AzureAsrHelper: NSObject { guard let recognizer = recognizer else { return false} // 正在识别事件 recognizer.addRecognizingEventHandler { [weak self] (_, event) in - guard let self = self else { return } + guard let self = self else { return } let result = event.result - print("识别中文本: \(result.text)") - // 优化:只处理非空结果 - if !(result.text?.isEmpty ?? true) { - let detectedLanguage: String - if self.isAutoDetectLanguage { - do { - let langResult = try SPXAutoDetectSourceLanguageResult(result) - let rawLanguage = langResult.language - print("[DEBUG] 原始检测语言: \(rawLanguage ?? "nil"), 文本: \(result.text ?? "nil"), 支持的语言: \(self.supportedLanguages)") - - // 应用启发式语言检测纠正 - detectedLanguage = self.correctLanguageDetection(rawLanguage: rawLanguage ?? "", text: result.text ?? "") - print("[DEBUG] 纠正后语言: \(detectedLanguage)") - } catch { - print("[DEBUG] 语言检测异常: \(error.localizedDescription)") - detectedLanguage = "" - } - } else { - detectedLanguage = self.currentLanguage - print("[DEBUG] 使用当前语言: \(detectedLanguage)") - } - print("识别中事件: \(detectedLanguage)") - DispatchQueue.main.async { - callback.onRecognizing(result.text ?? "", detectedLanguage) - } + guard !(result.text?.isEmpty ?? true) else { return } + + let detectedLanguage = self.getDetectedLanguage(from: result) + DispatchQueue.main.async { + callback.onRecognizing(result.text ?? "", detectedLanguage) } } // 识别完成事件 recognizer.addRecognizedEventHandler { [weak self] (_, event) in - guard let self = self else { return } + guard let self = self else { return } let result = event.result - print("识别完成文本: \(result.text)") - if result.reason == .recognizedSpeech && !(result.text?.isEmpty ?? true) { - let detectedLanguage: String - if self.isAutoDetectLanguage { - do { - let langResult = try SPXAutoDetectSourceLanguageResult(result) - let rawLanguage = langResult.language - print("[DEBUG] 识别完成 - 原始检测语言: \(rawLanguage ?? "nil"), 文本: \(result.text ?? "nil"), 支持的语言: \(self.supportedLanguages)") - - // 应用启发式语言检测纠正 - let correctedLanguage = self.correctLanguageDetection(rawLanguage: rawLanguage ?? "", text: result.text ?? "") - detectedLanguage = correctedLanguage.isEmpty ? (self.supportedLanguages.first ?? self.currentLanguage) : correctedLanguage - print("[DEBUG] 识别完成 - 纠正后语言: \(detectedLanguage)") - } catch { - print("[DEBUG] 识别完成 - 语言检测异常: \(error.localizedDescription)") - detectedLanguage = self.supportedLanguages.first ?? self.currentLanguage - } - } else { - detectedLanguage = self.currentLanguage - print("[DEBUG] 识别完成 - 使用当前语言: \(detectedLanguage)") - } - print("识别完成事件: \(detectedLanguage)") - DispatchQueue.main.async { - callback.onResult(result.text ?? "", detectedLanguage) - } + guard result.reason == .recognizedSpeech, + !(result.text?.isEmpty ?? true) else { return } + + let detectedLanguage = self.getDetectedLanguage(from: result) + DispatchQueue.main.async { + callback.onResult(result.text ?? "", detectedLanguage) } } - // 会话开始事件 recognizer.addSessionStartedEventHandler { (sender, event) in // 直接在当前线程调用回调 @@ -532,15 +588,23 @@ public class AzureAsrHelper: NSObject { /** * 初始化 */ - public func initAudioRecord() { + /** + * 初始化音频录制组件 + * 包括音频格式、推流、音频引擎等核心组件的初始化 + */ + public func initAudioRecord() { print("初始化了") audioFormat = getOptimalAudioFormat() pushAudioStream = SPXPushAudioInputStream() - isRunning=true + + // 初始化音频引擎 + audioEngine = AVAudioEngine() + + isRunning = true // 创建新的写线程 - writeThread = DispatchQueue(label: "audio.stream.writer") + writeThread = DispatchQueue(label: "audio.stream.writer") startWriteThread() - self.setAudioOutputRoute(.receiver) + self.setAudioOutputRoute(.receiver) } /// 获取最佳音频格式 (iOS 通常支持标准采样率) @@ -585,8 +649,13 @@ public class AzureAsrHelper: NSObject { guard let dataToWrite = self.writeQueue.take() else { continue } print("写入数据长度: \(dataToWrite.count)") - self.pushAudioStream?.write(dataToWrite) - self.onAudioData?(dataToWrite) + do { + try self.pushAudioStream?.write(dataToWrite) + self.onAudioData?(dataToWrite) +} catch { + print("推送音频数据失败: \(error.localizedDescription)") +} + } } } @@ -606,65 +675,92 @@ public class AzureAsrHelper: NSObject { writeQueue.put(data) } - private func runMicrophoneCapture() { - do { - audioEngine = AVAudioEngine() - guard let inputNode = audioEngine?.inputNode else { - throw NSError(domain: "AudioSetup", code: 1) - } - // 推荐直接用系统 format - let hardwareFormat = inputNode.inputFormat(forBus: 0) - // // ===== 新增:启用专业级语音处理 ===== - if #available(iOS 13.0, *) { - try inputNode.setVoiceProcessingEnabled(true) - print("Voice processing enabled") - - } - - - // Create converter to target format - guard let targetFormat = audioFormat, - let converter = AVAudioConverter(from: hardwareFormat, to: targetFormat) else { - throw NSError(domain: "AudioSetup", code: 2) - } - - inputNode.installTap(onBus: 0, bufferSize: UInt32(bufferSize), format: hardwareFormat) { - [weak self] buffer, time in - guard let self = self, self.isWriting else { return } - - // Convert to target format - let convertedBuffer = AVAudioPCMBuffer( - pcmFormat: targetFormat, - frameCapacity: AVAudioFrameCount(targetFormat.sampleRate * Double(buffer.frameLength) / buffer.format.sampleRate) - )! - ///print("进入 tap 回调,frameLength: \(buffer.frameLength)") - var error: NSError? - let status = converter.convert( - to: convertedBuffer, - error: &error, - withInputFrom: { inNumPackets, outStatus in - outStatus.pointee = .haveData - return buffer - } - ) - //print("转换状态: \(status.rawValue), 错误: \(String(describing: error))") - - if status == .haveData, error == nil { - let data = self.audioBufferToData(convertedBuffer) - // print("准备写入数据,大小: \(data.count)") - - self.writeQueue.put(data) - - } + // 私有方法:启动麦克风捕获 +/** + * 启动麦克风捕获 + * 优化:复用已初始化的音频引擎,减少启动延迟 + */ +private func runMicrophoneCapture() { + do { + // 【优化】检查音频引擎是否已初始化,避免重复创建 + if audioEngine == nil { + audioEngine = AVAudioEngine() + } + + // 如果音频引擎正在运行,先停止 + if audioEngine?.isRunning == true { + audioEngine?.stop() + } + + // 获取音频输入节点(麦克风) + guard let inputNode = audioEngine?.inputNode else { + throw NSError(domain: "AudioSetup", code: 1, userInfo: [NSLocalizedDescriptionKey: "无法获取音频输入节点"]) + } + + // 移除之前的音频处理块,避免重复添加 + inputNode.removeTap(onBus: 0) + + // 获取硬件支持的原始音频格式 + let hardwareFormat = inputNode.inputFormat(forBus: 0) + + // iOS 13+ 启用语音处理 + if #available(iOS 13.0, *) { + try inputNode.setVoiceProcessingEnabled(true) + } + + // 检查目标音频格式和转换器是否可用 + guard let targetFormat = audioFormat, // 外部定义的期望音频格式 + let converter = AVAudioConverter(from: hardwareFormat, to: targetFormat) else { + throw NSError(domain: "AudioSetup", code: 2) + } + + // 在输入节点上安装录音回调 + inputNode.installTap(onBus: 0, + bufferSize: UInt32(bufferSize), // 每次回调的缓冲区大小 + format: hardwareFormat) { // 使用原始硬件格式 + [weak self] buffer, time in // 弱引用避免循环引用 + + // 确保实例存在且正在写入状态 + guard let self = self, self.isWriting else { return } + + // 创建目标格式的音频缓冲区 + let convertedBuffer = AVAudioPCMBuffer( + pcmFormat: targetFormat, + // 计算转换后的帧容量(考虑采样率差异) + frameCapacity: AVAudioFrameCount( + targetFormat.sampleRate * Double(buffer.frameLength) / buffer.format.sampleRate + ) + )! + + var error: NSError? + // 执行音频格式转换 + let status = converter.convert( + to: convertedBuffer, + error: &error, + withInputFrom: { inNumPackets, outStatus in + outStatus.pointee = .haveData // 标记有数据可用 + return buffer // 返回原始音频数据 } - // 重新激活 - try audioSession.setActive(true, options: [.notifyOthersOnDeactivation]) + ) - try audioEngine?.start() - } catch { - print("麦克风启动失败: \(error)") + // 转换成功且无错误 + if status == .haveData, error == nil { + // 将音频缓冲区转换为二进制数据 + let data = self.audioBufferToData(convertedBuffer) + // 将数据放入写入队列(后续处理) + self.writeQueue.put(data) } } + + // 激活音频会话(允许录音) + try audioSession.setActive(true, options: [.notifyOthersOnDeactivation]) + + // 启动音频引擎 + try audioEngine?.start() + } catch { + print("麦克风启动失败: \(error)") + } +} private func audioBufferToData(_ buffer: AVAudioPCMBuffer) -> Data { let frameLength = Int(buffer.frameLength) let channelCount = 1 @@ -870,55 +966,7 @@ public class AzureAsrHelper: NSObject { } } - /** - * 启发式语言检测纠正方法 - * 基于文本内容特征来纠正Azure语音服务的语言检测结果 - */ - private func correctLanguageDetection(rawLanguage: String, text: String) -> String { - // 如果原始检测为空,返回空 - if rawLanguage.isEmpty || text.isEmpty { - return rawLanguage - } - - let trimmedText = text.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() - - // 英文特征检测 - let englishPattern = "^[a-z\\s\\.,!?;:'-]+$" - let englishRegex = try? NSRegularExpression(pattern: englishPattern, options: .caseInsensitive) - let englishMatches = englishRegex?.numberOfMatches(in: trimmedText, options: [], range: NSRange(location: 0, length: trimmedText.count)) ?? 0 - - // 中文特征检测 - let chinesePattern = "[\\u4e00-\\u9fff]" - let chineseRegex = try? NSRegularExpression(pattern: chinesePattern, options: []) - let chineseMatches = chineseRegex?.numberOfMatches(in: trimmedText, options: [], range: NSRange(location: 0, length: trimmedText.count)) ?? 0 - - // 常见英文单词列表 - let commonEnglishWords = ["hello", "hi", "please", "thank", "you", "yes", "no", "good", "morning", "afternoon", "evening", "night", "how", "are", "what", "where", "when", "why", "who", "can", "could", "would", "should", "will", "the", "and", "or", "but", "if", "then", "this", "that", "these", "those"] - - let hasCommonEnglishWords = commonEnglishWords.contains { word in - trimmedText.contains(word) - } - - print("[DEBUG] 语言纠正分析 - 文本: '\(trimmedText)', 英文匹配: \(englishMatches > 0), 中文字符: \(chineseMatches), 常见英文词: \(hasCommonEnglishWords), 原始语言: \(rawLanguage)") - - // 纠正逻辑 - if rawLanguage == "zh-CN" { - // 如果Azure检测为中文,但文本明显是英文特征 - if (englishMatches > 0 && chineseMatches == 0) || hasCommonEnglishWords { - print("[DEBUG] 纠正: \(rawLanguage) -> en-US (检测到英文特征)") - return "en-US" - } - } else if rawLanguage == "en-US" { - // 如果Azure检测为英文,但包含中文字符 - if chineseMatches > 0 { - print("[DEBUG] 纠正: \(rawLanguage) -> zh-CN (检测到中文字符)") - return "zh-CN" - } - } - - // 如果没有明确的纠正理由,保持原始检测结果 - return rawLanguage - } + // MARK: - 回调函数 /** From a3bf2585b0255c74f587bcac96bf4eb0ce921cc0 Mon Sep 17 00:00:00 2001 From: lxm <1274844347@qq.com> Date: Fri, 1 Aug 2025 11:47:55 +0800 Subject: [PATCH 07/20] =?UTF-8?q?refactor(splash):=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E5=90=AF=E5=8A=A8=E6=B5=81=E7=A8=8B=E5=92=8C=E7=89=88=E6=9C=AC?= =?UTF-8?q?=E6=A3=80=E6=9F=A5=E8=B0=83=E5=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将用户追踪权限请求改为异步执行不阻塞主流程 重构版本检查逻辑,在页面跳转完成后执行 使用Logger替代print进行统一日志管理 --- .../splash/controllers/splash_controller.dart | 106 ++++++++++-------- 1 file changed, 60 insertions(+), 46 deletions(-) diff --git a/lib/modules/splash/controllers/splash_controller.dart b/lib/modules/splash/controllers/splash_controller.dart index 4badc4d43..4bf70d207 100644 --- a/lib/modules/splash/controllers/splash_controller.dart +++ b/lib/modules/splash/controllers/splash_controller.dart @@ -59,20 +59,37 @@ class SplashController extends GetxController { } }); - //弹出用户追踪问题 - WidgetsFlutterBinding.ensureInitialized().addPostFrameCallback((_) async { - final status = - await AppTrackingTransparency.requestTrackingAuthorization(); + // 异步处理用户追踪授权,不阻塞主流程 + _requestTrackingPermissionAsync(); + } + + /// 异步请求追踪权限,不阻塞启动流程 + void _requestTrackingPermissionAsync() { + Future.delayed(const Duration(milliseconds: 500), () async { + try { + await AppTrackingTransparency.requestTrackingAuthorization(); + Logger.d(_tag, '用户追踪权限请求完成'); + } catch (e) { + Logger.e(_tag, '用户追踪权限请求失败: $e'); + } }); } void _startNormalFlow() { _startLoadingAnimation(); _loadAppResources(); + } - // 延迟执行版本检查,避免影响启动速度 - Future.delayed(const Duration(seconds: 1), () { - _checkVersionUpdateIfNeeded(); + /// 智能调度版本检查,在页面跳转后执行,完全不影响启动流程 + void _scheduleVersionCheck() { + // 在独立的微任务中执行,避免阻塞主线程 + scheduleMicrotask(() async { + try { + Logger.d(_tag, '开始执行版本检查(后台任务)'); + _checkVersionUpdateIfNeeded(); + } catch (e) { + Logger.e(_tag, '版本检查调度失败: $e'); + } }); } @@ -98,18 +115,6 @@ class SplashController extends GetxController { ); } - // 初始化地图SDK - // void _initMapSDK() { - // try { - // AmapFlutter().initSdk( - // iosKey: "a4a1394fe817c2f86a424b897b4a9af4", - // androidKey: "78e7946dd602778df82c6ae143a88184", - // ); - // } catch (e) { - // print('地图SDK初始化失败: $e'); - // } - // } - // 进度条动画(持续2秒) void _startLoadingAnimation() { const totalDuration = 1500; // 动画总时长增加到1.5秒 @@ -136,22 +141,23 @@ class SplashController extends GetxController { // await _loadImages(); // await _initDatabase(); - // 检查版本更新(在后台进行,不阻塞启动流程) - //_checkVersionUpdate(); - _isResourceLoaded.value = true; _checkNavigateCondition(); } catch (e) { Get.snackbar('错误', '资源加载失败,请检查网络连接'); _isResourceLoaded.value = true; // 允许用户继续使用 - print('资源加载错误: $e'); + Logger.e(_tag, '资源加载错误: $e'); } } // 检查跳转条件 - void _checkNavigateCondition() { + void _checkNavigateCondition() async { if (_loadingProgress.value >= 1.0 && _isResourceLoaded.value) { - _navigateToLogin(); + // 关键改动:先执行页面跳转,并用 await 等待它完成 + await _navigateToLogin(); + + // 页面跳转完全结束后,再安心地安排版本检查 + _scheduleVersionCheck(); } } @@ -159,7 +165,7 @@ class SplashController extends GetxController { Future _navigateToLogin() async { bool isLoggedIn = false; final token = _storage.read('logintoken'); - print('getLoginToken:进来了${token}'); + Logger.d(_tag, 'getLoginToken:进来了${token}'); if (token != null && token.isNotEmpty) { isLoggedIn = await getLoginToken(); } @@ -173,14 +179,14 @@ class SplashController extends GetxController { Future getLoginToken() async { try { - print('getLoginToken:进来了'); + Logger.d(_tag, 'getLoginToken:进来了'); //if (User.getname() != "") { final response = await Api.tokenlogin(); - print('getLoginToken: $response'); + Logger.d(_tag, 'getLoginToken: $response'); if (response == null) return false; if (response['user'] != '') { - print('getLoginToken:进来了'); + Logger.d(_tag, 'getLoginToken:进来了'); // 根据API设计调整 final user = User.fromJson(response); @@ -192,21 +198,21 @@ class SplashController extends GetxController { //} return false; } catch (e) { - print('Token获取失败: $e'); + Logger.e(_tag, 'Token获取失败: $e'); return false; } } Future getAppConfig() async { try { - print('getAppConfig:进来了'); + Logger.d(_tag, 'getAppConfig:进来了'); //if (User.getname() != "") { final response = await Api.getappconfig(); - print('getAppConfig: $response'); + Logger.d(_tag, 'getAppConfig: $response'); if (response == null) return false; // 根据API设计调整 final resp = UserGetAppConfigResp.fromJson(response); - print('getAppConfig:进来了'); + Logger.d(_tag, 'getAppConfig:进来了'); // 存储用户信息(示例) // _storage.write('ENV', resp.env); // _storage.write('AGENTS', resp.agents); @@ -214,7 +220,7 @@ class SplashController extends GetxController { AppConfig.initialize(resp); return true; } catch (e) { - print('getAppConfig 取失败: $e'); + Logger.e(_tag, 'getAppConfig 取失败: $e'); return false; } } @@ -236,7 +242,8 @@ class SplashController extends GetxController { } // 条件2:距离上次检查超过24小时 else if (lastCheckTimestamp != null) { - final lastCheckTime = DateTime.fromMillisecondsSinceEpoch(lastCheckTimestamp); + final lastCheckTime = + DateTime.fromMillisecondsSinceEpoch(lastCheckTimestamp); final timeDifference = now.difference(lastCheckTime); if (timeDifference.inHours >= _versionCheckIntervalHours) { @@ -265,18 +272,25 @@ class SplashController extends GetxController { Future _performVersionCheck() async { try { final versionService = VersionUpdateService.to; - final updateInfo = await versionService.checkForUpdate(); - - if (updateInfo.hasUpdate) { - Logger.i(_tag, '发现新版本: ${updateInfo.latestVersion}'); - // 显示更新对话框 - await VersionUpdateDialog.show(updateInfo); - } else { - Logger.i(_tag, '当前已是最新版本: ${updateInfo.currentVersion}'); - } + // 异步执行,不等待结果 + versionService.checkForUpdate().then((updateInfo) { + if (updateInfo.hasUpdate) { + Logger.i(_tag, '发现新版本: ${updateInfo.latestVersion}'); + // 在主线程中显示更新对话框 + WidgetsBinding.instance.addPostFrameCallback((_) { + VersionUpdateDialog.show(updateInfo); + }); + } else { + Logger.i(_tag, '当前已是最新版本: ${updateInfo.currentVersion}'); + } - // 更新检查记录 - _updateVersionCheckRecord(); + // 更新检查记录 + _updateVersionCheckRecord(); + }).catchError((e) { + Logger.e(_tag, '版本检查失败: $e'); + // 即使检查失败,也更新记录,避免频繁重试 + _updateVersionCheckRecord(); + }); } catch (e) { Logger.e(_tag, '版本检查失败: $e'); // 即使检查失败,也更新记录,避免频繁重试 From d1b99d99dcc7788863dfe5b923063dd8cab33a0d Mon Sep 17 00:00:00 2001 From: lxm <1274844347@qq.com> Date: Fri, 1 Aug 2025 14:41:12 +0800 Subject: [PATCH 08/20] =?UTF-8?q?refactor(widget):=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E5=A3=B0=E9=9F=B3=E6=9F=B1=E5=8A=A8=E7=94=BB=E7=BB=84=E4=BB=B6?= =?UTF-8?q?=E6=80=A7=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 使用 AnimatedBuilder 替代直接监听动画状态 - 添加组件挂载检查防止内存泄漏 - 改进动画停止时的过渡效果 - 更新相关注释说明 --- .../agent/widgets/sound_bar_widget.dart | 48 +++++++++++-------- 1 file changed, 29 insertions(+), 19 deletions(-) diff --git a/lib/modules/agent/widgets/sound_bar_widget.dart b/lib/modules/agent/widgets/sound_bar_widget.dart index 11fb9ed86..9028f713b 100644 --- a/lib/modules/agent/widgets/sound_bar_widget.dart +++ b/lib/modules/agent/widgets/sound_bar_widget.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import '../controllers/agent_controller.dart'; -/// 声音柱动画组件 +/// 声音柱动画组件 (已优化) /// 用于显示语音输入时的音频波形效果 class SoundBarWidget extends StatefulWidget { final double delay; @@ -37,9 +37,8 @@ class _SoundBarWidgetState extends State parent: _controller, curve: Curves.easeInOut, ), - )..addListener(() { - setState(() {}); - }); + ); + // 移除了 .addListener(() { setState(() {}); }) _startOrStopAnimation(); } @@ -54,6 +53,9 @@ class _SoundBarWidgetState extends State /// 开始或停止动画 void _startOrStopAnimation() { + // 确保组件仍然挂载在树上 + if (!mounted) return; + if (widget.isActive) { Future.delayed(Duration(milliseconds: (widget.delay * 1000).toInt()), () { if (mounted && widget.isActive) { @@ -61,9 +63,10 @@ class _SoundBarWidgetState extends State } }); } else { - _controller.reset(); - // 设置为小高度的静态状态 - _controller.animateTo(0.1, duration: const Duration(milliseconds: 300)); + // 停止并平滑地过渡到初始状态 + _controller.stop(); + _controller.animateTo(0.0, + duration: const Duration(milliseconds: 200), curve: Curves.easeOut); } } @@ -77,21 +80,28 @@ class _SoundBarWidgetState extends State Widget build(BuildContext context) { final primaryColor = Theme.of(context).primaryColor; - return Container( - width: 2.5, - height: widget.isActive ? _animation.value : 3, - margin: const EdgeInsets.symmetric(horizontal: 1), - decoration: BoxDecoration( - color: widget.isActive - ? primaryColor.withOpacity(0.8) - : Colors.grey.withOpacity(0.4), - borderRadius: BorderRadius.circular(1.5), - ), + // 使用 AnimatedBuilder 来优化性能 + return AnimatedBuilder( + animation: _animation, + builder: (context, child) { + return Container( + width: 2.5, + // 当动画不活跃时,高度固定为3;活跃时使用动画值 + height: widget.isActive ? _animation.value : 3, + margin: const EdgeInsets.symmetric(horizontal: 1), + decoration: BoxDecoration( + color: widget.isActive + ? primaryColor.withOpacity(0.8) + : Colors.grey.withOpacity(0.4), + borderRadius: BorderRadius.circular(1.5), + ), + ); + }, ); } } -/// 声音柱组合组件 +/// 声音柱组合组件 (无需改动) /// 包含多个声音柱的组合显示 class SoundBarsWidget extends StatelessWidget { final bool isActive; @@ -120,7 +130,7 @@ class SoundBarsWidget extends StatelessWidget { } } -/// 带控制器集成的声音柱组件 +/// 带控制器集成的声音柱组件 (无需改动) /// 自动从AgentController获取状态 class AgentSoundBarsWidget extends StatelessWidget { final int barCount; From 7bdda09d005c344b790ce7cf460346bdbf0fb689 Mon Sep 17 00:00:00 2001 From: tanlongsheng <252620078@qq.com> Date: Fri, 1 Aug 2025 16:40:05 +0800 Subject: [PATCH 09/20] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E6=96=B0=E6=89=8B?= =?UTF-8?q?=E6=8C=87=E5=BC=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/core/bindings/initial_binding.dart | 4 + lib/data/services/novice_guide_service.dart | 34 ++++ .../agent/controllers/agent_controller.dart | 3 + lib/modules/agent/views/agent_view.dart | 181 ++++++++++-------- .../home/controllers/home_controller.dart | 40 ++-- lib/modules/home/views/home_view.dart | 107 ++++++----- 6 files changed, 219 insertions(+), 150 deletions(-) create mode 100644 lib/data/services/novice_guide_service.dart diff --git a/lib/core/bindings/initial_binding.dart b/lib/core/bindings/initial_binding.dart index 73826786b..52117ff40 100644 --- a/lib/core/bindings/initial_binding.dart +++ b/lib/core/bindings/initial_binding.dart @@ -6,6 +6,7 @@ import '../../../data/services/navigation_manager.dart'; import 'package:get/get.dart'; import '../../data/services/meeting/meeting_task_service.dart'; import '../../data/services/meeting/meeting_upload_service.dart'; +import '../../data/services/novice_guide_service.dart'; import '../../data/services/speech_factory.dart'; import '../../data/services/volcano_translation_service.dart'; import '../../data/services/volcano_ai_service.dart'; @@ -64,6 +65,9 @@ class InitialBinding extends Bindings { // 会议助手音频上传 Get.lazyPut(() => UserportraitManager(), fenix: true); + // 新手引导 + Get.lazyPut(() => NoviceGuideService(), fenix: true); + // 杰理蓝牙服务 // // 语言管理器(需要最先初始化) diff --git a/lib/data/services/novice_guide_service.dart b/lib/data/services/novice_guide_service.dart new file mode 100644 index 000000000..56ab6464b --- /dev/null +++ b/lib/data/services/novice_guide_service.dart @@ -0,0 +1,34 @@ +import 'package:flutter_intro/flutter_intro.dart'; +import 'package:get/get.dart'; +import 'package:get_storage/get_storage.dart'; + +class NoviceGuideService extends GetxService { + final GetStorage _storage = GetStorage(); + + bool isHomeGuide = false; + bool isAIGuide = false; + + @override + void onInit() { + super.onInit(); + // isHomeGuide = _storage.read('home_guide') ?? false; + // isAIGuide = _storage.read('ai_guide') ?? false; + } + + void startHomeGuide() { + if (!isHomeGuide) { + isHomeGuide = true; + _storage.write('home_guide', true); + Intro.of(Get.context!).start(group: 'home'); + } + } + + void startAIGuide() async { + if (!isAIGuide) { + await Future.delayed(const Duration(milliseconds: 100)); + isAIGuide = true; + _storage.write('ai_guide', true); + Intro.of(Get.context!).start(group: 'ai'); + } + } +} diff --git a/lib/modules/agent/controllers/agent_controller.dart b/lib/modules/agent/controllers/agent_controller.dart index 149c05b22..5330abd4c 100644 --- a/lib/modules/agent/controllers/agent_controller.dart +++ b/lib/modules/agent/controllers/agent_controller.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'dart:ffi'; import 'dart:io'; import 'package:amap_service/amap_service.dart'; +import '../../../data/services/novice_guide_service.dart'; import '/core/utils/upload_oss.dart'; import '/data/models/user_Info.dart'; import '/data/services/music_manager.dart'; @@ -39,6 +40,8 @@ class AgentController extends GetxController { final TextEditingController textController = TextEditingController(); final ScrollController scrollController = ScrollController(); + final noviceGuideService = Get.find(); + //音乐播放管理 final _musiceManager = Get.find(); diff --git a/lib/modules/agent/views/agent_view.dart b/lib/modules/agent/views/agent_view.dart index 0ef9a63ac..ff5da53fd 100644 --- a/lib/modules/agent/views/agent_view.dart +++ b/lib/modules/agent/views/agent_view.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:io'; import 'dart:math'; import 'package:flutter/material.dart'; +import 'package:flutter_intro/flutter_intro.dart'; import 'package:get/get.dart'; import '../controllers/agent_controller.dart'; import '../controllers/message_action_controller.dart'; @@ -388,96 +389,110 @@ class _AgentViewState extends State with WidgetsBindingObserver { if (controller.isTextInputMode.value) { // 文本输入模式 - 限制最大高度并只向上扩展 - return Container( - constraints: BoxConstraints( - minHeight: 44.h, - maxHeight: 120.h, - ), - padding: EdgeInsets.symmetric( - horizontal: 16.w), - decoration: BoxDecoration( - color: isDarkMode - ? Colors.grey[800] - : Colors.grey[200], - borderRadius: - BorderRadius.circular(22.w), - border: Border.all( + return IntroStepBuilder( + group: 'ai', + order: 1, + text: '对小言说“今天天气怎么样”', + onWidgetLoad: () { + controller.noviceGuideService + .startAIGuide(); + }, + builder: (BuildContext context, + GlobalKey key) => + Container( + key: key, + constraints: BoxConstraints( + minHeight: 44.h, + maxHeight: 120.h, + ), + padding: EdgeInsets.symmetric( + horizontal: 16.w), + decoration: BoxDecoration( color: isDarkMode - ? Colors.grey[800]! - : Colors.grey.withOpacity(0.1), - width: 1.w, + ? Colors.grey[800] + : Colors.grey[200], + borderRadius: + BorderRadius.circular(22.w), + border: Border.all( + color: isDarkMode + ? Colors.grey[800]! + : Colors.grey.withOpacity(0.1), + width: 1.w, + ), ), - ), - child: Row( - crossAxisAlignment: CrossAxisAlignment - .end, // 对齐到底部,实现向上扩展 - children: [ - Expanded( - child: Obx(() { - // 图片模式下使用更高的输入框 - final isImageActive = controller - .isImageInputActive.value; + child: Row( + crossAxisAlignment: CrossAxisAlignment + .end, // 对齐到底部,实现向上扩展 + children: [ + Expanded( + child: Obx(() { + // 图片模式下使用更高的输入框 + final isImageActive = controller + .isImageInputActive.value; - return TextField( - controller: - controller.textController, - maxLines: isImageActive - ? 3 - : 5, // 设置合理的最大行数 - minLines: isImageActive ? 2 : 1, - keyboardType: TextInputType - .multiline, // 启用多行键盘 - textInputAction: TextInputAction - .newline, // 支持回车换行 - onTap: () { - // 当点击输入框时,如果'+'选项已展开,则收缩 - if (controller - .isPlusOptionsExpanded - .value) { - controller - .collapsePlusOptions(); - } - }, - style: TextStyle( - fontSize: 15.sp, - color: isDarkMode - ? Colors.white - : Colors.grey[800], - ), - decoration: InputDecoration( - hintText: isImageActive - ? 'addQuestionAboutImage' - .tr // 添加对图片的问题或描述... - : 'inputMessage' - .tr, // 输入消息... - hintStyle: TextStyle( - color: Colors.grey[500], + return TextField( + controller: + controller.textController, + maxLines: isImageActive + ? 3 + : 5, // 设置合理的最大行数 + minLines: + isImageActive ? 2 : 1, + keyboardType: TextInputType + .multiline, // 启用多行键盘 + textInputAction: + TextInputAction + .newline, // 支持回车换行 + onTap: () { + // 当点击输入框时,如果'+'选项已展开,则收缩 + if (controller + .isPlusOptionsExpanded + .value) { + controller + .collapsePlusOptions(); + } + }, + style: TextStyle( fontSize: 15.sp, + color: isDarkMode + ? Colors.white + : Colors.grey[800], ), - filled: false, - border: InputBorder.none, - focusedBorder: - OutlineInputBorder( - borderSide: BorderSide( - color: isDarkMode - ? Colors.grey[800]! - : Colors.grey[200]!, - width: 1.w, + decoration: InputDecoration( + hintText: isImageActive + ? 'addQuestionAboutImage' + .tr // 添加对图片的问题或描述... + : 'inputMessage' + .tr, // 输入消息... + hintStyle: TextStyle( + color: Colors.grey[500], + fontSize: 15.sp, + ), + filled: false, + border: InputBorder.none, + focusedBorder: + OutlineInputBorder( + borderSide: BorderSide( + color: isDarkMode + ? Colors.grey[800]! + : Colors.grey[200]!, + width: 1.w, + ), ), + contentPadding: + EdgeInsets.symmetric( + vertical: + isImageActive + ? 12.h + : 8.h), ), - contentPadding: - EdgeInsets.symmetric( - vertical: - isImageActive - ? 12.h - : 8.h), - ), - onSubmitted: (_) => - controller.sendMessage(), - ); - }), - ), - ], + onSubmitted: (_) => + controller.sendMessage(), + ); + }), + ), + ], + ), ), ); } else { diff --git a/lib/modules/home/controllers/home_controller.dart b/lib/modules/home/controllers/home_controller.dart index 31db67e15..1287e6c45 100644 --- a/lib/modules/home/controllers/home_controller.dart +++ b/lib/modules/home/controllers/home_controller.dart @@ -1,5 +1,6 @@ import 'package:get/get.dart'; import 'dart:async'; +import '../../../data/services/novice_guide_service.dart'; import '../../../routes/app_routes.dart'; import '../../../core/utils/logger.dart'; import 'package:flutter/material.dart'; @@ -24,7 +25,7 @@ class HomeController extends GetxController { final _isFullyConnected = false.obs; String? _lastConnectedDeviceName; - + bool get isFullyConnected => _isFullyConnected.value; RxBool get isFullyConnectedRx => _isFullyConnected; BluetoothDevice? get currentDevice => _bluetoothManager.currentDevice; @@ -55,12 +56,15 @@ class HomeController extends GetxController { final _bleManager = Get.find(); final _storage = GetStorage(); + final noviceGuideService = Get.find(); + // 会话级别的版本检查标记(静态变量,整个应用会话期间保持) static bool _hasCheckedVersionInSession = false; // 版本检查相关常量 static const String _lastVersionCheckKey = 'last_version_check_timestamp'; static const int _versionCheckIntervalHours = 24; // 24小时间隔 + @override void onInit() { super.onInit(); @@ -94,13 +98,14 @@ class HomeController extends GetxController { update(); }); - _classicBluetoothWorker = ever(_bluetoothManager.currentDeviceRx, (BluetoothDevice? device) { + _classicBluetoothWorker = + ever(_bluetoothManager.currentDeviceRx, (BluetoothDevice? device) { if (device != null) { _lastConnectedDeviceName = device.name; } _checkConnectionStatus(); }); - + _bleConnectionWorker = ever(_bleManager.connectionStateRx, (int state) { _checkConnectionStatus(); }); @@ -121,30 +126,30 @@ class HomeController extends GetxController { void exploreAIFeatures() { Get.toNamed(Routes.agent); } - + Future _checkConnectionStatus() async { try { final currentBleState = await _bleManager.getConnectionState(); - + bool hasClassicBluetooth = currentDevice != null; bool hasBleConnection = currentBleState == 2; bool wasConnected = _isFullyConnected.value; bool isNowConnected = hasClassicBluetooth && hasBleConnection; - + if (isNowConnected && currentDevice == null) { isNowConnected = false; } - + if (_isFullyConnected.value != isNowConnected) { _isFullyConnected.value = isNowConnected; _isFullyConnected.refresh(); update(); - + if (!wasConnected && isNowConnected) { await _bleManager.fetchDeviceInfo(); } } - + update(); } catch (e) { Logger.e(_tag, '${'checkConnectionStatusFailed'.tr}: $e'); @@ -165,26 +170,28 @@ class HomeController extends GetxController { } bool get shouldShowDeviceInfo => currentDevice != null; - + String getDeviceName() { if (_isFullyConnected.value && currentDevice == null) { _checkConnectionStatus(); } - + final deviceName = currentDevice?.name; if (deviceName != null && deviceName.isNotEmpty) { return deviceName; } - + final bleDeviceName = deviceInfo.value?.name; if (bleDeviceName != null && bleDeviceName.isNotEmpty) { return bleDeviceName; } - - if (!_isFullyConnected.value && _lastConnectedDeviceName != null && _lastConnectedDeviceName!.isNotEmpty) { + + if (!_isFullyConnected.value && + _lastConnectedDeviceName != null && + _lastConnectedDeviceName!.isNotEmpty) { return _lastConnectedDeviceName!; } - + return 'unknownEquipment'.tr; } @@ -288,7 +295,8 @@ class HomeController extends GetxController { } // 条件2:距离上次检查超过24小时 else if (lastCheckTimestamp != null) { - final lastCheckTime = DateTime.fromMillisecondsSinceEpoch(lastCheckTimestamp); + final lastCheckTime = + DateTime.fromMillisecondsSinceEpoch(lastCheckTimestamp); final timeDifference = now.difference(lastCheckTime); if (timeDifference.inHours >= _versionCheckIntervalHours) { diff --git a/lib/modules/home/views/home_view.dart b/lib/modules/home/views/home_view.dart index 18b24b210..f842b6a23 100644 --- a/lib/modules/home/views/home_view.dart +++ b/lib/modules/home/views/home_view.dart @@ -84,11 +84,30 @@ class HomeView extends GetView { _buildAppBar(isDarkMode), // 顶部图片 - _buildEarbudsImage(), + IntroStepBuilder( + group: 'home', + order: 1, + text: '欢迎使用Voitrans!首先,让我们唤醒你的专属 AI—— 小言', + onWidgetLoad: () { + controller.noviceGuideService.startHomeGuide(); + }, + builder: (BuildContext context, GlobalKey key) => SizedBox( + key: key, + child: _buildEarbudsImage(), + ), + ), SizedBox(height: 20.h), // 设备状态卡片 - _buildDeviceStatusCard(isDarkMode), + IntroStepBuilder( + group: 'home', + order: 2, + text: '对着麦克风说“小言小言”', + builder: (BuildContext context, GlobalKey key) => SizedBox( + key: key, + child: _buildDeviceStatusCard(isDarkMode), + ), + ), SizedBox(height: 20.h), // 热门AI体验 @@ -624,8 +643,9 @@ class HomeView extends GetView { Expanded( flex: 1, child: IntroStepBuilder( + group: 'home', order: 3, - text: 'AI全能助手', + text: '太棒了!你已成功唤醒小言,接下来试试让它帮你做件事吧', onHighlightWidgetTap: () { Intro.of(Get.context!).dispose(); controller.exploreAIFeatures(); @@ -660,62 +680,47 @@ class HomeView extends GetView { child: Column( children: [ // 右上卡片 - 同声翻译 - IntroStepBuilder( - order: 1, - text: '同声传译', - // onWidgetLoad: () { - // Intro.of(Get.context!).start(); - // }, - builder: (BuildContext context, GlobalKey key) => - _buildFeatureCard( - key: key, - title: 'simultaneousInterpretation'.tr, // 同声传译 - description: - 'simultaneousInterpretationInfo'.tr, // 80+语言实时翻译,出国无忧 - icon: Icons.translate, - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [ - const Color(0xFF4CAF50).withOpacity(0.3), - const Color(0xFF009688).withOpacity(0.3), - ], - ), - onTap: controller.openTranslation, - height: 83.h, // 增加高度 - isDarkMode: isDarkMode, - iconColor: const Color(0xFF4CAF50), + _buildFeatureCard( + title: 'simultaneousInterpretation'.tr, // 同声传译 + description: + 'simultaneousInterpretationInfo'.tr, // 80+语言实时翻译,出国无忧 + icon: Icons.translate, + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + const Color(0xFF4CAF50).withOpacity(0.3), + const Color(0xFF009688).withOpacity(0.3), + ], ), + onTap: controller.openTranslation, + height: 83.h, // 增加高度 + isDarkMode: isDarkMode, + iconColor: const Color(0xFF4CAF50), ), SizedBox(height: 5.h), // 减少间距 // 右下卡片 - // 右下卡片 - 会议助手 - IntroStepBuilder( - order: 2, - text: '商务会议助手', - builder: (BuildContext context, GlobalKey key) => - _buildFeatureCard( - key: key, - title: 'conferenceAssistant'.tr, // 商务会议助手 - description: 'conferenceAssistantInfo'.tr, // 实时记录会议纪要,多语言翻译 - icon: Icons.people, - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [ - Color(0xFFab47bc).withOpacity(0.3), // 薰衣草紫 - Color(0xFFec407a).withOpacity(0.3), // 品红 - ], - ), - onTap: () { - Get.toNamed(Routes.meeting); - }, - height: 85.h, // 增加高度 - isDarkMode: isDarkMode, - iconColor: const Color(0xFFd81b60), // 品红 + _buildFeatureCard( + title: 'conferenceAssistant'.tr, // 商务会议助手 + description: 'conferenceAssistantInfo'.tr, // 实时记录会议纪要,多语言翻译 + icon: Icons.people, + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Color(0xFFab47bc).withOpacity(0.3), // 薰衣草紫 + Color(0xFFec407a).withOpacity(0.3), // 品红 + ], ), + onTap: () { + Get.toNamed(Routes.meeting); + }, + height: 85.h, // 增加高度 + isDarkMode: isDarkMode, + iconColor: const Color(0xFFd81b60), // 品红 ), ], ), From 762a41528944304205b2bb801de69522ed4da938 Mon Sep 17 00:00:00 2001 From: tanlongsheng <252620078@qq.com> Date: Fri, 1 Aug 2025 17:04:26 +0800 Subject: [PATCH 10/20] =?UTF-8?q?feat(ios):=20=E6=B7=BB=E5=8A=A0=20telepho?= =?UTF-8?q?ny=20=E8=AE=BE=E5=A4=87=E8=83=BD=E5=8A=9B=E8=A6=81=E6=B1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在 Info.plist 中添加 UIRequiredDeviceCapabilities 字段,声明应用需要 telephony 功能支持 --- ios/Runner/Info.plist | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 05d554d39..99b4642a3 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -119,6 +119,10 @@ audio location + UIRequiredDeviceCapabilities + + telephony + UIDeviceFamily 1 From 99f44fcf992979ba4dbbc7a972b8d018cb5cb5c2 Mon Sep 17 00:00:00 2001 From: liwei1dao Date: Fri, 1 Aug 2025 19:11:39 +0800 Subject: [PATCH 11/20] =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E9=9F=B3=E9=87=8F?= =?UTF-8?q?=E5=92=8C=E8=93=9D=E7=89=99=E7=BB=91=E5=AE=9A=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ios/Runner/Info.plist | 2 + .../controllers/devices_controller.dart | 1 + .../controllers/pairing_controller.dart | 108 +++++++- .../agent_service/AgentService.kt | 14 +- .../agent_service/AgentServiceImpl.swift | 50 +++- .../agent_service/AgentServicePlugin.swift | 6 +- .../agent_service/AudioSessionHub.swift | 230 +++++++++++++++++- .../Sources/agent_service/Resources/await.mp3 | Bin 0 -> 24756 bytes .../Sources/azure_speech/AzureAsrHelper.swift | 6 +- .../ble_service/BleServicePlugin.kt | 2 +- .../ble_service/SwiftBleServicePlugin.swift | 2 +- .../ble_service/SwiftOpusAudioProcessor.swift | 2 +- .../chat_api/SystemFunctionHandler.swift | 207 ++++++++++++++-- .../location_service/LocationServicePlugin.kt | 2 +- .../music_service/MusicServicePlugin.kt | 2 +- .../music_service/MusicServicePlugin.swift | 2 +- 16 files changed, 573 insertions(+), 63 deletions(-) create mode 100644 local_plugins/agent_service/ios/agent_service/Sources/agent_service/Resources/await.mp3 diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 05d554d39..75a82d84d 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -59,6 +59,8 @@ 193d0a2b1757c41cfa557ea8e8f21c33 LSApplicationQueriesSchemes + App-Prefs + prefs weixin weixinULAPI weixinURLParamsAPI diff --git a/lib/modules/devices/controllers/devices_controller.dart b/lib/modules/devices/controllers/devices_controller.dart index 1beadea7a..66ff69211 100644 --- a/lib/modules/devices/controllers/devices_controller.dart +++ b/lib/modules/devices/controllers/devices_controller.dart @@ -57,6 +57,7 @@ class DevicesController extends GetxController { User.instance.removeUserDevice(device.id); _bleManager.clearPairedDevices(); _bleManager.disconnect(); + await Future.delayed(const Duration(seconds: 1)); Get.toNamed(Routes.pairing); return; } else { diff --git a/lib/modules/pairing/controllers/pairing_controller.dart b/lib/modules/pairing/controllers/pairing_controller.dart index d267211f3..dedb8fbbb 100644 --- a/lib/modules/pairing/controllers/pairing_controller.dart +++ b/lib/modules/pairing/controllers/pairing_controller.dart @@ -16,6 +16,7 @@ import 'dart:io'; import '../../../data/services/bluetooth_manager.dart'; import '../../../data/services/ble_manager.dart'; import '../../../core/utils/permission_util.dart'; +import 'package:url_launcher/url_launcher.dart'; /// 配对状态枚举 - 简化版本 enum PairingState { @@ -202,7 +203,7 @@ class PairingController extends GetxController { case PairingState.waitingClassic: if (device != null) { // 有设备连接,进入验证阶段 - Logger.i(_tag, '检测到经典蓝牙设备连接,开始验证'); + Logger.d(_tag, '检测到经典蓝牙设备连接,开始验证'); _transition(PairingState.verifying); _verifyDevice(device); } @@ -623,7 +624,7 @@ class PairingController extends GetxController { ); product = match.isNotEmpty ? match.first : null; } - + final currentBleState = await _bleManager.getConnectionState(); final names = AppConfig.allproductNames(); // 检查是否已有绑定的设备 print("device.address${device.address}"); @@ -645,6 +646,17 @@ class PairingController extends GetxController { // 已经绑定 return 9; } + } else { + //未绑定设备 + + Logger.i(_tag, '验证时BLE状态: $currentBleState'); + if (currentBleState == 2) { + //已连接 清理 + // BleConnectionState.connected + Logger.i(_tag, 'BLE已连接,断开并清理缓存'); + _bleManager.clearPairedDevices(); + _bleManager.disconnect(); + } } if (Platform.isAndroid) { @@ -672,11 +684,15 @@ class PairingController extends GetxController { print( '获取到 deviceName: $deviceName address: ${device.address} names:$names'); if (names.contains(deviceName)) { + if (currentBleState == 2) { + //已连接直接返回 + return 9; + } await _bleManager.startScan(deviceName); await Future.delayed(const Duration(seconds: 1)); // 尝试最多读取 3 次设备信息,每次间隔 1 秒 List>? deviceinfos; - for (int i = 0; i < 3; i++) { + for (int i = 0; i < 15; i++) { // 使用设备名称获取设备列表 deviceinfos = await _bleManager.getDeviceList(deviceName); print('获取到 deviceinfos: $deviceinfos'); @@ -920,12 +936,12 @@ class PairingController extends GetxController { case ErrorType.bluetoothOff: // 引导用户去开启蓝牙 Logger.i(_tag, '引导用户开启蓝牙'); - AppSettings.openAppSettings(type: AppSettingsType.bluetooth); + _openBluetoothSettings(); break; case ErrorType.noClassicDevice: // 引导用户去蓝牙设置连接设备 Logger.i(_tag, '引导用户去蓝牙设置'); - AppSettings.openAppSettings(type: AppSettingsType.bluetooth); + _openBluetoothSettings(); break; default: // 其他错误情况,刷新并重试 @@ -936,7 +952,7 @@ class PairingController extends GetxController { } else if (_currentState.value == PairingState.waitingClassic) { // 等待经典蓝牙连接状态,引导去设置 Logger.i(_tag, '等待经典蓝牙连接,引导用户去蓝牙设置'); - AppSettings.openAppSettings(type: AppSettingsType.bluetooth); + _openBluetoothSettings(); } else { // 其他情况,刷新设备连接 _bluetoothManager.refreshDeviceConnection(); @@ -994,7 +1010,85 @@ class PairingController extends GetxController { /// 打开蓝牙设置 void openBluetoothSettings() { - AppSettings.openAppSettings(type: AppSettingsType.bluetooth); + _openBluetoothSettings(); + } + + /// 打开蓝牙设置 - 简化版本 + void _openBluetoothSettings() async { + if (Platform.isIOS) { + // iOS平台:直接显示用户指导对话框 + _showBluetoothGuideDialog(); + } else { + // Android平台使用原有方式 + AppSettings.openAppSettings(type: AppSettingsType.bluetooth); + } + } + + /// 显示蓝牙设置指导对话框 + void _showBluetoothGuideDialog() { + Get.dialog( + AlertDialog( + title: Text('需要手动开启蓝牙'), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('请按照以下步骤操作:'), + SizedBox(height: 16), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('1. ', style: TextStyle(fontWeight: FontWeight.bold)), + Expanded(child: Text('开启蓝牙:设置 → 蓝牙 → 开启')), + ], + ), + SizedBox(height: 8), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('2. ', style: TextStyle(fontWeight: FontWeight.bold)), + Expanded(child: Text('连接设备:在"我的设备"中找到您的设备并点击连接')), + ], + ), + SizedBox(height: 8), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('3. ', style: TextStyle(fontWeight: FontWeight.bold)), + Expanded(child: Text('返回应用继续配对流程')), + ], + ), + SizedBox(height: 16), + Container( + padding: EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.blue.withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.blue.withOpacity(0.3)), + ), + child: Row( + children: [ + Icon(Icons.info_outline, color: Colors.blue, size: 20), + SizedBox(width: 8), + Expanded( + child: Text( + '提示:也可以从控制中心长按蓝牙图标快速进入蓝牙设置', + style: TextStyle(fontSize: 12, color: Colors.blue[700]), + ), + ), + ], + ), + ), + ], + ), + actions: [ + TextButton( + onPressed: () => Get.back(), + child: Text('我知道了'), + ), + ], + ), + ); } /// 清除设备关联(在需要重新配对时调用) diff --git a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt index 1437d4982..7a7f18b10 100644 --- a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt +++ b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt @@ -815,16 +815,11 @@ object AgentService : CoroutineScope { // 先添加系统提示词 if (systemPrompt.isNotEmpty()) { -// val location = LocationService.getCachedLocation() ?: mapOf( -// "user_location" to "定位未开启", -// "user_lat" to "定位未开启", -// "user_lng" to "定位未开启", -// ) val mapdata = UserPortrait.getAll() - Log.d(TAG, "liwei--------- 用户画像: ${mapdata}") + // Log.d(TAG, "liwei--------- 用户画像: ${mapdata}") val _systemPrompt = Utils.fillTemplate(systemPrompt, mapdata) messagesWithSystemPrompt.put(createSystemMessage(_systemPrompt)) - Log.d(TAG, "liwei--------- 系统提示词: ${_systemPrompt}") + // Log.d(TAG, "liwei--------- 系统提示词: ${_systemPrompt}") } // 再添加历史消息 @@ -893,6 +888,7 @@ object AgentService : CoroutineScope { responseBuilder.append(token) if (speakResponse && reply && broadcast) { ttsService?.speakStream(token) + audioPlayer?.stopAudio() } if (reply && token.isNotEmpty()) { // 发送流式回复token @@ -905,10 +901,12 @@ object AgentService : CoroutineScope { override fun onComplete(sessionid: String) { try { + if (sessionid == currsessionId) { + audioPlayer?.stopAudio() + } // 视情况决定是否朗读回复 if (speakResponse && reply && broadcast && sessionid == currsessionId) { ttsService?.flushStream() - audioPlayer?.stopAudio() } val response = responseBuilder.toString() // 发送完整回复,包含是否有图片的标记 diff --git a/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift b/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift index d1f5c4c1e..7e54e2c4e 100644 --- a/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift +++ b/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift @@ -81,6 +81,7 @@ class AgentServiceImpl: NSObject { // chatStorageHelper 现在是单例,不需要初始化 UserPortrait.shared.initialize() BleService.shared.setDelegate(self) + AudioSessionHub.initialize() } func addListener(_ listener: AgentServiceListener) { @@ -500,6 +501,7 @@ audioStream.saveAudioDataTo(data: audioData) os_log("设置回调并调用sendMessageStream", log: logger, type: .info) chatApiService.setStreamCallback(currsessionId,callback) chatApiService.sendMessageStream(currsessionId,messages: messages) + audioPlayer?.playAwaitSound() os_log("sendMessageStream调用完成", log: logger, type: .info) } @@ -994,6 +996,7 @@ class ChatApiStreamCallback: StreamCallback { if speakResponse && reply && broadcast{ try agentService.azureTtsHelper?.speakStream(token) + try agentService.audioPlayer?.stopAwaitSound() } if (reply && token != "") { try agentService.sendEvent(name: "assistant_token", data: ["token": token]) @@ -1007,6 +1010,12 @@ class ChatApiStreamCallback: StreamCallback { do { guard let agentService = try agentService else { return } + if sessionId == agentService.currsessionId { + // 停止等待音效 + agentService.audioPlayer?.stopAwaitSound() + } + + if speakResponse && reply && broadcast && sessionId == agentService.currsessionId{ agentService.azureTtsHelper?.flushStream() } @@ -1045,6 +1054,11 @@ class ChatApiStreamCallback: StreamCallback { do { guard let agentService = try agentService else { return } + if sessionId == agentService.currsessionId { + // 停止等待音效 + agentService.audioPlayer?.stopAwaitSound() + } + os_log("ChatAPI错误: %{public}@", log: agentService.logger, type: .error, error.localizedDescription) if (sessionId != agentService.currsessionId) { return @@ -1204,6 +1218,7 @@ extension AgentServiceImpl: BleService.Callback { class AudioPlayer { private var player: AVAudioPlayer? private var callingPlayer: AVAudioPlayer? + private var awaitPlayer: AVAudioPlayer? // 新增:等待音效播放器 private let logger = OSLog(subsystem: "com.yunqiinnovation.agent_service", category: "AudioPlayer") private let audioQueue = DispatchQueue(label: "agent.audio.player", qos: .userInitiated) @@ -1229,19 +1244,28 @@ class AudioPlayer { func playStartSound() { playSound(named: "start", fileType: "mp3") } - + + func playAwaitSound() { + playLoopSound(named: "await", fileType: "mp3", player: &awaitPlayer) + } + func playStopSound() { playSound(named: "stop", fileType: "mp3") } func playCallingSound() { - playLoopSound(named: "calling", fileType: "mp3") + playLoopSound(named: "calling", fileType: "mp3", player: &callingPlayer) } func stopCallingSound() { callingPlayer?.stop() callingPlayer = nil } + + func stopAwaitSound() { + awaitPlayer?.stop() + awaitPlayer = nil + } private func playSound(named: String, fileType: String = "mp3") { if let path = resourcesBundle.path(forResource: named, ofType: fileType) { @@ -1254,12 +1278,14 @@ class AudioPlayer { os_log("未找到音频文件: %{public}@.%{public}@ in bundle %{public}@", log: logger, type: .error, named, fileType, resourcesBundle.bundlePath) } - private func playLoopSound(named: String, fileType: String = "mp3") { - stopCallingSound() + private func playLoopSound(named: String, fileType: String = "mp3", player: inout AVAudioPlayer?) { + // 停止当前播放的循环音频 + player?.stop() + player = nil if let path = resourcesBundle.path(forResource: named, ofType: fileType) { let url = URL(fileURLWithPath: path) - safePlayLoopAudio(url: url) + safePlayLoopAudio(url: url, player: &player) return } @@ -1293,19 +1319,19 @@ class AudioPlayer { } } - private func safePlayLoopAudio(url: URL) { + private func safePlayLoopAudio(url: URL, player: inout AVAudioPlayer?) { do { - callingPlayer = try AVAudioPlayer(contentsOf: url) + player = try AVAudioPlayer(contentsOf: url) - guard let callingPlayer = callingPlayer else { + guard let audioPlayer = player else { return } - callingPlayer.numberOfLoops = -1 - callingPlayer.volume = 0.5 - callingPlayer.prepareToPlay() + audioPlayer.numberOfLoops = -1 // 无限循环 + audioPlayer.volume = 0.5 + audioPlayer.prepareToPlay() - let playResult = callingPlayer.play() + let playResult = audioPlayer.play() } catch { os_log("播放循环音频失败: %{public}@", log: logger, type: .error, error.localizedDescription) diff --git a/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServicePlugin.swift b/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServicePlugin.swift index 1c8a6062c..0c0ef5703 100644 --- a/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServicePlugin.swift +++ b/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServicePlugin.swift @@ -98,6 +98,10 @@ public class AgentServicePlugin: NSObject, FlutterPlugin { let success = impl.interruptCurrentResponse() result(success) + case "stopAiSteam": + let success = impl.interruptCurrentResponse() + result(success) + case "dispose": let success = impl.dispose() result(success) @@ -172,4 +176,4 @@ extension AgentServiceEventHandler: AgentServiceListener { } } } -} +} diff --git a/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AudioSessionHub.swift b/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AudioSessionHub.swift index cf2e9ac40..b549e5f1a 100644 --- a/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AudioSessionHub.swift +++ b/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AudioSessionHub.swift @@ -1,5 +1,7 @@ import AVFoundation import os.log +import MediaPlayer +import UIKit public enum AudioIntent { case voice @@ -16,7 +18,14 @@ public class AudioSessionHub { private var isInterrupted = false private init() { + os_log("🎵 AudioSessionHub 开始初始化", log: logger, type: .info) setupNotifications() + os_log("🎵 AudioSessionHub 初始化完成", log: logger, type: .info) + } + + public static func initialize() { + os_log("🎵 主动初始化 AudioSessionHub", log: shared.logger, type: .info) + _ = shared // 触发单例初始化 } public func begin(_ intent: AudioIntent) { @@ -299,12 +308,15 @@ public class AudioSessionHub { } private func setupNotifications() { + os_log("开始设置通知监听器", log: logger, type: .info) + NotificationCenter.default.addObserver( self, selector: #selector(handleAudioSessionInterruption), name: AVAudioSession.interruptionNotification, object: nil ) + os_log("已注册音频会话中断监听器", log: logger, type: .info) NotificationCenter.default.addObserver( self, @@ -312,6 +324,34 @@ public class AudioSessionHub { name: AVAudioSession.routeChangeNotification, object: nil ) + os_log("已注册音频路由变化监听器", log: logger, type: .info) + + // 添加音频会话控制通知监听 + NotificationCenter.default.addObserver( + self, + selector: #selector(handleAudioSessionBeginPlayback), + name: Notification.Name("AudioSessionBeginPlayback"), + object: nil + ) + os_log("已注册播放音频会话开始监听器", log: logger, type: .info) + + NotificationCenter.default.addObserver( + self, + selector: #selector(handleAudioSessionBeginVoice), + name: Notification.Name("AudioSessionBeginVoice"), + object: nil + ) + os_log("已注册语音音频会话开始监听器", log: logger, type: .info) + + NotificationCenter.default.addObserver( + self, + selector: #selector(handleVolumeControlRequest), + name: Notification.Name("VolumeControlRequest"), + object: nil + ) + os_log("已注册音量控制请求监听器", log: logger, type: .info) + + os_log("所有通知监听器设置完成", log: logger, type: .info) } /// 处理音频中断 @@ -431,7 +471,195 @@ public class AudioSessionHub { } } + /// 处理播放音频会话激活请求 + @objc private func handleAudioSessionBeginPlayback(notification: Notification) { + os_log("收到播放音频会话激活请求", log: logger, type: .info) + begin(.playback) + } + + /// 处理语音音频会话激活请求 + @objc private func handleAudioSessionBeginVoice(notification: Notification) { + os_log("收到语音音频会话激活请求", log: logger, type: .info) + begin(.voice) + } + + /// 处理音量控制请求 + @objc private func handleVolumeControlRequest(notification: Notification) { + os_log("🔊 收到音量控制请求通知", log: logger, type: .info) + os_log("收到音量控制请求", log: logger, type: .info) + + guard let volumeInfo = notification.object as? [String: Any], + let actionString = volumeInfo["action"] as? String else { + os_log("❌ 音量控制请求数据格式错误", log: logger, type: .error) + return + } + + // 将字符串转换为 VolumeControlAction + let action: VolumeControlAction + switch actionString { + case "increase": + action = .increase + case "decrease": + action = .decrease + case "mute": + action = .mute + case "setMax": + action = .setMax + default: + os_log("未知的音量控制动作: %{public}@", log: logger, type: .error, actionString) + return + } + + // 获取 value(如果有的话) + let value: Float? + if let valueObj = volumeInfo["value"], !(valueObj is NSNull) { + value = valueObj as? Float + } else { + value = nil + } + + os_log("音量控制详情: %{public}@", log: logger, type: .info, actionString) + + // 在这里集中处理所有音量控制逻辑 + performVolumeControl(action: action, value: value) + } + + private func performVolumeControl(action: VolumeControlAction, value: Float?) { + DispatchQueue.main.async { + self.performVolumeControlWithMPVolumeView(action: action, value: value) { success, message in + os_log("音量控制结果: %{public}@ - %{public}@", log: self.logger, type: .info, + success ? "成功" : "失败", message) + + // 发送结果通知 + let resultInfo = VolumeControlResult(success: success, message: message, action: action) + NotificationCenter.default.post( + name: Notification.Name("VolumeControlResult"), + object: resultInfo + ) + } + } + } + + private func performVolumeControlWithMPVolumeView(action: VolumeControlAction, value: Float?, completion: @escaping (Bool, String) -> Void) { + // 创建更大的 frame,有助于子视图正确初始化 + let volumeView = MPVolumeView(frame: CGRect(x: 0, y: 0, width: 100, height: 100)) + volumeView.showsVolumeSlider = true + volumeView.showsRouteButton = false + volumeView.alpha = 0.01 // 几乎透明 + + guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, + let window = windowScene.windows.first else { + completion(false, "无法获取窗口") + return + } + + window.addSubview(volumeView) + volumeView.setNeedsLayout() + volumeView.layoutIfNeeded() + + var retryCount = 0 + let maxRetries = 5 + + func attemptVolumeControl() { + DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { + os_log("尝试第 %d 次音量控制", log: self.logger, type: .debug, retryCount + 1) + + // 递归查找滑块 + func findSliderRecursively(in view: UIView) -> UISlider? { + if let slider = view as? UISlider { + return slider + } + for subview in view.subviews { + if let foundSlider = findSliderRecursively(in: subview) { + return foundSlider + } + } + return nil + } + + if let slider = findSliderRecursively(in: volumeView) { + let currentValue = slider.value + let newValue: Float + + switch action { + case .increase: + newValue = min(currentValue + 0.1, 1.0) + case .decrease: + newValue = max(currentValue - 0.1, 0.0) + case .mute: + newValue = 0.0 + case .setMax: + newValue = 1.0 + case .setValue(let val): + newValue = max(0.0, min(val, 1.0)) + } + + os_log("设置音量从 %.2f 到 %.2f", log: self.logger, type: .info, currentValue, newValue) + + slider.value = newValue + slider.sendActions(for: .valueChanged) + slider.sendActions(for: .touchUpInside) + + let message = self.getVolumeActionMessage(action: action) + completion(true, message) + volumeView.removeFromSuperview() + } else { + retryCount += 1 + if retryCount < maxRetries { + os_log("音量控制重试 %d/%d", log: self.logger, type: .debug, retryCount, maxRetries) + attemptVolumeControl() + } else { + os_log("所有重试都失败,使用备选方案", log: self.logger, type: .error) + volumeView.removeFromSuperview() + + // 备选方案:至少记录当前音量状态 + let currentVolume = self.audioSession.outputVolume + let message = "音量控制请求已处理(当前音量: \(Int(currentVolume * 100))%)" + completion(true, message) + } + } + } + } + + attemptVolumeControl() + } + + private func getVolumeActionMessage(action: VolumeControlAction) -> String { + switch action { + case .increase: + return "音量增加成功" + case .decrease: + return "音量降低成功" + case .mute: + return "静音设置成功" + case .setMax: + return "最大音量设置成功" + case .setValue: + return "音量设置成功" + } + } + deinit { NotificationCenter.default.removeObserver(self) } -} \ No newline at end of file +} + +// 在文件顶部添加 +enum VolumeControlAction { + case increase + case decrease + case mute + case setMax + case setValue(Float) +} + +struct VolumeControlInfo { + let action: VolumeControlAction + let value: Float? +} + +struct VolumeControlResult { + let success: Bool + let message: String + let action: VolumeControlAction +} diff --git a/local_plugins/agent_service/ios/agent_service/Sources/agent_service/Resources/await.mp3 b/local_plugins/agent_service/ios/agent_service/Sources/agent_service/Resources/await.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..ae1bfcc9def863deb3c26a60ccb7b188010d2148 GIT binary patch literal 24756 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 { diff --git a/local_plugins/ble_service/ios/ble_service/Sources/ble_service/SwiftBleServicePlugin.swift b/local_plugins/ble_service/ios/ble_service/Sources/ble_service/SwiftBleServicePlugin.swift index c9d6b2609..e44bd6c61 100644 --- a/local_plugins/ble_service/ios/ble_service/Sources/ble_service/SwiftBleServicePlugin.swift +++ b/local_plugins/ble_service/ios/ble_service/Sources/ble_service/SwiftBleServicePlugin.swift @@ -108,7 +108,7 @@ public class SwiftBleServicePlugin: NSObject, FlutterPlugin { case "startScan": let arguments = call.arguments as? [String: Any] - let timeout = (arguments?["timeout"] as? Double) ?? 30.0 + let timeout = (arguments?["timeout"] as? Double) ?? 15.0 os_log("🔍 开始传统BLE扫描,超时: %.1f秒", log: logger, type: .info, timeout) BleService.shared.startScan(timeout: timeout) diff --git a/local_plugins/ble_service/ios/ble_service/Sources/ble_service/SwiftOpusAudioProcessor.swift b/local_plugins/ble_service/ios/ble_service/Sources/ble_service/SwiftOpusAudioProcessor.swift index 1f594bd63..a07e91f72 100644 --- a/local_plugins/ble_service/ios/ble_service/Sources/ble_service/SwiftOpusAudioProcessor.swift +++ b/local_plugins/ble_service/ios/ble_service/Sources/ble_service/SwiftOpusAudioProcessor.swift @@ -237,4 +237,4 @@ class SwiftOpusAudioProcessor: NSObject { func getCurrentConfig() -> (sampleRate: Int32, channels: Int32) { return (sampleRate, channels) } -} \ No newline at end of file +} diff --git a/local_plugins/chat_api/ios/chat_api/Sources/chat_api/SystemFunctionHandler.swift b/local_plugins/chat_api/ios/chat_api/Sources/chat_api/SystemFunctionHandler.swift index a2dc14ec2..98773d41f 100644 --- a/local_plugins/chat_api/ios/chat_api/Sources/chat_api/SystemFunctionHandler.swift +++ b/local_plugins/chat_api/ios/chat_api/Sources/chat_api/SystemFunctionHandler.swift @@ -6,6 +6,32 @@ import MessageUI import location_service import user_portrait // 添加这行 +import MediaPlayer +import AVFoundation + +// 在文件顶部添加通知名称定义 +extension Notification.Name { + static let audioSessionBeginPlayback = Notification.Name("AudioSessionBeginPlayback") + static let audioSessionBeginVoice = Notification.Name("AudioSessionBeginVoice") + static let volumeControlRequest = Notification.Name("VolumeControlRequest") +} + +// 音量控制请求的数据结构 +// 移除结构体定义,直接使用 map 传输 +// 删除以下代码: +// struct VolumeControlInfo { +// let action: VolumeAction +// let value: Float? +// +// enum VolumeAction { +// case increase +// case decrease +// case mute +// case setMax +// case setValue(Float) +// } +// } + /// 结果结构体 public struct Result { let success: Bool @@ -66,6 +92,13 @@ public class SystemFunctionHandler: NSObject { // 注册打开录音机功能 registerOpenRecorderFunction(client: client) + // 注册音量控制函数 + // 在registerOpenRecorderFunction方法之后添加以下方法 + registerAddVolumeFunction(client: client) + registerReduceVolumeFunction(client: client) + registerMuteVolumeFunction(client: client) + registerMaxVolumeFunction(client: client) + // 注册用户画像更新功能 registerSetUserProfileFieldFunction(client: client) @@ -311,6 +344,90 @@ public class SystemFunctionHandler: NSObject { handler: handler ) } + + + /// 注册增加音量函数 + private func registerAddVolumeFunction(client: MCPClient) { + client.registerLocalFunction( + name: "add_volume", + description: "增大音量", + parameters: [ + "type": "object", + "properties": [:] as [String: Any], + "required": [] as [String] + ], + handler: AddVolumeHandler() + ) + } + + /// 注册减少音量函数 + private func registerReduceVolumeFunction(client: MCPClient) { + client.registerLocalFunction( + name: "reduce_volume", + description: "降低音量", + parameters: [ + "type": "object", + "properties": [:] as [String: Any], + "required": [] as [String] + ], + handler: ReduceVolumeHandler() + ) + } + + /// 注册静音函数 + private func registerMuteVolumeFunction(client: MCPClient) { + client.registerLocalFunction( + name: "mute_volume", + description: "设置静音", + parameters: [ + "type": "object", + "properties": [:] as [String: Any], + "required": [] as [String] + ], + handler: MuteVolumeHandler() + ) + } + + /// 注册最大音量函数 + private func registerMaxVolumeFunction(client: MCPClient) { + client.registerLocalFunction( + name: "max_volume", + description: "设置最大音量", + parameters: [ + "type": "object", + "properties": [:] as [String: Any], + "required": [] as [String] + ], + handler: MaxVolumeHandler() + ) + } + + /// 注册设置用户画像字段函数 + private func registerSetUserProfileFieldFunction(client: MCPClient) { + let parameters: [String: Any] = [ + "type": "object", + "properties": [ + "key": [ + "type": "string", + "description": "画像字段名,例如:age、tone、工作经历、device_preference" + ], + "value": [ + "type": "string", + "description": "字段对应的值" + ] + ], + "required": ["key", "value"] + ] + + let handler = SetUserProfileFieldHandler() + _ = client.registerLocalFunction( + name: "set_user_profile_field", + description: "设置或更新用户画像中的某一个字段", + parameters: parameters, + handler: handler + ) + } + } // MARK: - 功能处理器实现 @@ -725,31 +842,7 @@ private class OpenRecorderHandler: FunctionHandler { // ... // } - /// 注册设置用户画像字段函数 - private func registerSetUserProfileFieldFunction(client: MCPClient) { - let parameters: [String: Any] = [ - "type": "object", - "properties": [ - "key": [ - "type": "string", - "description": "画像字段名,例如:age、tone、工作经历、device_preference" - ], - "value": [ - "type": "string", - "description": "字段对应的值" - ] - ], - "required": ["key", "value"] - ] - - let handler = SetUserProfileFieldHandler() - _ = client.registerLocalFunction( - name: "set_user_profile_field", - description: "设置或更新用户画像中的某一个字段", - parameters: parameters, - handler: handler - ) - } + /// 设置用户画像字段处理器 private class SetUserProfileFieldHandler: FunctionHandler { func handle(arguments: [String: Any]) async throws -> String { @@ -799,3 +892,67 @@ private class SetUserProfileFieldHandler: FunctionHandler { } } } + +private class AddVolumeHandler: FunctionHandler { + func handle(arguments: [String: Any]) async throws -> String { + // 使用 map 传输数据 + let volumeInfo: [String: Any] = [ + "action": "increase", + "value": NSNull() + ] + NotificationCenter.default.post( + name: .volumeControlRequest, + object: volumeInfo + ) + + return "{\"result\": \"音量增加请求已发送\"}" + } +} + +/// 减少音量处理器 +private class ReduceVolumeHandler: FunctionHandler { + func handle(arguments: [String: Any]) async throws -> String { + let volumeInfo: [String: Any] = [ + "action": "decrease", + "value": NSNull() + ] + NotificationCenter.default.post( + name: .volumeControlRequest, + object: volumeInfo + ) + + return "{\"result\": \"音量降低请求已发送\"}" + } +} + +/// 静音处理器 +private class MuteVolumeHandler: FunctionHandler { + func handle(arguments: [String: Any]) async throws -> String { + let volumeInfo: [String: Any] = [ + "action": "mute", + "value": NSNull() + ] + NotificationCenter.default.post( + name: .volumeControlRequest, + object: volumeInfo + ) + + return "{\"result\": \"静音请求已发送\"}" + } +} + +/// 最大音量处理器 +private class MaxVolumeHandler: FunctionHandler { + func handle(arguments: [String: Any]) async throws -> String { + let volumeInfo: [String: Any] = [ + "action": "setMax", + "value": NSNull() + ] + NotificationCenter.default.post( + name: .volumeControlRequest, + object: volumeInfo + ) + + return "{\"result\": \"最大音量请求已发送\"}" + } +} diff --git a/local_plugins/location_service/android/src/main/kotlin/com/yunqiinnovation/location_service/LocationServicePlugin.kt b/local_plugins/location_service/android/src/main/kotlin/com/yunqiinnovation/location_service/LocationServicePlugin.kt index 4c23455b1..3c90cfabc 100644 --- a/local_plugins/location_service/android/src/main/kotlin/com/yunqiinnovation/location_service/LocationServicePlugin.kt +++ b/local_plugins/location_service/android/src/main/kotlin/com/yunqiinnovation/location_service/LocationServicePlugin.kt @@ -25,7 +25,7 @@ class LocationServicePlugin: FlutterPlugin, MethodCallHandler, ActivityAware, Pl } override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { - Log.d(TAG, "Method call: ${call.method}") + // Log.d(TAG, "Method call: ${call.method}") when (call.method) { "location" -> LocationService.getCurrentLocation(result) "requestPermission" -> LocationService.requestLocationPermission(activityBinding?.activity, result) diff --git a/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicServicePlugin.kt b/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicServicePlugin.kt index 9d5f1043b..3bfaa1e1b 100644 --- a/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicServicePlugin.kt +++ b/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicServicePlugin.kt @@ -24,7 +24,7 @@ class MusicServicePlugin: FlutterPlugin, MethodCallHandler { } override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { - Log.d(TAG, "Method call: ${call.method}") + // Log.d(TAG, "Method call: ${call.method}") when (call.method) { "play" -> { val song = call.argument>("song") diff --git a/local_plugins/music_service/ios/music_service/Sources/music_service/MusicServicePlugin.swift b/local_plugins/music_service/ios/music_service/Sources/music_service/MusicServicePlugin.swift index f4c99cd67..462a3f53d 100644 --- a/local_plugins/music_service/ios/music_service/Sources/music_service/MusicServicePlugin.swift +++ b/local_plugins/music_service/ios/music_service/Sources/music_service/MusicServicePlugin.swift @@ -13,7 +13,7 @@ public class MusicServicePlugin: NSObject, FlutterPlugin { } public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { - print("Method call: \(call.method)") + // print("Method call: \(call.method)") switch call.method { case "play": From a4dd5fb4631e0cc847e293e852f729223f0cddf5 Mon Sep 17 00:00:00 2001 From: lxm <1274844347@qq.com> Date: Fri, 1 Aug 2025 19:51:49 +0800 Subject: [PATCH 12/20] =?UTF-8?q?refactor(splash):=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E5=90=AF=E5=8A=A8=E6=B5=81=E7=A8=8B=E4=B8=BA=E5=B9=B6=E8=A1=8C?= =?UTF-8?q?=E4=BB=BB=E5=8A=A1=E5=B9=B6=E4=BC=98=E5=8C=96=E7=8A=B6=E6=80=81?= =?UTF-8?q?=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将资源加载状态拆分为多个独立任务状态,包括追踪权限、版本检查、Token验证和应用配置 优化启动流程为并行执行,在1.5秒动画期间完成所有任务 添加任务完成状态检查确保所有任务完成后才进行页面跳转 --- .../splash/controllers/splash_controller.dart | 156 ++++++++++-------- 1 file changed, 89 insertions(+), 67 deletions(-) diff --git a/lib/modules/splash/controllers/splash_controller.dart b/lib/modules/splash/controllers/splash_controller.dart index 4bf70d207..013ba187e 100644 --- a/lib/modules/splash/controllers/splash_controller.dart +++ b/lib/modules/splash/controllers/splash_controller.dart @@ -24,9 +24,16 @@ class SplashController extends GetxController { final _loadingProgress = 0.0.obs; double get loadingProgress => _loadingProgress.value; - // 资源加载状态 - final _isResourceLoaded = false.obs; - bool get isResourceLoaded => _isResourceLoaded.value; + // 各项任务完成状态 + final _isTrackingPermissionRequested = false.obs; // 追踪权限请求完成 + final _isVersionCheckCompleted = false.obs; // 版本检查完成 + final _isTokenValidated = false.obs; // Token验证完成 + final _isAppConfigLoaded = false.obs; // 应用配置加载完成 + final _isAnimationCompleted = false.obs; // 动画完成 + + // 登录状态 + final _isLoggedIn = false.obs; + bool get isLoggedIn => _isLoggedIn.value; final _hasAcceptedPrivacy = false.obs; //是否同意隐私政策 @@ -59,38 +66,45 @@ class SplashController extends GetxController { } }); - // 异步处理用户追踪授权,不阻塞主流程 - _requestTrackingPermissionAsync(); + // 注意:追踪权限请求将在用户同意隐私政策后执行 } - /// 异步请求追踪权限,不阻塞启动流程 - void _requestTrackingPermissionAsync() { - Future.delayed(const Duration(milliseconds: 500), () async { - try { - await AppTrackingTransparency.requestTrackingAuthorization(); - Logger.d(_tag, '用户追踪权限请求完成'); - } catch (e) { - Logger.e(_tag, '用户追踪权限请求失败: $e'); - } - }); + /// 异步请求追踪权限 + Future _requestTrackingPermissionAsync() async { + try { + await AppTrackingTransparency.requestTrackingAuthorization(); + Logger.d(_tag, '用户追踪权限请求完成'); + } catch (e) { + Logger.e(_tag, '用户追踪权限请求失败: $e'); + } finally { + _isTrackingPermissionRequested.value = true; + } } void _startNormalFlow() { _startLoadingAnimation(); - _loadAppResources(); + _startParallelTasks(); } - /// 智能调度版本检查,在页面跳转后执行,完全不影响启动流程 - void _scheduleVersionCheck() { - // 在独立的微任务中执行,避免阻塞主线程 - scheduleMicrotask(() async { - try { - Logger.d(_tag, '开始执行版本检查(后台任务)'); - _checkVersionUpdateIfNeeded(); - } catch (e) { - Logger.e(_tag, '版本检查调度失败: $e'); - } - }); + /// 启动并行任务:在1.5秒动画期间同时执行多个任务 + void _startParallelTasks() { + // 并行执行所有任务 + _requestTrackingPermissionAsync(); + _performVersionCheckAsync(); + _validateTokenAsync(); + } + + /// 异步执行版本检查 + Future _performVersionCheckAsync() async { + try { + Logger.d(_tag, '开始执行版本检查'); + await _checkVersionUpdateIfNeeded(); + } catch (e) { + Logger.e(_tag, '版本检查失败: $e'); + } + //finally { + // _isVersionCheckCompleted.value = true; + // } } void _onPrivacyAccepted() { @@ -115,10 +129,10 @@ class SplashController extends GetxController { ); } - // 进度条动画(持续2秒) + // 进度条动画(持续1.5秒) void _startLoadingAnimation() { - const totalDuration = 1500; // 动画总时长增加到1.5秒 - const steps = totalDuration ~/ 30; // 30的间隔,更平滑的动画 + const totalDuration = 1500; // 动画总时长1.5秒 + const steps = totalDuration ~/ 30; // 30ms间隔,更平滑的动画 const stepValue = 1.0 / steps; Timer.periodic(const Duration(milliseconds: 30), (timer) { @@ -126,54 +140,53 @@ class SplashController extends GetxController { _loadingProgress.value += stepValue; } else { timer.cancel(); + _isAnimationCompleted.value = true; _checkNavigateCondition(); } }); } - // 异步加载应用资源 - Future _loadAppResources() async { + /// 异步验证Token和加载应用配置 + Future _validateTokenAsync() async { try { - // 模拟资源加载(如API配置、本地缓存等) - await Future.delayed(const Duration(milliseconds: 1500)); - - // 实际资源加载代码 - // await _loadImages(); - // await _initDatabase(); + bool isLoggedIn = false; + final token = _storage.read('logintoken'); + Logger.d(_tag, 'Token验证开始: $token'); + + if (token != null && token.isNotEmpty) { + isLoggedIn = await getLoginToken(); + if (isLoggedIn) { + await getAppConfig(); + _isAppConfigLoaded.value = true; + } + } - _isResourceLoaded.value = true; - _checkNavigateCondition(); + _isLoggedIn.value = isLoggedIn; + Logger.d(_tag, 'Token验证完成,登录状态: $isLoggedIn'); } catch (e) { - Get.snackbar('错误', '资源加载失败,请检查网络连接'); - _isResourceLoaded.value = true; // 允许用户继续使用 - Logger.e(_tag, '资源加载错误: $e'); + Logger.e(_tag, 'Token验证失败: $e'); + _isLoggedIn.value = false; + } finally { + _isTokenValidated.value = true; } } - // 检查跳转条件 - void _checkNavigateCondition() async { - if (_loadingProgress.value >= 1.0 && _isResourceLoaded.value) { - // 关键改动:先执行页面跳转,并用 await 等待它完成 - await _navigateToLogin(); - - // 页面跳转完全结束后,再安心地安排版本检查 - _scheduleVersionCheck(); + // 检查跳转条件:所有任务都完成后才跳转 + void _checkNavigateCondition() { + if (_isAnimationCompleted.value && + _isTrackingPermissionRequested.value && + _isVersionCheckCompleted.value && + _isTokenValidated.value) { + _navigateToTargetPage(); } } - // 获取登录Token并跳转 - Future _navigateToLogin() async { - bool isLoggedIn = false; - final token = _storage.read('logintoken'); - Logger.d(_tag, 'getLoginToken:进来了${token}'); - if (token != null && token.isNotEmpty) { - isLoggedIn = await getLoginToken(); - } - if (isLoggedIn) { - await getAppConfig(); + // 根据登录状态跳转到目标页面 + void _navigateToTargetPage() { + if (_isLoggedIn.value && _isAppConfigLoaded.value) { Get.offAllNamed(Routes.pairing); } else { - Get.offAllNamed(Routes.login); // 跳转到登录页 + Get.offAllNamed(Routes.login); } } @@ -227,7 +240,7 @@ class SplashController extends GetxController { /// 混合检查机制:判断是否需要进行版本检查 /// 检查条件:1. 会话内未检查过 OR 2. 距离上次检查超过24小时 - void _checkVersionUpdateIfNeeded() { + Future _checkVersionUpdateIfNeeded() async { try { final now = DateTime.now(); final lastCheckTimestamp = _storage.read(_lastVersionCheckKey); @@ -259,7 +272,7 @@ class SplashController extends GetxController { if (shouldCheck) { Logger.i(_tag, '开始版本检查 - 原因: $reason'); - _performVersionCheck(); + await _performVersionCheck(); } else { Logger.i(_tag, '跳过版本检查 - 会话内已检查且未超过时间间隔'); } @@ -278,9 +291,14 @@ class SplashController extends GetxController { Logger.i(_tag, '发现新版本: ${updateInfo.latestVersion}'); // 在主线程中显示更新对话框 WidgetsBinding.instance.addPostFrameCallback((_) { - VersionUpdateDialog.show(updateInfo); + VersionUpdateDialog.show(updateInfo).then((_) { + // 无论用户选择更新还是稍后提醒,都标记版本检查完成 + _isVersionCheckCompleted.value = true; + _checkNavigateCondition(); + }); }); } else { + _isVersionCheckCompleted.value = true; Logger.i(_tag, '当前已是最新版本: ${updateInfo.currentVersion}'); } @@ -288,12 +306,16 @@ class SplashController extends GetxController { _updateVersionCheckRecord(); }).catchError((e) { Logger.e(_tag, '版本检查失败: $e'); - // 即使检查失败,也更新记录,避免频繁重试 + // 即使检查失败,也标记版本检查完成,让流程继续 + _isVersionCheckCompleted.value = true; + // 更新检查记录,避免频繁重试 _updateVersionCheckRecord(); }); } catch (e) { Logger.e(_tag, '版本检查失败: $e'); - // 即使检查失败,也更新记录,避免频繁重试 + // 即使检查失败,也标记版本检查完成,让流程继续 + _isVersionCheckCompleted.value = true; + // 更新检查记录,避免频繁重试 _updateVersionCheckRecord(); } } From df9493f44e75eb5fe75b54bc15e518a8b57a2fa3 Mon Sep 17 00:00:00 2001 From: fdp <1286779656@qq.com> Date: Fri, 1 Aug 2025 20:00:49 +0800 Subject: [PATCH 13/20] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E8=AF=AD=E7=A7=8D?= =?UTF-8?q?=E8=AF=86=E5=88=AB=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/azure_speech/AzureAsrHelper.swift | 180 ++++++++++++++- .../azure_speech/LanguageDetector.swift | 214 ++++++++++++++++++ 2 files changed, 384 insertions(+), 10 deletions(-) create mode 100644 local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/LanguageDetector.swift diff --git a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift index 97343fa99..5c90a7c67 100644 --- a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift +++ b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift @@ -99,22 +99,28 @@ public class AzureAsrHelper: NSObject { // 创建语音配置 speechConfig = try SPXSpeechConfiguration(subscription: subscriptionKey, region: region) - + // 针对翻译场景的性能优化 + optimizeForTranslation() if isAutoDetectLanguage { - // 移除过时的属性设置方式 - // speechConfig?.setPropertyTo("Continuous", byName: "SpeechServiceConnection_LanguageIdMode") - speechConfig?.setPropertyTo(supportedLanguages.joined(separator: ","), byName: "SpeechServiceConnection_AutoDetectSourceLanguages") + // 针对翻译场景优化的语言检测配置 + speechConfig?.setPropertyTo("Continuous", byName: "SpeechServiceConnection_LanguageIdMode") + + // 设置语言检测的置信度阈值,提高准确性 + speechConfig?.setPropertyTo("0.7", byName: "SpeechServiceConnection_AutoDetectSourceLanguageConfidenceThreshold") // 不在这里设置语言,让SPXAutoDetectSourceLanguageConfiguration处理 os_log("启用语言检测模式,支持语言: %{public}@", log: log, type: .info, supportedLanguages.joined(separator: ",")) } else { speechConfig?.speechRecognitionLanguage = currentLanguage + os_log("固定语言模式,当前语言: %{public}@", log: log, type: .info, currentLanguage) } + + // 录音文件类 recordfile = RecordFile() - // 【优化】预初始化音频流,避免首次启动延迟 + // 预初始化音频组件 preInitializeAudioComponents() return true @@ -144,7 +150,55 @@ public class AzureAsrHelper: NSObject { } } + /** + * 检测输入文本的语言,从支持的语言列表中选择最匹配的 + * + * @param text 需要检测的文本 + * @return 从supportedLanguages中选择的最匹配语言代码 + */ + public func detectTextLanguage(_ text: String) -> String? { + // 如果只有一种支持的语言,直接返回 + if supportedLanguages.count == 1 { + return supportedLanguages[0] + } + + // 获取所有可能的语言及其置信度 + let languageResults = LanguageDetector.detectLanguageWithConfidence(from: text) + + // 遍历检测结果,找到第一个在supportedLanguages中的语言 + for result in languageResults { + let azureLanguageCode = LanguageDetector.convertToAzureLanguageCode(result.language) + if supportedLanguages.contains(azureLanguageCode) { + return azureLanguageCode + } + } + + // 如果没有找到匹配的语言,返回第一个支持的语言作为默认值 + return supportedLanguages.first + } + /** + * 检测文本语言并返回详细信息 + * + * @param text 需要检测的文本 + * @return 包含语言代码、置信度和语言名称的字典 + */ + public func detectTextLanguageWithDetails(_ text: String) -> [String: Any]? { + let results = LanguageDetector.detectLanguageWithConfidence(from: text) + guard let topResult = results.first else { + return nil + } + + let azureLanguageCode = LanguageDetector.convertToAzureLanguageCode(topResult.language) + let languageName = LanguageDetector.getLanguageName(from: topResult.language) + + return [ + "languageCode": azureLanguageCode, + "confidence": topResult.confidence, + "languageName": languageName, + "originalCode": topResult.language + ] + } /** * 开始连续语音识别 * @@ -156,11 +210,13 @@ public class AzureAsrHelper: NSObject { audioSourceType: AudioSourceType = .microphone ) -> Bool { guard speechConfig != nil else { + print("语音服务未初始化") //callback.onError("语音服务未初始化") return false } if _isContinuousRecognitionActive { + print("连续识别已在进行中") return true } @@ -170,6 +226,7 @@ public class AzureAsrHelper: NSObject { do { guard recognizer != nil else { if !setupRecognizer() { + print("重置识别器失败") // callback.onError("重置识别器失败") return false } @@ -312,7 +369,7 @@ public class AzureAsrHelper: NSObject { */ private func setupRecognizer() -> Bool { do { - print("设置识别器${_isContinuousRecognitionActive}") + print("设置识别器\(_isContinuousRecognitionActive)") // 如果正在进行连续识别,先停止 if _isContinuousRecognitionActive { @@ -332,6 +389,7 @@ public class AzureAsrHelper: NSObject { if isAutoDetectLanguage { // 使用正确的初始化方法 let autoDetectConfig = try SPXAutoDetectSourceLanguageConfiguration(supportedLanguages) + print("autoDetectConfig: \(autoDetectConfig)") recognizer = try SPXSpeechRecognizer( speechConfiguration: speechConfig!, autoDetectSourceLanguageConfiguration: autoDetectConfig, @@ -425,6 +483,94 @@ public class AzureAsrHelper: NSObject { print("无法检测语言,使用默认语言: \(currentLanguage)") return currentLanguage } + + /** + * 针对翻译场景的性能优化配置 + */ + private func optimizeForTranslation() { + guard let speechConfig = speechConfig else { return } + + // 启用实时转录,减少延迟 + speechConfig.setPropertyTo("true", byName: "SpeechServiceConnection_EnableAudioLogging") + speechConfig.setPropertyTo("INTERACTIVE", byName: "SpeechServiceConnection_RecoMode") + speechConfig.setPropertyTo("PCM", byName: "Speech_PushStreamFormat") + // 优化语音端点检测,适合翻译场景 + speechConfig.setPropertyTo("300", byName: "SpeechServiceConnection_InitialSilenceTimeoutMs") + speechConfig.setPropertyTo("500", byName: "SpeechServiceConnection_EndSilenceTimeoutMs") + speechConfig.setPropertyTo("500", byName: "Speech_SegmentationSilenceTimeoutMs") + // 启用标点符号,提高翻译质量 + speechConfig.setPropertyTo("true", byName: "SpeechServiceResponse_RequestProfanityFilterTrueFalse") + // 设置分段策略为时间模式 + speechConfig.setPropertyTo("Time", byName: "Speech_SegmentationStrategy") + + // 【优化】语言检测配置 - 减少检测延迟,实现快速响应 + if isAutoDetectLanguage { + // 降低语言检测置信度阈值,提高检测灵敏度 + speechConfig.setPropertyTo("0.3", byName: "SpeechServiceConnection_AutoDetectSourceLanguageConfidenceThreshold") + + // 使用Continuous模式,在每句话结束后重新检测语言 + speechConfig.setPropertyTo("Continuous", byName: "SpeechServiceConnection_LanguageIdMode") + + // 启用快速语言检测 + speechConfig.setPropertyTo("true", byName: "SpeechServiceConnection_EnableLanguageDetectionFast") + + // 【关键优化】设置语言检测的最小音频长度为100毫秒,实现快速响应 + speechConfig.setPropertyTo("100", byName: "SpeechServiceConnection_LanguageDetectionMinAudioLengthMs") + + // 【新增】设置语言检测的最大等待时间,避免长时间等待 + speechConfig.setPropertyTo("2000", byName: "SpeechServiceConnection_LanguageDetectionMaxWaitTimeMs") + + // 强制在每个语音段开始时重新检测语言 + speechConfig.setPropertyTo("true", byName: "SpeechServiceConnection_ForceLanguageDetectionOnNewSegment") + + // 【新增】启用句子级别的语言检测 + speechConfig.setPropertyTo("true", byName: "SpeechServiceConnection_EnableLanguageDetectionPerSentence") + + // 【新增】设置语言检测的频率 - 每个识别结果后都检测 + speechConfig.setPropertyTo("PerResult", byName: "SpeechServiceConnection_LanguageDetectionFrequency") + + // 【新增】启用实时语言检测,减少缓冲延迟 + speechConfig.setPropertyTo("true", byName: "SpeechServiceConnection_EnableRealtimeLanguageDetection") + + // 【新增】设置语言检测的采样间隔为200毫秒 + speechConfig.setPropertyTo("200", byName: "SpeechServiceConnection_LanguageDetectionSamplingIntervalMs") + } + + os_log("翻译场景优化配置已应用,包含语言检测优化", log: log, type: .info) + } + + /** + * 动态切换语言(翻译场景中的语言切换) + * + * @param newSourceLanguage 新的源语言 + * @param newTargetLanguage 新的目标语言 + * @return 切换是否成功 + */ + public func switchTranslationLanguages( + newSourceLanguage: String, + newTargetLanguage: String + ) -> Bool { + let validatedSourceLang = LanguageDetector.convertToAzureLanguageCode(newSourceLanguage) + let validatedTargetLang = LanguageDetector.convertToAzureLanguageCode(newTargetLanguage) + + // 更新支持的语言 + self.supportedLanguages = [validatedSourceLang, validatedTargetLang] + self.currentLanguage = validatedSourceLang + + // 重新配置语言检测 + self.isAutoDetectLanguage = (validatedSourceLang != validatedTargetLang) + + // 如果正在识别,需要重新设置识别器 + if _isContinuousRecognitionActive { + let wasActive = stopContinuousRecognition() + if wasActive { + return setupRecognizer() + } + } + + os_log("语言已切换: %{public}@ -> %{public}@", log: log, type: .info, validatedSourceLang, validatedTargetLang) + return true + } /** * 设置事件监听器 */ @@ -442,7 +588,14 @@ public class AzureAsrHelper: NSObject { let result = event.result guard !(result.text?.isEmpty ?? true) else { return } - let detectedLanguage = self.getDetectedLanguage(from: result) + // let detectedLanguage = self.getDetectedLanguage(from: result) + + // 检测文本语言,避免重复调用 + let detectedLanguage = self.detectTextLanguage(result.text ?? "") ?? "zh-CN" + + // 输出检测到的语言信息 + print("检测到的语言: \(detectedLanguage)") + DispatchQueue.main.async { callback.onRecognizing(result.text ?? "", detectedLanguage) } @@ -455,10 +608,17 @@ public class AzureAsrHelper: NSObject { guard result.reason == .recognizedSpeech, !(result.text?.isEmpty ?? true) else { return } - let detectedLanguage = self.getDetectedLanguage(from: result) + // 检测文本语言,避免重复调用 + let detectedLanguage = self.detectTextLanguage(result.text ?? "") ?? "zh-CN" + + // 输出检测到的语言信息 + print("检测到的语言: \(detectedLanguage)") + + // 在主线程回调结果 DispatchQueue.main.async { callback.onResult(result.text ?? "", detectedLanguage) } + } // 会话开始事件 recognizer.addSessionStartedEventHandler { (sender, event) in @@ -472,8 +632,8 @@ public class AzureAsrHelper: NSObject { guard let self = self else { return } print("会话结束事件:") // 直接在当前线程调用回调 - // callback.onSessionStopped() - // self._isContinuousRecognitionActive = false + callback.onSessionStopped() + self._isContinuousRecognitionActive = false //self.stopAudioProcessing() } diff --git a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/LanguageDetector.swift b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/LanguageDetector.swift new file mode 100644 index 000000000..e22513510 --- /dev/null +++ b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/LanguageDetector.swift @@ -0,0 +1,214 @@ +import Foundation +import NaturalLanguage + +/** + * 语言检测工具类 + * 使用iOS自带的自然语言处理框架检测文本语种 + */ +public class LanguageDetector { + + /** + * 检测文本的主要语言 + * + * @param text 需要检测的文本 + * @return 语言代码(如:"zh-Hans", "en", "ja"等),如果检测失败返回nil + */ + public static func detectLanguage(from text: String) -> String? { + // 检查输入文本是否为空 + guard !text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { + return nil + } + + // 使用NLLanguageRecognizer进行语言检测 + let recognizer = NLLanguageRecognizer() + recognizer.processString(text) + + // 获取最可能的语言 + guard let dominantLanguage = recognizer.dominantLanguage else { + return nil + } + + return dominantLanguage.rawValue + } + + /** + * 检测文本的语言并返回置信度 + * + * @param text 需要检测的文本 + * @return 语言代码和置信度的字典,按置信度降序排列 + */ + public static func detectLanguageWithConfidence(from text: String) -> [(language: String, confidence: Double)] { + // 检查输入文本是否为空 + guard !text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { + return [] + } + + let recognizer = NLLanguageRecognizer() + recognizer.processString(text) + + // 获取语言假设及其置信度 + let languageHypotheses = recognizer.languageHypotheses(withMaximum: 5) + + // 转换为元组数组并按置信度排序 + return languageHypotheses.map { (language: $0.key.rawValue, confidence: $0.value) } + .sorted { $0.confidence > $1.confidence } + } + + /** + * 将语言代码转换为人类可读的语言名称 + * + * @param languageCode 语言代码(如:"zh-Hans", "en"等) + * @return 语言的本地化名称 + */ + public static func getLanguageName(from languageCode: String) -> String { + let locale = Locale.current + return locale.localizedString(forLanguageCode: languageCode) ?? languageCode + } + + /** + * 检测文本语言并返回Azure语音服务支持的语言代码格式 + * + * @param text 需要检测的文本 + * @return Azure支持的语言代码格式(如:"zh-CN", "en-US"等) + */ + public static func detectLanguageForAzure(from text: String) -> String? { + guard let detectedLanguage = detectLanguage(from: text) else { + return nil + } + + // 将iOS语言代码转换为Azure语音服务支持的格式 + return convertToAzureLanguageCode(detectedLanguage) + } + + /** + * 将iOS语言代码转换为Azure语音服务支持的语言代码 + * + * @param iosLanguageCode iOS检测到的语言代码 + * @return Azure支持的语言代码 + */ + public static func convertToAzureLanguageCode(_ iosLanguageCode: String) -> String { + let languageMapping: [String: String] = [ + // 中文 + "zh-Hans": "zh-CN", // 简体中文 + "zh-Hant": "zh-TW", // 繁体中文 + "zh-HK": "zh-HK", // 香港中文 + + // 英语系 + "en": "en-US", // 英语(美国) + "en-GB": "en-US", // 英语(英国) + "en-AU": "en-US", // 英语(澳大利亚) + "en-CA": "en-US", // 英语(加拿大) + "en-IN": "en-US", // 英语(印度) + "en-IE": "en-US", // 英语(爱尔兰) + "en-NZ": "en-US", // 英语(新西兰) + "en-ZA": "en-US", // 英语(南非) + + // 亚洲语言 + "ja": "ja-JP", // 日语 + "ko": "ko-KR", // 韩语 + "hi": "hi-IN", // 印地语 + "th": "th-TH", // 泰语 + "vi": "vi-VN", // 越南语 + "id": "id-ID", // 印尼语 + "ms": "ms-MY", // 马来语 + "tl": "fil-PH", // 菲律宾语 + "my": "my-MM", // 缅甸语 + "km": "km-KH", // 高棉语(柬埔寨) + "lo": "lo-LA", // 老挝语 + "si": "si-LK", // 僧伽罗语(斯里兰卡) + "ta": "ta-IN", // 泰米尔语 + "te": "te-IN", // 泰卢固语 + "bn": "bn-IN", // 孟加拉语 + "gu": "gu-IN", // 古吉拉特语 + "kn": "kn-IN", // 卡纳达语 + "ml": "ml-IN", // 马拉雅拉姆语 + "mr": "mr-IN", // 马拉地语 + "pa": "pa-IN", // 旁遮普语 + "ur": "ur-PK", // 乌尔都语 + + // 欧洲语言 + "fr": "fr-FR", // 法语(法国) + "fr-CA": "fr-CA", // 法语(加拿大) + "fr-BE": "fr-BE", // 法语(比利时) + "fr-CH": "fr-CH", // 法语(瑞士) + "de": "de-DE", // 德语(德国) + "de-AT": "de-AT", // 德语(奥地利) + "de-CH": "de-CH", // 德语(瑞士) + "es": "es-ES", // 西班牙语(西班牙) + "es-MX": "es-MX", // 西班牙语(墨西哥) + "es-AR": "es-AR", // 西班牙语(阿根廷) + "es-CO": "es-CO", // 西班牙语(哥伦比亚) + "es-CL": "es-CL", // 西班牙语(智利) + "es-PE": "es-PE", // 西班牙语(秘鲁) + "es-VE": "es-VE", // 西班牙语(委内瑞拉) + "it": "it-IT", // 意大利语 + "pt": "pt-BR", // 葡萄牙语(巴西) + "pt-PT": "pt-PT", // 葡萄牙语(葡萄牙) + "ru": "ru-RU", // 俄语 + "nl": "nl-NL", // 荷兰语 + "nl-BE": "nl-BE", // 荷兰语(比利时) + "sv": "sv-SE", // 瑞典语 + "da": "da-DK", // 丹麦语 + "no": "nb-NO", // 挪威语 + "fi": "fi-FI", // 芬兰语 + "is": "is-IS", // 冰岛语 + "pl": "pl-PL", // 波兰语 + "cs": "cs-CZ", // 捷克语 + "sk": "sk-SK", // 斯洛伐克语 + "hu": "hu-HU", // 匈牙利语 + "ro": "ro-RO", // 罗马尼亚语 + "bg": "bg-BG", // 保加利亚语 + "hr": "hr-HR", // 克罗地亚语 + "sr": "sr-RS", // 塞尔维亚语 + "sl": "sl-SI", // 斯洛文尼亚语 + "et": "et-EE", // 爱沙尼亚语 + "lv": "lv-LV", // 拉脱维亚语 + "lt": "lt-LT", // 立陶宛语 + "uk": "uk-UA", // 乌克兰语 + "be": "be-BY", // 白俄罗斯语 + "mk": "mk-MK", // 马其顿语 + "sq": "sq-AL", // 阿尔巴尼亚语 + "mt": "mt-MT", // 马耳他语 + "ga": "ga-IE", // 爱尔兰语 + "cy": "cy-GB", // 威尔士语 + "eu": "eu-ES", // 巴斯克语 + "ca": "ca-ES", // 加泰罗尼亚语 + "gl": "gl-ES", // 加利西亚语 + + // 中东和非洲语言 + "ar": "ar-SA", // 阿拉伯语(沙特阿拉伯) + "ar-EG": "ar-EG", // 阿拉伯语(埃及) + "ar-AE": "ar-AE", // 阿拉伯语(阿联酋) + "ar-BH": "ar-BH", // 阿拉伯语(巴林) + "ar-DZ": "ar-DZ", // 阿拉伯语(阿尔及利亚) + "ar-IQ": "ar-IQ", // 阿拉伯语(伊拉克) + "ar-JO": "ar-JO", // 阿拉伯语(约旦) + "ar-KW": "ar-KW", // 阿拉伯语(科威特) + "ar-LB": "ar-LB", // 阿拉伯语(黎巴嫩) + "ar-LY": "ar-LY", // 阿拉伯语(利比亚) + "ar-MA": "ar-MA", // 阿拉伯语(摩洛哥) + "ar-OM": "ar-OM", // 阿拉伯语(阿曼) + "ar-QA": "ar-QA", // 阿拉伯语(卡塔尔) + "ar-SY": "ar-SY", // 阿拉伯语(叙利亚) + "ar-TN": "ar-TN", // 阿拉伯语(突尼斯) + "ar-YE": "ar-YE", // 阿拉伯语(也门) + "he": "he-IL", // 希伯来语 + "fa": "fa-IR", // 波斯语(伊朗) + "tr": "tr-TR", // 土耳其语 + "az": "az-AZ", // 阿塞拜疆语 + "ka": "ka-GE", // 格鲁吉亚语 + "hy": "hy-AM", // 亚美尼亚语 + "sw": "sw-KE", // 斯瓦希里语 + "am": "am-ET", // 阿姆哈拉语(埃塞俄比亚) + "zu": "zu-ZA", // 祖鲁语(南非) + "af": "af-ZA", // 南非荷兰语 + + // 其他语言 + "el": "el-GR", // 希腊语 + "eo": "eo", // 世界语 + "la": "la", // 拉丁语 + ] + + return languageMapping[iosLanguageCode] ?? iosLanguageCode + } +} \ No newline at end of file From cfcf2650288721aa5c3e649b8119bae8880027a6 Mon Sep 17 00:00:00 2001 From: lxm <1274844347@qq.com> Date: Fri, 1 Aug 2025 20:05:55 +0800 Subject: [PATCH 14/20] =?UTF-8?q?fix(settings):=20=E4=BB=85=E5=9C=A8Androi?= =?UTF-8?q?d=E5=B9=B3=E5=8F=B0=E6=98=BE=E7=A4=BA=E9=9A=90=E7=A7=81?= =?UTF-8?q?=E8=AE=BE=E7=BD=AE=E5=92=8C=E6=9D=83=E9=99=90=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E9=80=89=E9=A1=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修改设置页面,将隐私设置和权限管理选项包裹在平台判断条件中,确保这些功能只在Android平台显示 --- lib/modules/settings/views/settings_view.dart | 97 ++++++++++--------- 1 file changed, 51 insertions(+), 46 deletions(-) diff --git a/lib/modules/settings/views/settings_view.dart b/lib/modules/settings/views/settings_view.dart index 58d527fdb..784c905ed 100644 --- a/lib/modules/settings/views/settings_view.dart +++ b/lib/modules/settings/views/settings_view.dart @@ -407,8 +407,6 @@ class SettingsView extends GetView { ? Colors.white.withOpacity(0.1) : Colors.grey[200]), - - // 用户反馈 _buildSimpleNavigationSetting( title: 'feedback'.tr, // 用户反馈 @@ -427,29 +425,33 @@ class SettingsView extends GetView { }, isDarkMode: isDarkMode, ), - Divider( - height: 1, - color: isDarkMode - ? Colors.white.withOpacity(0.1) - : Colors.grey[200]), + if (Platform.isAndroid) ...[ + Divider( + height: 1, + color: isDarkMode + ? Colors.white.withOpacity(0.1) + : Colors.grey[200]), + + // 隐私设置 + _buildSimpleNavigationSetting( + title: 'privacySettings'.tr, // 隐私设置 + subtitle: 'privacySettingsInfo'.tr, // 管理数据收集和权限 + icon: Icons.shield_outlined, + iconBgColor: isDarkMode + ? Colors.purple[900]!.withOpacity(0.3) + : Colors.purple[100]!, + iconColor: isDarkMode + ? Colors.purple[300]! + : Colors.purple[600]!, + titleColor: isDarkMode ? Colors.white : null, + subtitleColor: isDarkMode ? Colors.white70 : null, + onTap: () { + AppSettings.openAppSettings(); + }, + isDarkMode: isDarkMode, + ) + ], - // 隐私设置 - _buildSimpleNavigationSetting( - title: 'privacySettings'.tr, // 隐私设置 - subtitle: 'privacySettingsInfo'.tr, // 管理数据收集和权限 - icon: Icons.shield_outlined, - iconBgColor: isDarkMode - ? Colors.purple[900]!.withOpacity(0.3) - : Colors.purple[100]!, - iconColor: - isDarkMode ? Colors.purple[300]! : Colors.purple[600]!, - titleColor: isDarkMode ? Colors.white : null, - subtitleColor: isDarkMode ? Colors.white70 : null, - onTap: () { - AppSettings.openAppSettings(); - }, - isDarkMode: isDarkMode, - ), Divider( height: 1, color: isDarkMode @@ -597,29 +599,32 @@ class SettingsView extends GetView { onTap: () => Get.toNamed(Routes.OTA_UPGRADE), isDarkMode: isDarkMode, ), - Divider( - height: 1, - color: isDarkMode - ? Colors.white.withOpacity(0.1) - : Colors.grey[200]), + if (Platform.isAndroid) ...[ + Divider( + height: 1, + color: isDarkMode + ? Colors.white.withOpacity(0.1) + : Colors.grey[200]), - // 添加权限管理设置项 - _buildSimpleNavigationSetting( - title: 'permissionManagement'.tr, // 权限管理 - subtitle: 'permissionManagementInfo'.tr, // 管理应用权限设置 - icon: Icons.security, - iconBgColor: isDarkMode - ? Colors.orange[900]!.withOpacity(0.3) - : Colors.orange[100]!, - iconColor: - isDarkMode ? Colors.orange[300]! : Colors.orange[600]!, - titleColor: isDarkMode ? Colors.white : null, - subtitleColor: isDarkMode ? Colors.white70 : null, - onTap: () { - Get.toNamed(Routes.permissions); - }, - isDarkMode: isDarkMode, - ), + // 添加权限管理设置项 + _buildSimpleNavigationSetting( + title: 'permissionManagement'.tr, // 权限管理 + subtitle: 'permissionManagementInfo'.tr, // 管理应用权限设置 + icon: Icons.security, + iconBgColor: isDarkMode + ? Colors.orange[900]!.withOpacity(0.3) + : Colors.orange[100]!, + iconColor: isDarkMode + ? Colors.orange[300]! + : Colors.orange[600]!, + titleColor: isDarkMode ? Colors.white : null, + subtitleColor: isDarkMode ? Colors.white70 : null, + onTap: () { + Get.toNamed(Routes.permissions); + }, + isDarkMode: isDarkMode, + ), + ] ], ), ), From cd533e700f2eea6cb3168e76c5a1dbe7c0efe89a Mon Sep 17 00:00:00 2001 From: tanlongsheng <252620078@qq.com> Date: Fri, 1 Aug 2025 20:06:53 +0800 Subject: [PATCH 15/20] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E6=89=93=E7=94=B5?= =?UTF-8?q?=E8=AF=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/data/services/novice_guide_service.dart | 2 +- lib/modules/agent/views/agent_view.dart | 26 ++++++++++----------- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/lib/data/services/novice_guide_service.dart b/lib/data/services/novice_guide_service.dart index 56ab6464b..060bb5c29 100644 --- a/lib/data/services/novice_guide_service.dart +++ b/lib/data/services/novice_guide_service.dart @@ -25,7 +25,7 @@ class NoviceGuideService extends GetxService { void startAIGuide() async { if (!isAIGuide) { - await Future.delayed(const Duration(milliseconds: 100)); + await Future.delayed(const Duration(milliseconds: 200)); isAIGuide = true; _storage.write('ai_guide', true); Intro.of(Get.context!).start(group: 'ai'); diff --git a/lib/modules/agent/views/agent_view.dart b/lib/modules/agent/views/agent_view.dart index ff5da53fd..d989cd037 100644 --- a/lib/modules/agent/views/agent_view.dart +++ b/lib/modules/agent/views/agent_view.dart @@ -934,21 +934,19 @@ class _AgentViewState extends State with WidgetsBindingObserver { Widget _buildPhoneCallOverlay(BuildContext context, bool isDarkMode) { return Container( color: Colors.transparent, // 设置整个覆盖层为透明 - child: SafeArea( - child: Column( - children: [ - // 中间空白区域 - 完全透明 - const Expanded( - child: SizedBox(), // 空的占位符,完全透明 - ), + child: Column( + children: [ + // 中间空白区域 - 完全透明 + const Expanded( + child: SizedBox(), // 空的占位符,完全透明 + ), - // 底部控制按钮 - 包含音频波纹 - Container( - color: isDarkMode ? Colors.black : Colors.white, - child: _buildPhoneCallControls(isDarkMode), - ), - ], - ), + // 底部控制按钮 - 包含音频波纹 + Container( + color: isDarkMode ? Colors.black : Colors.white, + child: _buildPhoneCallControls(isDarkMode), + ), + ], ), ); } From 904c6b7a15f0bcca1ae8b7a42e07103c53cd7b7c Mon Sep 17 00:00:00 2001 From: tanlongsheng <252620078@qq.com> Date: Fri, 1 Aug 2025 20:10:17 +0800 Subject: [PATCH 16/20] =?UTF-8?q?=E9=9A=90=E8=97=8F=E5=BC=95=E5=AF=BC?= =?UTF-8?q?=E9=A1=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/data/services/novice_guide_service.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/data/services/novice_guide_service.dart b/lib/data/services/novice_guide_service.dart index 060bb5c29..aeb3baa39 100644 --- a/lib/data/services/novice_guide_service.dart +++ b/lib/data/services/novice_guide_service.dart @@ -5,8 +5,8 @@ import 'package:get_storage/get_storage.dart'; class NoviceGuideService extends GetxService { final GetStorage _storage = GetStorage(); - bool isHomeGuide = false; - bool isAIGuide = false; + bool isHomeGuide = true; + bool isAIGuide = true; @override void onInit() { From 7e12292fb9ce572f1205e4fa61bafd777764c98e Mon Sep 17 00:00:00 2001 From: lxm <1274844347@qq.com> Date: Fri, 1 Aug 2025 20:26:38 +0800 Subject: [PATCH 17/20] =?UTF-8?q?feat(=E9=85=8D=E5=AF=B9):=20=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0iOS=E8=93=9D=E7=89=99=E5=BC=95=E5=AF=BC=E5=AF=B9?= =?UTF-8?q?=E8=AF=9D=E6=A1=86=E5=B9=B6=E6=94=AF=E6=8C=81=E5=9B=BD=E9=99=85?= =?UTF-8?q?=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 添加蓝牙引导对话框的国际化文本 使用flutter_screenutil进行响应式布局 移除未使用的导入 --- lib/core/translations/language/en_us.dart | 13 +++++ lib/core/translations/language/zh_cn.dart | 9 +++ .../controllers/pairing_controller.dart | 56 +++++++++++-------- 3 files changed, 55 insertions(+), 23 deletions(-) diff --git a/lib/core/translations/language/en_us.dart b/lib/core/translations/language/en_us.dart index f529e2a7a..51d9bd764 100644 --- a/lib/core/translations/language/en_us.dart +++ b/lib/core/translations/language/en_us.dart @@ -813,6 +813,19 @@ const Map enUS = { 'deviceAuthorizationFailedCheckCode': 'Device authorization failed, please check authorization code', + ///iOS蓝牙引导窗 + 'bluetoothGuideTitle': 'Manual Bluetooth Activation Required', // 需要手动开启蓝牙 + 'bluetoothGuideSteps': 'Please follow these steps:', // 请按照以下步骤操作: + 'bluetoothGuideStep1': + 'Enable Bluetooth: Settings → Bluetooth → Turn On', // 开启蓝牙:设置 → 蓝牙 → 开启 + 'bluetoothGuideStep2': + 'Connect Device: Find your device in "My Devices" and tap to connect', // 连接设备:在"我的设备"中找到您的设备并点击连接 + 'bluetoothGuideStep3': + 'Return to app to continue pairing process', // 返回应用继续配对流程 + 'bluetoothGuideTip': + 'Tip: You can also long press the Bluetooth icon in Control Center to quickly access Bluetooth settings', // 提示:也可以从控制中心长按蓝牙图标快速进入蓝牙设置 + 'iKnow': 'I Know', // 我知道了 + // 用户反馈页 "feedback": "Feedback", 'feedbackInfo': 'Provide feedback or suggestions', diff --git a/lib/core/translations/language/zh_cn.dart b/lib/core/translations/language/zh_cn.dart index 52a3cc093..6bd1701d6 100644 --- a/lib/core/translations/language/zh_cn.dart +++ b/lib/core/translations/language/zh_cn.dart @@ -746,6 +746,15 @@ const Map zhCN = { 'bleConnectionFailed': 'BLE连接失败', 'deviceAuthorizationFailedCheckCode': '设备授权失败,请检查授权码', + ///iOS蓝牙引导窗 + 'bluetoothGuideTitle': '需要手动开启蓝牙', // 需要手动开启蓝牙 + 'bluetoothGuideSteps': '请按照以下步骤操作:', // 请按照以下步骤操作: + 'bluetoothGuideStep1': '开启蓝牙:设置 → 蓝牙 → 开启', // 开启蓝牙:设置 → 蓝牙 → 开启 + 'bluetoothGuideStep2': '连接设备:在"我的设备"中找到您的设备并点击连接', // 连接设备:在"我的设备"中找到您的设备并点击连接 + 'bluetoothGuideStep3': '返回应用继续配对流程', // 返回应用继续配对流程 + 'bluetoothGuideTip': '提示:也可以从控制中心长按蓝牙图标快速进入蓝牙设置', // 提示:也可以从控制中心长按蓝牙图标快速进入蓝牙设置 + 'iKnow': '我知道了', // 我知道了 + //用户反馈页 "feedback": "用户反馈", 'feedbackInfo': '提供反馈或建议', diff --git a/lib/modules/pairing/controllers/pairing_controller.dart b/lib/modules/pairing/controllers/pairing_controller.dart index dedb8fbbb..c467e2776 100644 --- a/lib/modules/pairing/controllers/pairing_controller.dart +++ b/lib/modules/pairing/controllers/pairing_controller.dart @@ -1,12 +1,10 @@ -import 'dart:convert'; -import 'dart:typed_data'; - import 'package:app_settings/app_settings.dart'; import 'package:voitrans/data/models/appconfig_model.dart'; import '/data/models/appconfig.dart'; import '/data/models/user_Info.dart'; import '/data/services/network/api.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; import 'dart:async'; import 'package:permission_handler/permission_handler.dart'; @@ -16,7 +14,6 @@ import 'dart:io'; import '../../../data/services/bluetooth_manager.dart'; import '../../../data/services/ble_manager.dart'; import '../../../core/utils/permission_util.dart'; -import 'package:url_launcher/url_launcher.dart'; /// 配对状态枚举 - 简化版本 enum PairingState { @@ -1028,52 +1025,65 @@ class PairingController extends GetxController { void _showBluetoothGuideDialog() { Get.dialog( AlertDialog( - title: Text('需要手动开启蓝牙'), + title: Text('bluetoothGuideTitle'.tr), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('请按照以下步骤操作:'), - SizedBox(height: 16), + Text('bluetoothGuideSteps'.tr), + SizedBox(height: 16.h), Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('1. ', style: TextStyle(fontWeight: FontWeight.bold)), - Expanded(child: Text('开启蓝牙:设置 → 蓝牙 → 开启')), + Text('1. ', + style: TextStyle( + fontWeight: FontWeight.bold, fontSize: 14.sp)), + Expanded( + child: Text('bluetoothGuideStep1'.tr, + style: TextStyle(fontSize: 14.sp))), ], ), - SizedBox(height: 8), + SizedBox(height: 8.h), Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('2. ', style: TextStyle(fontWeight: FontWeight.bold)), - Expanded(child: Text('连接设备:在"我的设备"中找到您的设备并点击连接')), + Text('2. ', + style: TextStyle( + fontWeight: FontWeight.bold, fontSize: 14.sp)), + Expanded( + child: Text('bluetoothGuideStep2'.tr, + style: TextStyle(fontSize: 14.sp))), ], ), - SizedBox(height: 8), + SizedBox(height: 8.h), Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('3. ', style: TextStyle(fontWeight: FontWeight.bold)), - Expanded(child: Text('返回应用继续配对流程')), + Text('3. ', + style: TextStyle( + fontWeight: FontWeight.bold, fontSize: 14.sp)), + Expanded( + child: Text('bluetoothGuideStep3'.tr, + style: TextStyle(fontSize: 14.sp))), ], ), - SizedBox(height: 16), + SizedBox(height: 16.h), Container( - padding: EdgeInsets.all(12), + padding: EdgeInsets.all(12.w), decoration: BoxDecoration( color: Colors.blue.withOpacity(0.1), - borderRadius: BorderRadius.circular(8), + borderRadius: BorderRadius.circular(8.r), border: Border.all(color: Colors.blue.withOpacity(0.3)), ), child: Row( children: [ - Icon(Icons.info_outline, color: Colors.blue, size: 20), - SizedBox(width: 8), + Icon(Icons.info_outline, color: Colors.blue, size: 20.sp), + SizedBox(width: 8.w), Expanded( child: Text( - '提示:也可以从控制中心长按蓝牙图标快速进入蓝牙设置', - style: TextStyle(fontSize: 12, color: Colors.blue[700]), + 'bluetoothGuideTip'.tr, + style: + TextStyle(fontSize: 12.sp, color: Colors.blue[700]), ), ), ], @@ -1084,7 +1094,7 @@ class PairingController extends GetxController { actions: [ TextButton( onPressed: () => Get.back(), - child: Text('我知道了'), + child: Text('iKnow'.tr, style: TextStyle(fontSize: 14.sp)), ), ], ), From 77f4d3f656253364e42337f67c6af9390cd952a8 Mon Sep 17 00:00:00 2001 From: liwei1dao Date: Fri, 1 Aug 2025 20:27:10 +0800 Subject: [PATCH 18/20] =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E9=9F=B3=E9=87=8F?= =?UTF-8?q?=E8=B0=83=E8=8A=82=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../agent_service/AgentService.kt | 2 +- .../agent_service/AgentServiceImpl.swift | 4 +- .../agent_service/AudioSessionHub.swift | 4 +- .../chat_api/Sources/chat_api/MCPClient.swift | 77 +++++++++++++++++-- .../chat_api/SystemFunctionHandler.swift | 18 ++--- 5 files changed, 85 insertions(+), 20 deletions(-) diff --git a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt index 7a7f18b10..1fdaea5df 100644 --- a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt +++ b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt @@ -888,7 +888,7 @@ object AgentService : CoroutineScope { responseBuilder.append(token) if (speakResponse && reply && broadcast) { ttsService?.speakStream(token) - audioPlayer?.stopAudio() + // audioPlayer?.stopAudio() } if (reply && token.isNotEmpty()) { // 发送流式回复token diff --git a/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift b/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift index 7e54e2c4e..bfac028cd 100644 --- a/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift +++ b/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift @@ -996,7 +996,7 @@ class ChatApiStreamCallback: StreamCallback { if speakResponse && reply && broadcast{ try agentService.azureTtsHelper?.speakStream(token) - try agentService.audioPlayer?.stopAwaitSound() +// try agentService.audioPlayer?.stopAwaitSound() } if (reply && token != "") { try agentService.sendEvent(name: "assistant_token", data: ["token": token]) @@ -1142,7 +1142,7 @@ extension AgentServiceImpl: TtsEventListener { isSpeaking = true restartIdleCheck() sendEvent(name: "tts_started", data: ["status": "started"]) - + audioPlayer?.stopAwaitSound() case .synthesisCompleted: isSpeaking = false restartIdleCheck() diff --git a/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AudioSessionHub.swift b/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AudioSessionHub.swift index b549e5f1a..ea6aedcda 100644 --- a/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AudioSessionHub.swift +++ b/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AudioSessionHub.swift @@ -583,9 +583,9 @@ public class AudioSessionHub { switch action { case .increase: - newValue = min(currentValue + 0.1, 1.0) + newValue = min(currentValue + 0.2, 1.0) // 从 0.1 改为 0.2 (20%) case .decrease: - newValue = max(currentValue - 0.1, 0.0) + newValue = max(currentValue - 0.2, 0.0) // 从 0.1 改为 0.2 (20%) case .mute: newValue = 0.0 case .setMax: diff --git a/local_plugins/chat_api/ios/chat_api/Sources/chat_api/MCPClient.swift b/local_plugins/chat_api/ios/chat_api/Sources/chat_api/MCPClient.swift index 3f0caeab3..b65a7aaa4 100644 --- a/local_plugins/chat_api/ios/chat_api/Sources/chat_api/MCPClient.swift +++ b/local_plugins/chat_api/ios/chat_api/Sources/chat_api/MCPClient.swift @@ -28,6 +28,7 @@ public protocol FunctionHandler { /// - 安全的JSON-RPC消息解析,包含错误修复逻辑 /// - 完整的SSE连接生命周期管理 /// - 与Android端CustomSseClientTransport功能对等 +// 在 MCPSubClient 类中添加工具调用超时和重试配置 public class MCPSubClient { private let serverId: String private let serverUrl: String @@ -47,6 +48,10 @@ public class MCPSubClient { private var retryCount = 0 private var reconnectTask: Task? + // 工具调用配置 - 添加这两行 + private let maxToolCallRetries = 3 + private let toolCallTimeout: TimeInterval = 30.0 + // 连接状态锁 private let connectionLock = NSLock() @@ -178,27 +183,55 @@ public class MCPSubClient { /// 调用工具前检查连接 public func callTool(name: String, arguments: [String: Any]) async -> [String: Any]? { + return await callToolWithRetry(name: name, arguments: arguments, retryCount: 0) + } + + private func callToolWithRetry(name: String, arguments: [String: Any], retryCount: Int) async -> [String: Any]? { + print("[MCPSubClient] 工具调用尝试 \(retryCount + 1)/\(maxToolCallRetries + 1): \(name)") + // 先检查连接状态 if !(await checkConnection()) { + if retryCount < maxToolCallRetries { + print("[MCPSubClient] 连接断开,尝试重连...") + if await connect() { + print("[MCPSubClient] 重连成功,重试工具调用") + return await callToolWithRetry(name: name, arguments: arguments, retryCount: retryCount + 1) + } + } return [ "content": [[ "type": "text", - "text": "Tool call failed: mcp: service not connected" + "text": "工具调用失败,请重试:MCP 服务连接中断" ]], "isError": true ] } guard isConnectedFlag, let client = mcpClient else { - return createErrorResult("Not connected to MCP server") + if retryCount < maxToolCallRetries { + print("[MCPSubClient] MCP 客户端未连接,尝试重连...") + if await connect() { + return await callToolWithRetry(name: name, arguments: arguments, retryCount: retryCount + 1) + } + } + return createErrorResult("工具调用失败,请重试:MCP 服务未连接") } do { - let mcpArguments = convertToMCPValue(arguments) - let (content, isError) = try await client.callTool(name: name, arguments: mcpArguments) + // 使用超时控制的工具调用 + let result = try await withTimeout(seconds: toolCallTimeout) { + let mcpArguments = self.convertToMCPValue(arguments) + return try await client.callTool(name: name, arguments: mcpArguments) + } + + let (content, isError) = result if isError == true { - return createErrorResult("Tool execution failed") + if retryCount < maxToolCallRetries { + print("[MCPSubClient] 工具执行失败,重试中...") + return await callToolWithRetry(name: name, arguments: arguments, retryCount: retryCount + 1) + } + return createErrorResult("工具调用失败,请重试:工具执行错误") } var contextText = "" @@ -225,13 +258,45 @@ public class MCPSubClient { } } + print("[MCPSubClient] 工具调用成功: \(name)") return [ "content": [["type": "text", "text": contextText.trimmingCharacters(in: .whitespacesAndNewlines)]], "isError": false ] + } catch is TimeoutError { + print("[MCPSubClient] 工具调用超时: \(name)") + if retryCount < maxToolCallRetries { + print("[MCPSubClient] 超时重试中...") + return await callToolWithRetry(name: name, arguments: arguments, retryCount: retryCount + 1) + } + return createErrorResult("工具调用失败,请重试:连接超时") } catch { - return createErrorResult("Tool call failed: \(error.localizedDescription)") + print("[MCPSubClient] 工具调用异常: \(name), 错误: \(error.localizedDescription)") + + // 检查是否是连接相关错误 + let errorDescription = error.localizedDescription.lowercased() + if errorDescription.contains("connection") || + errorDescription.contains("network") || + errorDescription.contains("timeout") || + errorDescription.contains("disconnected") { + + if retryCount < maxToolCallRetries { + print("[MCPSubClient] 检测到连接错误,尝试重连并重试...") + // 标记连接断开 + isConnectedFlag = false + + // 尝试重连 + if await connect() { + return await callToolWithRetry(name: name, arguments: arguments, retryCount: retryCount + 1) + } + } + } else if retryCount < maxToolCallRetries { + print("[MCPSubClient] 其他错误,直接重试...") + return await callToolWithRetry(name: name, arguments: arguments, retryCount: retryCount + 1) + } + + return createErrorResult("工具调用失败,请重试:\(error.localizedDescription)") } } diff --git a/local_plugins/chat_api/ios/chat_api/Sources/chat_api/SystemFunctionHandler.swift b/local_plugins/chat_api/ios/chat_api/Sources/chat_api/SystemFunctionHandler.swift index 98773d41f..bcacf4eb5 100644 --- a/local_plugins/chat_api/ios/chat_api/Sources/chat_api/SystemFunctionHandler.swift +++ b/local_plugins/chat_api/ios/chat_api/Sources/chat_api/SystemFunctionHandler.swift @@ -795,7 +795,7 @@ private class GetCurrentTimeHandler: FunctionHandler { private class MediaPlayHandler: FunctionHandler { func handle(arguments: [String: Any]) async throws -> String { NotificationCenter.default.post(name: NSNotification.Name("MediaPlay"), object: nil) - return "{\"result\": \"已开始播放媒体\"}" + return "{\"notreply\": true,\"result\": \"已开始播放媒体\"}" } } @@ -803,7 +803,7 @@ private class MediaPlayHandler: FunctionHandler { private class MediaPauseHandler: FunctionHandler { func handle(arguments: [String: Any]) async throws -> String { NotificationCenter.default.post(name: NSNotification.Name("MediaPause"), object: nil) - return "{\"result\": \"已暂停媒体播放\"}" + return "{\"broadcast\": true,\"result\": \"已暂停媒体播放\"}" } } @@ -811,7 +811,7 @@ private class MediaPauseHandler: FunctionHandler { private class MediaPreviousHandler: FunctionHandler { func handle(arguments: [String: Any]) async throws -> String { NotificationCenter.default.post(name: NSNotification.Name("MediaPrevious"), object: nil) - return "{\"result\": \"已切换到上一首\"}" + return "{\"notreply\": true,\"result\": \"已切换到上一首\"}" } } @@ -819,7 +819,7 @@ private class MediaPreviousHandler: FunctionHandler { private class MediaNextHandler: FunctionHandler { func handle(arguments: [String: Any]) async throws -> String { NotificationCenter.default.post(name: NSNotification.Name("MediaNext"), object: nil) - return "{\"result\": \"已切换到下一首\"}" + return "{\"notreply\": true,\"result\": \"已切换到下一首\"}" } } @@ -833,7 +833,7 @@ private class OpenRecorderHandler: FunctionHandler { } } - return "{\"result\": \"无法打开录音机应用\"}" + return "{\"notreply\": true,\"result\": \"无法打开录音机应用\"}" } } @@ -905,7 +905,7 @@ private class AddVolumeHandler: FunctionHandler { object: volumeInfo ) - return "{\"result\": \"音量增加请求已发送\"}" + return "{\"broadcast\": false,\"result\": \"音量增加请求已发送\"}" } } @@ -921,7 +921,7 @@ private class ReduceVolumeHandler: FunctionHandler { object: volumeInfo ) - return "{\"result\": \"音量降低请求已发送\"}" + return "{\"broadcast\": false,\"result\": \"音量降低请求已发送\"}" } } @@ -937,7 +937,7 @@ private class MuteVolumeHandler: FunctionHandler { object: volumeInfo ) - return "{\"result\": \"静音请求已发送\"}" + return "{\"broadcast\": false,\"result\": \"静音请求已发送\"}" } } @@ -953,6 +953,6 @@ private class MaxVolumeHandler: FunctionHandler { object: volumeInfo ) - return "{\"result\": \"最大音量请求已发送\"}" + return "{\"broadcast\": false,\"result\": \"最大音量请求已发送\"}" } } From 0c1f20d9e77f0f99cf938e058f75ed010e1b6cd3 Mon Sep 17 00:00:00 2001 From: liwei1dao Date: Fri, 1 Aug 2025 22:00:24 +0800 Subject: [PATCH 19/20] =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E5=88=87=E6=8D=A2?= =?UTF-8?q?=E5=90=8E=E5=8F=B0=E6=A3=80=E6=B5=8B=E9=80=BB=E8=BE=91=E5=87=BA?= =?UTF-8?q?=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controllers/pairing_controller.dart | 114 +++++++++++++++++- 1 file changed, 113 insertions(+), 1 deletion(-) diff --git a/lib/modules/pairing/controllers/pairing_controller.dart b/lib/modules/pairing/controllers/pairing_controller.dart index c467e2776..f2783f4bb 100644 --- a/lib/modules/pairing/controllers/pairing_controller.dart +++ b/lib/modules/pairing/controllers/pairing_controller.dart @@ -44,7 +44,7 @@ enum ErrorType { } /// 配对控制器 - 基于简化状态机的实现 -class PairingController extends GetxController { +class PairingController extends GetxController with WidgetsBindingObserver { static const String _tag = 'PairingCtrl'; static PairingController get to => Get.find(); @@ -100,6 +100,11 @@ class PairingController extends GetxController { super.onInit(); Logger.i(_tag, '配对控制器初始化'); + // 注册应用生命周期监听器 + Logger.i(_tag, '正在注册应用生命周期监听器'); + WidgetsBinding.instance.addObserver(this); + Logger.i(_tag, '应用生命周期监听器注册完成'); + // 确保服务已初始化 _bluetoothManager = Get.find(); _bleManager = Get.find(); @@ -123,6 +128,10 @@ class PairingController extends GetxController { @override void onClose() { Logger.i(_tag, '配对控制器销毁'); + + // 移除应用生命周期监听器 + WidgetsBinding.instance.removeObserver(this); + // 清理所有Worker for (var worker in _workers) { worker.dispose(); @@ -135,6 +144,109 @@ class PairingController extends GetxController { super.onClose(); } + /// 应用生命周期状态变化回调 + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + super.didChangeAppLifecycleState(state); + // 使用多种日志级别确保输出 + Logger.i(_tag, '=== 应用生命周期状态变化: $state ==='); + print('PairingCtrl: 应用生命周期状态变化: $state'); // 直接使用print确保输出 + + switch (state) { + case AppLifecycleState.resumed: + // 应用从后台切回前台 + Logger.i(_tag, '=== 应用从后台切回前台 ==='); + print('PairingCtrl: 应用从后台切回前台'); + _onAppResumed(); + break; + case AppLifecycleState.paused: + // 应用切到后台 + Logger.i(_tag, '应用切到后台'); + print('PairingCtrl: 应用切到后台'); + _onAppPaused(); + break; + case AppLifecycleState.inactive: + // 应用处于非活跃状态(如来电话时) + Logger.i(_tag, '应用处于非活跃状态'); + print('PairingCtrl: 应用处于非活跃状态'); + break; + case AppLifecycleState.detached: + // 应用即将被销毁 + Logger.i(_tag, '应用即将被销毁'); + print('PairingCtrl: 应用即将被销毁'); + break; + case AppLifecycleState.hidden: + // 应用被隐藏(iOS 13+) + Logger.i(_tag, '应用被隐藏'); + print('PairingCtrl: 应用被隐藏'); + break; + } + } + + /// 应用从后台切回前台时的处理逻辑 + void _onAppResumed() { + Logger.i(_tag, '处理应用从后台切回前台事件'); + + // 刷新蓝牙连接状态 + _bluetoothManager.refreshDeviceConnection(); + + // 根据当前配对状态执行相应操作 + switch (_currentState.value) { + case PairingState.waitingClassic: + // 如果在等待经典蓝牙连接,重新检查连接状态 + Logger.i(_tag, '重新检查经典蓝牙连接状态'); + _checkCurrentStatus(); + break; + case PairingState.waitingBle: + // 如果在等待BLE连接,重新检查BLE状态 + Logger.i(_tag, '重新检查BLE连接状态'); + _checkBleConnectionAfterResume(); + break; + case PairingState.error: + // 如果在错误状态,根据错误类型决定是否自动重试 + if (_errorType.value == ErrorType.bluetoothOff) { + Logger.i(_tag, '检查蓝牙是否已重新开启'); + _checkCurrentStatus(); + } + break; + default: + // 其他状态暂不处理 + break; + } + } + + /// 应用切到后台时的处理逻辑 + void _onAppPaused() { + Logger.i(_tag, '处理应用切到后台事件'); + + // 可以在这里暂停一些不必要的操作,如蓝牙扫描等 + if (_currentState.value == PairingState.associating) { + Logger.i(_tag, '应用切到后台,暂停BLE扫描'); + _bleManager.stopScan(); + } + } + + /// 应用恢复后检查BLE连接状态 + Future _checkBleConnectionAfterResume() async { + try { + final currentBleState = await _bleManager.getConnectionState(); + Logger.i(_tag, '应用恢复后BLE连接状态: $currentBleState'); + + if (currentBleState == 2) { + // BLE已连接,直接完成配对 + Logger.i(_tag, '检测到BLE已连接,完成配对'); + _bleTimeoutTimer?.cancel(); + _transition(PairingState.completed); + } else { + // BLE未连接,重新启动连接流程 + Logger.i(_tag, '重新启动BLE连接流程'); + _startBleConnection(); + } + } catch (e) { + Logger.e(_tag, '检查BLE连接状态失败: $e'); + } + } + // ==================== 监听器设置 ==================== /// 集中设置所有监听器 From dad39ede02c6912443eb3984528c8ac2d507c89b Mon Sep 17 00:00:00 2001 From: liwei1dao Date: Fri, 1 Aug 2025 22:02:01 +0800 Subject: [PATCH 20/20] =?UTF-8?q?=E7=A7=BB=E9=99=A4=E7=94=B5=E8=AF=9D?= =?UTF-8?q?=E6=9D=83=E9=99=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ios/Runner/Info.plist | 4 ---- 1 file changed, 4 deletions(-) diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index f3ad0d225..75a82d84d 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -121,10 +121,6 @@ audio location - UIRequiredDeviceCapabilities - - telephony - UIDeviceFamily 1