diff --git a/lib/core/widgets/intro_overlay_builder.dart b/lib/core/widgets/intro_overlay_builder.dart new file mode 100644 index 000000000..aab3dae94 --- /dev/null +++ b/lib/core/widgets/intro_overlay_builder.dart @@ -0,0 +1,72 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_intro/flutter_intro.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; + +class IntroOverlayBuilder { + static Widget aiBuilder( + StepWidgetParams params, + String text, { + String buttonText = '下一步', + bool alignment = true, + Function? onNext, + Function? onFinish, + }) { + return Column( + crossAxisAlignment: + alignment ? CrossAxisAlignment.start : CrossAxisAlignment.end, + children: [ + Padding( + padding: EdgeInsets.only(bottom: 10.w), + child: Text( + text, + style: TextStyle( + fontSize: 15.sp, + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + ), + Row( + children: [ + if (onNext != null) + _button( + buttonText, + () { + params.onNext!(); + onNext(); + }, + ), + if (onFinish != null) + _button( + buttonText, + () { + params.onFinish(); + onFinish(); + }, + ), + ], + ), + ], + ); + } + + static Widget _button(String text, Function() onTap) { + return GestureDetector( + onTap: onTap, + child: Container( + padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 5.w), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20.r), + border: Border.all(color: Colors.white), + ), + child: Text( + text, + style: TextStyle( + fontSize: 14.sp, + color: Colors.white, + ), + ), + ), + ); + } +} diff --git a/lib/core/widgets/select_location_map.dart b/lib/core/widgets/select_location_map.dart deleted file mode 100644 index c783901ed..000000000 --- a/lib/core/widgets/select_location_map.dart +++ /dev/null @@ -1,15 +0,0 @@ -import 'package:flutter/material.dart'; - -class SelectLocationMap extends StatefulWidget { - const SelectLocationMap({super.key}); - - @override - State createState() => _SelectLocationMapState(); -} - -class _SelectLocationMapState extends State { - @override - Widget build(BuildContext context) { - return const Placeholder(); - } -} diff --git a/lib/data/models/appconfig.dart b/lib/data/models/appconfig.dart index 19dcf0565..22a09d84a 100644 --- a/lib/data/models/appconfig.dart +++ b/lib/data/models/appconfig.dart @@ -4,9 +4,20 @@ import '../../../data/models/appconfig_model.dart'; class AppConfig { static late UserGetAppConfigResp config; + + // 新增一个私有的静态布尔值来跟踪初始化状态 + static bool _isInitialized = false; + + /// 检查AppConfig单例是否已经被初始化 + /// 外部通过调用此方法来安全地检查状态 + static bool isInitialized() { + return _isInitialized; + } + // 可选:提供初始化方法 static void initialize(UserGetAppConfigResp resp) { config = resp; + _isInitialized = true; // <-- 新增此行 } //环境变量 diff --git a/lib/data/models/user_Info.dart b/lib/data/models/user_Info.dart index e5efa62ec..d5d369ba6 100644 --- a/lib/data/models/user_Info.dart +++ b/lib/data/models/user_Info.dart @@ -1,9 +1,15 @@ -import 'dart:ffi'; +import 'package:get_storage/get_storage.dart'; +import '../../../core/utils/logger.dart'; class User { // 单例实例 static User? _instance; + /// 安全地检查用户是否已经登录(即单例是否已初始化) + static bool isLoggedIn() { + return _instance != null; + } + // 单例访问器(增加空安全保护) static User get instance { if (_instance == null) { @@ -12,6 +18,18 @@ class User { return _instance!; } + // --- 新增 --- + /// 清理所有与用户会话相关的数据 + static Future clearUserSession() async { + final GetStorage storage = GetStorage(); + // 1. 清理本地存储 + await storage.remove('logintoken'); + await storage.remove('user_info'); + // 2. 清理内存中的单例 + _instance = null; + Logger.i("User", "用户会话数据已安全清理。"); + } + // 最终属性(不可变) String name; String uid; @@ -72,7 +90,7 @@ class User { // 修复1:安全获取嵌套的 user 字段 final userData = json['user'] as Map?; // 允许空值 final devicesData = json['devices'] as List?; // 允许空值 - print("数据获取fronTokenJson${userData} ${devicesData}"); + print("数据获取fronTokenJson$userData $devicesData"); // 更新单例实例 return _instance = User._( token: '', diff --git a/lib/data/services/novice_guide_service.dart b/lib/data/services/novice_guide_service.dart index aeb3baa39..12faf1a2c 100644 --- a/lib/data/services/novice_guide_service.dart +++ b/lib/data/services/novice_guide_service.dart @@ -8,6 +8,47 @@ class NoviceGuideService extends GetxService { bool isHomeGuide = true; bool isAIGuide = true; + int aiStep = 0; + + List aiStepsList = [ + { + 'start': '对小言说:“今天天气怎么样”', + 'input': '今天天气怎么样', + 'send': '小言会为你实时为你播报天气', + 'end': '小言能实时为你播报天气,试试其他指令吧', + }, + { + 'start': '对小言说:“请播放一组摇滚乐”', + 'input': '请播放一组摇滚乐', + 'send': '小言会为你搜寻到一组音乐', + 'end': '小言已成功为您搜寻到一组音乐,试试其他指令吧', + }, + { + 'start': '对小言说:“导航到深圳机场”', + 'input': '导航到深圳机场', + 'send': '小言会为你规划路线', + 'end': '小言已成功为您导航到深圳机场,试试其他指令吧', + }, + { + 'start': '对小言说:“明天下午3点创建会议提醒”', + 'input': '明天下午3点创建会议提醒', + 'send': '小言会为你创建会议提醒', + 'end': '小言已成功为您创建会议提醒,试试其他指令吧', + }, + { + 'start': '对小言说:“拨打妈妈的电话”', + 'input': '拨打妈妈的电话', + 'send': '小言会为你拨打电话', + 'end': '小言已成功为您拨打电话,试试其他指令吧', + }, + { + 'start': '对小言说:“给爸爸发短信说晚上回家吃饭’”', + 'input': '给爸爸发短信说晚上回家吃饭’', + 'send': '小言会为你发送短信', + 'end': '短信已发送,语音操控更便捷', + }, + ]; + @override void onInit() { super.onInit(); @@ -15,8 +56,9 @@ class NoviceGuideService extends GetxService { // isAIGuide = _storage.read('ai_guide') ?? false; } - void startHomeGuide() { + void startHomeGuide() async { if (!isHomeGuide) { + await Future.delayed(const Duration(milliseconds: 200)); isHomeGuide = true; _storage.write('home_guide', true); Intro.of(Get.context!).start(group: 'home'); diff --git a/lib/data/services/version_update_service.dart b/lib/data/services/version_update_service.dart index f2696b5b8..f624ae7aa 100644 --- a/lib/data/services/version_update_service.dart +++ b/lib/data/services/version_update_service.dart @@ -270,29 +270,29 @@ class VersionUpdateService extends GetxService { return false; } - final platformInfo = await getCurrentPlatformInfo(); + //final platformInfo = await getCurrentPlatformInfo(); try { - switch (platformInfo.storeType) { - case AppStoreType.appStore: - return await _openAppStore(); - case AppStoreType.googlePlay: - return await _openGooglePlay(); - case AppStoreType.huawei: - return await _openHuaweiStore(); - case AppStoreType.xiaomi: - return await _openXiaomiStore(); - case AppStoreType.oppo: - return await _openOppoStore(); - case AppStoreType.vivo: - return await _openVivoStore(); - case AppStoreType.honor: - return await _openHonorStore(); - case AppStoreType.official: - return await _openOfficialDownload(updateInfo.appInfo!.address!); - default: - return await _openOfficialDownload(updateInfo.appInfo!.address!); - } + // switch (platformInfo.storeType) { + // case AppStoreType.appStore: + // return await _openAppStore(); + // case AppStoreType.googlePlay: + // return await _openGooglePlay(); + // case AppStoreType.huawei: + // return await _openHuaweiStore(); + // case AppStoreType.xiaomi: + // return await _openXiaomiStore(); + // case AppStoreType.oppo: + // return await _openOppoStore(); + // case AppStoreType.vivo: + // return await _openVivoStore(); + // case AppStoreType.honor: + // return await _openHonorStore(); + // case AppStoreType.official: + // return await _openOfficialDownload(updateInfo.appInfo!.address!); + // default: + return await _openOfficialDownload(updateInfo.appInfo!.address!); + // } } catch (e) { _logger.e('执行更新失败: $e'); return false; diff --git a/lib/modules/agent/views/agent_view.dart b/lib/modules/agent/views/agent_view.dart index d989cd037..849baa4cc 100644 --- a/lib/modules/agent/views/agent_view.dart +++ b/lib/modules/agent/views/agent_view.dart @@ -4,6 +4,7 @@ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter_intro/flutter_intro.dart'; import 'package:get/get.dart'; +import '../../../core/widgets/intro_overlay_builder.dart'; import '../controllers/agent_controller.dart'; import '../controllers/message_action_controller.dart'; import 'message_bubble.dart'; @@ -197,47 +198,79 @@ class _AgentViewState extends State with WidgetsBindingObserver { children: [ // 聊天内容 Expanded( - child: Obx( - () => Align( - alignment: Alignment.topCenter, - child: ListView.builder( - controller: controller.scrollController, - shrinkWrap: true, - reverse: true, - padding: EdgeInsets.symmetric( - vertical: 16.h, horizontal: 16.w), - itemCount: controller.messages.length + - (controller.hasMoreMessages.value ? 1 : 0), - itemBuilder: (context, index) { - // 如果是最后一项且有更多消息,显示加载指示器 - if (index == controller.messages.length && - controller.hasMoreMessages.value) { - return _buildLoadMoreIndicator(); - } - - final message = controller - .messages[controller.messages.length - 1 - index]; - return Padding( - padding: EdgeInsets.only(bottom: 8.h), - child: GestureDetector( - onLongPressStart: - (LongPressStartDetails details) { - messageActionController.onLongPressMessage( - context, isDarkMode, details, message); - }, - child: MessageBubble( - isUser: message.isUser, - message: message.text, - timestamp: message.timestamp, - isRecognizing: message.isRecognizing, - hasImage: message.hasImage, - imagePath: message.imagePath, - meta: message.meta, - ), - )); + child: Stack( + children: [ + IntroStepBuilder( + group: 'ai', + order: 3, + overlayBuilder: (params) { + return IntroOverlayBuilder.aiBuilder( + params, + controller.noviceGuideService.aiStepsList[ + controller.noviceGuideService.aiStep]['end'], + onFinish: () async { + if (controller.noviceGuideService.aiStep < 5) { + await Future.delayed( + const Duration(milliseconds: 300)); + controller.noviceGuideService.aiStep++; + Intro.of(Get.context!) + .start(group: 'ai', reset: true); + } + }, + ); }, + builder: (BuildContext context, GlobalKey key) => Align( + alignment: Alignment.bottomCenter, + child: SizedBox( + key: key, + width: double.infinity, + height: 430.w, + ), + ), ), - ), + Obx( + () => Align( + alignment: Alignment.topCenter, + child: ListView.builder( + controller: controller.scrollController, + shrinkWrap: true, + reverse: true, + padding: EdgeInsets.symmetric( + vertical: 16.h, horizontal: 16.w), + itemCount: controller.messages.length + + (controller.hasMoreMessages.value ? 1 : 0), + itemBuilder: (context, index) { + // 如果是最后一项且有更多消息,显示加载指示器 + if (index == controller.messages.length && + controller.hasMoreMessages.value) { + return _buildLoadMoreIndicator(); + } + + final message = controller.messages[ + controller.messages.length - 1 - index]; + return Padding( + padding: EdgeInsets.only(bottom: 8.h), + child: GestureDetector( + onLongPressStart: + (LongPressStartDetails details) { + messageActionController.onLongPressMessage( + context, isDarkMode, details, message); + }, + child: MessageBubble( + isUser: message.isUser, + message: message.text, + timestamp: message.timestamp, + isRecognizing: message.isRecognizing, + hasImage: message.hasImage, + imagePath: message.imagePath, + meta: message.meta, + ), + )); + }, + ), + ), + ), + ], ), ), @@ -392,7 +425,24 @@ class _AgentViewState extends State with WidgetsBindingObserver { return IntroStepBuilder( group: 'ai', order: 1, - text: '对小言说“今天天气怎么样”', + overlayBuilder: (params) { + return IntroOverlayBuilder.aiBuilder( + params, + controller.noviceGuideService + .aiStepsList[ + controller.noviceGuideService + .aiStep]['start'], + buttonText: '确定输入', + onNext: () { + controller.textController.text = + controller.noviceGuideService + .aiStepsList[ + controller + .noviceGuideService + .aiStep]['input']; + }, + ); + }, onWidgetLoad: () { controller.noviceGuideService .startAIGuide(); @@ -630,111 +680,143 @@ class _AgentViewState extends State with WidgetsBindingObserver { SizedBox(width: 8.w), // 动态按钮区域:展开按钮 + 发送按钮 或 图片选择按钮 - Obx(() { - final hasText = controller.hasTextInput.value; - final hasImage = - controller.isImageInputActive.value; - final shouldShowSend = hasText || hasImage; - final shouldShowExpand = - controller.shouldShowExpandButton.value; + Builder(builder: (context) { + StepWidgetParams? widgetParams; + return IntroStepBuilder( + group: 'ai', + order: 2, + onHighlightWidgetTap: () { + widgetParams!.onNext!(); + controller.sendMessage(); + }, + overlayBuilder: (params) { + widgetParams = params; + return IntroOverlayBuilder.aiBuilder( + params, + '点击发送,${controller.noviceGuideService.aiStepsList[controller.noviceGuideService.aiStep]['send']}', + alignment: false, + ); + }, + builder: + (BuildContext context, GlobalKey key) => + Obx( + key: key, + () { + final hasText = + controller.hasTextInput.value; + final hasImage = + controller.isImageInputActive.value; + final shouldShowSend = + hasText || hasImage; + final shouldShowExpand = controller + .shouldShowExpandButton.value; - if (shouldShowSend) { - // 显示发送按钮(可能还有展开按钮) - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - // 展开按钮(在发送按钮上方) - if (shouldShowExpand && hasText) - Padding( - padding: - EdgeInsets.only(bottom: 40.h), - child: GestureDetector( + if (shouldShowSend) { + // 显示发送按钮(可能还有展开按钮) + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + // 展开按钮(在发送按钮上方) + if (shouldShowExpand && hasText) + Padding( + padding: EdgeInsets.only( + bottom: 40.h), + child: GestureDetector( + onTap: () { + // 打开底部弹出编辑模式 + controller + .openFullScreenEdit(); + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: + Colors.transparent, + builder: (context) => + const FullScreenEditor(), + ).then((_) { + // 弹出页关闭时的回调 + controller + .closeFullScreenEdit(); + }); + }, + child: Container( + width: 32.w, + height: 32.w, + decoration: BoxDecoration( + color: isDarkMode + ? Colors.grey[700] + : Colors.grey[300], + borderRadius: + BorderRadius.circular( + 16.w), + ), + child: Icon( + Icons.expand_less, + color: isDarkMode + ? Colors.grey[300] + : Colors.grey[700], + size: 20.sp, + ), + ), + ), + ), + // 发送按钮 + GestureDetector( + onTap: () => + controller.sendMessage(), + child: Container( + width: 44.w, + height: 44.w, + decoration: BoxDecoration( + color: primaryColor, + borderRadius: + BorderRadius.circular( + 22.w), + ), + child: Icon( + Icons.send, + color: Colors.white, + size: 22.sp, + ), + ), + ), + ], + ); + } else { + // 显示'+'选择按钮 + return Obx(() { + final isExpanded = controller + .isPlusOptionsExpanded.value; + return GestureDetector( onTap: () { - // 打开底部弹出编辑模式 - controller.openFullScreenEdit(); - showModalBottomSheet( - context: context, - isScrollControlled: true, - backgroundColor: - Colors.transparent, - builder: (context) => - const FullScreenEditor(), - ).then((_) { - // 弹出页关闭时的回调 - controller - .closeFullScreenEdit(); - }); + controller.togglePlusOptions(); }, child: Container( - width: 32.w, - height: 32.w, + width: 44.w, + height: 44.w, decoration: BoxDecoration( color: isDarkMode - ? Colors.grey[700] - : Colors.grey[300], + ? Colors.grey[800] + : Colors.grey[200], borderRadius: - BorderRadius.circular(16.w), + BorderRadius.circular(22.w), ), child: Icon( - Icons.expand_less, + isExpanded + ? Icons.close + : Icons.add, color: isDarkMode ? Colors.grey[300] : Colors.grey[700], - size: 20.sp, + size: 22.sp, ), ), - ), - ), - // 发送按钮 - GestureDetector( - onTap: () => controller.sendMessage(), - child: Container( - width: 44.w, - height: 44.w, - decoration: BoxDecoration( - color: primaryColor, - borderRadius: - BorderRadius.circular(22.w), - ), - child: Icon( - Icons.send, - color: Colors.white, - size: 22.sp, - ), - ), - ), - ], - ); - } else { - // 显示'+'选择按钮 - return Obx(() { - final isExpanded = - controller.isPlusOptionsExpanded.value; - return GestureDetector( - onTap: () { - controller.togglePlusOptions(); - }, - child: Container( - width: 44.w, - height: 44.w, - decoration: BoxDecoration( - color: isDarkMode - ? Colors.grey[800] - : Colors.grey[200], - borderRadius: - BorderRadius.circular(22.w), - ), - child: Icon( - isExpanded ? Icons.close : Icons.add, - color: isDarkMode - ? Colors.grey[300] - : Colors.grey[700], - size: 22.sp, - ), - ), - ); - }); - } + ); + }); + } + }, + ), + ); }), ], ), diff --git a/lib/modules/home/views/home_view.dart b/lib/modules/home/views/home_view.dart index f842b6a23..b332f5941 100644 --- a/lib/modules/home/views/home_view.dart +++ b/lib/modules/home/views/home_view.dart @@ -2,6 +2,7 @@ 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 '../../../core/widgets/intro_overlay_builder.dart'; import '../controllers/home_controller.dart'; import '../../../routes/app_routes.dart'; import '../../../core/widgets/common_bottom_nav.dart'; @@ -84,31 +85,46 @@ class HomeView extends GetView { _buildAppBar(isDarkMode), // 顶部图片 + _buildEarbudsImage(), + + SizedBox(height: 20.h), + + // 设备状态卡片 IntroStepBuilder( group: 'home', order: 1, - text: '欢迎使用Voitrans!首先,让我们唤醒你的专属 AI—— 小言', onWidgetLoad: () { controller.noviceGuideService.startHomeGuide(); }, + overlayBuilder: (params) { + return IntroOverlayBuilder.aiBuilder( + params, + '欢迎使用Voitrans!首先,让我们唤醒你的专属 AI—— 小言', + onNext: () {}, + ); + }, builder: (BuildContext context, GlobalKey key) => SizedBox( key: key, - child: _buildEarbudsImage(), + child: _buildDeviceStatusCard(isDarkMode), ), ), + SizedBox(height: 20.h), - // 设备状态卡片 IntroStepBuilder( group: 'home', order: 2, - text: '对着麦克风说“小言小言”', + overlayBuilder: (params) { + return IntroOverlayBuilder.aiBuilder( + params, + '对着麦克风说“小言小言”', + onNext: () {}, + ); + }, builder: (BuildContext context, GlobalKey key) => SizedBox( key: key, - child: _buildDeviceStatusCard(isDarkMode), ), ), - SizedBox(height: 20.h), // 热门AI体验 _buildSectionTitle(isDarkMode), @@ -402,6 +418,10 @@ class HomeView extends GetView { final rightEarCharging = controller.rightEarCharging; final caseCharging = controller.caseCharging; + // 判断是否显示充电盒:充电盒电量大于0且设备已连接 + final shouldShowCase = + caseBattery.value > 0 && controller.isFullyConnectedRx.value; + // 使用更紧凑的布局 return Container( padding: EdgeInsets.symmetric(vertical: 8.h, horizontal: 4.w), @@ -435,20 +455,23 @@ class HomeView extends GetView { isDarkMode: isDarkMode, ), - // // 分隔线 - // Container( - // height: 30.h, - // width: 1, - // color: isDarkMode ? Colors.grey[800] : Colors.grey[300], - // ), - - // // 充电盒 - // _buildCompactBatteryItem( - // label: '充电盒', - // batteryLevel: caseBattery.value, - // isCharging: caseCharging.value, - // isDarkMode: isDarkMode, - // ), + // 只有在充电盒电量大于0且设备已连接时才显示充电盒 + if (shouldShowCase) ...[ + // 分隔线 + Container( + height: 30.h, + width: 1, + color: isDarkMode ? Colors.grey[800] : Colors.grey[300], + ), + + // 充电盒 + _buildCompactBatteryItem( + label: '充电盒', + batteryLevel: caseBattery.value, + isCharging: caseCharging.value, + isDarkMode: isDarkMode, + ), + ] ], ), ); @@ -645,11 +668,16 @@ class HomeView extends GetView { child: IntroStepBuilder( group: 'home', order: 3, - text: '太棒了!你已成功唤醒小言,接下来试试让它帮你做件事吧', onHighlightWidgetTap: () { Intro.of(Get.context!).dispose(); controller.exploreAIFeatures(); }, + overlayBuilder: (params) { + return IntroOverlayBuilder.aiBuilder( + params, + '太棒了!你已成功唤醒小言,接下来试试让它帮你做件事吧', + ); + }, builder: (BuildContext context, GlobalKey key) => _buildFeatureCard( key: key, title: 'aiAssistant'.tr, // AI全能助手 diff --git a/lib/modules/login/controllers/login_controller.dart b/lib/modules/login/controllers/login_controller.dart index 1ffb49ca0..1a57adf96 100644 --- a/lib/modules/login/controllers/login_controller.dart +++ b/lib/modules/login/controllers/login_controller.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import '../../../data/models/appconfig.dart'; import '../../../data/models/appconfig_model.dart'; import 'package:get/get.dart'; @@ -815,54 +817,79 @@ class LoginController extends GetxController { // 使用登陆请求服务器 Future requestLogin(Map params) async { - Logger().d("请求后台数据login=$params"); - var response = await Api.login({ - "mail": params["mail"], - "phone": params["phone"], - "stype": params["stype"], - "vcode": params["vcode"], - "ttoken": params["ttoken"], - "phonemodel": _appVersion + _countryCode + _userChannel.toString(), - "phonemac": params["phonemac"], - "name": params["name"], - "avatar": params["avatar"], - }); - if (response != null) { - Logger().d("请求后台数据login回复数据=$response"); - - // final instance = await GetStorage(); - await _storage.write('loginInfo', jsonEncode(response)); - // 2. 将响应数据转换为User对象 - try { - Logger().d('响应数据: ${response.toString()}'); // 调试输出原始数据 - - if (response['token'].toString() != '') { - // 新增:检查 user 字段是否存在且类型正确 - // if (response.containsKey('data') && response['data'] is Map) { - final user = User.fromJson(response); - Logger().d('用户初始化成功: ${response['token'].toString()}'); - await _storage.write("logintoken", response['token'].toString()); - // 存储到 SharedPreferences (仅存储用户数据) - //final prefs = await GetStorage(); - await _storage.write('user_info', user.toJson()); - - //获取app的配置信息 - await getAppConfig(); - Get.offAllNamed(Routes.pairing); - // } else { - // throw FormatException('API 响应缺少 user 字段'); - // } - } else { - throw Exception('接口返回错误: ${response['msg']}'); + try { + Logger().d("请求后台数据login=$params"); + // 在Api.login调用时添加超时 + var response = await Api.login({ + "mail": params["mail"], + "phone": params["phone"], + "stype": params["stype"], + "vcode": params["vcode"], + "ttoken": params["ttoken"], + "phonemodel": _appVersion + _countryCode + _userChannel.toString(), + "phonemac": params["phonemac"], + "name": params["name"], + "avatar": params["avatar"], + }).timeout( + const Duration(seconds: 30), // 30秒超时 + onTimeout: () { + throw TimeoutException('登录请求超时', const Duration(seconds: 30)); + }, + ); + if (response != null) { + Logger().d("请求后台数据login回复数据=$response"); + + await _storage.write('loginInfo', jsonEncode(response)); + + try { + Logger().d('响应数据: ${response.toString()}'); + + if (response['token'].toString() != '') { + final user = User.fromJson(response); + Logger().d('用户初始化成功: ${response['token'].toString()}'); + await _storage.write("logintoken", response['token'].toString()); + await _storage.write('user_info', user.toJson()); + + // 获取app的配置信息 + await getAppConfig(); + Get.offAllNamed(Routes.pairing); + } else { + throw Exception('接口返回错误: ${response['msg']}'); + } + } catch (e, stack) { + Logger().d('用户数据处理失败: $e'); + Logger().d('堆栈跟踪: $stack'); + Get.snackbar( + 'error'.tr, + 'userDataParseFailed'.tr, // 用户数据解析失败 + snackPosition: SnackPosition.TOP, + backgroundColor: Colors.red.withOpacity(0.1), + colorText: Colors.red, + ); } - } catch (e, stack) { - // 新增:打印堆栈跟踪 - Logger().d('用户数据处理失败: $e'); - Logger().d('堆栈跟踪: $stack'); - Get.snackbar('错误', '用户数据解析失败,请检查网络'); + } else { + // API返回null的情况 + Get.snackbar( + 'error'.tr, + 'networkError'.tr, // 网络错误,请重试 + snackPosition: SnackPosition.TOP, + backgroundColor: Colors.red.withOpacity(0.1), + colorText: Colors.red, + ); } - - // Get.offAll(() => IndexWidget()); + } catch (e) { + // 网络请求异常 + Logger().e('登录请求失败: $e'); + Get.snackbar( + 'error'.tr, + 'loginFailed'.tr, // 登录失败 + snackPosition: SnackPosition.TOP, + backgroundColor: Colors.red.withOpacity(0.1), + colorText: Colors.red, + ); + } finally { + // 确保无论成功还是失败都重置加载状态 + _isLoading.value = false; } } diff --git a/lib/modules/login/views/login_view.dart b/lib/modules/login/views/login_view.dart index 014740d9c..42318e96d 100644 --- a/lib/modules/login/views/login_view.dart +++ b/lib/modules/login/views/login_view.dart @@ -13,12 +13,17 @@ class LoginView extends GetView { Widget build(BuildContext context) { final isDarkMode = Theme.of(context).brightness == Brightness.dark; - return Scaffold( - backgroundColor: isDarkMode ? Colors.black : Colors.grey[50], - body: SafeArea( - child: Obx(() => controller.viewState == LoginViewState.welcome - ? _buildWelcomeView(isDarkMode) - : _buildVerificationView(context, isDarkMode)), + return GestureDetector( + onTap: () { + FocusScope.of(context).unfocus(); + }, + child: Scaffold( + backgroundColor: isDarkMode ? Colors.black : Colors.grey[50], + body: SafeArea( + child: Obx(() => controller.viewState == LoginViewState.welcome + ? _buildWelcomeView(isDarkMode) + : _buildVerificationView(context, isDarkMode)), + ), ), ); } diff --git a/lib/modules/splash/controllers/splash_controller.dart b/lib/modules/splash/controllers/splash_controller.dart index 013ba187e..5c90dacca 100644 --- a/lib/modules/splash/controllers/splash_controller.dart +++ b/lib/modules/splash/controllers/splash_controller.dart @@ -1,15 +1,14 @@ import 'dart:async'; import 'dart:io'; import 'package:app_tracking_transparency/app_tracking_transparency.dart'; - -import '../../../data/models/appconfig.dart'; -import '../../../data/models/appconfig_model.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:get_storage/get_storage.dart'; + +import '../../../data/models/appconfig.dart'; +import '../../../data/models/appconfig_model.dart'; import '../../../data/models/user_Info.dart'; import '../../../routes/app_routes.dart'; -import '../../../core/themes/theme_service.dart'; import '../../../data/services/network/api.dart'; import '../../../data/services/version_update_service.dart'; import '../../../modules/common/widgets/version_update_dialog.dart'; @@ -24,191 +23,174 @@ class SplashController extends GetxController { final _loadingProgress = 0.0.obs; double get loadingProgress => _loadingProgress.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; //是否同意隐私政策 - - // 版本号 - //final String version = 'V1.0.0'; - - // 主题服务 - final ThemeService _themeService = ThemeService(); - - // 会话级别的版本检查标记(静态变量,整个应用会话期间保持) - static bool _hasCheckedVersionInSession = false; - - // 版本检查相关常量 - static const String _lastVersionCheckKey = 'last_version_check_timestamp'; - static const int _versionCheckIntervalHours = 24; // 24小时间隔 + // 使用一个反应式变量来决定是否可以导航,替代所有布尔标记 + final _canNavigate = false.obs; @override void onInit() { super.onInit(); - - // 首先检查隐私政策状态 - final hasAcceptedPrivacy = _storage.read('hasAcceptedPrivacy') ?? false; - WidgetsBinding.instance.addPostFrameCallback((_) { - if (!hasAcceptedPrivacy) { - // 优先显示隐私政策弹窗,暂停其他初始化 - _showPrivacyDialog(); - } else { - // 已同意隐私政策,正常启动流程 - _startNormalFlow(); - } - }); - - // 注意:追踪权限请求将在用户同意隐私政策后执行 - } - - /// 异步请求追踪权限 - Future _requestTrackingPermissionAsync() async { - try { - await AppTrackingTransparency.requestTrackingAuthorization(); - Logger.d(_tag, '用户追踪权限请求完成'); - } catch (e) { - Logger.e(_tag, '用户追踪权限请求失败: $e'); - } finally { - _isTrackingPermissionRequested.value = true; + // 监听 _canNavigate 状态,一旦为 true,立即且仅执行一次导航 + once(_canNavigate, (_) => _navigateToTargetPage()); + // 启动整个初始化流程 + _initializeApp(); + } + + /// 启动并协调所有初始化任务 + Future _initializeApp() async { + // 步骤 1: 检查并等待隐私政策同意 + final hasAccepted = await _checkAndRequestPrivacy(); + if (!hasAccepted) { + // 如果用户拒绝隐私政策,应用会退出,后续代码不执行 + return; } - } - void _startNormalFlow() { - _startLoadingAnimation(); - _startParallelTasks(); - } + // 步骤 2: 并行执行所有启动任务,并等待它们全部完成 + // Future.wait 会等待数组中所有的 Future 都完成后才继续 + await Future.wait([ + _startLoadingAnimation(), // 任务1: 最小动画时长 + _performParallelTasks(), // 任务2: 其他所有后台任务 + ]); - /// 启动并行任务:在1.5秒动画期间同时执行多个任务 - void _startParallelTasks() { - // 并行执行所有任务 - _requestTrackingPermissionAsync(); - _performVersionCheckAsync(); - _validateTokenAsync(); + // 步骤 3: 所有任务(动画和后台)都已完成,触发导航 + _canNavigate.value = true; } - /// 异步执行版本检查 - Future _performVersionCheckAsync() async { - try { - Logger.d(_tag, '开始执行版本检查'); - await _checkVersionUpdateIfNeeded(); - } catch (e) { - Logger.e(_tag, '版本检查失败: $e'); + /// 检查隐私政策,如果未同意,则显示弹窗并等待用户操作 + /// 返回用户是否同意 + Future _checkAndRequestPrivacy() async { + if (_storage.read('hasAcceptedPrivacy') ?? false) { + return true; } - //finally { - // _isVersionCheckCompleted.value = true; - // } - } - - void _onPrivacyAccepted() { - _storage.write('hasAcceptedPrivacy', true); - Get.back(); // 关闭弹窗 - _startNormalFlow(); // 开始正常流程(已包含版本检查) - } - void _onPrivacyRejected() { - exit(0); // 退出应用 - } + final completer = Completer(); + WidgetsBinding.instance.addPostFrameCallback((_) { + PrivacyPolicyDialog.show( + onAccept: () { + _storage.write('hasAcceptedPrivacy', true); + Get.back(); // 关闭弹窗 + if (!completer.isCompleted) completer.complete(true); + }, + onReject: () { + if (!completer.isCompleted) completer.complete(false); + exit(0); // 退出应用 + }, + canDismiss: false, + ); + }); - // 显示隐私政策弹窗 - void _showPrivacyDialog() { - PrivacyPolicyDialog.show( - onAccept: () { - _onPrivacyAccepted(); - _hasAcceptedPrivacy.value = true; - }, - onReject: _onPrivacyRejected, - canDismiss: false, // 禁止通过返回键关闭弹窗 - ); + return completer.future; } - // 进度条动画(持续1.5秒) - void _startLoadingAnimation() { + /// 动画任务:返回一个在动画结束时完成的 Future + Future _startLoadingAnimation() { const totalDuration = 1500; // 动画总时长1.5秒 - const steps = totalDuration ~/ 30; // 30ms间隔,更平滑的动画 + const steps = totalDuration ~/ 30; const stepValue = 1.0 / steps; + final completer = Completer(); Timer.periodic(const Duration(milliseconds: 30), (timer) { if (_loadingProgress.value < 1.0) { _loadingProgress.value += stepValue; } else { + _loadingProgress.value = 1.0; timer.cancel(); - _isAnimationCompleted.value = true; - _checkNavigateCondition(); + if (!completer.isCompleted) completer.complete(); } }); + + return completer.future; + } + + /// 将所有并行的后台任务打包成一个 Future + Future _performParallelTasks() async { + // 同样使用 Future.wait,确保此函数在所有子任务完成后才结束 + await Future.wait([ + _requestTrackingPermissionAsync(), + _validateTokenAsync(), + _performVersionCheckAsync(), + ]); + Logger.d(_tag, '所有后台任务已完成'); + } + + /// 异步请求追踪权限 + Future _requestTrackingPermissionAsync() async { + try { + await AppTrackingTransparency.requestTrackingAuthorization(); + Logger.d(_tag, '用户追踪权限请求完成'); + } catch (e) { + Logger.e(_tag, '用户追踪权限请求失败: $e'); + // 即使失败,也视为完成,不阻塞主流程 + } } /// 异步验证Token和加载应用配置 Future _validateTokenAsync() async { try { - bool isLoggedIn = false; final token = _storage.read('logintoken'); - Logger.d(_tag, 'Token验证开始: $token'); - if (token != null && token.isNotEmpty) { - isLoggedIn = await getLoginToken(); + final isLoggedIn = await getLoginToken(); if (isLoggedIn) { + // 只有登录成功,才获取App配置 await getAppConfig(); - _isAppConfigLoaded.value = true; + } else { + await User.clearUserSession(); // } } - - _isLoggedIn.value = isLoggedIn; - Logger.d(_tag, 'Token验证完成,登录状态: $isLoggedIn'); } catch (e) { - Logger.e(_tag, 'Token验证失败: $e'); - _isLoggedIn.value = false; - } finally { - _isTokenValidated.value = true; + Logger.e(_tag, 'Token验证或配置加载失败: $e'); + await User.clearUserSession(); } } - // 检查跳转条件:所有任务都完成后才跳转 - void _checkNavigateCondition() { - if (_isAnimationCompleted.value && - _isTrackingPermissionRequested.value && - _isVersionCheckCompleted.value && - _isTokenValidated.value) { - _navigateToTargetPage(); + /// 异步执行版本检查 + Future _performVersionCheckAsync() async { + try { + final versionService = VersionUpdateService.to; + final updateInfo = await versionService.checkForUpdate(); + + if (updateInfo.hasUpdate) { + Logger.i(_tag, '发现新版本: ${updateInfo.latestVersion}'); + final completer = Completer(); + // 在主线程中显示更新对话框 + WidgetsBinding.instance.addPostFrameCallback((_) { + // 使用 whenComplete 确保无论弹窗如何关闭,Future 都会完成 + VersionUpdateDialog.show(updateInfo).whenComplete(() { + if (!completer.isCompleted) completer.complete(); + }); + }); + // 等待弹窗交互结束 + await completer.future; + } else { + Logger.i(_tag, '当前已是最新版本'); + } + } catch (e) { + Logger.e(_tag, '版本检查失败: $e'); + // 即使检查失败,也视为任务完成 } } - // 根据登录状态跳转到目标页面 + /// 根据最终状态跳转到目标页面 void _navigateToTargetPage() { - if (_isLoggedIn.value && _isAppConfigLoaded.value) { + // 导航时直接检查最终的数据状态,而不是依赖中间变量 + final token = _storage.read('logintoken'); + final isAppConfigInitialized = AppConfig.isInitialized(); + // 检查登录状态和配置初始化状态 + if (token != null && token.isNotEmpty && isAppConfigInitialized) { Get.offAllNamed(Routes.pairing); } else { Get.offAllNamed(Routes.login); } } + /// 异步获取登录Token Future getLoginToken() async { try { - Logger.d(_tag, 'getLoginToken:进来了'); - //if (User.getname() != "") { final response = await Api.tokenlogin(); - Logger.d(_tag, 'getLoginToken: $response'); - if (response == null) return false; - - if (response['user'] != '') { - Logger.d(_tag, 'getLoginToken:进来了'); - // 根据API设计调整 + if (response?['user'] != null && response['user'] != '') { final user = User.fromJson(response); - - // 存储用户信息(示例) await _storage.write('user_info', user.toJson()); return true; } - //return true; - //} return false; } catch (e) { Logger.e(_tag, 'Token获取失败: $e'); @@ -218,140 +200,14 @@ class SplashController extends GetxController { Future getAppConfig() async { try { - Logger.d(_tag, 'getAppConfig:进来了'); - //if (User.getname() != "") { final response = await Api.getappconfig(); - Logger.d(_tag, 'getAppConfig: $response'); if (response == null) return false; - // 根据API设计调整 final resp = UserGetAppConfigResp.fromJson(response); - Logger.d(_tag, 'getAppConfig:进来了'); - // 存储用户信息(示例) - // _storage.write('ENV', resp.env); - // _storage.write('AGENTS', resp.agents); - // _storage.write('MCPS', resp.mcps); AppConfig.initialize(resp); return true; } catch (e) { - Logger.e(_tag, 'getAppConfig 取失败: $e'); + Logger.e(_tag, 'getAppConfig 获取失败: $e'); return false; } } - - /// 混合检查机制:判断是否需要进行版本检查 - /// 检查条件:1. 会话内未检查过 OR 2. 距离上次检查超过24小时 - Future _checkVersionUpdateIfNeeded() async { - 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'); - await _performVersionCheck(); - } else { - Logger.i(_tag, '跳过版本检查 - 会话内已检查且未超过时间间隔'); - } - } catch (e) { - Logger.e(_tag, '版本检查判断失败: $e'); - } - } - - /// 执行版本检查(在后台进行,不阻塞启动流程) - Future _performVersionCheck() async { - try { - final versionService = VersionUpdateService.to; - // 异步执行,不等待结果 - versionService.checkForUpdate().then((updateInfo) { - if (updateInfo.hasUpdate) { - Logger.i(_tag, '发现新版本: ${updateInfo.latestVersion}'); - // 在主线程中显示更新对话框 - WidgetsBinding.instance.addPostFrameCallback((_) { - VersionUpdateDialog.show(updateInfo).then((_) { - // 无论用户选择更新还是稍后提醒,都标记版本检查完成 - _isVersionCheckCompleted.value = true; - _checkNavigateCondition(); - }); - }); - } else { - _isVersionCheckCompleted.value = true; - Logger.i(_tag, '当前已是最新版本: ${updateInfo.currentVersion}'); - } - - // 更新检查记录 - _updateVersionCheckRecord(); - }).catchError((e) { - Logger.e(_tag, '版本检查失败: $e'); - // 即使检查失败,也标记版本检查完成,让流程继续 - _isVersionCheckCompleted.value = true; - // 更新检查记录,避免频繁重试 - _updateVersionCheckRecord(); - }); - } catch (e) { - Logger.e(_tag, '版本检查失败: $e'); - // 即使检查失败,也标记版本检查完成,让流程继续 - _isVersionCheckCompleted.value = true; - // 更新检查记录,避免频繁重试 - _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'); - } - } } 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 bfbad2dfc..f70805fac 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 @@ -318,6 +318,8 @@ object AgentService : CoroutineScope { _isTtsSpeaking.set(true) restartIdleCheck() sendEvent("tts_started", mapOf("status" to "started")) + // 停止气泡音,避免与TTS同时播放 + audioPlayer?.stopAudio() } TtsEventType.SYNTHESIS_COMPLETED -> { @@ -330,11 +332,15 @@ object AgentService : CoroutineScope { _isTtsSpeaking.set(false) restartIdleCheck() sendEvent("tts_canceled", mapOf("status" to "canceled")) + // TTS取消时停止气泡音 + audioPlayer?.stopAudio() } TtsEventType.PLAYBACK_STARTED -> { //开始AI播报内容 restartIdleCheck() sendEvent("playback_started", mapOf("status" to "playback_started")) + // 确保气泡音已停止,避免与TTS播放重叠 + audioPlayer?.stopAudio() // 发送AI说话开始广播 val intent = Intent("com.yunqiinnovation.music_service.AI_SAY_START").apply { @@ -357,6 +363,8 @@ object AgentService : CoroutineScope { TtsEventType.ERROR -> { _isTtsSpeaking.set(false) restartIdleCheck() + // TTS错误时停止气泡音 + audioPlayer?.stopAudio() val params = event.params val code = params["errorCode"] as? String ?: "UNKNOWN_ERROR" val message = params["errorMessage"] as? String ?: "未知错误" @@ -665,6 +673,8 @@ object AgentService : CoroutineScope { // 并行执行停止操作,加快响应速度 stopTts() stopAiStream() + // 停止气泡音,避免在打断后继续播放 + audioPlayer?.stopAudio() // 记录打断耗时 @@ -891,7 +901,8 @@ object AgentService : CoroutineScope { responseBuilder.append(token) if (speakResponse && reply && broadcast) { ttsService?.speakStream(token) - // audioPlayer?.stopAudio() + // 在开始流式TTS时立即停止气泡音 + 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 bfac028cd..b884a0c71 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,8 @@ class ChatApiStreamCallback: StreamCallback { if speakResponse && reply && broadcast{ try agentService.azureTtsHelper?.speakStream(token) -// try agentService.audioPlayer?.stopAwaitSound() + // 在开始流式TTS时立即停止气泡音 + // try agentService.audioPlayer?.stopAwaitSound() } if (reply && token != "") { try agentService.sendEvent(name: "assistant_token", data: ["token": token]) @@ -1219,8 +1220,12 @@ class AudioPlayer { private var player: AVAudioPlayer? private var callingPlayer: AVAudioPlayer? private var awaitPlayer: AVAudioPlayer? // 新增:等待音效播放器 + private var awaitTimer: DispatchSourceTimer? // 新增:等待音效超时定时器 private let logger = OSLog(subsystem: "com.yunqiinnovation.agent_service", category: "AudioPlayer") private let audioQueue = DispatchQueue(label: "agent.audio.player", qos: .userInitiated) + + // 等待音效超时时间(秒)- 改为 var 以支持动态修改 + private var awaitTimeoutSeconds: TimeInterval = 10.0 private var resourcesBundle: Bundle { #if SWIFT_PACKAGE @@ -1246,7 +1251,14 @@ class AudioPlayer { } func playAwaitSound() { + // 先停止之前的等待音效和定时器 + stopAwaitSound() + + // 播放等待音效 playLoopSound(named: "await", fileType: "mp3", player: &awaitPlayer) + + // 启动超时定时器 + startAwaitTimeout() } func playStopSound() { @@ -1263,8 +1275,14 @@ class AudioPlayer { } func stopAwaitSound() { + // 停止音频播放 awaitPlayer?.stop() awaitPlayer = nil + + // 停止超时定时器 + stopAwaitTimeout() + + os_log("停止等待音效播放", log: logger, type: .debug) } private func playSound(named: String, fileType: String = "mp3") { @@ -1337,6 +1355,41 @@ class AudioPlayer { os_log("播放循环音频失败: %{public}@", log: logger, type: .error, error.localizedDescription) } } + + private func startAwaitTimeout() { + // 停止之前的定时器 + stopAwaitTimeout() + + // 创建新的定时器 + let timer = DispatchSource.makeTimerSource(queue: DispatchQueue.global(qos: .utility)) + awaitTimer = timer + + timer.schedule(deadline: .now() + awaitTimeoutSeconds) + timer.setEventHandler { [weak self] in + DispatchQueue.main.async { [weak self] in + guard let self = self else { return } + + os_log("等待音效超时,自动停止播放 - 超时时间: %.0f秒", + log: self.logger, type: .info, self.awaitTimeoutSeconds) + + self.stopAwaitSound() + } + } + timer.resume() + + os_log("启动等待音效超时定时器,超时时间: %.0f秒", + log: logger, type: .debug, awaitTimeoutSeconds) + } + + private func stopAwaitTimeout() { + awaitTimer?.cancel() + awaitTimer = nil + } + + func setAwaitTimeout(_ seconds: TimeInterval) { + awaitTimeoutSeconds = max(2.0, seconds) // 最小5秒 + os_log("设置等待音效超时时间: %.0f秒", log: logger, type: .info, awaitTimeoutSeconds) + } } extension AgentServiceImpl: AzureAsrHelper.ContinuousRecognizeCallback { 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 5c90a7c67..3106c1236 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 @@ -100,14 +100,9 @@ public class AzureAsrHelper: NSObject { // 创建语音配置 speechConfig = try SPXSpeechConfiguration(subscription: subscriptionKey, region: region) // 针对翻译场景的性能优化 - optimizeForTranslation() + // optimizeForTranslation() if isAutoDetectLanguage { - // 针对翻译场景优化的语言检测配置 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 { @@ -150,55 +145,7 @@ 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 - ] - } + /** * 开始连续语音识别 * @@ -249,28 +196,7 @@ 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 "" - // } - // } + /** * 停止连续语音识别 @@ -388,11 +314,20 @@ public class AzureAsrHelper: NSObject { // 创建识别器 if isAutoDetectLanguage { // 使用正确的初始化方法 - let autoDetectConfig = try SPXAutoDetectSourceLanguageConfiguration(supportedLanguages) - print("autoDetectConfig: \(autoDetectConfig)") + guard let autoDetectSourceLanguageConfig = try? SPXAutoDetectSourceLanguageConfiguration(supportedLanguages) else { + print("创建自动语言检测配置失败,使用默认语言配置") + // 如果创建失败,使用默认的单语言配置 + recognizer = try SPXSpeechRecognizer( + speechConfiguration: speechConfig!, + audioConfiguration: audioConfig! + ) + return true + } + + print("autoDetectConfig: \(autoDetectSourceLanguageConfig)") recognizer = try SPXSpeechRecognizer( speechConfiguration: speechConfig!, - autoDetectSourceLanguageConfiguration: autoDetectConfig, + autoDetectSourceLanguageConfiguration: autoDetectSourceLanguageConfig, audioConfiguration: audioConfig! ) } else { @@ -484,60 +419,7 @@ public class AzureAsrHelper: NSObject { 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) - } + /** * 动态切换语言(翻译场景中的语言切换) @@ -588,13 +470,16 @@ public class AzureAsrHelper: NSObject { let result = event.result guard !(result.text?.isEmpty ?? true) else { return } - // let detectedLanguage = self.getDetectedLanguage(from: result) - - // 检测文本语言,避免重复调用 - let detectedLanguage = self.detectTextLanguage(result.text ?? "") ?? "zh-CN" + // 使用现有的 getDetectedLanguage 方法替代 result.language + let detectedLanguage = self.getDetectedLanguage(from: result) + let text = result.text + + print("正在识别事件=检测到语言: \(detectedLanguage), 识别中: \(text)") + // // 检测文本语言,避免重复调用 + // let detectedLanguage = self.detectTextLanguage(result.text ?? "") ?? "zh-CN" - // 输出检测到的语言信息 - print("检测到的语言: \(detectedLanguage)") + // // 输出检测到的语言信息 + // print("检测到的语言: \(detectedLanguage)") DispatchQueue.main.async { callback.onRecognizing(result.text ?? "", detectedLanguage) @@ -608,11 +493,16 @@ public class AzureAsrHelper: NSObject { guard result.reason == .recognizedSpeech, !(result.text?.isEmpty ?? true) else { return } + // 使用现有的 getDetectedLanguage 方法替代 result.language + let detectedLanguage = self.getDetectedLanguage(from: result) + let text = result.text + + print("识别完成事件=检测到语言: \(detectedLanguage), 识别中: \(text)") // 检测文本语言,避免重复调用 - let detectedLanguage = self.detectTextLanguage(result.text ?? "") ?? "zh-CN" + // let detectedLanguage = self.detectTextLanguage(result.text ?? "") ?? "zh-CN" - // 输出检测到的语言信息 - print("检测到的语言: \(detectedLanguage)") + // // 输出检测到的语言信息 + // print("检测到的语言: \(detectedLanguage)") // 在主线程回调结果 DispatchQueue.main.async { @@ -1175,3 +1065,54 @@ private func runMicrophoneCapture() { func onError(_ error: String) } } + +// //ios 语言检测 +// /** +// * 检测输入文本的语言,从支持的语言列表中选择最匹配的 +// * +// * @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 +// ] +// } \ No newline at end of file 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 a07e91f72..1067a9211 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 @@ -26,22 +26,32 @@ class SwiftOpusAudioProcessor: NSObject { cleanup() } + /// 线程安全的解码器初始化 private func initializeOpusDecoder() -> Bool { + objc_sync_enter(self) + defer { objc_sync_exit(self) } + if decoder != nil { return true } var error: Int32 = 0 - decoder = opus_decoder_create(sampleRate, channels, &error) + let newDecoder = opus_decoder_create(sampleRate, channels, &error) - guard error == OPUS_OK, decoder != nil else { + guard error == OPUS_OK, newDecoder != nil else { + print("Opus解码器创建失败,错误代码: \(error)") return false } + decoder = newDecoder return true } + /// 线程安全的解码器销毁 private func destroyOpusDecoder() { + objc_sync_enter(self) + defer { objc_sync_exit(self) } + if let decoder = decoder { opus_decoder_destroy(decoder) self.decoder = nil @@ -112,23 +122,44 @@ class SwiftOpusAudioProcessor: NSObject { } } - // 简化的 Opus 解码方法,重点是容错处理 + /// 改进的 Opus 解码方法,增强容错和线程安全 private func decodeOpusPacket(_ packet: Data) { - guard let decoder = decoder else { return } + // 添加线程安全锁 + objc_sync_enter(self) + defer { objc_sync_exit(self) } + // 验证解码器状态 + guard let decoder = decoder, isDecodingActive else { + print("解码器未初始化或已停止") + return + } + let maxSamples = Int(maxFrameSize) var pcmBuffer = [opus_int16](repeating: 0, count: maxSamples * Int(channels)) let decodedSamples = packet.withUnsafeBytes { packetPtr in - let uint8Ptr = packetPtr.bindMemory(to: UInt8.self).baseAddress - return opus_decode( - decoder, - uint8Ptr, - opus_int32(packet.count), - &pcmBuffer, - Int32(maxSamples), - 0 - ) + // 确保指针有效 + guard let uint8Ptr = packetPtr.bindMemory(to: UInt8.self).baseAddress else { + print("无法获取数据包指针") + return opus_int32(-1) + } + + // 添加异常捕获机制 + var result: opus_int32 = -1 + + // 使用 autoreleasepool 确保内存管理 + autoreleasepool { + result = opus_decode( + decoder, + uint8Ptr, + opus_int32(packet.count), + &pcmBuffer, + Int32(maxSamples), + 0 + ) + } + + return result } // 增强的错误处理:检查各种错误情况 @@ -237,4 +268,4 @@ class SwiftOpusAudioProcessor: NSObject { func getCurrentConfig() -> (sampleRate: Int32, channels: Int32) { return (sampleRate, channels) } -} +}