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
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/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/modules/pairing/controllers/pairing_controller.dart b/lib/modules/pairing/controllers/pairing_controller.dart
index dedb8fbbb..f2783f4bb 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 {
@@ -47,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();
@@ -103,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();
@@ -126,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();
@@ -138,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');
+ }
+ }
+
// ==================== 监听器设置 ====================
/// 集中设置所有监听器
@@ -1028,52 +1137,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 +1206,7 @@ class PairingController extends GetxController {
actions: [
TextButton(
onPressed: () => Get.back(),
- child: Text('我知道了'),
+ child: Text('iKnow'.tr, style: TextStyle(fontSize: 14.sp)),
),
],
),
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,
+ ),
+ ]
],
),
),
diff --git a/lib/modules/splash/controllers/splash_controller.dart b/lib/modules/splash/controllers/splash_controller.dart
index 4bf70d207..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,178 +23,174 @@ class SplashController extends GetxController {
final _loadingProgress = 0.0.obs;
double get loadingProgress => _loadingProgress.value;
- // 资源加载状态
- final _isResourceLoaded = false.obs;
- bool get isResourceLoaded => _isResourceLoaded.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();
+ // 监听 _canNavigate 状态,一旦为 true,立即且仅执行一次导航
+ once(_canNavigate, (_) => _navigateToTargetPage());
+ // 启动整个初始化流程
+ _initializeApp();
+ }
+
+ /// 启动并协调所有初始化任务
+ Future _initializeApp() async {
+ // 步骤 1: 检查并等待隐私政策同意
+ final hasAccepted = await _checkAndRequestPrivacy();
+ if (!hasAccepted) {
+ // 如果用户拒绝隐私政策,应用会退出,后续代码不执行
+ return;
+ }
- // 首先检查隐私政策状态
- final hasAcceptedPrivacy = _storage.read('hasAcceptedPrivacy') ?? false;
- WidgetsBinding.instance.addPostFrameCallback((_) {
- if (!hasAcceptedPrivacy) {
- // 优先显示隐私政策弹窗,暂停其他初始化
- _showPrivacyDialog();
- } else {
- // 已同意隐私政策,正常启动流程
- _startNormalFlow();
- }
- });
-
- // 异步处理用户追踪授权,不阻塞主流程
- _requestTrackingPermissionAsync();
- }
+ // 步骤 2: 并行执行所有启动任务,并等待它们全部完成
+ // Future.wait 会等待数组中所有的 Future 都完成后才继续
+ await Future.wait([
+ _startLoadingAnimation(), // 任务1: 最小动画时长
+ _performParallelTasks(), // 任务2: 其他所有后台任务
+ ]);
- /// 异步请求追踪权限,不阻塞启动流程
- void _requestTrackingPermissionAsync() {
- Future.delayed(const Duration(milliseconds: 500), () async {
- try {
- await AppTrackingTransparency.requestTrackingAuthorization();
- Logger.d(_tag, '用户追踪权限请求完成');
- } catch (e) {
- Logger.e(_tag, '用户追踪权限请求失败: $e');
- }
- });
+ // 步骤 3: 所有任务(动画和后台)都已完成,触发导航
+ _canNavigate.value = true;
}
- void _startNormalFlow() {
- _startLoadingAnimation();
- _loadAppResources();
- }
+ /// 检查隐私政策,如果未同意,则显示弹窗并等待用户操作
+ /// 返回用户是否同意
+ Future _checkAndRequestPrivacy() async {
+ if (_storage.read('hasAcceptedPrivacy') ?? false) {
+ return true;
+ }
- /// 智能调度版本检查,在页面跳转后执行,完全不影响启动流程
- void _scheduleVersionCheck() {
- // 在独立的微任务中执行,避免阻塞主线程
- scheduleMicrotask(() async {
- try {
- Logger.d(_tag, '开始执行版本检查(后台任务)');
- _checkVersionUpdateIfNeeded();
- } catch (e) {
- Logger.e(_tag, '版本检查调度失败: $e');
- }
+ 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 _onPrivacyAccepted() {
- _storage.write('hasAcceptedPrivacy', true);
- Get.back(); // 关闭弹窗
- _startNormalFlow(); // 开始正常流程(已包含版本检查)
- }
-
- void _onPrivacyRejected() {
- exit(0); // 退出应用
- }
- // 显示隐私政策弹窗
- void _showPrivacyDialog() {
- PrivacyPolicyDialog.show(
- onAccept: () {
- _onPrivacyAccepted();
- _hasAcceptedPrivacy.value = true;
- },
- onReject: _onPrivacyRejected,
- canDismiss: false, // 禁止通过返回键关闭弹窗
- );
+ return completer.future;
}
- // 进度条动画(持续2秒)
- void _startLoadingAnimation() {
- const totalDuration = 1500; // 动画总时长增加到1.5秒
- const steps = totalDuration ~/ 30; // 30的间隔,更平滑的动画
+ /// 动画任务:返回一个在动画结束时完成的 Future
+ Future _startLoadingAnimation() {
+ const totalDuration = 1500; // 动画总时长1.5秒
+ 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();
- _checkNavigateCondition();
+ if (!completer.isCompleted) completer.complete();
}
});
- }
- // 异步加载应用资源
- Future _loadAppResources() async {
- try {
- // 模拟资源加载(如API配置、本地缓存等)
- await Future.delayed(const Duration(milliseconds: 1500));
+ return completer.future;
+ }
- // 实际资源加载代码
- // await _loadImages();
- // await _initDatabase();
+ /// 将所有并行的后台任务打包成一个 Future
+ Future _performParallelTasks() async {
+ // 同样使用 Future.wait,确保此函数在所有子任务完成后才结束
+ await Future.wait([
+ _requestTrackingPermissionAsync(),
+ _validateTokenAsync(),
+ _performVersionCheckAsync(),
+ ]);
+ Logger.d(_tag, '所有后台任务已完成');
+ }
- _isResourceLoaded.value = true;
- _checkNavigateCondition();
+ /// 异步请求追踪权限
+ Future _requestTrackingPermissionAsync() async {
+ try {
+ await AppTrackingTransparency.requestTrackingAuthorization();
+ Logger.d(_tag, '用户追踪权限请求完成');
} catch (e) {
- Get.snackbar('错误', '资源加载失败,请检查网络连接');
- _isResourceLoaded.value = true; // 允许用户继续使用
- Logger.e(_tag, '资源加载错误: $e');
+ Logger.e(_tag, '用户追踪权限请求失败: $e');
+ // 即使失败,也视为完成,不阻塞主流程
}
}
- // 检查跳转条件
- void _checkNavigateCondition() async {
- if (_loadingProgress.value >= 1.0 && _isResourceLoaded.value) {
- // 关键改动:先执行页面跳转,并用 await 等待它完成
- await _navigateToLogin();
+ /// 异步验证Token和加载应用配置
+ Future _validateTokenAsync() async {
+ try {
+ final token = _storage.read('logintoken');
+ if (token != null && token.isNotEmpty) {
+ final isLoggedIn = await getLoginToken();
+ if (isLoggedIn) {
+ // 只有登录成功,才获取App配置
+ await getAppConfig();
+ } else {
+ await User.clearUserSession(); //
+ }
+ }
+ } catch (e) {
+ Logger.e(_tag, 'Token验证或配置加载失败: $e');
+ await User.clearUserSession();
+ }
+ }
- // 页面跳转完全结束后,再安心地安排版本检查
- _scheduleVersionCheck();
+ /// 异步执行版本检查
+ 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');
+ // 即使检查失败,也视为任务完成
}
}
- // 获取登录Token并跳转
- Future _navigateToLogin() async {
- bool isLoggedIn = false;
+ /// 根据最终状态跳转到目标页面
+ void _navigateToTargetPage() {
+ // 导航时直接检查最终的数据状态,而不是依赖中间变量
final token = _storage.read('logintoken');
- Logger.d(_tag, 'getLoginToken:进来了${token}');
- if (token != null && token.isNotEmpty) {
- isLoggedIn = await getLoginToken();
- }
- if (isLoggedIn) {
- await getAppConfig();
+ final isAppConfigInitialized = AppConfig.isInitialized();
+ // 检查登录状态和配置初始化状态
+ if (token != null && token.isNotEmpty && isAppConfigInitialized) {
Get.offAllNamed(Routes.pairing);
} else {
- Get.offAllNamed(Routes.login); // 跳转到登录页
+ 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');
@@ -205,131 +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小时
- 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;
- // 异步执行,不等待结果
- 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();
- }).catchError((e) {
- Logger.e(_tag, '版本检查失败: $e');
- // 即使检查失败,也更新记录,避免频繁重试
- _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');
- }
- }
}
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/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/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\": \"最大音量请求已发送\"}"
}
}