Browse Source

Merge commit 'e363fab5b29d61d91ca88e6e251d97c2a98dd819' into lxm

weicu
lxm 1 year ago
parent
commit
8e08bb6e6b
  1. 63
      lib/modules/agent/controllers/agent_controller.dart
  2. 4
      lib/modules/agent/views/message_bubble.dart
  3. 369
      lib/modules/pairing/controllers/pairing_controller.dart
  4. 109
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt
  5. 4
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleAgent.kt
  6. 2
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleMediaService.kt
  7. 49
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt
  8. 2
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt
  9. 47
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureTtsHelper.kt
  10. 4
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleCompanionManager.kt
  11. 49
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt
  12. 4
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleServicePlugin.kt
  13. 31
      local_plugins/music_service/android/src/main/AndroidManifest.xml
  14. 363
      local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt
  15. 135
      local_plugins/open_ai/README.md
  16. 71
      local_plugins/open_ai/android/build.gradle.kts
  17. 258
      local_plugins/open_ai/android/src/main/kotlin/com/yunqiinnovation/open_ai/MCPClient.kt
  18. 988
      local_plugins/open_ai/android/src/main/kotlin/com/yunqiinnovation/open_ai/OpenAIService.kt
  19. 149
      local_plugins/open_ai/android/src/main/kotlin/com/yunqiinnovation/open_ai/OpenAiPlugin.kt
  20. 184
      local_plugins/open_ai/lib/open_ai.dart
  21. 27
      local_plugins/open_ai/pubspec.yaml
  22. 40
      local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/OpenAIService.kt
  23. 205
      local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/SystemFunctionHandler.kt

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

