From f64422df23ce6921b3c6c84a9805a4edcc3c1795 Mon Sep 17 00:00:00 2001 From: liwei1dao Date: Sat, 16 Aug 2025 15:54:08 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E7=AD=89=E5=BE=85?= =?UTF-8?q?=E5=A3=B0=E9=9F=B3=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../agent_service/AgentService.kt | 78 ++++++- .../chat_api/SystemFunctionHandler.swift | 203 +++--------------- 2 files changed, 106 insertions(+), 175 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 858cc7cd8..e22334690 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 @@ -2,6 +2,8 @@ package com.yunqiinnovation.agent_service import android.content.Context import android.content.Intent +import android.os.Handler +import android.os.Looper import org.json.JSONArray import org.json.JSONObject import com.yunqiinnovation.azure_speech.AzureAsrHelper @@ -38,6 +40,7 @@ import java.text.SimpleDateFormat import java.util.Locale import java.util.Date + /** * 代理服务事件监听接口 */ @@ -873,7 +876,7 @@ object AgentService : CoroutineScope { // 设置状态为正在流式输出 _isAiStreaming.set(true) Log.d(TAG, "processWithChatApiInternal") - audioPlayer?.playAudio(R.raw.await, true, 0.3f) + audioPlayer?.playAudio(R.raw.await, true, 0.3f,true) // 使用历史记录作为上下文发送到OpenAI val responseBuilder = StringBuilder() var aiMetadata: String = "" @@ -971,7 +974,7 @@ object AgentService : CoroutineScope { if (speakResponse && reply && broadcast && token.isNotEmpty()) { ttsService?.speakStream(token) // 在开始流式TTS时立即停止气泡音 - audioPlayer?.stopAudio() +// audioPlayer?.stopAudio() } if (reply && token.isNotEmpty()) { // 发送流式回复token @@ -1584,6 +1587,11 @@ object AgentService : CoroutineScope { private var mediaPlayer: MediaPlayer? = null private var isInitialized = false private val playbackLock = Object() // 添加同步锁 + + // 新增:超时相关属性 + private var timeoutHandler: Handler? = null + private var timeoutRunnable: Runnable? = null + private var timeoutSeconds: Long = 10000L // 默认10秒超时 /** * 安全检查 MediaPlayer 是否正在播放 @@ -1601,17 +1609,21 @@ object AgentService : CoroutineScope { } /** - * 播放音频资源 + * 播放音频资源(带超时控制) * @param resId 资源ID * @param isLooping 是否循环播放 * @param volume 音量大小,范围0.0-1.0,默认1.0 + * @param enableTimeout 是否启用超时自动停止,默认false */ - fun playAudio(resId: Int, isLooping: Boolean = false, volume: Float = 1.0f) { + fun playAudio(resId: Int, isLooping: Boolean = false, volume: Float = 1.0f, enableTimeout: Boolean = false) { synchronized(playbackLock) { try { - Log.d(TAG, "playAudio${isLooping}") + Log.d(TAG, "playAudio isLooping=$isLooping, enableTimeout=$enableTimeout") // 强制停止并释放之前的资源 forceStop() + + // 停止之前的超时定时器 + stopTimeout() // 创建播放器并设置资源 mediaPlayer = MediaPlayer().apply { @@ -1627,11 +1639,14 @@ object AgentService : CoroutineScope { if (!isLooping) { release() } + // 播放完成时停止超时定时器 + stopTimeout() } setOnErrorListener { _, what, extra -> Log.e(TAG, "MediaPlayer 错误: what=$what, extra=$extra") forceRelease() + stopTimeout() true // 返回true表示错误已处理 } @@ -1640,9 +1655,15 @@ object AgentService : CoroutineScope { Log.d(TAG, "开始播放音频资源: $resId, 循环: $isLooping") } isInitialized = true + + // 如果启用超时且是循环播放,启动超时定时器 + if (enableTimeout && isLooping) { + startTimeout() + } } catch (e: Exception) { Log.e(TAG, "播放音频资源异常: ${e.message}", e) forceRelease() + stopTimeout() } } } @@ -1654,6 +1675,9 @@ object AgentService : CoroutineScope { synchronized(playbackLock) { try { Log.d(TAG, "stopAudio") + // 停止超时定时器 + stopTimeout() + mediaPlayer?.apply { if (isMediaPlayerPlaying()) { stop() @@ -1672,6 +1696,42 @@ object AgentService : CoroutineScope { } } + /** + * 启动超时定时器 + */ + private fun startTimeout() { + stopTimeout() // 先停止之前的定时器 + + timeoutHandler = Handler(Looper.getMainLooper()) + timeoutRunnable = Runnable { + Log.d(TAG, "音频播放超时,自动停止 - 超时时间: ${timeoutSeconds}ms") + stopAudio() + } + + timeoutHandler?.postDelayed(timeoutRunnable!!, timeoutSeconds) + Log.d(TAG, "启动音频播放超时定时器,超时时间: ${timeoutSeconds}ms") + } + + /** + * 停止超时定时器 + */ + private fun stopTimeout() { + timeoutRunnable?.let { runnable -> + timeoutHandler?.removeCallbacks(runnable) + } + timeoutHandler = null + timeoutRunnable = null + } + + /** + * 设置超时时间 + * @param seconds 超时秒数,最小2秒 + */ + fun setTimeoutSeconds(seconds: Long) { + timeoutSeconds = maxOf(2000L, seconds * 1000L) // 最小2秒,转换为毫秒 + Log.d(TAG, "设置音频播放超时时间: ${timeoutSeconds}ms") + } + /** * 强制停止播放(不释放资源) */ @@ -1703,6 +1763,9 @@ object AgentService : CoroutineScope { fun release() { synchronized(playbackLock) { try { + // 停止超时定时器 + stopTimeout() + mediaPlayer?.apply { // 安全停止播放 if (isMediaPlayerPlaying()) { @@ -1733,6 +1796,7 @@ object AgentService : CoroutineScope { // 确保变量被清理 mediaPlayer = null isInitialized = false + stopTimeout() } } } @@ -1743,6 +1807,9 @@ object AgentService : CoroutineScope { private fun forceRelease() { synchronized(playbackLock) { try { + // 停止超时定时器 + stopTimeout() + mediaPlayer?.apply { try { reset() @@ -1760,6 +1827,7 @@ object AgentService : CoroutineScope { } finally { mediaPlayer = null isInitialized = false + stopTimeout() } } } diff --git a/local_plugins/chat_api/ios/chat_api/Sources/chat_api/SystemFunctionHandler.swift b/local_plugins/chat_api/ios/chat_api/Sources/chat_api/SystemFunctionHandler.swift index f16aa088d..a3f295014 100644 --- a/local_plugins/chat_api/ios/chat_api/Sources/chat_api/SystemFunctionHandler.swift +++ b/local_plugins/chat_api/ios/chat_api/Sources/chat_api/SystemFunctionHandler.swift @@ -447,14 +447,14 @@ private class GetCurrentLocationHandler: FunctionHandler { return await withCheckedContinuation { continuation in // 设置超时 let timeoutTask = Task { - try await Task.sleep(seconds: 15) // 15秒 + try await Task.sleep(nanoseconds: 15_000_000_000) // 15秒 (15 * 1_000_000_000 nanoseconds) continuation.resume(returning: self.createErrorResponse(message: "位置获取超时,请检查网络连接后重试")) } // 检查位置服务是否可用 guard CLLocationManager.locationServicesEnabled() else { - locationCompletion?(Result(success: false, message: "位置服务未启用。请前往 设置 > 隐私与安全 > 定位服务 启用定位服务")) - locationCompletion = nil + timeoutTask.cancel() + continuation.resume(returning: self.createErrorResponse(message: "位置服务未启用。请前往 设置 > 隐私与安全 > 定位服务 启用定位服务")) return } @@ -729,85 +729,37 @@ private class SetReminderHandler: FunctionHandler { } private func setReminder(title: String, content: String, time: String) async -> Result { - Log.d(TAG, "设置提醒: $title, 时间: $timeStr") + guard let eventStore = eventStore else { + return Result(success: false, message: "事件存储未初始化") + } - try { - // 解析时间字符串 - val time = try { - val dateFormat = java.text.SimpleDateFormat("yyyy-MM-dd HH:mm", java.util.Locale.getDefault()) - dateFormat.parse(timeStr)?.time ?: System.currentTimeMillis() - } catch (e: Exception) { - Log.e(TAG, "解析时间失败: $timeStr, ${e.message}") - return Result( - success = false, - message = "提醒时间格式无效,请使用'yyyy-MM-dd HH:mm'格式" - ) - } - - // 检查日历权限 - if (!checkPermissionAndGuide(Manifest.permission.WRITE_CALENDAR, "日历写入", "设置提醒")) { - return Result( - success = false, - message = "需要日历权限才能创建提醒,请在权限管理中开启日历权限" - ) - } - - // 直接使用ContentProvider写入日历 - val endTime = time + 3600000 // 默认1小时后结束 - - // 获取默认日历ID - val calendarId = getDefaultCalendarId() - if (calendarId == -1L) { - Log.w(TAG, "未找到默认日历") - return tryCreateAlarm(title, content, timeStr) - } - - // 创建事件 - val eventValues = ContentValues().apply { - put(CalendarContract.Events.CALENDAR_ID, calendarId) - put(CalendarContract.Events.TITLE, title) - put(CalendarContract.Events.DESCRIPTION, content) - put(CalendarContract.Events.DTSTART, time) - put(CalendarContract.Events.DTEND, endTime) - put(CalendarContract.Events.HAS_ALARM, 1) - put(CalendarContract.Events.EVENT_TIMEZONE, java.util.TimeZone.getDefault().id) - } - - val eventUri = context.contentResolver.insert(CalendarContract.Events.CONTENT_URI, eventValues) - if (eventUri == null) { - Log.e(TAG, "插入日历事件失败") - return tryCreateAlarm(title, content, timeStr) - } - - // 获取事件ID - val eventId = ContentUris.parseId(eventUri) - - // 添加提醒(15分钟前提醒) - val reminderValues = ContentValues().apply { - put(CalendarContract.Reminders.EVENT_ID, eventId) - put(CalendarContract.Reminders.METHOD, CalendarContract.Reminders.METHOD_ALERT) - put(CalendarContract.Reminders.MINUTES, 15) // 提前15分钟提醒 - } - - val reminderUri = context.contentResolver.insert(CalendarContract.Reminders.CONTENT_URI, reminderValues) - if (reminderUri != null) { - return Result( - success = true, - message = "已成功创建提醒: $title, 时间: $timeStr" - ) - } else { - // 仍然创建了事件,但没有提醒 - return Result( - success = true, - message = "已创建日历事件,但未设置提醒: $title" - ) - } - } catch (e: Exception) { - Log.e(TAG, "设置提醒失败: ${e.message}", e) - return Result( - success = false, - message = "设置提醒失败: ${e.message}" - ) + // 请求日历权限 + let status = await requestCalendarAccess() + guard status == .authorized else { + return Result(success: false, message: "日历权限被拒绝") + } + + // 解析时间 + let formatter = DateFormatter() + formatter.dateFormat = "yyyy-MM-dd HH:mm" + guard let date = formatter.date(from: time) else { + return Result(success: false, message: "时间格式错误") + } + + // 创建提醒 + let reminder = EKReminder(eventStore: eventStore) + reminder.title = title + reminder.notes = content + reminder.calendar = eventStore.defaultCalendarForNewReminders() + + let alarm = EKAlarm(absoluteDate: date) + reminder.addAlarm(alarm) + + do { + try eventStore.save(reminder, commit: true) + return Result(success: true, message: "提醒设置成功") + } catch { + return Result(success: false, message: "设置提醒失败:\(error.localizedDescription)") } } @@ -822,95 +774,6 @@ private class SetReminderHandler: FunctionHandler { } -// /** -// * 设置提醒 -// * @param title 提醒标题 -// * @param content 提醒内容(可选) -// * @param timeStr 提醒时间,格式为"yyyy-MM-dd HH:mm" -// * @return 操作结果 -// */ -// fun setReminder(title: String, content: String, timeStr: String): Result { -// Log.d(TAG, "设置提醒: $title, 时间: $timeStr") - -// try { -// // 解析时间字符串 -// val time = try { -// val dateFormat = java.text.SimpleDateFormat("yyyy-MM-dd HH:mm", java.util.Locale.getDefault()) -// dateFormat.parse(timeStr)?.time ?: System.currentTimeMillis() -// } catch (e: Exception) { -// Log.e(TAG, "解析时间失败: $timeStr, ${e.message}") -// return Result( -// success = false, -// message = "提醒时间格式无效,请使用'yyyy-MM-dd HH:mm'格式" -// ) -// } - -// // 检查日历权限 -// if (!checkPermissionAndGuide(Manifest.permission.WRITE_CALENDAR, "日历写入", "设置提醒")) { -// return Result( -// success = false, -// message = "需要日历权限才能创建提醒,请在权限管理中开启日历权限" -// ) -// } - -// // 直接使用ContentProvider写入日历 -// val endTime = time + 3600000 // 默认1小时后结束 - -// // 获取默认日历ID -// val calendarId = getDefaultCalendarId() -// if (calendarId == -1L) { -// Log.w(TAG, "未找到默认日历") -// return tryCreateAlarm(title, content, timeStr) -// } - -// // 创建事件 -// val eventValues = ContentValues().apply { -// put(CalendarContract.Events.CALENDAR_ID, calendarId) -// put(CalendarContract.Events.TITLE, title) -// put(CalendarContract.Events.DESCRIPTION, content) -// put(CalendarContract.Events.DTSTART, time) -// put(CalendarContract.Events.DTEND, endTime) -// put(CalendarContract.Events.HAS_ALARM, 1) -// put(CalendarContract.Events.EVENT_TIMEZONE, java.util.TimeZone.getDefault().id) -// } - -// val eventUri = context.contentResolver.insert(CalendarContract.Events.CONTENT_URI, eventValues) -// if (eventUri == null) { -// Log.e(TAG, "插入日历事件失败") -// return tryCreateAlarm(title, content, timeStr) -// } - -// // 获取事件ID -// val eventId = ContentUris.parseId(eventUri) - -// // 添加提醒(15分钟前提醒) -// val reminderValues = ContentValues().apply { -// put(CalendarContract.Reminders.EVENT_ID, eventId) -// put(CalendarContract.Reminders.METHOD, CalendarContract.Reminders.METHOD_ALERT) -// put(CalendarContract.Reminders.MINUTES, 15) // 提前15分钟提醒 -// } - -// val reminderUri = context.contentResolver.insert(CalendarContract.Reminders.CONTENT_URI, reminderValues) -// if (reminderUri != null) { -// return Result( -// success = true, -// message = "已成功创建提醒: $title, 时间: $timeStr" -// ) -// } else { -// // 仍然创建了事件,但没有提醒 -// return Result( -// success = true, -// message = "已创建日历事件,但未设置提醒: $title" -// ) -// } -// } catch (e: Exception) { -// Log.e(TAG, "设置提醒失败: ${e.message}", e) -// return Result( -// success = false, -// message = "设置提醒失败: ${e.message}" -// ) -// } -// } /// 获取当前时间处理器 private class GetCurrentTimeHandler: FunctionHandler { From 7b7c336aef8471cf977de8625efb06865a022411 Mon Sep 17 00:00:00 2001 From: liwei1dao Date: Sat, 16 Aug 2025 17:47:25 +0800 Subject: [PATCH 2/2] =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/yunqiinnovation/agent_service/AgentService.kt | 7 ++++--- .../Sources/agent_service/AgentServiceImpl.swift | 3 +-- 2 files changed, 5 insertions(+), 5 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 e22334690..423ed7fc6 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 @@ -774,6 +774,9 @@ object AgentService : CoroutineScope { // 通知ChatAPI服务终止当前流式请求 chatApiService.cancelCurrentStream() + //结束等待音 + audioPlayer?.stopAudio() + } catch (e: Exception) { Log.e(TAG, "停止AI流输出异常", e) // 确保状态被重置,即使发生异常 @@ -973,8 +976,6 @@ object AgentService : CoroutineScope { responseBuilder.append(token) if (speakResponse && reply && broadcast && token.isNotEmpty()) { ttsService?.speakStream(token) - // 在开始流式TTS时立即停止气泡音 -// audioPlayer?.stopAudio() } if (reply && token.isNotEmpty()) { // 发送流式回复token @@ -1591,7 +1592,7 @@ object AgentService : CoroutineScope { // 新增:超时相关属性 private var timeoutHandler: Handler? = null private var timeoutRunnable: Runnable? = null - private var timeoutSeconds: Long = 10000L // 默认10秒超时 + private var timeoutSeconds: Long = 30000L // 默认10秒超时 /** * 安全检查 MediaPlayer 是否正在播放 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 e7f69f96d..32cbf7d71 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 @@ -486,7 +486,7 @@ audioStream.saveAudioDataTo(data: audioData) if interrupted { sendEvent(name: "response_interrupted", data: ["status": "interrupted"]) } - + audioPlayer?.stopAwaitSound() return interrupted } @@ -1256,7 +1256,6 @@ class ChatApiStreamCallback: StreamCallback { if speakResponse && reply && broadcast{ try agentService.azureTtsHelper?.speakStream(token) // 在开始流式TTS时立即停止气泡音 - // try agentService.audioPlayer?.stopAwaitSound() } if (reply && token != "") { try agentService.sendEvent(name: "assistant_token", data: ["token": token])