Browse Source

Merge branch 'new_dev' of https://github.com/deepcloud2048/deep_voice into liwei

newdev_shunjiawei
liwei1dao 1 year ago
parent
commit
d70c010eea
  1. 12
      ios/Runner/Info.plist
  2. 4
      lib/core/bindings/initial_binding.dart
  3. 13
      lib/core/translations/language/en_us.dart
  4. 9
      lib/core/translations/language/zh_cn.dart
  5. 34
      lib/data/services/novice_guide_service.dart
  6. 9
      lib/main.dart
  7. 77
      lib/modules/agent/controllers/agent_controller.dart
  8. 414
      lib/modules/agent/views/agent_view.dart
  9. 50
      lib/modules/agent/widgets/sound_bar_widget.dart
  10. 1
      lib/modules/devices/controllers/devices_controller.dart
  11. 150
      lib/modules/home/controllers/home_controller.dart
  12. 69
      lib/modules/home/views/home_view.dart
  13. 238
      lib/modules/pairing/controllers/pairing_controller.dart
  14. 97
      lib/modules/settings/views/settings_view.dart
  15. 293
      lib/modules/splash/controllers/splash_controller.dart
  16. 14
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt
  17. 52
      local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift
  18. 6
      local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServicePlugin.swift
  19. 230
      local_plugins/agent_service/ios/agent_service/Sources/agent_service/AudioSessionHub.swift
  20. BIN
      local_plugins/agent_service/ios/agent_service/Sources/agent_service/Resources/await.mp3
  21. 608
      local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift
  22. 214
      local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/LanguageDetector.swift
  23. 2
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleServicePlugin.kt
  24. 2
      local_plugins/ble_service/ios/ble_service/Sources/ble_service/SwiftBleServicePlugin.swift
  25. 2
      local_plugins/ble_service/ios/ble_service/Sources/ble_service/SwiftOpusAudioProcessor.swift
  26. 77
      local_plugins/chat_api/ios/chat_api/Sources/chat_api/MCPClient.swift
  27. 217
      local_plugins/chat_api/ios/chat_api/Sources/chat_api/SystemFunctionHandler.swift
  28. 2
      local_plugins/location_service/android/src/main/kotlin/com/yunqiinnovation/location_service/LocationServicePlugin.kt
  29. 2
      local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicServicePlugin.kt
  30. 2
      local_plugins/music_service/ios/music_service/Sources/music_service/MusicServicePlugin.swift
  31. 5
      pubspec.yaml

12
ios/Runner/Info.plist

@ -59,6 +59,8 @@
<string>193d0a2b1757c41cfa557ea8e8f21c33</string>
<key>LSApplicationQueriesSchemes</key>
<array>
<string>App-Prefs</string>
<string>prefs</string>
<string>weixin</string>
<string>weixinULAPI</string>
<string>weixinURLParamsAPI</string>
@ -127,12 +129,12 @@
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations~ipad</key>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string> <!-- 竖屏正向 -->
<string>UIInterfaceOrientationPortraitUpsideDown</string> <!-- 竖屏倒置(关键缺失项) -->
<string>UIInterfaceOrientationLandscapeLeft</string> <!-- 横屏左 -->
<string>UIInterfaceOrientationLandscapeRight</string> <!-- 横屏右 -->
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>

4
lib/core/bindings/initial_binding.dart

@ -8,6 +8,7 @@ import '../../../data/services/navigation_manager.dart';
import 'package:get/get.dart';
import '../../data/services/meeting/meeting_task_service.dart';
import '../../data/services/meeting/meeting_upload_service.dart';
import '../../data/services/novice_guide_service.dart';
import '../../data/services/speech_factory.dart';
import '../../data/services/volcano_translation_service.dart';
import '../../data/services/volcano_ai_service.dart';
@ -68,6 +69,9 @@ class InitialBinding extends Bindings {
// qq音乐服务
Get.lazyPut<QqMusocService>(() => QqMusocService(), fenix: true);
// 新手引导
Get.lazyPut<NoviceGuideService>(() => NoviceGuideService(), fenix: true);
// 杰理蓝牙服务
// // 语言管理器(需要最先初始化)

13
lib/core/translations/language/en_us.dart

@ -813,6 +813,19 @@ const Map<String, String> 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',

9
lib/core/translations/language/zh_cn.dart

@ -746,6 +746,15 @@ const Map<String, String> zhCN = {
'bleConnectionFailed': 'BLE连接失败',
'deviceAuthorizationFailedCheckCode': '设备授权失败,请检查授权码',
///iOS蓝牙引导窗
'bluetoothGuideTitle': '需要手动开启蓝牙', // 需要手动开启蓝牙
'bluetoothGuideSteps': '请按照以下步骤操作:', // 请按照以下步骤操作:
'bluetoothGuideStep1': '开启蓝牙:设置 → 蓝牙 → 开启', // 开启蓝牙:设置 → 蓝牙 → 开启
'bluetoothGuideStep2': '连接设备:在"我的设备"中找到您的设备并点击连接', // 连接设备:在"我的设备"中找到您的设备并点击连接
'bluetoothGuideStep3': '返回应用继续配对流程', // 返回应用继续配对流程
'bluetoothGuideTip': '提示:也可以从控制中心长按蓝牙图标快速进入蓝牙设置', // 提示:也可以从控制中心长按蓝牙图标快速进入蓝牙设置
'iKnow': '我知道了', // 我知道了
//用户反馈页
"feedback": "用户反馈",
'feedbackInfo': '提供反馈或建议',

34
lib/data/services/novice_guide_service.dart

@ -0,0 +1,34 @@
import 'package:flutter_intro/flutter_intro.dart';
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
class NoviceGuideService extends GetxService {
final GetStorage _storage = GetStorage();
bool isHomeGuide = true;
bool isAIGuide = true;
@override
void onInit() {
super.onInit();
// isHomeGuide = _storage.read('home_guide') ?? false;
// isAIGuide = _storage.read('ai_guide') ?? false;
}
void startHomeGuide() {
if (!isHomeGuide) {
isHomeGuide = true;
_storage.write('home_guide', true);
Intro.of(Get.context!).start(group: 'home');
}
}
void startAIGuide() async {
if (!isAIGuide) {
await Future.delayed(const Duration(milliseconds: 200));
isAIGuide = true;
_storage.write('ai_guide', true);
Intro.of(Get.context!).start(group: 'ai');
}
}
}

9
lib/main.dart

@ -1,5 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:flutter_intro/flutter_intro.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:get/get.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
@ -19,6 +21,8 @@ void main() async {
// 确保Flutter绑定初始化
WidgetsFlutterBinding.ensureInitialized();
await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
// 初始化存储 - 在创建应用之前初始化
await GetStorage.init();
@ -92,7 +96,10 @@ class MainApp extends StatelessWidget {
data: MediaQuery.of(context).copyWith(
textScaler: TextScaler.noScaling,
),
child: widget!,
child: Intro(
padding: EdgeInsets.zero,
child: widget!,
),
);
},
),

77
lib/modules/agent/controllers/agent_controller.dart

@ -3,6 +3,7 @@ import 'dart:convert';
import 'dart:ffi';
import 'dart:io';
import 'package:amap_service/amap_service.dart';
import '../../../data/services/novice_guide_service.dart';
import '/core/utils/upload_oss.dart';
import '/data/models/user_Info.dart';
import '/data/services/music_manager.dart';
@ -39,6 +40,8 @@ class AgentController extends GetxController {
final TextEditingController textController = TextEditingController();
final ScrollController scrollController = ScrollController();
final noviceGuideService = Get.find<NoviceGuideService>();
//音乐播放管理
final _musiceManager = Get.find<MusicManager>();
@ -153,8 +156,6 @@ class AgentController extends GetxController {
_setupMediaSessionCallbacks(); //设置媒体会话回调
_setupTextInputListener(); //设置文本输入监听器
startVoiceInput(); //开启语音输入
}
@ -1013,8 +1014,6 @@ class AgentController extends GetxController {
}
}
// 开始语音输入
Future<void> startVoiceInput() async {
if (isListening.value) return;
@ -1309,29 +1308,71 @@ class AgentController extends GetxController {
return;
}
// 切换到语音模式并开始语音输入
// 打电话模式
isTextInputMode.value = false;
// 打电话模式不使用按住说话模式,而是持续语音输入
//isPushToTalkMode.value = false;
// 开始语音识别
Logger.i(TAG, '开始打电话模式,开始语音输入');
Logger.i(TAG, '打电话模式已启动,开始语音输入');
await AgentService.startConversation();
//}
}
// 结束打电话模式
void endPhoneCall() async {
Logger.i(TAG, '🔥 结束打电话模式');
// 退出打电话模式
isPhoneCallMode.value = false;
try {
// 如果正在录音,先停止录音
if (isListening.value) {
final result = await AgentService.stopConversation();
// 停止语音输入
//await stopVoiceInput();
// 开始语音识别
await AgentService.stopConversation();
// 恢复之前的输入模式,而不是强制切换到文字模式
_restorePreviousInputMode();
if (result) {
isListening.value = false;
}
}
// 退出打电话模式
isPhoneCallMode.value = false;
_restorePreviousInputMode();
} catch (e) {
Logger.e(TAG, '结束打电话模式失败: $e');
// 即使出现异常,也要确保退出打电话模式
isPhoneCallMode.value = false;
isListening.value = false;
_restorePreviousInputMode();
}
}
// 打电话模式专用:切换录音状态
Future<void> togglePhoneCallRecording() async {
Logger.i(TAG, '🎤 打电话模式切换录音状态');
// 只有在打电话模式下才能使用此方法
if (!isPhoneCallMode.value) {
Logger.w(TAG, '不在打电话模式,无法切换录音状态');
return;
}
try {
if (isListening.value) {
// 当前正在录音,停止录音
Logger.i(TAG, '停止打电话模式录音');
final result = await AgentService.stopConversation();
// 手动更新状态,确保UI同步
if (result) {
isListening.value = false;
}
} else {
// 当前未录音,开始录音
Logger.i(TAG, '开始打电话模式录音');
final result = await AgentService.startConversation();
// 手动更新状态,确保UI同步
if (result) {
isListening.value = true;
}
}
} catch (e) {
Logger.e(TAG, '打电话模式录音状态切换失败: $e');
}
}
// 耳机唤醒通话模式(专用入口)

414
lib/modules/agent/views/agent_view.dart

@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:io';
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_intro/flutter_intro.dart';
import 'package:get/get.dart';
import '../controllers/agent_controller.dart';
import '../controllers/message_action_controller.dart';
@ -388,96 +389,110 @@ class _AgentViewState extends State<AgentView> with WidgetsBindingObserver {
if (controller.isTextInputMode.value) {
// 文本输入模式 - 限制最大高度并只向上扩展
return Container(
constraints: BoxConstraints(
minHeight: 44.h,
maxHeight: 120.h,
),
padding: EdgeInsets.symmetric(
horizontal: 16.w),
decoration: BoxDecoration(
color: isDarkMode
? Colors.grey[800]
: Colors.grey[200],
borderRadius:
BorderRadius.circular(22.w),
border: Border.all(
return IntroStepBuilder(
group: 'ai',
order: 1,
text: '对小言说“今天天气怎么样”',
onWidgetLoad: () {
controller.noviceGuideService
.startAIGuide();
},
builder: (BuildContext context,
GlobalKey key) =>
Container(
key: key,
constraints: BoxConstraints(
minHeight: 44.h,
maxHeight: 120.h,
),
padding: EdgeInsets.symmetric(
horizontal: 16.w),
decoration: BoxDecoration(
color: isDarkMode
? Colors.grey[800]!
: Colors.grey.withOpacity(0.1),
width: 1.w,
? Colors.grey[800]
: Colors.grey[200],
borderRadius:
BorderRadius.circular(22.w),
border: Border.all(
color: isDarkMode
? Colors.grey[800]!
: Colors.grey.withOpacity(0.1),
width: 1.w,
),
),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment
.end, // 对齐到底部,实现向上扩展
children: [
Expanded(
child: Obx(() {
// 图片模式下使用更高的输入框
final isImageActive = controller
.isImageInputActive.value;
child: Row(
crossAxisAlignment: CrossAxisAlignment
.end, // 对齐到底部,实现向上扩展
children: [
Expanded(
child: Obx(() {
// 图片模式下使用更高的输入框
final isImageActive = controller
.isImageInputActive.value;
return TextField(
controller:
controller.textController,
maxLines: isImageActive
? 3
: 5, // 设置合理的最大行数
minLines: isImageActive ? 2 : 1,
keyboardType: TextInputType
.multiline, // 启用多行键盘
textInputAction: TextInputAction
.newline, // 支持回车换行
onTap: () {
// 当点击输入框时,如果'+'选项已展开,则收缩
if (controller
.isPlusOptionsExpanded
.value) {
controller
.collapsePlusOptions();
}
},
style: TextStyle(
fontSize: 15.sp,
color: isDarkMode
? Colors.white
: Colors.grey[800],
),
decoration: InputDecoration(
hintText: isImageActive
? 'addQuestionAboutImage'
.tr // 添加对图片的问题或描述...
: 'inputMessage'
.tr, // 输入消息...
hintStyle: TextStyle(
color: Colors.grey[500],
return TextField(
controller:
controller.textController,
maxLines: isImageActive
? 3
: 5, // 设置合理的最大行数
minLines:
isImageActive ? 2 : 1,
keyboardType: TextInputType
.multiline, // 启用多行键盘
textInputAction:
TextInputAction
.newline, // 支持回车换行
onTap: () {
// 当点击输入框时,如果'+'选项已展开,则收缩
if (controller
.isPlusOptionsExpanded
.value) {
controller
.collapsePlusOptions();
}
},
style: TextStyle(
fontSize: 15.sp,
color: isDarkMode
? Colors.white
: Colors.grey[800],
),
filled: false,
border: InputBorder.none,
focusedBorder:
OutlineInputBorder(
borderSide: BorderSide(
color: isDarkMode
? Colors.grey[800]!
: Colors.grey[200]!,
width: 1.w,
decoration: InputDecoration(
hintText: isImageActive
? 'addQuestionAboutImage'
.tr // 添加对图片的问题或描述...
: 'inputMessage'
.tr, // 输入消息...
hintStyle: TextStyle(
color: Colors.grey[500],
fontSize: 15.sp,
),
filled: false,
border: InputBorder.none,
focusedBorder:
OutlineInputBorder(
borderSide: BorderSide(
color: isDarkMode
? Colors.grey[800]!
: Colors.grey[200]!,
width: 1.w,
),
),
contentPadding:
EdgeInsets.symmetric(
vertical:
isImageActive
? 12.h
: 8.h),
),
contentPadding:
EdgeInsets.symmetric(
vertical:
isImageActive
? 12.h
: 8.h),
),
onSubmitted: (_) =>
controller.sendMessage(),
);
}),
),
],
onSubmitted: (_) =>
controller.sendMessage(),
);
}),
),
],
),
),
);
} else {
@ -919,212 +934,60 @@ class _AgentViewState extends State<AgentView> with WidgetsBindingObserver {
Widget _buildPhoneCallOverlay(BuildContext context, bool isDarkMode) {
return Container(
color: Colors.transparent, // 设置整个覆盖层为透明
child: SafeArea(
child: Column(
children: [
// 顶部标题栏 - 保持背景
// Container(
// color: isDarkMode
// ? Colors.black.withOpacity(0.8)
// : Colors.white.withOpacity(0.8),
// child: _buildPhoneCallHeader(isDarkMode),
// ),
// 中间音频可视化区域 - 完全透明
const Expanded(
child: SizedBox(), // 空的占位符,完全透明
),
// 底部控制按钮 - 保持背景
Container(
color: isDarkMode ? Colors.black : Colors.white,
child: _buildPhoneCallControls(isDarkMode),
),
],
),
),
);
}
// 构建打电话模式标题栏
Widget _buildPhoneCallHeader(bool isDarkMode) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Row(
child: Column(
children: [
// 返回按钮
GestureDetector(
onTap: () => controller.endPhoneCall(),
child: Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: isDarkMode ? Colors.grey[800] : Colors.grey[200],
borderRadius: BorderRadius.circular(20),
),
child: Icon(
Icons.arrow_back,
color: isDarkMode ? Colors.white : Colors.black,
size: 20,
),
),
// 中间空白区域 - 完全透明
const Expanded(
child: SizedBox(), // 空的占位符,完全透明
),
const SizedBox(width: 16),
// 标题
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'aiVoiceCall'.tr, // AI语音通话
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: isDarkMode ? Colors.white : Colors.black,
),
),
Obx(() {
final status = controller.isListening.value
? 'inCall'.tr // 正在通话中...
: controller.isSpeaking.value
? 'aiReplying'.tr // AI正在回复...
: 'waitingToStart'.tr; // 等待开始...
return Text(
status,
style: TextStyle(
fontSize: 14,
color: isDarkMode ? Colors.grey[400] : Colors.grey[600],
),
);
}),
],
),
// 底部控制按钮 - 包含音频波纹
Container(
color: isDarkMode ? Colors.black : Colors.white,
child: _buildPhoneCallControls(isDarkMode),
),
],
),
);
}
// 构建打电话模式音频可视化
Widget _buildPhoneCallVisualization(bool isDarkMode) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// 音频可视化圆圈
Obx(() {
final isActive =
controller.isListening.value || controller.isSpeaking.value;
return AnimatedContainer(
duration: const Duration(milliseconds: 300),
width: isActive ? 200 : 160,
height: isActive ? 200 : 160,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: isDarkMode
? Colors.blue[900]?.withOpacity(0.3)
: Colors.blue[100],
border: Border.all(
color: Colors.blue,
width: isActive ? 3 : 2,
),
),
child: Icon(
Icons.phone,
size: isActive ? 80 : 60,
color: Colors.blue,
),
);
}),
const SizedBox(height: 32),
// 状态文字
Obx(() {
String statusText = 'clickToStartCall'.tr; // 点击开始通话
if (controller.isListening.value) {
statusText = 'listeningToYou'.tr; // 正在听您说话...
} else if (controller.isSpeaking.value) {
statusText = 'aiReplying'.tr; // AI正在回复...
} else if (controller.isProcessing.value) {
statusText = 'processing'.tr; // 正在处理...
}
return Text(
statusText,
style: TextStyle(
fontSize: 16,
color: isDarkMode ? Colors.grey[300] : Colors.grey[700],
fontWeight: FontWeight.w500,
),
);
}),
],
),
);
}
// 构建打电话模式控制按钮
Widget _buildPhoneCallControls(bool isDarkMode) {
return Container(
padding: EdgeInsets.symmetric(horizontal: 32.w, vertical: 24.h),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
crossAxisAlignment: CrossAxisAlignment.center, // 确保垂直居中对齐
children: [
// 静音按钮
// 录音控制按钮(左侧) - 控制录音输入功能
Obx(() => _buildPhoneControlButton(
icon: controller.isTtsEnabled.value
? Icons.volume_up
: Icons.volume_off,
onTap: () => controller.toggleTts(),
icon: controller.isListening.value
? Icons.mic // 录音中显示麦克风图标
: Icons.mic_off, // 未录音显示麦克风关闭图标
onTap: () => controller.togglePhoneCallRecording(),
isDarkMode: isDarkMode,
isActive: controller.isTtsEnabled.value,
isActive: controller.isListening.value,
// backgroundColor: controller.isListening.value
// ? Colors.red // 录音中显示红色背景
// : Theme.of(context).primaryColor, // 未录音显示主题色
)),
// 主要通话按钮
Obx(() {
final isActive = controller.isListening.value;
return GestureDetector(
// onTap: () {
// if (isActive) {
// controller.stopVoiceInput();
// } else {
// controller.startVoiceInput();
// }
// },
child: Container(
width: 80.w,
height: 80.h,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: isActive ? Colors.red : Colors.green,
boxShadow: [
BoxShadow(
color: (isActive ? Colors.red : Colors.green)
.withOpacity(0.3),
blurRadius: 12.w,
spreadRadius: 2.w,
),
],
),
child: Icon(
isActive ? Icons.call_end : Icons.call,
color: Colors.white,
size: 36.sp,
),
),
);
}),
// 音频波纹组件(中间) - 仅显示动态波形效果,无其他UI元素
Container(
width: 120.w, // 设置固定宽度
height: 64.h, // 与按钮高度一致
alignment: Alignment.center, // 确保波纹居中
child: const AgentSoundBarsWidget(barCount: 12), // 适应宽度的波纹条数
),
// 结束通话按钮
// 关闭打电话模式按钮(右侧) - 退出打电话模式
_buildPhoneControlButton(
icon: Icons.call_end,
icon: Icons.close,
onTap: () => controller.endPhoneCall(),
isDarkMode: isDarkMode,
isActive: false,
backgroundColor: Colors.red,
//backgroundColor: Colors.red,
iconColor: Colors.redAccent,
),
],
),
@ -1138,24 +1001,31 @@ class _AgentViewState extends State<AgentView> with WidgetsBindingObserver {
required bool isDarkMode,
required bool isActive,
Color? backgroundColor,
Color? iconColor,
}) {
return GestureDetector(
onTap: onTap,
child: Container(
width: 56.w,
height: 56.h,
width: 64.w, // 按钮尺寸
height: 64.h,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: backgroundColor ??
(isDarkMode ? Colors.grey[800] : Colors.grey[200]),
border: isActive ? Border.all(color: Colors.blue, width: 2.w) : null,
(isDarkMode ? Colors.grey[800] : Colors.grey[250]),
border: Border.all(color: Colors.blue, width: 2.w),
boxShadow: [
BoxShadow(
color: (backgroundColor ?? Colors.grey).withValues(alpha: 0.3),
blurRadius: 8.w,
spreadRadius: 1.w,
offset: Offset(0, 2.h),
),
],
),
child: Icon(
icon,
color: backgroundColor != null
? Colors.white
: (isDarkMode ? Colors.white : Colors.black),
size: 24.sp,
color: iconColor ?? (isDarkMode ? Colors.white : Colors.black),
size: 28.sp, // 图标尺寸
),
),
);