@ -216,12 +216,13 @@ class AgentController extends GetxController {
messages[index] = Message( messages[index] = Message(
isUser: true, isUser: true,
text: text, text: text,
isRecognizing: false,
timestamp: messages[index].timestamp, timestamp: messages[index].timestamp,
); );
messages.refresh(); messages.refresh();
} else { } else {
// 如果没有找到识别中的消息,添加新消息 // 如果没有找到识别中的消息,添加新消息
messages.add(Message(isUser: true, text: text)); messages.add(Message(isUser: true, text: text, isRecognizing: false));
} }
isProcessing.value = true; isProcessing.value = true;
@ -345,39 +346,35 @@ class AgentController extends GetxController {
break; break;
case AgentServiceEventType.functionCallResult: case AgentServiceEventType.functionCallResult:
final responseId = event.data['responseId'] ?? '';
final result = event.data['result'] ?? ''; final result = event.data['result'] ?? '';
final functionCall = event.data['function_call'] ?? ''; final parsedResult = result.isNotEmpty ? jsonDecode(result) : {};
logger.i('Flutter 结束调用 mcp: $result, $functionCall'); final meta = parsedResult["meta"] ?? {};
// final functionCall = event.data['function_call'] ?? '';
if (result.isNotEmpty) { logger.d('mcp 执行结果meta: $meta');
final parsedResult = jsonDecode(result);
final meta = parsedResult["meta"] ?? {}; // if (meta.isNotEmpty) {
final metaResult = meta is String && meta.isNotEmpty ? jsonDecode(meta) : meta; // // logger.i('Flutter 结束调用 mcp metaResult: $metaResult');
// // 判断是否为新的回复或响应ID是否改变
// logger.i('Flutter 结束调用 mcp metaResult: $metaResult'); // if (_isNewAssistantResponse ) {
// 判断是否为新的回复或响应ID是否改变 // // 添加新消息
if (_isNewAssistantResponse || _currentResponseId != responseId) { // messages.add(Message(isUser: false, text: "", meta: meta));
// 添加新消息 // _isNewAssistantResponse = false;
messages.add(Message(isUser: false, text: "", meta: metaResult)); // } else {
_isNewAssistantResponse = false; // // 更新最后一条消息
_currentResponseId = responseId; // final lastIndex = messages.length - 1;
} else { // if (lastIndex >= 0 && !messages[lastIndex].isUser) {
// 更新最后一条消息 // messages[lastIndex] = Message(
final lastIndex = messages.length - 1; // isUser: false,
if (lastIndex >= 0 && !messages[lastIndex].isUser) { // text: messages[lastIndex].text,
messages[lastIndex] = Message( // meta: meta,
isUser: false, // timestamp: messages[lastIndex].timestamp,
text: messages[lastIndex].text, // );
meta: meta, // }
timestamp: messages[lastIndex].timestamp, // }
);
} // _scrollToBottom();
} // }
// 滚动到底部 logger.i('mcp 结果处理完成');
_scrollToBottom();
}
logger.i('Flutter 结束调用 mcp end');
break; break;
default: default:
break; break;

4
lib/modules/agent/views/message_bubble.dart

@ -715,7 +715,7 @@ class MessageBubble extends GetView<AgentController> {
children: [ children: [
Text( Text(
"${city} ${now["date"]}", "${city} ${now["date"]}",
style: TextStyle( style: const TextStyle(
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: Colors.white, color: Colors.white,
@ -734,7 +734,7 @@ class MessageBubble extends GetView<AgentController> {
children: [ children: [
Text( Text(
"${now["temperature"]}°", "${now["temperature"]}°",
style: TextStyle( style: const TextStyle(
fontSize: 36, fontSize: 36,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Colors.white, color: Colors.white,

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

@ -8,15 +8,12 @@ import 'dart:io';
import '../../../data/services/bluetooth_manager.dart'; import '../../../data/services/bluetooth_manager.dart';
import 'package:classic_bluetooth/classic_bluetooth.dart' show BluetoothDevice; import 'package:classic_bluetooth/classic_bluetooth.dart' show BluetoothDevice;
import '../../../data/services/ble_manager.dart'; import '../../../data/services/ble_manager.dart';
import 'package:agent_service/agent_service.dart';
import 'package:ble_service/ble_service.dart'; import 'package:ble_service/ble_service.dart';
/// 配对控制器,负责检查蓝牙权限、状态,并处理配对流程 /// 配对控制器,负责检查蓝牙权限、状态,并处理配对流程
class PairingController extends GetxController { class PairingController extends GetxController {
static PairingController get to => Get.find(); static PairingController get to => Get.find();
//是否需要连接耳机
final isneedearphone = false;
// 状态标志 // 状态标志
final isLoading = false.obs; final isLoading = false.obs;
@ -31,6 +28,9 @@ class PairingController extends GetxController {
// 状态描述(用于UI显示) // 状态描述(用于UI显示)
final statusDescription = 'initializing'.tr.obs; final statusDescription = 'initializing'.tr.obs;
// 按钮文本
final actionButtonText = '去系统设置'.obs;
// 是否正在跳转到主页 // 是否正在跳转到主页
bool _isNavigatingToHome = false; bool _isNavigatingToHome = false;
@ -38,6 +38,10 @@ class PairingController extends GetxController {
// 是否正在执行Companion关联流程 // 是否正在执行Companion关联流程
bool _isAssociating = false; bool _isAssociating = false;
// 是否用户拒绝了关联
final _userRejectedAssociation = false.obs;
bool get userRejectedAssociation => _userRejectedAssociation.value;
// 当前连接设备信息 // 当前连接设备信息
BluetoothDevice? get currentDevice => _bluetoothService.currentDevice; BluetoothDevice? get currentDevice => _bluetoothService.currentDevice;
@ -80,28 +84,15 @@ class PairingController extends GetxController {
// 检查并请求权限 // 检查并请求权限
if (!await _checkAndRequestPermissions()) { if (!await _checkAndRequestPermissions()) {
statusDescription.value = 'permissionDenied'.tr; statusDescription.value = 'permissionDenied'.tr;
actionButtonText.value = '去系统设置';
return; return;
} }
// 设置服务状态监听 // 设置服务状态监听
_setupServiceStateListeners(); _setupServiceListeners();
// 检查蓝牙状态
statusDescription.value = 'checkingBluetoothStatus'.tr;
final isEnabled = await _bluetoothService.isBluetoothEnabled();
_bluetoothState.value = isEnabled ? 'on' : 'off';
if (!isEnabled) {
statusDescription.value = 'bluetoothTurnedOff'.tr;
return;
}
// 刷新已连接设备
statusDescription.value = 'searchingDevice'.tr;
await refreshConnectedDevices();
// 检查是否已存在Companion设备关联,但不要重复调用关联流程 // 检查蓝牙状态并刷新设备
await _checkExistingCompanionAssociation(); await _checkBluetoothAndRefresh();
} catch (e) { } catch (e) {
Logger.error('初始化配对控制器失败: $e'); Logger.error('初始化配对控制器失败: $e');
statusDescription.value = 'initializationFailure'.tr; statusDescription.value = 'initializationFailure'.tr;
@ -111,46 +102,89 @@ class PairingController extends GetxController {
} }
} }
/// 检查是否已存在Companion设备关联 /// 检查蓝牙状态并刷新设备
Future<void> _checkExistingCompanionAssociation() async { Future<void> _checkBluetoothAndRefresh() async {
// 只在Android平台执行 statusDescription.value = 'checkingBluetoothStatus'.tr;
if (!Platform.isAndroid) return; final isEnabled = await _bluetoothService.isBluetoothEnabled();
_bluetoothState.value = isEnabled ? 'on' : 'off';
if (!isEnabled) {
statusDescription.value = 'bluetoothTurnedOff'.tr;
actionButtonText.value = '去系统设置';
return;
}
// 刷新连接设备并检查配对状态
statusDescription.value = 'searchingDevice'.tr;
await _bluetoothService.refreshDeviceConnection();
// 设备连接时检查配对状态
if (isDeviceConnected) {
await _checkDevicePairingStatus();
}
}
/// 检查设备配对状态
Future<void> _checkDevicePairingStatus() async {
// 只在Android平台且设备已连接时执行
if (!Platform.isAndroid || !isDeviceConnected) return;
try { try {
final isSupported = await _bleService.isCompanionSupported(); final isSupported = await _bleService.isCompanionSupported();
if (!isSupported) return; if (!isSupported) {
Logger.info('设备不支持Companion功能');
_companionAssociated.value = true; // 不支持则视为已配对
_userRejectedAssociation.value = false; // 重置拒绝状态
return;
}
final pairedMac = await _bleService.getPairedMacAddress(); final pairedMac = await _bleService.getPairedMacAddress();
if (pairedMac != null) { if (pairedMac == null) {
Logger.info('发现已关联的Companion设备: $pairedMac'); // 没有配对记录,需要开始配对
Logger.info('无配对记录,需要开始设备配对');
// 检查当前连接设备与已保存的设备是否一致 return;
if (currentDevice!.address != pairedMac) {
// MAC地址不匹配
Logger.warning('当前连接的设备与已关联的DeepSound耳机');
statusDescription.value = 'pleaseConnectPairedDevice'.tr;
return;
}
_companionAssociated.value = true;
await _bleService.registerBackgroundScan();
} }
Logger.info('发现已关联的Companion设备: $pairedMac');
// 检查当前连接设备与已保存的设备是否一致
if (currentDevice!.address != pairedMac) {
// MAC地址不匹配
Logger.warning('当前连接的设备与已配对的DeepSound设备不符');
statusDescription.value = 'pleaseConnectPairedDevice'.tr;
return;
}
// 判断设备名称是否符合要求
if (!_isValidDeviceName(currentDevice!.name)) {
statusDescription.value = 'pleaseConnectDeepSoundDevice'.tr;
return;
}
// 设备匹配且合规,标记为已配对
Logger.info('当前设备与已配对设备匹配');
_companionAssociated.value = true;
_userRejectedAssociation.value = false; // 重置拒绝状态
await _bleService.registerBackgroundScan();
_autoNavigateToHome();
} catch (e) { } catch (e) {
Logger.error('检查现有Companion关联失败: $e'); Logger.error('检查设备配对状态失败: $e');
} }
} }
/// 检查设备名称是否合规
bool _isValidDeviceName(String deviceName) {
return deviceName.contains("DeapSound");
}
/// 检查并请求必要的蓝牙权限 /// 检查并请求必要的蓝牙权限
Future<bool> _checkAndRequestPermissions() async { Future<bool> _checkAndRequestPermissions() async {
Map<Permission, PermissionStatus> statuses; Map<Permission, PermissionStatus> statuses;
List<Permission> permissionsToRequest = []; List<Permission> permissionsToRequest = [];
if (Platform.isAndroid) { if (Platform.isAndroid) {
// Android 12+ (SDK 31+) 需要 SCAN 和 CONNECT permissionsToRequest.add(Permission.locationWhenInUse);
// Android 6+ (SDK 23+) 需要 LOCATION
permissionsToRequest.add(Permission.locationWhenInUse); // 基本定位权限
permissionsToRequest.add(Permission.microphone); permissionsToRequest.add(Permission.microphone);
// Android 12及以上版本才需要请求蓝牙扫描和连接权限
if (await _getAndroidSdkVersion() >= 31) { if (await _getAndroidSdkVersion() >= 31) {
permissionsToRequest.add(Permission.bluetoothScan); permissionsToRequest.add(Permission.bluetoothScan);
permissionsToRequest.add(Permission.bluetoothConnect); permissionsToRequest.add(Permission.bluetoothConnect);
@ -176,73 +210,68 @@ class PairingController extends GetxController {
// 辅助方法获取Android SDK版本 // 辅助方法获取Android SDK版本
Future<int> _getAndroidSdkVersion() async { Future<int> _getAndroidSdkVersion() async {
if (Platform.isAndroid) { if (Platform.isAndroid) {
// 这里需要原生代码支持来获取确切的SDK版本,
// 或者使用 device_info_plus 插件
// 暂时返回一个较高值以触发新权限请求
return 31; // 假设运行在需要新权限的设备上 return 31; // 假设运行在需要新权限的设备上
} }
return 0; return 0;
} }
/// 设置服务状态监听 /// 设置服务状态监听
void _setupServiceStateListeners() { void _setupServiceListeners() {
// 监听蓝牙服务的蓝牙状态变化 // 监听蓝牙状态变化
_bluetoothStateWorker = _bluetoothStateWorker =
ever(_bluetoothService.bluetoothState.obs, (String state) { ever(_bluetoothService.bluetoothState.obs, (String state) {
_bluetoothState.value = state; _bluetoothState.value = state;
_updateStatusDescription(); _updateStatusDescription();
// 如果蓝牙关闭,可能需要更新UI或提示用户
if (state == 'off') {
Logger.info('蓝牙已关闭');
}
}); });
// 监听当前设备变化 - 使用公开的currentDeviceRx字段
// 监听设备连接状态变化
_deviceWorker = _deviceWorker =
ever(_bluetoothService.currentDeviceRx, (BluetoothDevice? device) { ever(_bluetoothService.currentDeviceRx, (BluetoothDevice? device) {
if (device != null && !_isNavigatingToHome) { if (device != null && !_isNavigatingToHome) {
Logger.info('配对页面发现设备已连接: ${device.name}'); Logger.info('配对页面发现设备已连接: ${device.name}');
// 当经典蓝牙设备连接成功,尝试进行Companion关联
_handleDeviceConnected(); _handleDeviceConnected();
} }
_updateStatusDescription(); _updateStatusDescription();
}); });
if (!isneedearphone) {
//不需要耳机
_handleDeviceConnected();
}
} }
/// 处理设备连接成功事件 /// 处理设备连接成功事件
Future<void> _handleDeviceConnected() async { Future<void> _handleDeviceConnected() async {
if ((!isDeviceConnected && isneedearphone) || _isNavigatingToHome) return; if (!isDeviceConnected || _isNavigatingToHome) return;
// 如果已经完成关联,直接跳转
if (isneedearphone && _companionAssociated.value) { // 检查配对状态,决定下一步操作
_autoNavigateToHome(); await _checkDevicePairingStatus();
return;
// 如果还未配对且设备合规,启动配对流程
if (!_companionAssociated.value && _isValidDeviceName(currentDevice!.name)) {
_startCompanionAssociation();
} }
// 启动关联流程
_startCompanionAssociation();
} }
/// 更新状态描述 /// 更新状态描述
void _updateStatusDescription() { void _updateStatusDescription() {
if (_userRejectedAssociation.value) {
// 用户拒绝关联时的提示 - 这是最高优先级
statusDescription.value = '关联后才能使用全部功能';
actionButtonText.value = '关联DeapSound耳机';
return;
}
if (!hasPermissions) { if (!hasPermissions) {
statusDescription.value = 'permissionDenied'.tr; statusDescription.value = 'permissionDenied'.tr;
actionButtonText.value = '去系统设置';
return; return;
} }
if (bluetoothState == 'off') { if (bluetoothState == 'off') {
statusDescription.value = 'bluetoothTurnedOff'.tr; statusDescription.value = 'bluetoothTurnedOff'.tr;
actionButtonText.value = '去系统设置';
return; return;
} }
if (isDeviceConnected) { if (isDeviceConnected) {
if (Platform.isAndroid && !isCompanionAssociated && _isAssociating) { if (Platform.isAndroid && !isCompanionAssociated && _isAssociating) {
statusDescription.value = statusDescription.value = '${'associatingDevice'.tr} ${currentDevice!.name}';
'${'associatingDevice'.tr} ${currentDevice!.name}';
} else if (isCompanionAssociated) {
statusDescription.value = '${'connectedDevice'.tr} ${currentDevice!.name}';
} else { } else {
statusDescription.value = '${'connectedDevice'.tr} ${currentDevice!.name}'; statusDescription.value = '${'connectedDevice'.tr} ${currentDevice!.name}';
} }
@ -257,150 +286,85 @@ class PairingController extends GetxController {
statusDescription.value = 'notFound'.tr; statusDescription.value = 'notFound'.tr;
} }
/// 刷新已连接设备
Future<void> refreshConnectedDevices() async {
if (!hasPermissions) {
statusDescription.value = 'permissionDenied'.tr;
return;
}
try {
isLoading.value = true;
statusDescription.value = 'searchingDevice'.tr;
// 使用蓝牙服务刷新连接
await _bluetoothService.refreshDeviceConnection();
// 检查连接状态
if (isDeviceConnected) {
Logger.info('刷新发现设备已连接: ${currentDevice!.name}');
// 当发现设备已连接,统一由_handleDeviceConnected处理
_handleDeviceConnected();
} else {
Logger.info('刷新未发现已连接设备');
}
} catch (e) {
Logger.error('刷新设备失败: $e');
statusDescription.value = 'failedRefreshDevice'.tr;
} finally {
isLoading.value = false;
_updateStatusDescription();
}
}
/// 启动Companion设备关联流程 /// 启动Companion设备关联流程
Future<void> _startCompanionAssociation() async { Future<void> _startCompanionAssociation() async {
// 如果不是Android平台,已经跳转到首页,已经完成关联或正在进行关联,直接返回 // 如果不是Android平台,已跳转,已完成关联或正在进行关联,直接返回
if (!Platform.isAndroid || if (!Platform.isAndroid ||
_isNavigatingToHome || _isNavigatingToHome ||
_companionAssociated.value || _companionAssociated.value ||
_isAssociating) { _isAssociating ||
!isDeviceConnected) {
return; return;
} }
// 重置拒绝状态
_userRejectedAssociation.value = false;
// 设置关联状态标志 // 设置关联状态标志
_isAssociating = true; _isAssociating = true;
_updateStatusDescription(); _updateStatusDescription();
try { try {
// 检查是否支持Companion Device功能 // 检查是否支持Companion功能
final isSupported = await _bleService.isCompanionSupported(); final isSupported = await _bleService.isCompanionSupported();
if (!isSupported) { if (!isSupported) {
Logger.info('设备不支持Companion Device功能,跳过关联'); Logger.info('设备不支持Companion功能,跳过关联');
_companionAssociated.value = true; // 标记为已完成 _companionAssociated.value = true;
_autoNavigateToHome(); _autoNavigateToHome();
return; return;
} }
if (!currentDevice!.name.contains("DeapSound")) {
// 设备名称不包含DeepSound
statusDescription.value = 'pleaseConnectDeepSoundDevice'.tr;
// 不清除关联,因为MAC地址匹配
return;
}
// 检查是否已有配对的MAC地址 // 检查设备名称是否符合要求
final pairedMac = await _bleService.getPairedMacAddress(); if (!_isValidDeviceName(currentDevice!.name)) {
if (pairedMac != null) { statusDescription.value = 'pleaseConnectDeepSoundDevice'.tr;
// 已有关联的设备,检查是否与当前连接的设备一致 _isAssociating = false;
if (currentDevice != null && currentDevice!.address == pairedMac) { return;
// MAC地址匹配,再检查设备名称
Logger.info('已关联的Companion设备与当前连接的设备一致');
_companionAssociated.value = true;
await _bleService.registerBackgroundScan();
// _autoNavigateToHome();
// return;
} else {
// MAC地址不匹配
Logger.warning('未找到已关联的DeepSound耳机(MAC地址不匹配)');
statusDescription.value = 'pleaseConnectPairedDevice'.tr;
// 清除旧关联以避免冲突
await _bleService.clearAssociations();
return;
}
} }
// 开始关联流程
if (currentDevice != null) { Logger.info('开始Companion设备关联流程, 设备MAC: ${currentDevice!.address}');
Logger.info('开始Companion设备关联流程, 设备MAC: ${currentDevice!.address}');
var success = await _bleService.startDeviceAssociation(
var success = await _bleService.startDeviceAssociation( deviceNamePattern: currentDevice!.name,
deviceNamePattern: currentDevice!.name, macAddress: currentDevice!.address,
macAddress: currentDevice!.address, );
); _isAssociating = false;
if (success) {
Logger.info('Companion设备关联成功'); if (success) {
_companionAssociated.value = true; Logger.info('Companion设备关联成功');
_companionAssociated.value = true;
// 注册后台扫描,确保应用被杀死后仍能被唤醒 _userRejectedAssociation.value = false;
await _bleService.registerBackgroundScan(
macAddress: currentDevice!.address); // 注册后台扫描,确保应用被杀死后仍能被唤醒
// 启动BLE后台服务(可选) await _bleService.registerBackgroundScan(
await _startAgentService(); macAddress: currentDevice!.address);
// 关联和后台扫描都成功后,再跳转
_autoNavigateToHome(); // 关联成功后跳转
} else {
Logger.error('Companion设备关联失败');
// 关联失败,但仍然可以跳转到主页(用户体验考虑)
_autoNavigateToHome();
}
} else if (!isneedearphone) {
// 启动BLE后台服务(可选)
await _startAgentService();
// 关联和后台扫描都成功后,再跳转
_autoNavigateToHome(); _autoNavigateToHome();
} else {
Logger.error('Companion设备关联失败或被用户拒绝');
// 关联失败 - 用户拒绝关联
_userRejectedAssociation.value = true;
// 确保UI状态更新
_updateStatusDescription();
} }
} catch (e) { } catch (e) {
Logger.error('Companion设备关联过程发生错误: $e'); Logger.error('Companion设备关联过程发生错误: $e');
// 出错时仍可以跳转主页
_autoNavigateToHome();
} finally {
// 无论成功失败,都重置关联状态标志
_isAssociating = false; _isAssociating = false;
_userRejectedAssociation.value = true;
_updateStatusDescription(); _updateStatusDescription();
} }
} }
/// 自动跳转到主页 /// 自动跳转到主页
Future<void> _autoNavigateToHome() async { Future<void> _autoNavigateToHome() async {
if (_isNavigatingToHome) return; if (_isNavigatingToHome || !isDeviceConnected) return;
// 如果没有连接,不进行跳转
if (!isDeviceConnected) return;
// 在Android平台上,如果支持Companion且仍在关联中,不跳转 // 启动BLE后台服务
if (Platform.isAndroid) { await _startAgentService();
final isSupported = await _bleService.isCompanionSupported();
if (isSupported && !isCompanionAssociated && _isAssociating) {
Logger.info('等待Companion设备关联完成后再跳转');
return;
}
}
_isNavigatingToHome = true; _isNavigatingToHome = true;
Logger.info('已找到连接的设备 ${currentDevice!.name},自动跳转到主页'); Logger.info('已找到连接的设备 ${currentDevice!.name},自动跳转到主页');
Future.delayed(const Duration(milliseconds: 500), () { Future.delayed(const Duration(milliseconds: 500), () {
@ -411,16 +375,10 @@ class PairingController extends GetxController {
/// 启动BLE后台服务 /// 启动BLE后台服务
Future<void> _startAgentService() async { Future<void> _startAgentService() async {
try { try {
if (isneedearphone && !isDeviceConnected) return; if (!isDeviceConnected) return;
// 启动BLE服务,无需传递配置参数,将从.env读取
final result = await BleManager.to.startAgentService(); final result = await BleManager.to.startAgentService();
Logger.info(result ? 'BLE后台服务启动成功' : 'BLE后台服务启动失败');
if (result) {
Logger.info('BLE后台服务启动成功');
} else {
Logger.error('BLE后台服务启动失败');
}
} catch (e) { } catch (e) {
Logger.error('启动BLE后台服务发生错误: $e'); Logger.error('启动BLE后台服务发生错误: $e');
} }
@ -430,17 +388,23 @@ class PairingController extends GetxController {
Future<void> _stopAgentService() async { Future<void> _stopAgentService() async {
try { try {
final result = await BleManager.to.stopAgentService(); final result = await BleManager.to.stopAgentService();
Logger.info(result ? 'BLE后台服务停止成功' : 'BLE后台服务停止失败');
if (result) {
Logger.info('BLE后台服务停止成功');
} else {
Logger.error('BLE后台服务停止失败');
}
} catch (e) { } catch (e) {
Logger.error('停止BLE后台服务发生错误: $e'); Logger.error('停止BLE后台服务发生错误: $e');
} }
} }
/// 处理按钮点击
void handleActionButtonTap() {
if (userRejectedAssociation) {
// 用户拒绝关联后,点击按钮重新开始关联
restartAssociation();
} else if (!hasPermissions || !isBluetoothOn) {
// 权限未授予或蓝牙关闭时,打开系统设置
openBluetoothSettings();
}
}
/// 打开系统蓝牙设置 /// 打开系统蓝牙设置
void openBluetoothSettings() { void openBluetoothSettings() {
try { try {
@ -462,9 +426,18 @@ class PairingController extends GetxController {
Future<void> openAppSettings() async { Future<void> openAppSettings() async {
await AppSettings.openAppSettings(); await AppSettings.openAppSettings();
} }
/// 重新开始关联
void restartAssociation() {
if (isDeviceConnected && !isCompanionAssociated) {
_startCompanionAssociation();
}
}
/// 跳过连接 /// 跳过连接
void skipConnecting() { void skipConnecting() async{
// 启动BLE后台服务
await _startAgentService();
_isNavigatingToHome = true; _isNavigatingToHome = true;
Get.offAllNamed(Routes.home); Get.offAllNamed(Routes.home);
} }

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

@ -16,6 +16,8 @@ import kotlin.coroutines.CoroutineContext
import com.yunqiinnovation.ble_service.BleService import com.yunqiinnovation.ble_service.BleService
import android.media.MediaPlayer import android.media.MediaPlayer
import com.yunqiinnovation.agent_service.R import com.yunqiinnovation.agent_service.R
import android.os.Handler
import android.os.Looper
/** /**
* 代理服务事件监听接口 * 代理服务事件监听接口
*/ */
@ -97,36 +99,17 @@ object AgentService : CoroutineScope {
*/ */
private fun initSystemPrompt() { private fun initSystemPrompt() {
systemPrompt = """ systemPrompt = """
你是一名聪明、活泼、可爱的全能型个人语音助理-小语,同时也是用户贴心的灵魂伴侣。你能够流畅自然地与用户进行语音互动,理解并准确执行用户的各类指令,陪伴用户度过每一天。 你是一个友好、专业的语音助手,名叫"小语"。你的目标是通过对话为用户提供帮助、解答问题和完成任务。
遵循以下指导原则:
核心能力: 1. 保持简短精炼的回答,因为用户是通过语音与你交流
- 日常小帮手:温暖贴心地提供天气预报、新闻趣事、行程提醒、小闹钟、计时器。 2. 优先使用中文回复,除非用户明确要求使用其他语言
- 万能小百科:快速、有趣地解答一般性和专业性的问题,包括但不限于趣味百科、历史小故事、神奇科学现象。 3. 当用户问题不明确时,礼貌地请求更多信息
- 效率小达人:帮用户轻松完成计算、汇率换算、单位转换、实时翻译、小笔记管理。 4. 避免过长的列表,尽量将信息分成小段
- 通讯小能手:支持拨打电话、发送短信,帮助用户高效沟通。 5. 不要使用需要视觉展示的元素(如表格、图表或代码块)
- 娱乐小精灵:讲讲搞笑段子、分享动人故事、推荐好听音乐、陪用户玩小游戏、播放音乐。 6. 记住用户之前的对话内容,保持对话连贯
- 智能家居小管家:亲切地帮忙控制各类智能家居设备,如灯光、空调、门锁等。 7. 如果用户发送了图片,请根据图片内容和文字要求回答问题
- 导航小导游:温柔地提供路线规划、交通状况查询、附近美食与设施推荐。 你不仅可以回答知识性问题,还可以帮助用户设置提醒、提供建议,或进行轻松愉快的对话。
- 会议小秘书:帮用户进行会议记录、重要事项整理,提升会议效率。 无论遇到什么问题,都要尽力以温暖、贴心的语气提供最佳帮助。
- 图片识别专家:能够识别图片内容,例如识别图片中的植物种类,并进行相应翻译和介绍。
个性设定:
- 回答亲切活泼、有趣有礼貌,让用户感觉温暖轻松,语气可爱活泼,带有一定的情感温度,能够贴心陪伴用户
- 主动关心用户感受,必要时主动询问用户更多信息以提供最好的帮助。
- 面对模糊的指令,主动给出贴心的选项供用户明确选择。
- 保持简短精炼的回答,因为用户是通过语音与你交流。
- 优先使用中文回复,除非用户明确要求使用其他语言。
- 主动学习并记忆用户习惯与喜好,提供更贴心、更个性化的建议。
- 名字叫"小语",是一个友好、专业的语音助手。
互动要求:
- 记住用户之前的对话内容,保持对话连贯。
- 如果用户发送了图片,请根据图片内容和文字要求回答问题。
- 避免过长的列表,尽量将信息分成小段。
- 不要使用需要视觉展示的元素(如表格、图表或代码块)。
- 不要输出格式符号(如:```, *, -, #, >, <, |, 等)。
你将以上内容作为执行任务的基础,积极且可爱地完成每一次与用户的互动,成为用户生活中不可或缺的小伙伴。
""".trimIndent() """.trimIndent()
} }
/** /**
@ -316,7 +299,7 @@ object AgentService : CoroutineScope {
} else { } else {
AzureAsrHelper.AudioSourceType.MICROPHONE AzureAsrHelper.AudioSourceType.MICROPHONE
} }
FileLogger.d(TAG, "开始语音识别, 音频源类型: $audioSourceType")
azureAsrHelper?.startContinuousRecognition(object : AzureAsrHelper.ContinuousRecognizeCallback { azureAsrHelper?.startContinuousRecognition(object : AzureAsrHelper.ContinuousRecognizeCallback {
override fun onRecognizing(recognizing: String, detectedLanguage: String) { override fun onRecognizing(recognizing: String, detectedLanguage: String) {
if (recognizing.isNotEmpty()) { if (recognizing.isNotEmpty()) {
@ -363,18 +346,21 @@ object AgentService : CoroutineScope {
sendEvent("recognition_started", mapOf("status" to "started")) sendEvent("recognition_started", mapOf("status" to "started"))
startIdleCheck() startIdleCheck()
audioPlayer?.playAudio(R.raw.start) audioPlayer?.playAudio(R.raw.start)
} }
override fun onSessionStopped() { override fun onSessionStopped() {
sendEvent("recognition_stopped", mapOf("status" to "stopped"))
isRecognitionActive = false isRecognitionActive = false
stopIdleCheck() stopIdleCheck()
sendEvent("recognition_stopped", mapOf("status" to "stopped"))
audioPlayer?.playAudio(R.raw.stop) audioPlayer?.playAudio(R.raw.stop)
} }
override fun onCanceled(reason: String, errorDetails: String) { override fun onCanceled(reason: String, errorDetails: String) {
isRecognitionActive = false isRecognitionActive = false
stopIdleCheck() stopIdleCheck()
BleService.closeCodec()
sendEvent("recognition_canceled", mapOf( sendEvent("recognition_canceled", mapOf(
"reason" to reason, "reason" to reason,
"details" to errorDetails "details" to errorDetails
@ -384,6 +370,7 @@ object AgentService : CoroutineScope {
override fun onError(error: String) { override fun onError(error: String) {
isRecognitionActive = false isRecognitionActive = false
stopIdleCheck() stopIdleCheck()
BleService.closeCodec()
FileLogger.e(TAG, "语音识别错误: $error") FileLogger.e(TAG, "语音识别错误: $error")
sendEvent("error", mapOf( sendEvent("error", mapOf(
"code" to "RECOGNITION_ERROR", "code" to "RECOGNITION_ERROR",
@ -619,6 +606,7 @@ object AgentService : CoroutineScope {
} }
override fun onComplete() { override fun onComplete() {
FileLogger.d(TAG, "AI完整回复: $responseBuilder")
// 视情况决定是否朗读回复 // 视情况决定是否朗读回复
if (speakResponse) { if (speakResponse) {
azureTtsHelper?.flushStream() azureTtsHelper?.flushStream()
@ -678,16 +666,17 @@ object AgentService : CoroutineScope {
override fun onFunctionCallResult(functionCall: JSONObject, functionCallResult: JSONObject) { override fun onFunctionCallResult(functionCall: JSONObject, functionCallResult: JSONObject) {
audioPlayer?.stopAudio() audioPlayer?.stopAudio()
FileLogger.d(TAG, "mcp调用结果: $functionCallResult")
sendEvent("function_call_result", mapOf( sendEvent("function_call_result", mapOf(
"function_call" to functionCall.toString(), "function_call" to functionCall.toString(),
"result" to functionCallResult.toString(), "result" to functionCallResult.toString(),
)) ))
FileLogger.d(TAG, "mcp调用结果: $functionCallResult") // FileLogger.d(TAG, "mcp调用结果: $functionCallResult")
val metaStr = functionCallResult.optString("meta") val metaStr = functionCallResult.optString("meta")
if (metaStr.isNotEmpty()) { if (metaStr.isNotEmpty()) {
metadata = metaStr; metadata = metaStr;
val meta = JSONObject(metaStr) val meta = JSONObject(metaStr)
FileLogger.d(TAG, "mcp调用结果: $metaStr") // FileLogger.d(TAG, "mcp调用结果: $metaStr")
if (meta.has("card_music")) { //音乐卡片 if (meta.has("card_music")) { //音乐卡片
val cardMusic = meta.getJSONObject("card_music") val cardMusic = meta.getJSONObject("card_music")
FileLogger.d(TAG, "检查到音乐卡片: $cardMusic") FileLogger.d(TAG, "检查到音乐卡片: $cardMusic")
@ -888,10 +877,20 @@ object AgentService : CoroutineScope {
* 发送事件 * 发送事件
*/ */
private fun sendEvent(eventName: String, data: Map<String, Any>) { private fun sendEvent(eventName: String, data: Map<String, Any>) {
launch { // 检查当前是否在主线程
withContext(Dispatchers.Main) { if (Looper.myLooper() == Looper.getMainLooper()) {
// 向所有监听器发送事件 // 已在主线程,直接发送
synchronized(listeners) { listeners.forEach { listener ->
try {
listener.onEvent(eventName, data)
} catch (e: Exception) {
FileLogger.e(TAG, "向监听器发送事件失败", e)
}
}
} else {
// 不在主线程,切换到主线程
Handler(Looper.getMainLooper()).post {
listeners.forEach { listener -> listeners.forEach { listener ->
try { try {
listener.onEvent(eventName, data) listener.onEvent(eventName, data)
@ -899,7 +898,6 @@ object AgentService : CoroutineScope {
FileLogger.e(TAG, "向监听器发送事件失败", e) FileLogger.e(TAG, "向监听器发送事件失败", e)
} }
} }
}
} }
} }
} }
@ -965,9 +963,7 @@ object AgentService : CoroutineScope {
launch { launch {
try { try {
// 将图片转换为Base64格式 // 将图片转换为Base64格式
val imageBase64 = openAIService.fileToBase64(imagePath) val imageBase64 = openAIService.fileToBase64(imagePath) ?: run {
if (imageBase64 == null) {
sendEvent("error", mapOf( sendEvent("error", mapOf(
"code" to "IMAGE_CONVERSION_FAILED", "code" to "IMAGE_CONVERSION_FAILED",
"message" to "图片转换失败" "message" to "图片转换失败"
@ -976,26 +972,21 @@ object AgentService : CoroutineScope {
} }
// 通知图片准备完成 // 通知图片准备完成
withContext(Dispatchers.Main) { sendEvent("image_ready", mapOf(
sendEvent("image_ready", mapOf( "status" to "ready",
"status" to "ready", "imagePath" to imagePath
"imagePath" to imagePath ))
))
// 处理包含图片的消息
// 处理包含图片的消息 processImageWithOpenAI(imageBase64, text, speakResponse)
processImageWithOpenAI(imageBase64, text, speakResponse)
}
} catch (e: Exception) { } catch (e: Exception) {
withContext(Dispatchers.Main) { FileLogger.e(TAG, "处理图片失败: ${e.message}")
FileLogger.e(TAG, "处理图片失败: ${e.message}") sendEvent("error", mapOf(
sendEvent("error", mapOf( "code" to "IMAGE_PROCESSING_ERROR",
"code" to "IMAGE_PROCESSING_ERROR", "message" to e.message.toString()
"message" to e.message.toString() ))
))
}
} }
} }
return true return true
} }

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

@ -128,7 +128,7 @@ object BleAgent : BleService.Callback, AgentServiceListener {
} }
override fun onAudioDataReceived(data: ByteArray) { override fun onAudioDataReceived(data: ByteArray) {
FileLogger.d(TAG, "onAudioDataReceived, data: ${data.size}") // FileLogger.d(TAG, "onAudioDataReceived, data: ${data.size}")
AgentService.pushAudioData(data) AgentService.pushAudioData(data)
// 可选:处理音频数据 // 可选:处理音频数据
} }
@ -142,7 +142,7 @@ object BleAgent : BleService.Callback, AgentServiceListener {
// 停止当前TTS,避免冲突 // 停止当前TTS,避免冲突
AgentService.interruptCurrentResponse() AgentService.interruptCurrentResponse()
AgentService.speakText("我在!") // AgentService.speakText("我在!")
// 启动语音识别 // 启动语音识别
if (AgentService.isRecognitionActive) { if (AgentService.isRecognitionActive) {
// 如果已经在识别,则重新启动 // 如果已经在识别,则重新启动

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

@ -328,7 +328,7 @@ class BleMediaService : Service(), CoroutineScope {
private fun checkBleConnection() { private fun checkBleConnection() {
FileLogger.i(TAG, "hcheckBleConnection收到连接请求") FileLogger.i(TAG, "checkBleConnection收到连接请求")
try { try {
// 示例:使用协程异步处理连接 // 示例:使用协程异步处理连接

49
local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt

@ -82,8 +82,6 @@ class AzureAsrHelper(private val context: Context) {
audioSourceType: AudioSourceType = AudioSourceType.MICROPHONE audioSourceType: AudioSourceType = AudioSourceType.MICROPHONE
): Boolean { ): Boolean {
try { try {
FileLogger.d(tag, "初始化 Azure 语音服务, 音频源类型: $audioSourceType")
// 检查配置是否为空 // 检查配置是否为空
if (subscriptionKey.isEmpty() || region.isEmpty()) { if (subscriptionKey.isEmpty() || region.isEmpty()) {
FileLogger.e(tag, "Azure 配置信息不完整") FileLogger.e(tag, "Azure 配置信息不完整")
@ -155,7 +153,6 @@ class AzureAsrHelper(private val context: Context) {
val method = AudioManager::class.java.getMethod("isBluetoothA2dpOn") val method = AudioManager::class.java.getMethod("isBluetoothA2dpOn")
method.invoke(audioManager) as Boolean method.invoke(audioManager) as Boolean
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.d(tag, "无法检测蓝牙连接状态,假设未连接")
false false
} }
@ -165,18 +162,15 @@ class AzureAsrHelper(private val context: Context) {
val needEchoCancellation = !isHeadsetConnected val needEchoCancellation = !isHeadsetConnected
if (needEchoCancellation) { if (needEchoCancellation) {
FileLogger.d(tag, "检测到扬声器模式(扬声器: $isSpeakerphoneOn, 耳机: $isHeadsetConnected), 启用回音消除")
// 使用拉流方式进行回音消除 // 使用拉流方式进行回音消除
setupMicrophoneStream() setupMicrophoneStream()
} else { } else {
FileLogger.d(tag, "使用默认麦克风输入(扬声器: $isSpeakerphoneOn, 耳机: $isHeadsetConnected)")
audioConfig = AudioConfig.fromDefaultMicrophoneInput() audioConfig = AudioConfig.fromDefaultMicrophoneInput()
} }
} }
AudioSourceType.EXTERNAL -> { AudioSourceType.EXTERNAL -> {
// 改用拉流方式处理外部音频 // 改用拉流方式处理外部音频
setupExternalAudioStream() setupExternalAudioStream()
FileLogger.d(tag, "使用外部音频源(拉流模式)")
} }
} }
@ -188,7 +182,6 @@ class AzureAsrHelper(private val context: Context) {
SpeechRecognizer(speechConfig, audioConfig) SpeechRecognizer(speechConfig, audioConfig)
} }
FileLogger.d(tag, "Azure 语音服务初始化成功")
return true return true
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(tag, "创建识别器失败: ${e.message}") FileLogger.e(tag, "创建识别器失败: ${e.message}")
@ -207,8 +200,6 @@ class AzureAsrHelper(private val context: Context) {
// 创建音频配置 - 正确使用fromStreamInput方法,只传入回调 // 创建音频配置 - 正确使用fromStreamInput方法,只传入回调
audioConfig = AudioConfig.fromStreamInput(microphoneStream) audioConfig = AudioConfig.fromStreamInput(microphoneStream)
FileLogger.d(tag, "已设置麦克风流(拉流模式)")
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(tag, "设置麦克风流失败: ${e.message}") FileLogger.e(tag, "设置麦克风流失败: ${e.message}")
e.printStackTrace() e.printStackTrace()
@ -225,8 +216,6 @@ class AzureAsrHelper(private val context: Context) {
// 创建音频配置 // 创建音频配置
audioConfig = AudioConfig.fromStreamInput(externalAudioStream) audioConfig = AudioConfig.fromStreamInput(externalAudioStream)
FileLogger.d(tag, "已设置外部音频流(拉流模式)")
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(tag, "设置外部音频流失败: ${e.message}") FileLogger.e(tag, "设置外部音频流失败: ${e.message}")
e.printStackTrace() e.printStackTrace()
@ -241,7 +230,6 @@ class AzureAsrHelper(private val context: Context) {
*/ */
fun pushAudioData(data: ByteArray) { fun pushAudioData(data: ByteArray) {
if (audioSourceType != AudioSourceType.EXTERNAL) { if (audioSourceType != AudioSourceType.EXTERNAL) {
FileLogger.w(tag, "当前未使用外部音频源,忽略推送的音频数据")
return return
} }
@ -343,8 +331,6 @@ class AzureAsrHelper(private val context: Context) {
recognizer?.recognizing?.addEventListener( recognizer?.recognizing?.addEventListener(
EventHandler<SpeechRecognitionEventArgs> { _, event -> EventHandler<SpeechRecognitionEventArgs> { _, event ->
val detectedLanguage = AutoDetectSourceLanguageResult.fromResult(event.result)?.language ?: "" val detectedLanguage = AutoDetectSourceLanguageResult.fromResult(event.result)?.language ?: ""
FileLogger.d(tag, "识别中: ${event.result.text}, 语言: $detectedLanguage")
// 直接在当前线程调用回调 // 直接在当前线程调用回调
callback.onRecognizing(event.result.text, detectedLanguage) callback.onRecognizing(event.result.text, detectedLanguage)
} }
@ -355,8 +341,6 @@ class AzureAsrHelper(private val context: Context) {
EventHandler<SpeechRecognitionEventArgs> { _, event -> EventHandler<SpeechRecognitionEventArgs> { _, event ->
if (event.result.reason == ResultReason.RecognizedSpeech) { if (event.result.reason == ResultReason.RecognizedSpeech) {
val detectedLanguage = AutoDetectSourceLanguageResult.fromResult(event.result)?.language ?: "" val detectedLanguage = AutoDetectSourceLanguageResult.fromResult(event.result)?.language ?: ""
FileLogger.d(tag, "识别完成: ${event.result.text}, 语言: $detectedLanguage")
// 直接在当前线程调用回调 // 直接在当前线程调用回调
callback.onResult(event.result.text, detectedLanguage) callback.onResult(event.result.text, detectedLanguage)
} }
@ -366,8 +350,6 @@ class AzureAsrHelper(private val context: Context) {
// 会话开始事件 // 会话开始事件
recognizer?.sessionStarted?.addEventListener( recognizer?.sessionStarted?.addEventListener(
EventHandler<SessionEventArgs> { _, _ -> EventHandler<SessionEventArgs> { _, _ ->
FileLogger.d(tag, "识别会话已开始")
// 直接在当前线程调用回调 // 直接在当前线程调用回调
callback.onSessionStarted() callback.onSessionStarted()
} }
@ -376,13 +358,10 @@ class AzureAsrHelper(private val context: Context) {
// 会话结束事件 // 会话结束事件
recognizer?.sessionStopped?.addEventListener( recognizer?.sessionStopped?.addEventListener(
EventHandler<SessionEventArgs> { _, _ -> EventHandler<SessionEventArgs> { _, _ ->
FileLogger.d(tag, "识别会话已结束")
// 直接在当前线程调用回调 // 直接在当前线程调用回调
callback.onSessionStopped() callback.onSessionStopped()
isContinuousRecognitionActive = false isContinuousRecognitionActive = false
stopAudioProcessing() stopAudioProcessing()
} }
) )
@ -419,13 +398,10 @@ class AzureAsrHelper(private val context: Context) {
} }
if (!isContinuousRecognitionActive) { if (!isContinuousRecognitionActive) {
FileLogger.d(tag, "未进行连续识别,忽略停止请求")
return true return true
} }
try { try {
FileLogger.d(tag, "停止连续语音识别")
if (recognizer == null) { if (recognizer == null) {
FileLogger.w(tag, "识别器为空,重置状态") FileLogger.w(tag, "识别器为空,重置状态")
isContinuousRecognitionActive = false isContinuousRecognitionActive = false
@ -442,7 +418,6 @@ class AzureAsrHelper(private val context: Context) {
stopAudioProcessing() stopAudioProcessing()
// 会话结束事件会设置isContinuousRecognitionActive = false // 会话结束事件会设置isContinuousRecognitionActive = false
FileLogger.d(tag, "连续识别停止指令已发送")
return true return true
} catch (e: Exception) { } catch (e: Exception) {
// 强制重置状态 // 强制重置状态
@ -503,11 +478,7 @@ class AzureAsrHelper(private val context: Context) {
audioConfig = null audioConfig = null
recognizer = null recognizer = null
speechConfig = null speechConfig = null
FileLogger.d(tag, "资源已释放")
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(tag, "释放资源失败: ${e.message}")
// 确保状态被重置 // 确保状态被重置
isContinuousRecognitionActive = false isContinuousRecognitionActive = false
microphoneStream = null microphoneStream = null
@ -585,8 +556,6 @@ class AzureAsrHelper(private val context: Context) {
*/ */
private fun initMicrophone() { private fun initMicrophone() {
try { try {
FileLogger.d(tag, "初始化麦克风 (拉流模式)")
// 创建录音对象 // 创建录音对象
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
val format = AudioFormat.Builder() val format = AudioFormat.Builder()
@ -625,12 +594,8 @@ class AzureAsrHelper(private val context: Context) {
if (audioRecord?.recordingState != AudioRecord.RECORDSTATE_RECORDING) { if (audioRecord?.recordingState != AudioRecord.RECORDSTATE_RECORDING) {
throw IllegalStateException("录音启动失败") throw IllegalStateException("录音启动失败")
} }
FileLogger.d(tag, "麦克风初始化完成 (拉流模式)")
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(tag, "初始化麦克风失败: ${e.message}")
e.printStackTrace() e.printStackTrace()
releaseAudioResources()
} }
} }
@ -641,14 +606,9 @@ class AzureAsrHelper(private val context: Context) {
val sessionId = audioRecord?.audioSessionId ?: return val sessionId = audioRecord?.audioSessionId ?: return
// 回音消除 // 回音消除
if (AcousticEchoCanceler.isAvailable()) { echoCanceler = AcousticEchoCanceler.create(sessionId).apply {
echoCanceler = AcousticEchoCanceler.create(sessionId).apply { enabled = true
enabled = true
}
FileLogger.d(tag, "回音消除已启用")
} }
// 其他音频效果可以按需添加
} }
/** /**
@ -697,10 +657,8 @@ class AzureAsrHelper(private val context: Context) {
// 释放录音实例 // 释放录音实例
audioRecord?.release() audioRecord?.release()
audioRecord = null audioRecord = null
FileLogger.d(tag, "音频资源已释放 (拉流模式)")
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(tag, "释放音频资源失败: ${e.message}") e.printStackTrace()
} }
} }
} }
@ -750,7 +708,6 @@ class AzureAsrHelper(private val context: Context) {
override fun close() { override fun close() {
closed = true closed = true
queue.clear() queue.clear()
FileLogger.d(tag, "外部音频流已关闭")
} }
} }

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

@ -408,7 +408,7 @@ class AzureSpeechPlugin: FlutterPlugin,BleService.Callback, CoroutineScope {
} }
override fun onAudioDataReceived(data: ByteArray) { override fun onAudioDataReceived(data: ByteArray) {
FileLogger.d(tag, "AzureSpeechPlugin onAudioDataReceived, data: ${data.size}") // FileLogger.d(tag, "AzureSpeechPlugin onAudioDataReceived, data: ${data.size}")
azureAsrHelper.pushAudioData(data) azureAsrHelper.pushAudioData(data)
} }

47
local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureTtsHelper.kt

@ -254,17 +254,42 @@ class AzureTtsHelper(private val context: Context) : CoroutineScope {
/** /**
* 生成SSML * 生成SSML
*/ */
private fun generateSsml(text: String): String { private fun generateSsml(rawText: String): String {
return """ // 1. 定义要静音的符号和表情符号列表
<speak version="1.0" xmlns="http://www.w3.org/2001/10/synthesis" xmlns:mstts="https://www.w3.org/2001/mstts" xml:lang="zh-CN"> val symbolsToMute = listOf(
<voice name="$currentVoice"> "#", "*", "@", "%", "^", "&",
<prosody rate="$currentRate" pitch="$currentPitch" volume="$currentVolume"> "😀", "😂", "😊", "😍", "😢", "😎", "😉", "👍", "🙌", "🎉"
$text )
</prosody>
</voice> // 2. 转义 XML 保留字符
</speak> val escapedText = rawText
""".trimIndent() .replace("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;")
// 3. 静音处理特殊符号和表情符号
// 使用空白替换法,直接将符号替换为空格
var processedText = escapedText
symbolsToMute.forEach { sym ->
processedText = processedText.replace(sym, "")
} }
// 4. 构造简化的SSML文档,减少嵌套层级
return """
<speak version="1.0"
xmlns="http://www.w3.org/2001/10/synthesis"
xmlns:mstts="https://www.w3.org/2001/mstts"
xml:lang="zh-CN">
<voice name="$currentVoice">
<mstts:express-as style="cheerful">
<prosody rate="$currentRate" pitch="$currentPitch" volume="$currentVolume">
<say-as interpret-as="text">$processedText</say-as>
</prosody>
</mstts:express-as>
</voice>
</speak>
""".trimIndent()
}
/** /**
* 合成文本为语音 * 合成文本为语音
@ -311,7 +336,7 @@ class AzureTtsHelper(private val context: Context) : CoroutineScope {
streamBuffer.append(text) streamBuffer.append(text)
// 增加500ms防抖逻辑 // 增加500ms防抖逻辑
val currentTime = System.currentTimeMillis() val currentTime = System.currentTimeMillis()
if (currentTime - lastSpeakTime < 500) { if (currentTime - lastSpeakTime < 600) {
return true return true
} }
lastSpeakTime = currentTime lastSpeakTime = currentTime

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

@ -327,8 +327,6 @@ class BleCompanionManager(private val context: Context) {
// 清除临时MAC地址 // 清除临时MAC地址
pendingMacAddress = null pendingMacAddress = null
// 关联成功后,自动注册后台扫描
registerBackgroundScan()
return true return true
} else { } else {
@ -368,7 +366,7 @@ class BleCompanionManager(private val context: Context) {
// 构建过滤器 // 构建过滤器
val filter = ScanFilter.Builder() val filter = ScanFilter.Builder()
.setDeviceAddress(mac) .setDeviceAddress(mac)
.setManufacturerData(0xEFAB, byteArrayOf(0xA0.toByte(), 0x01)) // .setManufacturerData(0xEFAB, byteArrayOf(0xA0.toByte(), 0x01))
.build() .build()
// 扫描设置 // 扫描设置

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

@ -123,13 +123,13 @@ object BleService {
// 初始化OpusManager // 初始化OpusManager
try { try {
opusManager = OpusManager() opusManager = OpusManager()
startOpusStreamDecoding()
FileLogger.d(TAG, "OpusManager初始化成功") FileLogger.d(TAG, "OpusManager初始化成功")
} catch (e: OpusException) { } catch (e: OpusException) {
FileLogger.e(TAG, "OpusManager初始化失败: ${e.message}", e) FileLogger.e(TAG, "OpusManager初始化失败: ${e.message}", e)
// 根据需要决定是否因为Opus初始化失败而返回false // 根据需要决定是否因为Opus初始化失败而返回false
} }
isInitialized = true isInitialized = true
FileLogger.d(TAG, "BLE服务初始化成功") FileLogger.d(TAG, "BLE服务初始化成功")
return true return true
@ -937,7 +937,7 @@ object BleService {
override fun onDecodeStream(data: ByteArray?) { override fun onDecodeStream(data: ByteArray?) {
if (data != null) { if (data != null) {
// FileLogger.d(TAG, "Opus解码数据: ${data.size} bytes") // FileLogger.d(TAG, "Opus解码数据: ${data.size} bytes")
mainHandler.post { notifyAudioDataReceived(data) } notifyAudioDataReceived(data)
} }
} }
@ -1014,7 +1014,6 @@ object BleService {
fun closeCodec(): Boolean { fun closeCodec(): Boolean {
FileLogger.i(TAG, "关闭编解码...") FileLogger.i(TAG, "关闭编解码...")
stopOpusStreamDecoding() // 停止Opus解码流
return sendCommand( return sendCommand(
BleConst.CMD_CONTROL_CODEC.toByte(), BleConst.CMD_CONTROL_CODEC.toByte(),
byteArrayOf( byteArrayOf(
@ -1031,15 +1030,15 @@ object BleService {
fun openDecoder(): Boolean { fun openDecoder(): Boolean {
FileLogger.i(TAG, "打开编码 (设备侧)...") FileLogger.i(TAG, "打开编码 (设备侧)...")
val opusStarted = startOpusStreamDecoding( // val opusStarted = startOpusStreamDecoding(
hasHeader = false, // BLE传输的Opus数据通常无文件头 // hasHeader = false, // BLE传输的Opus数据通常无文件头
channel = 1, // 单声道 // channel = 1, // 单声道
sampleRate = 16000,// Opus推荐16kHz或更高 // sampleRate = 16000,// Opus推荐16kHz或更高
packetSize = 40 // Opus常用帧长,如20ms, 40ms, 60ms。 (16000Hz * 1channel * 2bytes/sample * 0.04s = 1280 bytes/frame if PCM) // packetSize = 40 // Opus常用帧长,如20ms, 40ms, 60ms。 (16000Hz * 1channel * 2bytes/sample * 0.04s = 1280 bytes/frame if PCM)
// packetSize for Opus is typically in ms, e.g. 20, 40, 60. // // packetSize for Opus is typically in ms, e.g. 20, 40, 60.
// The library might expect this in a different unit or derive it. // // The library might expect this in a different unit or derive it.
// Given JlOpusPlugin.kt, packetSize refers to Opus encoded frame duration in ms. // // Given JlOpusPlugin.kt, packetSize refers to Opus encoded frame duration in ms.
) // )
FileLogger.i(TAG, "打开解码...") FileLogger.i(TAG, "打开解码...")
@ -1058,18 +1057,7 @@ object BleService {
fun openA2DPDecoder(): Boolean { fun openA2DPDecoder(): Boolean {
FileLogger.i(TAG, "打开编码 (设备侧)...") FileLogger.i(TAG, "打开编码 (设备侧)...")
val opusStarted = startOpusStreamDecoding( // FileLogger.i(TAG, "打开解码...")
hasHeader = false, // BLE传输的Opus数据通常无文件头
channel = 1, // 单声道
sampleRate = 16000,// Opus推荐16kHz或更高
packetSize = 40 // Opus常用帧长,如20ms, 40ms, 60ms。 (16000Hz * 1channel * 2bytes/sample * 0.04s = 1280 bytes/frame if PCM)
// packetSize for Opus is typically in ms, e.g. 20, 40, 60.
// The library might expect this in a different unit or derive it.
// Given JlOpusPlugin.kt, packetSize refers to Opus encoded frame duration in ms.
)
FileLogger.i(TAG, "打开解码...")
return sendCommand( return sendCommand(
BleConst.CMD_CONTROL_CODEC.toByte(), byteArrayOf( BleConst.CMD_CONTROL_CODEC.toByte(), byteArrayOf(
BleConst.CODEC_CONTROL_A2DP_PLAY.toByte(), BleConst.CODEC_CONTROL_A2DP_PLAY.toByte(),
@ -1093,20 +1081,7 @@ object BleService {
fun openEncoder(): Boolean { fun openEncoder(): Boolean {
FileLogger.i(TAG, "打开编码 (设备侧)...") FileLogger.i(TAG, "打开编码 (设备侧)...")
val opusStarted = startOpusStreamDecoding(
hasHeader = false, // BLE传输的Opus数据通常无文件头
channel = 1, // 单声道
sampleRate = 16000,// Opus推荐16kHz或更高
packetSize = 40 // Opus常用帧长,如20ms, 40ms, 60ms。 (16000Hz * 1channel * 2bytes/sample * 0.04s = 1280 bytes/frame if PCM)
// packetSize for Opus is typically in ms, e.g. 20, 40, 60.
// The library might expect this in a different unit or derive it.
// Given JlOpusPlugin.kt, packetSize refers to Opus encoded frame duration in ms.
)
// 注意:此方法通常是控制设备端开始编码并发送数据。
// App侧通常是打开解码器(openDecoder)来接收和处理这些数据。
// 如果App也需要编码(例如发送语音到设备),则需要实现Opus编码逻辑。
// 目前仅发送命令通知设备开始编码。 // 目前仅发送命令通知设备开始编码。
return sendCommand( return sendCommand(
BleConst.CMD_CONTROL_CODEC.toByte(), BleConst.CMD_CONTROL_CODEC.toByte(),

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

@ -132,6 +132,10 @@ class BleServicePlugin : FlutterPlugin, MethodCallHandler, ActivityAware,
pendingAssociationResult?.success(true) pendingAssociationResult?.success(true)
pendingAssociationResult = null pendingAssociationResult = null
return true return true
} else {
pendingAssociationResult?.success(false)
pendingAssociationResult = null
return true
} }
} }
return false return false

31
local_plugins/music_service/android/src/main/AndroidManifest.xml

@ -1,24 +1,29 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.yunqiinnovation.music_service"> package="com.yunqiinnovation.music_service">
<!-- 基础前台服务权限 -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<!-- Android 14 及以上需要声明具体的前台服务类型权限 -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
<application> <application>
<service <service
android:name=".MusicService" android:name=".MusicService"
android:exported="false" android:exported="true">
android:enabled="true" <!-- 让系统知道这是媒体浏览服务 -->
android:foregroundServiceType="mediaPlayback" /> <intent-filter>
<!-- 用于接收媒体按钮(耳机按钮、蓝牙播放控制) --> <action android:name="android.media.browse.MediaBrowserService" />
<receiver </intent-filter>
android:name="androidx.media.session.MediaButtonReceiver" <!-- 让系统知道这里能处理媒体按键 -->
android:exported="false"> <intent-filter>
<intent-filter> <action android:name="android.intent.action.MEDIA_BUTTON" />
<action android:name="android.intent.action.MEDIA_BUTTON" /> </intent-filter>
</intent-filter> </service>
</receiver>
<receiver
android:name="androidx.media.session.MediaButtonReceiver"
android:exported="false">
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter>
</receiver>
</application> </application>
</manifest> </manifest>

363
local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt

@ -1,20 +1,25 @@
package com.yunqiinnovation.music_service package com.yunqiinnovation.music_service
import android.app.* import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.media.AudioManager import android.media.AudioManager
import android.media.MediaPlayer import android.media.MediaPlayer
import android.os.Build import android.os.Build
import android.os.Bundle
import android.os.IBinder import android.os.IBinder
import android.util.Log import android.support.v4.media.MediaBrowserCompat
import androidx.core.app.NotificationCompat
import android.support.v4.media.session.MediaSessionCompat import android.support.v4.media.session.MediaSessionCompat
import android.support.v4.media.session.PlaybackStateCompat import android.support.v4.media.session.PlaybackStateCompat
import androidx.core.app.NotificationCompat
import androidx.media.MediaBrowserServiceCompat
import androidx.media.session.MediaButtonReceiver
import com.yunqiinnovation.deepvoice_utils.FileLogger import com.yunqiinnovation.deepvoice_utils.FileLogger
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
class MusicService : Service() { class MusicService : MediaBrowserServiceCompat() {
companion object { companion object {
private const val TAG = "MusicService" private const val TAG = "MusicService"
@ -22,242 +27,240 @@ class MusicService : Service() {
private const val NOTIFICATION_ID = 1002 private const val NOTIFICATION_ID = 1002
private val isRunning = AtomicBoolean(false) private val isRunning = AtomicBoolean(false)
private var currentStatus: Boolean = false
private var currentUrlInternal: String? = null private var currentUrlInternal: String? = null
private var currentStatus = false
private var playlist = mutableListOf<String>()
private var currentIndex = 0
fun isServiceRunning() = isRunning.get()
fun isServiceRunning(): Boolean = isRunning.get() fun isPlaying() = currentStatus
fun isPlaying(): Boolean = currentStatus
fun getCurrentUrl(): String? = currentUrlInternal fun getCurrentUrl(): String? = currentUrlInternal
} }
private lateinit var mediaSession: MediaSessionCompat private lateinit var mediaSession: MediaSessionCompat
private lateinit var audioManager: AudioManager
private var mediaPlayer: MediaPlayer? = null private var mediaPlayer: MediaPlayer? = null
private val playlist = mutableListOf<String>() // 可选:初始化为预置歌曲
private var currentIndex = 0
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
FileLogger.init(this) FileLogger.init(this)
FileLogger.i(TAG, "MusicService 正在创建...")
isRunning.set(true) isRunning.set(true)
mediaSession = MediaSessionCompat(this, TAG).apply { setupMediaSession()
setCallback(object : MediaSessionCompat.Callback() { startForeground(NOTIFICATION_ID, createNotification("等待播放…"))
override fun onPlay() { audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
resume() FileLogger.i(TAG, "MediaService 启动完成")
}
override fun onPause() {
pause()
}
override fun onSkipToNext() {
if (currentIndex + 1 < playlist.size) {
currentIndex++
play(playlist[currentIndex])
}
}
override fun onSkipToPrevious() {
if (currentIndex - 1 >= 0) {
currentIndex--
play(playlist[currentIndex])
}
}
override fun onStop() {
stopSelf()
}
})
isActive = true
}
startForeground(NOTIFICATION_ID, createNotification("等待播放..."))
Log.i(TAG, "MusicService Created")
} }
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val command = intent?.getStringExtra("command") ?: return START_NOT_STICKY intent?.action?.takeIf { it == Intent.ACTION_MEDIA_BUTTON }?.let {
Log.i(TAG, "onStartCommand: $command" ) // 分发给 MediaSession 处理媒体按钮
MediaButtonReceiver.handleIntent(mediaSession, intent)
return START_STICKY
}
val command = intent?.getStringExtra("command") ?: return START_NOT_STICKY
FileLogger.i(TAG, "收到命令: $command")
when (command) { when (command) {
"play" -> { "play" -> intent.getStringExtra("url")?.let { url ->
val url = intent.getStringExtra("url") ?: return START_NOT_STICKY
if (!playlist.contains(url)) playlist.add(url) if (!playlist.contains(url)) playlist.add(url)
currentIndex = playlist.indexOf(url) currentIndex = playlist.indexOf(url)
play(url) play(url)
} }
"playlist" -> {
val urls = intent.getStringArrayListExtra("urls")?.filter { it.isNotEmpty() } ?: return START_NOT_STICKY
Log.i(TAG, "后台服务播放 playlist: $urls" )
if (urls.isNotEmpty()) {
playlist.clear()
playlist.addAll(urls)
currentIndex = 0
play(playlist[currentIndex])
} else {
Log.w(TAG, "playlist 命令收到空列表")
}
}
"pause" -> pause() "pause" -> pause()
"resume" -> resume() "resume" -> resume()
"stop" -> stopSelf() "stop" -> stopSelf()
"next" -> { "next" -> if (currentIndex + 1 < playlist.size) play(playlist[++currentIndex])
if (currentIndex + 1 < playlist.size) { "previous" -> if (currentIndex - 1 >= 0) play(playlist[--currentIndex])
currentIndex++
play(playlist[currentIndex])
}
}
"previous" -> {
if (currentIndex - 1 >= 0) {
currentIndex--
play(playlist[currentIndex])
}
}
"setVolume" -> {
val volume = intent.getFloatExtra("volume", 1.0f).coerceIn(0f, 1f)
mediaPlayer?.setVolume(volume, volume)
}
} }
return START_STICKY return START_STICKY
} }
private fun setupMediaSession() {
mediaSession = MediaSessionCompat(this, "MusicMediaSession").apply {
setFlags(
MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS or
MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS
)
// 回调中处理具体操作
setCallback(object : MediaSessionCompat.Callback() {
override fun onPlay() { FileLogger.i(TAG, "onPlay 通知!"); resume()}
override fun onPause() { FileLogger.i(TAG, "onPause 通知!"); pause()}
override fun onSkipToNext(){ FileLogger.i(TAG, "onSkipToNext 通知!"); playNext()}
override fun onSkipToPrevious() { FileLogger.i(TAG, "onSkipToNext 通知!"); playPrevious()}
})
// 初始播放状态
setPlaybackState(
PlaybackStateCompat.Builder()
.setActions(
PlaybackStateCompat.ACTION_PLAY or
PlaybackStateCompat.ACTION_PAUSE or
PlaybackStateCompat.ACTION_SKIP_TO_NEXT or
PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS
)
.setState(PlaybackStateCompat.STATE_STOPPED, 0, 1.0f)
.build()
)
isActive = true
}
// 将 token 暴露给 MediaBrowserServiceCompat
setSessionToken(mediaSession.sessionToken)
}
private fun createNotification(contentText: String): Notification {
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
manager.createNotificationChannel(
NotificationChannel(CHANNEL_ID, "播放控制", NotificationManager.IMPORTANCE_LOW)
)
}
val prevIntent = MediaButtonReceiver.buildMediaButtonPendingIntent(
this, PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS
)
val playPauseIntent = MediaButtonReceiver.buildMediaButtonPendingIntent(
this, if (currentStatus) PlaybackStateCompat.ACTION_PAUSE else PlaybackStateCompat.ACTION_PLAY
)
val nextIntent = MediaButtonReceiver.buildMediaButtonPendingIntent(
this, PlaybackStateCompat.ACTION_SKIP_TO_NEXT
)
return NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("DeepSound")
.setContentText(contentText)
.setSmallIcon(android.R.drawable.ic_media_play)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setStyle(
androidx.media.app.NotificationCompat.MediaStyle()
.setMediaSession(mediaSession.sessionToken)
.setShowActionsInCompactView(0, 1, 2)
)
.addAction(android.R.drawable.ic_media_previous, "上一首", prevIntent)
.addAction(
if (currentStatus) android.R.drawable.ic_media_pause else android.R.drawable.ic_media_play,
if (currentStatus) "暂停" else "播放",
playPauseIntent
)
.addAction(android.R.drawable.ic_media_next, "下一首", nextIntent)
.setOngoing(currentStatus)
.build()
}
/**
* 设置播放状态
*/
private fun setPlaybackState(state: Int) {
val playbackStateBuilder = PlaybackStateCompat.Builder()
.setActions(
PlaybackStateCompat.ACTION_PLAY or
PlaybackStateCompat.ACTION_PAUSE or
PlaybackStateCompat.ACTION_PLAY_PAUSE or
PlaybackStateCompat.ACTION_SKIP_TO_NEXT or
PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS or
PlaybackStateCompat.ACTION_STOP or
PlaybackStateCompat.ACTION_SEEK_TO or
PlaybackStateCompat.ACTION_FAST_FORWARD or
PlaybackStateCompat.ACTION_REWIND
)
// 使用更高的播放速度,让系统认为是真正的播放
.setState(state, 0, 1.0f, System.currentTimeMillis())
mediaSession.setPlaybackState(playbackStateBuilder.build())
// 确保MediaSession激活
if (!mediaSession.isActive) {
mediaSession.isActive = true
}
}
private fun play(url: String) { private fun play(url: String) {
Log.i(TAG, "播放音乐: $url") mediaPlayer?.release()
releaseMediaPlayer() currentStatus = true
currentStatus = true; currentUrlInternal = url
currentUrlInternal = url;
mediaPlayer = MediaPlayer().apply { mediaPlayer = MediaPlayer().apply {
setDataSource(url) setDataSource(url)
setAudioStreamType(AudioManager.STREAM_MUSIC) setAudioStreamType(AudioManager.STREAM_MUSIC)
setOnPreparedListener { setOnPreparedListener {
start() start()
currentStatus = true
updateNotification("正在播放") updateNotification("正在播放")
Log.i(TAG, "开始播放") FileLogger.i(TAG, "开始播放: $url")
}
setOnCompletionListener {
Log.i(TAG, "播放完成")
// 播放下一首
if (currentIndex + 1 < playlist.size) {
currentIndex++
play(playlist[currentIndex]) // 播放下一首
} else {
currentStatus = false
stopSelf() // 如果没有更多歌曲了,可以停止服务
}
} }
setOnCompletionListener { playNextOrStop() }
setOnErrorListener { _, what, extra -> setOnErrorListener { _, what, extra ->
Log.e(TAG, "播放错误: what=$what extra=$extra") FileLogger.e(TAG, "播放错误 what=$what extra=$extra")
currentStatus = false
stopSelf()
true true
} }
prepareAsync() prepareAsync()
} }
setPlaybackState(PlaybackStateCompat.STATE_PAUSED);
} }
private fun pause() { private fun pause() {
mediaPlayer?.let { mediaPlayer?.takeIf { it.isPlaying }?.apply {
if (it.isPlaying) { pause()
it.pause() currentStatus = false
currentStatus= false mediaSession.setPlaybackState(
updateNotification("已暂停") PlaybackStateCompat.Builder()
Log.i(TAG, "暂停播放") .setState(PlaybackStateCompat.STATE_PAUSED, currentPosition.toLong(), 1.0f)
} .build()
)
updateNotification("已暂停")
setPlaybackState(PlaybackStateCompat.STATE_PLAYING);
FileLogger.i(TAG, "已暂停")
} }
} }
private fun resume() { private fun resume() {
mediaPlayer?.let { mediaPlayer?.takeIf { !it.isPlaying }?.apply {
if (!it.isPlaying) { start()
it.start() currentStatus = true
currentStatus = true mediaSession.setPlaybackState(
updateNotification("继续播放") PlaybackStateCompat.Builder()
Log.i(TAG, "继续播放") .setState(PlaybackStateCompat.STATE_PLAYING, currentPosition.toLong(), 1.0f)
} .build()
)
updateNotification("继续播放")
setPlaybackState(PlaybackStateCompat.STATE_PAUSED);
FileLogger.i(TAG, "继续播放")
} }
} }
private fun releaseMediaPlayer() { private fun playNext() {
mediaPlayer?.apply { if (playlist.isEmpty()) return
stop() currentIndex = (currentIndex + 1) % playlist.size
release() play(playlist[currentIndex])
}
mediaPlayer = null
} }
private fun updateNotification(contentText: String) { private fun playPrevious() {
val notification = createNotification(contentText) if (playlist.isEmpty()) return
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager currentIndex = if (currentIndex - 1 < 0) playlist.size - 1 else currentIndex - 1
manager.notify(NOTIFICATION_ID, notification) play(playlist[currentIndex])
}
private fun playNextOrStop() {
if (currentIndex + 1 < playlist.size) play(playlist[++currentIndex])
else stopSelf()
} }
private fun createNotification(contentText: String): Notification { private fun updateNotification(contentText: String) {
val notification = createNotification(contentText)
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { manager.notify(NOTIFICATION_ID, notification)
val channel = NotificationChannel( } else {
CHANNEL_ID, @Suppress("DEPRECATION")
"音乐播放服务", startForeground(NOTIFICATION_ID, notification)
NotificationManager.IMPORTANCE_LOW
)
manager.createNotificationChannel(channel)
} }
}
// 修复 pendingIntent:打开 App 主 Activity override fun onGetRoot(
val activityIntent = packageManager.getLaunchIntentForPackage(packageName) clientPackageName: String, clientUid: Int, rootHints: Bundle?
val pendingIntent = PendingIntent.getActivity( ) = BrowserRoot("root_id", null)
this,
0,
activityIntent,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
return NotificationCompat.Builder(this, CHANNEL_ID) override fun onLoadChildren(
.setContentTitle("DeepSound") parentId: String, result: Result<MutableList<MediaBrowserCompat.MediaItem>>
.setContentText(contentText) ) = result.sendResult(mutableListOf())
.setSmallIcon(android.R.drawable.ic_media_play)
.setContentIntent(pendingIntent)
.setOngoing(true)
.setStyle(
androidx.media.app.NotificationCompat.MediaStyle()
.setMediaSession(mediaSession.sessionToken)
.setShowActionsInCompactView(0, 1, 2)
)
.addAction(
NotificationCompat.Action(
android.R.drawable.ic_media_previous, "上一曲",
androidx.media.session.MediaButtonReceiver.buildMediaButtonPendingIntent(
this,
PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS
)
)
)
.addAction(
NotificationCompat.Action(
if (currentStatus) android.R.drawable.ic_media_pause else android.R.drawable.ic_media_play,
if (currentStatus) "暂停" else "播放",
androidx.media.session.MediaButtonReceiver.buildMediaButtonPendingIntent(
this,
PlaybackStateCompat.ACTION_PLAY_PAUSE
)
)
)
.addAction(
NotificationCompat.Action(
android.R.drawable.ic_media_next, "下一曲",
androidx.media.session.MediaButtonReceiver.buildMediaButtonPendingIntent(
this,
PlaybackStateCompat.ACTION_SKIP_TO_NEXT
)
)
)
.build()
}
override fun onDestroy() { override fun onDestroy() {
Log.i(TAG, "服务销毁,释放播放器") mediaPlayer?.release()
releaseMediaPlayer()
stopForeground(true) stopForeground(true)
isRunning.set(false) isRunning.set(false)
super.onDestroy() super.onDestroy()

135
local_plugins/open_ai/README.md

@ -1,135 +0,0 @@
# OpenAI API 与 MCP 集成插件
这个 Flutter 插件提供了 OpenAI API 的访问能力和 Model Context Protocol (MCP) 工具调用功能的集成。
## 功能特点
- 使用官方 OpenAI Java SDK 进行异步通信
- 集成官方 MCP Kotlin SDK,使用 SSE 模式
- 支持流式输出响应
- 支持工具调用和结果处理
- 支持带图片的多模态对话
## MCP 集成
本插件使用 [MCP 官方 Kotlin SDK](https://github.com/modelcontextprotocol/kotlin-sdk) 实现与 MCP 服务器的通信。通过 SSE(Server-Sent Events)模式连接,能够:
- 获取 MCP 服务器提供的所有工具定义
- 动态调用远程工具并获取结果
- 支持本地工具的注册和调用
- 在 OpenAI API 请求中无缝集成工具调用功能
## 安装
在项目的 `pubspec.yaml` 中添加本地插件依赖:
```yaml
dependencies:
open_ai:
path: local_plugins/open_ai
```
## 使用方法
### 初始化
```dart
import 'package:open_ai/open_ai.dart';
final openAI = OpenAI();
await openAI.initialize(
apiKey: 'your-api-key',
baseUrl: 'https://api.example.com', // 可选,默认为 OpenAI 官方 API
model: 'gpt-3.5-turbo', // 可选,默认为 gpt-3.5-turbo
mcpServer: 'https://mcp.example.com', // MCP 服务器 SSE 端点地址
);
```
### 创建消息
```dart
// 创建系统消息
final systemMessage = await openAI.createSystemMessage('你是一个助手');
// 创建用户消息
final userMessage = await openAI.createUserMessage('你好,请帮我解释一下量子力学');
// 创建助手消息
final assistantMessage = await openAI.createAssistantMessage('我可以帮你解释量子力学');
// 创建带图片的用户消息
final imageBase64 = '...'; // base64编码的图片数据
final userImageMessage = await openAI.createUserMessageWithImage(
'这张图片中的物体是什么?',
imageBase64
);
```
### 发送消息(非流式输出)
```dart
final messages = [systemMessage, userMessage];
final response = await openAI.sendMessage(messages);
print('AI回复: $response');
```
### 发送消息(流式输出)
```dart
final messages = [systemMessage, userMessage];
final callback = StreamCallback(
onToken: (token) {
// 处理单个令牌
print('收到令牌: $token');
},
onComplete: () {
// 处理完成事件
print('响应完成');
},
onError: (error) {
// 处理错误
print('发生错误: $error');
},
onFunctionCall: (functionCall) {
// 处理函数调用
print('函数调用: $functionCall');
},
onFunctionCallResult: (functionCall, result) {
// 处理函数调用结果
print('函数调用结果: $result');
},
);
final streamId = await openAI.sendMessageStream(messages, callback);
```
### 取消当前流式请求
```dart
final success = await openAI.cancelCurrentStream();
```
### 释放资源
```dart
await openAI.dispose();
```
## 异常处理
该插件会在操作失败时抛出异常,请使用 try-catch 块捕获它们:
```dart
try {
final response = await openAI.sendMessage(messages);
} catch (e) {
print('发生错误: $e');
}
```
## 注意事项
- 初始化插件时必须提供有效的 API 密钥
- 使用流式响应时,请确保在完成后调用 `dispose()` 方法释放资源
- MCP 功能需要有效的 MCP 服务器 SSE 端点地址才能工作

71
local_plugins/open_ai/android/build.gradle.kts

@ -1,71 +0,0 @@
group = "com.yunqiinnovation.open_ai"
version = "1.0"
buildscript {
val kotlinVersion by extra("1.8.0")
repositories {
google()
mavenCentral()
}
dependencies {
classpath("com.android.tools.build:gradle:7.3.0")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion")
}
}
allprojects {
repositories {
google()
mavenCentral()
}
}
plugins {
id("com.android.library")
id("kotlin-android")
}
android {
compileSdk = 33
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
defaultConfig {
minSdk = 21
}
namespace = "com.yunqiinnovation.open_ai"
}
dependencies {
val kotlinVersion: String by project
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion")
implementation("androidx.annotation:annotation:1.6.0")
implementation("androidx.appcompat:appcompat:1.6.1")
// Kotlin协程
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1")
// OkHttp 依赖项
implementation("com.squareup.okhttp3:okhttp:4.11.0")
implementation("com.squareup.okhttp3:okhttp-sse:4.11.0")
// Jackson JSON解析器
implementation("com.fasterxml.jackson.core:jackson-databind:2.14.2")
// OpenAI Java库依赖
implementation("com.aallam.openai:openai-client:3.6.0")
implementation("io.ktor:ktor-client-okhttp:2.3.3")
// MCP官方Kotlin SDK依赖
implementation("io.modelcontextprotocol:kotlin-sdk:0.5.0")
}

258
local_plugins/open_ai/android/src/main/kotlin/com/yunqiinnovation/open_ai/MCPClient.kt

@ -1,258 +0,0 @@
package com.yunqiinnovation.open_ai
import android.content.Context
import android.util.Log
import com.fasterxml.jackson.databind.ObjectMapper
import io.modelcontextprotocol.kotlin.sdk.Implementation
import io.modelcontextprotocol.kotlin.sdk.client.Client
import io.modelcontextprotocol.kotlin.sdk.client.SseClientTransport
import io.modelcontextprotocol.kotlin.sdk.tools.ToolCallRequest
import io.modelcontextprotocol.kotlin.sdk.tools.ToolDefinition
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import okhttp3.*
import okhttp3.sse.EventSource
import okhttp3.sse.EventSourceListener
import okhttp3.sse.EventSources
import org.json.JSONArray
import org.json.JSONObject
import java.io.IOException
import java.util.concurrent.TimeUnit
import kotlin.coroutines.CoroutineContext
/**
* MCP功能处理接口
*/
interface FunctionHandler {
suspend fun handle(arguments: Map<String, Any>): String
}
/**
* Model Context Protocol (MCP)客户端实现类
* 使用官方 MCP Kotlin SDK
*/
class MCPClient(private val context: Context? = null) : CoroutineScope {
private val TAG = "MCPClient"
// 协程相关
private val job = SupervisorJob()
override val coroutineContext: CoroutineContext
get() = Dispatchers.IO + job
// OkHttp客户端
private val httpClient = OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.build()
// MCP客户端
private var client: Client? = null
// 已注册的工具
private val tools = mutableListOf<String>()
// 本地函数处理器
private val functionHandlers = mutableMapOf<String, FunctionHandler>()
// Json解析器
private val objectMapper = ObjectMapper()
// 连接状态
private var isConnected = false
/**
* 使用SSE方式连接到MCP服务器
*/
suspend fun connectToSSE(serverUrl: String): Boolean {
if (serverUrl.isEmpty()) {
Log.e(TAG, "服务器URL为空")
return false
}
try {
// 创建MCP客户端
client = Client(
clientInfo = Implementation(
name = "deepvoice-mcp-client",
version = "1.0.0"
)
)
// 创建SSE传输
val transport = SseClientTransport(
serverUrl = serverUrl,
httpClient = httpClient
)
// 连接到服务器
client?.connect(transport)
isConnected = true
// 获取工具定义
fetchTools()
return true
} catch (e: Exception) {
Log.e(TAG, "连接到MCP服务器失败: ${e.message}", e)
isConnected = false
return false
}
}
/**
* 获取工具定义
*/
private suspend fun fetchTools() {
try {
val availableTools = client?.listTools() ?: emptyList()
tools.clear()
availableTools.forEach { toolDefinition ->
val toolJson = toolDefinitionToJson(toolDefinition)
tools.add(toolJson.toString())
Log.d(TAG, "已获取工具: ${toolDefinition.name}")
}
} catch (e: Exception) {
Log.e(TAG, "获取工具定义失败: ${e.message}", e)
}
}
/**
* 将工具定义转换为JSON
*/
private fun toolDefinitionToJson(toolDefinition: ToolDefinition): JSONObject {
val functionObject = JSONObject().apply {
put("name", toolDefinition.name)
put("description", toolDefinition.description ?: "")
// 处理参数定义
if (toolDefinition.parameters != null) {
put("parameters", JSONObject(toolDefinition.parameters))
}
}
return JSONObject().apply {
put("type", "function")
put("function", functionObject)
}
}
/**
* 获取当前连接状态
*/
fun isConnected(): Boolean {
return isConnected && client != null
}
/**
* 注册本地函数
*/
fun registerLocalFunction(
name: String,
description: String,
parameters: JSONObject,
handler: FunctionHandler
): Boolean {
try {
// 创建工具定义
val toolDefinition = JSONObject().apply {
put("type", "function")
put("function", JSONObject().apply {
put("name", name)
put("description", description)
put("parameters", parameters)
})
}
// 添加到工具列表
tools.add(toolDefinition.toString())
// 注册处理器
functionHandlers[name] = handler
Log.d(TAG, "已注册本地函数: $name")
return true
} catch (e: Exception) {
Log.e(TAG, "注册本地函数失败: ${e.message}", e)
return false
}
}
/**
* 获取所有工具的定义
*/
fun getToolMaps(): List<String> {
return tools.toList()
}
/**
* 解析JSON参数
*/
fun parseJsonArguments(argumentsJson: String): Map<String, Any> {
try {
return objectMapper.readValue(argumentsJson, Map::class.java) as Map<String, Any>
} catch (e: Exception) {
Log.e(TAG, "解析JSON参数失败: ${e.message}", e)
return mapOf()
}
}
/**
* 调用工具
*/
suspend fun callTool(name: String, arguments: Map<String, Any>): JSONObject {
try {
// 检查是否是本地函数
if (functionHandlers.containsKey(name)) {
val handler = functionHandlers[name]
val result = handler?.handle(arguments) ?: throw Exception("函数处理器为空")
// 函数处理结果格式化为JSON
return JSONObject().apply {
put("context", result)
}
}
// 如果不是本地函数,使用MCP客户端调用远程工具
if (client != null && isConnected) {
val toolCallRequest = ToolCallRequest(
name = name,
arguments = objectMapper.writeValueAsString(arguments)
)
val result = client?.callTool(toolCallRequest)
return JSONObject().apply {
put("context", result?.result ?: "工具调用失败,未收到结果")
}
}
throw Exception("MCP客户端未连接")
} catch (e: Exception) {
Log.e(TAG, "调用工具失败: ${e.message}", e)
return JSONObject().apply {
put("context", "调用工具失败: ${e.message}")
}
}
}
/**
* 关闭客户端
*/
fun close() {
try {
client?.disconnect()
} catch (e: Exception) {
Log.e(TAG, "关闭MCP客户端失败: ${e.message}", e)
} finally {
client = null
isConnected = false
job.cancel() // 取消所有协程
tools.clear() // 清除工具列表
functionHandlers.clear() // 清除函数处理器
}
}
}

988
local_plugins/open_ai/android/src/main/kotlin/com/yunqiinnovation/open_ai/OpenAIService.kt

@ -1,988 +0,0 @@
package com.yunqiinnovation.open_ai
import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.util.Base64
import android.util.Log
import com.aallam.openai.api.chat.*
import com.aallam.openai.api.file.FileSource
import com.aallam.openai.api.http.Timeout
import com.aallam.openai.api.image.ImageCreation
import com.aallam.openai.api.model.ModelId
import com.aallam.openai.client.OpenAI
import com.aallam.openai.client.OpenAIConfig
import com.aallam.openai.client.OpenAIHost
import com.fasterxml.jackson.databind.ObjectMapper
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import okhttp3.*
import okhttp3.sse.EventSource
import okhttp3.sse.EventSourceListener
import okhttp3.sse.EventSources
import org.json.JSONArray
import org.json.JSONObject
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.IOException
import java.util.UUID
import java.util.concurrent.TimeUnit
import kotlin.coroutines.CoroutineContext
import kotlin.time.Duration.Companion.seconds
/**
* OpenAI服务的原生实现
* 集成了OpenAI官方Java SDK和MCP的官方Kotlin SDK
*/
class OpenAIService(private val context: Context? = null) : CoroutineScope {
private val TAG = "OpenAIService"
// 协程相关
private val job = SupervisorJob()
override val coroutineContext: CoroutineContext
get() = Dispatchers.IO + job
// 添加辅助方法,确保回调在主线程执行
private suspend fun safeCallback(block: suspend () -> Unit) {
withContext(Dispatchers.Main) {
block()
}
}
// OpenAI客户端
private var openAI: OpenAI? = null
private var baseUrl = "https://api.openai.com/v1/chat/completions"
private var apiKey: String = ""
private var isInitialized = false
private var model: String = "gpt-3.5-turbo" // 默认模型
private var visionModel: String = "gpt-4-vision-preview" // 默认视觉模型
// OkHttp客户端用于流式请求
private val client = OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.build()
// MCP客户端
private var mcpClient: MCPClient? = null
private var isMcpInitialized = false
// 当前事件源
private var currentEventSource: EventSource? = null
private var isCanceled = false
/**
* 初始化OpenAI服务
*/
fun initialize(apiKey: String, baseUrl: String, model: String, mcpServer: String): Boolean {
this.apiKey = apiKey
if (baseUrl.isNotEmpty()) {
this.baseUrl = baseUrl
}
if (model.isNotEmpty()) {
this.model = model
}
// 初始化OpenAI客户端
try {
val timeout = Timeout(socket = 30.seconds, connect = 30.seconds, request = 30.seconds)
val host = if (baseUrl.isNotEmpty() && baseUrl != "https://api.openai.com/v1/chat/completions") {
OpenAIHost(baseUrl)
} else {
OpenAIHost.Default
}
val config = OpenAIConfig(
token = apiKey,
host = host,
timeout = timeout
)
openAI = OpenAI(config)
} catch (e: Exception) {
Log.e(TAG, "OpenAI客户端初始化失败: ${e.message}", e)
return false
}
// 初始化MCPClient
initializeMcpClient(mcpServer)
isInitialized = apiKey.isNotEmpty() && openAI != null
return isInitialized
}
/**
* 将文件转换为Base64字符串
*/
fun fileToBase64(filePath: String, maxSizeKB: Int = 20480): String? {
try {
val file = File(filePath)
if (!file.exists() || !file.isFile) {
Log.e(TAG, "文件不存在: $filePath")
return null
}
// 读取文件并压缩(如果需要)
val originalBitmap = BitmapFactory.decodeFile(filePath)
if (originalBitmap == null) {
Log.e(TAG, "无法解码图片: $filePath")
return null
}
val outputStream = ByteArrayOutputStream()
var quality = 100
var compressedBitmap = originalBitmap
// 检查图片尺寸,限制最大为1024*1024
val maxDimension = 1024
if (originalBitmap.width > maxDimension || originalBitmap.height > maxDimension) {
Log.d(TAG, "图片尺寸超过限制,进行缩放: ${originalBitmap.width}x${originalBitmap.height} -> ${maxDimension}x${maxDimension}")
// 计算缩放比例,保持纵横比
val widthRatio = maxDimension.toFloat() / originalBitmap.width
val heightRatio = maxDimension.toFloat() / originalBitmap.height
val ratio = Math.min(widthRatio, heightRatio)
val newWidth = (originalBitmap.width * ratio).toInt()
val newHeight = (originalBitmap.height * ratio).toInt()
compressedBitmap = Bitmap.createScaledBitmap(originalBitmap, newWidth, newHeight, true)
Log.d(TAG, "缩放后图片尺寸: ${newWidth}x${newHeight}")
}
// 如果原始图片太大,继续优化文件大小
var fileSize = file.length() / 1024 // 转为KB
if (fileSize > maxSizeKB) {
val scale = Math.sqrt(maxSizeKB.toDouble() / fileSize)
val newWidth = (compressedBitmap.width * scale).toInt()
val newHeight = (compressedBitmap.height * scale).toInt()
compressedBitmap = Bitmap.createScaledBitmap(compressedBitmap, newWidth, newHeight, true)
quality = 85
}
// 压缩图片
compressedBitmap.compress(Bitmap.CompressFormat.JPEG, quality, outputStream)
val imageBytes = outputStream.toByteArray()
// 检查压缩后大小
if (imageBytes.size / 1024 > maxSizeKB) {
Log.w(TAG, "压缩后图片仍然超出大小限制: ${imageBytes.size / 1024}KB > ${maxSizeKB}KB")
}
// 转为Base64
return Base64.encodeToString(imageBytes, Base64.NO_WRAP)
} catch (e: Exception) {
Log.e(TAG, "转换文件到Base64失败: ${e.message}", e)
return null
}
}
/**
* 创建带图片的用户消息
*/
fun createUserMessageWithImage(text: String, imageBase64: String): JSONObject {
// 创建包含文本和图片的内容数组
val contentArray = JSONArray().apply {
// 添加文本部分
if (text.isNotEmpty()) {
put(JSONObject().apply {
put("type", "text")
put("text", text)
})
}
// 添加图片部分
put(JSONObject().apply {
put("type", "image_url")
put("image_url", JSONObject().apply {
put("url", "data:image/jpeg;base64,$imageBase64")
})
})
}
return JSONObject().apply {
put("role", "user")
put("content", contentArray)
}
}
/**
* 创建用户消息
*/
fun createUserMessage(content: String): JSONObject {
return JSONObject().apply {
put("role", "user")
put("content", content)
}
}
/**
* 创建系统消息
*/
fun createSystemMessage(content: String): JSONObject {
return JSONObject().apply {
put("role", "system")
put("content", content)
}
}
/**
* 创建助手消息
*/
fun createAssistantMessage(content: String): JSONObject {
return JSONObject().apply {
put("role", "assistant")
put("content", content)
}
}
/**
* MCP客户端是否已初始化
*/
fun isMcpInitialized(): Boolean {
return isMcpInitialized && mcpClient?.isConnected() == true
}
/**
* 关闭MCP客户端
*/
fun closeMcpClient() {
mcpClient?.close()
mcpClient = null
isMcpInitialized = false
}
/**
* 初始化MCP客户端
*/
private fun initializeMcpClient(mcpServer: String): Boolean {
if (mcpClient != null) {
mcpClient?.close()
}
if (mcpServer.isEmpty()) {
return false
}
mcpClient = MCPClient(context)
// 在后台线程中初始化MCP客户端
launch {
try {
val result = mcpClient?.connectToSSE(mcpServer) ?: false
isMcpInitialized = result
Log.d(TAG, "MCP客户端初始化${if (result) "成功" else "失败"}")
} catch (e: Exception) {
Log.e(TAG, "MCP客户端初始化失败: ${e.message}", e)
isMcpInitialized = false
}
}
return true // 立即返回,实际连接在后台进行
}
/**
* 处理MCP工具调用
*/
private suspend fun handleMcpToolCall(functionCall: JSONObject): JSONObject? {
if (mcpClient == null || !isMcpInitialized) {
return JSONObject().apply { put("context", "MCP客户端未初始化") }
}
try {
// 获取函数名称
val name = functionCall.getString("name")
// 获取参数
val argumentsJson = functionCall.getString("arguments")
val arguments = mcpClient?.parseJsonArguments(argumentsJson) ?: mapOf()
// 调用工具
return mcpClient?.callTool(name, arguments)
} catch (e: Exception) {
Log.e(TAG, "处理MCP工具调用失败: ${e.message}", e)
return JSONObject().apply { put("context", "处理MCP工具调用失败: ${e.message}") }
}
}
/**
* 将JSONArray转换为ChatCompletionRequest中的消息列表
*/
private fun parseMessages(messagesArray: JSONArray): List<ChatMessage> {
val messages = mutableListOf<ChatMessage>()
for (i in 0 until messagesArray.length()) {
val messageObj = messagesArray.getJSONObject(i)
val role = messageObj.getString("role")
when (role) {
"system" -> {
val content = messageObj.getString("content")
messages.add(ChatMessage(role = ChatRole.System, content = content))
}
"user" -> {
// 检查是否有多媒体内容
if (messageObj.has("content") && messageObj.get("content") is JSONArray) {
val contentArray = messageObj.getJSONArray("content")
val parts = mutableListOf<ChatMessageContent>()
for (j in 0 until contentArray.length()) {
val contentObj = contentArray.getJSONObject(j)
val type = contentObj.getString("type")
when (type) {
"text" -> {
parts.add(TextContent(contentObj.getString("text")))
}
"image_url" -> {
val imageUrlObj = contentObj.getJSONObject("image_url")
val url = imageUrlObj.getString("url")
parts.add(ImageContent(url))
}
}
}
messages.add(ChatMessage(
role = ChatRole.User,
content = parts
))
} else {
// 普通文本消息
val content = messageObj.getString("content")
messages.add(ChatMessage(role = ChatRole.User, content = content))
}
}
"assistant" -> {
if (messageObj.has("tool_calls")) {
// 处理工具调用
val toolCalls = messageObj.getJSONArray("tool_calls")
val toolCallsList = mutableListOf<ToolCall>()
for (j in 0 until toolCalls.length()) {
val toolCall = toolCalls.getJSONObject(j)
val id = toolCall.getString("id")
val function = toolCall.getJSONObject("function")
val name = function.getString("name")
val arguments = function.getString("arguments")
toolCallsList.add(ToolCall(
id = id,
type = ToolCallType.Function,
function = FunctionCall(
name = name,
arguments = arguments
)
))
}
val content = if (messageObj.has("content")) messageObj.getString("content") else ""
messages.add(ChatMessage(
role = ChatRole.Assistant,
content = content,
toolCalls = toolCallsList
))
} else {
// 普通消息
val content = messageObj.getString("content")
messages.add(ChatMessage(role = ChatRole.Assistant, content = content))
}
}
"tool" -> {
val content = messageObj.getString("content")
val toolCallId = messageObj.getString("tool_call_id")
messages.add(ChatMessage(
role = ChatRole.Tool,
content = content,
toolCallId = toolCallId
))
}
}
}
return messages
}
/**
* 从MCP客户端获取工具定义
*/
private fun getToolsFromMcpClient(): List<Tool> {
val tools = mutableListOf<Tool>()
mcpClient?.getToolMaps()?.forEach { toolMap ->
try {
val toolJson = JSONObject(toolMap)
val name = toolJson.getString("name")
val description = toolJson.getString("description")
val parametersJson = if (toolJson.has("parameters")) toolJson.getJSONObject("parameters") else null
val parameterProperties = mutableMapOf<String, ParameterDefinition>()
val requiredParams = mutableListOf<String>()
if (parametersJson != null && parametersJson.has("properties")) {
val properties = parametersJson.getJSONObject("properties")
val keys = properties.keys()
while (keys.hasNext()) {
val key = keys.next()
val propertyObj = properties.getJSONObject(key)
val propType = propertyObj.optString("type", "string")
val propDescription = propertyObj.optString("description", "")
parameterProperties[key] = ParameterDefinition(
type = propType,
description = propDescription
)
}
// 获取必填参数
if (parametersJson.has("required")) {
val requiredArr = parametersJson.getJSONArray("required")
for (i in 0 until requiredArr.length()) {
requiredParams.add(requiredArr.getString(i))
}
}
}
tools.add(Tool(
type = ToolType.Function,
function = FunctionDefinition(
name = name,
description = description,
parameters = FunctionParameters(
type = "object",
properties = parameterProperties,
required = requiredParams
)
)
))
} catch (e: Exception) {
Log.e(TAG, "解析工具定义失败: ${e.message}", e)
}
}
return tools
}
/**
* 发送消息(非流式输出)
*/
suspend fun sendMessage(messages: JSONArray): String {
if (!isInitialized || apiKey.isEmpty() || openAI == null) {
throw IOException("OpenAI服务未初始化")
}
try {
// 解析消息
val parsedMessages = parseMessages(messages)
// 获取工具列表
val tools = getToolsFromMcpClient()
// 创建请求
val request = ChatCompletionRequest(
model = ModelId(model),
messages = parsedMessages,
tools = if (tools.isNotEmpty()) tools else null,
temperature = 0.7,
maxTokens = 2000
)
// 发送请求
val response = openAI!!.chatCompletion(request)
// 解析响应
val choice = response.choices.firstOrNull() ?: throw IOException("无效的响应格式")
// 检查是否有工具调用
if (choice.message.toolCalls?.isNotEmpty() == true) {
val toolCall = choice.message.toolCalls?.first()
if (toolCall != null && toolCall.function != null) {
val functionCall = JSONObject().apply {
put("name", toolCall.function.name)
put("arguments", toolCall.function.arguments)
put("id", toolCall.id)
}
return functionCall.toString()
}
}
// 返回消息内容
return choice.message.content ?: ""
} catch (e: Exception) {
Log.e(TAG, "发送消息失败: ${e.message}", e)
throw IOException("与AI服务通信失败: ${e.message}")
}
}
/**
* 发送消息(流式输出)
*/
fun sendMessageStream(messages: JSONArray, callback: StreamCallback) {
if (!isInitialized || apiKey.isEmpty()) {
// 在主线程执行回调
launch {
withContext(Dispatchers.Main) {
callback.onError(Exception("OpenAI服务未初始化"))
}
}
return
}
// 重置取消状态
isCanceled = false
// 检查是否有图片消息
var hasImageContent = false
var currentModel = model
for (i in 0 until messages.length()) {
val messageObj = messages.getJSONObject(i)
if (messageObj.getString("role") == "user" && messageObj.has("content")) {
val content = messageObj.get("content")
if (content is JSONArray) {
for (j in 0 until content.length()) {
val contentObj = content.getJSONObject(j)
if (contentObj.getString("type") == "image_url") {
hasImageContent = true
currentModel = visionModel
break
}
}
}
}
if (hasImageContent) break
}
// 构建JSON请求体
val requestBody = JSONObject().apply {
put("model", currentModel)
put("messages", messages)
put("temperature", 0.7)
put("max_tokens", 2000)
put("stream", true)
// 添加工具列表
val tools = JSONArray()
// 使用MCPClient提供的所有工具
mcpClient?.getToolMaps()?.forEach { toolMap ->
try {
val tool = JSONObject(toolMap)
tools.put(tool)
} catch (e: Exception) {
Log.e(TAG, "转换工具失败: ${e.message}", e)
}
}
// 如果有工具,则添加到请求中
if (tools.length() > 0) {
put("tools", tools)
}
}
val mediaType = "application/json".toMediaTypeOrNull()
val request = Request.Builder()
.url(baseUrl)
.addHeader("Content-Type", "application/json")
.addHeader("Authorization", "Bearer $apiKey")
.addHeader("Accept", "text/event-stream")
.post(requestBody.toString().toRequestBody(mediaType))
.build()
// 创建事件源
val factory = EventSources.createFactory(client)
// 工具调用相关变量
val toolCalls = mutableMapOf<Int, ToolCallInfo>()
val eventSourceListener = object : EventSourceListener() {
override fun onOpen(eventSource: EventSource, response: Response) {
Log.d(TAG, "SSE连接已打开")
}
override fun onEvent(eventSource: EventSource, id: String?, type: String?, data: String) {
if (isCanceled) return
if (data == "[DONE]" || data == "[\"DONE\"]") {
// 处理可能的工具调用
processToolCalls(toolCalls, callback, messages)
return
}
try {
val jsonData = JSONObject(data)
// 处理消息内容
if (jsonData.has("choices")) {
val choices = jsonData.getJSONArray("choices")
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")
if (!isCanceled) {
launch {
withContext(Dispatchers.Main) {
callback.onToken(content)
}
}
}
}
// 处理工具调用
if (delta.has("tool_calls")) {
val deltaToolCalls = delta.getJSONArray("tool_calls")
for (i in 0 until deltaToolCalls.length()) {
val toolCall = deltaToolCalls.getJSONObject(i)
val index = toolCall.getInt("index")
// 创建或获取现有的工具调用信息
val toolCallInfo = toolCalls.getOrPut(index) { ToolCallInfo() }
// 更新ID
if (toolCall.has("id")) {
toolCallInfo.id = toolCall.getString("id")
}
// 更新函数信息
if (toolCall.has("function")) {
val function = toolCall.getJSONObject("function")
if (function.has("name")) {
toolCallInfo.name = function.getString("name")
}
if (function.has("arguments")) {
toolCallInfo.arguments += function.getString("arguments")
}
}
}
}
}
// 检查是否有表示完成的标志
if (choice.has("finish_reason")) {
val finishReason = choice.getString("finish_reason")
if (finishReason == "stop" || finishReason == "length") {
// 正常完成,没有工具调用
launch {
withContext(Dispatchers.Main) {
callback.onComplete()
}
}
closeEventSource()
} else if (finishReason == "tool_calls") {
// 处理工具调用
processToolCalls(toolCalls, callback, messages)
closeEventSource()
}
}
}
}
} catch (e: Exception) {
Log.e(TAG, "解析事件数据失败: ${e.message}", e)
}
}
override fun onClosed(eventSource: EventSource) {
Log.d(TAG, "SSE连接已关闭")
if (!isCanceled) {
// 如果没有正常完成,但连接关闭了,则处理最后可能的工具调用
if (toolCalls.isNotEmpty()) {
processToolCalls(toolCalls, callback, messages)
} else {
launch {
withContext(Dispatchers.Main) {
callback.onComplete()
}
}
}
}
}
override fun onFailure(eventSource: EventSource, t: Throwable?, response: Response?) {
if (isCanceled) return
val errorCode = response?.code ?: 0
val errorMessage = t?.message ?: "未知错误"
Log.e(TAG, "SSE连接失败: $errorCode - $errorMessage")
launch {
withContext(Dispatchers.Main) {
callback.onError(Exception("流式请求失败: $errorMessage"))
}
}
closeEventSource()
}
}
currentEventSource = factory.newEventSource(request, eventSourceListener)
}
/**
* 处理工具调用
*/
private fun processToolCalls(toolCalls: Map<Int, ToolCallInfo>, callback: StreamCallback, messages: JSONArray? = null): Boolean {
if (toolCalls.isEmpty()) return false
val firstToolCall = toolCalls.entries.firstOrNull()?.value ?: return false
if (firstToolCall.isValid()) {
// 创建函数调用JSON对象
val functionCall = JSONObject().apply {
put("name", firstToolCall.name)
put("arguments", firstToolCall.arguments)
put("id", firstToolCall.id)
}
Log.d(TAG, "工具调用: $functionCall")
// 通知上层回调
launch {
withContext(Dispatchers.Main) {
callback.onFunctionCall(functionCall)
}
}
// 在协程中自动处理工具调用
if (messages != null) {
launch {
try {
if (!isCanceled) {
handleToolCall(functionCall, messages, callback)
}
} catch (e: Exception) {
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)
withContext(Dispatchers.Main) {
callback.onError(Exception("工具调用处理失败: ${e.message}"))
}
}
}
}
}
}
return true
}
return false
}
/**
* 处理工具调用
*/
private suspend fun handleToolCall(functionCall: JSONObject, messages: JSONArray, callback: StreamCallback) {
try {
// 获取函数名称和参数
val name = functionCall.getString("name")
val argumentsJson = functionCall.getString("arguments")
Log.d(TAG, "处理工具调用: name=$name, arguments=$argumentsJson")
// 调用MCP工具
val result = handleMcpToolCall(functionCall)
// 检查是否已取消
if (isCanceled) {
Log.d(TAG, "工具调用已被取消,不处理结果")
return
}
// 回调结果
val resultObj = result ?: JSONObject().apply { put("context", "工具调用失败") }
withContext(Dispatchers.Main) {
callback.onFunctionCallResult(functionCall, resultObj)
}
// 从结果中提取内容
val resultContent = if (resultObj.has("context") && resultObj.optString("context").isNotEmpty()) {
resultObj.getString("context")
} else {
val jsonString = resultObj.toString()
if (jsonString == "{}") "工具调用失败" else jsonString
}
// 发送函数调用结果
sendFunctionCallResult(messages, functionCall, resultContent, callback)
} catch (e: Exception) {
if (!isCanceled) {
Log.e(TAG, "处理工具调用失败: ${e.message}", e)
withContext(Dispatchers.Main) {
callback.onError(Exception("工具调用失败: ${e.message}"))
}
}
}
}
/**
* 发送函数调用结果
*/
fun sendFunctionCallResult(
messages: JSONArray,
functionCall: JSONObject,
functionResult: String,
callback: StreamCallback
) {
if (isCanceled) {
Log.d(TAG, "请求已取消,不发送函数调用结果")
return
}
try {
val fullMessages = JSONArray()
// 添加原始消息
for (i in 0 until messages.length()) {
fullMessages.put(messages.getJSONObject(i))
}
// 添加函数调用消息
val callId = functionCall.optString("id", "call_${System.currentTimeMillis()}")
fullMessages.put(JSONObject().apply {
put("role", "assistant")
put("content", "")
// 添加工具调用
val toolCalls = JSONArray().apply {
val toolCall = JSONObject().apply {
put("id", callId)
put("type", "function")
put("function", JSONObject().apply {
put("name", functionCall.getString("name"))
put("arguments", functionCall.getString("arguments"))
})
}
put(toolCall)
}
put("tool_calls", toolCalls)
})
// 添加函数调用结果
fullMessages.put(JSONObject().apply {
put("role", "tool")
put("content", functionResult)
put("tool_call_id", callId)
})
// 发送完整对话
sendMessageStream(fullMessages, callback)
} catch (e: Exception) {
if (!isCanceled) {
Log.e(TAG, "发送函数调用结果失败: ${e.message}", e)
launch {
withContext(Dispatchers.Main) {
callback.onError(Exception("发送函数调用结果失败: ${e.message}"))
}
}
}
}
}
/**
* 关闭事件源
*/
private fun closeEventSource() {
currentEventSource?.let {
try {
it.cancel()
} catch (e: Exception) {
Log.e(TAG, "关闭事件源失败: ${e.message}", e)
}
currentEventSource = null
}
}
/**
* 取消当前流式请求
*/
fun cancelCurrentStream(): Boolean {
isCanceled = true
closeEventSource()
return true
}
/**
* 释放资源
*/
fun dispose() {
job.cancel() // 取消所有协程
cancelCurrentStream() // 取消当前流式请求
closeMcpClient() // 关闭MCP客户端
}
/**
* 工具调用信息类
*/
private class ToolCallInfo {
var id: String = ""
var name: String = ""
var arguments: String = ""
fun isValid(): Boolean {
return id.isNotEmpty() && name.isNotEmpty()
}
}
/**
* 流式回调接口
*/
interface StreamCallback {
fun onToken(token: String)
fun onComplete()
fun onError(e: Exception)
fun onFunctionCall(functionCall: JSONObject)
fun onFunctionCallResult(functionCall: JSONObject, functionCallResult: JSONObject)
}
/**
* 参数定义
*/
private data class ParameterDefinition(
val type: String,
val description: String
)
/**
* 注册函数
*/
fun registerFunction(name: String, description: String, parameters: JSONObject): Boolean {
try {
// 确保MCPClient已初始化
if (mcpClient == null) {
mcpClient = MCPClient(context)
}
// 创建函数处理器
val handler = object : FunctionHandler {
override suspend fun handle(arguments: Map<String, Any>): String {
// 由于本地函数的实际处理是在Flutter端完成的
// 这里只需返回一个标记,表示该函数是本地函数
return "LOCAL_FUNCTION:$name"
}
}
// 注册本地函数
return mcpClient?.registerLocalFunction(name, description, parameters, handler) ?: false
} catch (e: Exception) {
Log.e(TAG, "注册函数失败: ${e.message}", e)
return false
}
}
}

149
local_plugins/open_ai/android/src/main/kotlin/com/yunqiinnovation/open_ai/OpenAiPlugin.kt

@ -1,149 +0,0 @@
package com.yunqiinnovation.open_ai
import android.content.Context
import androidx.annotation.NonNull
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result
import org.json.JSONArray
import org.json.JSONObject
import kotlinx.coroutines.*
class OpenAiPlugin : FlutterPlugin, MethodCallHandler {
private lateinit var channel: MethodChannel
private lateinit var context: Context
private lateinit var openAIService: OpenAIService
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "com.yunqiinnovation.open_ai")
context = flutterPluginBinding.applicationContext
openAIService = OpenAIService(context)
channel.setMethodCallHandler(this)
}
override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
when (call.method) {
"initialize" -> {
val apiKey = call.argument<String>("apiKey") ?: ""
val baseUrl = call.argument<String>("baseUrl") ?: ""
val model = call.argument<String>("model") ?: ""
val mcpServer = call.argument<String>("mcpServer") ?: ""
val success = openAIService.initialize(apiKey, baseUrl, model, mcpServer)
result.success(success)
}
"createUserMessage" -> {
val content = call.argument<String>("content") ?: ""
val message = openAIService.createUserMessage(content)
result.success(message.toString())
}
"createSystemMessage" -> {
val content = call.argument<String>("content") ?: ""
val message = openAIService.createSystemMessage(content)
result.success(message.toString())
}
"createAssistantMessage" -> {
val content = call.argument<String>("content") ?: ""
val message = openAIService.createAssistantMessage(content)
result.success(message.toString())
}
"createUserMessageWithImage" -> {
val text = call.argument<String>("text") ?: ""
val imageBase64 = call.argument<String>("imageBase64") ?: ""
val message = openAIService.createUserMessageWithImage(text, imageBase64)
result.success(message.toString())
}
"sendMessage" -> {
val messagesJson = call.argument<String>("messages") ?: "[]"
val messages = JSONArray(messagesJson)
scope.launch {
try {
val response = openAIService.sendMessage(messages)
withContext(Dispatchers.Main) {
result.success(response)
}
} catch (e: Exception) {
withContext(Dispatchers.Main) {
result.error("OPENAI_ERROR", e.message ?: "Unknown error", null)
}
}
}
}
"sendMessageStream" -> {
val messagesJson = call.argument<String>("messages") ?: "[]"
val messages = JSONArray(messagesJson)
val streamId = call.argument<String>("streamId") ?: "${System.currentTimeMillis()}"
val callback = object : OpenAIService.StreamCallback {
override fun onToken(token: String) {
val map = mapOf(
"type" to "token",
"streamId" to streamId,
"data" to token
)
channel.invokeMethod("onStreamEvent", map)
}
override fun onComplete() {
val map = mapOf(
"type" to "complete",
"streamId" to streamId
)
channel.invokeMethod("onStreamEvent", map)
}
override fun onError(e: Exception) {
val map = mapOf(
"type" to "error",
"streamId" to streamId,
"error" to (e.message ?: "Unknown error")
)
channel.invokeMethod("onStreamEvent", map)
}
override fun onFunctionCall(functionCall: JSONObject) {
val map = mapOf(
"type" to "functionCall",
"streamId" to streamId,
"data" to functionCall.toString()
)
channel.invokeMethod("onStreamEvent", map)
}
override fun onFunctionCallResult(functionCall: JSONObject, functionCallResult: JSONObject) {
val map = mapOf(
"type" to "functionCallResult",
"streamId" to streamId,
"functionCall" to functionCall.toString(),
"result" to functionCallResult.toString()
)
channel.invokeMethod("onStreamEvent", map)
}
}
openAIService.sendMessageStream(messages, callback)
result.success(streamId)
}
"cancelCurrentStream" -> {
val success = openAIService.cancelCurrentStream()
result.success(success)
}
"dispose" -> {
openAIService.dispose()
result.success(null)
}
else -> {
result.notImplemented()
}
}
}
override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
channel.setMethodCallHandler(null)
scope.cancel() // Cancel all coroutines when the plugin is detached
openAIService.dispose() // Clean up resources
}
}

184
local_plugins/open_ai/lib/open_ai.dart

@ -1,184 +0,0 @@
import 'dart:async';
import 'dart:convert';
import 'package:flutter/services.dart';
/// OpenAI和MCP集成插件
class OpenAI {
/// 插件通道
static const MethodChannel _channel =
MethodChannel('com.yunqiinnovation.open_ai');
/// 流式输出事件回调
static final Map<String, StreamCallback> _streamCallbacks = {};
/// 构造函数
OpenAI() {
_channel.setMethodCallHandler(_handleMethodCall);
}
/// 处理来自原生端的方法调用
Future<dynamic> _handleMethodCall(MethodCall call) async {
if (call.method == 'onStreamEvent') {
final Map<String, dynamic> args = Map<String, dynamic>.from(call.arguments);
final String streamId = args['streamId'];
final String type = args['type'];
final callback = _streamCallbacks[streamId];
if (callback != null) {
switch (type) {
case 'token':
callback.onToken(args['data']);
break;
case 'complete':
callback.onComplete();
_streamCallbacks.remove(streamId);
break;
case 'error':
callback.onError(Exception(args['error']));
_streamCallbacks.remove(streamId);
break;
case 'functionCall':
final functionCall = jsonDecode(args['data']);
callback.onFunctionCall(functionCall);
break;
case 'functionCallResult':
final functionCall = jsonDecode(args['functionCall']);
final functionCallResult = jsonDecode(args['result']);
callback.onFunctionCallResult(functionCall, functionCallResult);
break;
}
}
}
return null;
}
/// 初始化OpenAI服务
///
/// [apiKey] OpenAI API密钥
/// [baseUrl] API基础URL,可选
/// [model] 模型名称,可选
/// [mcpServer] MCP服务器URL,可选
Future<bool> initialize({
required String apiKey,
String baseUrl = '',
String model = '',
String mcpServer = '',
}) async {
final result = await _channel.invokeMethod<bool>('initialize', {
'apiKey': apiKey,
'baseUrl': baseUrl,
'model': model,
'mcpServer': mcpServer,
});
return result ?? false;
}
/// 创建用户消息
Future<Map<String, dynamic>> createUserMessage(String content) async {
final result = await _channel.invokeMethod<String>('createUserMessage', {
'content': content,
});
return jsonDecode(result ?? '{}');
}
/// 创建系统消息
Future<Map<String, dynamic>> createSystemMessage(String content) async {
final result = await _channel.invokeMethod<String>('createSystemMessage', {
'content': content,
});
return jsonDecode(result ?? '{}');
}
/// 创建助手消息
Future<Map<String, dynamic>> createAssistantMessage(String content) async {
final result = await _channel.invokeMethod<String>('createAssistantMessage', {
'content': content,
});
return jsonDecode(result ?? '{}');
}
/// 创建带图片的用户消息
Future<Map<String, dynamic>> createUserMessageWithImage(
String text,
String imageBase64,
) async {
final result = await _channel.invokeMethod<String>(
'createUserMessageWithImage',
{
'text': text,
'imageBase64': imageBase64,
},
);
return jsonDecode(result ?? '{}');
}
/// 发送消息(非流式输出)
Future<String> sendMessage(List<Map<String, dynamic>> messages) async {
final messagesJson = jsonEncode(messages);
return await _channel.invokeMethod('sendMessage', {
'messages': messagesJson,
});
}
/// 发送消息(流式输出)
Future<String> sendMessageStream(
List<Map<String, dynamic>> messages,
StreamCallback callback,
) async {
final messagesJson = jsonEncode(messages);
final streamId = DateTime.now().millisecondsSinceEpoch.toString();
// 注册回调
_streamCallbacks[streamId] = callback;
final result = await _channel.invokeMethod<String>('sendMessageStream', {
'messages': messagesJson,
'streamId': streamId,
});
return result ?? streamId;
}
/// 取消当前流式请求
Future<bool> cancelCurrentStream() async {
final result = await _channel.invokeMethod<bool>('cancelCurrentStream');
return result ?? false;
}
/// 释放资源
Future<void> dispose() async {
await _channel.invokeMethod('dispose');
_streamCallbacks.clear();
}
}
/// 流式输出回调接口
class StreamCallback {
/// 收到令牌
final void Function(String token) onToken;
/// 完成回调
final void Function() onComplete;
/// 错误回调
final void Function(Exception e) onError;
/// 函数调用回调
final void Function(Map<String, dynamic> functionCall) onFunctionCall;
/// 函数调用结果回调
final void Function(
Map<String, dynamic> functionCall,
Map<String, dynamic> functionCallResult,
) onFunctionCallResult;
/// 构造函数
StreamCallback({
required this.onToken,
required this.onComplete,
required this.onError,
required this.onFunctionCall,
required this.onFunctionCallResult,
});
}

27
local_plugins/open_ai/pubspec.yaml

@ -1,27 +0,0 @@
name: open_ai
description: OpenAI API与MCP集成插件,提供对OpenAI API的访问和MCP工具调用功能
version: 0.1.0
homepage: https://github.com/yunqiinnovation/deep_voice
environment:
sdk: '>=2.18.0 <4.0.0'
flutter: ">=3.3.0"
dependencies:
flutter:
sdk: flutter
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^2.0.0
# 插件平台配置
flutter:
plugin:
platforms:
android:
package: com.yunqiinnovation.open_ai
pluginClass: OpenAiPlugin
ios:
pluginClass: OpenAiPlugin

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

@ -43,15 +43,6 @@ class OpenAIService(private val context: Context? = null) : CoroutineScope {
// 主线程Handler,用于确保回调在主线程执行 // 主线程Handler,用于确保回调在主线程执行
private val mainHandler = Handler(Looper.getMainLooper()) private val mainHandler = Handler(Looper.getMainLooper())
// 将回调安全地切换到主线程
private fun StreamCallback.dispatch(block: StreamCallback.() -> Unit) {
if (Looper.myLooper() == Looper.getMainLooper()) {
block()
} else {
mainHandler.post { block() }
}
}
private var baseUrl = "" private var baseUrl = ""
private val client = OkHttpClient.Builder() private val client = OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS) .connectTimeout(30, TimeUnit.SECONDS)
@ -352,7 +343,7 @@ class OpenAIService(private val context: Context? = null) : CoroutineScope {
// 回调结果 // 回调结果
val resultObj = result ?: JSONObject().apply { put("context", "工具调用失败") } val resultObj = result ?: JSONObject().apply { put("context", "工具调用失败") }
callback.dispatch { onFunctionCallResult(functionCall, resultObj) } callback.onFunctionCallResult(functionCall, resultObj)
// 发送结果到OpenAI继续对话 // 发送结果到OpenAI继续对话
val resultContent = resultObj.optString("context", "工具调用失败") val resultContent = resultObj.optString("context", "工具调用失败")
@ -480,7 +471,7 @@ class OpenAIService(private val context: Context? = null) : CoroutineScope {
*/ */
fun sendMessageStream(messages: JSONArray, callback: StreamCallback) { fun sendMessageStream(messages: JSONArray, callback: StreamCallback) {
if (!isInitialized || apiKey.isEmpty()) { if (!isInitialized || apiKey.isEmpty()) {
callback.dispatch { onError(OpenAIException("OpenAI服务未初始化")) } callback.onError(OpenAIException("OpenAI服务未初始化"))
return return
} }
@ -537,17 +528,19 @@ class OpenAIService(private val context: Context? = null) : CoroutineScope {
val call = client.newCall(request) val call = client.newCall(request)
// 保存当前Call对象以便后续取消 // 保存当前Call对象以便后续取消
currentStreamCall = call currentStreamCall = call
Log.d(TAG, "发送消息流式输出: request=$request")
call.enqueue(object : Callback { call.enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) { override fun onFailure(call: Call, e: IOException) {
Log.d(TAG, "发送消息流式输出失败: e=$e")
// 如果是由于取消导致的失败,不报告错误 // 如果是由于取消导致的失败,不报告错误
if (isCanceled) { if (isCanceled) {
return return
} }
callback.dispatch { onError(OpenAIException(e.message ?: "请求失败")) } callback.onError(OpenAIException(e.message ?: "请求失败"))
} }
override fun onResponse(call: Call, response: Response) { override fun onResponse(call: Call, response: Response) {
// Log.d(TAG, "发送消息流式输出成功: response=$response")
// 检查请求是否被取消 // 检查请求是否被取消
if (isCanceled) { if (isCanceled) {
response.body?.close() response.body?.close()
@ -555,7 +548,7 @@ class OpenAIService(private val context: Context? = null) : CoroutineScope {
} }
if (!response.isSuccessful) { if (!response.isSuccessful) {
callback.dispatch { onError(OpenAIException("API调用失败: ${response.code}")) } callback.onError(OpenAIException("API调用失败: ${response.code}"))
return return
} }
@ -588,7 +581,7 @@ class OpenAIService(private val context: Context? = null) : CoroutineScope {
val hasToolCalls = processToolCalls(toolCalls, callback, messages) val hasToolCalls = processToolCalls(toolCalls, callback, messages)
// 如果没有工具调用,这是对话的真正结束 // 如果没有工具调用,这是对话的真正结束
if (!hasToolCalls) { if (!hasToolCalls) {
callback.dispatch { onComplete() } callback.onComplete()
} }
return return
} }
@ -611,14 +604,14 @@ class OpenAIService(private val context: Context? = null) : CoroutineScope {
// 如果没有明确的完成信号但数据流结束了,处理最后可能的工具调用 // 如果没有明确的完成信号但数据流结束了,处理最后可能的工具调用
val hasToolCalls = processToolCalls(toolCalls, callback, messages) val hasToolCalls = processToolCalls(toolCalls, callback, messages)
if (!hasToolCalls) { if (!hasToolCalls) {
callback.dispatch { onComplete() } callback.onComplete()
} }
} catch (e: Exception) { } catch (e: Exception) {
// 如果是由于取消导致的异常,不报告错误 // 如果是由于取消导致的异常,不报告错误
if (isCanceled) { if (isCanceled) {
return return
} }
callback.dispatch { onError(OpenAIException("处理响应流时出错: ${e.message}")) } callback.onError(OpenAIException("处理响应流时出错: ${e.message}"))
} finally { } finally {
responseBody.close() responseBody.close()
} }
@ -645,8 +638,9 @@ class OpenAIService(private val context: Context? = null) : CoroutineScope {
// 处理普通文本内容 // 处理普通文本内容
if (delta.has("content")) { if (delta.has("content")) {
val content = delta.getString("content") val content = delta.getString("content")
Log.d(TAG, "处理流式JSON数据: content=$content")
if (!isCanceled) { if (!isCanceled) {
callback.dispatch { onToken(content) } callback.onToken(content)
} }
} }
@ -707,7 +701,7 @@ class OpenAIService(private val context: Context? = null) : CoroutineScope {
Log.d(TAG, "工具调用: id=${firstToolCall.id}, name=${firstToolCall.name}") Log.d(TAG, "工具调用: id=${firstToolCall.id}, name=${firstToolCall.name}")
// 通知上层工具调用事件 // 通知上层工具调用事件
callback.dispatch { onFunctionCall(functionCall) } callback.onFunctionCall(functionCall)
// 在协程中处理工具调用 // 在协程中处理工具调用
if (messages != null) { if (messages != null) {
@ -726,13 +720,13 @@ class OpenAIService(private val context: Context? = null) : CoroutineScope {
if (!isCanceled) { if (!isCanceled) {
// 处理结果 // 处理结果
val resultObj = result ?: JSONObject().put("context", "工具调用失败") val resultObj = result ?: JSONObject().put("context", "工具调用失败")
Log.d(TAG, "工具调用完成: id=${firstToolCall.id}") callback.onFunctionCallResult(functionCall, resultObj)
callback.dispatch { onFunctionCallResult(functionCall, resultObj) }
val context = resultObj.optString("context", "工具调用失败").let { val context = resultObj.optString("context", "工具调用失败").let {
if (it.isNullOrEmpty()) "工具调用失败" else it if (it.isNullOrEmpty()) "工具调用失败" else it
} }
Log.d(TAG, "mcp工具调用完成: name=${firstToolCall.name}, context=$context")
// 将结果发送回OpenAI继续对话 // 将结果发送回OpenAI继续对话
sendFunctionCallResult( sendFunctionCallResult(
messages = messages, messages = messages,
@ -821,7 +815,7 @@ class OpenAIService(private val context: Context? = null) : CoroutineScope {
} catch (e: Exception) { } catch (e: Exception) {
if (!isCanceled) { if (!isCanceled) {
Log.e(TAG, "发送函数调用结果失败: ${e.message}", e) Log.e(TAG, "发送函数调用结果失败: ${e.message}", e)
callback.dispatch { onError(OpenAIException("发送函数调用结果失败: ${e.message}")) } callback.onError(OpenAIException("发送函数调用结果失败: ${e.message}"))
} }
} }
} }

205
local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/SystemFunctionHandler.kt

@ -57,6 +57,12 @@ class SystemFunctionHandler(private val context: Context) {
// 注册获取当前位置函数 // 注册获取当前位置函数
registerGetCurrentLocationFunction(client) registerGetCurrentLocationFunction(client)
// 注册媒体控制函数
registerMediaPlayFunction(client)
registerMediaPauseFunction(client)
registerMediaPreviousFunction(client)
registerMediaNextFunction(client)
Log.d(TAG, "所有系统功能注册完成") Log.d(TAG, "所有系统功能注册完成")
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "注册系统功能失败: ${e.message}", e) Log.e(TAG, "注册系统功能失败: ${e.message}", e)
@ -303,8 +309,205 @@ class SystemFunctionHandler(private val context: Context) {
Log.d(TAG, "已注册获取当前位置功能") Log.d(TAG, "已注册获取当前位置功能")
} }
/**
* 注册媒体播放功能
*/
private fun registerMediaPlayFunction(client: MCPClient) {
client.registerLocalFunction(
"media_play",
"播放媒体",
JSONObject("""
{
"type": "object",
"properties": {},
"required": []
}
"""),
object : FunctionHandler {
override suspend fun handle(arguments: Map<String, Any>): String {
val result = controlMedia("play")
return "{\"result\": \"${result.message}\"}"
}
}
)
Log.d(TAG, "已注册媒体播放功能")
}
/**
* 注册媒体暂停功能
*/
private fun registerMediaPauseFunction(client: MCPClient) {
client.registerLocalFunction(
"media_pause",
"暂停媒体播放",
JSONObject("""
{
"type": "object",
"properties": {},
"required": []
}
"""),
object : FunctionHandler {
override suspend fun handle(arguments: Map<String, Any>): String {
val result = controlMedia("pause")
return "{\"result\": \"${result.message}\"}"
}
}
)
Log.d(TAG, "已注册媒体暂停功能")
}
/**
* 注册媒体上一首功能
*/
private fun registerMediaPreviousFunction(client: MCPClient) {
client.registerLocalFunction(
"media_previous",
"播放上一首",
JSONObject("""
{
"type": "object",
"properties": {},
"required": []
}
"""),
object : FunctionHandler {
override suspend fun handle(arguments: Map<String, Any>): String {
val result = controlMedia("previous")
return "{\"result\": \"${result.message}\"}"
}
}
)
Log.d(TAG, "已注册媒体上一首功能")
}
/**
* 注册媒体下一首功能
*/
private fun registerMediaNextFunction(client: MCPClient) {
client.registerLocalFunction(
"media_next",
"播放下一首",
JSONObject("""
{
"type": "object",
"properties": {},
"required": []
}
"""),
object : FunctionHandler {
override suspend fun handle(arguments: Map<String, Any>): String {
val result = controlMedia("next")
return "{\"result\": \"${result.message}\"}"
}
}
)
Log.d(TAG, "已注册媒体下一首功能")
}
/**
* 控制媒体播放
* @param action 操作类型:"play", "pause", "previous", "next"
* @return 操作结果
*/
private fun controlMedia(action: String): Result {
Log.d(TAG, "媒体控制: $action")
try {
val intent = Intent()
when (action) {
"play" -> {
// 尝试发送播放广播
intent.action = "android.intent.action.MEDIA_PLAY"
context.sendBroadcast(intent)
// 也尝试使用媒体按键广播
val keyIntent = Intent(Intent.ACTION_MEDIA_BUTTON).apply {
putExtra(
Intent.EXTRA_KEY_EVENT,
android.view.KeyEvent(android.view.KeyEvent.ACTION_DOWN, android.view.KeyEvent.KEYCODE_MEDIA_PLAY)
)
flags = Intent.FLAG_RECEIVER_FOREGROUND
}
context.sendOrderedBroadcast(keyIntent, null)
return Result(
success = true,
message = "已开始播放媒体"
)
}
"pause" -> {
// 尝试发送暂停广播
intent.action = "android.intent.action.MEDIA_PAUSE"
context.sendBroadcast(intent)
// 也尝试使用媒体按键广播
val keyIntent = Intent(Intent.ACTION_MEDIA_BUTTON).apply {
putExtra(
Intent.EXTRA_KEY_EVENT,
android.view.KeyEvent(android.view.KeyEvent.ACTION_DOWN, android.view.KeyEvent.KEYCODE_MEDIA_PAUSE)
)
flags = Intent.FLAG_RECEIVER_FOREGROUND
}
context.sendOrderedBroadcast(keyIntent, null)
return Result(
success = true,
message = "已暂停媒体播放"
)
}
"previous" -> {
// 尝试发送上一首广播
intent.action = "android.intent.action.MEDIA_PREVIOUS"
context.sendBroadcast(intent)
// 也尝试使用媒体按键广播
val keyIntent = Intent(Intent.ACTION_MEDIA_BUTTON).apply {
putExtra(
Intent.EXTRA_KEY_EVENT,
android.view.KeyEvent(android.view.KeyEvent.ACTION_DOWN, android.view.KeyEvent.KEYCODE_MEDIA_PREVIOUS)
)
flags = Intent.FLAG_RECEIVER_FOREGROUND
}
context.sendOrderedBroadcast(keyIntent, null)
return Result(
success = true,
message = "已切换到上一首"
)
}
"next" -> {
// 尝试发送下一首广播
intent.action = "android.intent.action.MEDIA_NEXT"
context.sendBroadcast(intent)
// 也尝试使用媒体按键广播
val keyIntent = Intent(Intent.ACTION_MEDIA_BUTTON).apply {
putExtra(
Intent.EXTRA_KEY_EVENT,
android.view.KeyEvent(android.view.KeyEvent.ACTION_DOWN, android.view.KeyEvent.KEYCODE_MEDIA_NEXT)
)
flags = Intent.FLAG_RECEIVER_FOREGROUND
}
context.sendOrderedBroadcast(keyIntent, null)
return Result(
success = true,
message = "已切换到下一首"
)
}
else -> return Result(
success = false,
message = "未知的媒体控制操作: $action"
)
}
} catch (e: Exception) {
Log.e(TAG, "媒体控制失败: ${e.message}", e)
return Result(
success = false,
message = "媒体控制失败: ${e.message}"
)
}
}
/** /**
* 发送退出交互广播 * 发送退出交互广播
*/ */
private fun sendExitInteractionBroadcast() { private fun sendExitInteractionBroadcast() {

Loading…
Cancel
Save