wolfplus 1 year ago
parent
commit
65ff13a0c2
  1. 3
      lib/data/services/ble_manager.dart
  2. 171
      lib/modules/pairing/controllers/pairing_controller.dart
  3. 12
      local_plugins/agent_service/android/src/main/AndroidManifest.xml
  4. 184
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt
  5. 2
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleAgent.kt
  6. 131
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleMediaService.kt
  7. 85
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleWakeReceiver.kt
  8. 19
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BootReceiver.kt
  9. 0
      local_plugins/agent_service/android/src/main/res/raw/silence.mp3
  10. BIN
      local_plugins/agent_service/android/src/main/res/raw/start.mp3
  11. BIN
      local_plugins/agent_service/android/src/main/res/raw/stop.mp3
  12. BIN
      local_plugins/agent_service/android/src/main/res/raw/stop2.mp3
  13. 2
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt
  14. 32
      local_plugins/ble_service/android/src/main/AndroidManifest.xml
  15. 481
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleCompanionManager.kt
  16. 73
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleCompanionService.kt
  17. 28
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt
  18. 123
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleServicePlugin.kt
  19. 69
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleWakeReceiver.kt
  20. 26
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BootReceiver.kt
  21. 84
      local_plugins/ble_service/lib/ble_service.dart
  22. 127
      local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/OpenAIService.kt
  23. BIN
      local_plugins/opus/.gradle/8.10/fileHashes/fileHashes.lock
  24. BIN
      local_plugins/opus/.gradle/buildOutputCleanup/buildOutputCleanup.lock
  25. 2
      local_plugins/opus/.gradle/buildOutputCleanup/cache.properties

3
lib/data/services/ble_manager.dart