50
lib/modules/agent/widgets/sound_bar_widget.dart

@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../controllers/agent_controller.dart';
/// 声音柱动画组件
/// 声音柱动画组件 (已优化)
/// 用于显示语音输入时的音频波形效果
class SoundBarWidget extends StatefulWidget {
final double delay;
@ -37,9 +37,8 @@ class _SoundBarWidgetState extends State<SoundBarWidget>
parent: _controller,
curve: Curves.easeInOut,
),
)..addListener(() {
setState(() {});
});
);
// 移除了 .addListener(() { setState(() {}); })
_startOrStopAnimation();
}
@ -54,6 +53,9 @@ class _SoundBarWidgetState extends State<SoundBarWidget>
/// 开始或停止动画
void _startOrStopAnimation() {
// 确保组件仍然挂载在树上
if (!mounted) return;
if (widget.isActive) {
Future.delayed(Duration(milliseconds: (widget.delay * 1000).toInt()), () {
if (mounted && widget.isActive) {
@ -61,9 +63,10 @@ class _SoundBarWidgetState extends State<SoundBarWidget>
}
});
} else {
_controller.reset();
// 设置为小高度的静态状态
_controller.animateTo(0.1, duration: const Duration(milliseconds: 300));
// 停止并平滑地过渡到初始状态
_controller.stop();
_controller.animateTo(0.0,
duration: const Duration(milliseconds: 200), curve: Curves.easeOut);
}
}
@ -77,21 +80,28 @@ class _SoundBarWidgetState extends State<SoundBarWidget>
Widget build(BuildContext context) {
final primaryColor = Theme.of(context).primaryColor;
return Container(
width: 2.5,
height: widget.isActive ? _animation.value : 3,
margin: const EdgeInsets.symmetric(horizontal: 1),
decoration: BoxDecoration(
color: widget.isActive
? primaryColor.withOpacity(0.8)
: Colors.grey.withOpacity(0.4),
borderRadius: BorderRadius.circular(1.5),
),
// 使用 AnimatedBuilder 来优化性能
return AnimatedBuilder(
animation: _animation,
builder: (context, child) {
return Container(
width: 2.5,
// 当动画不活跃时,高度固定为3;活跃时使用动画值
height: widget.isActive ? _animation.value : 3,
margin: const EdgeInsets.symmetric(horizontal: 1),
decoration: BoxDecoration(
color: widget.isActive
? primaryColor.withOpacity(0.8)
: Colors.grey.withOpacity(0.4),
borderRadius: BorderRadius.circular(1.5),
),
);
},
);
}
}
/// 声音柱组合组件
/// 声音柱组合组件 (无需改动)
/// 包含多个声音柱的组合显示
class SoundBarsWidget extends StatelessWidget {
final bool isActive;
@ -106,7 +116,7 @@ class SoundBarsWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(barCount, (index) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 1),
@ -120,7 +130,7 @@ class SoundBarsWidget extends StatelessWidget {
}
}
/// 带控制器集成的声音柱组件
/// 带控制器集成的声音柱组件 (无需改动)
/// 自动从AgentController获取状态
class AgentSoundBarsWidget extends StatelessWidget {
final int barCount;

1
lib/modules/devices/controllers/devices_controller.dart

@ -57,6 +57,7 @@ class DevicesController extends GetxController {
User.instance.removeUserDevice(device.id);
_bleManager.clearPairedDevices();
_bleManager.disconnect();
await Future.delayed(const Duration(seconds: 1));
Get.toNamed(Routes.pairing);
return;
} else {

150
lib/modules/home/controllers/home_controller.dart

@ -1,13 +1,11 @@
import 'package:get/get.dart';
import 'dart:async';
import '../../../data/services/novice_guide_service.dart';
import '../../../routes/app_routes.dart';
import '../../../core/utils/logger.dart';
import 'package:flutter/material.dart';
import '../../../data/services/bluetooth_manager.dart';
import '../../../data/services/ble_manager.dart';
import '../../../data/services/version_update_service.dart';
import '../../common/widgets/version_update_dialog.dart';
import 'package:get_storage/get_storage.dart';
/// 连接状态枚举
enum ConnectionStatus {
@ -24,7 +22,7 @@ class HomeController extends GetxController {
final _isFullyConnected = false.obs;
String? _lastConnectedDeviceName;
bool get isFullyConnected => _isFullyConnected.value;
RxBool get isFullyConnectedRx => _isFullyConnected;
BluetoothDevice? get currentDevice => _bluetoothManager.currentDevice;
@ -53,14 +51,9 @@ class HomeController extends GetxController {
final _bluetoothManager = Get.find<BluetoothManager>();
final _bleManager = Get.find<BleManager>();
final _storage = GetStorage();
// 会话级别的版本检查标记(静态变量,整个应用会话期间保持)
static bool _hasCheckedVersionInSession = false;
final noviceGuideService = Get.find<NoviceGuideService>();
// 版本检查相关常量
static const String _lastVersionCheckKey = 'last_version_check_timestamp';
static const int _versionCheckIntervalHours = 24; // 24小时间隔
@override
void onInit() {
super.onInit();
@ -78,12 +71,6 @@ class HomeController extends GetxController {
void _initWithCachedInfo() {
_setupStateListeners();
_checkConnectionStatus();
// 延迟检查版本更新,避免影响页面加载
// 混合检查机制:会话检查 + 24小时间隔检查
Future.delayed(const Duration(seconds: 2), () {
_checkVersionUpdateIfNeeded();
});
}
void _setupStateListeners() {
@ -94,13 +81,14 @@ class HomeController extends GetxController {
update();
});
_classicBluetoothWorker = ever(_bluetoothManager.currentDeviceRx, (BluetoothDevice? device) {
_classicBluetoothWorker =
ever(_bluetoothManager.currentDeviceRx, (BluetoothDevice? device) {
if (device != null) {
_lastConnectedDeviceName = device.name;
}
_checkConnectionStatus();
});
_bleConnectionWorker = ever(_bleManager.connectionStateRx, (int state) {
_checkConnectionStatus();
});
@ -121,30 +109,30 @@ class HomeController extends GetxController {
void exploreAIFeatures() {
Get.toNamed(Routes.agent);
}
Future<void> _checkConnectionStatus() async {
try {
final currentBleState = await _bleManager.getConnectionState();
bool hasClassicBluetooth = currentDevice != null;
bool hasBleConnection = currentBleState == 2;
bool wasConnected = _isFullyConnected.value;
bool isNowConnected = hasClassicBluetooth && hasBleConnection;
if (isNowConnected && currentDevice == null) {
isNowConnected = false;
}
if (_isFullyConnected.value != isNowConnected) {
_isFullyConnected.value = isNowConnected;
_isFullyConnected.refresh();
update();
if (!wasConnected && isNowConnected) {
await _bleManager.fetchDeviceInfo();
}
}
update();
} catch (e) {
Logger.e(_tag, '${'checkConnectionStatusFailed'.tr}: $e');
@ -165,26 +153,28 @@ class HomeController extends GetxController {
}
bool get shouldShowDeviceInfo => currentDevice != null;
String getDeviceName() {
if (_isFullyConnected.value && currentDevice == null) {
_checkConnectionStatus();
}
final deviceName = currentDevice?.name;
if (deviceName != null && deviceName.isNotEmpty) {
return deviceName;
}
final bleDeviceName = deviceInfo.value?.name;
if (bleDeviceName != null && bleDeviceName.isNotEmpty) {
return bleDeviceName;
}
if (!_isFullyConnected.value && _lastConnectedDeviceName != null && _lastConnectedDeviceName!.isNotEmpty) {
if (!_isFullyConnected.value &&
_lastConnectedDeviceName != null &&
_lastConnectedDeviceName!.isNotEmpty) {
return _lastConnectedDeviceName!;
}
return 'unknownEquipment'.tr;
}
@ -270,104 +260,4 @@ class HomeController extends GetxController {
? deviceInfo.value!.firmwareVersion
: 'unknown'.tr;
}
/// 混合检查机制:判断是否需要进行版本检查
/// 检查条件:1. 会话内未检查过 OR 2. 距离上次检查超过24小时
void _checkVersionUpdateIfNeeded() {
try {
final now = DateTime.now();
final lastCheckTimestamp = _storage.read(_lastVersionCheckKey);
bool shouldCheck = false;
String reason = '';
// 条件1:会话内未检查过
if (!_hasCheckedVersionInSession) {
shouldCheck = true;
reason = '会话内首次检查';
}
// 条件2:距离上次检查超过24小时
else if (lastCheckTimestamp != null) {
final lastCheckTime = DateTime.fromMillisecondsSinceEpoch(lastCheckTimestamp);
final timeDifference = now.difference(lastCheckTime);
if (timeDifference.inHours >= _versionCheckIntervalHours) {
shouldCheck = true;
reason = '距离上次检查已超过${_versionCheckIntervalHours}小时';
}
}
// 条件3:从未检查过(首次使用应用)
else {
shouldCheck = true;
reason = '首次使用应用';
}
if (shouldCheck) {
Logger.i(_tag, '开始版本检查 - 原因: $reason');
_performVersionCheck();
} else {
Logger.i(_tag, '跳过版本检查 - 会话内已检查且未超过时间间隔');
}
} catch (e) {
Logger.e(_tag, '版本检查判断失败: $e');
}
}
/// 执行版本检查(在后台进行,不阻塞启动流程)
Future<void> _performVersionCheck() async {
try {
final versionService = VersionUpdateService.to;
final updateInfo = await versionService.checkForUpdate();
if (updateInfo.hasUpdate) {
Logger.i(_tag, '发现新版本: ${updateInfo.latestVersion}');
// 显示更新对话框
await VersionUpdateDialog.show(updateInfo);
} else {
Logger.i(_tag, '当前已是最新版本: ${updateInfo.currentVersion}');
}
// 更新检查记录
_updateVersionCheckRecord();
} catch (e) {
Logger.e(_tag, '版本检查失败: $e');
// 即使检查失败,也更新记录,避免频繁重试
_updateVersionCheckRecord();
}
}
/// 更新版本检查记录
void _updateVersionCheckRecord() {
try {
// 更新时间戳
final now = DateTime.now();
_storage.write(_lastVersionCheckKey, now.millisecondsSinceEpoch);
// 标记本次会话已检查过版本
_hasCheckedVersionInSession = true;
Logger.i(_tag, '版本检查记录已更新: ${now.toIso8601String()}');
} catch (e) {
Logger.e(_tag, '更新版本检查记录失败: $e');
}
}
/// 重置版本检查标记(可用于强制重新检查)
/// 注意:这会清除会话标记,但不会清除时间戳记录
static void resetVersionCheckFlag() {
_hasCheckedVersionInSession = false;
Logger.i('HomeCtrl', '版本检查会话标记已重置');
}
/// 完全重置版本检查状态(包括时间戳)
/// 用于测试或特殊情况下强制重新检查
void forceResetVersionCheck() {
try {
_hasCheckedVersionInSession = false;
_storage.remove(_lastVersionCheckKey);
Logger.i(_tag, '版本检查状态已完全重置');
} catch (e) {
Logger.e(_tag, '重置版本检查状态失败: $e');
}
}
}

69
lib/modules/home/views/home_view.dart

@ -1,8 +1,8 @@
import 'package:flutter/material.dart';
import 'package:flutter_intro/flutter_intro.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../controllers/home_controller.dart';
import '../../../routes/app_pages.dart';
import '../../../routes/app_routes.dart';
import '../../../core/widgets/common_bottom_nav.dart';
@ -84,11 +84,30 @@ class HomeView extends GetView<HomeController> {
_buildAppBar(isDarkMode),
// 顶部图片
_buildEarbudsImage(),
IntroStepBuilder(
group: 'home',
order: 1,
text: '欢迎使用Voitrans!首先,让我们唤醒你的专属 AI—— 小言',
onWidgetLoad: () {
controller.noviceGuideService.startHomeGuide();
},
builder: (BuildContext context, GlobalKey key) => SizedBox(
key: key,
child: _buildEarbudsImage(),
),
),
SizedBox(height: 20.h),
// 设备状态卡片
_buildDeviceStatusCard(isDarkMode),
IntroStepBuilder(
group: 'home',
order: 2,
text: '对着麦克风说“小言小言”',
builder: (BuildContext context, GlobalKey key) => SizedBox(
key: key,
child: _buildDeviceStatusCard(isDarkMode),
),
),
SizedBox(height: 20.h),
// 热门AI体验
@ -623,23 +642,33 @@ class HomeView extends GetView<HomeController> {
// 左侧大卡片 - AI助理
Expanded(
flex: 1,
child: _buildFeatureCard(
title: 'aiAssistant'.tr, // AI全能助手
description: 'aiAssistantInfo'.tr, // 24小时专属顾问,解答问题、提供建议
icon: Icons.assistant,
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Color(0xFF0066CC).withOpacity(0.3),
Color(0xFF5E5CE6).withOpacity(0.3),
],
child: IntroStepBuilder(
group: 'home',
order: 3,
text: '太棒了!你已成功唤醒小言,接下来试试让它帮你做件事吧',
onHighlightWidgetTap: () {
Intro.of(Get.context!).dispose();
controller.exploreAIFeatures();
},
builder: (BuildContext context, GlobalKey key) => _buildFeatureCard(
key: key,
title: 'aiAssistant'.tr, // AI全能助手
description: 'aiAssistantInfo'.tr, // 24小时专属顾问,解答问题、提供建议
icon: Icons.assistant,
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Color(0xFF0066CC).withOpacity(0.3),
Color(0xFF5E5CE6).withOpacity(0.3),
],
),
onTap: controller.exploreAIFeatures,
iconOnTop: true,
height: 175.h, // 增加高度
isDarkMode: isDarkMode,
iconColor: const Color(0xFF0066CC),
),
onTap: controller.exploreAIFeatures,
iconOnTop: true,
height: 175.h, // 增加高度
isDarkMode: isDarkMode,
iconColor: const Color(0xFF0066CC),
),
),
@ -702,6 +731,7 @@ class HomeView extends GetView<HomeController> {
// 特性卡片构建方法
Widget _buildFeatureCard({
GlobalKey? key,
required String title,
required String description,
required IconData icon,
@ -713,6 +743,7 @@ class HomeView extends GetView<HomeController> {
Color iconColor = Colors.white,
}) {
return GestureDetector(
key: key,
onTap: onTap,
child: Container(
height: height ?? 175.h,

238
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';
@ -46,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();
@ -102,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<BluetoothManager>();
_bleManager = Get.find<BleManager>();
@ -125,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();
@ -137,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<void> _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');
}
}
// ==================== 监听器设置 ====================
/// 集中设置所有监听器
@ -202,7 +312,7 @@ class PairingController extends GetxController {
case PairingState.waitingClassic:
if (device != null) {
// 有设备连接,进入验证阶段
Logger.i(_tag, '检测到经典蓝牙设备连接,开始验证');
Logger.d(_tag, '检测到经典蓝牙设备连接,开始验证');
_transition(PairingState.verifying);
_verifyDevice(device);
}
@ -623,7 +733,7 @@ class PairingController extends GetxController {
);
product = match.isNotEmpty ? match.first : null;
}
final currentBleState = await _bleManager.getConnectionState();
final names = AppConfig.allproductNames();
// 检查是否已有绑定的设备
print("device.address${device.address}");
@ -645,6 +755,17 @@ class PairingController extends GetxController {
// 已经绑定
return 9;
}
} else {
//未绑定设备
Logger.i(_tag, '验证时BLE状态: $currentBleState');
if (currentBleState == 2) {
//已连接 清理
// BleConnectionState.connected
Logger.i(_tag, 'BLE已连接,断开并清理缓存');
_bleManager.clearPairedDevices();
_bleManager.disconnect();
}
}
if (Platform.isAndroid) {
@ -672,11 +793,15 @@ class PairingController extends GetxController {
print(
'获取到 deviceName: $deviceName address: ${device.address} names:$names');
if (names.contains(deviceName)) {
if (currentBleState == 2) {
//已连接直接返回
return 9;
}
await _bleManager.startScan(deviceName);
await Future.delayed(const Duration(seconds: 1));
// 尝试最多读取 3 次设备信息,每次间隔 1 秒
List<Map<String, String>>? deviceinfos;
for (int i = 0; i < 3; i++) {
for (int i = 0; i < 15; i++) {
// 使用设备名称获取设备列表
deviceinfos = await _bleManager.getDeviceList(deviceName);
print('获取到 deviceinfos: $deviceinfos');
@ -920,12 +1045,12 @@ class PairingController extends GetxController {
case ErrorType.bluetoothOff:
// 引导用户去开启蓝牙
Logger.i(_tag, '引导用户开启蓝牙');
AppSettings.openAppSettings(type: AppSettingsType.bluetooth);
_openBluetoothSettings();
break;
case ErrorType.noClassicDevice:
// 引导用户去蓝牙设置连接设备
Logger.i(_tag, '引导用户去蓝牙设置');
AppSettings.openAppSettings(type: AppSettingsType.bluetooth);
_openBluetoothSettings();
break;
default:
// 其他错误情况,刷新并重试
@ -936,7 +1061,7 @@ class PairingController extends GetxController {
} else if (_currentState.value == PairingState.waitingClassic) {
// 等待经典蓝牙连接状态,引导去设置
Logger.i(_tag, '等待经典蓝牙连接,引导用户去蓝牙设置');
AppSettings.openAppSettings(type: AppSettingsType.bluetooth);
_openBluetoothSettings();
} else {
// 其他情况,刷新设备连接
_bluetoothManager.refreshDeviceConnection();
@ -994,7 +1119,98 @@ class PairingController extends GetxController {
/// 打开蓝牙设置
void openBluetoothSettings() {
AppSettings.openAppSettings(type: AppSettingsType.bluetooth);
_openBluetoothSettings();
}
/// 打开蓝牙设置 - 简化版本
void _openBluetoothSettings() async {
if (Platform.isIOS) {
// iOS平台:直接显示用户指导对话框
_showBluetoothGuideDialog();
} else {
// Android平台使用原有方式
AppSettings.openAppSettings(type: AppSettingsType.bluetooth);
}
}
/// 显示蓝牙设置指导对话框
void _showBluetoothGuideDialog() {
Get.dialog(
AlertDialog(
title: Text('bluetoothGuideTitle'.tr),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('bluetoothGuideSteps'.tr),
SizedBox(height: 16.h),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('1. ',
style: TextStyle(
fontWeight: FontWeight.bold, fontSize: 14.sp)),
Expanded(
child: Text('bluetoothGuideStep1'.tr,
style: TextStyle(fontSize: 14.sp))),
],
),
SizedBox(height: 8.h),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('2. ',
style: TextStyle(
fontWeight: FontWeight.bold, fontSize: 14.sp)),
Expanded(
child: Text('bluetoothGuideStep2'.tr,
style: TextStyle(fontSize: 14.sp))),
],
),
SizedBox(height: 8.h),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('3. ',
style: TextStyle(
fontWeight: FontWeight.bold, fontSize: 14.sp)),
Expanded(
child: Text('bluetoothGuideStep3'.tr,
style: TextStyle(fontSize: 14.sp))),
],
),
SizedBox(height: 16.h),
Container(
padding: EdgeInsets.all(12.w),
decoration: BoxDecoration(
color: Colors.blue.withOpacity(0.1),
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.sp),
SizedBox(width: 8.w),
Expanded(
child: Text(
'bluetoothGuideTip'.tr,
style:
TextStyle(fontSize: 12.sp, color: Colors.blue[700]),
),
),
],
),
),
],
),
actions: [
TextButton(
onPressed: () => Get.back(),
child: Text('iKnow'.tr, style: TextStyle(fontSize: 14.sp)),
),
],
),
);
}
/// 清除设备关联(在需要重新配对时调用)

97
lib/modules/settings/views/settings_view.dart

@ -407,8 +407,6 @@ class SettingsView extends GetView<SettingsController> {
? Colors.white.withOpacity(0.1)
: Colors.grey[200]),
// 用户反馈
_buildSimpleNavigationSetting(
title: 'feedback'.tr, // 用户反馈
@ -427,29 +425,33 @@ class SettingsView extends GetView<SettingsController> {
},
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<SettingsController> {
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,
),
]
],
),
),

293
lib/modules/splash/controllers/splash_controller.dart

@ -14,17 +14,26 @@ import '../../../data/services/network/api.dart';
import '../../../data/services/version_update_service.dart';
import '../../../modules/common/widgets/version_update_dialog.dart';
import '../../../modules/common/widgets/privacy_policy_dialog.dart';
import '../../common/webview_page.dart';
import '../../../core/utils/logger.dart';
class SplashController extends GetxController {
final GetStorage _storage = GetStorage();
static const String _tag = 'SplashCtrl';
// 进度条动画控制
final _loadingProgress = 0.0.obs;
double get loadingProgress => _loadingProgress.value;
// 资源加载状态
final _isResourceLoaded = false.obs;
bool get isResourceLoaded => _isResourceLoaded.value;
// 各项任务完成状态
final _isTrackingPermissionRequested = false.obs; // 追踪权限请求完成
final _isVersionCheckCompleted = false.obs; // 版本检查完成
final _isTokenValidated = false.obs; // Token验证完成
final _isAppConfigLoaded = false.obs; // 应用配置加载完成
final _isAnimationCompleted = false.obs; // 动画完成
// 登录状态
final _isLoggedIn = false.obs;
bool get isLoggedIn => _isLoggedIn.value;
final _hasAcceptedPrivacy = false.obs; //是否同意隐私政策
@ -34,6 +43,13 @@ class SplashController extends GetxController {
// 主题服务
final ThemeService _themeService = ThemeService();
// 会话级别的版本检查标记(静态变量,整个应用会话期间保持)
static bool _hasCheckedVersionInSession = false;
// 版本检查相关常量
static const String _lastVersionCheckKey = 'last_version_check_timestamp';
static const int _versionCheckIntervalHours = 24; // 24小时间隔
@override
void onInit() {
super.onInit();
@ -50,22 +66,51 @@ class SplashController extends GetxController {
}
});
//弹出用户追中问题
WidgetsFlutterBinding.ensureInitialized().addPostFrameCallback((_) async {
final status =
await AppTrackingTransparency.requestTrackingAuthorization();
});
// 注意:追踪权限请求将在用户同意隐私政策后执行
}
/// 异步请求追踪权限
Future<void> _requestTrackingPermissionAsync() async {
try {
await AppTrackingTransparency.requestTrackingAuthorization();
Logger.d(_tag, '用户追踪权限请求完成');
} catch (e) {
Logger.e(_tag, '用户追踪权限请求失败: $e');
} finally {
_isTrackingPermissionRequested.value = true;
}
}
void _startNormalFlow() {
_startLoadingAnimation();
_loadAppResources();
_startParallelTasks();
}
/// 启动并行任务:在1.5秒动画期间同时执行多个任务
void _startParallelTasks() {
// 并行执行所有任务
_requestTrackingPermissionAsync();
_performVersionCheckAsync();
_validateTokenAsync();
}
/// 异步执行版本检查
Future<void> _performVersionCheckAsync() async {
try {
Logger.d(_tag, '开始执行版本检查');
await _checkVersionUpdateIfNeeded();
} catch (e) {
Logger.e(_tag, '版本检查失败: $e');
}
//finally {
// _isVersionCheckCompleted.value = true;
// }
}
void _onPrivacyAccepted() {
_storage.write('hasAcceptedPrivacy', true);
Get.back(); // 关闭弹窗
_startNormalFlow(); // 开始正常流程
_startNormalFlow(); // 开始正常流程(已包含版本检查)
}
void _onPrivacyRejected() {
@ -84,22 +129,10 @@ class SplashController extends GetxController {
);
}
// 初始化地图SDK
// void _initMapSDK() {
// try {
// AmapFlutter().initSdk(
// iosKey: "a4a1394fe817c2f86a424b897b4a9af4",
// androidKey: "78e7946dd602778df82c6ae143a88184",
// );
// } catch (e) {
// print('地图SDK初始化失败: $e');
// }
// }
// 进度条动画(持续2秒)
// 进度条动画(持续1.5秒)
void _startLoadingAnimation() {
const totalDuration = 1500; // 动画总时长增加到1.5秒
const steps = totalDuration ~/ 30; // 30的间隔,更平滑的动画
const totalDuration = 1500; // 动画总时长1.5秒
const steps = totalDuration ~/ 30; // 30ms间隔,更平滑的动画
const stepValue = 1.0 / steps;
Timer.periodic(const Duration(milliseconds: 30), (timer) {
@ -107,66 +140,66 @@ class SplashController extends GetxController {
_loadingProgress.value += stepValue;
} else {
timer.cancel();
_isAnimationCompleted.value = true;
_checkNavigateCondition();
}
});
}
// 异步加载应用资源
Future<void> _loadAppResources() async {
/// 异步验证Token和加载应用配置
Future<void> _validateTokenAsync() async {
try {
// 模拟资源加载(如API配置、本地缓存等)
await Future.delayed(const Duration(milliseconds: 1500));
bool isLoggedIn = false;
final token = _storage.read('logintoken');
Logger.d(_tag, 'Token验证开始: $token');
// 实际资源加载代码
// await _loadImages();
// await _initDatabase();
// 检查版本更新(在后台进行,不阻塞启动流程)
//_checkVersionUpdate();
if (token != null && token.isNotEmpty) {
isLoggedIn = await getLoginToken();
if (isLoggedIn) {
await getAppConfig();
_isAppConfigLoaded.value = true;
}
}
_isResourceLoaded.value = true;
_checkNavigateCondition();
_isLoggedIn.value = isLoggedIn;
Logger.d(_tag, 'Token验证完成,登录状态: $isLoggedIn');
} catch (e) {
Get.snackbar('错误', '资源加载失败,请检查网络连接');
_isResourceLoaded.value = true; // 允许用户继续使用
print('资源加载错误: $e');
Logger.e(_tag, 'Token验证失败: $e');
_isLoggedIn.value = false;
} finally {
_isTokenValidated.value = true;
}
}
// 检查跳转条件
// 检查跳转条件:所有任务都完成后才跳转
void _checkNavigateCondition() {
if (_loadingProgress.value >= 1.0 && _isResourceLoaded.value) {
_navigateToLogin();
if (_isAnimationCompleted.value &&
_isTrackingPermissionRequested.value &&
_isVersionCheckCompleted.value &&
_isTokenValidated.value) {
_navigateToTargetPage();
}
}
// 获取登录Token并跳转
Future<void> _navigateToLogin() async {
bool isLoggedIn = false;
final token = _storage.read('logintoken');
print('getLoginToken:进来了${token}');
if (token != null && token.isNotEmpty) {
isLoggedIn = await getLoginToken();
}
if (isLoggedIn) {
await getAppConfig();
// 根据登录状态跳转到目标页面
void _navigateToTargetPage() {
if (_isLoggedIn.value && _isAppConfigLoaded.value) {
Get.offAllNamed(Routes.pairing);
} else {
Get.offAllNamed(Routes.login); // 跳转到登录页
Get.offAllNamed(Routes.login);
}
}
Future<bool> getLoginToken() async {
try {
print('getLoginToken:进来了');
Logger.d(_tag, 'getLoginToken:进来了');
//if (User.getname() != "") {
final response = await Api.tokenlogin();
print('getLoginToken: $response');
Logger.d(_tag, 'getLoginToken: $response');
if (response == null) return false;
if (response['user'] != '') {
print('getLoginToken:进来了');
Logger.d(_tag, 'getLoginToken:进来了');
// 根据API设计调整
final user = User.fromJson(response);
@ -178,21 +211,21 @@ class SplashController extends GetxController {
//}
return false;
} catch (e) {
print('Token获取失败: $e');
Logger.e(_tag, 'Token获取失败: $e');
return false;
}
}
Future<bool> getAppConfig() async {
try {
print('getAppConfig:进来了');
Logger.d(_tag, 'getAppConfig:进来了');
//if (User.getname() != "") {
final response = await Api.getappconfig();
print('getAppConfig: $response');
Logger.d(_tag, 'getAppConfig: $response');
if (response == null) return false;
// 根据API设计调整
final resp = UserGetAppConfigResp.fromJson(response);
print('getAppConfig:进来了');
Logger.d(_tag, 'getAppConfig:进来了');
// 存储用户信息(示例)
// _storage.write('ENV', resp.env);
// _storage.write('AGENTS', resp.agents);
@ -200,27 +233,125 @@ class SplashController extends GetxController {
AppConfig.initialize(resp);
return true;
} catch (e) {
print('getAppConfig 取失败: $e');
Logger.e(_tag, 'getAppConfig 取失败: $e');
return false;
}
}
/// 检查版本更新(后台执行,不阻塞启动流程)
// void _checkVersionUpdate() {
// // 延迟执行版本检查,避免影响启动速度
// Timer(const Duration(seconds: 1), () async {
// try {
// final versionService = VersionUpdateService.to;
// final updateInfo = await versionService.checkForUpdate();
// if (updateInfo.hasUpdate) {
// // 显示更新对话框
// await VersionUpdateDialog.show(updateInfo);
// }
// } catch (e) {
// print('版本检查失败: $e');
// // 版本检查失败不影响用户使用,静默处理
// }
// });
// }
/// 混合检查机制:判断是否需要进行版本检查
/// 检查条件:1. 会话内未检查过 OR 2. 距离上次检查超过24小时
Future<void> _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<void> _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');
}
}
}

14
local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt

@ -815,16 +815,11 @@ object AgentService : CoroutineScope {
// 先添加系统提示词
if (systemPrompt.isNotEmpty()) {
// val location = LocationService.getCachedLocation() ?: mapOf(
// "user_location" to "定位未开启",
// "user_lat" to "定位未开启",
// "user_lng" to "定位未开启",
// )
val mapdata = UserPortrait.getAll()
Log.d(TAG, "liwei--------- 用户画像: ${mapdata}")
// Log.d(TAG, "liwei--------- 用户画像: ${mapdata}")
val _systemPrompt = Utils.fillTemplate(systemPrompt, mapdata)
messagesWithSystemPrompt.put(createSystemMessage(_systemPrompt))
Log.d(TAG, "liwei--------- 系统提示词: ${_systemPrompt}")
// Log.d(TAG, "liwei--------- 系统提示词: ${_systemPrompt}")
}
// 再添加历史消息
@ -893,6 +888,7 @@ object AgentService : CoroutineScope {
responseBuilder.append(token)
if (speakResponse && reply && broadcast) {
ttsService?.speakStream(token)
// audioPlayer?.stopAudio()
}
if (reply && token.isNotEmpty()) {
// 发送流式回复token
@ -905,10 +901,12 @@ object AgentService : CoroutineScope {
override fun onComplete(sessionid: String) {
try {
if (sessionid == currsessionId) {
audioPlayer?.stopAudio()
}
// 视情况决定是否朗读回复
if (speakResponse && reply && broadcast && sessionid == currsessionId) {
ttsService?.flushStream()
audioPlayer?.stopAudio()
}
val response = responseBuilder.toString()
// 发送完整回复,包含是否有图片的标记

52
local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift

@ -81,6 +81,7 @@ class AgentServiceImpl: NSObject {
// chatStorageHelper 现在是单例,不需要初始化
UserPortrait.shared.initialize()
BleService.shared.setDelegate(self)
AudioSessionHub.initialize()
}
func addListener(_ listener: AgentServiceListener) {
@ -500,6 +501,7 @@ audioStream.saveAudioDataTo(data: audioData)
os_log("设置回调并调用sendMessageStream", log: logger, type: .info)
chatApiService.setStreamCallback(currsessionId,callback)
chatApiService.sendMessageStream(currsessionId,messages: messages)
audioPlayer?.playAwaitSound()
os_log("sendMessageStream调用完成", log: logger, type: .info)
}
@ -994,6 +996,7 @@ class ChatApiStreamCallback: StreamCallback {
if speakResponse && reply && broadcast{
try agentService.azureTtsHelper?.speakStream(token)
// try agentService.audioPlayer?.stopAwaitSound()
}
if (reply && token != "") {
try agentService.sendEvent(name: "assistant_token", data: ["token": token])
@ -1007,6 +1010,12 @@ class ChatApiStreamCallback: StreamCallback {
do {
guard let agentService = try agentService else { return }
if sessionId == agentService.currsessionId {
// 停止等待音效
agentService.audioPlayer?.stopAwaitSound()
}
if speakResponse && reply && broadcast && sessionId == agentService.currsessionId{
agentService.azureTtsHelper?.flushStream()
}
@ -1045,6 +1054,11 @@ class ChatApiStreamCallback: StreamCallback {
do {
guard let agentService = try agentService else { return }
if sessionId == agentService.currsessionId {
// 停止等待音效
agentService.audioPlayer?.stopAwaitSound()
}
os_log("ChatAPI错误: %{public}@", log: agentService.logger, type: .error, error.localizedDescription)
if (sessionId != agentService.currsessionId) {
return
@ -1128,7 +1142,7 @@ extension AgentServiceImpl: TtsEventListener {
isSpeaking = true
restartIdleCheck()
sendEvent(name: "tts_started", data: ["status": "started"])
audioPlayer?.stopAwaitSound()
case .synthesisCompleted:
isSpeaking = false
restartIdleCheck()
@ -1204,6 +1218,7 @@ extension AgentServiceImpl: BleService.Callback {
class AudioPlayer {
private var player: AVAudioPlayer?
private var callingPlayer: AVAudioPlayer?
private var awaitPlayer: AVAudioPlayer? // 新增:等待音效播放器
private let logger = OSLog(subsystem: "com.yunqiinnovation.agent_service", category: "AudioPlayer")
private let audioQueue = DispatchQueue(label: "agent.audio.player", qos: .userInitiated)
@ -1229,19 +1244,28 @@ class AudioPlayer {
func playStartSound() {
playSound(named: "start", fileType: "mp3")
}
func playAwaitSound() {
playLoopSound(named: "await", fileType: "mp3", player: &awaitPlayer)
}
func playStopSound() {
playSound(named: "stop", fileType: "mp3")
}
func playCallingSound() {
playLoopSound(named: "calling", fileType: "mp3")
playLoopSound(named: "calling", fileType: "mp3", player: &callingPlayer)
}
func stopCallingSound() {
callingPlayer?.stop()
callingPlayer = nil
}
func stopAwaitSound() {
awaitPlayer?.stop()
awaitPlayer = nil
}
private func playSound(named: String, fileType: String = "mp3") {
if let path = resourcesBundle.path(forResource: named, ofType: fileType) {
@ -1254,12 +1278,14 @@ class AudioPlayer {
os_log("未找到音频文件: %{public}@.%{public}@ in bundle %{public}@", log: logger, type: .error, named, fileType, resourcesBundle.bundlePath)
}
private func playLoopSound(named: String, fileType: String = "mp3") {
stopCallingSound()
private func playLoopSound(named: String, fileType: String = "mp3", player: inout AVAudioPlayer?) {
// 停止当前播放的循环音频
player?.stop()
player = nil
if let path = resourcesBundle.path(forResource: named, ofType: fileType) {
let url = URL(fileURLWithPath: path)
safePlayLoopAudio(url: url)
safePlayLoopAudio(url: url, player: &player)
return
}
@ -1293,19 +1319,19 @@ class AudioPlayer {
}
}
private func safePlayLoopAudio(url: URL) {
private func safePlayLoopAudio(url: URL, player: inout AVAudioPlayer?) {
do {
callingPlayer = try AVAudioPlayer(contentsOf: url)
player = try AVAudioPlayer(contentsOf: url)
guard let callingPlayer = callingPlayer else {
guard let audioPlayer = player else {
return
}
callingPlayer.numberOfLoops = -1
callingPlayer.volume = 0.5
callingPlayer.prepareToPlay()
audioPlayer.numberOfLoops = -1 // 无限循环
audioPlayer.volume = 0.5
audioPlayer.prepareToPlay()
let playResult = callingPlayer.play()
let playResult = audioPlayer.play()
} catch {
os_log("播放循环音频失败: %{public}@", log: logger, type: .error, error.localizedDescription)

6
local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServicePlugin.swift

@ -98,6 +98,10 @@ public class AgentServicePlugin: NSObject, FlutterPlugin {
let success = impl.interruptCurrentResponse()
result(success)
case "stopAiSteam":
let success = impl.interruptCurrentResponse()
result(success)
case "dispose":
let success = impl.dispose()
result(success)
@ -172,4 +176,4 @@ extension AgentServiceEventHandler: AgentServiceListener {
}
}
}
}
}

230
local_plugins/agent_service/ios/agent_service/Sources/agent_service/AudioSessionHub.swift

@ -1,5 +1,7 @@
import AVFoundation
import os.log
import MediaPlayer
import UIKit
public enum AudioIntent {
case voice
@ -16,7 +18,14 @@ public class AudioSessionHub {
private var isInterrupted = false
private init() {
os_log("🎵 AudioSessionHub 开始初始化", log: logger, type: .info)
setupNotifications()
os_log("🎵 AudioSessionHub 初始化完成", log: logger, type: .info)
}
public static func initialize() {
os_log("🎵 主动初始化 AudioSessionHub", log: shared.logger, type: .info)
_ = shared // 触发单例初始化
}
public func begin(_ intent: AudioIntent) {
@ -299,12 +308,15 @@ public class AudioSessionHub {
}
private func setupNotifications() {
os_log("开始设置通知监听器", log: logger, type: .info)
NotificationCenter.default.addObserver(
self,
selector: #selector(handleAudioSessionInterruption),
name: AVAudioSession.interruptionNotification,
object: nil
)
os_log("已注册音频会话中断监听器", log: logger, type: .info)
NotificationCenter.default.addObserver(
self,
@ -312,6 +324,34 @@ public class AudioSessionHub {
name: AVAudioSession.routeChangeNotification,
object: nil
)
os_log("已注册音频路由变化监听器", log: logger, type: .info)
// 添加音频会话控制通知监听
NotificationCenter.default.addObserver(
self,
selector: #selector(handleAudioSessionBeginPlayback),
name: Notification.Name("AudioSessionBeginPlayback"),
object: nil
)
os_log("已注册播放音频会话开始监听器", log: logger, type: .info)
NotificationCenter.default.addObserver(
self,
selector: #selector(handleAudioSessionBeginVoice),
name: Notification.Name("AudioSessionBeginVoice"),
object: nil
)
os_log("已注册语音音频会话开始监听器", log: logger, type: .info)
NotificationCenter.default.addObserver(
self,
selector: #selector(handleVolumeControlRequest),
name: Notification.Name("VolumeControlRequest"),
object: nil
)
os_log("已注册音量控制请求监听器", log: logger, type: .info)
os_log("所有通知监听器设置完成", log: logger, type: .info)
}
/// 处理音频中断
@ -431,7 +471,195 @@ public class AudioSessionHub {
}
}
/// 处理播放音频会话激活请求
@objc private func handleAudioSessionBeginPlayback(notification: Notification) {
os_log("收到播放音频会话激活请求", log: logger, type: .info)
begin(.playback)
}
/// 处理语音音频会话激活请求
@objc private func handleAudioSessionBeginVoice(notification: Notification) {
os_log("收到语音音频会话激活请求", log: logger, type: .info)
begin(.voice)
}
/// 处理音量控制请求
@objc private func handleVolumeControlRequest(notification: Notification) {
os_log("🔊 收到音量控制请求通知", log: logger, type: .info)
os_log("收到音量控制请求", log: logger, type: .info)
guard let volumeInfo = notification.object as? [String: Any],
let actionString = volumeInfo["action"] as? String else {
os_log("❌ 音量控制请求数据格式错误", log: logger, type: .error)
return
}
// 将字符串转换为 VolumeControlAction
let action: VolumeControlAction
switch actionString {
case "increase":
action = .increase
case "decrease":
action = .decrease
case "mute":
action = .mute
case "setMax":
action = .setMax
default:
os_log("未知的音量控制动作: %{public}@", log: logger, type: .error, actionString)
return
}
// 获取 value(如果有的话)
let value: Float?
if let valueObj = volumeInfo["value"], !(valueObj is NSNull) {
value = valueObj as? Float
} else {
value = nil
}
os_log("音量控制详情: %{public}@", log: logger, type: .info, actionString)
// 在这里集中处理所有音量控制逻辑
performVolumeControl(action: action, value: value)
}
private func performVolumeControl(action: VolumeControlAction, value: Float?) {
DispatchQueue.main.async {
self.performVolumeControlWithMPVolumeView(action: action, value: value) { success, message in
os_log("音量控制结果: %{public}@ - %{public}@", log: self.logger, type: .info,
success ? "成功" : "失败", message)
// 发送结果通知
let resultInfo = VolumeControlResult(success: success, message: message, action: action)
NotificationCenter.default.post(
name: Notification.Name("VolumeControlResult"),
object: resultInfo
)
}
}
}
private func performVolumeControlWithMPVolumeView(action: VolumeControlAction, value: Float?, completion: @escaping (Bool, String) -> Void) {
// 创建更大的 frame,有助于子视图正确初始化
let volumeView = MPVolumeView(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
volumeView.showsVolumeSlider = true
volumeView.showsRouteButton = false
volumeView.alpha = 0.01 // 几乎透明
guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let window = windowScene.windows.first else {
completion(false, "无法获取窗口")
return
}
window.addSubview(volumeView)
volumeView.setNeedsLayout()
volumeView.layoutIfNeeded()
var retryCount = 0
let maxRetries = 5
func attemptVolumeControl() {
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
os_log("尝试第 %d 次音量控制", log: self.logger, type: .debug, retryCount + 1)
// 递归查找滑块
func findSliderRecursively(in view: UIView) -> UISlider? {
if let slider = view as? UISlider {
return slider
}
for subview in view.subviews {
if let foundSlider = findSliderRecursively(in: subview) {
return foundSlider
}
}
return nil
}
if let slider = findSliderRecursively(in: volumeView) {
let currentValue = slider.value
let newValue: Float
switch action {
case .increase:
newValue = min(currentValue + 0.2, 1.0) // 从 0.1 改为 0.2 (20%)
case .decrease:
newValue = max(currentValue - 0.2, 0.0) // 从 0.1 改为 0.2 (20%)
case .mute:
newValue = 0.0
case .setMax:
newValue = 1.0
case .setValue(let val):
newValue = max(0.0, min(val, 1.0))
}
os_log("设置音量从 %.2f 到 %.2f", log: self.logger, type: .info, currentValue, newValue)
slider.value = newValue
slider.sendActions(for: .valueChanged)
slider.sendActions(for: .touchUpInside)
let message = self.getVolumeActionMessage(action: action)
completion(true, message)
volumeView.removeFromSuperview()
} else {
retryCount += 1
if retryCount < maxRetries {
os_log("音量控制重试 %d/%d", log: self.logger, type: .debug, retryCount, maxRetries)
attemptVolumeControl()
} else {
os_log("所有重试都失败,使用备选方案", log: self.logger, type: .error)
volumeView.removeFromSuperview()
// 备选方案:至少记录当前音量状态
let currentVolume = self.audioSession.outputVolume
let message = "音量控制请求已处理(当前音量: \(Int(currentVolume * 100))%)"
completion(true, message)
}
}
}
}
attemptVolumeControl()
}
private func getVolumeActionMessage(action: VolumeControlAction) -> String {
switch action {
case .increase:
return "音量增加成功"
case .decrease:
return "音量降低成功"
case .mute:
return "静音设置成功"
case .setMax:
return "最大音量设置成功"
case .setValue:
return "音量设置成功"
}
}
deinit {
NotificationCenter.default.removeObserver(self)
}
}
}
// 在文件顶部添加
enum VolumeControlAction {
case increase
case decrease
case mute
case setMax
case setValue(Float)
}
struct VolumeControlInfo {
let action: VolumeControlAction
let value: Float?
}
struct VolumeControlResult {
let success: Bool
let message: String
let action: VolumeControlAction
}

BIN
local_plugins/agent_service/ios/agent_service/Sources/agent_service/Resources/await.mp3

Binary file not shown.

608
local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift

@ -32,6 +32,7 @@ public class AzureAsrHelper: NSObject {
private var subscriptionKey = ""
private var region = ""
// 音频源配置
public enum AudioSourceType {
/** 使用设备麦克风 */
@ -59,10 +60,14 @@ public class AzureAsrHelper: NSObject {
* @param audioSourceType 音频源类型
* @return 初始化是否成功
*/
/**
* 初始化Azure语音服务
* 预初始化关键组件以提升首次启动性能
*/
public func initialize(
subscriptionKey: String,
region: String,
supportedLanguages: [String] = ["zh-CN"],
supportedLanguages: [String] = ["zh-CN", "en-US"],
audioSourceType: AudioSourceType = .microphone
) -> Bool {
do {
@ -94,22 +99,30 @@ public class AzureAsrHelper: NSObject {
// 创建语音配置
speechConfig = try SPXSpeechConfiguration(subscription: subscriptionKey, region: region)
speechConfig?.setPropertyTo("800", byName: "SpeechServiceConnection_EndSilenceTimeoutMs")
speechConfig?.setPropertyTo("800", byName: "Speech_SegmentationSilenceTimeoutMs")
speechConfig?.setPropertyTo("Time", byName: "Speech_SegmentationStrategy")
// 针对翻译场景的性能优化
optimizeForTranslation()
if isAutoDetectLanguage {
// 直接启用语言检测模式
// 针对翻译场景优化的语言检测配置
speechConfig?.setPropertyTo("Continuous", byName: "SpeechServiceConnection_LanguageIdMode")
os_log("启用语言检测模式", log: log, type: .info)
// 设置语言检测的置信度阈值,提高准确性
speechConfig?.setPropertyTo("0.7", byName: "SpeechServiceConnection_AutoDetectSourceLanguageConfidenceThreshold")
// 不在这里设置语言,让SPXAutoDetectSourceLanguageConfiguration处理
os_log("启用语言检测模式,支持语言: %{public}@", log: log, type: .info, supportedLanguages.joined(separator: ","))
} else {
// 设置指定的识别语言
speechConfig?.speechRecognitionLanguage = currentLanguage
os_log("固定语言模式,当前语言: %{public}@", log: log, type: .info, currentLanguage)
}
// 录音文件类
recordfile = RecordFile()
// 创建识别器
// 预初始化音频组件
preInitializeAudioComponents()
return true
} catch {
os_log("初始化失败: %{public}@", log: log, type: .error, error.localizedDescription)
@ -117,7 +130,75 @@ public class AzureAsrHelper: NSObject {
}
}
/**
* 预初始化音频组件
* 在初始化阶段就准备好音频流和相关组件,减少首次启动延迟
*/
private func preInitializeAudioComponents() {
if audioStream == nil {
audioStream = AudioStream()
audioStream?.initAudioRecord()
audioStream?.onAudioData = { [weak self] data in
self?.recordfile?.saveAudioDataToWav(data)
}
// 预创建音频配置
if let pushStream = audioStream?.pushAudioStream {
audioConfig = SPXAudioConfiguration(streamInput: pushStream)
os_log("预初始化音频组件完成", log: log, type: .info)
}
}
}
/**
* 检测输入文本的语言,从支持的语言列表中选择最匹配的
*
* @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
]
}
/**
* 开始连续语音识别
*
@ -129,11 +210,13 @@ public class AzureAsrHelper: NSObject {
audioSourceType: AudioSourceType = .microphone
) -> Bool {
guard speechConfig != nil else {
print("语音服务未初始化")
//callback.onError("语音服务未初始化")
return false
}
if _isContinuousRecognitionActive {
print("连续识别已在进行中")
return true
}
@ -143,6 +226,7 @@ public class AzureAsrHelper: NSObject {
do {
guard recognizer != nil else {
if !setupRecognizer() {
print("重置识别器失败")
// callback.onError("重置识别器失败")
return false
}
@ -165,6 +249,28 @@ public class AzureAsrHelper: NSObject {
return false
}
}
// /**
// * 获取检测到的语言
// */
// private func getDetectedLanguage(from result: SPXSpeechRecognitionResult) -> String {
// if !isAutoDetectLanguage {
// return ""
// }
// // 尝试从属性中获取语言
// if let properties = result.properties,
// let language = properties.getPropertyByName("SpeechServiceResponse_RecognitionLanguage") {
// return language
// }
// // 尝试另一种方式获取语言
// do {
// let langResult = try SPXAutoDetectSourceLanguageResult(result)
// return langResult.language ?? ""
// } catch {
// return ""
// }
// }
/**
* 停止连续语音识别
@ -257,30 +363,33 @@ public class AzureAsrHelper: NSObject {
/**
* 设置识别器
*/
/**
* 设置识别器
* 优化:避免重复创建音频流,复用已初始化的组件
*/
private func setupRecognizer() -> Bool {
do {
print("设置识别器${_isContinuousRecognitionActive}")
print("设置识别器\(_isContinuousRecognitionActive)")
// 如果正在进行连续识别,先停止
if _isContinuousRecognitionActive {
// 直接停止,不等待结果
try? recognizer?.stopContinuousRecognition()
_isContinuousRecognitionActive = false
}
setupMicrophoneStream()
// 【优化】只有在音频流未初始化时才创建,避免重复初始化
if audioStream == nil || audioConfig == nil {
setupMicrophoneStream()
}
// 清理旧的识别器
recognizer = nil
// 创建识别器
if isAutoDetectLanguage {
// 使用更精确的语言配置方式
var sourceLanguageConfigs: [SPXSourceLanguageConfiguration] = []
for language in supportedLanguages {
if let config = try? SPXSourceLanguageConfiguration(language) {
sourceLanguageConfigs.append(config)
}
}
let autoDetectConfig = try SPXAutoDetectSourceLanguageConfiguration(sourceLanguageConfigurations: sourceLanguageConfigs)
// 使用正确的初始化方法
let autoDetectConfig = try SPXAutoDetectSourceLanguageConfiguration(supportedLanguages)
print("autoDetectConfig: \(autoDetectConfig)")
recognizer = try SPXSpeechRecognizer(
speechConfiguration: speechConfig!,
autoDetectSourceLanguageConfiguration: autoDetectConfig,
@ -296,7 +405,7 @@ public class AzureAsrHelper: NSObject {
return true
} catch {
os_log("创建识别器失败: %{public}@", log: log, type: .error, error.localizedDescription)
try? recognizer?.stopContinuousRecognition()
try? recognizer?.stopContinuousRecognition()
return false
}
}
@ -304,31 +413,164 @@ public class AzureAsrHelper: NSObject {
/**
* 设置麦克风流 - 使用推流方式
* 优化:避免重复初始化已存在的音频流
*/
private func setupMicrophoneStream() {
do {
if (audioStream == nil) {
// 创建外部音频拉流对象
// 明确指定使用外部音频源(推流模式)
// 【优化】检查是否已经初始化,避免重复创建
if audioStream == nil {
audioStream = AudioStream()
audioStream?.initAudioRecord()
audioStream?.onAudioData = { [weak self] data in
self?.recordfile?.saveAudioDataToWav(data)
}
audioStream?.onAudioData = { [weak self] data in
self?.recordfile?.saveAudioDataToWav(data)
}
}
// 创建音频配置
if let pushStream = audioStream?.pushAudioStream { // Safely unwrap optional
// 【优化】检查音频配置是否已存在
if audioConfig == nil, let pushStream = audioStream?.pushAudioStream {
audioConfig = SPXAudioConfiguration(streamInput: pushStream)
os_log("设置麦克风流:")
os_log("设置麦克风流完成", log: log, type: .info)
}
} catch {
os_log("设置麦克风流失败: %{public}@", log: log, type: .error, error.localizedDescription)
audioStream = nil
}
}
/**
* 从语音识别结果中获取检测到的语言
* @param result 语音识别结果
* @return 检测到的语言代码,如果检测失败则返回当前语言
*/
private func getDetectedLanguage(from result: SPXSpeechRecognitionResult) -> String {
guard isAutoDetectLanguage else {
return currentLanguage
}
print("result: \(result)")
// 备用方案:从属性中获取语言
if let properties = result.properties,
let language = properties.getPropertyByName("SpeechServiceResponse_RecognitionLanguage"),
!language.isEmpty {
print("从属性中检测到语言: \(language)")
return language
}
// 最后的备用方案:从JSON响应中解析
if let properties = result.properties,
let jsonString = properties.getPropertyByName("SpeechServiceResponse_Json"),
let jsonData = jsonString.data(using: .utf8) {
do {
if let json = try JSONSerialization.jsonObject(with: jsonData, options: []) as? [String: Any],
let language = json["Language"] as? String {
print("从JSON中检测到语言: \(language)")
return language
}
} catch {
print("JSON解析失败: \(error)")
}
}
// 使用正确的SPXAutoDetectSourceLanguageResult初始化方法
do {
let langResult = try SPXAutoDetectSourceLanguageResult(result)
if let language = langResult.language, !language.isEmpty {
print("SDK方法检测到语言: \(language)")
return language
}
} catch {
print("SDK语言检测失败: \(error.localizedDescription)")
}
print("无法检测语言,使用默认语言: \(currentLanguage)")
return currentLanguage
}
/**
* 针对翻译场景的性能优化配置
*/
private func optimizeForTranslation() {
guard let speechConfig = speechConfig else { return }
// 启用实时转录,减少延迟
speechConfig.setPropertyTo("true", byName: "SpeechServiceConnection_EnableAudioLogging")
speechConfig.setPropertyTo("INTERACTIVE", byName: "SpeechServiceConnection_RecoMode")
speechConfig.setPropertyTo("PCM", byName: "Speech_PushStreamFormat")
// 优化语音端点检测,适合翻译场景
speechConfig.setPropertyTo("300", byName: "SpeechServiceConnection_InitialSilenceTimeoutMs")
speechConfig.setPropertyTo("500", byName: "SpeechServiceConnection_EndSilenceTimeoutMs")
speechConfig.setPropertyTo("500", byName: "Speech_SegmentationSilenceTimeoutMs")
// 启用标点符号,提高翻译质量
speechConfig.setPropertyTo("true", byName: "SpeechServiceResponse_RequestProfanityFilterTrueFalse")
// 设置分段策略为时间模式
speechConfig.setPropertyTo("Time", byName: "Speech_SegmentationStrategy")
// 【优化】语言检测配置 - 减少检测延迟,实现快速响应
if isAutoDetectLanguage {
// 降低语言检测置信度阈值,提高检测灵敏度
speechConfig.setPropertyTo("0.3", byName: "SpeechServiceConnection_AutoDetectSourceLanguageConfidenceThreshold")
// 使用Continuous模式,在每句话结束后重新检测语言
speechConfig.setPropertyTo("Continuous", byName: "SpeechServiceConnection_LanguageIdMode")
// 启用快速语言检测
speechConfig.setPropertyTo("true", byName: "SpeechServiceConnection_EnableLanguageDetectionFast")
// 【关键优化】设置语言检测的最小音频长度为100毫秒,实现快速响应
speechConfig.setPropertyTo("100", byName: "SpeechServiceConnection_LanguageDetectionMinAudioLengthMs")
// 【新增】设置语言检测的最大等待时间,避免长时间等待
speechConfig.setPropertyTo("2000", byName: "SpeechServiceConnection_LanguageDetectionMaxWaitTimeMs")
// 强制在每个语音段开始时重新检测语言
speechConfig.setPropertyTo("true", byName: "SpeechServiceConnection_ForceLanguageDetectionOnNewSegment")
// 【新增】启用句子级别的语言检测
speechConfig.setPropertyTo("true", byName: "SpeechServiceConnection_EnableLanguageDetectionPerSentence")
// 【新增】设置语言检测的频率 - 每个识别结果后都检测
speechConfig.setPropertyTo("PerResult", byName: "SpeechServiceConnection_LanguageDetectionFrequency")
// 【新增】启用实时语言检测,减少缓冲延迟
speechConfig.setPropertyTo("true", byName: "SpeechServiceConnection_EnableRealtimeLanguageDetection")
// 【新增】设置语言检测的采样间隔为200毫秒
speechConfig.setPropertyTo("200", byName: "SpeechServiceConnection_LanguageDetectionSamplingIntervalMs")
}
os_log("翻译场景优化配置已应用,包含语言检测优化", log: log, type: .info)
}
/**
* 动态切换语言(翻译场景中的语言切换)
*
* @param newSourceLanguage 新的源语言
* @param newTargetLanguage 新的目标语言
* @return 切换是否成功
*/
public func switchTranslationLanguages(
newSourceLanguage: String,
newTargetLanguage: String
) -> Bool {
let validatedSourceLang = LanguageDetector.convertToAzureLanguageCode(newSourceLanguage)
let validatedTargetLang = LanguageDetector.convertToAzureLanguageCode(newTargetLanguage)
// 更新支持的语言
self.supportedLanguages = [validatedSourceLang, validatedTargetLang]
self.currentLanguage = validatedSourceLang
// 重新配置语言检测
self.isAutoDetectLanguage = (validatedSourceLang != validatedTargetLang)
// 如果正在识别,需要重新设置识别器
if _isContinuousRecognitionActive {
let wasActive = stopContinuousRecognition()
if wasActive {
return setupRecognizer()
}
}
os_log("语言已切换: %{public}@ -> %{public}@", log: log, type: .info, validatedSourceLang, validatedTargetLang)
return true
}
/**
* 设置事件监听器
*/
@ -342,68 +584,42 @@ public class AzureAsrHelper: NSObject {
guard let recognizer = recognizer else { return false}
// 正在识别事件
recognizer.addRecognizingEventHandler { [weak self] (_, event) in
guard let self = self else { return }
guard let self = self else { return }
let result = event.result
print("识别中文本: \(result.text)")
// 优化:只处理非空结果
if !(result.text?.isEmpty ?? true) {
let detectedLanguage: String
if self.isAutoDetectLanguage {
do {
let langResult = try SPXAutoDetectSourceLanguageResult(result)
let rawLanguage = langResult.language
print("[DEBUG] 原始检测语言: \(rawLanguage ?? "nil"), 文本: \(result.text ?? "nil"), 支持的语言: \(self.supportedLanguages)")
// 应用启发式语言检测纠正
detectedLanguage = self.correctLanguageDetection(rawLanguage: rawLanguage ?? "", text: result.text ?? "")
print("[DEBUG] 纠正后语言: \(detectedLanguage)")
} catch {
print("[DEBUG] 语言检测异常: \(error.localizedDescription)")
detectedLanguage = ""
}
} else {
detectedLanguage = self.currentLanguage
print("[DEBUG] 使用当前语言: \(detectedLanguage)")
}
print("识别中事件: \(detectedLanguage)")
DispatchQueue.main.async {
callback.onRecognizing(result.text ?? "", detectedLanguage)
}
guard !(result.text?.isEmpty ?? true) else { return }
// let detectedLanguage = self.getDetectedLanguage(from: result)
// 检测文本语言,避免重复调用
let detectedLanguage = self.detectTextLanguage(result.text ?? "") ?? "zh-CN"
// 输出检测到的语言信息
print("检测到的语言: \(detectedLanguage)")
DispatchQueue.main.async {
callback.onRecognizing(result.text ?? "", detectedLanguage)
}
}
// 识别完成事件
recognizer.addRecognizedEventHandler { [weak self] (_, event) in
guard let self = self else { return }
guard let self = self else { return }
let result = event.result
print("识别完成文本: \(result.text)")
if result.reason == .recognizedSpeech && !(result.text?.isEmpty ?? true) {
let detectedLanguage: String
if self.isAutoDetectLanguage {
do {
let langResult = try SPXAutoDetectSourceLanguageResult(result)
let rawLanguage = langResult.language
print("[DEBUG] 识别完成 - 原始检测语言: \(rawLanguage ?? "nil"), 文本: \(result.text ?? "nil"), 支持的语言: \(self.supportedLanguages)")
// 应用启发式语言检测纠正
let correctedLanguage = self.correctLanguageDetection(rawLanguage: rawLanguage ?? "", text: result.text ?? "")
detectedLanguage = correctedLanguage.isEmpty ? (self.supportedLanguages.first ?? self.currentLanguage) : correctedLanguage
print("[DEBUG] 识别完成 - 纠正后语言: \(detectedLanguage)")
} catch {
print("[DEBUG] 识别完成 - 语言检测异常: \(error.localizedDescription)")
detectedLanguage = self.supportedLanguages.first ?? self.currentLanguage
}
} else {
detectedLanguage = self.currentLanguage
print("[DEBUG] 识别完成 - 使用当前语言: \(detectedLanguage)")
}
print("识别完成事件: \(detectedLanguage)")
DispatchQueue.main.async {
callback.onResult(result.text ?? "", detectedLanguage)
}
guard result.reason == .recognizedSpeech,
!(result.text?.isEmpty ?? true) else { return }
// 检测文本语言,避免重复调用
let detectedLanguage = self.detectTextLanguage(result.text ?? "") ?? "zh-CN"
// 输出检测到的语言信息
print("检测到的语言: \(detectedLanguage)")
// 在主线程回调结果
DispatchQueue.main.async {
callback.onResult(result.text ?? "", detectedLanguage)
}
}
}
// 会话开始事件
recognizer.addSessionStartedEventHandler { (sender, event) in
// 直接在当前线程调用回调
@ -416,8 +632,8 @@ public class AzureAsrHelper: NSObject {
guard let self = self else { return }
print("会话结束事件:")
// 直接在当前线程调用回调
// callback.onSessionStopped()
// self._isContinuousRecognitionActive = false
callback.onSessionStopped()
self._isContinuousRecognitionActive = false
//self.stopAudioProcessing()
}
@ -532,15 +748,23 @@ public class AzureAsrHelper: NSObject {
/**
* 初始化
*/
public func initAudioRecord() {
/**
* 初始化音频录制组件
* 包括音频格式、推流、音频引擎等核心组件的初始化
*/
public func initAudioRecord() {
print("初始化了")
audioFormat = getOptimalAudioFormat()
pushAudioStream = SPXPushAudioInputStream()
isRunning=true
// 初始化音频引擎
audioEngine = AVAudioEngine()
isRunning = true
// 创建新的写线程
writeThread = DispatchQueue(label: "audio.stream.writer")
writeThread = DispatchQueue(label: "audio.stream.writer")
startWriteThread()
self.setAudioOutputRoute(.receiver)
self.setAudioOutputRoute(.receiver)
}
/// 获取最佳音频格式 (iOS 通常支持标准采样率)
@ -584,9 +808,14 @@ public class AzureAsrHelper: NSObject {
guard let dataToWrite = self.writeQueue.take() else { continue }
print("写入数据长度: \(dataToWrite.count)")
self.pushAudioStream?.write(dataToWrite)
self.onAudioData?(dataToWrite)
// print("写入数据长度: \(dataToWrite.count)")
do {
try self.pushAudioStream?.write(dataToWrite)
self.onAudioData?(dataToWrite)
} catch {
print("推送音频数据失败: \(error.localizedDescription)")
}
}
}
}
@ -601,70 +830,97 @@ public class AzureAsrHelper: NSObject {
if audioSourceType != .external {
return
}
print("外部data=\(data)")
// print("外部data=\(data)")
// 放入队列,由写线程写入
writeQueue.put(data)
}
private func runMicrophoneCapture() {
do {
audioEngine = AVAudioEngine()
guard let inputNode = audioEngine?.inputNode else {
throw NSError(domain: "AudioSetup", code: 1)
}
// 推荐直接用系统 format
let hardwareFormat = inputNode.inputFormat(forBus: 0)
// // ===== 新增:启用专业级语音处理 =====
if #available(iOS 13.0, *) {
try inputNode.setVoiceProcessingEnabled(true)
print("Voice processing enabled")
}
// Create converter to target format
guard let targetFormat = audioFormat,
let converter = AVAudioConverter(from: hardwareFormat, to: targetFormat) else {
throw NSError(domain: "AudioSetup", code: 2)
}
inputNode.installTap(onBus: 0, bufferSize: UInt32(bufferSize), format: hardwareFormat) {
[weak self] buffer, time in
guard let self = self, self.isWriting else { return }
// Convert to target format
let convertedBuffer = AVAudioPCMBuffer(
pcmFormat: targetFormat,
frameCapacity: AVAudioFrameCount(targetFormat.sampleRate * Double(buffer.frameLength) / buffer.format.sampleRate)
)!
///print("进入 tap 回调,frameLength: \(buffer.frameLength)")
var error: NSError?
let status = converter.convert(
to: convertedBuffer,
error: &error,
withInputFrom: { inNumPackets, outStatus in
outStatus.pointee = .haveData
return buffer
}
)
//print("转换状态: \(status.rawValue), 错误: \(String(describing: error))")
if status == .haveData, error == nil {
let data = self.audioBufferToData(convertedBuffer)
// print("准备写入数据,大小: \(data.count)")
self.writeQueue.put(data)
}
// 私有方法:启动麦克风捕获
/**
* 启动麦克风捕获
* 优化:复用已初始化的音频引擎,减少启动延迟
*/
private func runMicrophoneCapture() {
do {
// 【优化】检查音频引擎是否已初始化,避免重复创建
if audioEngine == nil {
audioEngine = AVAudioEngine()
}
// 如果音频引擎正在运行,先停止
if audioEngine?.isRunning == true {
audioEngine?.stop()
}
// 获取音频输入节点(麦克风)
guard let inputNode = audioEngine?.inputNode else {
throw NSError(domain: "AudioSetup", code: 1, userInfo: [NSLocalizedDescriptionKey: "无法获取音频输入节点"])
}
// 移除之前的音频处理块,避免重复添加
inputNode.removeTap(onBus: 0)
// 获取硬件支持的原始音频格式
let hardwareFormat = inputNode.inputFormat(forBus: 0)
// iOS 13+ 启用语音处理
if #available(iOS 13.0, *) {
try inputNode.setVoiceProcessingEnabled(true)
}
// 检查目标音频格式和转换器是否可用
guard let targetFormat = audioFormat, // 外部定义的期望音频格式
let converter = AVAudioConverter(from: hardwareFormat, to: targetFormat) else {
throw NSError(domain: "AudioSetup", code: 2)
}
// 在输入节点上安装录音回调
inputNode.installTap(onBus: 0,
bufferSize: UInt32(bufferSize), // 每次回调的缓冲区大小
format: hardwareFormat) { // 使用原始硬件格式
[weak self] buffer, time in // 弱引用避免循环引用
// 确保实例存在且正在写入状态
guard let self = self, self.isWriting else { return }
// 创建目标格式的音频缓冲区
let convertedBuffer = AVAudioPCMBuffer(
pcmFormat: targetFormat,
// 计算转换后的帧容量(考虑采样率差异)
frameCapacity: AVAudioFrameCount(
targetFormat.sampleRate * Double(buffer.frameLength) / buffer.format.sampleRate
)
)!
var error: NSError?
// 执行音频格式转换
let status = converter.convert(
to: convertedBuffer,
error: &error,
withInputFrom: { inNumPackets, outStatus in
outStatus.pointee = .haveData // 标记有数据可用
return buffer // 返回原始音频数据
}
// 重新激活
try audioSession.setActive(true, options: [.notifyOthersOnDeactivation])
)
try audioEngine?.start()
} catch {
print("麦克风启动失败: \(error)")
// 转换成功且无错误
if status == .haveData, error == nil {
// 将音频缓冲区转换为二进制数据
let data = self.audioBufferToData(convertedBuffer)
// 将数据放入写入队列(后续处理)
self.writeQueue.put(data)
}
}
// 激活音频会话(允许录音)
try audioSession.setActive(true, options: [.notifyOthersOnDeactivation])
// 启动音频引擎
try audioEngine?.start()
} catch {
print("麦克风启动失败: \(error)")
}
}
private func audioBufferToData(_ buffer: AVAudioPCMBuffer) -> Data {
let frameLength = Int(buffer.frameLength)
let channelCount = 1
@ -870,55 +1126,7 @@ public class AzureAsrHelper: NSObject {
}
}
/**
* 启发式语言检测纠正方法
* 基于文本内容特征来纠正Azure语音服务的语言检测结果
*/
private func correctLanguageDetection(rawLanguage: String, text: String) -> String {
// 如果原始检测为空,返回空
if rawLanguage.isEmpty || text.isEmpty {
return rawLanguage
}
let trimmedText = text.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
// 英文特征检测
let englishPattern = "^[a-z\\s\\.,!?;:'-]+$"
let englishRegex = try? NSRegularExpression(pattern: englishPattern, options: .caseInsensitive)
let englishMatches = englishRegex?.numberOfMatches(in: trimmedText, options: [], range: NSRange(location: 0, length: trimmedText.count)) ?? 0
// 中文特征检测
let chinesePattern = "[\\u4e00-\\u9fff]"
let chineseRegex = try? NSRegularExpression(pattern: chinesePattern, options: [])
let chineseMatches = chineseRegex?.numberOfMatches(in: trimmedText, options: [], range: NSRange(location: 0, length: trimmedText.count)) ?? 0
// 常见英文单词列表
let commonEnglishWords = ["hello", "hi", "please", "thank", "you", "yes", "no", "good", "morning", "afternoon", "evening", "night", "how", "are", "what", "where", "when", "why", "who", "can", "could", "would", "should", "will", "the", "and", "or", "but", "if", "then", "this", "that", "these", "those"]
let hasCommonEnglishWords = commonEnglishWords.contains { word in
trimmedText.contains(word)
}
print("[DEBUG] 语言纠正分析 - 文本: '\(trimmedText)', 英文匹配: \(englishMatches > 0), 中文字符: \(chineseMatches), 常见英文词: \(hasCommonEnglishWords), 原始语言: \(rawLanguage)")
// 纠正逻辑
if rawLanguage == "zh-CN" {
// 如果Azure检测为中文,但文本明显是英文特征
if (englishMatches > 0 && chineseMatches == 0) || hasCommonEnglishWords {
print("[DEBUG] 纠正: \(rawLanguage) -> en-US (检测到英文特征)")
return "en-US"
}
} else if rawLanguage == "en-US" {
// 如果Azure检测为英文,但包含中文字符
if chineseMatches > 0 {
print("[DEBUG] 纠正: \(rawLanguage) -> zh-CN (检测到中文字符)")
return "zh-CN"
}
}
// 如果没有明确的纠正理由,保持原始检测结果
return rawLanguage
}
// MARK: - 回调函数
/**

214
local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/LanguageDetector.swift

@ -0,0 +1,214 @@
import Foundation
import NaturalLanguage
/**
* 语言检测工具类
* 使用iOS自带的自然语言处理框架检测文本语种
*/
public class LanguageDetector {
/**
* 检测文本的主要语言
*
* @param text 需要检测的文本
* @return 语言代码(如:"zh-Hans", "en", "ja"等),如果检测失败返回nil
*/
public static func detectLanguage(from text: String) -> String? {
// 检查输入文本是否为空
guard !text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
return nil
}
// 使用NLLanguageRecognizer进行语言检测
let recognizer = NLLanguageRecognizer()
recognizer.processString(text)
// 获取最可能的语言
guard let dominantLanguage = recognizer.dominantLanguage else {
return nil
}
return dominantLanguage.rawValue
}
/**
* 检测文本的语言并返回置信度
*
* @param text 需要检测的文本
* @return 语言代码和置信度的字典,按置信度降序排列
*/
public static func detectLanguageWithConfidence(from text: String) -> [(language: String, confidence: Double)] {
// 检查输入文本是否为空
guard !text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
return []
}
let recognizer = NLLanguageRecognizer()
recognizer.processString(text)
// 获取语言假设及其置信度
let languageHypotheses = recognizer.languageHypotheses(withMaximum: 5)
// 转换为元组数组并按置信度排序
return languageHypotheses.map { (language: $0.key.rawValue, confidence: $0.value) }
.sorted { $0.confidence > $1.confidence }
}
/**
* 将语言代码转换为人类可读的语言名称
*
* @param languageCode 语言代码(如:"zh-Hans", "en"等)
* @return 语言的本地化名称
*/
public static func getLanguageName(from languageCode: String) -> String {
let locale = Locale.current
return locale.localizedString(forLanguageCode: languageCode) ?? languageCode
}
/**
* 检测文本语言并返回Azure语音服务支持的语言代码格式
*
* @param text 需要检测的文本
* @return Azure支持的语言代码格式(如:"zh-CN", "en-US"等)
*/
public static func detectLanguageForAzure(from text: String) -> String? {
guard let detectedLanguage = detectLanguage(from: text) else {
return nil
}
// 将iOS语言代码转换为Azure语音服务支持的格式
return convertToAzureLanguageCode(detectedLanguage)
}
/**
* 将iOS语言代码转换为Azure语音服务支持的语言代码
*
* @param iosLanguageCode iOS检测到的语言代码
* @return Azure支持的语言代码
*/
public static func convertToAzureLanguageCode(_ iosLanguageCode: String) -> String {
let languageMapping: [String: String] = [
// 中文
"zh-Hans": "zh-CN", // 简体中文
"zh-Hant": "zh-TW", // 繁体中文
"zh-HK": "zh-HK", // 香港中文
// 英语系
"en": "en-US", // 英语(美国)
"en-GB": "en-US", // 英语(英国)
"en-AU": "en-US", // 英语(澳大利亚)
"en-CA": "en-US", // 英语(加拿大)
"en-IN": "en-US", // 英语(印度)
"en-IE": "en-US", // 英语(爱尔兰)
"en-NZ": "en-US", // 英语(新西兰)
"en-ZA": "en-US", // 英语(南非)
// 亚洲语言
"ja": "ja-JP", // 日语
"ko": "ko-KR", // 韩语
"hi": "hi-IN", // 印地语
"th": "th-TH", // 泰语
"vi": "vi-VN", // 越南语
"id": "id-ID", // 印尼语
"ms": "ms-MY", // 马来语
"tl": "fil-PH", // 菲律宾语
"my": "my-MM", // 缅甸语
"km": "km-KH", // 高棉语(柬埔寨)
"lo": "lo-LA", // 老挝语
"si": "si-LK", // 僧伽罗语(斯里兰卡)
"ta": "ta-IN", // 泰米尔语
"te": "te-IN", // 泰卢固语
"bn": "bn-IN", // 孟加拉语
"gu": "gu-IN", // 古吉拉特语
"kn": "kn-IN", // 卡纳达语
"ml": "ml-IN", // 马拉雅拉姆语
"mr": "mr-IN", // 马拉地语
"pa": "pa-IN", // 旁遮普语
"ur": "ur-PK", // 乌尔都语
// 欧洲语言
"fr": "fr-FR", // 法语(法国)
"fr-CA": "fr-CA", // 法语(加拿大)
"fr-BE": "fr-BE", // 法语(比利时)
"fr-CH": "fr-CH", // 法语(瑞士)
"de": "de-DE", // 德语(德国)
"de-AT": "de-AT", // 德语(奥地利)
"de-CH": "de-CH", // 德语(瑞士)
"es": "es-ES", // 西班牙语(西班牙)
"es-MX": "es-MX", // 西班牙语(墨西哥)
"es-AR": "es-AR", // 西班牙语(阿根廷)
"es-CO": "es-CO", // 西班牙语(哥伦比亚)
"es-CL": "es-CL", // 西班牙语(智利)
"es-PE": "es-PE", // 西班牙语(秘鲁)
"es-VE": "es-VE", // 西班牙语(委内瑞拉)
"it": "it-IT", // 意大利语
"pt": "pt-BR", // 葡萄牙语(巴西)
"pt-PT": "pt-PT", // 葡萄牙语(葡萄牙)
"ru": "ru-RU", // 俄语
"nl": "nl-NL", // 荷兰语
"nl-BE": "nl-BE", // 荷兰语(比利时)
"sv": "sv-SE", // 瑞典语
"da": "da-DK", // 丹麦语
"no": "nb-NO", // 挪威语
"fi": "fi-FI", // 芬兰语
"is": "is-IS", // 冰岛语
"pl": "pl-PL", // 波兰语
"cs": "cs-CZ", // 捷克语
"sk": "sk-SK", // 斯洛伐克语
"hu": "hu-HU", // 匈牙利语
"ro": "ro-RO", // 罗马尼亚语
"bg": "bg-BG", // 保加利亚语
"hr": "hr-HR", // 克罗地亚语
"sr": "sr-RS", // 塞尔维亚语
"sl": "sl-SI", // 斯洛文尼亚语
"et": "et-EE", // 爱沙尼亚语
"lv": "lv-LV", // 拉脱维亚语
"lt": "lt-LT", // 立陶宛语
"uk": "uk-UA", // 乌克兰语
"be": "be-BY", // 白俄罗斯语
"mk": "mk-MK", // 马其顿语
"sq": "sq-AL", // 阿尔巴尼亚语
"mt": "mt-MT", // 马耳他语
"ga": "ga-IE", // 爱尔兰语
"cy": "cy-GB", // 威尔士语
"eu": "eu-ES", // 巴斯克语
"ca": "ca-ES", // 加泰罗尼亚语
"gl": "gl-ES", // 加利西亚语
// 中东和非洲语言
"ar": "ar-SA", // 阿拉伯语(沙特阿拉伯)
"ar-EG": "ar-EG", // 阿拉伯语(埃及)
"ar-AE": "ar-AE", // 阿拉伯语(阿联酋)
"ar-BH": "ar-BH", // 阿拉伯语(巴林)
"ar-DZ": "ar-DZ", // 阿拉伯语(阿尔及利亚)
"ar-IQ": "ar-IQ", // 阿拉伯语(伊拉克)
"ar-JO": "ar-JO", // 阿拉伯语(约旦)
"ar-KW": "ar-KW", // 阿拉伯语(科威特)
"ar-LB": "ar-LB", // 阿拉伯语(黎巴嫩)
"ar-LY": "ar-LY", // 阿拉伯语(利比亚)
"ar-MA": "ar-MA", // 阿拉伯语(摩洛哥)
"ar-OM": "ar-OM", // 阿拉伯语(阿曼)
"ar-QA": "ar-QA", // 阿拉伯语(卡塔尔)
"ar-SY": "ar-SY", // 阿拉伯语(叙利亚)
"ar-TN": "ar-TN", // 阿拉伯语(突尼斯)
"ar-YE": "ar-YE", // 阿拉伯语(也门)
"he": "he-IL", // 希伯来语
"fa": "fa-IR", // 波斯语(伊朗)
"tr": "tr-TR", // 土耳其语
"az": "az-AZ", // 阿塞拜疆语
"ka": "ka-GE", // 格鲁吉亚语
"hy": "hy-AM", // 亚美尼亚语
"sw": "sw-KE", // 斯瓦希里语
"am": "am-ET", // 阿姆哈拉语(埃塞俄比亚)
"zu": "zu-ZA", // 祖鲁语(南非)
"af": "af-ZA", // 南非荷兰语
// 其他语言
"el": "el-GR", // 希腊语
"eo": "eo", // 世界语
"la": "la", // 拉丁语
]
return languageMapping[iosLanguageCode] ?? iosLanguageCode
}
}

2
local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleServicePlugin.kt

@ -174,7 +174,7 @@ class BleServicePlugin : FlutterPlugin, MethodCallHandler, ActivityAware,
}
override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
Log.d(TAG, "Method call: ${call.method}")
// Log.d(TAG, "Method call: ${call.method}")
when (call.method) {
"isConnected" -> {

2
local_plugins/ble_service/ios/ble_service/Sources/ble_service/SwiftBleServicePlugin.swift

@ -108,7 +108,7 @@ public class SwiftBleServicePlugin: NSObject, FlutterPlugin {
case "startScan":
let arguments = call.arguments as? [String: Any]
let timeout = (arguments?["timeout"] as? Double) ?? 30.0
let timeout = (arguments?["timeout"] as? Double) ?? 15.0
os_log("🔍 开始传统BLE扫描,超时: %.1f秒", log: logger, type: .info, timeout)
BleService.shared.startScan(timeout: timeout)

2
local_plugins/ble_service/ios/ble_service/Sources/ble_service/SwiftOpusAudioProcessor.swift

@ -237,4 +237,4 @@ class SwiftOpusAudioProcessor: NSObject {
func getCurrentConfig() -> (sampleRate: Int32, channels: Int32) {
return (sampleRate, channels)
}
}
}

77
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<Void, Never>?
// 工具调用配置 - 添加这两行
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)")
}
}

217
local_plugins/chat_api/ios/chat_api/Sources/chat_api/SystemFunctionHandler.swift

@ -6,6 +6,32 @@ import MessageUI
import location_service
import user_portrait // 添加这行
import MediaPlayer
import AVFoundation
// 在文件顶部添加通知名称定义
extension Notification.Name {
static let audioSessionBeginPlayback = Notification.Name("AudioSessionBeginPlayback")
static let audioSessionBeginVoice = Notification.Name("AudioSessionBeginVoice")
static let volumeControlRequest = Notification.Name("VolumeControlRequest")
}
// 音量控制请求的数据结构
// 移除结构体定义,直接使用 map 传输
// 删除以下代码:
// struct VolumeControlInfo {
// let action: VolumeAction
// let value: Float?
//
// enum VolumeAction {
// case increase
// case decrease
// case mute
// case setMax
// case setValue(Float)
// }
// }
/// 结果结构体
public struct Result {
let success: Bool
@ -66,6 +92,13 @@ public class SystemFunctionHandler: NSObject {
// 注册打开录音机功能
registerOpenRecorderFunction(client: client)
// 注册音量控制函数
// 在registerOpenRecorderFunction方法之后添加以下方法
registerAddVolumeFunction(client: client)
registerReduceVolumeFunction(client: client)
registerMuteVolumeFunction(client: client)
registerMaxVolumeFunction(client: client)
// 注册用户画像更新功能
registerSetUserProfileFieldFunction(client: client)
@ -311,6 +344,90 @@ public class SystemFunctionHandler: NSObject {
handler: handler
)
}
/// 注册增加音量函数
private func registerAddVolumeFunction(client: MCPClient) {
client.registerLocalFunction(
name: "add_volume",
description: "增大音量",
parameters: [
"type": "object",
"properties": [:] as [String: Any],
"required": [] as [String]
],
handler: AddVolumeHandler()
)
}
/// 注册减少音量函数
private func registerReduceVolumeFunction(client: MCPClient) {
client.registerLocalFunction(
name: "reduce_volume",
description: "降低音量",
parameters: [
"type": "object",
"properties": [:] as [String: Any],
"required": [] as [String]
],
handler: ReduceVolumeHandler()
)
}
/// 注册静音函数
private func registerMuteVolumeFunction(client: MCPClient) {
client.registerLocalFunction(
name: "mute_volume",
description: "设置静音",
parameters: [
"type": "object",
"properties": [:] as [String: Any],
"required": [] as [String]
],
handler: MuteVolumeHandler()
)
}
/// 注册最大音量函数
private func registerMaxVolumeFunction(client: MCPClient) {
client.registerLocalFunction(
name: "max_volume",
description: "设置最大音量",
parameters: [
"type": "object",
"properties": [:] as [String: Any],
"required": [] as [String]
],
handler: MaxVolumeHandler()
)
}
/// 注册设置用户画像字段函数
private func registerSetUserProfileFieldFunction(client: MCPClient) {
let parameters: [String: Any] = [
"type": "object",
"properties": [
"key": [
"type": "string",
"description": "画像字段名,例如:age、tone、工作经历、device_preference"
],
"value": [
"type": "string",
"description": "字段对应的值"
]
],
"required": ["key", "value"]
]
let handler = SetUserProfileFieldHandler()
_ = client.registerLocalFunction(
name: "set_user_profile_field",
description: "设置或更新用户画像中的某一个字段",
parameters: parameters,
handler: handler
)
}
}
// MARK: - 功能处理器实现
@ -678,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\": \"已开始播放媒体\"}"
}
}
@ -686,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\": \"已暂停媒体播放\"}"
}
}
@ -694,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\": \"已切换到上一首\"}"
}
}
@ -702,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\": \"已切换到下一首\"}"
}
}
@ -716,7 +833,7 @@ private class OpenRecorderHandler: FunctionHandler {
}
}
return "{\"result\": \"无法打开录音机应用\"}"
return "{\"notreply\": true,\"result\": \"无法打开录音机应用\"}"
}
}
@ -725,31 +842,7 @@ private class OpenRecorderHandler: FunctionHandler {
// ...
// }
/// 注册设置用户画像字段函数
private func registerSetUserProfileFieldFunction(client: MCPClient) {
let parameters: [String: Any] = [
"type": "object",
"properties": [
"key": [
"type": "string",
"description": "画像字段名,例如:age、tone、工作经历、device_preference"
],
"value": [
"type": "string",
"description": "字段对应的值"
]
],
"required": ["key", "value"]
]
let handler = SetUserProfileFieldHandler()
_ = client.registerLocalFunction(
name: "set_user_profile_field",
description: "设置或更新用户画像中的某一个字段",
parameters: parameters,
handler: handler
)
}
/// 设置用户画像字段处理器
private class SetUserProfileFieldHandler: FunctionHandler {
func handle(arguments: [String: Any]) async throws -> String {
@ -799,3 +892,67 @@ private class SetUserProfileFieldHandler: FunctionHandler {
}
}
}
private class AddVolumeHandler: FunctionHandler {
func handle(arguments: [String: Any]) async throws -> String {
// 使用 map 传输数据
let volumeInfo: [String: Any] = [
"action": "increase",
"value": NSNull()
]
NotificationCenter.default.post(
name: .volumeControlRequest,
object: volumeInfo
)
return "{\"broadcast\": false,\"result\": \"音量增加请求已发送\"}"
}
}
/// 减少音量处理器
private class ReduceVolumeHandler: FunctionHandler {
func handle(arguments: [String: Any]) async throws -> String {
let volumeInfo: [String: Any] = [
"action": "decrease",
"value": NSNull()
]
NotificationCenter.default.post(
name: .volumeControlRequest,
object: volumeInfo
)
return "{\"broadcast\": false,\"result\": \"音量降低请求已发送\"}"
}
}
/// 静音处理器
private class MuteVolumeHandler: FunctionHandler {
func handle(arguments: [String: Any]) async throws -> String {
let volumeInfo: [String: Any] = [
"action": "mute",
"value": NSNull()
]
NotificationCenter.default.post(
name: .volumeControlRequest,
object: volumeInfo
)
return "{\"broadcast\": false,\"result\": \"静音请求已发送\"}"
}
}
/// 最大音量处理器
private class MaxVolumeHandler: FunctionHandler {
func handle(arguments: [String: Any]) async throws -> String {
let volumeInfo: [String: Any] = [
"action": "setMax",
"value": NSNull()
]
NotificationCenter.default.post(
name: .volumeControlRequest,
object: volumeInfo
)
return "{\"broadcast\": false,\"result\": \"最大音量请求已发送\"}"
}
}

2
local_plugins/location_service/android/src/main/kotlin/com/yunqiinnovation/location_service/LocationServicePlugin.kt

@ -25,7 +25,7 @@ class LocationServicePlugin: FlutterPlugin, MethodCallHandler, ActivityAware, Pl
}
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
Log.d(TAG, "Method call: ${call.method}")
// Log.d(TAG, "Method call: ${call.method}")
when (call.method) {
"location" -> LocationService.getCurrentLocation(result)
"requestPermission" -> LocationService.requestLocationPermission(activityBinding?.activity, result)

2
local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicServicePlugin.kt

@ -24,7 +24,7 @@ class MusicServicePlugin: FlutterPlugin, MethodCallHandler {
}
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
Log.d(TAG, "Method call: ${call.method}")
// Log.d(TAG, "Method call: ${call.method}")
when (call.method) {
"play" -> {
val song = call.argument<Map<String, Any>>("song")

2
local_plugins/music_service/ios/music_service/Sources/music_service/MusicServicePlugin.swift

@ -13,7 +13,7 @@ public class MusicServicePlugin: NSObject, FlutterPlugin {
}
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
print("Method call: \(call.method)")
// print("Method call: \(call.method)")
switch call.method {
case "play":

5
pubspec.yaml

@ -1,7 +1,7 @@
name: voitrans
description: "Voitrans - AI Voice Assistant."
publish_to: "none"
version: 1.0.3+11
version: 1.0.4+20
environment:
sdk: ">=3.3.0 <4.0.0"
@ -27,6 +27,7 @@ dependencies:
parchment: ^1.24.0
htmltopdfwidgets: ^1.1.0
wifi_iot: ^0.3.19+2
flutter_intro: ^3.4.0
permission_handler: ^10.4.5
web_socket_channel: ^2.4.0
flutter_dotenv: ^5.1.0
@ -59,7 +60,7 @@ dependencies:
font_awesome_flutter: ^10.8.0
lottie: ^3.3.1
app_settings: 5.2.0
fluwx: ^5.4.1
fluwx: ^5.7.1
url_launcher: ^6.3.1
firebase_core: ^3.12.1
firebase_auth: ^5.5.1

Loading…
Cancel
Save