From 8fedcc0bbd27b951d3074d77539fcd1009660c28 Mon Sep 17 00:00:00 2001 From: fdp <1286779656@qq.com> Date: Sat, 2 Aug 2025 11:25:00 +0800 Subject: [PATCH 01/11] =?UTF-8?q?=E5=8A=A0=E5=85=A5=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E8=AF=86=E5=88=AB=E7=BF=BB=E8=AF=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/azure_speech/AzureAsrHelper.swift | 227 +++++++----------- 1 file changed, 84 insertions(+), 143 deletions(-) diff --git a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift index 5c90a7c67..3106c1236 100644 --- a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift +++ b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift @@ -100,14 +100,9 @@ public class AzureAsrHelper: NSObject { // 创建语音配置 speechConfig = try SPXSpeechConfiguration(subscription: subscriptionKey, region: region) // 针对翻译场景的性能优化 - optimizeForTranslation() + // optimizeForTranslation() if isAutoDetectLanguage { - // 针对翻译场景优化的语言检测配置 speechConfig?.setPropertyTo("Continuous", byName: "SpeechServiceConnection_LanguageIdMode") - - // 设置语言检测的置信度阈值,提高准确性 - speechConfig?.setPropertyTo("0.7", byName: "SpeechServiceConnection_AutoDetectSourceLanguageConfidenceThreshold") - // 不在这里设置语言,让SPXAutoDetectSourceLanguageConfiguration处理 os_log("启用语言检测模式,支持语言: %{public}@", log: log, type: .info, supportedLanguages.joined(separator: ",")) } else { @@ -150,55 +145,7 @@ public class AzureAsrHelper: NSObject { } } - /** - * 检测输入文本的语言,从支持的语言列表中选择最匹配的 - * - * @param text 需要检测的文本 - * @return 从supportedLanguages中选择的最匹配语言代码 - */ - public func detectTextLanguage(_ text: String) -> String? { - // 如果只有一种支持的语言,直接返回 - if supportedLanguages.count == 1 { - return supportedLanguages[0] - } - - // 获取所有可能的语言及其置信度 - let languageResults = LanguageDetector.detectLanguageWithConfidence(from: text) - - // 遍历检测结果,找到第一个在supportedLanguages中的语言 - for result in languageResults { - let azureLanguageCode = LanguageDetector.convertToAzureLanguageCode(result.language) - if supportedLanguages.contains(azureLanguageCode) { - return azureLanguageCode - } - } - - // 如果没有找到匹配的语言,返回第一个支持的语言作为默认值 - return supportedLanguages.first - } - - /** - * 检测文本语言并返回详细信息 - * - * @param text 需要检测的文本 - * @return 包含语言代码、置信度和语言名称的字典 - */ - public func detectTextLanguageWithDetails(_ text: String) -> [String: Any]? { - let results = LanguageDetector.detectLanguageWithConfidence(from: text) - guard let topResult = results.first else { - return nil - } - - let azureLanguageCode = LanguageDetector.convertToAzureLanguageCode(topResult.language) - let languageName = LanguageDetector.getLanguageName(from: topResult.language) - - return [ - "languageCode": azureLanguageCode, - "confidence": topResult.confidence, - "languageName": languageName, - "originalCode": topResult.language - ] - } + /** * 开始连续语音识别 * @@ -249,28 +196,7 @@ public class AzureAsrHelper: NSObject { return false } } - // /** - // * 获取检测到的语言 - // */ - // private func getDetectedLanguage(from result: SPXSpeechRecognitionResult) -> String { - // if !isAutoDetectLanguage { - // return "" - // } - - // // 尝试从属性中获取语言 - // if let properties = result.properties, - // let language = properties.getPropertyByName("SpeechServiceResponse_RecognitionLanguage") { - // return language - // } - - // // 尝试另一种方式获取语言 - // do { - // let langResult = try SPXAutoDetectSourceLanguageResult(result) - // return langResult.language ?? "" - // } catch { - // return "" - // } - // } + /** * 停止连续语音识别 @@ -388,11 +314,20 @@ public class AzureAsrHelper: NSObject { // 创建识别器 if isAutoDetectLanguage { // 使用正确的初始化方法 - let autoDetectConfig = try SPXAutoDetectSourceLanguageConfiguration(supportedLanguages) - print("autoDetectConfig: \(autoDetectConfig)") + guard let autoDetectSourceLanguageConfig = try? SPXAutoDetectSourceLanguageConfiguration(supportedLanguages) else { + print("创建自动语言检测配置失败,使用默认语言配置") + // 如果创建失败,使用默认的单语言配置 + recognizer = try SPXSpeechRecognizer( + speechConfiguration: speechConfig!, + audioConfiguration: audioConfig! + ) + return true + } + + print("autoDetectConfig: \(autoDetectSourceLanguageConfig)") recognizer = try SPXSpeechRecognizer( speechConfiguration: speechConfig!, - autoDetectSourceLanguageConfiguration: autoDetectConfig, + autoDetectSourceLanguageConfiguration: autoDetectSourceLanguageConfig, audioConfiguration: audioConfig! ) } else { @@ -484,60 +419,7 @@ public class AzureAsrHelper: NSObject { return currentLanguage } - /** - * 针对翻译场景的性能优化配置 - */ - private func optimizeForTranslation() { - guard let speechConfig = speechConfig else { return } - - // 启用实时转录,减少延迟 - speechConfig.setPropertyTo("true", byName: "SpeechServiceConnection_EnableAudioLogging") - speechConfig.setPropertyTo("INTERACTIVE", byName: "SpeechServiceConnection_RecoMode") - speechConfig.setPropertyTo("PCM", byName: "Speech_PushStreamFormat") - // 优化语音端点检测,适合翻译场景 - speechConfig.setPropertyTo("300", byName: "SpeechServiceConnection_InitialSilenceTimeoutMs") - speechConfig.setPropertyTo("500", byName: "SpeechServiceConnection_EndSilenceTimeoutMs") - speechConfig.setPropertyTo("500", byName: "Speech_SegmentationSilenceTimeoutMs") - // 启用标点符号,提高翻译质量 - speechConfig.setPropertyTo("true", byName: "SpeechServiceResponse_RequestProfanityFilterTrueFalse") - // 设置分段策略为时间模式 - speechConfig.setPropertyTo("Time", byName: "Speech_SegmentationStrategy") - - // 【优化】语言检测配置 - 减少检测延迟,实现快速响应 - if isAutoDetectLanguage { - // 降低语言检测置信度阈值,提高检测灵敏度 - speechConfig.setPropertyTo("0.3", byName: "SpeechServiceConnection_AutoDetectSourceLanguageConfidenceThreshold") - - // 使用Continuous模式,在每句话结束后重新检测语言 - speechConfig.setPropertyTo("Continuous", byName: "SpeechServiceConnection_LanguageIdMode") - - // 启用快速语言检测 - speechConfig.setPropertyTo("true", byName: "SpeechServiceConnection_EnableLanguageDetectionFast") - - // 【关键优化】设置语言检测的最小音频长度为100毫秒,实现快速响应 - speechConfig.setPropertyTo("100", byName: "SpeechServiceConnection_LanguageDetectionMinAudioLengthMs") - - // 【新增】设置语言检测的最大等待时间,避免长时间等待 - speechConfig.setPropertyTo("2000", byName: "SpeechServiceConnection_LanguageDetectionMaxWaitTimeMs") - - // 强制在每个语音段开始时重新检测语言 - speechConfig.setPropertyTo("true", byName: "SpeechServiceConnection_ForceLanguageDetectionOnNewSegment") - - // 【新增】启用句子级别的语言检测 - speechConfig.setPropertyTo("true", byName: "SpeechServiceConnection_EnableLanguageDetectionPerSentence") - - // 【新增】设置语言检测的频率 - 每个识别结果后都检测 - speechConfig.setPropertyTo("PerResult", byName: "SpeechServiceConnection_LanguageDetectionFrequency") - - // 【新增】启用实时语言检测,减少缓冲延迟 - speechConfig.setPropertyTo("true", byName: "SpeechServiceConnection_EnableRealtimeLanguageDetection") - - // 【新增】设置语言检测的采样间隔为200毫秒 - speechConfig.setPropertyTo("200", byName: "SpeechServiceConnection_LanguageDetectionSamplingIntervalMs") - } - - os_log("翻译场景优化配置已应用,包含语言检测优化", log: log, type: .info) - } + /** * 动态切换语言(翻译场景中的语言切换) @@ -588,13 +470,16 @@ public class AzureAsrHelper: NSObject { let result = event.result guard !(result.text?.isEmpty ?? true) else { return } - // let detectedLanguage = self.getDetectedLanguage(from: result) - - // 检测文本语言,避免重复调用 - let detectedLanguage = self.detectTextLanguage(result.text ?? "") ?? "zh-CN" + // 使用现有的 getDetectedLanguage 方法替代 result.language + let detectedLanguage = self.getDetectedLanguage(from: result) + let text = result.text + + print("正在识别事件=检测到语言: \(detectedLanguage), 识别中: \(text)") + // // 检测文本语言,避免重复调用 + // let detectedLanguage = self.detectTextLanguage(result.text ?? "") ?? "zh-CN" - // 输出检测到的语言信息 - print("检测到的语言: \(detectedLanguage)") + // // 输出检测到的语言信息 + // print("检测到的语言: \(detectedLanguage)") DispatchQueue.main.async { callback.onRecognizing(result.text ?? "", detectedLanguage) @@ -608,11 +493,16 @@ public class AzureAsrHelper: NSObject { guard result.reason == .recognizedSpeech, !(result.text?.isEmpty ?? true) else { return } + // 使用现有的 getDetectedLanguage 方法替代 result.language + let detectedLanguage = self.getDetectedLanguage(from: result) + let text = result.text + + print("识别完成事件=检测到语言: \(detectedLanguage), 识别中: \(text)") // 检测文本语言,避免重复调用 - let detectedLanguage = self.detectTextLanguage(result.text ?? "") ?? "zh-CN" + // let detectedLanguage = self.detectTextLanguage(result.text ?? "") ?? "zh-CN" - // 输出检测到的语言信息 - print("检测到的语言: \(detectedLanguage)") + // // 输出检测到的语言信息 + // print("检测到的语言: \(detectedLanguage)") // 在主线程回调结果 DispatchQueue.main.async { @@ -1175,3 +1065,54 @@ private func runMicrophoneCapture() { func onError(_ error: String) } } + +// //ios 语言检测 +// /** +// * 检测输入文本的语言,从支持的语言列表中选择最匹配的 +// * +// * @param text 需要检测的文本 +// * @return 从supportedLanguages中选择的最匹配语言代码 +// */ +// public func detectTextLanguage(_ text: String) -> String? { +// // 如果只有一种支持的语言,直接返回 +// if supportedLanguages.count == 1 { +// return supportedLanguages[0] +// } + +// // 获取所有可能的语言及其置信度 +// let languageResults = LanguageDetector.detectLanguageWithConfidence(from: text) + +// // 遍历检测结果,找到第一个在supportedLanguages中的语言 +// for result in languageResults { +// let azureLanguageCode = LanguageDetector.convertToAzureLanguageCode(result.language) +// if supportedLanguages.contains(azureLanguageCode) { +// return azureLanguageCode +// } +// } + +// // 如果没有找到匹配的语言,返回第一个支持的语言作为默认值 +// return supportedLanguages.first +// } + +// /** +// * 检测文本语言并返回详细信息 +// * +// * @param text 需要检测的文本 +// * @return 包含语言代码、置信度和语言名称的字典 +// */ +// public func detectTextLanguageWithDetails(_ text: String) -> [String: Any]? { +// let results = LanguageDetector.detectLanguageWithConfidence(from: text) +// guard let topResult = results.first else { +// return nil +// } + +// let azureLanguageCode = LanguageDetector.convertToAzureLanguageCode(topResult.language) +// let languageName = LanguageDetector.getLanguageName(from: topResult.language) + +// return [ +// "languageCode": azureLanguageCode, +// "confidence": topResult.confidence, +// "languageName": languageName, +// "originalCode": topResult.language +// ] +// } \ No newline at end of file From ebb170c93a97dd8e74374f208bcbc16267e84664 Mon Sep 17 00:00:00 2001 From: lxm <1274844347@qq.com> Date: Sat, 2 Aug 2025 17:21:07 +0800 Subject: [PATCH 02/11] =?UTF-8?q?refactor(splash):=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E5=90=AF=E5=8A=A8=E6=B5=81=E7=A8=8B=E5=B9=B6=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E7=8A=B6=E6=80=81=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 使用单一反应式变量 `_canNavigate` 替代多个布尔标记 - 将并行任务封装为独立方法并通过 `Future.wait` 协调 - 新增 `AppConfig.isInitialized()` 和 `User.isLoggedIn()` 方法 - 清理用户会话时增加本地存储和内存数据的清除 - 简化导航逻辑,直接检查最终数据状态 --- lib/data/models/appconfig.dart | 11 + lib/data/models/user_Info.dart | 22 +- .../splash/controllers/splash_controller.dart | 376 ++++++------------ 3 files changed, 147 insertions(+), 262 deletions(-) diff --git a/lib/data/models/appconfig.dart b/lib/data/models/appconfig.dart index 19dcf0565..22a09d84a 100644 --- a/lib/data/models/appconfig.dart +++ b/lib/data/models/appconfig.dart @@ -4,9 +4,20 @@ import '../../../data/models/appconfig_model.dart'; class AppConfig { static late UserGetAppConfigResp config; + + // 新增一个私有的静态布尔值来跟踪初始化状态 + static bool _isInitialized = false; + + /// 检查AppConfig单例是否已经被初始化 + /// 外部通过调用此方法来安全地检查状态 + static bool isInitialized() { + return _isInitialized; + } + // 可选:提供初始化方法 static void initialize(UserGetAppConfigResp resp) { config = resp; + _isInitialized = true; // <-- 新增此行 } //环境变量 diff --git a/lib/data/models/user_Info.dart b/lib/data/models/user_Info.dart index e5efa62ec..d5d369ba6 100644 --- a/lib/data/models/user_Info.dart +++ b/lib/data/models/user_Info.dart @@ -1,9 +1,15 @@ -import 'dart:ffi'; +import 'package:get_storage/get_storage.dart'; +import '../../../core/utils/logger.dart'; class User { // 单例实例 static User? _instance; + /// 安全地检查用户是否已经登录(即单例是否已初始化) + static bool isLoggedIn() { + return _instance != null; + } + // 单例访问器(增加空安全保护) static User get instance { if (_instance == null) { @@ -12,6 +18,18 @@ class User { return _instance!; } + // --- 新增 --- + /// 清理所有与用户会话相关的数据 + static Future clearUserSession() async { + final GetStorage storage = GetStorage(); + // 1. 清理本地存储 + await storage.remove('logintoken'); + await storage.remove('user_info'); + // 2. 清理内存中的单例 + _instance = null; + Logger.i("User", "用户会话数据已安全清理。"); + } + // 最终属性(不可变) String name; String uid; @@ -72,7 +90,7 @@ class User { // 修复1:安全获取嵌套的 user 字段 final userData = json['user'] as Map?; // 允许空值 final devicesData = json['devices'] as List?; // 允许空值 - print("数据获取fronTokenJson${userData} ${devicesData}"); + print("数据获取fronTokenJson$userData $devicesData"); // 更新单例实例 return _instance = User._( token: '', diff --git a/lib/modules/splash/controllers/splash_controller.dart b/lib/modules/splash/controllers/splash_controller.dart index 013ba187e..5c90dacca 100644 --- a/lib/modules/splash/controllers/splash_controller.dart +++ b/lib/modules/splash/controllers/splash_controller.dart @@ -1,15 +1,14 @@ import 'dart:async'; import 'dart:io'; import 'package:app_tracking_transparency/app_tracking_transparency.dart'; - -import '../../../data/models/appconfig.dart'; -import '../../../data/models/appconfig_model.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:get_storage/get_storage.dart'; + +import '../../../data/models/appconfig.dart'; +import '../../../data/models/appconfig_model.dart'; import '../../../data/models/user_Info.dart'; import '../../../routes/app_routes.dart'; -import '../../../core/themes/theme_service.dart'; import '../../../data/services/network/api.dart'; import '../../../data/services/version_update_service.dart'; import '../../../modules/common/widgets/version_update_dialog.dart'; @@ -24,191 +23,174 @@ class SplashController extends GetxController { final _loadingProgress = 0.0.obs; double get loadingProgress => _loadingProgress.value; - // 各项任务完成状态 - final _isTrackingPermissionRequested = false.obs; // 追踪权限请求完成 - final _isVersionCheckCompleted = false.obs; // 版本检查完成 - final _isTokenValidated = false.obs; // Token验证完成 - final _isAppConfigLoaded = false.obs; // 应用配置加载完成 - final _isAnimationCompleted = false.obs; // 动画完成 - - // 登录状态 - final _isLoggedIn = false.obs; - bool get isLoggedIn => _isLoggedIn.value; - - final _hasAcceptedPrivacy = false.obs; //是否同意隐私政策 - - // 版本号 - //final String version = 'V1.0.0'; - - // 主题服务 - final ThemeService _themeService = ThemeService(); - - // 会话级别的版本检查标记(静态变量,整个应用会话期间保持) - static bool _hasCheckedVersionInSession = false; - - // 版本检查相关常量 - static const String _lastVersionCheckKey = 'last_version_check_timestamp'; - static const int _versionCheckIntervalHours = 24; // 24小时间隔 + // 使用一个反应式变量来决定是否可以导航,替代所有布尔标记 + final _canNavigate = false.obs; @override void onInit() { super.onInit(); - - // 首先检查隐私政策状态 - final hasAcceptedPrivacy = _storage.read('hasAcceptedPrivacy') ?? false; - WidgetsBinding.instance.addPostFrameCallback((_) { - if (!hasAcceptedPrivacy) { - // 优先显示隐私政策弹窗,暂停其他初始化 - _showPrivacyDialog(); - } else { - // 已同意隐私政策,正常启动流程 - _startNormalFlow(); - } - }); - - // 注意:追踪权限请求将在用户同意隐私政策后执行 - } - - /// 异步请求追踪权限 - Future _requestTrackingPermissionAsync() async { - try { - await AppTrackingTransparency.requestTrackingAuthorization(); - Logger.d(_tag, '用户追踪权限请求完成'); - } catch (e) { - Logger.e(_tag, '用户追踪权限请求失败: $e'); - } finally { - _isTrackingPermissionRequested.value = true; + // 监听 _canNavigate 状态,一旦为 true,立即且仅执行一次导航 + once(_canNavigate, (_) => _navigateToTargetPage()); + // 启动整个初始化流程 + _initializeApp(); + } + + /// 启动并协调所有初始化任务 + Future _initializeApp() async { + // 步骤 1: 检查并等待隐私政策同意 + final hasAccepted = await _checkAndRequestPrivacy(); + if (!hasAccepted) { + // 如果用户拒绝隐私政策,应用会退出,后续代码不执行 + return; } - } - void _startNormalFlow() { - _startLoadingAnimation(); - _startParallelTasks(); - } + // 步骤 2: 并行执行所有启动任务,并等待它们全部完成 + // Future.wait 会等待数组中所有的 Future 都完成后才继续 + await Future.wait([ + _startLoadingAnimation(), // 任务1: 最小动画时长 + _performParallelTasks(), // 任务2: 其他所有后台任务 + ]); - /// 启动并行任务:在1.5秒动画期间同时执行多个任务 - void _startParallelTasks() { - // 并行执行所有任务 - _requestTrackingPermissionAsync(); - _performVersionCheckAsync(); - _validateTokenAsync(); + // 步骤 3: 所有任务(动画和后台)都已完成,触发导航 + _canNavigate.value = true; } - /// 异步执行版本检查 - Future _performVersionCheckAsync() async { - try { - Logger.d(_tag, '开始执行版本检查'); - await _checkVersionUpdateIfNeeded(); - } catch (e) { - Logger.e(_tag, '版本检查失败: $e'); + /// 检查隐私政策,如果未同意,则显示弹窗并等待用户操作 + /// 返回用户是否同意 + Future _checkAndRequestPrivacy() async { + if (_storage.read('hasAcceptedPrivacy') ?? false) { + return true; } - //finally { - // _isVersionCheckCompleted.value = true; - // } - } - - void _onPrivacyAccepted() { - _storage.write('hasAcceptedPrivacy', true); - Get.back(); // 关闭弹窗 - _startNormalFlow(); // 开始正常流程(已包含版本检查) - } - void _onPrivacyRejected() { - exit(0); // 退出应用 - } + final completer = Completer(); + WidgetsBinding.instance.addPostFrameCallback((_) { + PrivacyPolicyDialog.show( + onAccept: () { + _storage.write('hasAcceptedPrivacy', true); + Get.back(); // 关闭弹窗 + if (!completer.isCompleted) completer.complete(true); + }, + onReject: () { + if (!completer.isCompleted) completer.complete(false); + exit(0); // 退出应用 + }, + canDismiss: false, + ); + }); - // 显示隐私政策弹窗 - void _showPrivacyDialog() { - PrivacyPolicyDialog.show( - onAccept: () { - _onPrivacyAccepted(); - _hasAcceptedPrivacy.value = true; - }, - onReject: _onPrivacyRejected, - canDismiss: false, // 禁止通过返回键关闭弹窗 - ); + return completer.future; } - // 进度条动画(持续1.5秒) - void _startLoadingAnimation() { + /// 动画任务:返回一个在动画结束时完成的 Future + Future _startLoadingAnimation() { const totalDuration = 1500; // 动画总时长1.5秒 - const steps = totalDuration ~/ 30; // 30ms间隔,更平滑的动画 + const steps = totalDuration ~/ 30; const stepValue = 1.0 / steps; + final completer = Completer(); Timer.periodic(const Duration(milliseconds: 30), (timer) { if (_loadingProgress.value < 1.0) { _loadingProgress.value += stepValue; } else { + _loadingProgress.value = 1.0; timer.cancel(); - _isAnimationCompleted.value = true; - _checkNavigateCondition(); + if (!completer.isCompleted) completer.complete(); } }); + + return completer.future; + } + + /// 将所有并行的后台任务打包成一个 Future + Future _performParallelTasks() async { + // 同样使用 Future.wait,确保此函数在所有子任务完成后才结束 + await Future.wait([ + _requestTrackingPermissionAsync(), + _validateTokenAsync(), + _performVersionCheckAsync(), + ]); + Logger.d(_tag, '所有后台任务已完成'); + } + + /// 异步请求追踪权限 + Future _requestTrackingPermissionAsync() async { + try { + await AppTrackingTransparency.requestTrackingAuthorization(); + Logger.d(_tag, '用户追踪权限请求完成'); + } catch (e) { + Logger.e(_tag, '用户追踪权限请求失败: $e'); + // 即使失败,也视为完成,不阻塞主流程 + } } /// 异步验证Token和加载应用配置 Future _validateTokenAsync() async { try { - bool isLoggedIn = false; final token = _storage.read('logintoken'); - Logger.d(_tag, 'Token验证开始: $token'); - if (token != null && token.isNotEmpty) { - isLoggedIn = await getLoginToken(); + final isLoggedIn = await getLoginToken(); if (isLoggedIn) { + // 只有登录成功,才获取App配置 await getAppConfig(); - _isAppConfigLoaded.value = true; + } else { + await User.clearUserSession(); // } } - - _isLoggedIn.value = isLoggedIn; - Logger.d(_tag, 'Token验证完成,登录状态: $isLoggedIn'); } catch (e) { - Logger.e(_tag, 'Token验证失败: $e'); - _isLoggedIn.value = false; - } finally { - _isTokenValidated.value = true; + Logger.e(_tag, 'Token验证或配置加载失败: $e'); + await User.clearUserSession(); } } - // 检查跳转条件:所有任务都完成后才跳转 - void _checkNavigateCondition() { - if (_isAnimationCompleted.value && - _isTrackingPermissionRequested.value && - _isVersionCheckCompleted.value && - _isTokenValidated.value) { - _navigateToTargetPage(); + /// 异步执行版本检查 + Future _performVersionCheckAsync() async { + try { + final versionService = VersionUpdateService.to; + final updateInfo = await versionService.checkForUpdate(); + + if (updateInfo.hasUpdate) { + Logger.i(_tag, '发现新版本: ${updateInfo.latestVersion}'); + final completer = Completer(); + // 在主线程中显示更新对话框 + WidgetsBinding.instance.addPostFrameCallback((_) { + // 使用 whenComplete 确保无论弹窗如何关闭,Future 都会完成 + VersionUpdateDialog.show(updateInfo).whenComplete(() { + if (!completer.isCompleted) completer.complete(); + }); + }); + // 等待弹窗交互结束 + await completer.future; + } else { + Logger.i(_tag, '当前已是最新版本'); + } + } catch (e) { + Logger.e(_tag, '版本检查失败: $e'); + // 即使检查失败,也视为任务完成 } } - // 根据登录状态跳转到目标页面 + /// 根据最终状态跳转到目标页面 void _navigateToTargetPage() { - if (_isLoggedIn.value && _isAppConfigLoaded.value) { + // 导航时直接检查最终的数据状态,而不是依赖中间变量 + final token = _storage.read('logintoken'); + final isAppConfigInitialized = AppConfig.isInitialized(); + // 检查登录状态和配置初始化状态 + if (token != null && token.isNotEmpty && isAppConfigInitialized) { Get.offAllNamed(Routes.pairing); } else { Get.offAllNamed(Routes.login); } } + /// 异步获取登录Token Future getLoginToken() async { try { - Logger.d(_tag, 'getLoginToken:进来了'); - //if (User.getname() != "") { final response = await Api.tokenlogin(); - Logger.d(_tag, 'getLoginToken: $response'); - if (response == null) return false; - - if (response['user'] != '') { - Logger.d(_tag, 'getLoginToken:进来了'); - // 根据API设计调整 + if (response?['user'] != null && response['user'] != '') { final user = User.fromJson(response); - - // 存储用户信息(示例) await _storage.write('user_info', user.toJson()); return true; } - //return true; - //} return false; } catch (e) { Logger.e(_tag, 'Token获取失败: $e'); @@ -218,140 +200,14 @@ class SplashController extends GetxController { Future getAppConfig() async { try { - Logger.d(_tag, 'getAppConfig:进来了'); - //if (User.getname() != "") { final response = await Api.getappconfig(); - Logger.d(_tag, 'getAppConfig: $response'); if (response == null) return false; - // 根据API设计调整 final resp = UserGetAppConfigResp.fromJson(response); - Logger.d(_tag, 'getAppConfig:进来了'); - // 存储用户信息(示例) - // _storage.write('ENV', resp.env); - // _storage.write('AGENTS', resp.agents); - // _storage.write('MCPS', resp.mcps); AppConfig.initialize(resp); return true; } catch (e) { - Logger.e(_tag, 'getAppConfig 取失败: $e'); + Logger.e(_tag, 'getAppConfig 获取失败: $e'); return false; } } - - /// 混合检查机制:判断是否需要进行版本检查 - /// 检查条件:1. 会话内未检查过 OR 2. 距离上次检查超过24小时 - Future _checkVersionUpdateIfNeeded() async { - try { - final now = DateTime.now(); - final lastCheckTimestamp = _storage.read(_lastVersionCheckKey); - - bool shouldCheck = false; - String reason = ''; - - // 条件1:会话内未检查过 - if (!_hasCheckedVersionInSession) { - shouldCheck = true; - reason = '会话内首次检查'; - } - // 条件2:距离上次检查超过24小时 - else if (lastCheckTimestamp != null) { - final lastCheckTime = - DateTime.fromMillisecondsSinceEpoch(lastCheckTimestamp); - final timeDifference = now.difference(lastCheckTime); - - if (timeDifference.inHours >= _versionCheckIntervalHours) { - shouldCheck = true; - reason = '距离上次检查已超过${_versionCheckIntervalHours}小时'; - } - } - // 条件3:从未检查过(首次使用应用) - else { - shouldCheck = true; - reason = '首次使用应用'; - } - - if (shouldCheck) { - Logger.i(_tag, '开始版本检查 - 原因: $reason'); - await _performVersionCheck(); - } else { - Logger.i(_tag, '跳过版本检查 - 会话内已检查且未超过时间间隔'); - } - } catch (e) { - Logger.e(_tag, '版本检查判断失败: $e'); - } - } - - /// 执行版本检查(在后台进行,不阻塞启动流程) - Future _performVersionCheck() async { - try { - final versionService = VersionUpdateService.to; - // 异步执行,不等待结果 - versionService.checkForUpdate().then((updateInfo) { - if (updateInfo.hasUpdate) { - Logger.i(_tag, '发现新版本: ${updateInfo.latestVersion}'); - // 在主线程中显示更新对话框 - WidgetsBinding.instance.addPostFrameCallback((_) { - VersionUpdateDialog.show(updateInfo).then((_) { - // 无论用户选择更新还是稍后提醒,都标记版本检查完成 - _isVersionCheckCompleted.value = true; - _checkNavigateCondition(); - }); - }); - } else { - _isVersionCheckCompleted.value = true; - Logger.i(_tag, '当前已是最新版本: ${updateInfo.currentVersion}'); - } - - // 更新检查记录 - _updateVersionCheckRecord(); - }).catchError((e) { - Logger.e(_tag, '版本检查失败: $e'); - // 即使检查失败,也标记版本检查完成,让流程继续 - _isVersionCheckCompleted.value = true; - // 更新检查记录,避免频繁重试 - _updateVersionCheckRecord(); - }); - } catch (e) { - Logger.e(_tag, '版本检查失败: $e'); - // 即使检查失败,也标记版本检查完成,让流程继续 - _isVersionCheckCompleted.value = true; - // 更新检查记录,避免频繁重试 - _updateVersionCheckRecord(); - } - } - - /// 更新版本检查记录 - void _updateVersionCheckRecord() { - try { - // 更新时间戳 - final now = DateTime.now(); - _storage.write(_lastVersionCheckKey, now.millisecondsSinceEpoch); - - // 标记本次会话已检查过版本 - _hasCheckedVersionInSession = true; - - Logger.i(_tag, '版本检查记录已更新: ${now.toIso8601String()}'); - } catch (e) { - Logger.e(_tag, '更新版本检查记录失败: $e'); - } - } - - /// 重置版本检查标记(可用于强制重新检查) - /// 注意:这会清除会话标记,但不会清除时间戳记录 - static void resetVersionCheckFlag() { - _hasCheckedVersionInSession = false; - Logger.i('SplashCtrl', '版本检查会话标记已重置'); - } - - /// 完全重置版本检查状态(包括时间戳) - /// 用于测试或特殊情况下强制重新检查 - void forceResetVersionCheck() { - try { - _hasCheckedVersionInSession = false; - _storage.remove(_lastVersionCheckKey); - Logger.i(_tag, '版本检查状态已完全重置'); - } catch (e) { - Logger.e(_tag, '重置版本检查状态失败: $e'); - } - } } From dc8f8f1d0b7796b9d6a7e94e303a660150eabc36 Mon Sep 17 00:00:00 2001 From: tanlongsheng <252620078@qq.com> Date: Sat, 2 Aug 2025 17:59:42 +0800 Subject: [PATCH 03/11] =?UTF-8?q?=E4=BF=AE=E6=94=B9ai=E5=8A=A9=E6=89=8B?= =?UTF-8?q?=E5=BC=95=E5=AF=BC=E9=A1=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/core/widgets/intro_overlay_builder.dart | 71 ++++ lib/core/widgets/select_location_map.dart | 15 - lib/data/services/novice_guide_service.dart | 43 ++- lib/modules/agent/views/agent_view.dart | 343 ++++++++++++-------- 4 files changed, 325 insertions(+), 147 deletions(-) create mode 100644 lib/core/widgets/intro_overlay_builder.dart delete mode 100644 lib/core/widgets/select_location_map.dart diff --git a/lib/core/widgets/intro_overlay_builder.dart b/lib/core/widgets/intro_overlay_builder.dart new file mode 100644 index 000000000..5df0ba579 --- /dev/null +++ b/lib/core/widgets/intro_overlay_builder.dart @@ -0,0 +1,71 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_intro/flutter_intro.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; + +class IntroOverlayBuilder { + static Widget aiBuilder( + StepWidgetParams params, + String text, { + bool alignment = true, + Function? onNext, + Function? onFinish, + }) { + return Column( + crossAxisAlignment: + alignment ? CrossAxisAlignment.start : CrossAxisAlignment.end, + children: [ + Padding( + padding: EdgeInsets.only(bottom: 10.w), + child: Text( + text, + style: TextStyle( + fontSize: 15.sp, + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + ), + Row( + children: [ + if (onNext != null) + _button( + '确定输入', + () { + params.onNext!(); + onNext(); + }, + ), + if (onFinish != null) + _button( + '下一步', + () { + params.onFinish(); + onFinish(); + }, + ), + ], + ), + ], + ); + } + + static Widget _button(String text, Function() onTap) { + return GestureDetector( + onTap: onTap, + child: Container( + padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 5.w), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20.r), + border: Border.all(color: Colors.white), + ), + child: Text( + text, + style: TextStyle( + fontSize: 14.sp, + color: Colors.white, + ), + ), + ), + ); + } +} diff --git a/lib/core/widgets/select_location_map.dart b/lib/core/widgets/select_location_map.dart deleted file mode 100644 index c783901ed..000000000 --- a/lib/core/widgets/select_location_map.dart +++ /dev/null @@ -1,15 +0,0 @@ -import 'package:flutter/material.dart'; - -class SelectLocationMap extends StatefulWidget { - const SelectLocationMap({super.key}); - - @override - State createState() => _SelectLocationMapState(); -} - -class _SelectLocationMapState extends State { - @override - Widget build(BuildContext context) { - return const Placeholder(); - } -} diff --git a/lib/data/services/novice_guide_service.dart b/lib/data/services/novice_guide_service.dart index aeb3baa39..b875236d8 100644 --- a/lib/data/services/novice_guide_service.dart +++ b/lib/data/services/novice_guide_service.dart @@ -6,7 +6,48 @@ class NoviceGuideService extends GetxService { final GetStorage _storage = GetStorage(); bool isHomeGuide = true; - bool isAIGuide = true; + bool isAIGuide = false; + + int aiStep = 0; + + List aiStepsList = [ + { + 'start': '对小言说:“今天天气怎么样”', + 'input': '今天天气怎么样', + 'send': '小言会为你实时为你播报天气', + 'end': '小言能实时为你播报天气,试试其他指令吧', + }, + { + 'start': '对小言说:“请播放一组摇滚乐”', + 'input': '请播放一组摇滚乐', + 'send': '小言会为你搜寻到一组音乐', + 'end': '小言已成功为您搜寻到一组音乐,试试其他指令吧', + }, + { + 'start': '对小言说:“导航到深圳机场”', + 'input': '导航到深圳机场', + 'send': '小言会为你规划路线', + 'end': '小言已成功为您导航到深圳机场,试试其他指令吧', + }, + { + 'start': '对小言说:“明天下午3点创建会议提醒”', + 'input': '明天下午3点创建会议提醒', + 'send': '小言会为你创建会议提醒', + 'end': '小言已成功为您创建会议提醒,试试其他指令吧', + }, + { + 'start': '对小言说:“拨打妈妈的电话”', + 'input': '拨打妈妈的电话', + 'send': '小言会为你拨打电话', + 'end': '小言已成功为您拨打电话,试试其他指令吧', + }, + { + 'start': '对小言说:“给爸爸发短信说晚上回家吃饭’”', + 'input': '给爸爸发短信说晚上回家吃饭’', + 'send': '小言会为你发送短信', + 'end': '短信已发送,语音操控更便捷', + }, + ]; @override void onInit() { diff --git a/lib/modules/agent/views/agent_view.dart b/lib/modules/agent/views/agent_view.dart index d989cd037..3a89e0f01 100644 --- a/lib/modules/agent/views/agent_view.dart +++ b/lib/modules/agent/views/agent_view.dart @@ -4,6 +4,7 @@ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter_intro/flutter_intro.dart'; import 'package:get/get.dart'; +import '../../../core/widgets/intro_overlay_builder.dart'; import '../controllers/agent_controller.dart'; import '../controllers/message_action_controller.dart'; import 'message_bubble.dart'; @@ -197,47 +198,79 @@ class _AgentViewState extends State with WidgetsBindingObserver { children: [ // 聊天内容 Expanded( - child: Obx( - () => Align( - alignment: Alignment.topCenter, - child: ListView.builder( - controller: controller.scrollController, - shrinkWrap: true, - reverse: true, - padding: EdgeInsets.symmetric( - vertical: 16.h, horizontal: 16.w), - itemCount: controller.messages.length + - (controller.hasMoreMessages.value ? 1 : 0), - itemBuilder: (context, index) { - // 如果是最后一项且有更多消息,显示加载指示器 - if (index == controller.messages.length && - controller.hasMoreMessages.value) { - return _buildLoadMoreIndicator(); - } - - final message = controller - .messages[controller.messages.length - 1 - index]; - return Padding( - padding: EdgeInsets.only(bottom: 8.h), - child: GestureDetector( - onLongPressStart: - (LongPressStartDetails details) { - messageActionController.onLongPressMessage( - context, isDarkMode, details, message); - }, - child: MessageBubble( - isUser: message.isUser, - message: message.text, - timestamp: message.timestamp, - isRecognizing: message.isRecognizing, - hasImage: message.hasImage, - imagePath: message.imagePath, - meta: message.meta, - ), - )); + child: Stack( + children: [ + IntroStepBuilder( + group: 'ai', + order: 3, + overlayBuilder: (params) { + return IntroOverlayBuilder.aiBuilder( + params, + controller.noviceGuideService.aiStepsList[ + controller.noviceGuideService.aiStep]['end'], + onFinish: () async { + if (controller.noviceGuideService.aiStep < 5) { + await Future.delayed( + const Duration(milliseconds: 300)); + controller.noviceGuideService.aiStep++; + Intro.of(Get.context!) + .start(group: 'ai', reset: true); + } + }, + ); }, + builder: (BuildContext context, GlobalKey key) => Align( + alignment: Alignment.bottomCenter, + child: SizedBox( + key: key, + width: double.infinity, + height: 430.w, + ), + ), ), - ), + Obx( + () => Align( + alignment: Alignment.topCenter, + child: ListView.builder( + controller: controller.scrollController, + shrinkWrap: true, + reverse: true, + padding: EdgeInsets.symmetric( + vertical: 16.h, horizontal: 16.w), + itemCount: controller.messages.length + + (controller.hasMoreMessages.value ? 1 : 0), + itemBuilder: (context, index) { + // 如果是最后一项且有更多消息,显示加载指示器 + if (index == controller.messages.length && + controller.hasMoreMessages.value) { + return _buildLoadMoreIndicator(); + } + + final message = controller.messages[ + controller.messages.length - 1 - index]; + return Padding( + padding: EdgeInsets.only(bottom: 8.h), + child: GestureDetector( + onLongPressStart: + (LongPressStartDetails details) { + messageActionController.onLongPressMessage( + context, isDarkMode, details, message); + }, + child: MessageBubble( + isUser: message.isUser, + message: message.text, + timestamp: message.timestamp, + isRecognizing: message.isRecognizing, + hasImage: message.hasImage, + imagePath: message.imagePath, + meta: message.meta, + ), + )); + }, + ), + ), + ), + ], ), ), @@ -392,7 +425,23 @@ class _AgentViewState extends State with WidgetsBindingObserver { return IntroStepBuilder( group: 'ai', order: 1, - text: '对小言说“今天天气怎么样”', + overlayBuilder: (params) { + return IntroOverlayBuilder.aiBuilder( + params, + controller.noviceGuideService + .aiStepsList[ + controller.noviceGuideService + .aiStep]['start'], + onNext: () { + controller.textController.text = + controller.noviceGuideService + .aiStepsList[ + controller + .noviceGuideService + .aiStep]['input']; + }, + ); + }, onWidgetLoad: () { controller.noviceGuideService .startAIGuide(); @@ -630,111 +679,143 @@ class _AgentViewState extends State with WidgetsBindingObserver { SizedBox(width: 8.w), // 动态按钮区域:展开按钮 + 发送按钮 或 图片选择按钮 - Obx(() { - final hasText = controller.hasTextInput.value; - final hasImage = - controller.isImageInputActive.value; - final shouldShowSend = hasText || hasImage; - final shouldShowExpand = - controller.shouldShowExpandButton.value; + Builder(builder: (context) { + StepWidgetParams? widgetParams; + return IntroStepBuilder( + group: 'ai', + order: 2, + onHighlightWidgetTap: () { + widgetParams!.onNext!(); + controller.sendMessage(); + }, + overlayBuilder: (params) { + widgetParams = params; + return IntroOverlayBuilder.aiBuilder( + params, + '点击发送,${controller.noviceGuideService.aiStepsList[controller.noviceGuideService.aiStep]['send']}', + alignment: false, + ); + }, + builder: + (BuildContext context, GlobalKey key) => + Obx( + key: key, + () { + final hasText = + controller.hasTextInput.value; + final hasImage = + controller.isImageInputActive.value; + final shouldShowSend = + hasText || hasImage; + final shouldShowExpand = controller + .shouldShowExpandButton.value; - if (shouldShowSend) { - // 显示发送按钮(可能还有展开按钮) - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - // 展开按钮(在发送按钮上方) - if (shouldShowExpand && hasText) - Padding( - padding: - EdgeInsets.only(bottom: 40.h), - child: GestureDetector( + if (shouldShowSend) { + // 显示发送按钮(可能还有展开按钮) + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + // 展开按钮(在发送按钮上方) + if (shouldShowExpand && hasText) + Padding( + padding: EdgeInsets.only( + bottom: 40.h), + child: GestureDetector( + onTap: () { + // 打开底部弹出编辑模式 + controller + .openFullScreenEdit(); + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: + Colors.transparent, + builder: (context) => + const FullScreenEditor(), + ).then((_) { + // 弹出页关闭时的回调 + controller + .closeFullScreenEdit(); + }); + }, + child: Container( + width: 32.w, + height: 32.w, + decoration: BoxDecoration( + color: isDarkMode + ? Colors.grey[700] + : Colors.grey[300], + borderRadius: + BorderRadius.circular( + 16.w), + ), + child: Icon( + Icons.expand_less, + color: isDarkMode + ? Colors.grey[300] + : Colors.grey[700], + size: 20.sp, + ), + ), + ), + ), + // 发送按钮 + GestureDetector( + onTap: () => + controller.sendMessage(), + child: Container( + width: 44.w, + height: 44.w, + decoration: BoxDecoration( + color: primaryColor, + borderRadius: + BorderRadius.circular( + 22.w), + ), + child: Icon( + Icons.send, + color: Colors.white, + size: 22.sp, + ), + ), + ), + ], + ); + } else { + // 显示'+'选择按钮 + return Obx(() { + final isExpanded = controller + .isPlusOptionsExpanded.value; + return GestureDetector( onTap: () { - // 打开底部弹出编辑模式 - controller.openFullScreenEdit(); - showModalBottomSheet( - context: context, - isScrollControlled: true, - backgroundColor: - Colors.transparent, - builder: (context) => - const FullScreenEditor(), - ).then((_) { - // 弹出页关闭时的回调 - controller - .closeFullScreenEdit(); - }); + controller.togglePlusOptions(); }, child: Container( - width: 32.w, - height: 32.w, + width: 44.w, + height: 44.w, decoration: BoxDecoration( color: isDarkMode - ? Colors.grey[700] - : Colors.grey[300], + ? Colors.grey[800] + : Colors.grey[200], borderRadius: - BorderRadius.circular(16.w), + BorderRadius.circular(22.w), ), child: Icon( - Icons.expand_less, + isExpanded + ? Icons.close + : Icons.add, color: isDarkMode ? Colors.grey[300] : Colors.grey[700], - size: 20.sp, + size: 22.sp, ), ), - ), - ), - // 发送按钮 - GestureDetector( - onTap: () => controller.sendMessage(), - child: Container( - width: 44.w, - height: 44.w, - decoration: BoxDecoration( - color: primaryColor, - borderRadius: - BorderRadius.circular(22.w), - ), - child: Icon( - Icons.send, - color: Colors.white, - size: 22.sp, - ), - ), - ), - ], - ); - } else { - // 显示'+'选择按钮 - return Obx(() { - final isExpanded = - controller.isPlusOptionsExpanded.value; - return GestureDetector( - onTap: () { - controller.togglePlusOptions(); - }, - child: Container( - width: 44.w, - height: 44.w, - decoration: BoxDecoration( - color: isDarkMode - ? Colors.grey[800] - : Colors.grey[200], - borderRadius: - BorderRadius.circular(22.w), - ), - child: Icon( - isExpanded ? Icons.close : Icons.add, - color: isDarkMode - ? Colors.grey[300] - : Colors.grey[700], - size: 22.sp, - ), - ), - ); - }); - } + ); + }); + } + }, + ), + ); }), ], ), From 5e560b255c2025a1c23f8f7441aa21ad681b7d87 Mon Sep 17 00:00:00 2001 From: tanlongsheng <252620078@qq.com> Date: Sat, 2 Aug 2025 19:13:01 +0800 Subject: [PATCH 04/11] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E5=BC=95=E5=AF=BC?= =?UTF-8?q?=E9=A1=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/core/widgets/intro_overlay_builder.dart | 5 +-- lib/data/services/novice_guide_service.dart | 5 +-- lib/modules/agent/views/agent_view.dart | 1 + lib/modules/home/views/home_view.dart | 35 ++++++++++++++++----- 4 files changed, 35 insertions(+), 11 deletions(-) diff --git a/lib/core/widgets/intro_overlay_builder.dart b/lib/core/widgets/intro_overlay_builder.dart index 5df0ba579..aab3dae94 100644 --- a/lib/core/widgets/intro_overlay_builder.dart +++ b/lib/core/widgets/intro_overlay_builder.dart @@ -6,6 +6,7 @@ class IntroOverlayBuilder { static Widget aiBuilder( StepWidgetParams params, String text, { + String buttonText = '下一步', bool alignment = true, Function? onNext, Function? onFinish, @@ -29,7 +30,7 @@ class IntroOverlayBuilder { children: [ if (onNext != null) _button( - '确定输入', + buttonText, () { params.onNext!(); onNext(); @@ -37,7 +38,7 @@ class IntroOverlayBuilder { ), if (onFinish != null) _button( - '下一步', + buttonText, () { params.onFinish(); onFinish(); diff --git a/lib/data/services/novice_guide_service.dart b/lib/data/services/novice_guide_service.dart index b875236d8..12faf1a2c 100644 --- a/lib/data/services/novice_guide_service.dart +++ b/lib/data/services/novice_guide_service.dart @@ -6,7 +6,7 @@ class NoviceGuideService extends GetxService { final GetStorage _storage = GetStorage(); bool isHomeGuide = true; - bool isAIGuide = false; + bool isAIGuide = true; int aiStep = 0; @@ -56,8 +56,9 @@ class NoviceGuideService extends GetxService { // isAIGuide = _storage.read('ai_guide') ?? false; } - void startHomeGuide() { + void startHomeGuide() async { if (!isHomeGuide) { + await Future.delayed(const Duration(milliseconds: 200)); isHomeGuide = true; _storage.write('home_guide', true); Intro.of(Get.context!).start(group: 'home'); diff --git a/lib/modules/agent/views/agent_view.dart b/lib/modules/agent/views/agent_view.dart index 3a89e0f01..849baa4cc 100644 --- a/lib/modules/agent/views/agent_view.dart +++ b/lib/modules/agent/views/agent_view.dart @@ -432,6 +432,7 @@ class _AgentViewState extends State with WidgetsBindingObserver { .aiStepsList[ controller.noviceGuideService .aiStep]['start'], + buttonText: '确定输入', onNext: () { controller.textController.text = controller.noviceGuideService diff --git a/lib/modules/home/views/home_view.dart b/lib/modules/home/views/home_view.dart index f842b6a23..6660fbafa 100644 --- a/lib/modules/home/views/home_view.dart +++ b/lib/modules/home/views/home_view.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_intro/flutter_intro.dart'; import 'package:get/get.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; +import '../../../core/widgets/intro_overlay_builder.dart'; import '../controllers/home_controller.dart'; import '../../../routes/app_routes.dart'; import '../../../core/widgets/common_bottom_nav.dart'; @@ -84,31 +85,46 @@ class HomeView extends GetView { _buildAppBar(isDarkMode), // 顶部图片 + _buildEarbudsImage(), + + SizedBox(height: 20.h), + + // 设备状态卡片 IntroStepBuilder( group: 'home', order: 1, - text: '欢迎使用Voitrans!首先,让我们唤醒你的专属 AI—— 小言', onWidgetLoad: () { controller.noviceGuideService.startHomeGuide(); }, + overlayBuilder: (params) { + return IntroOverlayBuilder.aiBuilder( + params, + '欢迎使用Voitrans!首先,让我们唤醒你的专属 AI—— 小言', + onNext: () {}, + ); + }, builder: (BuildContext context, GlobalKey key) => SizedBox( key: key, - child: _buildEarbudsImage(), + child: _buildDeviceStatusCard(isDarkMode), ), ), + SizedBox(height: 20.h), - // 设备状态卡片 IntroStepBuilder( group: 'home', order: 2, - text: '对着麦克风说“小言小言”', + overlayBuilder: (params) { + return IntroOverlayBuilder.aiBuilder( + params, + '对着麦克风说“小言小言”', + onNext: () {}, + ); + }, builder: (BuildContext context, GlobalKey key) => SizedBox( key: key, - child: _buildDeviceStatusCard(isDarkMode), ), ), - SizedBox(height: 20.h), // 热门AI体验 _buildSectionTitle(isDarkMode), @@ -645,11 +661,16 @@ class HomeView extends GetView { child: IntroStepBuilder( group: 'home', order: 3, - text: '太棒了!你已成功唤醒小言,接下来试试让它帮你做件事吧', onHighlightWidgetTap: () { Intro.of(Get.context!).dispose(); controller.exploreAIFeatures(); }, + overlayBuilder: (params) { + return IntroOverlayBuilder.aiBuilder( + params, + '太棒了!你已成功唤醒小言,接下来试试让它帮你做件事吧', + ); + }, builder: (BuildContext context, GlobalKey key) => _buildFeatureCard( key: key, title: 'aiAssistant'.tr, // AI全能助手 From 21f31874eea11dcf3123e2ac74b4a28ac5311fdd Mon Sep 17 00:00:00 2001 From: fdp <1286779656@qq.com> Date: Sat, 2 Aug 2025 19:14:27 +0800 Subject: [PATCH 05/11] =?UTF-8?q?opus=E8=A7=A3=E7=A0=81=E5=BC=82=E5=B8=B8?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ble_service/SwiftOpusAudioProcessor.swift | 59 ++++++++++++++----- 1 file changed, 45 insertions(+), 14 deletions(-) diff --git a/local_plugins/ble_service/ios/ble_service/Sources/ble_service/SwiftOpusAudioProcessor.swift b/local_plugins/ble_service/ios/ble_service/Sources/ble_service/SwiftOpusAudioProcessor.swift index a07e91f72..1067a9211 100644 --- a/local_plugins/ble_service/ios/ble_service/Sources/ble_service/SwiftOpusAudioProcessor.swift +++ b/local_plugins/ble_service/ios/ble_service/Sources/ble_service/SwiftOpusAudioProcessor.swift @@ -26,22 +26,32 @@ class SwiftOpusAudioProcessor: NSObject { cleanup() } + /// 线程安全的解码器初始化 private func initializeOpusDecoder() -> Bool { + objc_sync_enter(self) + defer { objc_sync_exit(self) } + if decoder != nil { return true } var error: Int32 = 0 - decoder = opus_decoder_create(sampleRate, channels, &error) + let newDecoder = opus_decoder_create(sampleRate, channels, &error) - guard error == OPUS_OK, decoder != nil else { + guard error == OPUS_OK, newDecoder != nil else { + print("Opus解码器创建失败,错误代码: \(error)") return false } + decoder = newDecoder return true } + /// 线程安全的解码器销毁 private func destroyOpusDecoder() { + objc_sync_enter(self) + defer { objc_sync_exit(self) } + if let decoder = decoder { opus_decoder_destroy(decoder) self.decoder = nil @@ -112,23 +122,44 @@ class SwiftOpusAudioProcessor: NSObject { } } - // 简化的 Opus 解码方法,重点是容错处理 + /// 改进的 Opus 解码方法,增强容错和线程安全 private func decodeOpusPacket(_ packet: Data) { - guard let decoder = decoder else { return } + // 添加线程安全锁 + objc_sync_enter(self) + defer { objc_sync_exit(self) } + // 验证解码器状态 + guard let decoder = decoder, isDecodingActive else { + print("解码器未初始化或已停止") + return + } + let maxSamples = Int(maxFrameSize) var pcmBuffer = [opus_int16](repeating: 0, count: maxSamples * Int(channels)) let decodedSamples = packet.withUnsafeBytes { packetPtr in - let uint8Ptr = packetPtr.bindMemory(to: UInt8.self).baseAddress - return opus_decode( - decoder, - uint8Ptr, - opus_int32(packet.count), - &pcmBuffer, - Int32(maxSamples), - 0 - ) + // 确保指针有效 + guard let uint8Ptr = packetPtr.bindMemory(to: UInt8.self).baseAddress else { + print("无法获取数据包指针") + return opus_int32(-1) + } + + // 添加异常捕获机制 + var result: opus_int32 = -1 + + // 使用 autoreleasepool 确保内存管理 + autoreleasepool { + result = opus_decode( + decoder, + uint8Ptr, + opus_int32(packet.count), + &pcmBuffer, + Int32(maxSamples), + 0 + ) + } + + return result } // 增强的错误处理:检查各种错误情况 @@ -237,4 +268,4 @@ class SwiftOpusAudioProcessor: NSObject { func getCurrentConfig() -> (sampleRate: Int32, channels: Int32) { return (sampleRate, channels) } -} +} From abf286475b54e6f71b61d739791b03949fe71c9d Mon Sep 17 00:00:00 2001 From: lxm <1274844347@qq.com> Date: Sun, 3 Aug 2025 13:59:19 +0800 Subject: [PATCH 06/11] =?UTF-8?q?fix(login):=20=E4=BF=AE=E5=A4=8D=E7=99=BB?= =?UTF-8?q?=E5=BD=95=E8=A7=86=E5=9B=BE=E7=82=B9=E5=87=BB=E7=A9=BA=E7=99=BD?= =?UTF-8?q?=E5=A4=84=E9=94=AE=E7=9B=98=E4=B8=8D=E6=94=B6=E8=B5=B7=E7=9A=84?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit refactor(login): 优化登录请求处理逻辑,增加超时和错误处理 - 为登录请求添加30秒超时控制 - 完善错误处理,增加用户友好的错误提示 - 添加finally块确保加载状态重置 refactor(version): 简化版本更新服务逻辑,默认使用官方下载 --- lib/data/services/version_update_service.dart | 42 +++---- .../login/controllers/login_controller.dart | 119 +++++++++++------- lib/modules/login/views/login_view.dart | 17 ++- 3 files changed, 105 insertions(+), 73 deletions(-) diff --git a/lib/data/services/version_update_service.dart b/lib/data/services/version_update_service.dart index f2696b5b8..f624ae7aa 100644 --- a/lib/data/services/version_update_service.dart +++ b/lib/data/services/version_update_service.dart @@ -270,29 +270,29 @@ class VersionUpdateService extends GetxService { return false; } - final platformInfo = await getCurrentPlatformInfo(); + //final platformInfo = await getCurrentPlatformInfo(); try { - switch (platformInfo.storeType) { - case AppStoreType.appStore: - return await _openAppStore(); - case AppStoreType.googlePlay: - return await _openGooglePlay(); - case AppStoreType.huawei: - return await _openHuaweiStore(); - case AppStoreType.xiaomi: - return await _openXiaomiStore(); - case AppStoreType.oppo: - return await _openOppoStore(); - case AppStoreType.vivo: - return await _openVivoStore(); - case AppStoreType.honor: - return await _openHonorStore(); - case AppStoreType.official: - return await _openOfficialDownload(updateInfo.appInfo!.address!); - default: - return await _openOfficialDownload(updateInfo.appInfo!.address!); - } + // switch (platformInfo.storeType) { + // case AppStoreType.appStore: + // return await _openAppStore(); + // case AppStoreType.googlePlay: + // return await _openGooglePlay(); + // case AppStoreType.huawei: + // return await _openHuaweiStore(); + // case AppStoreType.xiaomi: + // return await _openXiaomiStore(); + // case AppStoreType.oppo: + // return await _openOppoStore(); + // case AppStoreType.vivo: + // return await _openVivoStore(); + // case AppStoreType.honor: + // return await _openHonorStore(); + // case AppStoreType.official: + // return await _openOfficialDownload(updateInfo.appInfo!.address!); + // default: + return await _openOfficialDownload(updateInfo.appInfo!.address!); + // } } catch (e) { _logger.e('执行更新失败: $e'); return false; diff --git a/lib/modules/login/controllers/login_controller.dart b/lib/modules/login/controllers/login_controller.dart index 3cd59f5fd..5b67a8d8a 100644 --- a/lib/modules/login/controllers/login_controller.dart +++ b/lib/modules/login/controllers/login_controller.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import '../../../data/models/appconfig.dart'; import '../../../data/models/appconfig_model.dart'; import 'package:get/get.dart'; @@ -815,54 +817,79 @@ class LoginController extends GetxController { // 使用登陆请求服务器 Future requestLogin(Map params) async { - Logger().d("请求后台数据login=$params"); - var response = await Api.login({ - "mail": params["mail"], - "phone": params["phone"], - "stype": params["stype"], - "vcode": params["vcode"], - "ttoken": params["ttoken"], - "phonemodel": _appVersion + _countryCode + _userChannel.toString(), - "phonemac": params["phonemac"], - "name": params["name"], - "avatar": params["avatar"], - }); - if (response != null) { - Logger().d("请求后台数据login回复数据=$response"); - - // final instance = await GetStorage(); - await _storage.write('loginInfo', jsonEncode(response)); - // 2. 将响应数据转换为User对象 - try { - Logger().d('响应数据: ${response.toString()}'); // 调试输出原始数据 - - if (response['token'].toString() != '') { - // 新增:检查 user 字段是否存在且类型正确 - // if (response.containsKey('data') && response['data'] is Map) { - final user = User.fromJson(response); - Logger().d('用户初始化成功: ${response['token'].toString()}'); - await _storage.write("logintoken", response['token'].toString()); - // 存储到 SharedPreferences (仅存储用户数据) - //final prefs = await GetStorage(); - await _storage.write('user_info', user.toJson()); - - //获取app的配置信息 - await getAppConfig(); - Get.offAllNamed(Routes.pairing); - // } else { - // throw FormatException('API 响应缺少 user 字段'); - // } - } else { - throw Exception('接口返回错误: ${response['msg']}'); + try { + Logger().d("请求后台数据login=$params"); + // 在Api.login调用时添加超时 + var response = await Api.login({ + "mail": params["mail"], + "phone": params["phone"], + "stype": params["stype"], + "vcode": params["vcode"], + "ttoken": params["ttoken"], + "phonemodel": _appVersion + _countryCode + _userChannel.toString(), + "phonemac": params["phonemac"], + "name": params["name"], + "avatar": params["avatar"], + }).timeout( + const Duration(seconds: 30), // 30秒超时 + onTimeout: () { + throw TimeoutException('登录请求超时', const Duration(seconds: 30)); + }, + ); + if (response != null) { + Logger().d("请求后台数据login回复数据=$response"); + + await _storage.write('loginInfo', jsonEncode(response)); + + try { + Logger().d('响应数据: ${response.toString()}'); + + if (response['token'].toString() != '') { + final user = User.fromJson(response); + Logger().d('用户初始化成功: ${response['token'].toString()}'); + await _storage.write("logintoken", response['token'].toString()); + await _storage.write('user_info', user.toJson()); + + // 获取app的配置信息 + await getAppConfig(); + Get.offAllNamed(Routes.pairing); + } else { + throw Exception('接口返回错误: ${response['msg']}'); + } + } catch (e, stack) { + Logger().d('用户数据处理失败: $e'); + Logger().d('堆栈跟踪: $stack'); + Get.snackbar( + 'error'.tr, + 'userDataParseFailed'.tr, // 用户数据解析失败 + snackPosition: SnackPosition.TOP, + backgroundColor: Colors.red.withOpacity(0.1), + colorText: Colors.red, + ); } - } catch (e, stack) { - // 新增:打印堆栈跟踪 - Logger().d('用户数据处理失败: $e'); - Logger().d('堆栈跟踪: $stack'); - Get.snackbar('错误', '用户数据解析失败,请检查网络'); + } else { + // API返回null的情况 + Get.snackbar( + 'error'.tr, + 'networkError'.tr, // 网络错误,请重试 + snackPosition: SnackPosition.TOP, + backgroundColor: Colors.red.withOpacity(0.1), + colorText: Colors.red, + ); } - - // Get.offAll(() => IndexWidget()); + } catch (e) { + // 网络请求异常 + Logger().e('登录请求失败: $e'); + Get.snackbar( + 'error'.tr, + 'loginFailed'.tr, // 登录失败 + snackPosition: SnackPosition.TOP, + backgroundColor: Colors.red.withOpacity(0.1), + colorText: Colors.red, + ); + } finally { + // 确保无论成功还是失败都重置加载状态 + _isLoading.value = false; } } diff --git a/lib/modules/login/views/login_view.dart b/lib/modules/login/views/login_view.dart index 014740d9c..42318e96d 100644 --- a/lib/modules/login/views/login_view.dart +++ b/lib/modules/login/views/login_view.dart @@ -13,12 +13,17 @@ class LoginView extends GetView { Widget build(BuildContext context) { final isDarkMode = Theme.of(context).brightness == Brightness.dark; - return Scaffold( - backgroundColor: isDarkMode ? Colors.black : Colors.grey[50], - body: SafeArea( - child: Obx(() => controller.viewState == LoginViewState.welcome - ? _buildWelcomeView(isDarkMode) - : _buildVerificationView(context, isDarkMode)), + return GestureDetector( + onTap: () { + FocusScope.of(context).unfocus(); + }, + child: Scaffold( + backgroundColor: isDarkMode ? Colors.black : Colors.grey[50], + body: SafeArea( + child: Obx(() => controller.viewState == LoginViewState.welcome + ? _buildWelcomeView(isDarkMode) + : _buildVerificationView(context, isDarkMode)), + ), ), ); } From 6530748e4e891c4bee0bbb5a4ed0469929a53897 Mon Sep 17 00:00:00 2001 From: lxm <1274844347@qq.com> Date: Sun, 3 Aug 2025 15:24:39 +0800 Subject: [PATCH 07/11] =?UTF-8?q?fix(tts):=20=E5=9C=A8TTS=E6=92=AD?= =?UTF-8?q?=E6=94=BE=E6=97=B6=E5=81=9C=E6=AD=A2=E6=B0=94=E6=B3=A1=E9=9F=B3?= =?UTF-8?q?=E9=81=BF=E5=85=8D=E5=A3=B0=E9=9F=B3=E9=87=8D=E5=8F=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修改iOS和Android端的TTS播放逻辑,在开始流式TTS、播放完成、取消或出错时立即停止气泡音播放。避免两种声音同时播放造成干扰,提升用户体验。 --- .../yunqiinnovation/agent_service/AgentService.kt | 13 ++++++++++++- .../Sources/agent_service/AgentServiceImpl.swift | 3 ++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt index 1fdaea5df..a1921b405 100644 --- a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt +++ b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt @@ -316,6 +316,8 @@ object AgentService : CoroutineScope { _isTtsSpeaking.set(true) restartIdleCheck() sendEvent("tts_started", mapOf("status" to "started")) + // 停止气泡音,避免与TTS同时播放 + audioPlayer?.stopAudio() } TtsEventType.SYNTHESIS_COMPLETED -> { @@ -328,11 +330,15 @@ object AgentService : CoroutineScope { _isTtsSpeaking.set(false) restartIdleCheck() sendEvent("tts_canceled", mapOf("status" to "canceled")) + // TTS取消时停止气泡音 + audioPlayer?.stopAudio() } TtsEventType.PLAYBACK_STARTED -> { //开始AI播报内容 restartIdleCheck() sendEvent("playback_started", mapOf("status" to "playback_started")) + // 确保气泡音已停止,避免与TTS播放重叠 + audioPlayer?.stopAudio() // 发送AI说话开始广播 val intent = Intent("com.yunqiinnovation.music_service.AI_SAY_START").apply { @@ -355,6 +361,8 @@ object AgentService : CoroutineScope { TtsEventType.ERROR -> { _isTtsSpeaking.set(false) restartIdleCheck() + // TTS错误时停止气泡音 + audioPlayer?.stopAudio() val params = event.params val code = params["errorCode"] as? String ?: "UNKNOWN_ERROR" val message = params["errorMessage"] as? String ?: "未知错误" @@ -663,6 +671,8 @@ object AgentService : CoroutineScope { // 并行执行停止操作,加快响应速度 stopTts() stopAiStream() + // 停止气泡音,避免在打断后继续播放 + audioPlayer?.stopAudio() // 记录打断耗时 @@ -888,7 +898,8 @@ object AgentService : CoroutineScope { responseBuilder.append(token) if (speakResponse && reply && broadcast) { ttsService?.speakStream(token) - // audioPlayer?.stopAudio() + // 在开始流式TTS时立即停止气泡音 + audioPlayer?.stopAudio() } if (reply && token.isNotEmpty()) { // 发送流式回复token diff --git a/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift b/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift index bfac028cd..684616bc8 100644 --- a/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift +++ b/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift @@ -996,7 +996,8 @@ class ChatApiStreamCallback: StreamCallback { if speakResponse && reply && broadcast{ try agentService.azureTtsHelper?.speakStream(token) -// try agentService.audioPlayer?.stopAwaitSound() + // 在开始流式TTS时立即停止气泡音 + try agentService.audioPlayer?.stopAwaitSound() } if (reply && token != "") { try agentService.sendEvent(name: "assistant_token", data: ["token": token]) From 64039c0cd2b32f13de4970836e8534e58739d8fe Mon Sep 17 00:00:00 2001 From: lxm <1274844347@qq.com> Date: Sun, 3 Aug 2025 16:36:06 +0800 Subject: [PATCH 08/11] =?UTF-8?q?feat(home):=20=E4=BB=85=E5=9C=A8=E5=85=85?= =?UTF-8?q?=E7=94=B5=E7=9B=92=E6=9C=89=E7=94=B5=E4=B8=94=E8=AE=BE=E5=A4=87?= =?UTF-8?q?=E8=BF=9E=E6=8E=A5=E6=97=B6=E6=98=BE=E7=A4=BA=E5=85=85=E7=94=B5?= =?UTF-8?q?=E7=9B=92=E7=8A=B6=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 添加条件判断,当充电盒电量大于0且设备完全连接时才显示充电盒电量状态,避免在不必要的情况下显示充电盒信息 --- lib/modules/home/views/home_view.dart | 35 ++++++++++++++++----------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/lib/modules/home/views/home_view.dart b/lib/modules/home/views/home_view.dart index 6660fbafa..b332f5941 100644 --- a/lib/modules/home/views/home_view.dart +++ b/lib/modules/home/views/home_view.dart @@ -418,6 +418,10 @@ class HomeView extends GetView { final rightEarCharging = controller.rightEarCharging; final caseCharging = controller.caseCharging; + // 判断是否显示充电盒:充电盒电量大于0且设备已连接 + final shouldShowCase = + caseBattery.value > 0 && controller.isFullyConnectedRx.value; + // 使用更紧凑的布局 return Container( padding: EdgeInsets.symmetric(vertical: 8.h, horizontal: 4.w), @@ -451,20 +455,23 @@ class HomeView extends GetView { isDarkMode: isDarkMode, ), - // // 分隔线 - // Container( - // height: 30.h, - // width: 1, - // color: isDarkMode ? Colors.grey[800] : Colors.grey[300], - // ), - - // // 充电盒 - // _buildCompactBatteryItem( - // label: '充电盒', - // batteryLevel: caseBattery.value, - // isCharging: caseCharging.value, - // isDarkMode: isDarkMode, - // ), + // 只有在充电盒电量大于0且设备已连接时才显示充电盒 + if (shouldShowCase) ...[ + // 分隔线 + Container( + height: 30.h, + width: 1, + color: isDarkMode ? Colors.grey[800] : Colors.grey[300], + ), + + // 充电盒 + _buildCompactBatteryItem( + label: '充电盒', + batteryLevel: caseBattery.value, + isCharging: caseCharging.value, + isDarkMode: isDarkMode, + ), + ] ], ), ); From 8be5b27dcf224696ace745876c469266fec52cf7 Mon Sep 17 00:00:00 2001 From: lxm <1274844347@qq.com> Date: Sun, 3 Aug 2025 16:48:34 +0800 Subject: [PATCH 09/11] =?UTF-8?q?perf(agent=5Fservice):=20=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E9=9F=B3=E9=A2=91=E5=A4=84=E7=90=86=E5=92=8C=E4=BA=8B?= =?UTF-8?q?=E4=BB=B6=E5=88=86=E5=8F=91=E6=80=A7=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 针对高频识别事件优化JSON处理逻辑,减少内存分配 - 实现音频队列积压检测和自动清理机制 - 使用协程优化事件分发,避免主线程阻塞 - 增加BLE模式下的超时调整和编码器管理 - 添加详细的日志和错误处理 --- .../agent_service/AgentService.kt | 316 ++++++++++++++---- .../agent_service/AgentServicePlugin.kt | 42 ++- .../agent_service/AgentServiceImpl.swift | 112 +++++-- .../azure_speech/AzureAsrHelper.kt | 35 +- .../Sources/azure_speech/AzureAsrHelper.swift | 40 ++- 5 files changed, 424 insertions(+), 121 deletions(-) diff --git a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt index a1921b405..bb34a6d5c 100644 --- a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt +++ b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt @@ -15,6 +15,7 @@ import com.yunqiinnovation.user_portrait.UserPortrait import com.yunqiinnovation.location_service.LocationService import android.util.Log import kotlinx.coroutines.* +import kotlinx.coroutines.channels.Channel import java.util.Collections import kotlin.coroutines.CoroutineContext import com.yunqiinnovation.ble_service.BleService @@ -116,7 +117,7 @@ object AgentService : CoroutineScope { // 空闲检测相关 private var idleCheckJob: Job? = null - private val maxIdleSeconds = 10 // 最大空闲秒数 + private val maxIdleSeconds = 10 // 最大空闲秒数(保持原有设置) // 打断防抖相关 private var lastInterruptTime = 0L @@ -192,27 +193,31 @@ object AgentService : CoroutineScope { } // 初始化Azure ASR azureAsrHelper = AzureAsrHelper(context).apply { - // 使用协程处理异步初始化 - CoroutineScope(Dispatchers.IO).launch { - //获取语音识别的语种 - val supportedLanguagesConfig = config["supportedLanguages"] as? List - val supportedLanguages = supportedLanguagesConfig?.toTypedArray() - ?: arrayOf("zh-CN", "en-US") // 默认支持多语言 - - val asrInitSuccess = initialize( - subscriptionKey = config["azureSpeechKey"]?.toString() ?: "", - region = config["azureSpeechRegion"]?.toString() ?: "", - supportedLanguages = supportedLanguages, - audioSourceType = if (isExternalActive) AzureAsrHelper.AudioSourceType.EXTERNAL - else AzureAsrHelper.AudioSourceType.MICROPHONE - ) + // 使用AgentService的协程作用域,确保生命周期管理 + launch(Dispatchers.IO) { + try { + //获取语音识别的语种 + val supportedLanguagesConfig = config["supportedLanguages"] as? List + val supportedLanguages = supportedLanguagesConfig?.toTypedArray() + ?: arrayOf("zh-CN", "en-US") // 默认支持多语言 + + val asrInitSuccess = initialize( + subscriptionKey = config["azureSpeechKey"]?.toString() ?: "", + region = config["azureSpeechRegion"]?.toString() ?: "", + supportedLanguages = supportedLanguages, + audioSourceType = if (isExternalActive) AzureAsrHelper.AudioSourceType.EXTERNAL + else AzureAsrHelper.AudioSourceType.MICROPHONE + ) - if (asrInitSuccess) { - withContext(Dispatchers.Main) { - recognizeCallback() + if (asrInitSuccess) { + withContext(Dispatchers.Main) { + recognizeCallback() + } + } else { + Log.w(TAG, "Azure ASR初始化失败") } - } else { - Log.w(TAG, "Azure ASR初始化失败") + } catch (e: Exception) { + Log.e(TAG, "Azure ASR初始化异常", e) } } } @@ -244,35 +249,71 @@ object AgentService : CoroutineScope { } /** - * 释放所有资源 + * 释放所有资源 - 增强版本 */ fun dispose() { try { + Log.i(TAG, "开始释放AgentService资源...") + + // 1. 停止所有活动 stopRecognition() stopTts() stopAiStream() stopIdleCheck() + // 2. 停止事件处理器 + eventProcessorJob?.cancel() + eventProcessorJob = null + eventQueue.close() + + // 3. 释放音频播放器 audioPlayer?.release() audioPlayer = null - azureAsrHelper?.dispose() + // 4. 释放Azure ASR(确保完全清理) + azureAsrHelper?.let { helper -> + try { + helper.dispose() + } catch (e: Exception) { + Log.e(TAG, "释放Azure ASR异常", e) + } + } + azureAsrHelper = null - // 释放ChatAPI服务 + // 5. 释放ChatAPI服务 if (::chatApiService.isInitialized) { - chatApiService.cancelCurrentStream() - chatApiService.dispose() + try { + chatApiService.cancelCurrentStream() + chatApiService.dispose() + } catch (e: Exception) { + Log.e(TAG, "释放ChatAPI服务异常", e) + } } - // 释放TTS服务 - ttsService?.release() + // 6. 释放TTS服务 + ttsService?.let { tts -> + try { + tts.release() + } catch (e: Exception) { + Log.e(TAG, "释放TTS服务异常", e) + } + } ttsService = null + // 7. 取消所有协程并清理监听器 job.cancel() clearListeners() + + // 8. 重置状态 _isInitialized.set(false) + _isRecognitionActive.set(false) + _hasSpeechDetected.set(false) + _isTtsSpeaking.set(false) + _isAiStreaming.set(false) + + Log.i(TAG, "AgentService资源释放完成") } catch (e: Exception) { - Log.e(TAG, "释放资源异常: ${e.message}") + Log.e(TAG, "释放资源异常: ${e.message}", e) } } @@ -415,19 +456,31 @@ object AgentService : CoroutineScope { stopIdleCheck() // 先停止现有的检查 if (!isRecognitionActive) return + // BLE模式下使用稍长的超时时间,但不影响正常使用 + val timeoutSeconds = if (audioSourceType == AzureAsrHelper.AudioSourceType.EXTERNAL) { + maxIdleSeconds + 3 // BLE模式额外增加3秒,避免过度延长 + } else { + maxIdleSeconds + } + idleCheckJob = launch { try { - delay(maxIdleSeconds * 1000L) + Log.d(TAG, "启动空闲检测,超时时间: ${timeoutSeconds}秒,音频源: $audioSourceType") + delay(timeoutSeconds * 1000L) if (isActive && isRecognitionActive && !hasSpeechDetected && !isTtsSpeaking && !isAiStreaming) { + Log.i(TAG, "空闲超时,停止识别 - 超时时间: ${timeoutSeconds}秒") stopRecognition() sendEvent( "auto_stop", - mapOf("reason" to "idle_timeout", "seconds" to maxIdleSeconds) + mapOf("reason" to "idle_timeout", "seconds" to timeoutSeconds) ) + } else { + Log.d(TAG, "空闲检测结束,但条件不满足停止要求") } } catch (e: CancellationException) { // 协程被取消,正常行为 + Log.d(TAG, "空闲检测被取消") } catch (e: Exception) { Log.e(TAG, "空闲检测异常: ${e.message}") } @@ -460,26 +513,12 @@ object AgentService : CoroutineScope { Log.d(TAG, "服务未初始化") return false } - Log.d("TAG", "开始语音识别startRecognition?") + + Log.i(TAG, "开始语音识别 - 模式: ${if (isExternalActive) "BLE外部音频" else "设备麦克风"}") + + // 先停止当前识别 stopRecognition() - BleService.openEncoder() - // if (isRecognitionActive&&audioSourceType==AzureAsrHelper.AudioSourceType.MICROPHONE){ - // Log.d(TAG, "开始语音识别stopRecognition") - // // BleService.closeCodec() - // stopRecognition() - - // } - // if((isExternalActive&&audioSourceType==AzureAsrHelper.AudioSourceType.MICROPHONE)||!isRecognitionActive) - // { - // Log.d(TAG, "开始语音识别BleService.openEncoder") - // BleService.openEncoder() - - // } - // if(isRecognitionActive) - // { - // Log.d(TAG, "开始语音识别azureAsrHelper?.startContinuousRecognition(audioSourceType)") - // return true - // } + // 检查录音权限 if (!isExternalActive && !checkRecordAudioPermission()) { Log.e(TAG, "需要录音权限") @@ -487,22 +526,45 @@ object AgentService : CoroutineScope { return false } - _isRecognitionActive.set(true) - _hasSpeechDetected.set(false) + // 设置音频源类型 + audioSourceType = if (isExternalActive) { + AzureAsrHelper.AudioSourceType.EXTERNAL + } else { + AzureAsrHelper.AudioSourceType.MICROPHONE + } + + Log.i(TAG, "音频源类型设置为: $audioSourceType") try { - audioSourceType = if (isExternalActive) { - AzureAsrHelper.AudioSourceType.EXTERNAL - } else { - AzureAsrHelper.AudioSourceType.MICROPHONE + // 启动Azure ASR + val asrStarted = azureAsrHelper?.startContinuousRecognition(audioSourceType) ?: false + if (!asrStarted) { + Log.e(TAG, "Azure ASR启动失败") + return false + } + + // 如果是BLE模式,在ASR启动后再打开编码器 + if (isExternalActive) { + Log.i(TAG, "BLE模式:启动编码器") + // 使用协程延迟而不是阻塞主线程 + launch { + delay(50) // 给ASR一些启动时间 + val encoderOpened = BleService.openEncoder() + if (!encoderOpened) { + Log.w(TAG, "BLE编码器启动失败,但继续尝试识别") + } + } } - Log.d("TAG", "azureAsrHelper?.startContinuousRecognition") - azureAsrHelper?.startContinuousRecognition(audioSourceType) + _isRecognitionActive.set(true) + _hasSpeechDetected.set(false) + + Log.i(TAG, "语音识别启动成功") return true + } catch (e: Exception) { _isRecognitionActive.set(false) - Log.e(TAG, "启动语音识别失败: ${e.message}") + Log.e(TAG, "启动语音识别失败: ${e.message}", e) sendEvent( "error", mapOf( "code" to "RECOGNITION_START_ERROR", @@ -525,13 +587,17 @@ object AgentService : CoroutineScope { _hasSpeechDetected.set(true) if (!previousHasSpeech) { + Log.i(TAG, "首次检测到语音输入: $recognizing (音频源: $audioSourceType)") restartIdleCheck() } + // 实时发送识别结果,确保UI及时更新 + Log.v(TAG, "实时识别: $recognizing") sendEvent( "recognizing", mapOf( "text" to recognizing, - "language" to detectedLanguage + "language" to detectedLanguage, + "audioSource" to audioSourceType.toString() ) ) @@ -604,28 +670,42 @@ object AgentService : CoroutineScope { override fun onCanceled(reason: String, errorDetails: String) { _isRecognitionActive.set(false) stopIdleCheck() - Log.d(TAG, "recognition_canceled:") - BleService.closeCodec() + Log.w(TAG, "语音识别被取消: $reason, 详情: $errorDetails") + + // 如果是BLE模式,关闭编码器 + if (audioSourceType == AzureAsrHelper.AudioSourceType.EXTERNAL) { + BleService.closeCodec() + } + sendEvent( "recognition_canceled", mapOf( "reason" to reason, "details" to errorDetails ) ) + + // 重新设置回调,但不自动重启识别 recognizeCallback() } override fun onError(error: String) { _isRecognitionActive.set(false) stopIdleCheck() - BleService.closeCodec() - Log.d(TAG, "语音识别错误: $error") + Log.e(TAG, "语音识别错误: $error") + + // 如果是BLE模式,关闭编码器 + if (audioSourceType == AzureAsrHelper.AudioSourceType.EXTERNAL) { + BleService.closeCodec() + } + sendEvent( "error", mapOf( "code" to "RECOGNITION_ERROR", "message" to error ) ) + + // 重新设置回调,但不自动重启识别 recognizeCallback() } }) @@ -635,10 +715,68 @@ object AgentService : CoroutineScope { } } + // 音频数据推送统计 + private var audioDataCount = 0 + private var lastAudioDataTime = 0L + + // 性能监控 + private var lastMemoryCheckTime = 0L + private val MEMORY_CHECK_INTERVAL = 30000L // 30秒检查一次内存 + fun pushAudioData(data: ByteArray) { + val currentTime = System.currentTimeMillis() + audioDataCount++ + + // 每100个数据包或每5秒输出一次统计 + if (audioDataCount % 100 == 0 || (currentTime - lastAudioDataTime) > 5000) { + Log.i(TAG, "音频数据统计: 已推送${audioDataCount}包, 当前包大小: ${data.size} bytes, 音频源: $audioSourceType") + lastAudioDataTime = currentTime + + // 定期检查内存使用情况 + checkMemoryUsage(currentTime) + } + + // 添加调试日志,但保持原有逻辑的兼容性 + if (!isRecognitionActive) { + Log.w(TAG, "语音识别未激活,但仍尝试推送音频数据: ${data.size} bytes") + } + + if (audioSourceType != AzureAsrHelper.AudioSourceType.EXTERNAL) { + Log.w(TAG, "音频源类型不匹配: $audioSourceType,推送音频数据: ${data.size} bytes") + } + + // 保持原有逻辑,确保兼容性 azureAsrHelper?.audioStream?.saveAudioDataTo(data) } + /** + * 检查内存使用情况 + */ + private fun checkMemoryUsage(currentTime: Long) { + if (currentTime - lastMemoryCheckTime > MEMORY_CHECK_INTERVAL) { + lastMemoryCheckTime = currentTime + + val runtime = Runtime.getRuntime() + val usedMemory = runtime.totalMemory() - runtime.freeMemory() + val maxMemory = runtime.maxMemory() + val memoryUsagePercent = (usedMemory * 100 / maxMemory).toInt() + + Log.i(TAG, "内存使用情况: ${usedMemory / 1024 / 1024}MB / ${maxMemory / 1024 / 1024}MB (${memoryUsagePercent}%)") + + // 如果内存使用超过80%,发出警告 + if (memoryUsagePercent > 80) { + Log.w(TAG, "内存使用率过高: ${memoryUsagePercent}%,建议检查内存泄漏") + + // 发送内存警告事件 + sendEvent("memory_warning", mapOf( + "usedMemoryMB" to (usedMemory / 1024 / 1024), + "maxMemoryMB" to (maxMemory / 1024 / 1024), + "usagePercent" to memoryUsagePercent + )) + } + } + } + /** * 停止语音识别 */ @@ -1236,22 +1374,56 @@ object AgentService : CoroutineScope { } } - /** - * 发送事件 - */ - private fun sendEvent(eventName: String, data: Map) { - // 切换到主线程执行监听器回调,避免 UI 更新问题 - launch(Dispatchers.Main) { - listeners.forEach { listener -> + // 事件发送优化:使用单一协程处理事件队列 + private val eventQueue = Channel>>(capacity = Channel.UNLIMITED) + private var eventProcessorJob: Job? = null + + init { + // 启动事件处理协程 + startEventProcessor() + } + + private fun startEventProcessor() { + eventProcessorJob = launch(Dispatchers.Main) { + for ((eventName, data) in eventQueue) { try { - listener.onEvent(eventName, data) + // 创建监听器快照,避免并发修改 + val currentListeners = synchronized(listeners) { listeners.toList() } + + currentListeners.forEach { listener -> + try { + listener.onEvent(eventName, data) + } catch (e: Exception) { + Log.e(TAG, "向监听器发送事件失败: $eventName", e) + } + } } catch (e: Exception) { - Log.e(TAG, "向监听器发送事件失败", e) + Log.e(TAG, "处理事件失败: $eventName", e) } } } } + /** + * 发送事件 - 优化版本,使用事件队列避免主线程阻塞 + */ + private fun sendEvent(eventName: String, data: Map) { + try { + // 对于高频事件,进行限流处理 + if (eventName == "recognizing" && eventQueue.trySend(Pair(eventName, data)).isFailure) { + Log.v(TAG, "识别事件队列已满,跳过当前事件") + return + } + + // 非阻塞发送到事件队列 + if (!eventQueue.trySend(Pair(eventName, data)).isSuccess) { + Log.w(TAG, "事件队列已满,丢弃事件: $eventName") + } + } catch (e: Exception) { + Log.e(TAG, "发送事件异常: $eventName", e) + } + } + /** * 检查录音权限 */ diff --git a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt index 60b919843..dc5428345 100644 --- a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt +++ b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt @@ -286,19 +286,43 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream } /** - * 实现AgentServiceListener接口 + * 实现AgentServiceListener接口 - 优化版本 * 接收来自AgentService的事件,并转发到Flutter */ override fun onEvent(eventName: String, data: Map) { - // 将事件转换为JSON字符串 - val eventData = JSONObject().apply { - put("event", eventName) - put("data", JSONObject(data)) - } + try { + // 对于高频事件,进行优化处理 + val eventData = when (eventName) { + "recognizing" -> { + // 实时识别事件,简化数据结构 + JSONObject().apply { + put("event", eventName) + put("data", JSONObject().apply { + put("text", data["text"] ?: "") + put("language", data["language"] ?: "") + put("audioSource", data["audioSource"] ?: "") + }) + } + } + else -> { + // 其他事件,正常处理 + JSONObject().apply { + put("event", eventName) + put("data", JSONObject(data)) + } + } + } - // 在主线程发送事件到Flutter - MainScope().launch { - eventSink?.success(eventData.toString()) + // 使用协程在主线程发送事件 + MainScope().launch { + try { + eventSink?.success(eventData.toString()) + } catch (e: Exception) { + Log.e(TAG, "发送事件到Flutter失败: $eventName", e) + } + } + } catch (e: Exception) { + Log.e(TAG, "处理事件异常: $eventName", e) } } diff --git a/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift b/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift index 684616bc8..6854e8974 100644 --- a/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift +++ b/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift @@ -104,13 +104,34 @@ class AgentServiceImpl: NSObject { listenersLock.unlock() } + // 事件发送优化:使用队列避免主线程阻塞 + private let eventQueue = DispatchQueue(label: "com.yunqiinnovation.agent_service.events", qos: .userInitiated) + internal func sendEvent(name eventName: String, data: [String: Any]) { + // 对于高频事件,使用异步处理 + if eventName == "recognizing" { + eventQueue.async { [weak self] in + self?.dispatchEventToListeners(eventName: eventName, data: data) + } + } else { + // 其他事件在主线程处理 + DispatchQueue.main.async { [weak self] in + self?.dispatchEventToListeners(eventName: eventName, data: data) + } + } + } + + private func dispatchEventToListeners(eventName: String, data: [String: Any]) { listenersLock.lock() let currentListeners = self.listeners listenersLock.unlock() - + for listener in currentListeners { - listener.onEvent(eventName: eventName, data: data) + do { + listener.onEvent(eventName: eventName, data: data) + } catch { + os_log("发送事件失败: %{public}@", log: logger, type: .error, eventName) + } } } @@ -275,25 +296,39 @@ class AgentServiceImpl: NSObject { private func startIdleCheck() { stopIdleCheck() - + guard isRecognizing else { return } - + + // BLE模式下使用稍长的超时时间 + let timeoutSeconds: TimeInterval + if let azureHelper = azureAsrHelper, + azureHelper.audioSourceType == .external { + timeoutSeconds = maxIdleSeconds + 3 // BLE模式额外增加3秒 + } else { + timeoutSeconds = maxIdleSeconds + } + + os_log("启动空闲检测,超时时间: %.0f秒", log: logger, type: .debug, timeoutSeconds) + // 使用全局队列而不是主队列,避免后台挂起问题 let timer = DispatchSource.makeTimerSource(queue: DispatchQueue.global(qos: .utility)) idleTimer = timer - - timer.schedule(deadline: .now() + maxIdleSeconds) + + timer.schedule(deadline: .now() + timeoutSeconds) timer.setEventHandler { [weak self] in // 使用更安全的方式检查self是否存在 DispatchQueue.main.async { [weak self] in guard let self = self else { return } - + if self.isRecognizing && !self.hasSpeechDetected && !self.isSpeaking && !self.isAiStreaming { + os_log("空闲超时,停止识别 - 超时时间: %.0f秒", log: self.logger, type: .info, timeoutSeconds) self.stopRecognition() self.sendEvent(name: "auto_stop", data: [ - "reason": "idle_timeout", - "seconds": self.maxIdleSeconds + "reason": "idle_timeout", + "seconds": timeoutSeconds ]) + } else { + os_log("空闲检测结束,但条件不满足停止要求", log: self.logger, type: .debug) } } } @@ -315,34 +350,49 @@ class AgentServiceImpl: NSObject { sendError("服务未初始化", code: "NOT_INITIALIZED") return false } - + + os_log("开始语音识别 - 模式: %{public}@", log: logger, type: .info, useBle ? "BLE外部音频" : "设备麦克风") + if !useBle { guard AVAudioSession.sharedInstance().recordPermission == .granted else { sendError("无麦克风权限", code: "PERMISSION_DENIED") return false } } - + if isRecognizing { stopRecognition() } - + let audioSourceType: AzureAsrHelper.AudioSourceType = useBle ? .external : .microphone - + if useBle { AudioSessionHub.shared.begin(.playback) } else { AudioSessionHub.shared.begin(.voice) } -print("ai启动语音=\(audioSourceType)=\(useBle)") + + os_log("音频源类型设置为: %{public}@", log: logger, type: .info, audioSourceType == .external ? "EXTERNAL" : "MICROPHONE") + guard let success = azureAsrHelper?.startContinuousRecognition( - // callback: self, audioSourceType: audioSourceType ), success else { sendError("启动语音识别失败", code: "RECOGNITION_START_ERROR") return false } - + + // 如果是BLE模式,在ASR启动后再打开编码器 + if useBle { + os_log("BLE模式:启动编码器", log: logger, type: .info) + DispatchQueue.global().asyncAfter(deadline: .now() + 0.05) { + let encoderOpened = BleService.shared.openEncoder() + if !encoderOpened { + os_log("BLE编码器启动失败,但继续尝试识别", log: self.logger, type: .warning) + } + } + } + + os_log("语音识别启动成功", log: logger, type: .info) return true } func recognizeCallback(useBle: Bool = false) -> Bool { @@ -363,13 +413,15 @@ print("ai启动语音=\(audioSourceType)=\(useBle)") } func pushAudioData(_ audioData: Data) -> Bool { + // 添加调试日志,但保持兼容性 if !isRecognizing || !isInitialized { + os_log("语音识别未激活或服务未初始化,但仍尝试推送音频数据: %d bytes", log: logger, type: .debug, audioData.count) + } + + guard let audioStream = azureAsrHelper?.audioStream else { + os_log("音频流未初始化", log: logger, type: .error) return false } - guard let audioStream = azureAsrHelper?.audioStream else { - os_log("音频流未初始化", log: logger,type: .error) - return false -} audioStream.saveAudioDataTo(data: audioData) return true @@ -1398,6 +1450,8 @@ extension AgentServiceImpl: AzureAsrHelper.ContinuousRecognizeCallback { } func onCanceled(_ reason: String, _ errorDetails: String) { + os_log("语音识别被取消: %{public}@, 详情: %{public}@", log: logger, type: .warning, reason, errorDetails) + var data: [String: Any] = [:] if !reason.isEmpty { data["reason"] = reason @@ -1405,13 +1459,27 @@ extension AgentServiceImpl: AzureAsrHelper.ContinuousRecognizeCallback { if !errorDetails.isEmpty { data["details"] = errorDetails } - + + // 如果是BLE模式,关闭编码器 + if let azureHelper = azureAsrHelper, + azureHelper.audioSourceType == .external { + BleService.shared.closeCodec() + } + sendEvent(name: "recognition_canceled", data: data) isRecognizing = false stopIdleCheck() } - + func onError(_ error: String) { + os_log("语音识别错误: %{public}@", log: logger, type: .error, error) + + // 如果是BLE模式,关闭编码器 + if let azureHelper = azureAsrHelper, + azureHelper.audioSourceType == .external { + BleService.shared.closeCodec() + } + let data: [String: Any] = ["message": error.isEmpty ? "未知错误" : error] sendEvent(name: "error", data: data) isRecognizing = false diff --git a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt index 02d909d4a..d5b8dae6c 100644 --- a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt +++ b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt @@ -146,6 +146,17 @@ class AzureAsrHelper(private val context: Context) { // 设置分段策略为时间模式 setProperty("Speech_SegmentationStrategy", "Time") + + // 针对EXTERNAL音频源的实时性优化 + if (audioSourceType == AudioSourceType.EXTERNAL) { + // 减少缓冲延迟 + setProperty("SpeechServiceConnection_ReceiveTimeout", "1000") + setProperty("SpeechServiceConnection_SendTimeout", "1000") + // 启用实时模式 + setProperty("SpeechServiceConnection_EnableRealTimeRecognition", "true") + // 减少音频缓冲 + setProperty("SpeechServiceConnection_AudioBufferSizeMs", "100") + } } // 录音文件类 recordfile = RecordFile; @@ -706,7 +717,9 @@ class AzureAsrHelper(private val context: Context) { // 情况2:不在录制但队列有数据 -> 从队列获取 else if (writeQueue.isNotEmpty()) { data = writeQueue.poll() - Log.d("tag", "写入数据: ${data?.size}") + if (data != null) { + Log.v("tag", "从队列写入数据: ${data.size} bytes, 队列剩余: ${writeQueue.size}") + } bytesToWrite = data?.size ?: 0 } @@ -724,7 +737,8 @@ class AzureAsrHelper(private val context: Context) { Log.e("tag", "写入失败: ${e.message}") } } else { - Thread.yield() // 避免空转消耗CPU + // 使用更短的等待时间,提高响应性 + Thread.sleep(1) // 1ms等待,避免空转但保持响应性 } } } catch (e: Exception) { @@ -744,11 +758,22 @@ class AzureAsrHelper(private val context: Context) { * 外部音频输入 */ fun saveAudioDataTo(buffer: ByteArray) { - if (audioSourceType == AudioSourceType.MICROPHONE) return - // 放入队列,由写线程写入 - writeQueue.offer(buffer.copyOf()) + // 检查队列大小,避免积压 + if (writeQueue.size > 50) { // 限制队列大小 + Log.w("AudioStream", "音频队列积压严重(${writeQueue.size}),清理旧数据") + // 清理一半的旧数据,保持实时性 + repeat(writeQueue.size / 2) { + writeQueue.poll() + } + } + + // 放入队列,由写线程写入 + val offered = writeQueue.offer(buffer.copyOf()) + if (!offered) { + Log.w("AudioStream", "音频队列已满,丢弃数据包: ${buffer.size} bytes") + } } /** diff --git a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift index 3106c1236..d8ee80e97 100644 --- a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift +++ b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift @@ -690,22 +690,27 @@ public class AzureAsrHelper: NSObject { guard let self = self else { return } while self.isRunning { - // print("是否写入: \(self.isWriting)") if !self.isWriting { - usleep(10_000) + usleep(1_000) // 减少等待时间,提高响应性 continue } - - guard let dataToWrite = self.writeQueue.take() else { continue } - - // print("写入数据长度: \(dataToWrite.count)") - do { - try self.pushAudioStream?.write(dataToWrite) - self.onAudioData?(dataToWrite) -} catch { - print("推送音频数据失败: \(error.localizedDescription)") -} + guard let dataToWrite = self.writeQueue.take() else { + usleep(1_000) // 短暂等待,避免空转 + continue + } + + // 添加队列状态日志 + if self.writeQueue.count > 10 { + print("音频写入队列积压: \(self.writeQueue.count)") + } + + do { + try self.pushAudioStream?.write(dataToWrite) + self.onAudioData?(dataToWrite) + } catch { + print("推送音频数据失败: \(error.localizedDescription)") + } } } } @@ -720,7 +725,16 @@ public class AzureAsrHelper: NSObject { if audioSourceType != .external { return } - // print("外部data=\(data)") + + // 检查队列大小,避免积压 + if writeQueue.count > 50 { + os_log("音频队列积压严重(%d),清理旧数据", log: OSLog.default, type: .warning, writeQueue.count) + // 清理一半的旧数据,保持实时性 + for _ in 0..<(writeQueue.count / 2) { + _ = writeQueue.take() + } + } + // 放入队列,由写线程写入 writeQueue.put(data) } From 18e98e2fdcd76d7ef304ebdf22f9605ebfd2f2b1 Mon Sep 17 00:00:00 2001 From: lxm <1274844347@qq.com> Date: Mon, 4 Aug 2025 10:27:39 +0800 Subject: [PATCH 10/11] =?UTF-8?q?Revert=20"perf(agent=5Fservice):=20?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E9=9F=B3=E9=A2=91=E5=A4=84=E7=90=86=E5=92=8C?= =?UTF-8?q?=E4=BA=8B=E4=BB=B6=E5=88=86=E5=8F=91=E6=80=A7=E8=83=BD"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 89e99cbe670206d6beb08b65ccb8a8088ac186c3. --- .../agent_service/AgentService.kt | 316 ++++-------------- .../agent_service/AgentServicePlugin.kt | 42 +-- .../agent_service/AgentServiceImpl.swift | 112 ++----- .../azure_speech/AzureAsrHelper.kt | 35 +- .../Sources/azure_speech/AzureAsrHelper.swift | 40 +-- 5 files changed, 121 insertions(+), 424 deletions(-) diff --git a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt index bb34a6d5c..a1921b405 100644 --- a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt +++ b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt @@ -15,7 +15,6 @@ import com.yunqiinnovation.user_portrait.UserPortrait import com.yunqiinnovation.location_service.LocationService import android.util.Log import kotlinx.coroutines.* -import kotlinx.coroutines.channels.Channel import java.util.Collections import kotlin.coroutines.CoroutineContext import com.yunqiinnovation.ble_service.BleService @@ -117,7 +116,7 @@ object AgentService : CoroutineScope { // 空闲检测相关 private var idleCheckJob: Job? = null - private val maxIdleSeconds = 10 // 最大空闲秒数(保持原有设置) + private val maxIdleSeconds = 10 // 最大空闲秒数 // 打断防抖相关 private var lastInterruptTime = 0L @@ -193,31 +192,27 @@ object AgentService : CoroutineScope { } // 初始化Azure ASR azureAsrHelper = AzureAsrHelper(context).apply { - // 使用AgentService的协程作用域,确保生命周期管理 - launch(Dispatchers.IO) { - try { - //获取语音识别的语种 - val supportedLanguagesConfig = config["supportedLanguages"] as? List - val supportedLanguages = supportedLanguagesConfig?.toTypedArray() - ?: arrayOf("zh-CN", "en-US") // 默认支持多语言 - - val asrInitSuccess = initialize( - subscriptionKey = config["azureSpeechKey"]?.toString() ?: "", - region = config["azureSpeechRegion"]?.toString() ?: "", - supportedLanguages = supportedLanguages, - audioSourceType = if (isExternalActive) AzureAsrHelper.AudioSourceType.EXTERNAL - else AzureAsrHelper.AudioSourceType.MICROPHONE - ) + // 使用协程处理异步初始化 + CoroutineScope(Dispatchers.IO).launch { + //获取语音识别的语种 + val supportedLanguagesConfig = config["supportedLanguages"] as? List + val supportedLanguages = supportedLanguagesConfig?.toTypedArray() + ?: arrayOf("zh-CN", "en-US") // 默认支持多语言 + + val asrInitSuccess = initialize( + subscriptionKey = config["azureSpeechKey"]?.toString() ?: "", + region = config["azureSpeechRegion"]?.toString() ?: "", + supportedLanguages = supportedLanguages, + audioSourceType = if (isExternalActive) AzureAsrHelper.AudioSourceType.EXTERNAL + else AzureAsrHelper.AudioSourceType.MICROPHONE + ) - if (asrInitSuccess) { - withContext(Dispatchers.Main) { - recognizeCallback() - } - } else { - Log.w(TAG, "Azure ASR初始化失败") + if (asrInitSuccess) { + withContext(Dispatchers.Main) { + recognizeCallback() } - } catch (e: Exception) { - Log.e(TAG, "Azure ASR初始化异常", e) + } else { + Log.w(TAG, "Azure ASR初始化失败") } } } @@ -249,71 +244,35 @@ object AgentService : CoroutineScope { } /** - * 释放所有资源 - 增强版本 + * 释放所有资源 */ fun dispose() { try { - Log.i(TAG, "开始释放AgentService资源...") - - // 1. 停止所有活动 stopRecognition() stopTts() stopAiStream() stopIdleCheck() - // 2. 停止事件处理器 - eventProcessorJob?.cancel() - eventProcessorJob = null - eventQueue.close() - - // 3. 释放音频播放器 audioPlayer?.release() audioPlayer = null - // 4. 释放Azure ASR(确保完全清理) - azureAsrHelper?.let { helper -> - try { - helper.dispose() - } catch (e: Exception) { - Log.e(TAG, "释放Azure ASR异常", e) - } - } - azureAsrHelper = null + azureAsrHelper?.dispose() - // 5. 释放ChatAPI服务 + // 释放ChatAPI服务 if (::chatApiService.isInitialized) { - try { - chatApiService.cancelCurrentStream() - chatApiService.dispose() - } catch (e: Exception) { - Log.e(TAG, "释放ChatAPI服务异常", e) - } + chatApiService.cancelCurrentStream() + chatApiService.dispose() } - // 6. 释放TTS服务 - ttsService?.let { tts -> - try { - tts.release() - } catch (e: Exception) { - Log.e(TAG, "释放TTS服务异常", e) - } - } + // 释放TTS服务 + ttsService?.release() ttsService = null - // 7. 取消所有协程并清理监听器 job.cancel() clearListeners() - - // 8. 重置状态 _isInitialized.set(false) - _isRecognitionActive.set(false) - _hasSpeechDetected.set(false) - _isTtsSpeaking.set(false) - _isAiStreaming.set(false) - - Log.i(TAG, "AgentService资源释放完成") } catch (e: Exception) { - Log.e(TAG, "释放资源异常: ${e.message}", e) + Log.e(TAG, "释放资源异常: ${e.message}") } } @@ -456,31 +415,19 @@ object AgentService : CoroutineScope { stopIdleCheck() // 先停止现有的检查 if (!isRecognitionActive) return - // BLE模式下使用稍长的超时时间,但不影响正常使用 - val timeoutSeconds = if (audioSourceType == AzureAsrHelper.AudioSourceType.EXTERNAL) { - maxIdleSeconds + 3 // BLE模式额外增加3秒,避免过度延长 - } else { - maxIdleSeconds - } - idleCheckJob = launch { try { - Log.d(TAG, "启动空闲检测,超时时间: ${timeoutSeconds}秒,音频源: $audioSourceType") - delay(timeoutSeconds * 1000L) + delay(maxIdleSeconds * 1000L) if (isActive && isRecognitionActive && !hasSpeechDetected && !isTtsSpeaking && !isAiStreaming) { - Log.i(TAG, "空闲超时,停止识别 - 超时时间: ${timeoutSeconds}秒") stopRecognition() sendEvent( "auto_stop", - mapOf("reason" to "idle_timeout", "seconds" to timeoutSeconds) + mapOf("reason" to "idle_timeout", "seconds" to maxIdleSeconds) ) - } else { - Log.d(TAG, "空闲检测结束,但条件不满足停止要求") } } catch (e: CancellationException) { // 协程被取消,正常行为 - Log.d(TAG, "空闲检测被取消") } catch (e: Exception) { Log.e(TAG, "空闲检测异常: ${e.message}") } @@ -513,12 +460,26 @@ object AgentService : CoroutineScope { Log.d(TAG, "服务未初始化") return false } - - Log.i(TAG, "开始语音识别 - 模式: ${if (isExternalActive) "BLE外部音频" else "设备麦克风"}") - - // 先停止当前识别 + Log.d("TAG", "开始语音识别startRecognition?") stopRecognition() - + BleService.openEncoder() + // if (isRecognitionActive&&audioSourceType==AzureAsrHelper.AudioSourceType.MICROPHONE){ + // Log.d(TAG, "开始语音识别stopRecognition") + // // BleService.closeCodec() + // stopRecognition() + + // } + // if((isExternalActive&&audioSourceType==AzureAsrHelper.AudioSourceType.MICROPHONE)||!isRecognitionActive) + // { + // Log.d(TAG, "开始语音识别BleService.openEncoder") + // BleService.openEncoder() + + // } + // if(isRecognitionActive) + // { + // Log.d(TAG, "开始语音识别azureAsrHelper?.startContinuousRecognition(audioSourceType)") + // return true + // } // 检查录音权限 if (!isExternalActive && !checkRecordAudioPermission()) { Log.e(TAG, "需要录音权限") @@ -526,45 +487,22 @@ object AgentService : CoroutineScope { return false } - // 设置音频源类型 - audioSourceType = if (isExternalActive) { - AzureAsrHelper.AudioSourceType.EXTERNAL - } else { - AzureAsrHelper.AudioSourceType.MICROPHONE - } - - Log.i(TAG, "音频源类型设置为: $audioSourceType") + _isRecognitionActive.set(true) + _hasSpeechDetected.set(false) try { - // 启动Azure ASR - val asrStarted = azureAsrHelper?.startContinuousRecognition(audioSourceType) ?: false - if (!asrStarted) { - Log.e(TAG, "Azure ASR启动失败") - return false - } - - // 如果是BLE模式,在ASR启动后再打开编码器 - if (isExternalActive) { - Log.i(TAG, "BLE模式:启动编码器") - // 使用协程延迟而不是阻塞主线程 - launch { - delay(50) // 给ASR一些启动时间 - val encoderOpened = BleService.openEncoder() - if (!encoderOpened) { - Log.w(TAG, "BLE编码器启动失败,但继续尝试识别") - } - } + audioSourceType = if (isExternalActive) { + AzureAsrHelper.AudioSourceType.EXTERNAL + } else { + AzureAsrHelper.AudioSourceType.MICROPHONE } + Log.d("TAG", "azureAsrHelper?.startContinuousRecognition") + azureAsrHelper?.startContinuousRecognition(audioSourceType) - _isRecognitionActive.set(true) - _hasSpeechDetected.set(false) - - Log.i(TAG, "语音识别启动成功") return true - } catch (e: Exception) { _isRecognitionActive.set(false) - Log.e(TAG, "启动语音识别失败: ${e.message}", e) + Log.e(TAG, "启动语音识别失败: ${e.message}") sendEvent( "error", mapOf( "code" to "RECOGNITION_START_ERROR", @@ -587,17 +525,13 @@ object AgentService : CoroutineScope { _hasSpeechDetected.set(true) if (!previousHasSpeech) { - Log.i(TAG, "首次检测到语音输入: $recognizing (音频源: $audioSourceType)") restartIdleCheck() } - // 实时发送识别结果,确保UI及时更新 - Log.v(TAG, "实时识别: $recognizing") sendEvent( "recognizing", mapOf( "text" to recognizing, - "language" to detectedLanguage, - "audioSource" to audioSourceType.toString() + "language" to detectedLanguage ) ) @@ -670,42 +604,28 @@ object AgentService : CoroutineScope { override fun onCanceled(reason: String, errorDetails: String) { _isRecognitionActive.set(false) stopIdleCheck() - Log.w(TAG, "语音识别被取消: $reason, 详情: $errorDetails") - - // 如果是BLE模式,关闭编码器 - if (audioSourceType == AzureAsrHelper.AudioSourceType.EXTERNAL) { - BleService.closeCodec() - } - + Log.d(TAG, "recognition_canceled:") + BleService.closeCodec() sendEvent( "recognition_canceled", mapOf( "reason" to reason, "details" to errorDetails ) ) - - // 重新设置回调,但不自动重启识别 recognizeCallback() } override fun onError(error: String) { _isRecognitionActive.set(false) stopIdleCheck() - Log.e(TAG, "语音识别错误: $error") - - // 如果是BLE模式,关闭编码器 - if (audioSourceType == AzureAsrHelper.AudioSourceType.EXTERNAL) { - BleService.closeCodec() - } - + BleService.closeCodec() + Log.d(TAG, "语音识别错误: $error") sendEvent( "error", mapOf( "code" to "RECOGNITION_ERROR", "message" to error ) ) - - // 重新设置回调,但不自动重启识别 recognizeCallback() } }) @@ -715,68 +635,10 @@ object AgentService : CoroutineScope { } } - // 音频数据推送统计 - private var audioDataCount = 0 - private var lastAudioDataTime = 0L - - // 性能监控 - private var lastMemoryCheckTime = 0L - private val MEMORY_CHECK_INTERVAL = 30000L // 30秒检查一次内存 - fun pushAudioData(data: ByteArray) { - val currentTime = System.currentTimeMillis() - audioDataCount++ - - // 每100个数据包或每5秒输出一次统计 - if (audioDataCount % 100 == 0 || (currentTime - lastAudioDataTime) > 5000) { - Log.i(TAG, "音频数据统计: 已推送${audioDataCount}包, 当前包大小: ${data.size} bytes, 音频源: $audioSourceType") - lastAudioDataTime = currentTime - - // 定期检查内存使用情况 - checkMemoryUsage(currentTime) - } - - // 添加调试日志,但保持原有逻辑的兼容性 - if (!isRecognitionActive) { - Log.w(TAG, "语音识别未激活,但仍尝试推送音频数据: ${data.size} bytes") - } - - if (audioSourceType != AzureAsrHelper.AudioSourceType.EXTERNAL) { - Log.w(TAG, "音频源类型不匹配: $audioSourceType,推送音频数据: ${data.size} bytes") - } - - // 保持原有逻辑,确保兼容性 azureAsrHelper?.audioStream?.saveAudioDataTo(data) } - /** - * 检查内存使用情况 - */ - private fun checkMemoryUsage(currentTime: Long) { - if (currentTime - lastMemoryCheckTime > MEMORY_CHECK_INTERVAL) { - lastMemoryCheckTime = currentTime - - val runtime = Runtime.getRuntime() - val usedMemory = runtime.totalMemory() - runtime.freeMemory() - val maxMemory = runtime.maxMemory() - val memoryUsagePercent = (usedMemory * 100 / maxMemory).toInt() - - Log.i(TAG, "内存使用情况: ${usedMemory / 1024 / 1024}MB / ${maxMemory / 1024 / 1024}MB (${memoryUsagePercent}%)") - - // 如果内存使用超过80%,发出警告 - if (memoryUsagePercent > 80) { - Log.w(TAG, "内存使用率过高: ${memoryUsagePercent}%,建议检查内存泄漏") - - // 发送内存警告事件 - sendEvent("memory_warning", mapOf( - "usedMemoryMB" to (usedMemory / 1024 / 1024), - "maxMemoryMB" to (maxMemory / 1024 / 1024), - "usagePercent" to memoryUsagePercent - )) - } - } - } - /** * 停止语音识别 */ @@ -1374,53 +1236,19 @@ object AgentService : CoroutineScope { } } - // 事件发送优化:使用单一协程处理事件队列 - private val eventQueue = Channel>>(capacity = Channel.UNLIMITED) - private var eventProcessorJob: Job? = null - - init { - // 启动事件处理协程 - startEventProcessor() - } - - private fun startEventProcessor() { - eventProcessorJob = launch(Dispatchers.Main) { - for ((eventName, data) in eventQueue) { - try { - // 创建监听器快照,避免并发修改 - val currentListeners = synchronized(listeners) { listeners.toList() } - - currentListeners.forEach { listener -> - try { - listener.onEvent(eventName, data) - } catch (e: Exception) { - Log.e(TAG, "向监听器发送事件失败: $eventName", e) - } - } - } catch (e: Exception) { - Log.e(TAG, "处理事件失败: $eventName", e) - } - } - } - } - /** - * 发送事件 - 优化版本,使用事件队列避免主线程阻塞 + * 发送事件 */ private fun sendEvent(eventName: String, data: Map) { - try { - // 对于高频事件,进行限流处理 - if (eventName == "recognizing" && eventQueue.trySend(Pair(eventName, data)).isFailure) { - Log.v(TAG, "识别事件队列已满,跳过当前事件") - return - } - - // 非阻塞发送到事件队列 - if (!eventQueue.trySend(Pair(eventName, data)).isSuccess) { - Log.w(TAG, "事件队列已满,丢弃事件: $eventName") + // 切换到主线程执行监听器回调,避免 UI 更新问题 + launch(Dispatchers.Main) { + listeners.forEach { listener -> + try { + listener.onEvent(eventName, data) + } catch (e: Exception) { + Log.e(TAG, "向监听器发送事件失败", e) + } } - } catch (e: Exception) { - Log.e(TAG, "发送事件异常: $eventName", e) } } diff --git a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt index dc5428345..60b919843 100644 --- a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt +++ b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt @@ -286,43 +286,19 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream } /** - * 实现AgentServiceListener接口 - 优化版本 + * 实现AgentServiceListener接口 * 接收来自AgentService的事件,并转发到Flutter */ override fun onEvent(eventName: String, data: Map) { - try { - // 对于高频事件,进行优化处理 - val eventData = when (eventName) { - "recognizing" -> { - // 实时识别事件,简化数据结构 - JSONObject().apply { - put("event", eventName) - put("data", JSONObject().apply { - put("text", data["text"] ?: "") - put("language", data["language"] ?: "") - put("audioSource", data["audioSource"] ?: "") - }) - } - } - else -> { - // 其他事件,正常处理 - JSONObject().apply { - put("event", eventName) - put("data", JSONObject(data)) - } - } - } + // 将事件转换为JSON字符串 + val eventData = JSONObject().apply { + put("event", eventName) + put("data", JSONObject(data)) + } - // 使用协程在主线程发送事件 - MainScope().launch { - try { - eventSink?.success(eventData.toString()) - } catch (e: Exception) { - Log.e(TAG, "发送事件到Flutter失败: $eventName", e) - } - } - } catch (e: Exception) { - Log.e(TAG, "处理事件异常: $eventName", e) + // 在主线程发送事件到Flutter + MainScope().launch { + eventSink?.success(eventData.toString()) } } diff --git a/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift b/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift index 6854e8974..684616bc8 100644 --- a/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift +++ b/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift @@ -104,34 +104,13 @@ class AgentServiceImpl: NSObject { listenersLock.unlock() } - // 事件发送优化:使用队列避免主线程阻塞 - private let eventQueue = DispatchQueue(label: "com.yunqiinnovation.agent_service.events", qos: .userInitiated) - internal func sendEvent(name eventName: String, data: [String: Any]) { - // 对于高频事件,使用异步处理 - if eventName == "recognizing" { - eventQueue.async { [weak self] in - self?.dispatchEventToListeners(eventName: eventName, data: data) - } - } else { - // 其他事件在主线程处理 - DispatchQueue.main.async { [weak self] in - self?.dispatchEventToListeners(eventName: eventName, data: data) - } - } - } - - private func dispatchEventToListeners(eventName: String, data: [String: Any]) { listenersLock.lock() let currentListeners = self.listeners listenersLock.unlock() - + for listener in currentListeners { - do { - listener.onEvent(eventName: eventName, data: data) - } catch { - os_log("发送事件失败: %{public}@", log: logger, type: .error, eventName) - } + listener.onEvent(eventName: eventName, data: data) } } @@ -296,39 +275,25 @@ class AgentServiceImpl: NSObject { private func startIdleCheck() { stopIdleCheck() - + guard isRecognizing else { return } - - // BLE模式下使用稍长的超时时间 - let timeoutSeconds: TimeInterval - if let azureHelper = azureAsrHelper, - azureHelper.audioSourceType == .external { - timeoutSeconds = maxIdleSeconds + 3 // BLE模式额外增加3秒 - } else { - timeoutSeconds = maxIdleSeconds - } - - os_log("启动空闲检测,超时时间: %.0f秒", log: logger, type: .debug, timeoutSeconds) - + // 使用全局队列而不是主队列,避免后台挂起问题 let timer = DispatchSource.makeTimerSource(queue: DispatchQueue.global(qos: .utility)) idleTimer = timer - - timer.schedule(deadline: .now() + timeoutSeconds) + + timer.schedule(deadline: .now() + maxIdleSeconds) timer.setEventHandler { [weak self] in // 使用更安全的方式检查self是否存在 DispatchQueue.main.async { [weak self] in guard let self = self else { return } - + if self.isRecognizing && !self.hasSpeechDetected && !self.isSpeaking && !self.isAiStreaming { - os_log("空闲超时,停止识别 - 超时时间: %.0f秒", log: self.logger, type: .info, timeoutSeconds) self.stopRecognition() self.sendEvent(name: "auto_stop", data: [ - "reason": "idle_timeout", - "seconds": timeoutSeconds + "reason": "idle_timeout", + "seconds": self.maxIdleSeconds ]) - } else { - os_log("空闲检测结束,但条件不满足停止要求", log: self.logger, type: .debug) } } } @@ -350,49 +315,34 @@ class AgentServiceImpl: NSObject { sendError("服务未初始化", code: "NOT_INITIALIZED") return false } - - os_log("开始语音识别 - 模式: %{public}@", log: logger, type: .info, useBle ? "BLE外部音频" : "设备麦克风") - + if !useBle { guard AVAudioSession.sharedInstance().recordPermission == .granted else { sendError("无麦克风权限", code: "PERMISSION_DENIED") return false } } - + if isRecognizing { stopRecognition() } - + let audioSourceType: AzureAsrHelper.AudioSourceType = useBle ? .external : .microphone - + if useBle { AudioSessionHub.shared.begin(.playback) } else { AudioSessionHub.shared.begin(.voice) } - - os_log("音频源类型设置为: %{public}@", log: logger, type: .info, audioSourceType == .external ? "EXTERNAL" : "MICROPHONE") - +print("ai启动语音=\(audioSourceType)=\(useBle)") guard let success = azureAsrHelper?.startContinuousRecognition( + // callback: self, audioSourceType: audioSourceType ), success else { sendError("启动语音识别失败", code: "RECOGNITION_START_ERROR") return false } - - // 如果是BLE模式,在ASR启动后再打开编码器 - if useBle { - os_log("BLE模式:启动编码器", log: logger, type: .info) - DispatchQueue.global().asyncAfter(deadline: .now() + 0.05) { - let encoderOpened = BleService.shared.openEncoder() - if !encoderOpened { - os_log("BLE编码器启动失败,但继续尝试识别", log: self.logger, type: .warning) - } - } - } - - os_log("语音识别启动成功", log: logger, type: .info) + return true } func recognizeCallback(useBle: Bool = false) -> Bool { @@ -413,15 +363,13 @@ class AgentServiceImpl: NSObject { } func pushAudioData(_ audioData: Data) -> Bool { - // 添加调试日志,但保持兼容性 if !isRecognizing || !isInitialized { - os_log("语音识别未激活或服务未初始化,但仍尝试推送音频数据: %d bytes", log: logger, type: .debug, audioData.count) - } - - guard let audioStream = azureAsrHelper?.audioStream else { - os_log("音频流未初始化", log: logger, type: .error) return false } + guard let audioStream = azureAsrHelper?.audioStream else { + os_log("音频流未初始化", log: logger,type: .error) + return false +} audioStream.saveAudioDataTo(data: audioData) return true @@ -1450,8 +1398,6 @@ extension AgentServiceImpl: AzureAsrHelper.ContinuousRecognizeCallback { } func onCanceled(_ reason: String, _ errorDetails: String) { - os_log("语音识别被取消: %{public}@, 详情: %{public}@", log: logger, type: .warning, reason, errorDetails) - var data: [String: Any] = [:] if !reason.isEmpty { data["reason"] = reason @@ -1459,27 +1405,13 @@ extension AgentServiceImpl: AzureAsrHelper.ContinuousRecognizeCallback { if !errorDetails.isEmpty { data["details"] = errorDetails } - - // 如果是BLE模式,关闭编码器 - if let azureHelper = azureAsrHelper, - azureHelper.audioSourceType == .external { - BleService.shared.closeCodec() - } - + sendEvent(name: "recognition_canceled", data: data) isRecognizing = false stopIdleCheck() } - + func onError(_ error: String) { - os_log("语音识别错误: %{public}@", log: logger, type: .error, error) - - // 如果是BLE模式,关闭编码器 - if let azureHelper = azureAsrHelper, - azureHelper.audioSourceType == .external { - BleService.shared.closeCodec() - } - let data: [String: Any] = ["message": error.isEmpty ? "未知错误" : error] sendEvent(name: "error", data: data) isRecognizing = false diff --git a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt index d5b8dae6c..02d909d4a 100644 --- a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt +++ b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt @@ -146,17 +146,6 @@ class AzureAsrHelper(private val context: Context) { // 设置分段策略为时间模式 setProperty("Speech_SegmentationStrategy", "Time") - - // 针对EXTERNAL音频源的实时性优化 - if (audioSourceType == AudioSourceType.EXTERNAL) { - // 减少缓冲延迟 - setProperty("SpeechServiceConnection_ReceiveTimeout", "1000") - setProperty("SpeechServiceConnection_SendTimeout", "1000") - // 启用实时模式 - setProperty("SpeechServiceConnection_EnableRealTimeRecognition", "true") - // 减少音频缓冲 - setProperty("SpeechServiceConnection_AudioBufferSizeMs", "100") - } } // 录音文件类 recordfile = RecordFile; @@ -717,9 +706,7 @@ class AzureAsrHelper(private val context: Context) { // 情况2:不在录制但队列有数据 -> 从队列获取 else if (writeQueue.isNotEmpty()) { data = writeQueue.poll() - if (data != null) { - Log.v("tag", "从队列写入数据: ${data.size} bytes, 队列剩余: ${writeQueue.size}") - } + Log.d("tag", "写入数据: ${data?.size}") bytesToWrite = data?.size ?: 0 } @@ -737,8 +724,7 @@ class AzureAsrHelper(private val context: Context) { Log.e("tag", "写入失败: ${e.message}") } } else { - // 使用更短的等待时间,提高响应性 - Thread.sleep(1) // 1ms等待,避免空转但保持响应性 + Thread.yield() // 避免空转消耗CPU } } } catch (e: Exception) { @@ -758,22 +744,11 @@ class AzureAsrHelper(private val context: Context) { * 外部音频输入 */ fun saveAudioDataTo(buffer: ByteArray) { - if (audioSourceType == AudioSourceType.MICROPHONE) return - - // 检查队列大小,避免积压 - if (writeQueue.size > 50) { // 限制队列大小 - Log.w("AudioStream", "音频队列积压严重(${writeQueue.size}),清理旧数据") - // 清理一半的旧数据,保持实时性 - repeat(writeQueue.size / 2) { - writeQueue.poll() - } - } + if (audioSourceType == AudioSourceType.MICROPHONE) return // 放入队列,由写线程写入 - val offered = writeQueue.offer(buffer.copyOf()) - if (!offered) { - Log.w("AudioStream", "音频队列已满,丢弃数据包: ${buffer.size} bytes") - } + writeQueue.offer(buffer.copyOf()) + } /** diff --git a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift index d8ee80e97..3106c1236 100644 --- a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift +++ b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift @@ -690,27 +690,22 @@ public class AzureAsrHelper: NSObject { guard let self = self else { return } while self.isRunning { + // print("是否写入: \(self.isWriting)") if !self.isWriting { - usleep(1_000) // 减少等待时间,提高响应性 + usleep(10_000) continue } + + guard let dataToWrite = self.writeQueue.take() else { continue } + + // print("写入数据长度: \(dataToWrite.count)") + do { + try self.pushAudioStream?.write(dataToWrite) + self.onAudioData?(dataToWrite) +} catch { + print("推送音频数据失败: \(error.localizedDescription)") +} - guard let dataToWrite = self.writeQueue.take() else { - usleep(1_000) // 短暂等待,避免空转 - continue - } - - // 添加队列状态日志 - if self.writeQueue.count > 10 { - print("音频写入队列积压: \(self.writeQueue.count)") - } - - do { - try self.pushAudioStream?.write(dataToWrite) - self.onAudioData?(dataToWrite) - } catch { - print("推送音频数据失败: \(error.localizedDescription)") - } } } } @@ -725,16 +720,7 @@ public class AzureAsrHelper: NSObject { if audioSourceType != .external { return } - - // 检查队列大小,避免积压 - if writeQueue.count > 50 { - os_log("音频队列积压严重(%d),清理旧数据", log: OSLog.default, type: .warning, writeQueue.count) - // 清理一半的旧数据,保持实时性 - for _ in 0..<(writeQueue.count / 2) { - _ = writeQueue.take() - } - } - + // print("外部data=\(data)") // 放入队列,由写线程写入 writeQueue.put(data) } From 870e60963b1d6b0952f15e83542f925443dd5460 Mon Sep 17 00:00:00 2001 From: liwei1dao Date: Mon, 4 Aug 2025 11:11:45 +0800 Subject: [PATCH 11/11] =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E9=9F=B3=E6=95=88?= =?UTF-8?q?=E6=92=AD=E6=94=BE=E8=B6=85=E6=97=B6=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../agent_service/AgentServiceImpl.swift | 54 ++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift b/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift index 684616bc8..b884a0c71 100644 --- a/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift +++ b/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift @@ -997,7 +997,7 @@ class ChatApiStreamCallback: StreamCallback { if speakResponse && reply && broadcast{ try agentService.azureTtsHelper?.speakStream(token) // 在开始流式TTS时立即停止气泡音 - try agentService.audioPlayer?.stopAwaitSound() + // try agentService.audioPlayer?.stopAwaitSound() } if (reply && token != "") { try agentService.sendEvent(name: "assistant_token", data: ["token": token]) @@ -1220,8 +1220,12 @@ class AudioPlayer { private var player: AVAudioPlayer? private var callingPlayer: AVAudioPlayer? private var awaitPlayer: AVAudioPlayer? // 新增:等待音效播放器 + private var awaitTimer: DispatchSourceTimer? // 新增:等待音效超时定时器 private let logger = OSLog(subsystem: "com.yunqiinnovation.agent_service", category: "AudioPlayer") private let audioQueue = DispatchQueue(label: "agent.audio.player", qos: .userInitiated) + + // 等待音效超时时间(秒)- 改为 var 以支持动态修改 + private var awaitTimeoutSeconds: TimeInterval = 10.0 private var resourcesBundle: Bundle { #if SWIFT_PACKAGE @@ -1247,7 +1251,14 @@ class AudioPlayer { } func playAwaitSound() { + // 先停止之前的等待音效和定时器 + stopAwaitSound() + + // 播放等待音效 playLoopSound(named: "await", fileType: "mp3", player: &awaitPlayer) + + // 启动超时定时器 + startAwaitTimeout() } func playStopSound() { @@ -1264,8 +1275,14 @@ class AudioPlayer { } func stopAwaitSound() { + // 停止音频播放 awaitPlayer?.stop() awaitPlayer = nil + + // 停止超时定时器 + stopAwaitTimeout() + + os_log("停止等待音效播放", log: logger, type: .debug) } private func playSound(named: String, fileType: String = "mp3") { @@ -1338,6 +1355,41 @@ class AudioPlayer { os_log("播放循环音频失败: %{public}@", log: logger, type: .error, error.localizedDescription) } } + + private func startAwaitTimeout() { + // 停止之前的定时器 + stopAwaitTimeout() + + // 创建新的定时器 + let timer = DispatchSource.makeTimerSource(queue: DispatchQueue.global(qos: .utility)) + awaitTimer = timer + + timer.schedule(deadline: .now() + awaitTimeoutSeconds) + timer.setEventHandler { [weak self] in + DispatchQueue.main.async { [weak self] in + guard let self = self else { return } + + os_log("等待音效超时,自动停止播放 - 超时时间: %.0f秒", + log: self.logger, type: .info, self.awaitTimeoutSeconds) + + self.stopAwaitSound() + } + } + timer.resume() + + os_log("启动等待音效超时定时器,超时时间: %.0f秒", + log: logger, type: .debug, awaitTimeoutSeconds) + } + + private func stopAwaitTimeout() { + awaitTimer?.cancel() + awaitTimer = nil + } + + func setAwaitTimeout(_ seconds: TimeInterval) { + awaitTimeoutSeconds = max(2.0, seconds) // 最小5秒 + os_log("设置等待音效超时时间: %.0f秒", log: logger, type: .info, awaitTimeoutSeconds) + } } extension AgentServiceImpl: AzureAsrHelper.ContinuousRecognizeCallback {