@ -483,9 +483,6 @@ class BleManager extends GetxService {
}
}
registerBackgroundScan() {
_bleService.registerBackgroundScan();
}
/// 获取电池电量
Future<int> getBatteryLevel() async {

171
lib/modules/pairing/controllers/pairing_controller.dart

@ -9,6 +9,7 @@ import '../../../data/services/bluetooth_manager.dart';
import 'package:classic_bluetooth/classic_bluetooth.dart' show BluetoothDevice;
import '../../../data/services/ble_manager.dart';
import 'package:agent_service/agent_service.dart';
import 'package:ble_service/ble_service.dart';
/// 配对控制器,负责检查蓝牙权限、状态,并处理配对流程
class PairingController extends GetxController {
static PairingController get to => Get.find();
@ -30,12 +31,19 @@ class PairingController extends GetxController {
// 是否正在跳转到主页
bool _isNavigatingToHome = false;
// 是否正在执行Companion关联流程
bool _isAssociating = false;
// 当前连接设备信息
BluetoothDevice? get currentDevice => _bluetoothService.currentDevice;
// 是否已连接设备
bool get isDeviceConnected => currentDevice != null;
// 是否已完成Companion关联
final _companionAssociated = false.obs;
bool get isCompanionAssociated => _companionAssociated.value;
// 服务状态监听
Worker? _bluetoothStateWorker;
@ -43,6 +51,9 @@ class PairingController extends GetxController {
// 蓝牙服务
final _bluetoothService = Get.find<BluetoothManager>();
// BLE服务
final _bleService = BleService();
@override
void onInit() {
@ -60,8 +71,6 @@ class PairingController extends GetxController {
/// 初始化流程
Future<void> _initialize() async {
try {
isLoading.value = true;
statusDescription.value = 'checkingPermissions'.tr;
@ -86,11 +95,10 @@ class PairingController extends GetxController {
// 刷新已连接设备
statusDescription.value = 'searchingDevice'.tr;
// 设备连接时启动BLE后台服务
_startAgentService();
await refreshConnectedDevices();
// 检查是否已存在Companion设备关联,但不要重复调用关联流程
await _checkExistingCompanionAssociation();
} catch (e) {
Logger.error('初始化配对控制器失败: $e');
statusDescription.value = 'initializationFailure'.tr;
@ -99,6 +107,26 @@ class PairingController extends GetxController {
_updateStatusDescription();
}
}
/// 检查是否已存在Companion设备关联
Future<void> _checkExistingCompanionAssociation() async {
// 只在Android平台执行
if (!Platform.isAndroid) return;
try {
final isSupported = await _bleService.isCompanionSupported();
if (!isSupported) return;
final pairedMac = await _bleService.getPairedMacAddress();
if (pairedMac != null) {
Logger.info('发现已关联的Companion设备: $pairedMac');
_companionAssociated.value = true;
await _bleService.registerBackgroundScan();
}
} catch (e) {
Logger.error('检查现有Companion关联失败: $e');
}
}
/// 检查并请求必要的蓝牙权限
Future<bool> _checkAndRequestPermissions() async {
@ -161,11 +189,27 @@ class PairingController extends GetxController {
_deviceWorker = ever(_bluetoothService.currentDeviceRx, (BluetoothDevice? device) {
if (device != null && !_isNavigatingToHome) {
Logger.info('配对页面发现设备已连接: ${device.name}');
_autoNavigateToHome();
// 当经典蓝牙设备连接成功,尝试进行Companion关联
_handleDeviceConnected();
}
_updateStatusDescription();
});
}
/// 处理设备连接成功事件
Future<void> _handleDeviceConnected() async {
if (!isDeviceConnected || _isNavigatingToHome) return;
// 如果已经完成关联,直接跳转
if (_companionAssociated.value) {
_autoNavigateToHome();
return;
}
// 启动关联流程
_startCompanionAssociation();
}
/// 更新状态描述
void _updateStatusDescription() {
@ -180,8 +224,13 @@ class PairingController extends GetxController {
}
if (isDeviceConnected) {
// 有设备连接时,显示设备名称
statusDescription.value = '${'connectedDevice'.tr} ${currentDevice!.name}';
if (Platform.isAndroid && !isCompanionAssociated && _isAssociating) {
statusDescription.value = '${'associatingDevice'.tr} ${currentDevice!.name}';
} else if (isCompanionAssociated) {
statusDescription.value = '${'connectedDevice'.tr} ${currentDevice!.name}';
} else {
statusDescription.value = '${'connectedDevice'.tr} ${currentDevice!.name}';
}
return;
}
@ -210,7 +259,9 @@ class PairingController extends GetxController {
// 检查连接状态
if (isDeviceConnected) {
Logger.info('刷新发现设备已连接: ${currentDevice!.name}');
_autoNavigateToHome();
// 当发现设备已连接,统一由_handleDeviceConnected处理
_handleDeviceConnected();
} else {
Logger.info('刷新未发现已连接设备');
}
@ -222,20 +273,103 @@ class PairingController extends GetxController {
_updateStatusDescription();
}
}
/// 启动Companion设备关联流程
Future<void> _startCompanionAssociation() async {
// 如果不是Android平台,已经跳转到首页,已经完成关联或正在进行关联,直接返回
if (!Platform.isAndroid || _isNavigatingToHome || _companionAssociated.value || _isAssociating) {
return;
}
// 设置关联状态标志
_isAssociating = true;
_updateStatusDescription();
try {
// 检查是否支持Companion Device功能
final isSupported = await _bleService.isCompanionSupported();
if (!isSupported) {
Logger.info('设备不支持Companion Device功能,跳过关联');
_companionAssociated.value = true; // 标记为已完成
_autoNavigateToHome();
return;
}
// 检查是否已有配对的MAC地址
final pairedMac = await _bleService.getPairedMacAddress();
if (pairedMac != null) {
// 已有关联的设备,检查是否与当前连接的设备一致
if (currentDevice?.address == pairedMac) {
Logger.info('已关联的Companion设备与当前连接的设备一致');
_companionAssociated.value = true;
await _bleService.registerBackgroundScan();
_autoNavigateToHome();
return;
} else {
// 不一致,清除旧关联
Logger.info('已关联的Companion设备与当前连接的设备不一致,清除旧关联');
await _bleService.clearAssociations();
}
}
// 开始关联流程
if (currentDevice != null) {
Logger.info('开始Companion设备关联流程, 设备MAC: ${currentDevice!.address}');
final success = await _bleService.startDeviceAssociation(
deviceNamePattern: currentDevice!.name,
macAddress: currentDevice!.address,
);
if (success) {
Logger.info('Companion设备关联成功');
_companionAssociated.value = true;
// 注册后台扫描,确保应用被杀死后仍能被唤醒
await _bleService.registerBackgroundScan(macAddress: currentDevice!.address);
// 启动BLE后台服务(可选)
await _startAgentService();
// 关联和后台扫描都成功后,再跳转
_autoNavigateToHome();
} else {
Logger.error('Companion设备关联失败');
// 关联失败,但仍然可以跳转到主页(用户体验考虑)
_autoNavigateToHome();
}
}
} catch (e) {
Logger.error('Companion设备关联过程发生错误: $e');
// 出错时仍可以跳转主页
_autoNavigateToHome();
} finally {
// 无论成功失败,都重置关联状态标志
_isAssociating = false;
_updateStatusDescription();
}
}
/// 自动跳转到主页
void _autoNavigateToHome() {
Future<void> _autoNavigateToHome() async {
if (_isNavigatingToHome) return;
// 如果没有连接,不进行跳转
if (!isDeviceConnected) return;
// 在Android平台上,如果支持Companion且仍在关联中,不跳转
if (Platform.isAndroid) {
final isSupported = await _bleService.isCompanionSupported();
if (isSupported && !isCompanionAssociated && _isAssociating) {
Logger.info('等待Companion设备关联完成后再跳转');
return;
}
}
_isNavigatingToHome = true;
Logger.info('已找到连接的设备 ${currentDevice!.name},自动跳转到主页');
Future.delayed(const Duration(milliseconds: 500), () {
Get.offAllNamed(Routes.home);
});
@ -244,17 +378,8 @@ class PairingController extends GetxController {
/// 启动BLE后台服务
Future<void> _startAgentService() async {
try {
// if (!isDeviceConnected) return;
if (!isDeviceConnected) return;
// 确保麦克风权限已被授予
final microphoneStatus = await Permission.microphone.status;
if (!microphoneStatus.isGranted) {
Logger.warning('麦克风权限未授予,语音交互需要此权限');
final result = await Permission.microphone.request();
if (!result.isGranted) {
Logger.error('麦克风权限被拒绝,语音功能将无法正常工作');
}
}
// 启动BLE服务,无需传递配置参数,将从.env读取
final result = await BleManager.to.startAgentService();

12
local_plugins/agent_service/android/src/main/AndroidManifest.xml

@ -14,29 +14,29 @@
android:exported="true"
android:enabled="true"
android:foregroundServiceType="mediaPlayback">
<intent-filter>
<!-- <intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter>
</intent-filter> -->
</service>
<!-- 媒体按钮 Receiver -->
<receiver
<!-- <receiver
android:name="androidx.media.session.MediaButtonReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter>
</receiver>
</receiver> -->
<!-- 被系统 PendingIntent 触发的 Receiver -->
<receiver
<!-- <receiver
android:name=".BleWakeReceiver"
android:exported="true"
android:enabled="true">
<intent-filter>
<action android:name="com.yunqiinnovation.ble_service.ACTION_BLE_SCAN" />
</intent-filter>
</receiver>
</receiver> -->
</application>
</manifest>

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

@ -13,6 +13,8 @@ import kotlinx.coroutines.*
import java.util.Collections
import kotlin.coroutines.CoroutineContext
import com.yunqiinnovation.ble_service.BleService
import android.media.MediaPlayer
import com.yunqiinnovation.agent_service.R
/**
* 代理服务事件监听接口
*/
@ -68,6 +70,9 @@ object AgentService : CoroutineScope {
// 系统提示词
private var systemPrompt = ""
// 音频播放器
private var audioPlayer: AudioPlayer? = null // 初始化音频播放器
// 状态
private var isInitialized = false
var isRecognitionActive = false
@ -120,6 +125,8 @@ object AgentService : CoroutineScope {
this.context = appContext.applicationContext
this.openAIService = OpenAIService(context)
this.chatStorageHelper = ChatStorageHelper(context)
this.audioPlayer = AudioPlayer(context)
// 从配置中获取参数
azureSpeechKey = config["azureSpeechKey"] as? String ?: ""
@ -353,12 +360,14 @@ object AgentService : CoroutineScope {
sendEvent("recognition_started", mapOf("status" to "started"))
// 启动空闲检测
startIdleCheck()
audioPlayer?.playAudio(R.raw.start)
}
override fun onSessionStopped() {
isRecognitionActive = false
stopIdleCheck()
sendEvent("recognition_stopped", mapOf("status" to "stopped"))
audioPlayer?.playAudio(R.raw.stop)
}
override fun onCanceled(reason: String, errorDetails: String) {
@ -438,24 +447,25 @@ object AgentService : CoroutineScope {
*/
private fun stopAiStream() {
if (isAiStreaming) {
// 取消当前AI生成任务
currentAiJob?.cancel()
currentAiJob = null
// 通知OpenAI服务终止当前流式请求
openAIService.cancelCurrentStream()
// 更新状态
isAiStreaming = false
// 记录日志
FileLogger.d(TAG, "AI流输出已停止")
// // 向客户端发送取消事件,表明AI回复已被终止
// sendEvent("assistant_stream_canceled", mapOf(
// "status" to "canceled",
// "timestamp" to System.currentTimeMillis()
// ))
try {
// 先更新状态,避免回调时的状态不一致
isAiStreaming = false
// 取消当前AI生成任务
currentAiJob?.cancel()
currentAiJob = null
// 通知OpenAI服务终止当前流式请求
openAIService.cancelCurrentStream()
// 记录日志
FileLogger.d(TAG, "AI流输出已停止")
} catch (e: Exception) {
FileLogger.e(TAG, "停止AI流输出异常", e)
// 确保状态被重置,即使发生异常
isAiStreaming = false
currentAiJob = null
}
}
}
@ -657,21 +667,30 @@ object AgentService : CoroutineScope {
}
override fun onFunctionCall(call: JSONObject) {
sendEvent("function_call", mapOf(
"name" to call.getString("name"),
"arguments" to call.toString()
))
if(call.getString("name") == "exit_interaction") {
stopRecognition()
try {
val name = call.getString("name")
sendEvent("function_call", mapOf(
"name" to name,
"arguments" to call.toString()
))
if(name == "exit_interaction") {
stopRecognition()
}
} catch (e: Exception) {
FileLogger.e(TAG, "处理函数调用事件失败", e)
}
}
override fun onFunctionCallResult(functionCall: JSONObject, functionCallResult: JSONObject) {
sendEvent("function_call_result", mapOf(
"function_call" to functionCall.toString(),
"result" to functionCallResult.toString(),
))
FileLogger.d(TAG, "mcp调用结果: $functionCallResult")
try {
sendEvent("function_call_result", mapOf(
"function_call" to functionCall.toString(),
"result" to functionCallResult.toString()
))
FileLogger.d(TAG, "mcp调用结果: $functionCallResult")
} catch (e: Exception) {
FileLogger.e(TAG, "处理函数调用结果事件失败", e)
}
}
}
)
@ -850,25 +869,43 @@ object AgentService : CoroutineScope {
}
/**
* 释放资源
* 释放所有资源
*/
fun dispose() {
stopRecognition()
stopTts()
stopAiStream()
stopIdleCheck()
azureAsrHelper?.dispose()
azureTtsHelper?.dispose()
job.cancel()
// 清除所有监听器
clearListeners()
// 重置单例状态,以便下次使用时可以重新初始化
isInitialized = false
FileLogger.d(TAG, "代理服务资源已释放")
try {
// 停止语音识别
stopRecognition()
// 停止TTS播放
stopTts()
// 停止AI流输出
stopAiStream()
// 停止空闲检测
stopIdleCheck()
// 释放音频播放器资源
audioPlayer?.release()
audioPlayer = null
// 释放Azure服务
azureAsrHelper?.dispose()
azureTtsHelper?.dispose()
// 取消所有协程
job.cancel()
// 清除监听器
clearListeners()
// 标记为未初始化
isInitialized = false
FileLogger.d(TAG, "代理服务资源已释放")
} catch (e: Exception) {
FileLogger.e(TAG, "释放资源异常", e)
}
}
/**
@ -948,4 +985,59 @@ object AgentService : CoroutineScope {
return true
}
/**
* 简单音频播放器,用于播放提示音等
*/
class AudioPlayer(private val context: Context) {
private var mediaPlayer: MediaPlayer? = null
/**
* 播放音频资源
* @param resId 资源ID
*/
fun playAudio(resId: Int) {
try {
// 释放之前的资源
release()
// 创建播放器并设置资源
mediaPlayer = MediaPlayer().apply {
// 设置资源
context.resources.openRawResourceFd(resId)?.use { fd ->
setDataSource(fd.fileDescriptor, fd.startOffset, fd.length)
}
// 播放完成后自动释放资源
setOnCompletionListener {
release()
}
// 准备并播放
prepare()
start()
}
FileLogger.d(TAG, "开始播放音频资源")
} catch (e: Exception) {
FileLogger.e(TAG, "播放音频资源异常: ${e.message}", e)
release()
}
}
/**
* 释放资源
*/
fun release() {
try {
mediaPlayer?.apply {
if (isPlaying) stop()
release()
}
mediaPlayer = null
} catch (e: Exception) {
// 忽略异常
}
}
}
}

2
local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleAgent.kt

@ -142,7 +142,7 @@ object BleAgent : BleService.Callback, AgentServiceListener {
// 停止当前TTS,避免冲突
AgentService.interruptCurrentResponse()
AgentService.speakText("我在!")
// AgentService.speakText("我在!")
// 启动语音识别
if (AgentService.isRecognitionActive) {
// 如果已经在识别,则重新启动

131
local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleMediaService.kt

@ -20,7 +20,7 @@ import android.support.v4.media.session.MediaSessionCompat
import android.support.v4.media.session.PlaybackStateCompat
import androidx.core.app.NotificationCompat
import androidx.media.session.MediaButtonReceiver
import com.yunqiinnovation.ble_service.R
import com.yunqiinnovation.agent_service.R
import com.yunqiinnovation.deepvoice_utils.FileLogger
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
@ -70,7 +70,6 @@ class BleMediaService : Service(), CoroutineScope {
private lateinit var mediaSession: MediaSessionCompat
private lateinit var audioManager: AudioManager
private var isPlaying = false
// 媒体操作防抖
@ -94,7 +93,7 @@ class BleMediaService : Service(), CoroutineScope {
// 设置服务状态
isRunning.set(true)
startForeground(NOTIFICATION_ID, createNotification())
// 初始化音频管理器
audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
@ -102,23 +101,20 @@ class BleMediaService : Service(), CoroutineScope {
setupMediaSession()
// 注册媒体按钮接收器
registerMediaButtonReceiver()
// 注册后台扫描
registerBackgroundScan()
// registerMediaButtonReceiver()
// // 注册后台扫描
// registerBackgroundScan()
// 创建音频播放器,并播放静音文件
audioPlayer = MiniMediaPlayer(this)
audioPlayer?.play(R.raw.silence, 0f)
setPlaybackState(PlaybackStateCompat.STATE_PAUSED)
// 确保尽快启动前台服务,防止系统杀死服务
startForeground(NOTIFICATION_ID, createNotification())
FileLogger.i(TAG, "媒体服务已启动")
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
FileLogger.i(TAG, "BleMediaService.onStartCommand: ${intent?.action}")
// FileLogger.i(TAG, "BleMediaService.onStartCommand: ${intent?.action}")
// 保存当前Intent,用于后续初始化
currentIntent = intent
@ -134,18 +130,12 @@ class BleMediaService : Service(), CoroutineScope {
// 处理蓝牙扫描结果事件
if (intent?.action == BleConst.ACTION_SCAN_RESULT) {
val mac = intent.getStringExtra("mac")
if (mac != null) {
val currentTime = System.currentTimeMillis()
if (currentTime - lastScanTime >= ACTION_DEBOUNCE_MS) {
lastScanTime = currentTime
checkBleConnection(mac)
} else {
// FileLogger.i(TAG, "扫描操作防抖: 忽略${ACTION_DEBOUNCE_MS}ms内的重复MAC请求")
}
} else {
FileLogger.w(TAG, "收到扫描结果但MAC地址为空")
}
checkBleConnection()
}
return START_STICKY
}
@ -297,24 +287,7 @@ class BleMediaService : Service(), CoroutineScope {
setCallback(object : MediaSessionCompat.Callback() {
override fun onMediaButtonEvent(mediaButtonIntent: Intent): Boolean {
FileLogger.i(TAG, "onMediaButtonEvent: ${mediaButtonIntent.action}")
// 这里可以添加切换播放/暂停状态的逻辑
isPlaying = !isPlaying
// 更新播放状态
val newState = if (isPlaying) {
PlaybackStateCompat.STATE_PLAYING
} else {
PlaybackStateCompat.STATE_PAUSED
}
// 设置新的播放状态
setPlaybackState(newState)
// 更新通知
updateNotification()
handleMediaAction()
// 使用系统默认的媒体按钮处理
return super.onMediaButtonEvent(mediaButtonIntent)
}
@ -348,98 +321,24 @@ class BleMediaService : Service(), CoroutineScope {
FileLogger.e(TAG, "媒体按钮接收器注册失败: ${e.message}", e)
}
}
/**
* 处理媒体操作
* 包含防抖逻辑,避免短时间内重复触发
*/
private fun handleMediaAction() {
// FileLogger.i(TAG, "handleMediaAction")
// // 防抖处理逻辑
// val currentTime = System.currentTimeMillis()
// if (currentTime - lastActionTime >= ACTION_DEBOUNCE_MS) {
// lastActionTime = currentTime
// BleAgent.onWakeupSignalReceived()
// } else {
// FileLogger.i(TAG, "媒体操作防抖: 忽略${ACTION_DEBOUNCE_MS}ms内的重复请求")
// }
}
private fun checkBleConnection(mac: String) {
FileLogger.i(TAG, "handleMediaAction收到连接请求: ${mac}")
private fun checkBleConnection() {
FileLogger.i(TAG, "hcheckBleConnection收到连接请求")
try {
// 示例:使用协程异步处理连接
launch {
if(!BleService.isConnected()) {
BleService.connect(mac)
BleService.connect()
}
}
} catch (e: Exception) {
FileLogger.e(TAG, "连接设备失败: ${e.message}", e)
}
}
// ======================================================================================================
// 后台扫描与唤醒
// ======================================================================================================
/**
* 注册后台持久扫描,APP被杀死后也能唤醒
*/
fun registerBackgroundScan() {
val scanner = (getSystemService(Context.BLUETOOTH_SERVICE) as? BluetoothManager)?.adapter?.bluetoothLeScanner ?: return
try {
val filter = ScanFilter.Builder()
.setManufacturerData(0xEFAB, byteArrayOf(0xA0.toByte(), 0x01))
.build()
val settings = ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_POWER)
.setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES)
.build()
// 创建Intent并设置前台接收器标志
val scanIntent = Intent(BleConst.ACTION_SCAN_RESULT)
.setClass(this, BleWakeReceiver::class.java)
.addFlags(Intent.FLAG_RECEIVER_FOREGROUND)
val pi = PendingIntent.getBroadcast(
this, 0, scanIntent,
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
} else {
PendingIntent.FLAG_UPDATE_CURRENT
}
)
scanner.startScan(listOf(filter), settings, pi)
FileLogger.i(TAG, "后台持久扫描已注册")
} catch (e: Exception) {
FileLogger.e(TAG, "注册后台扫描失败: ${e.message}", e)
}
}
/**
* 注销后台持久扫描
*/
fun unregisterBackgroundScan() {
// 获取一个局部引用,避免并发访问问题
val scanner = (getSystemService(Context.BLUETOOTH_SERVICE) as? BluetoothManager)?.adapter?.bluetoothLeScanner ?: return
try {
// 创建Intent并设置前台接收器标志
val scanIntent = Intent(BleConst.ACTION_SCAN_RESULT)
.setClass(this, BleWakeReceiver::class.java)
.addFlags(Intent.FLAG_RECEIVER_FOREGROUND)
val pi = PendingIntent.getBroadcast(
this, 0, scanIntent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
scanner.stopScan(pi)
FileLogger.i(TAG, "后台持久扫描已注销")
} catch (e: Exception) {
FileLogger.e(TAG, "注销后台扫描失败: ${e.message}", e)
}
}
/**
* 更新通知内容
*/
@ -512,7 +411,7 @@ class BleMediaService : Service(), CoroutineScope {
// 构建通知
val builder = NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("DeepSound")
.setContentText(if (isPlaying) "正在播放" else "已暂停")
// .setContentText(if (isPlaying) "正在播放" else "已暂停")
.setSmallIcon(android.R.drawable.ic_media_play)
// 设置点击意图

85
local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleWakeReceiver.kt

@ -1,85 +0,0 @@
package com.yunqiinnovation.agent_service
import android.content.*
import android.bluetooth.le.*
import android.os.Build
import android.view.KeyEvent
import android.media.AudioManager
import com.yunqiinnovation.deepvoice_utils.FileLogger
import com.yunqiinnovation.ble_service.BleService
import com.yunqiinnovation.ble_service.BleConst
/**
* BLE唤醒接收器
* 当耳机发送含特定UUID广播时,系统会触发此接收器
*/
class BleWakeReceiver : BroadcastReceiver() {
companion object {
private const val TAG = "BleWakeReceiver"
}
override fun onReceive(ctx: Context, intent: Intent) {
FileLogger.d(TAG, "onReceive, intent: $intent, ${BleConst.ACTION_SCAN_RESULT}")
FileLogger.init(ctx)
try {
if (intent.action != BleConst.ACTION_SCAN_RESULT) {
return
}
val list = intent.getParcelableArrayListExtra<ScanResult>(
BluetoothLeScanner.EXTRA_LIST_SCAN_RESULT
)
// FileLogger.d(TAG, "list: $list")
if (list?.isEmpty() == true || list == null) {
FileLogger.d(TAG, "扫描结果为空")
return
}
val mac = list.firstOrNull()?.device?.address
if (mac == null) {
FileLogger.d(TAG, "没有获取到设备MAC地址")
return
}
FileLogger.d(TAG, "被系统唤醒,发现设备 $mac")
if(!BleMediaService.isServiceRunning()) {
val am = ctx.getSystemService(Context.AUDIO_SERVICE) as AudioManager
val keyEvent = KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_AUDIO_TRACK)
am.dispatchMediaKeyEvent(keyEvent)
// val mediaIntent = Intent(Intent.ACTION_MEDIA_BUTTON)
// mediaIntent.putExtra(Intent.EXTRA_KEY_EVENT, keyEvent)
// mediaIntent.putExtra("mac", mac) // 额外传递MAC地址
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// // 通过AudioManager发送媒体按键
// am.dispatchMediaKeyEvent(keyEvent)
// // 发送UP事件
// // am.dispatchMediaKeyEvent(KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE))
// } else {
// // 通过广播发送媒体按钮
// mediaIntent.component = ComponentName(ctx.packageName, "androidx.media.session.MediaButtonReceiver")
// ctx.sendBroadcast(mediaIntent)
// }
}
if (!BleService.isConnected()) {
val serviceIntent = Intent(ctx, BleMediaService::class.java).apply {
action = BleConst.ACTION_SCAN_RESULT
putExtra("mac", mac)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
ctx.startForegroundService(serviceIntent)
} else {
ctx.startService(serviceIntent)
}
return
}
}
catch (e: Exception) {
FileLogger.e(TAG, "接收器异常: ${e}")
}
}
}

19
local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BootReceiver.kt

@ -1,19 +0,0 @@
package com.yunqiinnovation.agent_service
import android.content.*
import android.os.Build
/**
* 系统启动接收器
* 当系统重启完成后,自动启动BLE前台服务
* 注意:需要用户在设置中授予应用自启动权限才能生效
*/
class BootReceiver : BroadcastReceiver() {
override fun onReceive(ctx: Context, i: Intent) {
if (i.action == Intent.ACTION_BOOT_COMPLETED) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
ctx.startForegroundService(Intent(ctx, BleMediaService::class.java))
else ctx.startService(Intent(ctx, BleMediaService::class.java))
}
}
}

0
local_plugins/ble_service/android/src/main/res/raw/silence.mp3 → local_plugins/agent_service/android/src/main/res/raw/silence.mp3

BIN
local_plugins/agent_service/android/src/main/res/raw/start.mp3

Binary file not shown.

BIN
local_plugins/agent_service/android/src/main/res/raw/stop.mp3

Binary file not shown.

BIN
local_plugins/agent_service/android/src/main/res/raw/stop2.mp3

Binary file not shown.

2
local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt

@ -261,7 +261,7 @@ class AzureSpeechPlugin: FlutterPlugin, CoroutineScope {
"initialize" -> {
val subscriptionKey = call.argument<String>("subscriptionKey") ?: ""
val region = call.argument<String>("region") ?: ""
val useCustomAudioOutput = call.argument<Boolean>("useCustomAudioOutput") ?: true // 默认使用自定义播放器
val useCustomAudioOutput = call.argument<Boolean>("useCustomAudioOutput") ?: false // 默认使用自定义播放器
// 先释放现有资源
azureTtsHelper.dispose()

32
local_plugins/ble_service/android/src/main/AndroidManifest.xml

@ -3,20 +3,50 @@
xmlns:tools="http://schemas.android.com/tools"
package="com.yunqiinnovation.ble_service">
<!-- 告诉系统:我用得到 Companion Device Manager -->
<uses-feature android:name="android.software.companion_device_setup"/>
<!-- 权限 -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
<!-- Companion 特权:后台运行 & 启动前台服务 -->
<uses-permission android:name="android.permission.REQUEST_COMPANION_RUN_IN_BACKGROUND"/>
<uses-permission android:name="android.permission.REQUEST_COMPANION_USE_DATA_IN_BACKGROUND"/>
<uses-permission android:name="android.permission.REQUEST_COMPANION_START_FOREGROUND_SERVICES_FROM_BACKGROUND" />
<!-- BLE 基础权限(扫描 & 连接)-->
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"
android:usesPermissionFlags="neverForLocation"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<!-- 添加媒体按钮接收权限 -->
<uses-permission android:name="android.permission.MEDIA_CONTENT_CONTROL" />
<uses-permission android:name="android.permission.RECEIVE_MEDIA_BUTTON" />
<application>
<!-- Companion Device Service -->
<!-- <service
android:name=".BleCompanionService"
android:permission="android.permission.BIND_COMPANION_DEVICE_SERVICE"
android:exported="true">
<intent-filter>
<action android:name="android.companion.CompanionDeviceService"/>
</intent-filter>
</service> -->
<!-- BLE Wake Receiver -->
<receiver
android:name=".BleWakeReceiver"
android:exported="true">
<intent-filter>
<action android:name="com.yunqiinnovation.ble_service.ACTION_BLE_SCAN" />
</intent-filter>
</receiver>
</application>
</manifest>

481
local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleCompanionManager.kt

@ -0,0 +1,481 @@
package com.yunqiinnovation.ble_service
import android.app.Activity
import android.app.PendingIntent
import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothManager
import android.bluetooth.le.ScanFilter
import android.bluetooth.le.ScanSettings
import android.companion.AssociationInfo
import android.companion.AssociationRequest
import android.companion.BluetoothDeviceFilter
import android.companion.CompanionDeviceManager
import android.content.Context
import android.content.Intent
import android.content.IntentSender
import android.content.SharedPreferences
import android.os.Build
import android.os.ParcelUuid
import androidx.annotation.RequiresApi
import com.yunqiinnovation.deepvoice_utils.FileLogger
import java.util.regex.Pattern
/**
* BleCompanionManager
* 用于管理BLE设备的配对和关联
*/
class BleCompanionManager(private val context: Context) {
private val TAG = "BleCompanionManager"
// 常量定义
companion object {
const val REQUEST_CODE_ASSOCIATE = 42
private const val PREF_NAME = "ble_companion_prefs"
private const val KEY_ASSOCIATION_IDS = "association_ids"
private const val KEY_PAIRED_MAC = "paired_mac"
}
// 会话保存
private val sharedPreferences: SharedPreferences by lazy {
context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
}
// 获取CompanionDeviceManager服务
private val companionDeviceManager: CompanionDeviceManager? by lazy {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.getSystemService(Context.COMPANION_DEVICE_SERVICE) as? CompanionDeviceManager
} else {
null
}
}
/**
* 检查是否支持CompanionDeviceManager
*/
fun isCompanionDeviceManagerSupported(): Boolean {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && companionDeviceManager != null
}
/**
* 获取已关联的设备MAC地址
*/
fun getPairedMacAddress(): String? {
return sharedPreferences.getString(KEY_PAIRED_MAC, null)
}
/**
* 保存已关联的设备MAC地址
*/
fun savePairedMacAddress(macAddress: String) {
sharedPreferences.edit().putString(KEY_PAIRED_MAC, macAddress).apply()
FileLogger.i(TAG, "已保存配对设备MAC: $macAddress")
}
/**
* 清除已关联的设备MAC地址
*/
fun clearPairedMacAddress() {
sharedPreferences.edit().remove(KEY_PAIRED_MAC).apply()
FileLogger.i(TAG, "已清除配对设备MAC")
}
/**
* 检查是否有已关联的设备
*/
@RequiresApi(Build.VERSION_CODES.O)
fun hasAssociations(): Boolean {
val associationIds = getAssociationIds()
return associationIds.isNotEmpty()
}
/**
* 获取所有关联ID
*/
@RequiresApi(Build.VERSION_CODES.O)
fun getAssociationIds(): List<String> {
// 注意: allAssociations 仅在API 33 (Android 13)及以上版本可用
// 在早期版本中,使用associations (返回的是String数组)
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
try {
// 由于AssociationInfo类在API 30+引入,使用反射获取更安全
val method = CompanionDeviceManager::class.java.getMethod("getAllAssociations")
@Suppress("UNCHECKED_CAST")
val associations = method.invoke(companionDeviceManager) as? List<Any>
associations?.map { association ->
// 尝试获取AssociationInfo.id
val idField = association.javaClass.getField("id")
idField.get(association).toString()
} ?: emptyList()
} catch (e: Exception) {
FileLogger.e(TAG, "获取关联ID失败: ${e.message}", e)
emptyList()
}
} else {
// API 26-32 使用 associations 方法
companionDeviceManager?.associations?.toList() ?: emptyList()
}
}
/**
* 开始设备关联流程
*
* @param activity 当前Activity,用于启动系统关联对话框
* @param deviceNamePattern 设备名称正则表达式,例如"MyHeadset.*"
* @param serviceUuid 可选的服务UUID
* @param macAddress 可选的MAC地址
*/
@RequiresApi(Build.VERSION_CODES.O)
fun startAssociation(
activity: Activity,
deviceNamePattern: String? = null,
serviceUuid: String? = null,
macAddress: String? = null
): Boolean {
if (!isCompanionDeviceManagerSupported()) {
FileLogger.e(TAG, "设备不支持CompanionDeviceManager")
return false
}
try {
// 构建设备过滤器
val deviceFilterBuilder = BluetoothDeviceFilter.Builder()
// 添加设备名称模式
deviceNamePattern?.let {
deviceFilterBuilder.setNamePattern(Pattern.compile(it))
}
// 添加服务UUID
serviceUuid?.let {
try {
val uuid = ParcelUuid(java.util.UUID.fromString(it))
deviceFilterBuilder.addServiceUuid(uuid, null)
} catch (e: Exception) {
FileLogger.e(TAG, "设置服务UUID失败: ${e.message}", e)
}
}
// 添加MAC地址 - 注意:setAddress方法在不同API版本有差异
macAddress?.let {
try {
// 检查是否是有效的MAC地址格式
if (!isValidMacAddress(it)) {
FileLogger.e(TAG, "无效的MAC地址格式: $it")
return@let
}
// 保存MAC地址,用于后续验证和处理
savePairedMacAddress(it)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
try {
// Android O(8.0)及以上版本都应该使用字符串格式的MAC地址
deviceFilterBuilder.setAddress(it)
FileLogger.i(TAG, "成功设置MAC地址过滤: $it")
} catch (e: Exception) {
FileLogger.e(TAG, "setAddress失败: ${e.message}")
// 如果设置MAC地址失败,回退到使用设备名称过滤
deviceFilterBuilder.setNamePattern(Pattern.compile(".*"))
}
} else {
// Android 8.0以下版本不支持MAC地址过滤,使用设备名称过滤
FileLogger.w(TAG, "当前Android版本不支持MAC地址过滤,将使用设备名称过滤")
deviceFilterBuilder.setNamePattern(Pattern.compile(".*"))
}
} catch (e: Exception) {
FileLogger.e(TAG, "处理MAC地址失败: ${e.message}", e)
// 发生异常时,使用通配名称模式
deviceFilterBuilder.setNamePattern(Pattern.compile(".*"))
}
} ?: run {
// 如果没有提供MAC地址且没有设置名称模式,则使用通配名称
if (deviceNamePattern == null) {
deviceFilterBuilder.setNamePattern(Pattern.compile(".*"))
FileLogger.i(TAG, "未提供MAC地址,使用通配名称模式")
}
}
// 创建请求
val associationRequest = AssociationRequest.Builder()
.addDeviceFilter(deviceFilterBuilder.build())
.setSingleDevice(true) // 找到一个即停止
.build()
// 发起关联请求
companionDeviceManager?.associate(
associationRequest,
object : CompanionDeviceManager.Callback() {
override fun onDeviceFound(intentSender: IntentSender) {
try {
activity.startIntentSenderForResult(
intentSender,
REQUEST_CODE_ASSOCIATE,
null,
0,
0,
0
)
FileLogger.i(TAG, "已启动设备选择对话框")
} catch (e: Exception) {
FileLogger.e(TAG, "启动设备选择对话框失败: ${e.message}", e)
}
}
// API 26 (Android 8) 使用 onDeviceFound
// API 28+ (Android 9+) 使用 onAssociationPending
@RequiresApi(Build.VERSION_CODES.P)
override fun onAssociationPending(intentSender: IntentSender) {
try {
activity.startIntentSenderForResult(
intentSender,
REQUEST_CODE_ASSOCIATE,
null,
0,
0,
0
)
FileLogger.i(TAG, "已启动设备选择对话框")
} catch (e: Exception) {
FileLogger.e(TAG, "启动设备选择对话框失败: ${e.message}", e)
}
}
// API 33+ (Android 13+) 使用 onAssociationCreated
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
override fun onAssociationCreated(associationInfo: AssociationInfo) {
FileLogger.i(TAG, "设备关联已创建: ${associationInfo.displayName}, ID: ${associationInfo.id}")
// 保存关联ID
try {
val macAddressMethod = associationInfo.javaClass.getMethod("getDeviceMacAddress")
val mac = macAddressMethod.invoke(associationInfo) as? String
mac?.let { savePairedMacAddress(it) }
} catch (e: Exception) {
FileLogger.e(TAG, "获取设备MAC地址失败: ${e.message}", e)
}
}
override fun onFailure(error: CharSequence?) {
FileLogger.e(TAG, "设备关联失败: $error")
}
},
null
)
return true
} catch (e: Exception) {
FileLogger.e(TAG, "启动设备关联失败: ${e.message}", e)
return false
}
}
/**
* 处理关联结果
*
* @param requestCode 请求码
* @param resultCode 结果码
* @param data Intent数据
* @return 是否处理了关联结果
*/
@RequiresApi(Build.VERSION_CODES.O)
fun handleAssociationResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean {
if (requestCode != REQUEST_CODE_ASSOCIATE) {
return false
}
FileLogger.i(TAG, "收到关联结果: resultCode=$resultCode")
if (resultCode == Activity.RESULT_OK && data != null) {
// 获取选中的蓝牙设备
val bluetoothDevice: BluetoothDevice? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
data.getParcelableExtra(CompanionDeviceManager.EXTRA_DEVICE, BluetoothDevice::class.java)
} else {
@Suppress("DEPRECATION")
data.getParcelableExtra(CompanionDeviceManager.EXTRA_DEVICE)
}
// 获取关联ID (Android 13+)
val associationId = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
try {
// 由于EXTRA_ASSOCIATION_ID常量仅在API 33+可用,直接使用已知的常量字符串值
// 不再尝试使用反射获取字段
val extraAssociationIdKey = "android.companion.extra.ASSOCIATION_ID"
data.getStringExtra(extraAssociationIdKey)
} catch (e: Exception) {
FileLogger.e(TAG, "获取关联ID失败: ${e.message}", e)
null
}
} else {
null
}
// 获取并验证MAC地址
if (bluetoothDevice != null) {
val deviceMac = bluetoothDevice.address
val deviceName = bluetoothDevice.name ?: "未知设备"
// 验证MAC地址格式
if (!isValidMacAddress(deviceMac)) {
FileLogger.e(TAG, "设备 '$deviceName' 返回的MAC地址格式无效: $deviceMac")
return false
}
// 检查是否与预期的MAC地址匹配
val savedMac = getPairedMacAddress()
if (!savedMac.isNullOrEmpty() && savedMac != deviceMac) {
FileLogger.w(TAG, "选中的设备MAC地址 ($deviceMac) 与预期的MAC地址 ($savedMac) 不匹配,但仍会保存")
}
// 保存新的MAC地址
savePairedMacAddress(deviceMac)
FileLogger.i(TAG, "设备关联成功: $deviceName ($deviceMac), 关联ID: $associationId")
// 关联成功后,自动注册后台扫描
registerBackgroundScan()
return true
} else {
FileLogger.e(TAG, "无法获取关联的蓝牙设备信息")
}
} else {
FileLogger.w(TAG, "设备关联被取消或失败: resultCode=$resultCode")
}
return false
}
/**
* 注册后台扫描
*
* @param macAddress 可选的MAC地址,如果不提供则使用已保存的MAC地址
*/
fun registerBackgroundScan() {
try {
val mac = getPairedMacAddress()
if (mac.isNullOrEmpty()) {
FileLogger.e(TAG, "注册后台扫描失败: 没有配对的设备MAC地址")
return
}
val bluetoothManager = context.getSystemService(Context.BLUETOOTH_SERVICE) as? BluetoothManager
val bluetoothAdapter = bluetoothManager?.adapter
val bluetoothLeScanner = bluetoothAdapter?.bluetoothLeScanner
if (bluetoothLeScanner == null) {
FileLogger.e(TAG, "注册后台扫描失败: 无法获取蓝牙扫描器")
return
}
// 构建过滤器
val filter = ScanFilter.Builder()
.setDeviceAddress(mac)
.build()
// 扫描设置
val settings = ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_POWER)
.build()
// 创建PendingIntent
val scanIntent = Intent(BleConst.ACTION_SCAN_RESULT)
.setClass(context, BleWakeReceiver::class.java)
val pendingIntent = PendingIntent.getBroadcast(
context,
0,
scanIntent,
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
} else {
PendingIntent.FLAG_UPDATE_CURRENT
}
)
// 启动后台扫描
bluetoothLeScanner.startScan(listOf(filter), settings, pendingIntent)
FileLogger.i(TAG, "已注册后台扫描,目标设备MAC: $mac")
} catch (e: Exception) {
FileLogger.e(TAG, "注册后台扫描失败: ${e.message}", e)
}
}
/**
* 取消后台扫描
*/
fun unregisterBackgroundScan() {
try {
val bluetoothManager = context.getSystemService(Context.BLUETOOTH_SERVICE) as? BluetoothManager
val bluetoothAdapter = bluetoothManager?.adapter
val bluetoothLeScanner = bluetoothAdapter?.bluetoothLeScanner
if (bluetoothLeScanner == null) {
FileLogger.e(TAG, "取消后台扫描失败: 无法获取蓝牙扫描器")
return
}
// 创建PendingIntent
val scanIntent = Intent(BleConst.ACTION_SCAN_RESULT)
.setClass(context, BleWakeReceiver::class.java)
val pendingIntent = PendingIntent.getBroadcast(
context,
0,
scanIntent,
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
} else {
PendingIntent.FLAG_UPDATE_CURRENT
}
)
// 停止后台扫描
bluetoothLeScanner.stopScan(pendingIntent)
FileLogger.i(TAG, "已取消后台扫描")
} catch (e: Exception) {
FileLogger.e(TAG, "取消后台扫描失败: ${e.message}", e)
}
}
/**
* 清除所有关联
*/
@RequiresApi(Build.VERSION_CODES.O)
fun clearAllAssociations() {
try {
// 清除本地保存的MAC地址
clearPairedMacAddress()
// 获取所有关联ID
val associationIds = getAssociationIds()
// 遍历解除关联
for (id in associationIds) {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
companionDeviceManager?.disassociate(id)
} else {
companionDeviceManager?.disassociate(id)
}
FileLogger.i(TAG, "已解除设备关联: $id")
} catch (e: Exception) {
FileLogger.e(TAG, "解除设备关联失败: $id, ${e.message}", e)
}
}
} catch (e: Exception) {
FileLogger.e(TAG, "清除所有关联失败: ${e.message}", e)
}
}
/**
* 验证是否是有效的MAC地址格式
*
* @param macAddress MAC地址字符串
* @return 是否是有效的MAC地址
*/
private fun isValidMacAddress(macAddress: String): Boolean {
// MAC地址格式验证:XX:XX:XX:XX:XX:XX 或 XX-XX-XX-XX-XX-XX
val macPattern = Pattern.compile(
"^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$"
)
return macPattern.matcher(macAddress).matches()
}
}

73
local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleCompanionService.kt

@ -0,0 +1,73 @@
package com.yunqiinnovation.ble_service
import android.companion.AssociationInfo
import android.companion.CompanionDeviceService
import android.content.Intent
import android.os.Build
import android.os.IBinder
import androidx.annotation.RequiresApi
import com.yunqiinnovation.deepvoice_utils.FileLogger
/**
* BleCompanionService
* 用于监听配对设备的出现和消失
* 注:此服务仅在Android 11 (API 30)及以上版本可用
*/
@RequiresApi(Build.VERSION_CODES.R)
class BleCompanionService : CompanionDeviceService() {
private val TAG = "BleCompanionService"
override fun onCreate() {
super.onCreate()
FileLogger.init(applicationContext)
FileLogger.i(TAG, "BleCompanionService已创建")
}
override fun onDeviceAppeared(info: AssociationInfo) {
super.onDeviceAppeared(info)
FileLogger.i(TAG, "检测到配对设备出现: ${info.displayName}, ID: ${info.id}")
// 当配对设备出现时,启动前台服务
try {
val serviceIntent = Intent().apply {
// 尝试使用BleMediaService,如果不存在则回退到BleService
val serviceClassName = try {
Class.forName("com.yunqiinnovation.agent_service.BleMediaService").name
} catch (e: ClassNotFoundException) {
BleService::class.java.name
}
setClassName(this@BleCompanionService, serviceClassName)
action = "android.intent.action.DEVICE_APPEARED"
putExtra("assocId", info.id)
putExtra("displayName", info.displayName)
// 如果设备信息中有MAC地址,也可以传递
// 使用反射获取MAC地址,因为deviceMacAddress在不同API版本可能不同或不存在
try {
val macAddressMethod = info.javaClass.getMethod("getDeviceMacAddress")
val mac = macAddressMethod.invoke(info) as? String
mac?.let { putExtra("mac", it) }
} catch (e: Exception) {
FileLogger.e(TAG, "获取设备MAC地址失败: ${e.message}", e)
}
}
// 启动前台服务
startForegroundService(serviceIntent)
FileLogger.i(TAG, "已启动服务")
} catch (e: Exception) {
FileLogger.e(TAG, "启动服务失败: ${e.message}", e)
}
}
override fun onDeviceDisappeared(info: AssociationInfo) {
super.onDeviceDisappeared(info)
FileLogger.i(TAG, "配对设备消失: ${info.displayName}, ID: ${info.id}")
// 可以选择停止服务或执行其他清理操作
}
// 注意: 不需要重写 onBindCompanionDeviceService 和 onUnbindCompanionDeviceService
// 这些方法在 Android 12+ 才添加,我们可以使用父类的默认实现
}

28
local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt

@ -29,6 +29,7 @@ import com.jieli.jl_audio_decode.callback.OnDecodeStreamCallback
import com.jieli.jl_audio_decode.callback.OnStateCallback
import com.jieli.jl_audio_decode.opus.OpusManager
import com.jieli.jl_audio_decode.exceptions.OpusException
import com.yunqiinnovation.ble_service.BleCompanionManager
/**
* BLE服务类:提供蓝牙低功耗设备的扫描、连接和通信功能
@ -317,6 +318,33 @@ object BleService {
// ======================================================================================================
// 设备连接与通信
// ======================================================================================================
/**
* 自动连接已配对的BLE设备
* 如果没有找到已配对的MAC地址,将不会进行连接
*/
fun connect() {
try {
// 创建 BleCompanionManager 实例
val companionManager = BleCompanionManager(context)
// 获取已配对的 MAC 地址
val pairedMac = companionManager.getPairedMacAddress()
if (pairedMac.isNullOrEmpty()) {
FileLogger.w(TAG, "未找到已配对的设备 MAC 地址")
return
}
FileLogger.i(TAG, "检测到已配对设备,MAC: $pairedMac,尝试连接")
// 调用现有的 connect 方法
connect(pairedMac)
} catch (e: Exception) {
FileLogger.e(TAG, "自动连接已配对设备失败: ${e.message}", e)
updateConnectionState(BleConst.STATE_ERROR)
}
}
/**
* 连接到指定地址的BLE设备
* @param address 设备MAC地址

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

@ -21,7 +21,8 @@ import com.yunqiinnovation.deepvoice_utils.FileLogger
* BleServicePlugin
* 作为Flutter与原生BLE通信的插件接口
*/
class BleServicePlugin : FlutterPlugin, MethodCallHandler, BleService.Callback {
class BleServicePlugin : FlutterPlugin, MethodCallHandler, ActivityAware,
PluginRegistry.ActivityResultListener, BleService.Callback {
private val TAG = "BleServicePlugin"
// 添加主线程Handler
@ -40,6 +41,15 @@ class BleServicePlugin : FlutterPlugin, MethodCallHandler, BleService.Callback {
// 事件接收器 - 只保留数据和状态通道
private var dataEventSink: EventChannel.EventSink? = null
private var statusEventSink: EventChannel.EventSink? = null
// 添加CompanionManager
private lateinit var bleCompanionManager: BleCompanionManager
// 保存最近一次的方法调用结果回调
private var pendingAssociationResult: Result? = null
// 保存当前Activity
private var activity: android.app.Activity? = null
override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
FileLogger.init(flutterPluginBinding.applicationContext)
@ -55,10 +65,11 @@ class BleServicePlugin : FlutterPlugin, MethodCallHandler, BleService.Callback {
context = flutterPluginBinding.applicationContext
// 初始化BleService
BleService.initialize(context)
BleService.addCallback(this)
// 初始化CompanionManager
bleCompanionManager = BleCompanionManager(context)
}
// 设置事件通道 - 简化版
@ -90,6 +101,42 @@ class BleServicePlugin : FlutterPlugin, MethodCallHandler, BleService.Callback {
}
}
// 实现ActivityAware接口
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
binding.addActivityResultListener(this)
activity = binding.activity
}
override fun onDetachedFromActivityForConfigChanges() {
// 配置变更时不做特殊处理
activity = null
}
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
binding.addActivityResultListener(this)
activity = binding.activity
}
override fun onDetachedFromActivity() {
// 解绑Activity时不做特殊处理
activity = null
}
// 实现ActivityResultListener接口
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean {
// 处理设备关联结果
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val handled = bleCompanionManager.handleAssociationResult(requestCode, resultCode, data)
if (handled) {
// 返回设备关联结果
pendingAssociationResult?.success(true)
pendingAssociationResult = null
return true
}
}
return false
}
// 添加sendEvent函数
private fun <T> sendEvent(eventSink: EventChannel.EventSink?, data: T, errorPrefix: String) {
if (eventSink == null) return
@ -121,7 +168,6 @@ class BleServicePlugin : FlutterPlugin, MethodCallHandler, BleService.Callback {
// 解除事件通道
dataEventChannel?.setStreamHandler(null)
statusEventChannel?.setStreamHandler(null)
}
override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
@ -132,7 +178,7 @@ class BleServicePlugin : FlutterPlugin, MethodCallHandler, BleService.Callback {
val connected = BleService.isConnected()
result.success(connected)
}
"getBatteryInfo" -> {
"getBatteryLevel" -> {
val level = BleService.getBatteryInfo()
result.success(level)
}
@ -160,7 +206,66 @@ class BleServicePlugin : FlutterPlugin, MethodCallHandler, BleService.Callback {
val success = BleService.playA2DP()
result.success(success)
}
// 新增Companion Device相关方法
"isCompanionSupported" -> {
val isSupported = bleCompanionManager.isCompanionDeviceManagerSupported()
result.success(isSupported)
}
"startDeviceAssociation" -> {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
result.error("UNSUPPORTED", "设备不支持Companion Device Manager", null)
return
}
// 保存结果回调,用于后续关联完成后返回结果
pendingAssociationResult = result
// 获取参数
val deviceNamePattern = call.argument<String>("deviceNamePattern")
val serviceUuid = call.argument<String>("serviceUuid")
val macAddress = call.argument<String>("macAddress")
val currentActivity = activity
if (currentActivity == null) {
pendingAssociationResult = null
result.error("NO_ACTIVITY", "没有可用的Activity", null)
return
}
// 启动设备关联
val success = bleCompanionManager.startAssociation(
currentActivity,
deviceNamePattern,
serviceUuid,
macAddress
)
if (!success) {
pendingAssociationResult = null
result.error("FAILED", "启动设备关联失败", null)
}
// 不要立即返回结果,等待onActivityResult回调
}
"getPairedMacAddress" -> {
val macAddress = bleCompanionManager.getPairedMacAddress()
result.success(macAddress)
}
"registerBackgroundScan" -> {
bleCompanionManager.registerBackgroundScan()
result.success(true)
}
"unregisterBackgroundScan" -> {
bleCompanionManager.unregisterBackgroundScan()
result.success(true)
}
"clearAssociations" -> {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
bleCompanionManager.clearAllAssociations()
result.success(true)
} else {
result.error("UNSUPPORTED", "设备不支持Companion Device Manager", null)
}
}
else -> {
result.notImplemented()
}
@ -170,7 +275,10 @@ class BleServicePlugin : FlutterPlugin, MethodCallHandler, BleService.Callback {
// BleService.Callback 接口实现
override fun onScanResult(devices: List<Map<String, Any>>) {
// 将扫描结果发送到Flutter
if (devices.isNotEmpty()) {
sendEvent(statusEventSink, mapOf("type" to "scanResult", "devices" to devices), "发送扫描结果异常")
}
}
override fun onConnectionStateChanged(state: Int) {
@ -212,5 +320,4 @@ class BleServicePlugin : FlutterPlugin, MethodCallHandler, BleService.Callback {
// 发送设备信息事件
sendEvent(statusEventSink, resultMap, "发送设备信息异常")
}
}

69
local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleWakeReceiver.kt

@ -0,0 +1,69 @@
package com.yunqiinnovation.ble_service
import android.bluetooth.le.ScanResult
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.os.Build
import com.yunqiinnovation.deepvoice_utils.FileLogger
/**
* BleWakeReceiver
* 用于接收后台BLE扫描结果,并启动前台服务
*/
class BleWakeReceiver : BroadcastReceiver() {
private val TAG = "BleWakeReceiver"
override fun onReceive(context: Context, intent: Intent) {
FileLogger.init(context)
FileLogger.i(TAG, "收到BLE扫描广播: ${intent.action}")
if (intent.action == BleConst.ACTION_SCAN_RESULT) {
// 从扫描结果中提取MAC地址
// val scanResults = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
// intent.getParcelableArrayListExtra(
// android.bluetooth.le.BluetoothLeScanner.EXTRA_LIST_SCAN_RESULT,
// ScanResult::class.java
// )
// } else {
// @Suppress("DEPRECATION")
// intent.getParcelableArrayListExtra(
// android.bluetooth.le.BluetoothLeScanner.EXTRA_LIST_SCAN_RESULT
// )
// }
startBleService(context)
}
}
private fun startBleService(context: Context) {
try {
// 检查是否存在BleMediaService类,如果不存在则使用BleService
val serviceClass = try {
Class.forName("com.yunqiinnovation.agent_service.BleMediaService")
} catch (e: ClassNotFoundException) {
FileLogger.i(TAG, "BleMediaService不存在")
return
}
// 创建服务Intent,并携带MAC地址
val serviceIntent = Intent().apply {
setClassName(context, serviceClass.name)
action = BleConst.ACTION_SCAN_RESULT
}
// 启动前台服务
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(serviceIntent)
FileLogger.i(TAG, "已启动前台服务: ${serviceClass.name}")
} else {
context.startService(serviceIntent)
FileLogger.i(TAG, "已启动服务: ${serviceClass.name}")
}
} catch (e: Exception) {
FileLogger.e(TAG, "启动BLE服务失败: ${e.message}", e)
}
}
}

26
local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BootReceiver.kt

@ -0,0 +1,26 @@
package com.yunqiinnovation.ble_service
import android.content.*
import android.os.Build
import android.util.Log
/**
* 系统启动接收器
* 当系统重启完成后,自动启动BLE前台服务
* 注意:需要用户在设置中授予应用自启动权限才能生效
*/
class BootReceiver : BroadcastReceiver() {
private val TAG = "BootReceiver"
override fun onReceive(ctx: Context, i: Intent) {
val serviceClass = try {
Class.forName("com.yunqiinnovation.agent_service.BleMediaService")
} catch (e: ClassNotFoundException) {
Log.i(TAG, "BleMediaService不存在")
return
}
if (i.action == Intent.ACTION_BOOT_COMPLETED) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
ctx.startForegroundService(Intent(ctx, serviceClass))
else ctx.startService(Intent(ctx, serviceClass))
}
}
}

84
local_plugins/ble_service/lib/ble_service.dart

@ -163,13 +163,80 @@ class BleService {
}
}
//========================================================================
// 以下是Companion Device相关功能
//========================================================================
/// 检查设备是否支持Companion Device Manager
///
/// 返回设备是否支持Companion Device Manager API
/// 仅在Android 8.0及以上版本可用
Future<bool> isCompanionSupported() async {
try {
final result = await _methodChannel.invokeMethod<bool>('isCompanionSupported');
return result ?? false;
} catch (e) {
print('检查Companion设备支持失败: $e');
return false;
}
}
/// 启动设备关联流程
///
/// 弹出系统设备选择对话框,用户选择后将建立持久化关联
/// 关联成功后,应用将获得后台扫描和启动前台服务的特权
///
/// [deviceNamePattern] 设备名称正则表达式,例如"MyHeadset.*"
/// [serviceUuid] 可选的服务UUID
/// [macAddress] 可选的MAC地址
///
/// 返回是否成功启动关联流程
Future<bool> startDeviceAssociation({
String? deviceNamePattern,
String? serviceUuid,
String? macAddress,
}) async {
try {
final result = await _methodChannel.invokeMethod<bool>(
'startDeviceAssociation',
{
'deviceNamePattern': deviceNamePattern,
'serviceUuid': serviceUuid,
'macAddress': macAddress,
},
);
return result ?? false;
} catch (e) {
print('启动设备关联失败: $e');
return false;
}
}
/// 获取已关联的设备MAC地址
///
/// 返回已关联的设备MAC地址,如果没有则返回null
Future<String?> getPairedMacAddress() async {
try {
final result = await _methodChannel.invokeMethod<String>('getPairedMacAddress');
return result;
} catch (e) {
print('获取配对MAC地址失败: $e');
return null;
}
}
/// 注册BLE后台扫描
///
/// 注册后,即使应用被杀死,系统也会在检测到设备广播时唤醒应用
/// 这是确保设备可以在应用后台时被自动连接的关键功能
Future<bool> registerBackgroundScan() async {
///
/// [macAddress] 可选的设备MAC地址,如果不提供则使用已关联的设备
Future<bool> registerBackgroundScan({String? macAddress}) async {
try {
final result = await _methodChannel.invokeMethod<bool>('registerBackgroundScan');
final result = await _methodChannel.invokeMethod<bool>(
'registerBackgroundScan',
{'macAddress': macAddress},
);
return result ?? false;
} catch (e) {
print('注册后台扫描失败: $e');
@ -190,6 +257,19 @@ class BleService {
}
}
/// 清除所有设备关联
///
/// 用于重置设备关联状态,通常在用户主动解除关联或调试时使用
Future<bool> clearAssociations() async {
try {
final result = await _methodChannel.invokeMethod<bool>('clearAssociations');
return result ?? false;
} catch (e) {
print('清除设备关联失败: $e');
return false;
}
}
//========================================================================
// 以下是ChatAgent相关功能
//========================================================================

127
local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/OpenAIService.kt

@ -338,29 +338,42 @@ class OpenAIService(private val context: Context? = null) {
// 获取参数
val argumentsJson = functionCall.getString("arguments")
Log.e(TAG, "自动处理MCP工具调用: name:${name} argumentsJson:${argumentsJson}")
Log.d(TAG, "自动处理MCP工具调用: name=${name}, arguments=${argumentsJson}")
val arguments = mcpClient?.parseJsonArguments(argumentsJson) ?: mapOf()
// 调用工具(统一通过MCPClient处理)
val result = mcpClient?.callTool(name, arguments)
callback.onFunctionCallResult(functionCall, result?: JSONObject().apply { put("context","工具调用失败") })
// 不论是本地函数还是MCP工具,都直接将结果发送回OpenAI
// 检查是否已取消
if (isCanceled) {
Log.d(TAG, "工具调用已被取消,不处理结果")
return
}
// 回调结果
val resultObj = result ?: JSONObject().apply { put("context", "工具调用失败") }
callback.onFunctionCallResult(functionCall, resultObj)
// 发送结果到OpenAI继续对话
val resultContent = resultObj.optString("context", "工具调用失败")
sendFunctionCallResult(
messages = messages,
functionCall = functionCall,
functionResult = result?.getString("context")?: "工具调用失败",
functionResult = resultContent,
callback = callback
)
} catch (e: Exception) {
Log.e(TAG, "自动处理工具调用失败: ${e.message}", e)
// 失败时返回错误给回调函数
val errorMessage = "工具调用失败: ${e.message}"
sendFunctionCallResult(
messages = messages,
functionCall = functionCall,
functionResult = errorMessage,
callback = callback
)
if (!isCanceled) {
Log.e(TAG, "自动处理工具调用失败: ${e.message}", e)
// 失败时返回错误给回调函数
val errorMessage = "工具调用失败: ${e.message}"
sendFunctionCallResult(
messages = messages,
functionCall = functionCall,
functionResult = errorMessage,
callback = callback
)
}
}
}
@ -554,7 +567,6 @@ class OpenAIService(private val context: Context? = null) {
try {
// 用于存储函数调用的各个部分
val finalToolCalls = mutableMapOf<Int, ToolCallInfo>()
val lineBuffer = StringBuilder()
// 逐块读取数据,而不是一次性预取
val BUFFER_SIZE = 8192L // 8KB的块大小
@ -571,14 +583,11 @@ class OpenAIService(private val context: Context? = null) {
if (line.startsWith("data:")) {
val data = line.substring(5).trim()
// Log.d("OpenAIService", "$data")
// 处理[DONE]消息
if (data == "[DONE]" || data == "[\"DONE\"]") {
val hasToolCalls = processToolCalls(finalToolCalls, callback, messages)
// Log.d(TAG, "收到[DONE]消息, $hasToolCalls")
// 只有在没有工具调用时才认为对话真正完成
// 如果没有工具调用,这是对话的真正结束
if (!hasToolCalls) {
callback.onComplete()
}
@ -594,22 +603,25 @@ class OpenAIService(private val context: Context? = null) {
if (choices.length() > 0) {
val choice = choices.getJSONObject(0)
// 处理delta内容
if (choice.has("delta")) {
val delta = choice.getJSONObject("delta")
// 处理普通文本内容
if (delta.has("content")) {
val content = delta.getString("content")
callback.onToken(content)
if (!isCanceled) {
callback.onToken(content)
}
}
// 处理工具调用(函数调用)
// 处理工具调用
if (delta.has("tool_calls")) {
val toolCalls = delta.getJSONArray("tool_calls")
for (i in 0 until toolCalls.length()) {
val toolCall = toolCalls.getJSONObject(i)
val index = toolCall.getInt("index")
// Log.e(TAG, "解析道AI需要调用工具: $toolCall")
// 创建或获取现有的工具调用信息
val toolCallInfo = finalToolCalls.getOrPut(index) { ToolCallInfo() }
@ -633,25 +645,40 @@ class OpenAIService(private val context: Context? = null) {
}
}
}
// 检查是否有表示完成的标志
if (choice.has("finish_reason")) {
val finishReason = choice.getString("finish_reason")
if (finishReason == "stop" || finishReason == "length") {
// 正常完成,没有工具调用
callback.onComplete()
return
} else if (finishReason == "tool_calls") {
// 处理工具调用
val hasToolCalls = processToolCalls(finalToolCalls, callback, messages)
if (!hasToolCalls) {
callback.onComplete()
}
return
}
}
}
}
} catch (e: Exception) {
// 忽略解析错误
Log.e(TAG, "解析JSON出错: ${e.message}")
// 忽略解析错误,但记录日志
Log.e(TAG, "解析JSON出错: ${e.message}", e)
}
}
}
}
// 如果是因为取消导致循环结束,不继续处理
// 如果是因为取消导致循环结束,直接返回
if (isCanceled) {
return
}
// 处理可能尚未处理的工具调用
// 如果没有明确的完成信号但数据流结束了,处理最后可能的工具调用
val hasToolCalls = processToolCalls(finalToolCalls, callback, messages)
// 只有在没有工具调用时才认为对话真正完成
if (!hasToolCalls) {
callback.onComplete()
}
@ -677,6 +704,11 @@ class OpenAIService(private val context: Context? = null) {
functionResult: String,
callback: StreamCallback
) {
if (isCanceled) {
Log.d(TAG, "请求已取消,不发送函数调用结果")
return
}
try {
val fullMessages = JSONArray()
@ -686,6 +718,7 @@ class OpenAIService(private val context: Context? = null) {
}
// 添加函数调用消息
val callId = functionCall.optString("id", "call_${System.currentTimeMillis()}")
fullMessages.put(JSONObject().apply {
put("role", "assistant")
put("content", "")
@ -693,7 +726,7 @@ class OpenAIService(private val context: Context? = null) {
// 添加工具调用
val toolCalls = JSONArray().apply {
val toolCall = JSONObject().apply {
put("id", functionCall.optString("id", "call_${System.currentTimeMillis()}"))
put("id", callId)
put("type", "function")
put("function", JSONObject().apply {
put("name", functionCall.getString("name"))
@ -709,13 +742,17 @@ class OpenAIService(private val context: Context? = null) {
fullMessages.put(JSONObject().apply {
put("role", "tool")
put("content", functionResult)
put("tool_call_id", functionCall.optString("id", "call_${System.currentTimeMillis()}"))
put("tool_call_id", callId)
})
// 发送完整对话
sendMessageStream(fullMessages, callback)
} catch (e: Exception) {
callback.onError(OpenAIException("发送函数调用结果失败: ${e.message}"))
if (!isCanceled) {
Log.e(TAG, "发送函数调用结果失败: ${e.message}", e)
callback.onError(OpenAIException("发送函数调用结果失败: ${e.message}"))
}
}
}
@ -736,17 +773,35 @@ class OpenAIService(private val context: Context? = null) {
put("id", firstToolCall.id)
}
Log.d(TAG, "工具调用: $functionCall")
// 通知上层工具调用事件
callback.onFunctionCall(functionCall)
// 在协程中处理工具调用
if (messages != null) {
kotlinx.coroutines.GlobalScope.launch {
try {
//通知上层回调事件,但不关心结果
callback.onFunctionCall(functionCall)
autoHandleMcpToolCall(functionCall, messages, callback)
if (!isCanceled) {
autoHandleMcpToolCall(functionCall, messages, callback)
} else {
Log.d(TAG, "请求已取消,不处理工具调用")
}
} catch (e: Exception) {
Log.e(TAG, "工具调用处理失败: ${e.message}", e)
if (!isCanceled) {
Log.e(TAG, "处理工具调用时发生异常: ${e.message}", e)
// 处理异常的情况,向回调通知错误
try {
val errorMessage = "工具调用处理失败: ${e.message}"
sendFunctionCallResult(
messages = messages,
functionCall = functionCall,
functionResult = errorMessage,
callback = callback
)
} catch (e2: Exception) {
Log.e(TAG, "发送工具调用错误结果失败: ${e2.message}", e2)
}
}
}
}
}

BIN
local_plugins/opus/.gradle/8.10/fileHashes/fileHashes.lock

Binary file not shown.

BIN
local_plugins/opus/.gradle/buildOutputCleanup/buildOutputCleanup.lock

Binary file not shown.

2
local_plugins/opus/.gradle/buildOutputCleanup/cache.properties

@ -1,2 +1,2 @@
#Fri May 09 15:56:53 IST 2025
#Sat May 10 17:59:51 IST 2025
gradle.version=8.10

Loading…
Cancel
Save