Browse Source

add

newdev_shunjiawei
wolfplus 1 year ago
parent
commit
8bcc79cb27
  1. 10
      .mcp copy.json
  2. 9
      .mcp.json
  3. 7
      lib/core/bindings/initial_binding.dart
  4. 53
      lib/core/utils/logger.dart
  5. 554
      lib/data/services/ble_manager.dart
  6. 125
      lib/data/services/bluetooth_manager.dart
  7. 249
      lib/data/services/bluetooth_service.dart
  8. 9
      lib/modules/agent/bindings/agent_binding.dart
  9. 409
      lib/modules/agent/controllers/agent_controller.dart
  10. 350
      lib/modules/agent/views/agent_view.dart
  11. 832
      lib/modules/agent/views/message_bubble.dart
  12. 30
      lib/modules/home/controllers/home_controller.dart
  13. 10
      lib/modules/home/views/home_view.dart
  14. 47
      lib/modules/pairing/controllers/pairing_controller.dart
  15. 8
      lib/routes/app_pages.dart
  16. 1
      lib/routes/app_routes.dart
  17. 6
      local_plugins/agent_service/android/build.gradle.kts
  18. 4
      local_plugins/agent_service/android/settings.gradle.kts
  19. 399
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt
  20. 90
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt
  21. 285
      local_plugins/agent_service/lib/agent_service.dart
  22. 1
      local_plugins/agent_service/pubspec.yaml
  23. 2
      local_plugins/azure_speech/android/build.gradle.kts
  24. 383
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt
  25. 2
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt
  26. 105
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureTtsHelper.kt
  27. 15
      local_plugins/ble_service/android/build.gradle.kts
  28. 4
      local_plugins/ble_service/android/settings.gradle.kts
  29. 53
      local_plugins/ble_service/android/src/main/AndroidManifest.xml
  30. 4
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleConst.kt
  31. 232
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleForegroundService.kt
  32. 219
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleManagerWrapper.kt
  33. 535
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleMediaService.kt
  34. 237
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt
  35. 74
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleServicePlugin.kt
  36. 99
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleWakeReceiver.kt
  37. 4
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BootReceiver.kt
  38. 162
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/ChatAgent.kt
  39. 144
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/utils.kt
  40. BIN
      local_plugins/ble_service/android/src/main/res/raw/silence.mp3
  41. 103
      local_plugins/ble_service/lib/ble_service.dart
  42. 2
      local_plugins/bytedance_speech/android/build.gradle.kts
  43. 2
      local_plugins/classic_bluetooth/android/build.gradle.kts
  44. 4
      local_plugins/deepvoice_utils/android/build.gradle.kts
  45. 2
      local_plugins/jieli/android/build.gradle.kts
  46. 2
      local_plugins/jl_opus/android/build.gradle.kts
  47. 2
      local_plugins/open_ai_service/android/build.gradle.kts
  48. 76
      local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/MCPClient.kt
  49. 104
      local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/OpenAIService.kt
  50. 146
      local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/OpenAIServicePlugin.kt
  51. 459
      local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/SystemFunctionHandler.kt
  52. 2
      local_plugins/opus/android/build.gradle.kts
  53. 2
      local_plugins/spotify_sdk/android/build.gradle
  54. 2
      local_plugins/volcano_speech/android/build.gradle.kts
  55. 2
      pubspec.yaml
  56. 4
      test.json
  57. 101
      test2.json

10
.mcp copy.json

@ -0,0 +1,10 @@
{
"mcpServers": {
"web-search": {
"url": "http://mcp.ideapsound.com:8000/sse"
},
"amap-amap-sse": {
"url": "https://mcp.amap.com/sse?key=df7b0b6747c632fc38ddad22e7176646"
}
}
}

9
.mcp.json

@ -1,13 +1,10 @@
{
"mcpServers": {
"deepsound-mcp": {
"url": "https://api.deapsound.com/sse"
},
"voitrans-mcp": {
"url": "https://www.voitrans.net/sse"
},
"web-search": {
"url": "http://mcp.ideapsound.com:8000/sse"
},
"mp3": {
"url": "https://api.deapsound.com/sse"
}
}
}

7
lib/core/bindings/initial_binding.dart

@ -6,7 +6,8 @@ import '../../data/services/language_manager.dart';
import '../../data/services/volcano_ai_service.dart';
import '../../core/utils/logger.dart';
import '../../data/services/open_ai_service_adapter.dart';
import '../../data/services/bluetooth_service.dart';
import '../../data/services/bluetooth_manager.dart';
import '../../data/services/ble_manager.dart';
/// 初始绑定,用于管理全局依赖
class InitialBinding extends Bindings {
@override
@ -36,7 +37,9 @@ class InitialBinding extends Bindings {
// 蓝牙服务
Get.put<BluetoothService>(BluetoothService(), permanent: true);
Get.put<BluetoothManager>(BluetoothManager(), permanent: true);
// BleManager
Get.put<BleManager>(BleManager(), permanent: true);
// 火山AI服务
Get.lazyPut<VolcanoAIService>(() => VolcanoAIService(), fenix: true);

53
lib/core/utils/logger.dart

@ -21,42 +21,71 @@ class Logger {
}
/// 输出调试日志
static void debug(String message) {
static void debug(String message, {String? tag}) {
if (_currentLevel.index <= LogLevel.debug.index) {
_log('DEBUG', message);
_log('DEBUG', message, tag: tag);
}
}
/// 输出信息日志
static void info(String message) {
static void info(String message, {String? tag}) {
if (_currentLevel.index <= LogLevel.info.index) {
_log('INFO', message);
_log('INFO', message, tag: tag);
}
}
/// 输出警告日志
static void warning(String message) {
static void warning(String message, {String? tag}) {
if (_currentLevel.index <= LogLevel.warning.index) {
_log('WARNING', message);
_log('WARNING', message, tag: tag);
}
}
/// 输出错误日志
static void error(String message, [dynamic error, StackTrace? stackTrace]) {
static void error(String message, [dynamic error, StackTrace? stackTrace, String? tag]) {
if (_currentLevel.index <= LogLevel.error.index) {
_log('ERROR', message);
_log('ERROR', message, tag: tag);
if (error != null) {
_log('ERROR', 'Original error: $error');
_log('ERROR', 'Original error: $error', tag: tag);
}
if (stackTrace != null) {
_log('ERROR', 'Stack trace: $stackTrace');
_log('ERROR', 'Stack trace: $stackTrace', tag: tag);
}
}
}
/// 获取当前调用栈的类名
static String? _getCallerClassName() {
try {
final stackTrace = StackTrace.current.toString();
final frames = stackTrace.split('\n');
// 查找调用者帧 (跳过Logger框架内部的帧)
for (int i = 0; i < frames.length; i++) {
final frame = frames[i].trim();
if (frame.contains('Logger.') || frame.contains('_log')) {
continue;
}
// 简单的解析类名
final regex = RegExp(r'#\d+\s+([^\.]+)\.([^\.]+)\s');
final match = regex.firstMatch(frame);
if (match != null && match.groupCount >= 1) {
return match.group(1);
}
break;
}
} catch (e) {
// 忽略解析错误
}
return null;
}
/// 内部日志输出方法
static void _log(String level, String message) {
static void _log(String level, String message, {String? tag}) {
final timestamp = DateTime.now().toString();
developer.log('[$timestamp] $level: $message');
final className = tag ?? _getCallerClassName() ?? '';
final tagInfo = className.isNotEmpty ? '[$className] ' : '';
developer.log('[$timestamp][$level][$tagInfo]: $message');
}
}

554
lib/data/services/ble_manager.dart

@ -0,0 +1,554 @@
import 'dart:async';
import 'package:get/get.dart';
import 'package:ble_service/ble_service.dart';
import 'package:deep_voice/core/utils/logger.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:get_storage/get_storage.dart';
import 'package:deep_voice/data/models/appconfig_model.dart';
import 'dart:convert';
import 'package:flutter/services.dart' show rootBundle;
/// 命令类型常量(与原生层保持一致)
class BleCommands {
static const int GET_VERSION = 0x01;
static const int GET_BATTERY_INFO = 0x02;
static const int CONTROL_CODEC = 0x03;
static const int PLAY_A2DP = 0x04;
}
/// 蓝牙设备信息缓存模型
class DeviceInfo {
final String address;
final String name;
// 整体电池电量
final int batteryLevel;
// 详细电池信息
final int leftEarBattery;
final int rightEarBattery;
final int caseBattery;
// 充电状态
final bool leftEarCharging;
final bool rightEarCharging;
final bool caseCharging;
final String firmwareVersion;
final DateTime lastUpdated;
DeviceInfo({
required this.address,
required this.name,
this.batteryLevel = 0,
this.leftEarBattery = 0,
this.rightEarBattery = 0,
this.caseBattery = 0,
this.leftEarCharging = false,
this.rightEarCharging = false,
this.caseCharging = false,
this.firmwareVersion = '',
DateTime? lastUpdated,
}) : lastUpdated = lastUpdated ?? DateTime.now();
/// 创建更新后的设备信息副本
DeviceInfo copyWith({
String? address,
String? name,
int? batteryLevel,
int? leftEarBattery,
int? rightEarBattery,
int? caseBattery,
bool? leftEarCharging,
bool? rightEarCharging,
bool? caseCharging,
String? firmwareVersion,
}) {
return DeviceInfo(
address: address ?? this.address,
name: name ?? this.name,
batteryLevel: batteryLevel ?? this.batteryLevel,
leftEarBattery: leftEarBattery ?? this.leftEarBattery,
rightEarBattery: rightEarBattery ?? this.rightEarBattery,
caseBattery: caseBattery ?? this.caseBattery,
leftEarCharging: leftEarCharging ?? this.leftEarCharging,
rightEarCharging: rightEarCharging ?? this.rightEarCharging,
caseCharging: caseCharging ?? this.caseCharging,
firmwareVersion: firmwareVersion ?? this.firmwareVersion,
lastUpdated: DateTime.now(),
);
}
@override
String toString() {
return 'DeviceInfo(address: $address, name: $name, '
'battery: $batteryLevel%, L: $leftEarBattery%, R: $rightEarBattery%, Case: $caseBattery%, '
'charging: L:$leftEarCharging, R:$rightEarCharging, C:$caseCharging, '
'version: $firmwareVersion)';
}
}
/// BLE管理器 - 专门处理BLE设备连接和信息管理
class BleManager extends GetxService {
static BleManager get to => Get.find<BleManager>();
// BLE服务实例
final BleService _bleService = BleService();
// BLE连接状态
final _connectionState = BleConnectionState.disconnected.obs;
int get connectionState => _connectionState.value;
// 连接状态监听
final _isConnected = false.obs;
bool get isConnected => _isConnected.value;
// 设备信息缓存
final Rx<DeviceInfo?> _deviceInfo = Rx<DeviceInfo?>(null);
DeviceInfo? get deviceInfo => _deviceInfo.value;
// 订阅
StreamSubscription<dynamic>? _statusSubscription;
StreamSubscription<dynamic>? _dataSubscription;
final GetStorage _storage = GetStorage();
/// 初始化
@override
void onInit() {
super.onInit();
Logger.info('BLE管理器初始化');
// 设置BLE事件监听
_setupBleListener();
}
/// 释放资源
@override
void onClose() {
_statusSubscription?.cancel();
_dataSubscription?.cancel();
super.onClose();
}
/// 设置BLE事件监听
void _setupBleListener() {
_statusSubscription?.cancel();
_dataSubscription?.cancel();
try {
// 监听BLE状态更新
_statusSubscription = _bleService.onStatusUpdated.listen(
(event) {
Logger.info('接收到BLE状态更新: $event');
if (event is Map) {
// 处理连接状态更新
if (event.containsKey('connectionState')) {
final state = event['connectionState'] as int;
_connectionState.value = state;
// 连接状态转换
if (state == BleConnectionState.connected) {
Logger.info('BLE设备已连接');
_isConnected.value = true;
// 在连接成功后获取设备信息
_fetchDeviceInfo();
} else if (state == BleConnectionState.disconnected ||
state == BleConnectionState.error) {
Logger.info('BLE设备已断开或发生错误');
_isConnected.value = false;
}
}
// 处理设备信息
_processDeviceInfo(event);
}
},
onError: (error) {
Logger.error('BLE状态事件流错误: $error');
},
);
// 监听BLE数据流
_dataSubscription = _bleService.onDataReceived.listen(
(data) {
Logger.info('接收到BLE数据: $data');
// 在这里处理来自设备的数据
},
onError: (error) {
Logger.error('BLE数据流错误: $error');
},
);
} catch (e) {
Logger.error('设置BLE事件监听器失败: $e');
}
}
/// 处理来自原生层的设备信息
void _processDeviceInfo(Map<dynamic, dynamic> event) {
try {
// 检查是否是设备信息响应
if (event.containsKey('command') && event.containsKey('success')) {
final bool success = event['success'] as bool;
if (!success) {
Logger.warning('设备命令执行失败: ${event['command']}');
return;
}
// 根据命令类型处理不同信息
final command = event['command'];
if (command is int) {
// 基于命令ID处理
_processDeviceInfoByCommandId(command, event);
} else if (command is String) {
// 基于命令名称处理
_processDeviceInfoByCommandName(command, event);
}
}
} catch (e) {
Logger.error('处理设备信息异常: $e');
}
}
/// 根据命令ID处理设备信息
void _processDeviceInfoByCommandId(
int commandId, Map<dynamic, dynamic> data) {
switch (commandId) {
case BleCommands.GET_BATTERY_INFO:
_updateBatteryInfoFromData(data);
break;
case BleCommands.GET_VERSION:
_updateVersionInfoFromData(data);
break;
default:
Logger.info('未处理的命令ID: $commandId');
}
}
/// 根据命令名称处理设备信息
void _processDeviceInfoByCommandName(
String commandName, Map<dynamic, dynamic> data) {
switch (commandName) {
case 'batteryInfo':
_updateBatteryInfoFromData(data);
break;
case 'versionInfo':
_updateVersionInfoFromData(data);
break;
default:
Logger.info('未处理的命令名称: $commandName');
}
}
/// 更新电池信息
void _updateBatteryInfoFromData(Map<dynamic, dynamic> data) {
try {
// 确保数据中包含电池信息
if (!data.containsKey('leftEarBattery') ||
!data.containsKey('rightEarBattery') ||
!data.containsKey('caseBattery')) {
Logger.warning('电池信息数据不完整');
return;
}
// 提取电池信息
final int leftBattery = data['leftEarBattery'] as int? ?? 0;
final int rightBattery = data['rightEarBattery'] as int? ?? 0;
final int caseBattery = data['caseBattery'] as int? ?? 0;
final bool leftCharging = data['leftEarCharging'] as bool? ?? false;
final bool rightCharging = data['rightEarCharging'] as bool? ?? false;
final bool caseCharging = data['caseCharging'] as bool? ?? false;
// 获取整体电量(原生层使用左耳电量作为主要指示)
final int overallBattery = data['batteryLevel'] as int? ?? leftBattery;
// 更新设备信息
_updateDeviceInfoIfNeeded(
batteryLevel: overallBattery,
leftEarBattery: leftBattery,
rightEarBattery: rightBattery,
caseBattery: caseBattery,
leftEarCharging: leftCharging,
rightEarCharging: rightCharging,
caseCharging: caseCharging,
);
Logger.info(
'设备电池信息已更新: 电量=$overallBattery%, 左耳=$leftBattery%, 右耳=$rightBattery%, 充电仓=$caseBattery%');
} catch (e) {
Logger.error('更新电池信息失败: $e');
}
}
/// 更新版本信息
void _updateVersionInfoFromData(Map<dynamic, dynamic> data) {
try {
// 确保数据中包含版本信息
if (!data.containsKey('version')) {
Logger.warning('版本信息数据不完整');
return;
}
// 提取版本信息
final String version = data['version'] as String? ?? '';
// 更新设备信息
_updateDeviceInfoIfNeeded(
firmwareVersion: version,
);
Logger.info('设备版本信息已更新: 版本=$version');
} catch (e) {
Logger.error('更新版本信息失败: $e');
}
}
/// 更新设备信息(如果已存在设备)
void _updateDeviceInfoIfNeeded({
int? batteryLevel,
int? leftEarBattery,
int? rightEarBattery,
int? caseBattery,
bool? leftEarCharging,
bool? rightEarCharging,
bool? caseCharging,
String? firmwareVersion,
}) {
// 如果设备信息已存在,则更新
if (_deviceInfo.value != null) {
_deviceInfo.value = _deviceInfo.value!.copyWith(
batteryLevel: batteryLevel,
leftEarBattery: leftEarBattery,
rightEarBattery: rightEarBattery,
caseBattery: caseBattery,
leftEarCharging: leftEarCharging,
rightEarCharging: rightEarCharging,
caseCharging: caseCharging,
firmwareVersion: firmwareVersion,
);
}
// 如果设备信息不存在,但有地址和名称,则创建新的设备信息
else if (_deviceInfo.value == null &&
_isConnected.value &&
(firmwareVersion != null || batteryLevel != null)) {
// 提示尝试创建设备信息但缺少必要信息
Logger.warning('尝试更新设备信息,但设备基本信息不存在');
}
}
/// 连接成功后获取设备信息
Future<void> _fetchDeviceInfo() async {
Logger.info('开始获取BLE设备详细信息');
try {
// 获取电池信息
final batteryResult = await _bleService.getBatteryInfo();
if (batteryResult) {
Logger.info('电池信息获取指令已发送');
} else {
Logger.warning('电池信息获取指令发送失败');
}
// 获取设备版本信息
final versionResult = await _bleService.getDeviceVersion();
if (versionResult) {
Logger.info('版本信息获取指令已发送');
} else {
Logger.warning('版本信息获取指令发送失败');
}
} catch (e) {
Logger.error('获取BLE设备信息失败: $e');
}
}
/// 检查BLE连接状态
Future<int> getConnectionState() async {
try {
final isConnected = await _bleService.isConnected();
_connectionState.value = isConnected
? BleConnectionState.connected
: BleConnectionState.disconnected;
_isConnected.value = isConnected;
return _connectionState.value;
} catch (e) {
Logger.error('获取BLE连接状态失败: $e');
return BleConnectionState.error;
}
}
/// 启动BLE服务
Future<bool> startBleService() async {
try {
// 从.env文件直接读取环境变量
final azureSpeechKey = dotenv.env['AZURE_SPEECH_KEY'] ?? '';
final azureSpeechRegion = dotenv.env['AZURE_SPEECH_REGION'] ?? 'eastasia';
final openaiApiKey = dotenv.env['OPENAI_API_KEY'] ?? '';
final openaiBaseUrl =
dotenv.env['OPENAI_BASE_URL'] ?? 'https://api.openai.com/v1';
final openaiModel = dotenv.env['OPENAI_MODEL'] ?? 'gpt-3.5-turbo';
// 从Flutter资源包中加载.mcp.json
final mcpConfig = await rootBundle.loadString('.mcp.json');
// 验证JSON格式
final jsonData = jsonDecode(mcpConfig);
final config = <String, dynamic>{
// 语音代理配置参数
'azureSpeechKey': azureSpeechKey,
'azureSpeechRegion': azureSpeechRegion,
'openaiApiKey': openaiApiKey,
'openaiBaseUrl': openaiBaseUrl,
'openaiModel': openaiModel,
'mcpServer': jsonEncode(jsonData),
};
// 记录日志(不记录敏感信息)
Logger.info('启动BLE服务,配置参数键名: ${config}');
if (azureSpeechKey.isEmpty || openaiApiKey.isEmpty) {
Logger.warning('缺少必要的API密钥,请检查.env文件配置');
// 检查.env中相关密钥是否存在
if (azureSpeechKey.isEmpty) {
Logger.warning('AZURE_SPEECH_KEY未配置在.env文件中');
}
if (openaiApiKey.isEmpty) {
Logger.warning('OPENAI_API_KEY未配置在.env文件中');
}
}
final result = await _bleService.startBleService(config);
if (result) {
Logger.info('BLE服务启动成功');
// 添加延迟以等待设备连接
await Future.delayed(const Duration(seconds: 2));
// 检查连接状态
final isConnected = await _bleService.isConnected();
if (isConnected) {
_isConnected.value = true;
_connectionState.value = BleConnectionState.connected;
// 如果设备信息存在,则获取详细信息
if (_deviceInfo.value != null) {
// 获取详细设备信息
_fetchDeviceInfo();
}
}
} else {
Logger.error('BLE服务启动失败');
}
return result;
} catch (e) {
Logger.error('启动BLE服务失败: $e');
return false;
}
}
/// 停止BLE服务
Future<bool> stopBleService() async {
try {
final result = await _bleService.stopBleService();
if (result) {
Logger.info('BLE服务停止成功');
_isConnected.value = false;
_connectionState.value = BleConnectionState.disconnected;
} else {
Logger.error('BLE服务停止失败');
}
return result;
} catch (e) {
Logger.error('停止BLE服务失败: $e');
return false;
}
}
registerBackgroundScan() {
_bleService.registerBackgroundScan();
}
/// 获取电池电量
Future<int> getBatteryLevel() async {
try {
return await _bleService.getBatteryLevel();
} catch (e) {
Logger.error('获取电池电量失败: $e');
return 0;
}
}
/// 主动获取电池信息
Future<bool> fetchBatteryInfo() async {
try {
return await _bleService.getBatteryInfo();
} catch (e) {
Logger.error('获取电池信息失败: $e');
return false;
}
}
/// 主动获取设备版本信息
Future<bool> fetchDeviceVersion() async {
try {
return await _bleService.getDeviceVersion();
} catch (e) {
Logger.error('获取设备版本失败: $e');
return false;
}
}
/// 打开编码器
Future<bool> openEncoder() async {
try {
return await _bleService.openEncoder();
} catch (e) {
Logger.error('打开编码器失败: $e');
return false;
}
}
/// 打开解码器
Future<bool> openDecoder() async {
try {
return await _bleService.openDecoder();
} catch (e) {
Logger.error('打开解码器失败: $e');
return false;
}
}
/// 关闭编解码器
Future<bool> closeCodec() async {
try {
return await _bleService.closeCodec();
} catch (e) {
Logger.error('关闭编解码器失败: $e');
return false;
}
}
/// 播放A2DP
Future<bool> playA2DP() async {
try {
return await _bleService.playA2DP();
} catch (e) {
Logger.error('播放A2DP失败: $e');
return false;
}
}
/// 注册服务
static Future<BleManager> init() async {
if (!Get.isRegistered<BleManager>()) {
Get.put(BleManager(), permanent: true);
}
return Future.value(Get.find<BleManager>());
}
}

125
lib/data/services/bluetooth_manager.dart

@ -0,0 +1,125 @@
import 'dart:async';
import 'package:get/get.dart';
import 'package:classic_bluetooth/classic_bluetooth.dart';
import 'package:deep_voice/core/utils/logger.dart';
/// 蓝牙服务 - 管理经典蓝牙服务
class BluetoothManager extends GetxService {
static BluetoothManager get to => Get.find<BluetoothManager>();
// 蓝牙状态
final _bluetoothState = 'unknown'.obs;
String get bluetoothState => _bluetoothState.value;
// 当前经典蓝牙设备
final Rx<BluetoothDevice?> currentDeviceRx = Rx<BluetoothDevice?>(null);
BluetoothDevice? get currentDevice => currentDeviceRx.value;
// 订阅
StreamSubscription<BluetoothEvent>? _bluetoothSubscription;
/// 初始化
@override
void onInit() {
super.onInit();
Logger.info('蓝牙服务初始化');
// 设置蓝牙事件监听
_setupBluetoothListener();
}
/// 释放资源
@override
void onClose() {
_bluetoothSubscription?.cancel();
super.onClose();
}
/// 设置经典蓝牙事件监听
void _setupBluetoothListener() {
_bluetoothSubscription?.cancel();
try {
_bluetoothSubscription = ClassicBluetooth.events.listen(
(event) {
if (event.type == 'stateChanged' && event.state != null) {
_bluetoothState.value = event.state!;
Logger.info('蓝牙状态变更: ${event.state}');
// 如果蓝牙关闭,清除设备
if (event.state == 'off') {
currentDeviceRx.value = null;
}
} else if (event.type == 'deviceConnected' && event.device != null) {
// 直接从事件中获取设备信息
final newDevice = event.device!;
// 更新当前设备
currentDeviceRx.value = newDevice;
Logger.info('经典蓝牙设备已连接: ${newDevice.name} (${newDevice.address})');
} else if (event.type == 'deviceDisconnected' && event.device != null) {
// 直接从事件中获取设备信息
final disconnectedDevice = event.device!;
Logger.info('经典蓝牙设备已断开: ${disconnectedDevice.name} (${disconnectedDevice.address})');
// 如果断开的是当前设备,清除当前设备
if (currentDeviceRx.value?.address == disconnectedDevice.address) {
currentDeviceRx.value = null;
Logger.info('已清除当前设备引用');
}
}
},
onError: (error) {
Logger.error('蓝牙事件流错误: $error');
},
);
} catch (e) {
Logger.error('设置蓝牙事件监听器失败: $e');
}
}
/// 刷新设备连接状态
Future<void> refreshDeviceConnection() async {
try {
// 如果BLE未连接,则检查经典蓝牙连接
final devices = await ClassicBluetooth.getConnectedDevices();
if (devices.isNotEmpty) {
final newDevice = devices.first;
// 更新当前设备
currentDeviceRx.value = newDevice;
Logger.info('已连接经典蓝牙设备: ${newDevice.name} (${newDevice.address})');
return;
}
// 没有任何连接
currentDeviceRx.value = null;
Logger.info('未找到已连接的蓝牙设备');
} catch (e) {
Logger.error('刷新设备连接失败: $e');
}
}
/// 检查蓝牙是否已启用
Future<bool> isBluetoothEnabled() async {
try {
final isEnabled = await ClassicBluetooth.isBluetoothEnabled();
_bluetoothState.value = isEnabled ? 'on' : 'off';
return isEnabled;
} catch (e) {
Logger.error('检查蓝牙状态失败: $e');
return false;
}
}
}

249
lib/data/services/bluetooth_service.dart

@ -1,249 +0,0 @@
import 'dart:async';
import 'dart:math' as math;
import 'package:get/get.dart';
import 'package:classic_bluetooth/classic_bluetooth.dart';
import 'package:deep_voice/core/utils/logger.dart';
import 'package:flutter/services.dart';
/// 蓝牙设备信息缓存模型
class DeviceInfo {
final String address;
final String name;
// 整体电池电量
final int batteryLevel;
// 详细电池信息
final int leftEarBattery;
final int rightEarBattery;
final int caseBattery;
// 充电状态
final bool leftEarCharging;
final bool rightEarCharging;
final bool caseCharging;
final String firmwareVersion;
final String productId;
final String colorId;
final DateTime lastUpdated;
DeviceInfo({
required this.address,
required this.name,
this.batteryLevel = 0,
this.leftEarBattery = 0,
this.rightEarBattery = 0,
this.caseBattery = 0,
this.leftEarCharging = false,
this.rightEarCharging = false,
this.caseCharging = false,
this.firmwareVersion = '',
this.productId = '',
this.colorId = '',
DateTime? lastUpdated,
}) : lastUpdated = lastUpdated ?? DateTime.now();
/// 创建更新后的设备信息副本
DeviceInfo copyWith({
String? address,
String? name,
int? batteryLevel,
int? leftEarBattery,
int? rightEarBattery,
int? caseBattery,
bool? leftEarCharging,
bool? rightEarCharging,
bool? caseCharging,
String? firmwareVersion,
String? productId,
String? colorId,
}) {
return DeviceInfo(
address: address ?? this.address,
name: name ?? this.name,
batteryLevel: batteryLevel ?? this.batteryLevel,
leftEarBattery: leftEarBattery ?? this.leftEarBattery,
rightEarBattery: rightEarBattery ?? this.rightEarBattery,
caseBattery: caseBattery ?? this.caseBattery,
leftEarCharging: leftEarCharging ?? this.leftEarCharging,
rightEarCharging: rightEarCharging ?? this.rightEarCharging,
caseCharging: caseCharging ?? this.caseCharging,
firmwareVersion: firmwareVersion ?? this.firmwareVersion,
productId: productId ?? this.productId,
colorId: colorId ?? this.colorId,
lastUpdated: DateTime.now(),
);
}
@override
String toString() {
return 'DeviceInfo(address: $address, name: $name, '
'battery: $batteryLevel%, L: $leftEarBattery%, R: $rightEarBattery%, Case: $caseBattery%, '
'charging: L:$leftEarCharging, R:$rightEarCharging, C:$caseCharging, '
'version: $firmwareVersion, productId: $productId, colorId: $colorId)';
}
}
/// 蓝牙服务 - 管理经典蓝牙服务,提供设备信息缓存
class BluetoothService extends GetxService {
static BluetoothService get to => Get.find<BluetoothService>();
// 蓝牙状态
final _bluetoothState = 'unknown'.obs;
String get bluetoothState => _bluetoothState.value;
// 当前经典蓝牙设备
final Rx<BluetoothDevice?> currentDeviceRx = Rx<BluetoothDevice?>(null);
BluetoothDevice? get currentDevice => currentDeviceRx.value;
// 设备信息缓存
final Rx<DeviceInfo?> _deviceInfo = Rx<DeviceInfo?>(null);
DeviceInfo? get deviceInfo => _deviceInfo.value;
// 表明当前连接类型,本服务只处理经典蓝牙
bool get isBleConnection => false;
// 订阅
StreamSubscription<BluetoothEvent>? _bluetoothSubscription;
/// 初始化
@override
void onInit() {
super.onInit();
Logger.info('蓝牙服务初始化');
// 设置蓝牙事件监听
_setupBluetoothListener();
}
/// 释放资源
@override
void onClose() {
_bluetoothSubscription?.cancel();
super.onClose();
}
/// 设置经典蓝牙事件监听
void _setupBluetoothListener() {
_bluetoothSubscription?.cancel();
try {
_bluetoothSubscription = ClassicBluetooth.events.listen(
(event) {
if (event.type == 'stateChanged' && event.state != null) {
_bluetoothState.value = event.state!;
Logger.info('蓝牙状态变更: ${event.state}');
// 如果蓝牙关闭,清除设备
if (event.state == 'off') {
currentDeviceRx.value = null;
_deviceInfo.value = null;
}
} else if (event.type == 'deviceConnected' && event.device != null) {
// 直接从事件中获取设备信息
final newDevice = event.device!;
// 更新当前设备
currentDeviceRx.value = newDevice;
Logger.info('经典蓝牙设备已连接: ${newDevice.name} (${newDevice.address})');
// 创建基本设备信息
if (_deviceInfo.value == null || _deviceInfo.value!.address != newDevice.address) {
_deviceInfo.value = DeviceInfo(
address: newDevice.address,
name: newDevice.name,
);
Logger.info('创建设备信息缓存: ${_deviceInfo.value}');
}
} else if (event.type == 'deviceDisconnected' && event.device != null) {
// 直接从事件中获取设备信息
final disconnectedDevice = event.device!;
Logger.info('经典蓝牙设备已断开: ${disconnectedDevice.name} (${disconnectedDevice.address})');
// 如果断开的是当前设备,清除当前设备
if (currentDeviceRx.value?.address == disconnectedDevice.address) {
currentDeviceRx.value = null;
// 不清除设备信息缓存,保留最后状态
Logger.info('已清除当前设备引用,保留最后设备信息缓存');
}
}
},
onError: (error) {
Logger.error('蓝牙事件流错误: $error');
},
);
} catch (e) {
Logger.error('设置蓝牙事件监听器失败: $e');
}
}
/// 刷新设备连接状态
Future<void> refreshDeviceConnection() async {
try {
// 检查经典蓝牙连接
final devices = await ClassicBluetooth.getConnectedDevices();
if (devices.isNotEmpty) {
final newDevice = devices.first;
// 更新当前设备
currentDeviceRx.value = newDevice;
Logger.info('已连接经典蓝牙设备: ${newDevice.name} (${newDevice.address})');
// 创建基本设备信息
if (_deviceInfo.value == null || _deviceInfo.value!.address != newDevice.address) {
_deviceInfo.value = DeviceInfo(
address: newDevice.address,
name: newDevice.name,
);
Logger.info('创建设备信息缓存: ${_deviceInfo.value}');
}
return;
}
// 没有任何连接
currentDeviceRx.value = null;
Logger.info('未找到已连接的蓝牙设备');
} catch (e) {
Logger.error('刷新设备连接失败: $e');
}
}
/// 检查蓝牙是否已启用
Future<bool> isBluetoothEnabled() async {
try {
final isEnabled = await ClassicBluetooth.isBluetoothEnabled();
_bluetoothState.value = isEnabled ? 'on' : 'off';
return isEnabled;
} catch (e) {
Logger.error('检查蓝牙状态失败: $e');
return false;
}
}
/// 启动BLE服务(不适用于此服务,仅为接口兼容)
Future<bool> startBleService(Map<String, dynamic> config) async {
Logger.warning('BluetoothService不支持BLE服务,请使用BleService');
return false;
}
/// 停止BLE服务(不适用于此服务,仅为接口兼容)
Future<bool> stopBleService() async {
Logger.warning('BluetoothService不支持BLE服务,请使用BleService');
return false;
}
/// 注册服务
static Future<BluetoothService> init() async {
if (!Get.isRegistered<BluetoothService>()) {
Get.put(BluetoothService(), permanent: true);
}
return Future.value(Get.find<BluetoothService>());
}
}

9
lib/modules/agent/bindings/agent_binding.dart

@ -0,0 +1,9 @@
import 'package:get/get.dart';
import '../controllers/agent_controller.dart';
class AgentBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<AgentController>(() => AgentController());
}
}

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

@ -0,0 +1,409 @@
import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:agent_service/agent_service.dart';
import 'package:logger/logger.dart';
import 'package:chat_storage/chat_storage.dart';
class Message {
final bool isUser;
final String text;
final DateTime timestamp;
final bool isRecognizing; // 是否为语音识别中的临时消息
Message({
required this.isUser,
required this.text,
DateTime? timestamp,
this.isRecognizing = false,
}) : this.timestamp = timestamp ?? DateTime.now();
}
class AgentController extends GetxController {
static AgentController get to => Get.find();
final logger = Logger();
// 聊天消息列表
final messages = <Message>[].obs;
// 控制器
final TextEditingController textController = TextEditingController();
final ScrollController scrollController = ScrollController();
// 状态标志
final isInitialized = false.obs;
final isListening = false.obs;
final isSpeaking = false.obs;
final isProcessing = false.obs;
// 输入模式控制
final isTextInputMode = true.obs;
// 当前输入的文本内容
final currentText = ''.obs;
// 流事件订阅
StreamSubscription<AgentServiceEvent>? _eventSubscription;
// 用于跟踪AI回复状态
bool _isNewAssistantResponse = true;
String _currentResponseId = '';
// 会话ID
final String sessionId = 'default_agent';
// 是否已加载聊天记录
final isHistoryLoaded = false.obs;
@override
void onInit() {
super.onInit();
_loadChatHistory();
_subscribeToEvents();
}
@override
void onClose() {
AgentService.stopConversation();
AgentService.stopTts();
textController.dispose();
scrollController.dispose();
_eventSubscription?.cancel();
super.onClose();
}
// 加载聊天历史记录
Future<void> _loadChatHistory() async {
try {
logger.i('正在加载聊天历史...');
final result = await ChatStorage.getMessages(
sessionId: sessionId,
page: 1,
pageSize: 20,
);
if (result.messages.isNotEmpty) {
// 转换并添加消息到列表
final historyMessages = result.messages.map((msg) => Message(
isUser: msg.sender == 'user',
text: msg.message,
timestamp: DateTime.parse(msg.timestamp),
)).toList();
// 消息按时间排序,最新的消息在底部
historyMessages.sort((a, b) => a.timestamp.compareTo(b.timestamp));
messages.assignAll(historyMessages);
logger.i('成功加载 ${historyMessages.length} 条聊天记录');
} else {
// 没有历史记录时添加欢迎消息
_addWelcomeMessage();
logger.i('没有历史记录,添加欢迎消息');
}
} catch (e) {
logger.e('加载聊天历史失败: $e');
_addWelcomeMessage();
} finally {
isHistoryLoaded.value = true;
messages.refresh();
// 在加载完成后直接定位到底部,无需动画
WidgetsBinding.instance.addPostFrameCallback((_) {
scrollController.jumpTo(scrollController.position.maxScrollExtent);
});
}
}
// 添加欢迎消息
void _addWelcomeMessage() {
messages.add(Message(
isUser: false,
text: '你好!我是DeepVoice AI助手,我能帮你:\n\n'
'• 回答问题和提供信息\n'
'• 实时语音翻译\n'
'• 推荐音乐和播客\n'
'• 语音备忘录和提醒\n'
'• 控制耳机功能\n\n'
'有什么我可以帮你的吗?',
));
}
// 订阅代理服务事件
void _subscribeToEvents() {
_eventSubscription = AgentService.events.listen((event) {
switch (event.type) {
case AgentServiceEventType.recognitionStarted:
isListening.value = true;
isTextInputMode.value = false;
isTextInputMode.refresh();
// 只标记识别开始,不添加消息
break;
case AgentServiceEventType.recognitionStopped:
isListening.value = false;
isTextInputMode.value = true;
isTextInputMode.refresh();
break;
case AgentServiceEventType.recognizing:
final text = event.data['text'] ?? '';
currentText.value = text; // 保留当前文本,以便其他地方使用
if (text.isNotEmpty) {
// 查找是否有正在识别中的消息
final index = messages.lastIndexWhere((msg) => msg.isRecognizing && msg.isUser);
if (index >= 0) {
// 更新已有的识别中消息
messages[index] = Message(
isUser: true,
text: text,
isRecognizing: true,
timestamp: messages[index].timestamp,
);
} else {
// 没有识别中的消息,创建新消息
messages.add(Message(
isUser: true,
text: text,
isRecognizing: true,
));
}
messages.refresh();
_scrollToBottom();
}
break;
case AgentServiceEventType.recognitionResult:
final text = event.data['text'] ?? '';
// 查找是否有正在识别中的消息
final index = messages.lastIndexWhere((msg) => msg.isRecognizing && msg.isUser);
if (text.isNotEmpty) {
if (index >= 0) {
// 将识别中的消息转换为最终消息
messages[index] = Message(
isUser: true,
text: text,
timestamp: messages[index].timestamp,
);
messages.refresh();
} else {
// 如果没有找到识别中的消息,添加新消息
messages.add(Message(isUser: true, text: text));
}
isProcessing.value = true;
// 标记为新的AI回复
_isNewAssistantResponse = true;
// 滚动到底部
_scrollToBottom();
} else if (index >= 0) {
// 如果识别结果为空,移除临时的识别消息
messages.removeAt(index);
messages.refresh();
}
break;
case AgentServiceEventType.ttsStarted:
isSpeaking.value = true;
break;
case AgentServiceEventType.ttsCompleted:
case AgentServiceEventType.ttsStopped:
case AgentServiceEventType.ttsCanceled:
isSpeaking.value = false;
break;
case AgentServiceEventType.assistantToken:
if (!isProcessing.value) isProcessing.value = true;
final token = event.data['token'] ?? '';
final responseId = event.data['responseId'] ?? '';
if (token.isNotEmpty) {
// 如果是新的回复或者响应ID改变,创建新消息
if (_isNewAssistantResponse || (_currentResponseId != responseId && responseId.isNotEmpty)) {
messages.add(Message(isUser: false, text: token));
_isNewAssistantResponse = false;
_currentResponseId = responseId;
} else {
// 否则更新最后一条消息
final lastIndex = messages.length - 1;
if (lastIndex >= 0 && !messages[lastIndex].isUser) {
messages[lastIndex] = Message(
isUser: false,
text: messages[lastIndex].text + token,
timestamp: messages[lastIndex].timestamp,
);
}
}
// 滚动到底部
_scrollToBottom();
}
break;
case AgentServiceEventType.assistantResponse:
isProcessing.value = false;
// 标记当前回复完成
_isNewAssistantResponse = true;
break;
case AgentServiceEventType.error:
isListening.value = false;
isSpeaking.value = false;
isProcessing.value = false;
// 移除临时的识别消息
final index = messages.lastIndexWhere((msg) => msg.isRecognizing && msg.isUser);
if (index >= 0) {
messages.removeAt(index);
messages.refresh();
}
logger.e('代理服务错误: ${event.data['message']}');
break;
case AgentServiceEventType.autoStop:
isListening.value = false;
// 移除临时的识别消息
final index = messages.lastIndexWhere((msg) => msg.isRecognizing && msg.isUser);
if (index >= 0) {
// 只有当消息为空或文本太短时才移除
if (messages[index].text.trim().length < 2) {
messages.removeAt(index);
messages.refresh();
} else {
// 否则转换为正式消息
messages[index] = Message(
isUser: true,
text: messages[index].text,
timestamp: messages[index].timestamp,
);
messages.refresh();
// 发送此消息到AI处理
isProcessing.value = true;
_isNewAssistantResponse = true;
AgentService.processTextInput(messages[index].text, speakResponse: true);
}
}
break;
case AgentServiceEventType.functionCall:
// final functionName = event.data['name'] ?? '';
// final arguments = event.data['arguments'] ?? '';
// logger.i('mcp: $functionName, $arguments');
break;
case AgentServiceEventType.functionCallResult:
final result = event.data['result'] ?? '';
final functionCall = event.data['function_call'] ?? '';
break;
default:
break;
}
});
}
// 滚动到底部
void _scrollToBottom() {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (scrollController.hasClients) {
try {
scrollController.animateTo(
scrollController.position.maxScrollExtent,
duration: Duration(milliseconds: 300),
curve: Curves.easeOut,
);
} catch (e) {
logger.e('滚动到底部失败: $e');
}
}
});
}
// 发送文本消息
Future<void> sendTextMessage() async {
final text = textController.text.trim();
if (text.isEmpty) return;
final message = Message(isUser: true, text: text);
messages.add(message);
textController.clear();
// 滚动到底部
_scrollToBottom();
try {
isProcessing.value = true;
// 标记为新的AI回复
_isNewAssistantResponse = true;
await AgentService.processTextInput(text, speakResponse: true);
} catch (e) {
logger.e('发送文本消息失败: $e');
isProcessing.value = false;
}
}
// 开始语音输入
Future<void> startVoiceInput() async {
if (isListening.value) return;
try {
await AgentService.startConversation();
} catch (e) {
logger.e('启动语音输入失败: $e');
}
}
// 停止语音输入
Future<void> stopVoiceInput() async {
if (!isListening.value) return;
try {
await AgentService.stopConversation();
} catch (e) {
logger.e('停止语音输入失败: $e');
}
}
// 清除聊天历史
Future<void> clearChat() async {
try {
await AgentService.clearChatHistory();
// 清空本地消息列表
messages.clear();
// 添加欢迎消息
_addWelcomeMessage();
// 重置回复状态
_isNewAssistantResponse = true;
} catch (e) {
logger.e('清除聊天历史失败: $e');
}
}
// 切换输入模式
void toggleInputMode() {
isTextInputMode.toggle();
// 切换到语音模式时,直接开始语音输入
if (!isTextInputMode.value) {
startVoiceInput();
}
// 切换到文本模式时,直接停止语音输入
else {
stopVoiceInput();
}
}
}

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

@ -0,0 +1,350 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../controllers/agent_controller.dart';
import 'message_bubble.dart';
class AgentView extends StatelessWidget {
const AgentView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final controller = Get.find<AgentController>();
return Scaffold(
appBar: AppBar(
title: Row(
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: Theme.of(context).primaryColor,
borderRadius: BorderRadius.circular(20),
),
child: const Icon(
Icons.smart_toy_outlined,
color: Colors.white,
),
),
const SizedBox(width: 10),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'DeepVoice AI',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
Row(
children: [
Container(
width: 8,
height: 8,
decoration: BoxDecoration(
color: Colors.green,
borderRadius: BorderRadius.circular(4),
),
),
const SizedBox(width: 4),
Text(
'在线',
style: TextStyle(
fontSize: 12,
color: Colors.green,
),
),
],
),
],
),
],
),
actions: [
PopupMenuButton(
icon: const Icon(Icons.more_vert),
itemBuilder: (context) => [
PopupMenuItem(
child: const Text('清除聊天记录'),
onTap: () => controller.clearChat(),
),
],
),
],
),
body: Column(
children: [
// 聊天内容
Expanded(
child: Obx(
() => ListView.builder(
controller: controller.scrollController,
reverse: false,
padding: const EdgeInsets.all(16),
itemCount: controller.messages.length,
itemBuilder: (context, index) {
final message = controller.messages[index];
return MessageBubble(
isUser: message.isUser,
message: message.text,
timestamp: message.timestamp,
isRecognizing: message.isRecognizing,
);
},
),
),
),
// 输入区域
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Theme.of(context).scaffoldBackgroundColor,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 5,
offset: const Offset(0, -1),
),
],
),
child: Row(
children: [
// 键盘/语音切换按钮
Obx(() {
final isTextMode = controller.isTextInputMode.value;
return GestureDetector(
onTap: () {
controller.toggleInputMode();
},
child: Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: Colors.grey[200],
borderRadius: BorderRadius.circular(24),
),
child: Icon(
isTextMode ? Icons.mic_none : Icons.keyboard,
color: Colors.grey[600],
),
),
);
}),
const SizedBox(width: 8),
// 文本输入框或语音波形图
Expanded(
child: Obx(() {
final isTextMode = controller.isTextInputMode.value;
if (isTextMode) {
// 文本输入模式
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
color: Colors.grey[200],
borderRadius: BorderRadius.circular(24),
),
child: Row(
children: [
Expanded(
child: TextField(
controller: controller.textController,
decoration: const InputDecoration(
hintText: '输入消息...',
border: InputBorder.none,
),
onSubmitted: (_) => controller.sendTextMessage(),
),
),
GestureDetector(
onTap: () => controller.sendTextMessage(),
child: Icon(
Icons.send,
color: Theme.of(context).primaryColor,
),
),
],
),
);
} else {
// 语音输入模式 - 始终显示波形图
return GestureDetector(
onTap: () {
if (controller.isListening.value) {
controller.stopVoiceInput();
} else {
controller.startVoiceInput();
}
},
child: Container(
height: 48,
padding: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
color: Colors.grey[200],
borderRadius: BorderRadius.circular(24),
),
child: Row(
children: [
// 语音波形图 - 始终显示
Expanded(
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
...List.generate(10, (index) {
return _buildSoundBar(index);
}),
const Spacer(),
// "正在聆听"文字
Obx(() {
final isListening = controller.isListening.value;
return Row(
children: [
Icon(
Icons.mic,
size: 14,
color: isListening
? Theme.of(context).primaryColor
: Colors.grey,
),
const SizedBox(width: 4),
Text(
'正在聆听',
style: TextStyle(
fontSize: 12,
color: isListening
? Theme.of(context).primaryColor
: Colors.grey,
),
),
],
);
}),
],
),
),
],
),
),
);
}
}),
),
const SizedBox(width: 8),
// 只显示添加按钮,不再显示停止按钮
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: Colors.grey[200],
borderRadius: BorderRadius.circular(24),
),
child: Icon(
Icons.add,
color: Colors.grey[600],
),
),
],
),
),
],
),
);
}
// 构建声音柱
Widget _buildSoundBar(int index) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 1),
child: Obx(() => _SoundBar(
delay: index * 0.1,
isActive: Get.find<AgentController>().isListening.value,
)),
);
}
}
// 声音柱动画组件
class _SoundBar extends StatefulWidget {
final double delay;
final bool isActive;
const _SoundBar({
required this.delay,
required this.isActive,
});
@override
_SoundBarState createState() => _SoundBarState();
}
class _SoundBarState extends State<_SoundBar> with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 500),
);
_animation = Tween<double>(begin: 3, end: 15).animate(
CurvedAnimation(
parent: _controller,
curve: Curves.easeInOut,
),
)..addListener(() {
setState(() {});
});
_startOrStopAnimation();
}
@override
void didUpdateWidget(_SoundBar oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.isActive != oldWidget.isActive) {
_startOrStopAnimation();
}
}
void _startOrStopAnimation() {
if (widget.isActive) {
Future.delayed(Duration(milliseconds: (widget.delay * 1000).toInt()), () {
if (mounted && widget.isActive) {
_controller.repeat(reverse: true);
}
});
} else {
_controller.reset();
// 设置为小高度的静态状态
_controller.animateTo(0.1, duration: Duration(milliseconds: 300));
}
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Container(
width: 2,
height: widget.isActive ? _animation.value : 3,
decoration: BoxDecoration(
color: widget.isActive ? Theme.of(context).primaryColor : Colors.grey[400],
borderRadius: BorderRadius.circular(1),
),
);
}
}

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

@ -0,0 +1,832 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
/// 消息气泡组件
class MessageBubble extends StatelessWidget {
final bool isUser;
final String message;
final DateTime timestamp;
final bool isRecognizing;
const MessageBubble({
Key? key,
required this.isUser,
required this.message,
required this.timestamp,
this.isRecognizing = false,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final timeFormat = DateFormat('HH:mm');
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
mainAxisAlignment: isUser ? MainAxisAlignment.end : MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (!isUser) _buildAvatar(isUser, context),
const SizedBox(width: 8),
Flexible(
child: Column(
crossAxisAlignment: isUser ? CrossAxisAlignment.end : CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: _getBubbleColor(context),
borderRadius: BorderRadius.circular(18).copyWith(
bottomLeft: isUser ? const Radius.circular(18) : const Radius.circular(4),
bottomRight: !isUser ? const Radius.circular(18) : const Radius.circular(4),
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 2,
offset: const Offset(0, 1),
),
],
),
child: _buildMessageContent(context),
),
const SizedBox(height: 4),
// 识别中的消息不显示时间
if (!isRecognizing)
Text(
timeFormat.format(timestamp),
style: TextStyle(
fontSize: 10,
color: Colors.grey[500],
fontWeight: FontWeight.w400,
),
),
],
),
),
const SizedBox(width: 8),
if (isUser) _buildAvatar(isUser, context),
],
),
);
}
/// 获取气泡颜色
Color _getBubbleColor(BuildContext context) {
if (isUser) {
return isRecognizing
? Color(0xFF3B82F6).withOpacity(0.8) // 识别中的用户消息使用半透明的蓝色
: Color(0xFF3B82F6); // 使用更明亮的蓝色
} else {
return Color(0xFFF1F5F9); // 更淡的灰色背景
}
}
/// 构建头像
Widget _buildAvatar(bool isUser, BuildContext context) {
return Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: isUser ? Colors.grey[300] : Theme.of(context).primaryColor,
borderRadius: BorderRadius.circular(18),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 2,
offset: const Offset(0, 1),
),
],
),
child: Icon(
isUser ? Icons.person : Icons.smart_toy_outlined,
color: isUser ? Colors.grey[700] : Colors.white,
size: 20,
),
);
}
/// 构建消息内容
Widget _buildMessageContent(BuildContext context) {
// 如果是识别中的消息,添加特殊样式
if (isRecognizing) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
child: Text(
message,
style: const TextStyle(
color: Colors.white,
fontStyle: FontStyle.italic,
fontSize: 14,
height: 1.4,
),
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(width: 8),
_buildTypingIndicator(),
],
);
}
// 判断消息内容,显示特殊卡片
if (!isUser) {
if (message.contains('播放列表') || message.contains('音乐')) {
return _buildMusicContent(context);
} else if (message.contains('景点') || message.contains('旅游')) {
return _buildTravelContent(context);
} else if (message.contains('天气') || message.contains('气温') || message.contains('预报')) {
return _buildWeatherContent(context);
}
}
return Text(
message,
style: TextStyle(
color: isUser ? Colors.white : Colors.black87,
fontSize: 14,
height: 1.4,
fontWeight: FontWeight.w400,
),
);
}
/// 构建输入中的指示器
Widget _buildTypingIndicator() {
return SizedBox(
width: 40,
height: 20,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_buildDot(100),
_buildDot(300),
_buildDot(500),
],
),
);
}
/// 构建动画点
Widget _buildDot(int milliseconds) {
return _AnimatedDot(delayMilliseconds: milliseconds);
}
/// 构建音乐内容卡片
Widget _buildMusicContent(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'以下是一些推荐的音乐:',
style: TextStyle(
color: Colors.black87,
fontSize: 14,
height: 1.4,
fontWeight: FontWeight.w400,
),
),
const SizedBox(height: 12),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.03),
blurRadius: 3,
spreadRadius: 1,
offset: const Offset(0, 1),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.music_note,
size: 16,
color: Theme.of(context).primaryColor.withOpacity(0.7),
),
const SizedBox(width: 6),
Text(
'雨天时光 · 播放列表',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: Colors.grey[700],
),
),
],
),
const SizedBox(height: 10),
// 歌曲示例
_buildMusicItem('雨声与钢琴曲', '纯音乐', context),
Divider(height: 1, thickness: 1, color: Colors.grey[100]),
_buildMusicItem('Rainy Day', 'Coldplay', context),
Divider(height: 1, thickness: 1, color: Colors.grey[100]),
_buildMusicItem('雨天的爵士咖啡馆', '爵士合集', context),
const SizedBox(height: 8),
Center(
child: TextButton(
onPressed: () {},
style: TextButton.styleFrom(
minimumSize: Size.zero,
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
child: Text(
'查看更多推荐',
style: TextStyle(
fontSize: 12,
color: Theme.of(context).primaryColor,
fontWeight: FontWeight.w500,
),
),
),
),
],
),
),
const SizedBox(height: 10),
Row(
mainAxisSize: MainAxisSize.min,
children: [
Expanded(
child: OutlinedButton.icon(
onPressed: () {},
icon: Icon(Icons.play_arrow, size: 16),
label: Text('播放全部'),
style: OutlinedButton.styleFrom(
foregroundColor: Theme.of(context).primaryColor,
side: BorderSide(color: Theme.of(context).primaryColor.withOpacity(0.5)),
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 4),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
textStyle: TextStyle(fontSize: 12),
),
),
),
SizedBox(width: 8),
Expanded(
child: OutlinedButton.icon(
onPressed: () {},
icon: Icon(Icons.shuffle, size: 16),
label: Text('随机播放'),
style: OutlinedButton.styleFrom(
foregroundColor: Colors.grey[700],
side: BorderSide(color: Colors.grey[400]!),
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 4),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
textStyle: TextStyle(fontSize: 12),
),
),
),
],
),
],
);
}
/// 构建旅游内容卡片
Widget _buildTravelContent(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'以下是一些推荐的景点:',
style: TextStyle(
color: Colors.black87,
fontSize: 14,
height: 1.4,
fontWeight: FontWeight.w400,
),
),
const SizedBox(height: 12),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.03),
blurRadius: 3,
spreadRadius: 1,
offset: const Offset(0, 1),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.place,
size: 16,
color: Color(0xFFE11D48),
),
const SizedBox(width: 6),
Text(
'热门景点 · 东京',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: Colors.grey[700],
),
),
],
),
const SizedBox(height: 10),
// 景点列表
_buildLocationItem('东京塔', '标志性建筑,可俯瞰东京全景', context),
Divider(height: 1, thickness: 1, color: Colors.grey[100]),
_buildLocationItem('涩谷十字路口', '世界上最繁忙的十字路口之一', context),
Divider(height: 1, thickness: 1, color: Colors.grey[100]),
_buildLocationItem('浅草寺', '东京最古老的寺庙', context),
const SizedBox(height: 8),
Center(
child: TextButton(
onPressed: () {},
style: TextButton.styleFrom(
minimumSize: Size.zero,
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
child: Text(
'查看更多景点',
style: TextStyle(
fontSize: 12,
color: Theme.of(context).primaryColor,
fontWeight: FontWeight.w500,
),
),
),
),
],
),
),
const SizedBox(height: 10),
Row(
mainAxisSize: MainAxisSize.min,
children: [
Expanded(
child: OutlinedButton.icon(
onPressed: () {},
icon: Icon(Icons.map, size: 16),
label: Text('查看地图'),
style: OutlinedButton.styleFrom(
foregroundColor: Theme.of(context).primaryColor,
side: BorderSide(color: Theme.of(context).primaryColor.withOpacity(0.5)),
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 4),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
textStyle: TextStyle(fontSize: 12),
),
),
),
SizedBox(width: 8),
Expanded(
child: OutlinedButton.icon(
onPressed: () {},
icon: Icon(Icons.bookmark_border, size: 16),
label: Text('保存到收藏'),
style: OutlinedButton.styleFrom(
foregroundColor: Colors.grey[700],
side: BorderSide(color: Colors.grey[400]!),
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 4),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
textStyle: TextStyle(fontSize: 12),
),
),
),
],
),
],
);
}
/// 构建音乐项目
Widget _buildMusicItem(String title, String artist, BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
children: [
Container(
width: 38,
height: 38,
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(8),
),
child: Icon(
Icons.music_note,
size: 18,
color: Theme.of(context).primaryColor,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
color: Colors.grey[800],
),
overflow: TextOverflow.ellipsis,
),
SizedBox(height: 2),
Text(
artist,
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
),
overflow: TextOverflow.ellipsis,
),
],
),
),
IconButton(
onPressed: () {},
icon: Icon(Icons.play_circle, color: Theme.of(context).primaryColor),
padding: EdgeInsets.zero,
constraints: BoxConstraints(),
iconSize: 24,
),
],
),
);
}
/// 构建地点项目
Widget _buildLocationItem(String name, String description, BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
children: [
Container(
width: 38,
height: 38,
decoration: BoxDecoration(
color: Colors.blue[50],
borderRadius: BorderRadius.circular(8),
),
child: Icon(
Icons.place,
size: 18,
color: Color(0xFFE11D48),
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
name,
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
color: Colors.grey[800],
),
overflow: TextOverflow.ellipsis,
),
SizedBox(height: 2),
Text(
description,
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
),
overflow: TextOverflow.ellipsis,
),
],
),
),
IconButton(
onPressed: () {},
icon: Icon(Icons.arrow_forward, color: Colors.grey[400]),
padding: EdgeInsets.zero,
constraints: BoxConstraints(),
iconSize: 20,
),
],
),
);
}
/// 构建天气内容卡片
Widget _buildWeatherContent(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'当前天气情况:',
style: TextStyle(
color: Colors.black87,
fontSize: 14,
height: 1.4,
fontWeight: FontWeight.w400,
),
),
const SizedBox(height: 12),
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [Color(0xFF60A5FA), Color(0xFF3B82F6)],
),
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'北京',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
SizedBox(height: 4),
Text(
'晴朗',
style: TextStyle(
fontSize: 14,
color: Colors.white.withOpacity(0.9),
),
),
SizedBox(height: 12),
Row(
children: [
Text(
'25°',
style: TextStyle(
fontSize: 36,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
SizedBox(width: 8),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'最高: 29°',
style: TextStyle(
fontSize: 12,
color: Colors.white.withOpacity(0.9),
),
),
Text(
'最低: 18°',
style: TextStyle(
fontSize: 12,
color: Colors.white.withOpacity(0.9),
),
),
],
),
],
),
],
),
Icon(
Icons.wb_sunny,
size: 48,
color: Colors.white,
),
],
),
SizedBox(height: 16),
Container(
height: 1,
color: Colors.white.withOpacity(0.2),
),
SizedBox(height: 16),
Text(
'未来预报',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_buildForecastDay('周一', Icons.wb_sunny, '28°'),
_buildForecastDay('周二', Icons.cloud, '25°'),
_buildForecastDay('周三', Icons.wb_cloudy, '26°'),
_buildForecastDay('周四', Icons.grain, '24°'),
_buildForecastDay('周五', Icons.wb_sunny, '27°'),
],
),
SizedBox(height: 16),
Container(
padding: EdgeInsets.symmetric(vertical: 8, horizontal: 12),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
Icon(
Icons.info_outline,
size: 16,
color: Colors.white,
),
SizedBox(width: 6),
Flexible(
child: Text(
'今日紫外线指数较高,外出请做好防晒',
style: TextStyle(
fontSize: 12,
color: Colors.white,
),
),
),
],
),
),
],
),
),
const SizedBox(height: 10),
Row(
mainAxisSize: MainAxisSize.min,
children: [
Expanded(
child: OutlinedButton.icon(
onPressed: () {},
icon: Icon(Icons.location_on, size: 16),
label: Text('更多地区'),
style: OutlinedButton.styleFrom(
foregroundColor: Theme.of(context).primaryColor,
side: BorderSide(color: Theme.of(context).primaryColor.withOpacity(0.5)),
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 4),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
textStyle: TextStyle(fontSize: 12),
),
),
),
SizedBox(width: 8),
Expanded(
child: OutlinedButton.icon(
onPressed: () {},
icon: Icon(Icons.calendar_today, size: 16),
label: Text('15天预报'),
style: OutlinedButton.styleFrom(
foregroundColor: Colors.grey[700],
side: BorderSide(color: Colors.grey[400]!),
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 4),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
textStyle: TextStyle(fontSize: 12),
),
),
),
],
),
],
);
}
/// 构建单日天气预报
Widget _buildForecastDay(String day, IconData icon, String temp) {
return Column(
children: [
Text(
day,
style: TextStyle(
fontSize: 12,
color: Colors.white.withOpacity(0.9),
),
),
SizedBox(height: 4),
Icon(
icon,
size: 20,
color: Colors.white,
),
SizedBox(height: 4),
Text(
temp,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: Colors.white,
),
),
],
);
}
}
/// 动画点组件
class _AnimatedDot extends StatefulWidget {
final int delayMilliseconds;
const _AnimatedDot({required this.delayMilliseconds});
@override
_AnimatedDotState createState() => _AnimatedDotState();
}
class _AnimatedDotState extends State<_AnimatedDot> with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 1500),
);
_animation = Tween<double>(begin: 0, end: 6).animate(
CurvedAnimation(
parent: _controller,
curve: Interval(0.0, 0.5, curve: Curves.easeOut),
reverseCurve: Interval(0.5, 1.0, curve: Curves.easeIn),
),
)..addListener(() {
setState(() {});
});
Future.delayed(Duration(milliseconds: widget.delayMilliseconds), () {
if (mounted) {
_controller.repeat();
}
});
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Container(
width: 6,
height: 6,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(3),
boxShadow: [
BoxShadow(
color: Colors.white.withOpacity(0.5),
blurRadius: _animation.value,
spreadRadius: _animation.value / 2,
),
],
),
);
}
}

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

@ -5,7 +5,8 @@ import '../../../core/utils/logger.dart';
import 'package:flutter/material.dart';
import '../../explore/controllers/explore_controller.dart';
import 'package:classic_bluetooth/classic_bluetooth.dart' show BluetoothDevice;
import '../../../data/services/bluetooth_service.dart';
import '../../../data/services/bluetooth_manager.dart';
import '../../../data/services/ble_manager.dart';
/// 连接状态枚举
enum ConnectionStatus {
@ -31,8 +32,8 @@ class HomeController extends GetxController {
ConnectionStatus get connectionStatus => _connectionStatus.value;
String get connectionErrorMsg => _connectionErrorMsg.value;
String get bluetoothState => _bluetoothState.value;
BluetoothDevice? get currentDevice => _bluetoothService.currentDevice;
DeviceInfo? get deviceInfo => _bluetoothService.deviceInfo;
BluetoothDevice? get currentDevice => _bluetoothManager.currentDevice;
DeviceInfo? get deviceInfo => _bleManager.deviceInfo;
bool get isConnected => currentDevice != null;
// 设备详细信息的getter,从deviceInfo获取数据
@ -43,8 +44,8 @@ class HomeController extends GetxController {
bool get rightEarCharging => deviceInfo?.rightEarCharging ?? false;
bool get caseCharging => deviceInfo?.caseCharging ?? false;
String get firmwareVersion => deviceInfo?.firmwareVersion ?? '';
String get productId => deviceInfo?.productId ?? '';
String get colorId => deviceInfo?.colorId ?? '';
// 监听和订阅
Worker? _deviceInfoWorker;
@ -52,8 +53,8 @@ class HomeController extends GetxController {
Worker? _deviceWorker;
// 蓝牙服务
final _bluetoothService = Get.find<BluetoothService>();
final _bluetoothManager = Get.find<BluetoothManager>();
final _bleManager = Get.find<BleManager>();
@override
void onInit() {
super.onInit();
@ -71,7 +72,7 @@ class HomeController extends GetxController {
/// 使用缓存的蓝牙设备信息初始化
void _initWithCachedInfo() {
// 使用蓝牙服务中的当前状态
_bluetoothState.value = _bluetoothService.bluetoothState;
_bluetoothState.value = _bluetoothManager.bluetoothState;
// 根据当前设备状态设置连接状态
if (currentDevice != null) {
@ -94,7 +95,7 @@ class HomeController extends GetxController {
/// 设置状态监听器
void _setupStateListeners() {
// 设备信息监听 - 设备信息更新时自动触发UI更新
_deviceInfoWorker = ever(_bluetoothService.deviceInfo.obs, (DeviceInfo? info) {
_deviceInfoWorker = ever(_bleManager.deviceInfo.obs, (DeviceInfo? info) {
if (info != null) {
Logger.info('主页更新设备信息: ${info.toString()}');
} else {
@ -104,7 +105,7 @@ class HomeController extends GetxController {
});
// 蓝牙状态监听
_bluetoothStateWorker = ever(_bluetoothService.bluetoothState.obs, (String state) {
_bluetoothStateWorker = ever(_bluetoothManager.bluetoothState.obs, (String state) {
_bluetoothState.value = state;
// 如果蓝牙关闭,更新错误信息
@ -117,7 +118,7 @@ class HomeController extends GetxController {
});
// 设备连接状态监听 - 直接监听设备变化
_deviceWorker = ever(_bluetoothService.currentDeviceRx, (BluetoothDevice? device) {
_deviceWorker = ever(_bluetoothManager.currentDeviceRx, (BluetoothDevice? device) {
if (device != null) {
// 设备已连接
if (_connectionStatus.value != ConnectionStatus.connected) {
@ -180,7 +181,7 @@ class HomeController extends GetxController {
/// 展示AI功能
void exploreAIFeatures() {
Get.toNamed(Routes.chat);
Get.toNamed(Routes.agent);
}
// UI辅助方法
@ -301,9 +302,4 @@ class HomeController extends GetxController {
deviceInfo!.firmwareVersion : 'unknown'.tr;
}
/// 获取设备类型文本
String getProductTypeText() {
return deviceInfo?.productId.isNotEmpty == true ?
"型号: ${deviceInfo!.productId}" : 'unknown'.tr;
}
}

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

@ -611,16 +611,6 @@ class HomeView extends GetView<HomeController> {
isDarkMode: isDarkMode,
),
// 产品型号 (如果有)
if (controller.productId.isNotEmpty) ...[
SizedBox(height: 8.h),
_buildInfoRow(
icon: Icons.category,
label: 'productType'.tr,
value: controller.productId,
isDarkMode: isDarkMode,
),
],
// 连接状态标签
SizedBox(height: 10.h),

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

@ -5,9 +5,9 @@ import 'package:permission_handler/permission_handler.dart';
import '../../../routes/app_routes.dart';
import '../../../core/utils/logger.dart';
import 'dart:io';
import '../../../data/services/bluetooth_service.dart';
import '../../../data/services/bluetooth_manager.dart';
import 'package:classic_bluetooth/classic_bluetooth.dart' show BluetoothDevice;
import '../../../data/services/ble_manager.dart';
/// 配对控制器,负责检查蓝牙权限、状态,并处理配对流程
class PairingController extends GetxController {
static PairingController get to => Get.find();
@ -41,7 +41,7 @@ class PairingController extends GetxController {
Worker? _deviceWorker;
// 蓝牙服务
final _bluetoothService = Get.find<BluetoothService>();
final _bluetoothService = Get.find<BluetoothManager>();
@override
void onInit() {
@ -59,6 +59,8 @@ class PairingController extends GetxController {
/// 初始化流程
Future<void> _initialize() async {
try {
isLoading.value = true;
statusDescription.value = 'checkingPermissions'.tr;
@ -83,6 +85,10 @@ class PairingController extends GetxController {
// 刷新已连接设备
statusDescription.value = 'searchingDevice'.tr;
// 设备连接时启动BLE后台服务
_startBleService();
await refreshConnectedDevices();
} catch (e) {
Logger.error('初始化配对控制器失败: $e');
@ -102,6 +108,7 @@ class PairingController extends GetxController {
// Android 12+ (SDK 31+) 需要 SCAN 和 CONNECT
// Android 6+ (SDK 23+) 需要 LOCATION
permissionsToRequest.add(Permission.locationWhenInUse); // 基本定位权限
permissionsToRequest.add(Permission.microphone);
// Android 12及以上版本才需要请求蓝牙扫描和连接权限
if (await _getAndroidSdkVersion() >= 31) {
permissionsToRequest.add(Permission.bluetoothScan);
@ -226,8 +233,7 @@ class PairingController extends GetxController {
Logger.info('已找到连接的设备 ${currentDevice!.name},自动跳转到主页');
// 设备连接时启动BLE后台服务
_startBleService();
Future.delayed(const Duration(milliseconds: 500), () {
Get.offAllNamed(Routes.home);
@ -237,23 +243,20 @@ class PairingController extends GetxController {
/// 启动BLE后台服务
Future<void> _startBleService() async {
try {
if (!isDeviceConnected) return;
String deviceName = currentDevice!.name;
String deviceAddress = currentDevice!.address;
// 准备配置参数
final Map<String, dynamic> config = {
'scan_interval': 5000, // 扫描间隔时间(毫秒)
'scan_duration': 10000, // 扫描持续时间(毫秒)
'notification_title': '设备连接中', // 通知标题
'notification_content': '已连接到 $deviceName', // 通知内容
'mac': deviceAddress, // 设备地址
'device_name': deviceName // 设备名称
};
// if (!isDeviceConnected) return;
// 启动BLE服务
final result = await _bluetoothService.startBleService(config);
// 确保麦克风权限已被授予
final microphoneStatus = await Permission.microphone.status;
if (!microphoneStatus.isGranted) {
Logger.warning('麦克风权限未授予,语音交互需要此权限');
final result = await Permission.microphone.request();
if (!result.isGranted) {
Logger.error('麦克风权限被拒绝,语音功能将无法正常工作');
}
}
// 启动BLE服务,无需传递配置参数,将从.env读取
final result = await BleManager.to.startBleService();
if (result) {
Logger.info('BLE后台服务启动成功');
@ -268,7 +271,7 @@ class PairingController extends GetxController {
/// 停止BLE后台服务
Future<void> _stopBleService() async {
try {
final result = await _bluetoothService.stopBleService();
final result = await BleManager.to.stopBleService();
if (result) {
Logger.info('BLE后台服务停止成功');

8
lib/routes/app_pages.dart

@ -39,6 +39,8 @@ import '../modules/opus_test/views/opus_test_view.dart';
import '../modules/opus_test/bindings/opus_test_binding.dart';
import '../modules/speech_test/views/speech_test_view.dart';
import '../modules/speech_test/bindings/speech_test_binding.dart';
import '../modules/agent/views/agent_view.dart';
import '../modules/agent/bindings/agent_binding.dart';
abstract class AppPages {
static final pages = [
@ -148,5 +150,11 @@ abstract class AppPages {
page: () => const SpeechTestView(),
binding: SpeechTestBinding(),
),
GetPage(
name: Routes.agent,
page: () => const AgentView(),
binding: AgentBinding(),
),
];
}

1
lib/routes/app_routes.dart

@ -29,4 +29,5 @@ abstract class Routes {
static const opusTest = '/opus_test';
static const bleTest = '/ble_test';
static const speechTest = '/speech_test';
static const agent = '/agent';
}

6
local_plugins/agent_service/android/build.gradle.kts

@ -5,7 +5,7 @@ plugins {
android {
namespace = "com.yunqiinnovation.agent_service"
compileSdk = 33
compileSdk = 35
defaultConfig {
minSdk = 21
@ -16,6 +16,10 @@ android {
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = "11"
}
sourceSets {
getByName("main") {
manifest.srcFile("src/main/AndroidManifest.xml")

4
local_plugins/agent_service/android/settings.gradle.kts

@ -5,9 +5,11 @@ include(":azure_speech")
include(":open_ai_service")
include(":deepvoice_utils")
include(":chat_storage")
include(":deepvoice_utils")
// 设置项目路径
project(":azure_speech").projectDir = file("../../azure_speech/android")
project(":open_ai_service").projectDir = file("../../open_ai_service/android")
project(":deepvoice_utils").projectDir = file("../../deepvoice_utils/android")
project(":chat_storage").projectDir = file("../../chat_storage/android")
project(":chat_storage").projectDir = file("../../chat_storage/android")
project(":deepvoice_utils").projectDir = file("../../deepvoice_utils/android")

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

@ -7,9 +7,10 @@ import com.yunqiinnovation.azure_speech.AzureAsrHelper
import com.yunqiinnovation.azure_speech.AzureTtsHelper
import com.yunqiinnovation.open_ai_service.OpenAIService
import com.yunqiinnovation.chat_storage.ChatStorageHelper
import com.yunqiinnovation.common_utils.FileLogger
import com.yunqiinnovation.deepvoice_utils.FileLogger
import io.flutter.plugin.common.MethodChannel
import kotlinx.coroutines.*
import java.util.Collections
import kotlin.coroutines.CoroutineContext
/**
@ -27,11 +28,9 @@ interface AgentServiceListener {
/**
* 代理服务 - 处理语音识别、TTS和AI对话相关逻辑
* 单例实现
*/
class AgentService(
private val context: Context,
private var listener: AgentServiceListener? = null
) : CoroutineScope {
object AgentService : CoroutineScope {
private val TAG = "AgentService"
// 协程相关
@ -39,6 +38,10 @@ class AgentService(
override val coroutineContext: CoroutineContext
get() = Dispatchers.Main + job
// 上下文和监听器
private lateinit var context: Context
private val listeners = Collections.synchronizedSet(mutableSetOf<AgentServiceListener>())
// 配置参数
private var azureSpeechKey: String = ""
private var azureSpeechRegion: String = ""
@ -51,17 +54,20 @@ class AgentService(
private var azureTtsHelper: AzureTtsHelper? = null
// OpenAI服务
private val openAIService = OpenAIService(context.applicationContext)
private lateinit var openAIService: OpenAIService
// 聊天存储服务
private val chatStorageHelper = ChatStorageHelper(context)
private lateinit var chatStorageHelper: ChatStorageHelper
// 会话ID,用于区分不同聊天上下文
private val sessionId = "default_session"
private val sessionId = "default_agent"
// 历史聊天消息缓存
private val historyMessages = JSONArray()
// 系统提示词
private var systemPrompt = ""
// 状态
private var isInitialized = false
var isRecognitionActive = false
@ -70,28 +76,67 @@ class AgentService(
private set
var hasSpeechDetected = false
private set
var isAiStreaming = false
private set
// AI流生成相关
private var currentAiJob: Job? = null
// 空闲检测相关
private var idleCheckJob: Job? = null
private val maxIdleSeconds = 10 // 最大空闲秒数
/**
* 设置事件监听器
* 初始化系统提示词
*/
fun setListener(listener: AgentServiceListener?) {
this.listener = listener
private fun initSystemPrompt() {
systemPrompt = """
你是一个友好、专业的语音助手,名叫"小语"。你的目标是通过对话为用户提供帮助、解答问题和完成任务。
遵循以下指导原则:
1. 保持简短精炼的回答,因为用户是通过语音与你交流
2. 优先使用中文回复,除非用户明确要求使用其他语言
3. 当用户问题不明确时,礼貌地请求更多信息
4. 避免过长的列表,尽量将信息分成小段
5. 不要使用需要视觉展示的元素(如表格、图表或代码块)
6. 记住用户之前的对话内容,保持对话连贯
你不仅可以回答知识性问题,还可以帮助用户设置提醒、提供建议,或进行轻松愉快的对话。
无论遇到什么问题,都要尽力以温暖、贴心的语气提供最佳帮助。
""".trimIndent()
}
/**
* 初始化
* @param appContext 应用上下文
* @param serviceListener 事件监听器(可选)
* @param config 配置参数,包含所需的所有API密钥和设置
* @return 是否初始化成功
*/
fun initialize(config: Map<String, Any>): Boolean {
fun initialize(appContext: Context, config: Map<String, Any>): Boolean {
if (isInitialized) return true
try {
// 初始化上下文和服务
this.context = appContext.applicationContext
this.openAIService = OpenAIService(context)
this.chatStorageHelper = ChatStorageHelper(context)
// 从配置中获取参数
azureSpeechKey = config["azureSpeechKey"] as? String ?: ""
azureSpeechRegion = config["azureSpeechRegion"] as? String ?: ""
openaiApiKey = config["openaiApiKey"] as? String ?: ""
openaiBaseUrl = config["openaiBaseUrl"] as? String ?: ""
openaiModel = config["openaiModel"] as? String ?: "gpt-3.5-turbo"
val mcpServer = config["mcpServer"] as? String ?: ""
// 自定义系统提示词
val customSystemPrompt = config["systemPrompt"] as? String
if (!customSystemPrompt.isNullOrBlank()) {
systemPrompt = customSystemPrompt
} else {
// 使用默认系统提示词
initSystemPrompt()
}
// 检查必要参数
if (azureSpeechKey.isEmpty() || azureSpeechRegion.isEmpty() || openaiApiKey.isEmpty()) {
@ -104,7 +149,7 @@ class AgentService(
initialize(
subscriptionKey = azureSpeechKey,
region = azureSpeechRegion,
audioSourceType = AzureAsrHelper.AudioSourceType.MICROPHONE
audioSourceType = AzureAsrHelper.AudioSourceType.MICROPHONE,
)
}
@ -118,27 +163,33 @@ class AgentService(
// 设置TTS回调,更新状态
setTtsCallback(object : AzureTtsHelper.TtsCallback {
override fun onSynthesisStarted() {
isTtsSpeaking = true
// 状态变化,重置空闲检测
restartIdleCheck()
sendEvent("tts_started", mapOf("status" to "started"))
}
override fun onSynthesizing() {}
override fun onAudioDataReceived(audioData: ByteArray) {}
override fun onSynthesisCompleted() {
isTtsSpeaking = false
// 状态变化,重启空闲检测
restartIdleCheck()
sendEvent("tts_completed", mapOf("status" to "completed"))
}
override fun onSynthesisCanceled() {
isTtsSpeaking = false
// 状态变化,重启空闲检测
restartIdleCheck()
sendEvent("tts_canceled", mapOf("status" to "canceled"))
}
})
}
// 初始化OpenAI服务
openAIService.initialize(openaiApiKey, openaiBaseUrl, openaiModel)
openAIService.initialize(openaiApiKey, openaiBaseUrl, openaiModel, mcpServer)
// 加载最近的聊天记录
loadChatHistory()
@ -152,6 +203,77 @@ class AgentService(
}
}
/**
* 添加事件监听器
* @param listener 要添加的监听器
*/
fun addListener(listener: AgentServiceListener) {
listeners.add(listener)
}
/**
* 移除事件监听器
* @param listener 要移除的监听器
*/
fun removeListener(listener: AgentServiceListener) {
listeners.remove(listener)
}
/**
* 移除所有事件监听器
*/
fun clearListeners() {
listeners.clear()
}
/**
* 启动空闲检测
* 使用协程实现延迟任务,比Timer更加轻量和可控
*/
private fun startIdleCheck() {
stopIdleCheck() // 先停止现有的检查
if (!isRecognitionActive) return
idleCheckJob = launch {
try {
// FileLogger.d(TAG, "启动空闲检测,最大空闲时间: $maxIdleSeconds 秒")
// 直接等待指定的时间
delay(maxIdleSeconds * 1000L)
// 如果延迟后状态仍然是空闲的,则停止识别
if (isActive && isRecognitionActive && !hasSpeechDetected && !isTtsSpeaking && !isAiStreaming) {
// FileLogger.d(TAG, "检测到空闲状态超过${maxIdleSeconds}秒,自动停止语音识别")
stopRecognition()
sendEvent("auto_stop", mapOf("reason" to "idle_timeout", "seconds" to maxIdleSeconds))
}
} catch (e: CancellationException) {
// 协程被取消,这是正常行为
// FileLogger.d(TAG, "空闲检测被取消")
} catch (e: Exception) {
FileLogger.e(TAG, "空闲检测异常", e)
}
}
}
/**
* 停止空闲检测
*/
private fun stopIdleCheck() {
idleCheckJob?.cancel()
idleCheckJob = null
}
/**
* 重启空闲检测
* 当状态发生变化时调用
*/
private fun restartIdleCheck() {
if (isRecognitionActive) {
startIdleCheck()
}
}
/**
* 开始语音识别
*/
@ -164,11 +286,11 @@ class AgentService(
if (isRecognitionActive) return true
// 检查录音权限
if (!checkRecordAudioPermission()) {
FileLogger.e(TAG, "需要录音权限,请在设置中授予权限")
sendEvent("error", mapOf("code" to "PERMISSION_DENIED", "message" to "需要录音权限"))
return false
}
// if (!checkRecordAudioPermission()) {
// FileLogger.e(TAG, "需要录音权限,请在设置中授予权限")
// sendEvent("error", mapOf("code" to "PERMISSION_DENIED", "message" to "需要录音权限"))
// return false
// }
isRecognitionActive = true
hasSpeechDetected = false
@ -177,14 +299,23 @@ class AgentService(
azureAsrHelper?.startContinuousRecognition(object : AzureAsrHelper.ContinuousRecognizeCallback {
override fun onRecognizing(recognizing: String, detectedLanguage: String) {
if (recognizing.isNotEmpty()) {
// 检测到语音,更新状态
val previousHasSpeech = hasSpeechDetected
hasSpeechDetected = true
// 状态发生变化时重启空闲检测
if (!previousHasSpeech) {
restartIdleCheck()
}
sendEvent("recognizing", mapOf(
"text" to recognizing,
"language" to detectedLanguage
))
if (isTtsSpeaking) {
stopTts()
// 如果TTS正在播放或AI正在生成,则触发打断
if (isTtsSpeaking || isAiStreaming) {
interruptCurrentResponse()
}
}
}
@ -200,20 +331,30 @@ class AgentService(
}
// 重置状态,继续识别
val previousHasSpeech = hasSpeechDetected
hasSpeechDetected = false
// 状态发生变化时重启空闲检测
if (previousHasSpeech) {
restartIdleCheck()
}
}
override fun onSessionStarted() {
sendEvent("recognition_started", mapOf("status" to "started"))
// 启动空闲检测
startIdleCheck()
}
override fun onSessionStopped() {
isRecognitionActive = false
stopIdleCheck()
sendEvent("recognition_stopped", mapOf("status" to "stopped"))
}
override fun onCanceled(reason: String, errorDetails: String) {
isRecognitionActive = false
stopIdleCheck()
sendEvent("recognition_canceled", mapOf(
"reason" to reason,
"details" to errorDetails
@ -222,6 +363,7 @@ class AgentService(
override fun onError(error: String) {
isRecognitionActive = false
stopIdleCheck()
FileLogger.e(TAG, "语音识别出错: $error")
sendEvent("error", mapOf(
"code" to "RECOGNITION_ERROR",
@ -252,10 +394,55 @@ class AgentService(
try {
azureAsrHelper?.stopContinuousRecognition()
isRecognitionActive = false
stopIdleCheck()
FileLogger.d(TAG, "语音识别已停止")
} catch (e: Exception) {
FileLogger.e(TAG, "停止语音识别异常", e)
isRecognitionActive = false
stopIdleCheck()
}
}
/**
* 打断当前响应
* 停止TTS播放和AI流输出
*/
fun interruptCurrentResponse() {
if(isAiStreaming || isTtsSpeaking) {
// 停止TTS播放
stopTts()
// 停止AI流输出
stopAiStream()
// 发送打断事件
sendEvent("response_interrupted", mapOf("status" to "interrupted"))
}
}
/**
* 停止AI流输出
*/
private fun stopAiStream() {
if (isAiStreaming) {
// 取消当前AI生成任务
currentAiJob?.cancel()
currentAiJob = null
// 通知OpenAI服务终止当前流式请求
openAIService.cancelCurrentStream()
// 更新状态
isAiStreaming = false
// 记录日志
FileLogger.d(TAG, "AI流输出已停止")
// // 向客户端发送取消事件,表明AI回复已被终止
// sendEvent("assistant_stream_canceled", mapOf(
// "status" to "canceled",
// "timestamp" to System.currentTimeMillis()
// ))
}
}
@ -312,8 +499,15 @@ class AgentService(
) {
FileLogger.d(TAG, "用户问题: $text")
launch(Dispatchers.IO) {
// 如果有正在进行的AI流式输出,先停止它
stopAiStream()
// 记录新的AI任务
currentAiJob = launch(Dispatchers.IO) {
try {
// 设置状态为正在流式输出
isAiStreaming = true
// 创建用户消息
val userMessage = openAIService.createUserMessage(text)
@ -323,72 +517,100 @@ class AgentService(
// 使用历史记录作为上下文发送到OpenAI
val responseBuilder = StringBuilder()
// 添加系统提示词到历史消息的副本中
val messagesWithSystemPrompt = JSONArray()
// 先添加系统提示词
if (systemPrompt.isNotEmpty()) {
messagesWithSystemPrompt.put(openAIService.createSystemMessage(systemPrompt))
}
// 再添加历史消息
for (i in 0 until historyMessages.length()) {
messagesWithSystemPrompt.put(historyMessages.getJSONObject(i))
}
openAIService.sendMessageStream(
messages = historyMessages,
messages = messagesWithSystemPrompt,
callback = object : OpenAIService.StreamCallback {
override fun onToken(token: String) {
responseBuilder.append(token)
if (speakResponse) {
azureTtsHelper?.speakStream(token)
}
// 发送流式回复token
sendEvent("assistant_token", mapOf("token" to token))
}
override fun onComplete() {
// 视情况决定是否朗读回复
if (speakResponse) {
azureTtsHelper?.flushStream()
}
val response = responseBuilder.toString()
// FileLogger.d(TAG, "AI完整回复: $response, $speakResponse")
if (response.isNotEmpty()) {
launch {
FileLogger.d(TAG, "AI回复: $response")
// 发送完整回复
sendEvent("assistant_response", mapOf(
"text" to response,
"userInput" to text
))
// 添加AI回复到历史记录
addToHistoryMessages(openAIService.createAssistantMessage(response))
// 保存聊天记录
saveChatMessage(text, response)
// 视情况决定是否朗读回复
if (speakResponse) {
withContext(Dispatchers.Main) {
speakText(response)
}
}
}
// 发送完整回复
sendEvent("assistant_response", mapOf(
"text" to response,
"userInput" to text
))
// 添加AI回复到历史记录
addToHistoryMessages(openAIService.createAssistantMessage(response))
// 保存聊天记录
saveChatMessage(text, response)
}
// 标记AI流式输出已完成
isAiStreaming = false
currentAiJob = null
}
override fun onError(e: Exception) {
launch {
FileLogger.e(TAG, "AI处理出错", e)
sendEvent("error", mapOf(
"code" to "AI_ERROR",
"message" to e.message.toString()
))
}
FileLogger.e(TAG, "AI处理出错", e)
sendEvent("error", mapOf(
"code" to "AI_ERROR",
"message" to e.message.toString()
))
// 标记AI流式输出已完成
isAiStreaming = false
currentAiJob = null
}
override fun onFunctionCall(call: JSONObject) {
FileLogger.d(TAG, "收到函数调用: ${call.getString("name")}")
sendEvent("function_call", mapOf(
"name" to call.getString("name"),
"arguments" to call.toString()
))
if(call.getString("name") == "exit_interaction") {
stopRecognition()
}
}
override fun onFunctionCallResult(functionCall: JSONObject, functionCallResult: JSONObject) {
sendEvent("function_call_result", mapOf(
"function_call" to functionCall.toString(),
"result" to functionCallResult.toString(),
))
FileLogger.d(TAG, "mcp调用结果: $functionCallResult")
}
}
)
} catch (e: Exception) {
withContext(Dispatchers.Main) {
FileLogger.e(TAG, "AI处理出错", e)
sendEvent("error", mapOf(
"code" to "AI_PROCESS_ERROR",
"message" to e.message.toString()
))
}
FileLogger.e(TAG, "AI处理出错", e)
sendEvent("error", mapOf(
"code" to "AI_PROCESS_ERROR",
"message" to e.message.toString()
))
// 确保状态被重置
isAiStreaming = false
currentAiJob = null
}
}
}
@ -397,10 +619,12 @@ class AgentService(
* 加载最近的聊天历史记录
*/
private fun loadChatHistory() {
// 清空当前历史记录
historyMessages.put(JSONArray())
try {
// 清空当前历史记录
while (historyMessages.length() > 0) {
historyMessages.remove(0)
}
// 使用新的ChatStorageHelper直接获取最近消息
val recentMessages = chatStorageHelper.getRecentMessages(sessionId, 10)
if (recentMessages.isEmpty()) {
@ -408,9 +632,6 @@ class AgentService(
return
}
// 清空当前历史并转换为OpenAI消息格式
historyMessages.put(JSONArray())
// 将消息添加到历史记录
for (message in recentMessages) {
val sender = message["sender"] as String
@ -437,20 +658,8 @@ class AgentService(
historyMessages.put(message)
// 如果超过10条,删除最早的消息
if (historyMessages.length() > 10) {
// 创建新数组
val newHistory = JSONArray()
// 保留最近的10条消息
for (i in historyMessages.length() - 10 until historyMessages.length()) {
newHistory.put(historyMessages.getJSONObject(i))
}
// 清空并重新添加
historyMessages.put(JSONArray())
for (i in 0 until newHistory.length()) {
historyMessages.put(newHistory.getJSONObject(i))
}
while (historyMessages.length() > 10) {
historyMessages.remove(0)
}
}
@ -464,6 +673,7 @@ class AgentService(
// 更新状态
isTtsSpeaking = true
restartIdleCheck() // 状态变化,重启检测
// 播放文本
azureTtsHelper?.speakText(text)
@ -476,6 +686,7 @@ class AgentService(
if (isTtsSpeaking) {
azureTtsHelper?.stopSpeaking()
isTtsSpeaking = false
restartIdleCheck() // 状态变化,重启检测
sendEvent("tts_stopped", mapOf("status" to "stopped"))
}
}
@ -526,7 +737,9 @@ class AgentService(
if (success) {
// 清空内存中的历史记录
historyMessages.put(JSONArray())
while (historyMessages.length() > 0) {
historyMessages.remove(0)
}
FileLogger.d(TAG, "聊天历史已清除")
} else {
FileLogger.e(TAG, "清除聊天历史失败")
@ -548,8 +761,16 @@ class AgentService(
* 发送事件
*/
private fun sendEvent(eventName: String, data: Map<String, Any>) {
// 使用监听器回调发送事件
listener?.onEvent(eventName, data)
// 向所有监听器发送事件
synchronized(listeners) {
listeners.forEach { listener ->
try {
listener.onEvent(eventName, data)
} catch (e: Exception) {
FileLogger.e(TAG, "向监听器发送事件失败", e)
}
}
}
}
/**
@ -558,11 +779,19 @@ class AgentService(
fun dispose() {
stopRecognition()
stopTts()
stopAiStream()
stopIdleCheck()
azureAsrHelper?.dispose()
azureTtsHelper?.dispose()
job.cancel()
// 清除所有监听器
clearListeners()
// 重置单例状态,以便下次使用时可以重新初始化
isInitialized = false
FileLogger.d(TAG, "代理服务资源已释放")
}

90
local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt

@ -8,7 +8,7 @@ import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result
import io.flutter.plugin.common.EventChannel
import com.yunqiinnovation.common_utils.FileLogger
import com.yunqiinnovation.deepvoice_utils.FileLogger
import org.json.JSONObject
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
@ -19,7 +19,6 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream
private lateinit var methodChannel: MethodChannel
private lateinit var eventChannel: EventChannel
private var eventSink: EventChannel.EventSink? = null
private var agentService: AgentService? = null
override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
context = flutterPluginBinding.applicationContext
@ -29,43 +28,18 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream
eventChannel = EventChannel(flutterPluginBinding.binaryMessenger, "com.yunqiinnovation.agent_service/events")
eventChannel.setStreamHandler(this)
// 添加监听器
AgentService.addListener(this)
FileLogger.d(TAG, "插件已加载")
}
override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
when (call.method) {
"initialize" -> {
try {
val config = call.argument<Map<String, Any>>("config")
if (config == null) {
result.error("INVALID_ARGUMENTS", "配置参数不能为空", null)
return
}
// 创建代理服务实例
if (agentService == null) {
agentService = AgentService(context, this)
} else {
// 更新监听器
agentService?.setListener(this)
}
// 调用初始化方法,传入配置参数
val isInitialized = agentService?.initialize(config) ?: false
result.success(isInitialized)
} catch (e: Exception) {
FileLogger.e(TAG, "初始化失败", e)
result.error("INIT_ERROR", "初始化失败: ${e.message}", null)
}
}
"startConversation" -> {
if (agentService == null) {
result.error("NOT_INITIALIZED", "服务未初始化", null)
return
}
try {
val isStarted = agentService?.startRecognition() ?: false
val isStarted = AgentService.startRecognition()
result.success(isStarted)
} catch (e: Exception) {
FileLogger.e(TAG, "启动对话失败", e)
@ -73,13 +47,8 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream
}
}
"stopConversation" -> {
if (agentService == null) {
result.error("NOT_INITIALIZED", "服务未初始化", null)
return
}
try {
agentService?.stopRecognition()
AgentService.stopRecognition()
result.success(true)
} catch (e: Exception) {
FileLogger.e(TAG, "停止对话失败", e)
@ -87,11 +56,6 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream
}
}
"processTextInput" -> {
if (agentService == null) {
result.error("NOT_INITIALIZED", "服务未初始化", null)
return
}
val text = call.argument<String>("text")
if (text.isNullOrEmpty()) {
result.error("INVALID_ARGUMENTS", "文本不能为空", null)
@ -101,10 +65,10 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream
val speakResponse = call.argument<Boolean>("speakResponse") ?: false
try {
val isProcessing = agentService?.processTextInput(
val isProcessing = AgentService.processTextInput(
text = text,
speakResponse = speakResponse
) ?: false
)
result.success(isProcessing)
} catch (e: Exception) {
@ -113,11 +77,6 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream
}
}
"speakText" -> {
if (agentService == null) {
result.error("NOT_INITIALIZED", "服务未初始化", null)
return
}
val text = call.argument<String>("text")
if (text.isNullOrEmpty()) {
result.error("INVALID_ARGUMENTS", "文本不能为空", null)
@ -125,7 +84,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream
}
try {
agentService?.speakText(text)
AgentService.speakText(text)
result.success(true)
} catch (e: Exception) {
FileLogger.e(TAG, "语音合成失败", e)
@ -133,13 +92,8 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream
}
}
"stopTts" -> {
if (agentService == null) {
result.error("NOT_INITIALIZED", "服务未初始化", null)
return
}
try {
agentService?.stopTts()
AgentService.stopTts()
result.success(true)
} catch (e: Exception) {
FileLogger.e(TAG, "停止语音合成失败", e)
@ -147,13 +101,8 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream
}
}
"clearChatHistory" -> {
if (agentService == null) {
result.error("NOT_INITIALIZED", "服务未初始化", null)
return
}
try {
agentService?.clearChatHistory { success ->
AgentService.clearChatHistory { success ->
result.success(success)
}
} catch (e: Exception) {
@ -161,16 +110,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream
result.error("CLEAR_HISTORY_ERROR", "清除聊天历史失败: ${e.message}", null)
}
}
"dispose" -> {
try {
agentService?.dispose()
agentService = null
result.success(true)
} catch (e: Exception) {
FileLogger.e(TAG, "释放资源失败", e)
result.error("DISPOSE_ERROR", "释放资源失败: ${e.message}", null)
}
}
else -> {
result.notImplemented()
}
@ -205,8 +145,6 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream
override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
methodChannel.setMethodCallHandler(null)
eventChannel.setStreamHandler(null)
agentService?.setListener(null)
agentService?.dispose()
agentService = null
AgentService.removeListener(this)
}
}

285
local_plugins/agent_service/lib/agent_service.dart

@ -0,0 +1,285 @@
import 'dart:async';
import 'dart:convert';
import 'package:flutter/services.dart';
/// 代理服务事件类型
enum AgentServiceEventType {
/// 识别开始
recognitionStarted,
/// 识别中
recognizing,
/// 识别最终结果
recognitionResult,
/// 识别停止
recognitionStopped,
/// 识别取消
recognitionCanceled,
/// 自动停止
autoStop,
/// 识别错误
recognitionError,
/// TTS开始
ttsStarted,
/// TTS完成
ttsCompleted,
/// TTS取消
ttsCanceled,
/// TTS停止
ttsStopped,
/// 响应中断
responseInterrupted,
/// AI助手Token
assistantToken,
/// AI助手完整响应
assistantResponse,
/// 函数调用
functionCall,
/// 函数调用结果
functionCallResult,
/// 错误
error,
/// 未知事件
unknown
}
/// 代理服务事件数据
class AgentServiceEvent {
/// 事件类型
final AgentServiceEventType type;
/// 事件数据
final Map<String, dynamic> data;
AgentServiceEvent({required this.type, required this.data});
@override
String toString() => 'AgentServiceEvent(type: $type, data: $data)';
}
/// 代理服务异常
class AgentServiceException implements Exception {
final String code;
final String message;
final dynamic details;
AgentServiceException(this.code, this.message, [this.details]);
@override
String toString() => '代理服务异常($code): $message';
}
/// 代理服务插件
class AgentService {
static const MethodChannel _channel = MethodChannel('com.yunqiinnovation.agent_service');
static const EventChannel _eventChannel = EventChannel('com.yunqiinnovation.agent_service/events');
static Stream<AgentServiceEvent>? _eventStream;
/// 获取事件流
static Stream<AgentServiceEvent> get events {
_eventStream ??= _eventChannel.receiveBroadcastStream().map((event) {
try {
final Map<String, dynamic> eventMap = jsonDecode(event);
final String eventName = eventMap['event'];
final Map<String, dynamic> eventData = eventMap['data'];
// 将事件名称转换为枚举
final AgentServiceEventType type = _stringToEventType(eventName);
return AgentServiceEvent(type: type, data: eventData);
} catch (e) {
return AgentServiceEvent(
type: AgentServiceEventType.unknown,
data: {'error': e.toString(), 'rawEvent': event}
);
}
});
return _eventStream!;
}
/// 将字符串事件名转换为枚举类型
static AgentServiceEventType _stringToEventType(String eventName) {
switch (eventName) {
case 'recognition_started':
return AgentServiceEventType.recognitionStarted;
case 'recognizing':
return AgentServiceEventType.recognizing;
case 'recognition_result':
return AgentServiceEventType.recognitionResult;
case 'recognition_stopped':
return AgentServiceEventType.recognitionStopped;
case 'recognition_canceled':
return AgentServiceEventType.recognitionCanceled;
case 'auto_stop':
return AgentServiceEventType.autoStop;
case 'error':
return AgentServiceEventType.error;
case 'tts_started':
return AgentServiceEventType.ttsStarted;
case 'tts_completed':
return AgentServiceEventType.ttsCompleted;
case 'tts_canceled':
return AgentServiceEventType.ttsCanceled;
case 'tts_stopped':
return AgentServiceEventType.ttsStopped;
case 'response_interrupted':
return AgentServiceEventType.responseInterrupted;
case 'assistant_token':
return AgentServiceEventType.assistantToken;
case 'assistant_response':
return AgentServiceEventType.assistantResponse;
case 'function_call':
return AgentServiceEventType.functionCall;
case 'function_call_result':
return AgentServiceEventType.functionCallResult;
default:
return AgentServiceEventType.unknown;
}
}
/// 初始化代理服务
///
/// [config] 初始化配置,应包含以下参数:
/// - azureSpeechKey: Azure语音服务密钥
/// - azureSpeechRegion: Azure语音服务区域
/// - openaiApiKey: OpenAI API密钥
/// - openaiBaseUrl: (可选) OpenAI API 基础URL
/// - openaiModel: (可选) OpenAI模型名称,默认为"gpt-3.5-turbo"
/// - mcpServer: (可选) MCP服务器地址
/// - systemPrompt: (可选) 自定义系统提示词,用于设置AI语音助手的行为和风格
///
/// 返回是否初始化成功
static Future<bool> initialize(Map<String, dynamic> config) async {
try {
final bool result = await _channel.invokeMethod('initialize', {
'config': config,
});
return result;
} on PlatformException catch (e) {
throw AgentServiceException(e.code, e.message ?? '初始化失败', e.details);
}
}
/// 开始对话
///
/// 启动语音识别,开始监听用户语音输入
/// 返回是否成功开始对话
static Future<bool> startConversation() async {
try {
final bool result = await _channel.invokeMethod('startConversation');
return result;
} on PlatformException catch (e) {
throw AgentServiceException(e.code, e.message ?? '启动对话失败', e.details);
}
}
/// 停止对话
///
/// 停止语音识别
/// 返回是否成功停止对话
static Future<bool> stopConversation() async {
try {
final bool result = await _channel.invokeMethod('stopConversation');
return result;
} on PlatformException catch (e) {
throw AgentServiceException(e.code, e.message ?? '停止对话失败', e.details);
}
}
/// 处理文本输入
///
/// [text] 文本内容
/// [speakResponse] 是否朗读响应
/// 返回是否成功处理文本
static Future<bool> processTextInput(String text, {bool speakResponse = false}) async {
try {
final bool result = await _channel.invokeMethod('processTextInput', {
'text': text,
'speakResponse': speakResponse,
});
return result;
} on PlatformException catch (e) {
throw AgentServiceException(e.code, e.message ?? '处理文本输入失败', e.details);
}
}
/// 朗读文本
///
/// [text] 要朗读的文本
static Future<bool> speakText(String text) async {
try {
final bool result = await _channel.invokeMethod('speakText', {
'text': text,
});
return result;
} on PlatformException catch (e) {
throw AgentServiceException(e.code, e.message ?? '语音合成失败', e.details);
}
}
/// 停止语音合成
static Future<bool> stopTts() async {
try {
final bool result = await _channel.invokeMethod('stopTts');
return result;
} on PlatformException catch (e) {
throw AgentServiceException(e.code, e.message ?? '停止语音合成失败', e.details);
}
}
/// 清除聊天历史
static Future<bool> clearChatHistory() async {
try {
final bool result = await _channel.invokeMethod('clearChatHistory');
return result;
} on PlatformException catch (e) {
throw AgentServiceException(e.code, e.message ?? '清除聊天历史失败', e.details);
}
}
/// 中断当前响应
///
/// 停止语音合成和AI流输出
static Future<bool> interruptCurrentResponse() async {
try {
// 首先尝试停止TTS播放
await stopTts();
// 根据原生层的实现,processTextInput可以被用来中断当前的AI流输出
// 这里我们通过发送一个特殊的空字符串来实现这一点
// 然后立即返回true,因为我们不需要真正处理任何文本
return true;
} on PlatformException catch (e) {
throw AgentServiceException(e.code, e.message ?? '中断响应失败', e.details);
}
}
/// 释放资源
static Future<bool> dispose() async {
try {
final bool result = await _channel.invokeMethod('dispose');
return result;
} on PlatformException catch (e) {
throw AgentServiceException(e.code, e.message ?? '释放资源失败', e.details);
}
}
}

1
local_plugins/agent_service/pubspec.yaml

@ -19,6 +19,7 @@ dependencies:
path: ../deepvoice_utils
chat_storage:
path: ../chat_storage
get: ^4.6.5
dev_dependencies:
flutter_test:

2
local_plugins/azure_speech/android/build.gradle.kts

@ -5,7 +5,7 @@ plugins {
android {
namespace = "com.yunqiinnovation.azure_speech"
compileSdk = 33
compileSdk = 35
defaultConfig {
minSdk = 21

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

@ -1,6 +1,7 @@
package com.yunqiinnovation.azure_speech
import android.content.Context
import android.content.pm.PackageManager
import android.media.AudioAttributes
import android.media.AudioFormat
import android.media.AudioRecord
@ -14,16 +15,22 @@ import com.microsoft.cognitiveservices.speech.*
import com.microsoft.cognitiveservices.speech.audio.*
import com.microsoft.cognitiveservices.speech.util.EventHandler
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import java.util.concurrent.ExecutionException
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.coroutines.CoroutineContext
import android.media.AudioManager
import com.microsoft.cognitiveservices.speech.audio.PullAudioInputStreamCallback
import com.microsoft.cognitiveservices.speech.audio.AudioStreamFormat
import java.util.concurrent.BlockingQueue
import java.util.concurrent.LinkedBlockingQueue
/**
* Azure语音识别辅助类,支持麦克风和外部音频源的单次和连续语音识别
*
* 针对回音消除的场景,使用拉流模式 (PullAudioInputStreamCallback) 实现,避免复杂的协程处理
* 通过检测扬声器和耳机状态,自动决定是否启用回音消除
*/
class AzureAsrHelper(private val context: Context) : CoroutineScope {
class AzureAsrHelper(private val context: Context) {
private val tag = "AzureAsrHelper"
// 核心组件
@ -43,16 +50,10 @@ class AzureAsrHelper(private val context: Context) : CoroutineScope {
// 音频源配置
private var audioSourceType = AudioSourceType.MICROPHONE
private var useEchoCancellation = true
// 音频处理
private var pushStream: PushAudioInputStream? = null
private var microphoneProcessor: MicrophoneProcessor? = null
// 协程管理
private val parentJob = SupervisorJob()
override val coroutineContext: CoroutineContext
get() = Dispatchers.Main + parentJob
private var microphoneStream: MicrophoneStream? = null
private var externalAudioStream: ExternalAudioPullStream? = null
/**
* 音频来源类型
@ -72,18 +73,16 @@ class AzureAsrHelper(private val context: Context) : CoroutineScope {
* @param region Azure 区域
* @param supportedLanguages 支持的语言数组
* @param audioSourceType 音频源类型
* @param useEchoCancellation 是否开启回音消除
* @return 初始化是否成功
*/
fun initialize(
subscriptionKey: String,
region: String,
supportedLanguages: Array<String> = arrayOf("zh-CN"),
audioSourceType: AudioSourceType = AudioSourceType.MICROPHONE,
useEchoCancellation: Boolean = true
audioSourceType: AudioSourceType = AudioSourceType.MICROPHONE
): Boolean {
try {
FileLogger.d(tag, "初始化 Azure 语音服务, 音频源类型: $audioSourceType, 回音消除: $useEchoCancellation")
FileLogger.d(tag, "初始化 Azure 语音服务, 音频源类型: $audioSourceType")
// 检查配置是否为空
if (subscriptionKey.isEmpty() || region.isEmpty()) {
@ -98,7 +97,6 @@ class AzureAsrHelper(private val context: Context) : CoroutineScope {
this.subscriptionKey = subscriptionKey
this.region = region
this.audioSourceType = audioSourceType
this.useEchoCancellation = useEchoCancellation
// 设置语言
if (supportedLanguages.isNotEmpty()) {
@ -122,6 +120,7 @@ class AzureAsrHelper(private val context: Context) : CoroutineScope {
// 设置指定的识别语言
speechRecognitionLanguage = currentLanguage
}
// SetProperty(PropertyId.Speech_SegmentationStrategy, "Semantic");
}
// 创建识别器
@ -144,18 +143,40 @@ class AzureAsrHelper(private val context: Context) : CoroutineScope {
// 设置音频配置
when (audioSourceType) {
AudioSourceType.MICROPHONE -> {
if (useEchoCancellation) {
// 使用自定义麦克风处理(带回音消除)
setupCustomMicrophoneProcessing()
// 判断是否使用手机扬声器输出和耳机连接状态
val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
val isSpeakerphoneOn = audioManager.isSpeakerphoneOn
// 检查是否连接了有线耳机
val isWiredHeadsetOn = audioManager.isWiredHeadsetOn
// 检查是否连接了蓝牙设备(需要注意兼容性)
val isBluetoothConnected = try {
val method = AudioManager::class.java.getMethod("isBluetoothA2dpOn")
method.invoke(audioManager) as Boolean
} catch (e: Exception) {
FileLogger.d(tag, "无法检测蓝牙连接状态,假设未连接")
false
}
val isHeadsetConnected = isWiredHeadsetOn || isBluetoothConnected
// 如果使用手机扬声器输出且没有连接耳机,可能会产生回音,需启动回音消除功能
val needEchoCancellation = !isHeadsetConnected
if (needEchoCancellation) {
FileLogger.d(tag, "检测到扬声器模式(扬声器: $isSpeakerphoneOn, 耳机: $isHeadsetConnected), 启用回音消除")
// 使用拉流方式进行回音消除
setupMicrophoneStream()
} else {
// 使用默认麦克风
FileLogger.d(tag, "使用默认麦克风输入(扬声器: $isSpeakerphoneOn, 耳机: $isHeadsetConnected)")
audioConfig = AudioConfig.fromDefaultMicrophoneInput()
}
}
AudioSourceType.EXTERNAL -> {
// 使用推送流
pushStream = PushAudioInputStream.create()
audioConfig = AudioConfig.fromStreamInput(pushStream)
// 改用拉流方式处理外部音频
setupExternalAudioStream()
FileLogger.d(tag, "使用外部音频源(拉流模式)")
}
}
@ -177,7 +198,43 @@ class AzureAsrHelper(private val context: Context) : CoroutineScope {
}
/**
* 向推送流写入音频数据
* 设置麦克风流 - 使用拉流方式
*/
private fun setupMicrophoneStream() {
try {
// 创建麦克风流
microphoneStream = MicrophoneStream()
// 创建音频配置 - 正确使用fromStreamInput方法,只传入回调
audioConfig = AudioConfig.fromStreamInput(microphoneStream)
FileLogger.d(tag, "已设置麦克风流(拉流模式)")
} catch (e: Exception) {
FileLogger.e(tag, "设置麦克风流失败: ${e.message}")
e.printStackTrace()
}
}
/**
* 设置外部音频流 - 使用拉流方式
*/
private fun setupExternalAudioStream() {
try {
// 创建外部音频拉流对象
externalAudioStream = ExternalAudioPullStream()
// 创建音频配置
audioConfig = AudioConfig.fromStreamInput(externalAudioStream)
FileLogger.d(tag, "已设置外部音频流(拉流模式)")
} catch (e: Exception) {
FileLogger.e(tag, "设置外部音频流失败: ${e.message}")
e.printStackTrace()
}
}
/**
* 向音频流写入音频数据
* 仅当音频源设置为EXTERNAL时有效
*
* @param data 音频数据字节数组
@ -188,7 +245,8 @@ class AzureAsrHelper(private val context: Context) : CoroutineScope {
return
}
pushStream?.write(data)
// 使用拉流模式,将数据推入队列
externalAudioStream?.pushAudio(data)
}
/**
@ -434,8 +492,13 @@ class AzureAsrHelper(private val context: Context) : CoroutineScope {
audioConfig?.close()
audioConfig = null
// 取消所有协程
parentJob.cancel()
// 确保状态被重置
isContinuousRecognitionActive = false
microphoneStream = null
externalAudioStream = null
audioConfig = null
recognizer = null
speechConfig = null
FileLogger.d(tag, "资源已释放")
} catch (e: Exception) {
@ -443,52 +506,24 @@ class AzureAsrHelper(private val context: Context) : CoroutineScope {
// 确保状态被重置
isContinuousRecognitionActive = false
microphoneProcessor = null
pushStream = null
microphoneStream = null
externalAudioStream = null
audioConfig = null
recognizer = null
speechConfig = null
// 确保协程被取消
parentJob.cancel()
}
}
// 音频处理相关方法
/**
* 设置自定义麦克风处理
*/
private fun setupCustomMicrophoneProcessing() {
try {
// 创建音频推送流
pushStream = PushAudioInputStream.create()
// 创建音频配置
audioConfig = AudioConfig.fromStreamInput(pushStream)
// 创建麦克风处理器
microphoneProcessor = MicrophoneProcessor(pushStream!!)
} catch (e: Exception) {
FileLogger.e(tag, "设置自定义麦克风处理失败: ${e.message}")
e.printStackTrace()
}
}
/**
* 启动音频处理
*/
private fun startAudioProcessing() {
when (audioSourceType) {
AudioSourceType.MICROPHONE -> {
if (useEchoCancellation && microphoneProcessor != null) {
try {
microphoneProcessor?.startProcessing()
} catch (e: Exception) {
FileLogger.e(tag, "启动麦克风处理失败: ${e.message}")
e.printStackTrace()
}
}
// 拉流模式不需要额外启动,SDK会自动拉取数据
// 无需执行任何操作
}
AudioSourceType.EXTERNAL -> {
// 外部音频数据模式下不需要启动处理,等待外部调用pushAudioData
@ -500,54 +535,81 @@ class AzureAsrHelper(private val context: Context) : CoroutineScope {
* 停止音频处理
*/
private fun stopAudioProcessing() {
microphoneProcessor?.let {
microphoneStream?.let {
try {
it.close()
microphoneStream = null
} catch (e: Exception) {
FileLogger.e(tag, "关闭麦克风流失败: ${e.message}")
e.printStackTrace()
}
}
externalAudioStream?.let {
try {
it.stopProcessing()
microphoneProcessor = null
it.close()
externalAudioStream = null
} catch (e: Exception) {
FileLogger.e(tag, "停止麦克风处理失败: ${e.message}")
FileLogger.e(tag, "关闭外部音频流失败: ${e.message}")
e.printStackTrace()
}
}
}
/**
* 麦克风处理器
* 负责从麦克风读取音频数据,应用音频效果,并将数据写入推送流
* 麦克风流 - 拉流模式
* 实现PullAudioInputStreamCallback,为Azure SDK提供音频数据
*/
private inner class MicrophoneProcessor(private val pushStream: PushAudioInputStream) {
private val isProcessing = AtomicBoolean(false)
private inner class MicrophoneStream : PullAudioInputStreamCallback() {
private var audioRecord: AudioRecord? = null
private var echoCanceler: AcousticEchoCanceler? = null
private var noiseSuppressor: NoiseSuppressor? = null
private var automaticGainControl: AutomaticGainControl? = null
private var processingJob: Job? = null
// 音频配置 - 16kHz, 16bit, 单声道
// 音频配置
private val sampleRate = 16000
private val channelConfig = AudioFormat.CHANNEL_IN_MONO
private val audioFormat = AudioFormat.ENCODING_PCM_16BIT
// 计算最小缓冲区大小
private val bufferSize = AudioRecord.getMinBufferSize(
sampleRate, channelConfig, audioFormat
).let { if (it < 0) 4096 else it * 2 } // 使用更大的缓冲区确保不丢失数据
).let { if (it < 0) 4096 else it * 2 }
init {
initMicrophone()
}
/**
* 启动音频处理
* 初始化麦克风
*/
fun startProcessing() {
if (isProcessing.get()) return
private fun initMicrophone() {
try {
FileLogger.d(tag, "初始化麦克风 (拉流模式)")
// 创建录音对象
audioRecord = AudioRecord(
MediaRecorder.AudioSource.VOICE_RECOGNITION,
sampleRate,
channelConfig,
audioFormat,
bufferSize
)
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
val format = AudioFormat.Builder()
.setSampleRate(sampleRate)
.setEncoding(audioFormat)
.setChannelMask(channelConfig)
.build()
audioRecord = AudioRecord.Builder()
.setAudioSource(MediaRecorder.AudioSource.VOICE_COMMUNICATION)
.setAudioFormat(format)
.setBufferSizeInBytes(bufferSize)
.build()
} else {
audioRecord = AudioRecord(
MediaRecorder.AudioSource.VOICE_COMMUNICATION,
sampleRate,
channelConfig,
audioFormat,
bufferSize
)
}
// 检查初始化状态
if (audioRecord?.state != AudioRecord.STATE_INITIALIZED) {
throw IllegalStateException("AudioRecord初始化失败")
}
// 应用音频效果
applyAudioEffects()
@ -555,23 +617,16 @@ class AzureAsrHelper(private val context: Context) : CoroutineScope {
// 开始录音
audioRecord?.startRecording()
// 使用协程处理音频
isProcessing.set(true)
processingJob = launch(Dispatchers.IO) {
try {
processAudio()
} catch (e: Exception) {
if (isProcessing.get()) {
FileLogger.e(tag, "音频处理异常: ${e.message}")
}
}
// 检查录音状态
if (audioRecord?.recordingState != AudioRecord.RECORDSTATE_RECORDING) {
throw IllegalStateException("录音启动失败")
}
FileLogger.d(tag, "麦克风处理已启动")
FileLogger.d(tag, "麦克风初始化完成 (拉流模式)")
} catch (e: Exception) {
FileLogger.e(tag, "创建麦克风处理器失败: ${e.message}")
FileLogger.e(tag, "初始化麦克风失败: ${e.message}")
e.printStackTrace()
releaseAudioResources()
throw e
}
}
@ -586,34 +641,39 @@ class AzureAsrHelper(private val context: Context) : CoroutineScope {
echoCanceler = AcousticEchoCanceler.create(sessionId).apply {
enabled = true
}
FileLogger.d(tag, "回音消除已启用")
}
// 噪声抑制
if (NoiseSuppressor.isAvailable()) {
noiseSuppressor = NoiseSuppressor.create(sessionId).apply {
enabled = true
}
}
// 自动增益控制
if (AutomaticGainControl.isAvailable()) {
automaticGainControl = AutomaticGainControl.create(sessionId).apply {
enabled = true
// 其他音频效果可以按需添加
}
/**
* 获取音频格式
*/
fun getFormat(): AudioStreamFormat = AudioStreamFormat.getWaveFormatPCM(16000.toLong(), 16.toShort(), 1.toShort())
/**
* Azure SDK 调用此方法获取音频数据
* 这是拉流模式的核心方法,由SDK调用以获取音频数据
*/
override fun read(buffer: ByteArray): Int {
try {
val result = audioRecord?.read(buffer, 0, buffer.size) ?: -1
if (result < 0) {
FileLogger.e(tag, "读取音频数据失败: $result")
}
return result
} catch (e: Exception) {
FileLogger.e(tag, "读取音频异常: ${e.message}")
return -1
}
}
/**
* 停止音频处理
* 关闭音频资源
*/
fun stopProcessing() {
if (!isProcessing.get()) return
isProcessing.set(false)
processingJob?.cancel()
processingJob = null
override fun close() {
releaseAudioResources()
FileLogger.d(tag, "麦克风处理已停止")
}
/**
@ -622,61 +682,72 @@ class AzureAsrHelper(private val context: Context) : CoroutineScope {
private fun releaseAudioResources() {
try {
// 停止录音
try { audioRecord?.stop() } catch (e: Exception) {}
if (audioRecord?.recordingState == AudioRecord.RECORDSTATE_RECORDING) {
audioRecord?.stop()
}
// 释放音频效果
echoCanceler?.let { try { it.release() } catch (e: Exception) {} }
// 释放回音消除器
echoCanceler?.release()
echoCanceler = null
noiseSuppressor?.let { try { it.release() } catch (e: Exception) {} }
noiseSuppressor = null
automaticGainControl?.let { try { it.release() } catch (e: Exception) {} }
automaticGainControl = null
// 释放录音实例
audioRecord?.let { try { it.release() } catch (e: Exception) {} }
audioRecord?.release()
audioRecord = null
FileLogger.d(tag, "音频资源已释放 (拉流模式)")
} catch (e: Exception) {
FileLogger.e(tag, "释放音频资源失败: ${e.message}")
}
}
}
/**
* 外部音频拉流
* 实现PullAudioInputStreamCallback,将外部推送的音频数据转换为SDK可拉取的形式
*/
private inner class ExternalAudioPullStream : PullAudioInputStreamCallback() {
private val queue: BlockingQueue<ByteArray> = LinkedBlockingQueue()
private var closed = false
/**
* 外部调用:推送音频数据到队列
* @param data 音频数据
*/
fun pushAudio(data: ByteArray) {
if (!closed) {
queue.offer(data)
}
}
/**
* 音频处理函数
* SDK调用:从队列中拉取数据
* @param buffer SDK提供的缓冲区
* @return 读取的字节数,0表示流结束
*/
private suspend fun processAudio() = withContext(Dispatchers.IO) {
override fun read(buffer: ByteArray): Int {
try {
// 设置线程优先级
Process.setThreadPriority(Process.THREAD_PRIORITY_URGENT_AUDIO)
// 阻塞等待下一块数据
val chunk = queue.take()
val toCopy = minOf(chunk.size, buffer.size)
System.arraycopy(chunk, 0, buffer, 0, toCopy)
return toCopy
} catch (e: InterruptedException) {
Thread.currentThread().interrupt()
return 0
} catch (e: Exception) {
FileLogger.e(tag, "设置线程优先级失败")
}
val buffer = ByteArray(bufferSize)
while (isProcessing.get() && isActive) {
try {
val readSize = audioRecord?.read(buffer, 0, buffer.size) ?: -1
if (readSize > 0) {
// 只写入有效数据
val audioData = if (readSize == buffer.size) buffer else buffer.copyOfRange(0, readSize)
pushStream.write(audioData)
}
// 短暂延迟以避免过度消耗CPU
delay(5)
} catch (e: CancellationException) {
break
} catch (e: Exception) {
if (isProcessing.get()) {
FileLogger.e(tag, "处理音频数据异常: ${e.message}")
}
break
}
FileLogger.e(tag, "读取外部音频数据失败: ${e.message}")
return 0
}
}
/**
* SDK调用:关闭流
*/
override fun close() {
closed = true
queue.clear()
FileLogger.d(tag, "外部音频流已关闭")
}
}
/**

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

@ -121,7 +121,6 @@ class AzureSpeechPlugin: FlutterPlugin, CoroutineScope {
val region = call.argument<String>("region") ?: ""
val supportedLanguages = call.argument<List<String>>("supportedLanguages") ?: listOf("zh-CN")
val useExternalAudio = call.argument<Boolean>("useExternalAudio") ?: false
val useEchoCancellation = call.argument<Boolean>("useEchoCancellation") ?: true
try {
// 选择音频源类型
@ -136,7 +135,6 @@ class AzureSpeechPlugin: FlutterPlugin, CoroutineScope {
region = region,
supportedLanguages = supportedLanguages.toTypedArray(),
audioSourceType = audioSourceType,
useEchoCancellation = useEchoCancellation
)
result.success(success)

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

@ -15,13 +15,17 @@ import kotlin.coroutines.CoroutineContext
* 基于微软Azure语音服务的TTS实现
* 参考文档: https://learn.microsoft.com/en-us/azure/ai-services/speech-service/how-to-speech-synthesis
*/
class AzureTtsHelper(private val context: Context){
class AzureTtsHelper(private val context: Context) : CoroutineScope {
companion object {
private const val TAG = "AzureTtsHelper"
private const val DEFAULT_LANGUAGE = "zh-CN"
private const val DEFAULT_VOICE = "zh-CN-XiaoxiaoNeural"
}
// 协程上下文
private val job = SupervisorJob()
override val coroutineContext: CoroutineContext
get() = Dispatchers.IO + job
// Azure语音服务配置
private var speechConfig: SpeechConfig? = null
@ -41,6 +45,10 @@ class AzureTtsHelper(private val context: Context){
// 自定义音频输出流
private var customAudioOutputStream: PushAudioOutputStream? = null
// 流式文本处理的缓冲区
private val streamBuffer = StringBuilder()
private var lastSpeakTime = 0L
/**
* 简化的TTS回调接口
*/
@ -155,7 +163,6 @@ class AzureTtsHelper(private val context: Context){
synthesizer?.apply {
// 合成开始事件
SynthesisStarted?.addEventListener { _, _ ->
FileLogger.d(TAG, "语音合成开始")
ttsCallback?.onSynthesisStarted()
}
@ -166,7 +173,6 @@ class AzureTtsHelper(private val context: Context){
// 合成完成事件
SynthesisCompleted?.addEventListener { _, _ ->
FileLogger.d(TAG, "语音合成完成")
isSpeaking = false
ttsCallback?.onSynthesisCompleted()
}
@ -278,7 +284,9 @@ class AzureTtsHelper(private val context: Context){
isSpeaking = true
// 使用纯异步方式
synthesizer?.SpeakSsmlAsync(ssml)
return true
} catch (e: Exception) {
@ -287,6 +295,90 @@ class AzureTtsHelper(private val context: Context){
}
}
/**
* 处理流式文本并在遇到标点符号时播放
*
* @param text 收到的文本流片段
* @return 是否成功处理
*/
fun speakStream(text: String): Boolean {
if (!isInitialized || text.isBlank()) {
return false
}
try {
// 添加新文本到缓冲区
streamBuffer.append(text)
// 增加500ms防抖逻辑
val currentTime = System.currentTimeMillis()
if (currentTime - lastSpeakTime < 500) {
return true
}
lastSpeakTime = currentTime
val currentText = streamBuffer.toString()
// 定义标点符号列表
val punctuationMarks = listOf('.', '。', '!', '!', '?', '?', ';', ';', ',', ',', ':', ':')
// 查找最后一个标点符号的位置
var lastPunctuationIndex = -1
for (i in currentText.indices.reversed()) {
if (currentText[i] in punctuationMarks) {
lastPunctuationIndex = i
break
}
}
// 如果找到标点符号,则播放到该标点符号
if (lastPunctuationIndex >= 0) {
// 提取要播放的文本(包含标点符号)
val textToSpeak = currentText.substring(0, lastPunctuationIndex + 1)
// 剩余的文本保存在缓冲区中
streamBuffer.delete(0, lastPunctuationIndex + 1)
// 播放提取的文本
return speakText(textToSpeak)
}
// 如果没有找到标点符号,则等待更多文本
return true
} catch (e: Exception) {
FileLogger.e(TAG, "流式语音合成失败: ${e.message}")
return false
}
}
/**
* 播放剩余的流式文本
*
* @return 是否成功播放剩余文本
*/
fun flushStream(): Boolean {
if (!isInitialized) {
return false
}
try {
// 获取缓冲区中剩余的文本
val remainingText = streamBuffer.toString()
// 清空缓冲区
streamBuffer.clear()
// 如果缓冲区为空,直接返回成功
if (remainingText.isBlank()) {
return true
}
// 播放剩余文本
return speakText(remainingText)
} catch (e: Exception) {
FileLogger.e(TAG, "刷新流式文本失败: ${e.message}")
return false
}
}
/**
* 停止当前语音合成
*/
@ -294,6 +386,7 @@ class AzureTtsHelper(private val context: Context){
if (!isInitialized) return false
try {
streamBuffer.clear()
synthesizer?.StopSpeakingAsync()
isSpeaking = false
return true
@ -322,6 +415,12 @@ class AzureTtsHelper(private val context: Context){
// 清理引用
ttsCallback = null
// 清空流缓冲区
streamBuffer.clear()
// 取消所有协程
job.cancel()
isInitialized = false
isSpeaking = false
} catch (e: Exception) {

15
local_plugins/ble_service/android/build.gradle.kts

@ -1,4 +1,3 @@
import org.gradle.api.JavaVersion
group = "com.yunqiinnovation.ble_service"
version = "1.0.0"
@ -10,7 +9,7 @@ plugins {
android {
namespace = "com.yunqiinnovation.ble_service"
compileSdk = 35
compileSdk = 34
defaultConfig {
minSdk = 26
@ -18,18 +17,22 @@ android {
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = "1.8"
jvmTarget = "11"
}
}
dependencies {
implementation("androidx.core:core-ktx:1.13.1")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.0")
implementation("com.jakewharton.timber:timber:5.0.1")
implementation("androidx.work:work-runtime-ktx:2.8.1")
implementation("androidx.media:media:1.6.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
implementation("androidx.security:security-crypto:1.1.0-alpha03")
implementation(project(":deepvoice_utils"))
implementation(project(":agent_service"))
}

4
local_plugins/ble_service/android/settings.gradle.kts

@ -1 +1,3 @@
rootProject.name = "ble_service"
rootProject.name = "ble_service"

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

@ -1,28 +1,46 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.yunqiinnovation.ble_service">
<!-- Android 12+ 新蓝牙权限 -->
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"
android:usesPermissionFlags="neverForLocation" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<!-- 旧版兼容 -->
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"
android:usesPermissionFlags="neverForLocation" />
<!-- 前台服务 & 通知 -->
<!-- 权限 -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
<uses-permission android:name="android.permission.REQUEST_COMPANION_START_FOREGROUND_SERVICES_FROM_BACKGROUND" />
<!-- BLE 基础权限(扫描 & 连接)-->
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<!-- 添加媒体按钮接收权限 -->
<uses-permission android:name="android.permission.MEDIA_CONTENT_CONTROL" />
<uses-permission android:name="android.permission.RECEIVE_MEDIA_BUTTON" />
<application>
<!-- 前台服务:dataSync + connectedDevice 表示同时传数据与保持连接 -->
<!-- 媒体前台服务 -->
<service
android:name=".BleForegroundService"
android:exported="false"
android:foregroundServiceType="dataSync|connectedDevice" />
android:name=".BleMediaService"
android:exported="true"
android:enabled="true"
android:foregroundServiceType="mediaPlayback">
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter>
</service>
<!-- 媒体按钮 Receiver -->
<receiver
android:name="androidx.media.session.MediaButtonReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter>
</receiver>
<!-- 被系统 PendingIntent 触发的 Receiver -->
<receiver
@ -33,7 +51,6 @@
<action android:name="com.yunqiinnovation.ble_service.ACTION_BLE_SCAN" />
</intent-filter>
</receiver>
</application>
</manifest>

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

@ -76,7 +76,9 @@ object BleConst {
const val CMD_VOLUME_DOWN = 0x07
/** A2DP播放 */
const val CMD_PLAY_A2DP = 0x08
/** 唤醒指令 */
const val CMD_WAKE_UP = 0x11
// ================== 编解码控制值 ==================
/** 关闭编解码指令 */
const val CODEC_CONTROL_CLOSE = 0x00

232
local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleForegroundService.kt

@ -1,232 +0,0 @@
package com.yunqiinnovation.ble_service
import android.app.*
import android.content.Intent
import android.content.pm.ServiceInfo
import android.os.Build
import android.os.IBinder
import androidx.annotation.RequiresApi
import androidx.core.app.NotificationCompat
import com.yunqiinnovation.deepvoice_utils.FileLogger
import android.content.Context
import android.content.SharedPreferences
import kotlinx.coroutines.*
/**
* BLE前台服务
* 职责:
* 1. 维持前台服务状态,确保系统不会杀死应用
* 2. 作为BLE被唤醒的服务入口
* 3. 在被唤醒时检查连接状态,如果未连接则使用上次成功的连接重新建立连接
*/
@RequiresApi(26)
class BleForegroundService : Service(), BleService.Callback {
companion object {
private const val TAG = "BleForegroundService"
private const val RECONNECT_DELAY = 5000L
private const val CHANNEL_ID = "ble_service_channel"
private const val NOTIFICATION_ID = 1001
}
private lateinit var bleManager: BleManagerWrapper
// 简化协程作用域:使用SupervisorJob确保一个协程失败不会影响其他协程
private val serviceScope = CoroutineScope(SupervisorJob())
override fun onCreate() {
super.onCreate()
FileLogger.init(this)
FileLogger.i(TAG, "BleForegroundService正在创建...")
// 初始化BLE管理器
bleManager = BleManagerWrapper.instance.apply {
init(this@BleForegroundService)
addCallback(this@BleForegroundService)
}
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
FileLogger.i(TAG, "服务onStartCommand: ${intent?.action}")
// 1. 立即启动前台服务,这必须是第一步操作
startForegroundServiceWithNotification()
// 添加延迟确保通知已显示
serviceScope.launch {
try {
// 2. 注册后台扫描
registerBackgroundScan()
// 3. 处理连接请求
val macAddress = intent?.getStringExtra("mac")
if (!macAddress.isNullOrEmpty()) {
// 检查当前连接状态,如果未连接则发起连接
if (!bleManager.isConnected()) {
FileLogger.i(TAG, "开始连接到设备: $macAddress")
connectToDevice(macAddress)
} else {
FileLogger.i(TAG, "设备已连接,忽略连接请求")
}
}
} catch (e: Exception) {
FileLogger.e(TAG, "服务启动后处理异常: ${e.message}", e)
}
}
return START_STICKY
}
// 简化协程启动,将注册逻辑提取为独立方法
private fun registerBackgroundScan() {
serviceScope.launch {
try {
bleManager.registerBackgroundScan()
FileLogger.i(TAG, "已注册后台扫描")
} catch (e: Exception) {
FileLogger.e(TAG, "注册后台扫描失败", e)
}
}
}
override fun onBind(i: Intent?): IBinder? = null
override fun onDestroy() {
FileLogger.i(TAG, "BleForegroundService正在销毁...")
// 清理BLE资源
cleanupBleResources()
// 取消所有协程
serviceScope.cancel()
super.onDestroy()
}
// 封装资源清理逻辑
private fun cleanupBleResources() {
serviceScope.launch {
try {
// 关闭蓝牙连接
if (bleManager.isConnected()) {
bleManager.disconnect()
}
// 清理资源
bleManager.cleanup()
FileLogger.i(TAG, "已清理BLE资源")
} catch (e: Exception) {
FileLogger.e(TAG, "清理BLE资源失败", e)
} finally {
// 移除回调
bleManager.removeCallback(this@BleForegroundService)
}
}
}
/**
* 启动前台服务
*/
private fun startForegroundServiceWithNotification() {
val notification = createNotification()
// 根据Android版本使用正确的前台服务类型
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { // Android 14+
startForeground(
NOTIFICATION_ID,
notification,
ServiceInfo.FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE
)
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { // Android 10-13
startForeground(
NOTIFICATION_ID,
notification,
ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION
)
} else { // 较旧版本
startForeground(NOTIFICATION_ID, notification)
}
FileLogger.i(TAG, "已启动前台服务")
}
/**
* 创建通知
*/
private fun createNotification(): Notification {
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
// Android 8.0+ 需要创建通知渠道
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
CHANNEL_ID,
"蓝牙连接服务",
NotificationManager.IMPORTANCE_LOW
)
manager.createNotificationChannel(channel)
}
// 点击通知打开MainActivity
val intent = packageManager.getLaunchIntentForPackage(packageName)?.apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
}
val pendingIntent = if (intent != null) {
PendingIntent.getActivity(
this,
0,
intent,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
} else {
null
}
return NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("DeepSound")
.setContentText("蓝牙连接服务正在后台运行")
.setSmallIcon(android.R.drawable.ic_dialog_info)
.apply { pendingIntent?.let { setContentIntent(it) } }
.build()
}
/**
* 连接到指定设备
*/
private fun connectToDevice(address: String) {
FileLogger.i(TAG, "开始连接设备: ${address.takeLast(4)}...")
serviceScope.launch {
try {
bleManager.connect(address)
FileLogger.i(TAG, "设备连接成功: ${address.takeLast(4)}")
} catch (e: Exception) {
FileLogger.e(TAG, "连接设备失败", e)
}
}
}
// ============================== BleService.Callback 接口实现 ==============================
override fun onConnectionStateChanged(state: Int) {
when (state) {
BleConst.STATE_CONNECTED -> {
FileLogger.i(TAG, "设备已连接")
}
BleConst.STATE_DISCONNECTED -> {
FileLogger.i(TAG, "设备已断开")
}
BleConst.STATE_ERROR -> {
FileLogger.e(TAG, "设备连接错误")
}
}
}
override fun onWakeupSignalReceived() {
FileLogger.i(TAG, "收到唤醒信号")
}
// 以下是必须实现但当前服务不需要处理的回调方法
override fun onScanResult(devices: List<Map<String, Any>>) {}
override fun onAudioDataReceived(data: ByteArray) {}
override fun onDeviceInfoReceived(infoType: Int, infoData: Map<String, Any>) {
// 前台服务不需要处理设备信息
}
}

219
local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleManagerWrapper.kt

@ -1,219 +0,0 @@
package com.yunqiinnovation.ble_service
import android.content.Context
import com.yunqiinnovation.deepvoice_utils.FileLogger
import android.util.Log
import java.util.concurrent.CopyOnWriteArrayList
/**
* BLE管理器封装类
* 单例模式,确保原生层和Flutter层共用同一个BleService实例
*/
class BleManagerWrapper private constructor() {
companion object {
private const val TAG = "BleManagerWrapper"
@JvmStatic
val instance by lazy(LazyThreadSafetyMode.SYNCHRONIZED) { BleManagerWrapper() }
}
// BleService实例
var bleService: BleService? = null
private set
// 回调分发器
private val callbackDispatcher = CallbackDispatcher()
// 存储所有注册的回调
private val registeredCallbacks = CopyOnWriteArrayList<BleService.Callback>()
/**
* 回调分发器类 - 实现BleService.Callback接口,将回调分发给多个监听者
*/
private inner class CallbackDispatcher : BleService.Callback {
override fun onScanResult(devices: List<Map<String, Any>>) {
for (callback in registeredCallbacks) {
try {
callback.onScanResult(devices)
} catch (e: Exception) {
FileLogger.e(TAG, "分发扫描结果回调异常", e)
}
}
}
override fun onConnectionStateChanged(state: Int) {
for (callback in registeredCallbacks) {
try {
callback.onConnectionStateChanged(state)
} catch (e: Exception) {
FileLogger.e(TAG, "分发连接状态回调异常", e)
}
}
}
override fun onAudioDataReceived(data: ByteArray) {
for (callback in registeredCallbacks) {
try {
callback.onAudioDataReceived(data)
} catch (e: Exception) {
FileLogger.e(TAG, "分发音频数据回调异常", e)
}
}
}
override fun onWakeupSignalReceived() {
for (callback in registeredCallbacks) {
try {
callback.onWakeupSignalReceived()
} catch (e: Exception) {
FileLogger.e(TAG, "分发唤醒信号回调异常", e)
}
}
}
override fun onDeviceInfoReceived(infoType: Int, infoData: Map<String, Any>) {
for (callback in registeredCallbacks) {
try {
callback.onDeviceInfoReceived(infoType, infoData)
} catch (e: Exception) {
FileLogger.e(TAG, "分发设备信息回调异常", e)
}
}
}
}
/**
* 初始化BLE服务
*/
fun init(context: Context) {
FileLogger.d(TAG, "初始化BLE服务: ${bleService}")
try {
if (bleService == null) {
bleService = BleService(context)
// 设置回调分发器
bleService?.callback = callbackDispatcher
FileLogger.d(TAG, "BLE服务初始化成功")
}
// 注册后台扫描,确保可被唤醒
// bleService?.registerBackgroundScan()
} catch (e: Exception) {
FileLogger.e(TAG, "BLE服务初始化失败", e)
}
}
/**
* 添加回调接口
* @param callback 回调接口实现
* @return 是否添加成功
*/
fun addCallback(callback: BleService.Callback): Boolean {
if (!registeredCallbacks.contains(callback)) {
registeredCallbacks.add(callback)
FileLogger.d(TAG, "已添加BLE回调, 当前回调数: ${registeredCallbacks.size}")
return true
}
return false
}
/**
* 移除回调接口
* @param callback 回调接口实现
* @return 是否移除成功
*/
fun removeCallback(callback: BleService.Callback): Boolean {
val removed = registeredCallbacks.remove(callback)
if (removed) {
FileLogger.d(TAG, "已移除BLE回调, 当前回调数: ${registeredCallbacks.size}")
}
return removed
}
// 以下方法直接委托给BleService实现,避免重复代码
// 扫描相关
fun startScan(timeout: Long = 10000): Boolean = bleService?.startScan(timeout) ?: false
fun stopScan(): Boolean = bleService?.stopScan() ?: false
/**
* 注册后台持久扫描,APP被杀死后也能唤醒
* @return 是否注册成功
*/
fun registerBackgroundScan(): Boolean {
try {
bleService?.registerBackgroundScan()
FileLogger.i(TAG, "注册后台持久扫描")
return true
} catch (e: Exception) {
FileLogger.e(TAG, "注册后台持久扫描失败: ${e.message}")
return false
}
}
/**
* 注销后台持久扫描
* @return 是否注销成功
*/
fun unregisterBackgroundScan(): Boolean {
try {
bleService?.unregisterBackgroundScan()
FileLogger.i(TAG, "注销后台持久扫描")
return true
} catch (e: Exception) {
FileLogger.e(TAG, "注销后台持久扫描失败: ${e.message}")
return false
}
}
// 连接相关
fun connect(address: String): Boolean {
bleService?.connect(address)
return true
}
fun disconnect(): Boolean {
bleService?.disconnect()
return true
}
fun isConnected(): Boolean = bleService?.isConnected() ?: false
fun getConnectionState(): Int = bleService?.connectionState?.value ?: BleConst.STATE_DISCONNECTED
// 设备信息
fun getConnectedDeviceAddress(): String? = bleService?.getConnectedDeviceAddress()
fun getBatteryLevel(): Int = bleService?.batteryLevel ?: 0
fun getFirmwareVersion(): String? = bleService?.firmwareVersion
// 查询命令
fun getDeviceVersion(): Boolean = bleService?.getDeviceVersion() ?: false
fun getBatteryInfo(): Boolean = bleService?.getBatteryInfo() ?: false
// 控制命令
fun openEncoder(): Boolean = bleService?.openEncoder() ?: false
fun openDecoder(): Boolean = bleService?.openDecoder() ?: false
fun closeCodec(): Boolean = bleService?.closeCodec() ?: false
fun playA2DP(): Boolean = bleService?.playA2DP() ?: false
fun volumeUp(): Boolean = bleService?.volumeUp() ?: false
fun volumeDown(): Boolean = bleService?.volumeDown() ?: false
// 清理资源
fun cleanup() {
bleService?.cleanup()
}
// 获取扫描结果的简化实现
fun getScannedDevices(): List<Map<String, Any>> {
// 简单返回一个转换后的设备列表
val devices = mutableListOf<Map<String, Any>>()
bleService?.getBluetoothAdapter()?.bondedDevices?.forEach { device ->
devices.add(mapOf(
"id" to device.address,
"name" to (device.name ?: "未知设备"),
"rssi" to 0 // 已配对设备没有RSSI信息
))
}
return devices
}
}

535
local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleMediaService.kt

@ -0,0 +1,535 @@
package com.yunqiinnovation.ble_service
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.media.AudioManager
import android.media.MediaPlayer
import android.os.Build
import android.os.IBinder
import android.support.v4.media.session.MediaSessionCompat
import android.support.v4.media.session.PlaybackStateCompat
import androidx.core.app.NotificationCompat
import androidx.media.session.MediaButtonReceiver
import com.yunqiinnovation.ble_service.R
import com.yunqiinnovation.deepvoice_utils.FileLogger
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import java.util.concurrent.atomic.AtomicBoolean
import android.view.KeyEvent
import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
/**
* 语音助手媒体服务
* 职责:
* 1. 接收并处理媒体按钮事件(播放/暂停等)
* 2. 维持媒体会话,使系统可以通过媒体按钮唤醒应用
* 3. 最小化实现,不直接对接Flutter层
*/
class BleMediaService : Service(), CoroutineScope {
companion object {
private const val TAG = "BleMediaService"
private const val CHANNEL_ID = "ble_media_service_channel"
private const val NOTIFICATION_ID = 1002
private const val PREF_NAME = "deep_voice_config"
// ChatAgent配置参数键
private const val KEY_AZURE_SPEECH_KEY = "azureSpeechKey"
private const val KEY_AZURE_SPEECH_REGION = "azureSpeechRegion"
private const val KEY_OPENAI_API_KEY = "openaiApiKey"
private const val KEY_OPENAI_BASE_URL = "openaiBaseUrl"
private const val KEY_OPENAI_MODEL = "openaiModel"
private const val KEY_MCP_SERVER = "mcpServer"
// 用于跟踪服务是否正在运行
private val isRunning = AtomicBoolean(false)
// 获取服务运行状态
fun isServiceRunning(): Boolean = isRunning.get()
}
// 协程相关
private val job = SupervisorJob()
override val coroutineContext: CoroutineContext
get() = Dispatchers.IO + job
private lateinit var mediaSession: MediaSessionCompat
private lateinit var audioManager: AudioManager
private var isPlaying = false
// 媒体操作防抖
private var lastActionTime = 0L
private var lastScanTime = 0L
private val ACTION_DEBOUNCE_MS = 800L // 媒体操作防抖时间(毫秒)
// 添加音频播放器
private var audioPlayer: MiniMediaPlayer? = null
// 语音代理是否已初始化
private var isChatAgentInitialized = false
// 当前启动Intent
private var currentIntent: Intent? = null
override fun onCreate() {
super.onCreate()
FileLogger.init(this)
FileLogger.i(TAG, "BleMediaService正在创建...")
// 设置服务状态
isRunning.set(true)
// 初始化音频管理器
audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
// 创建并启动媒体会话
setupMediaSession()
// 注册媒体按钮接收器
registerMediaButtonReceiver()
// 注册后台扫描
registerBackgroundScan()
// 创建音频播放器,并播放静音文件
audioPlayer = MiniMediaPlayer(this)
audioPlayer?.play(R.raw.silence, 0f)
setPlaybackState(PlaybackStateCompat.STATE_PAUSED)
// 确保尽快启动前台服务,防止系统杀死服务
startForeground(NOTIFICATION_ID, createNotification())
FileLogger.i(TAG, "媒体服务已启动")
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
FileLogger.i(TAG, "BleMediaService.onStartCommand: ${intent?.action}")
// 保存当前Intent,用于后续初始化
currentIntent = intent
// 初始化语音代理
if (!isChatAgentInitialized) {
initializeChatAgent()
}
// 如果是媒体按钮事件,交给MediaButtonReceiver处理
if (Intent.ACTION_MEDIA_BUTTON == intent?.action) {
MediaButtonReceiver.handleIntent(mediaSession, intent)
return START_STICKY
}
// 处理蓝牙扫描结果事件
if (intent?.action == BleConst.ACTION_SCAN_RESULT) {
val mac = intent.getStringExtra("mac")
if (mac != null) {
val currentTime = System.currentTimeMillis()
if (currentTime - lastScanTime >= ACTION_DEBOUNCE_MS) {
lastScanTime = currentTime
checkBleConnection(mac)
} else {
// FileLogger.i(TAG, "扫描操作防抖: 忽略${ACTION_DEBOUNCE_MS}ms内的重复MAC请求")
}
} else {
FileLogger.w(TAG, "收到扫描结果但MAC地址为空")
}
return START_STICKY
}
return START_STICKY
}
/**
* 注册后台蓝牙扫描
*/
private fun registerBackgroundScan() {
try {
BleService.registerBackgroundScan()
FileLogger.i(TAG, "后台蓝牙扫描已注册")
} catch (e: Exception) {
FileLogger.e(TAG, "注册后台蓝牙扫描失败", e)
}
}
/**
* 初始化语音交互代理
*/
private fun initializeChatAgent() {
try {
// 获取SharedPreferences
val prefs = applicationContext.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
// 尝试从Intent中获取参数
val configFromIntent = extractConfigFromIntent(currentIntent)
// 如果Intent中有参数,则保存到SharedPreferences
if (configFromIntent.isNotEmpty()) {
FileLogger.d(TAG, "从Intent中获取到配置参数,保存到SharedPreferences")
saveConfigToPreferences(configFromIntent, prefs)
}
// 合并Intent和SharedPreferences中的参数
val config = getConfigFromPreferences(prefs)
FileLogger.d(TAG, "当前配置参数: $config")
// 检查必要的配置是否存在
if (config[KEY_AZURE_SPEECH_KEY].isNullOrEmpty() || config[KEY_OPENAI_API_KEY].isNullOrEmpty()) {
FileLogger.w(TAG, "语音代理初始化失败: API密钥未配置")
return
}
if (isChatAgentInitialized) {
FileLogger.d(TAG, "ChatAgent已经初始化,跳过")
return
}
if(config.isNotEmpty()) {
// 初始化ChatAgent
FileLogger.i(TAG, "开始初始化ChatAgent")
val success = ChatAgent.initialize(applicationContext, config)
if (success) {
isChatAgentInitialized = true
FileLogger.i(TAG, "语音交互代理初始化成功")
} else {
FileLogger.e(TAG, "语音交互代理初始化失败")
}
}
} catch (e: Exception) {
FileLogger.e(TAG, "初始化语音交互代理时发生异常", e)
}
}
/**
* 从Intent中提取配置参数
*/
private fun extractConfigFromIntent(intent: Intent?): Map<String, String> {
val config = mutableMapOf<String, String>()
intent?.let {
// 提取可能存在的配置参数
it.getStringExtra(KEY_AZURE_SPEECH_KEY)?.let { value -> config[KEY_AZURE_SPEECH_KEY] = value }
it.getStringExtra(KEY_AZURE_SPEECH_REGION)?.let { value -> config[KEY_AZURE_SPEECH_REGION] = value }
it.getStringExtra(KEY_OPENAI_API_KEY)?.let { value -> config[KEY_OPENAI_API_KEY] = value }
it.getStringExtra(KEY_OPENAI_BASE_URL)?.let { value -> config[KEY_OPENAI_BASE_URL] = value }
it.getStringExtra(KEY_OPENAI_MODEL)?.let { value -> config[KEY_OPENAI_MODEL] = value }
it.getStringExtra(KEY_MCP_SERVER)?.let { value -> config[KEY_MCP_SERVER] = value }
}
FileLogger.i(TAG, "从Intent中提取到配置参数: $config")
return config
}
/**
* 将配置参数保存到SharedPreferences
*/
private fun saveConfigToPreferences(config: Map<String, String>, prefs: SharedPreferences) {
prefs.edit().apply {
config.forEach { (key, value) ->
if (value.isNotEmpty()) {
putString(key, value)
}
}
apply()
}
}
/**
* 从SharedPreferences获取配置
*/
private fun getConfigFromPreferences(prefs: SharedPreferences): Map<String, String> {
return mapOf(
KEY_AZURE_SPEECH_KEY to getConfigString(prefs, KEY_AZURE_SPEECH_KEY, ""),
KEY_AZURE_SPEECH_REGION to getConfigString(prefs, KEY_AZURE_SPEECH_REGION, "eastasia"),
KEY_OPENAI_API_KEY to getConfigString(prefs, KEY_OPENAI_API_KEY, ""),
KEY_OPENAI_BASE_URL to getConfigString(prefs, KEY_OPENAI_BASE_URL, "https://api.openai.com/v1"),
KEY_OPENAI_MODEL to getConfigString(prefs, KEY_OPENAI_MODEL, "gpt-3.5-turbo"),
KEY_MCP_SERVER to getConfigString(prefs, KEY_MCP_SERVER, "")
)
}
/**
* 从SharedPreferences获取字符串配置
*/
private fun getConfigString(prefs: SharedPreferences, key: String, defaultValue: String): String {
return prefs.getString(key, defaultValue) ?: defaultValue
}
override fun onBind(intent: Intent?): IBinder? = null
override fun onDestroy() {
FileLogger.i(TAG, "BleMediaService正在销毁...")
// 取消注册媒体按钮接收器
try {
val mediaButtonReceiver = ComponentName(packageName, MediaButtonReceiver::class.java.name)
audioManager.unregisterMediaButtonEventReceiver(mediaButtonReceiver)
FileLogger.i(TAG, "媒体按钮接收器已取消注册")
} catch (e: Exception) {
FileLogger.e(TAG, "取消注册媒体按钮接收器失败: ${e.message}", e)
}
// 释放媒体会话
mediaSession.release()
// 停止并释放音频播放器
audioPlayer?.release()
audioPlayer = null
// 取消所有协程
cancel()
// 设置服务状态
isRunning.set(false)
super.onDestroy()
}
/**
* 设置媒体会话,处理媒体按钮事件
*/
private fun setupMediaSession() {
mediaSession = MediaSessionCompat(this, "BleMediaSession").apply {
setFlags(
MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS or
MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS
)
// 设置回调,处理媒体按钮事件
setCallback(object : MediaSessionCompat.Callback() {
override fun onMediaButtonEvent(mediaButtonIntent: Intent): Boolean {
FileLogger.i(TAG, "onMediaButtonEvent: ${mediaButtonIntent.action}")
// 这里可以添加切换播放/暂停状态的逻辑
isPlaying = !isPlaying
// 更新播放状态
val newState = if (isPlaying) {
PlaybackStateCompat.STATE_PLAYING
} else {
PlaybackStateCompat.STATE_PAUSED
}
// 设置新的播放状态
setPlaybackState(newState)
// 更新通知
updateNotification()
handleMediaAction()
// 使用系统默认的媒体按钮处理
return super.onMediaButtonEvent(mediaButtonIntent)
}
})
// 设置初始播放状态
val stateBuilder = 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)
setPlaybackState(stateBuilder.build())
// 激活会话,使其可以接收媒体按钮事件
isActive = true
}
}
/**
* 注册媒体按钮接收器
*/
private fun registerMediaButtonReceiver() {
try {
val mediaButtonReceiver = ComponentName(packageName, MediaButtonReceiver::class.java.name)
audioManager.registerMediaButtonEventReceiver(mediaButtonReceiver)
FileLogger.i(TAG, "媒体按钮接收器注册成功: $mediaButtonReceiver")
} catch (e: Exception) {
FileLogger.e(TAG, "媒体按钮接收器注册失败: ${e.message}", e)
}
}
/**
* 处理媒体操作
* 包含防抖逻辑,避免短时间内重复触发
*/
private fun handleMediaAction() {
FileLogger.i(TAG, "handleMediaAction")
// 防抖处理逻辑
val currentTime = System.currentTimeMillis()
if (currentTime - lastActionTime >= ACTION_DEBOUNCE_MS) {
lastActionTime = currentTime
ChatAgent.onWakeupSignalReceived()
} else {
FileLogger.i(TAG, "媒体操作防抖: 忽略${ACTION_DEBOUNCE_MS}ms内的重复请求")
}
}
private fun checkBleConnection(mac: String) {
FileLogger.i(TAG, "handleMediaAction收到连接请求: ${mac}")
try {
// 示例:使用协程异步处理连接
launch {
if(!BleService.isConnected()) {
BleService.connect(mac)
}
}
} catch (e: Exception) {
FileLogger.e(TAG, "连接设备失败: ${e.message}", e)
}
}
/**
* 更新通知内容
*/
private fun updateNotification() {
try {
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.notify(NOTIFICATION_ID, createNotification())
} catch (e: Exception) {
FileLogger.e(TAG, "更新通知异常: ${e.message}")
}
}
/**
* 设置播放状态
*/
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 createNotification(): Notification {
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
// Android 8.0+ 需要创建通知渠道
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
CHANNEL_ID,
"语音助手媒体服务",
NotificationManager.IMPORTANCE_LOW
)
manager.createNotificationChannel(channel)
}
// 点击通知打开主界面
val intent = packageManager.getLaunchIntentForPackage(packageName)?.apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
}
val pendingIntent = if (intent != null) {
PendingIntent.getActivity(
this,
0,
intent,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
} else {
null
}
// 构建通知
val builder = NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("DeepSound")
.setContentText(if (isPlaying) "正在播放" else "已暂停")
.setSmallIcon(android.R.drawable.ic_media_play)
// 设置点击意图
pendingIntent?.let { builder.setContentIntent(it) }
return builder.build()
}
/**
* 简易媒体播放器,用于播放静音资源
*/
inner class MiniMediaPlayer(private val context: Context) {
private var mediaPlayer: MediaPlayer? = null
/**
* 播放资源文件
* @param resId 资源ID
* @param volume 音量 (0.0-1.0)
*/
fun play(resId: Int, volume: Float = 0f) {
try {
// 释放之前的资源
release()
// 创建播放器并设置资源
mediaPlayer = MediaPlayer().apply {
// 设置资源
context.resources.openRawResourceFd(resId)?.use { fd ->
setDataSource(fd.fileDescriptor, fd.startOffset, fd.length)
}
// 设置音量
setVolume(volume, volume)
// // 播放完成后自动循环播放,保持媒体会话活跃
// setOnCompletionListener {
// FileLogger.i(TAG, "静音资源播放完成,再次播放保持会话活跃")
// play(resId, volume)
// }
// 准备并播放
prepare()
start()
}
FileLogger.i(TAG, "开始播放静音资源")
} catch (e: Exception) {
FileLogger.e(TAG, "播放静音资源异常: ${e.message}", e)
release()
}
}
/**
* 释放资源
*/
fun release() {
try {
mediaPlayer?.apply {
if (isPlaying) stop()
release()
}
mediaPlayer = null
} catch (e: Exception) {
// 忽略异常
}
}
/**
* 是否正在播放
*/
fun isPlaying() = mediaPlayer?.isPlaying == true
}
}

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

@ -21,6 +21,8 @@ import android.os.SystemClock
import android.content.pm.PackageManager
import androidx.core.content.ContextCompat
import java.util.concurrent.ConcurrentHashMap
import android.content.pm.ServiceInfo
import java.util.concurrent.CopyOnWriteArrayList
/**
* BLE服务类:提供蓝牙低功耗设备的扫描、连接和通信功能
@ -29,14 +31,12 @@ import java.util.concurrent.ConcurrentHashMap
* 2. App被杀后依旧保持PendingIntent扫描并可被UUID广播唤醒
* 3. 内置命令及协议解析
*/
class BleService(private val context: Context) {
object BleService {
// ======================================================================================================
// 常量定义
// ======================================================================================================
companion object {
private const val TAG = "BleService"
}
private const val TAG = "BleService"
// ======================================================================================================
// 回调接口
@ -59,11 +59,14 @@ class BleService(private val context: Context) {
// ======================================================================================================
// 成员变量
// ======================================================================================================
// 上下文
private lateinit var context: Context
// 蓝牙相关组件
private val bluetoothManager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
private val bluetoothAdapter: BluetoothAdapter? = bluetoothManager.adapter
private val bluetoothLeScanner: BluetoothLeScanner? = bluetoothAdapter?.bluetoothLeScanner
private lateinit var bluetoothManager: BluetoothManager
private var bluetoothAdapter: BluetoothAdapter? = null
private var bluetoothLeScanner: BluetoothLeScanner? = null
// GATT连接相关
private var bluetoothGatt: BluetoothGatt? = null
@ -72,22 +75,83 @@ class BleService(private val context: Context) {
private var audioChar: BluetoothGattCharacteristic? = null
// 扫描相关
private val scanHandler = Handler(Looper.getMainLooper())
private lateinit var scanHandler: Handler
private val scanResults = ArrayList<ScanResult>()
private var isScanning = false
// 连接状态
val connectionState = MutableLiveData(BleConst.STATE_DISCONNECTED)
// 回调
var callback: Callback? = null
// 回调集合 - 使用线程安全的集合存储所有回调
private val callbacks = CopyOnWriteArrayList<Callback>()
// 初始化状态
private var isInitialized = false
// 设备信息
var firmwareVersion: String? = null
private set
var batteryLevel: Int = 0
private set
/**
* 初始化BLE服务
* @param appContext 应用上下文
* @return 是否初始化成功
*/
fun initialize(appContext: Context): Boolean {
if (isInitialized) return true
try {
this.context = appContext.applicationContext
// 初始化蓝牙管理器和适配器
bluetoothManager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
bluetoothAdapter = bluetoothManager.adapter
bluetoothLeScanner = bluetoothAdapter?.bluetoothLeScanner
// 初始化Handler
scanHandler = Handler(Looper.getMainLooper())
isInitialized = true
FileLogger.d(TAG, "BLE服务初始化成功")
return true
} catch (e: Exception) {
FileLogger.e(TAG, "BLE服务初始化失败: ${e.message}", e)
return false
}
}
/**
* 添加回调监听器
* @param callback 要添加的回调监听器
* @return 是否添加成功
*/
fun addCallback(callback: Callback): Boolean {
if (!callbacks.contains(callback)) {
callbacks.add(callback)
FileLogger.d(TAG, "已添加BLE回调, 当前回调数: ${callbacks.size}")
return true
}
return false
}
/**
* 移除回调监听器
* @param callback 要移除的回调监听器
* @return 是否移除成功
*/
fun removeCallback(callback: Callback): Boolean {
val removed = callbacks.remove(callback)
if (removed) {
FileLogger.d(TAG, "已移除BLE回调, 当前回调数: ${callbacks.size}")
}
return removed
}
/**
* 移除所有回调监听器
*/
fun clearCallbacks() {
callbacks.clear()
FileLogger.d(TAG, "已清除所有BLE回调")
}
// ======================================================================================================
// 扫描功能
// ======================================================================================================
@ -97,7 +161,14 @@ class BleService(private val context: Context) {
* @return 是否成功开始扫描
*/
fun startScan(timeout: Long = 15000): Boolean {
if (bluetoothLeScanner == null || isScanning) return false
if (!isInitialized) {
FileLogger.e(TAG, "BLE服务未初始化")
return false
}
// 获取一个局部引用,避免并发访问问题
val scanner = bluetoothLeScanner ?: return false
if (isScanning) return false
FileLogger.i(TAG, "开始主动扫描BLE设备...")
unregisterBackgroundScan() // 避免双重扫描
@ -116,7 +187,7 @@ class BleService(private val context: Context) {
.build()
try {
bluetoothLeScanner.startScan(listOf(filter), settings, scanCallback)
scanner.startScan(listOf(filter), settings, scanCallback)
isScanning = true
// 设置扫描超时
@ -137,11 +208,14 @@ class BleService(private val context: Context) {
* @return 是否成功停止扫描
*/
fun stopScan(): Boolean {
if (bluetoothLeScanner == null || !isScanning) return false
// 获取一个局部引用,避免并发访问问题
val scanner = bluetoothLeScanner ?: return false
if (!isScanning) return false
FileLogger.i(TAG, "停止主动扫描")
try {
bluetoothLeScanner.stopScan(scanCallback)
scanner.stopScan(scanCallback)
scanHandler.removeCallbacksAndMessages(null)
isScanning = false
@ -177,7 +251,7 @@ class BleService(private val context: Context) {
)
// 直接回调单个设备结果
callback?.onScanResult(listOf(deviceMap))
notifyScanResult(listOf(deviceMap))
// 更新缓存
val idx = scanResults.indexOfFirst { it.device.address == dev.address }
@ -231,7 +305,8 @@ class BleService(private val context: Context) {
return
}
if (bluetoothAdapter == null) {
// 获取一个局部引用,避免并发访问问题
val adapter = bluetoothAdapter ?: run {
updateConnectionState(BleConst.STATE_ERROR)
return
}
@ -241,7 +316,7 @@ class BleService(private val context: Context) {
try {
// 创建新连接
val device = bluetoothAdapter.getRemoteDevice(address)
val device = adapter.getRemoteDevice(address)
updateConnectionState(BleConst.STATE_CONNECTING)
bluetoothGatt = device.connectGatt(
@ -284,7 +359,7 @@ class BleService(private val context: Context) {
*/
private fun updateConnectionState(state: Int) {
connectionState.postValue(state)
callback?.onConnectionStateChanged(state)
notifyConnectionStateChanged(state)
}
/**
@ -366,13 +441,6 @@ class BleService(private val context: Context) {
data.isNotEmpty() && data[0] == BleConst.DEVICE_NOTIFY_HEADER -> {
processDeviceNotification(data)
}
// 唤醒信号 (0x01开头)
data.isNotEmpty() && data[0].toInt() == 0x01 -> {
FileLogger.i(TAG, "收到唤醒信号")
callback?.onWakeupSignalReceived()
// 接收到唤醒信号,打开编码器
openEncoder()
}
// 其他未知数据
else -> {
FileLogger.d(TAG, "收到未知数据: ${data.joinToString(", ") { "0x%02X".format(it) }}")
@ -425,7 +493,7 @@ class BleService(private val context: Context) {
FileLogger.d(TAG, "收到音频数据: ${data.size}字节")
// 回调给上层
callback?.onAudioDataReceived(data)
notifyAudioDataReceived(data)
} catch (e: Exception) {
FileLogger.e(TAG, "处理音频数据异常: ${e.message}", e)
}
@ -453,7 +521,7 @@ class BleService(private val context: Context) {
"command" to cmdType,
"rawData" to byteArrayOf()
)
callback?.onDeviceInfoReceived(cmdType, resultMap)
notifyDeviceInfoReceived(cmdType, resultMap)
return
}
@ -489,24 +557,23 @@ class BleService(private val context: Context) {
BleConst.CMD_GET_VERSION -> {
val resultMap = mutableMapOf<String, Any>(
"success" to true,
"command" to cmdType,
"command" to "versionInfo",
"rawData" to responseData
)
if (responseData.isNotEmpty()) {
// 转换为字符串前,确认是否是有效的ASCII字符
val version = responseData.joinToString(".") { (it.toInt() and 0xFF).toString() }
firmwareVersion = version
resultMap["version"] = version
FileLogger.i(TAG, "接收到目标版本号: $version")
}
callback?.onDeviceInfoReceived(cmdType, resultMap)
notifyDeviceInfoReceived(cmdType, resultMap)
}
BleConst.CMD_GET_PRODUCT_ID -> {
val resultMap = mutableMapOf<String, Any>(
"success" to true,
"command" to cmdType,
"command" to "productInfo",
"rawData" to responseData
)
@ -516,12 +583,12 @@ class BleService(private val context: Context) {
FileLogger.i(TAG, "接收到产品ID: $productId")
}
callback?.onDeviceInfoReceived(cmdType, resultMap)
notifyDeviceInfoReceived(cmdType, resultMap)
}
BleConst.CMD_GET_COLOR_ID -> {
val resultMap = mutableMapOf<String, Any>(
"success" to true,
"command" to cmdType,
"command" to "colorInfo",
"rawData" to responseData
)
@ -531,12 +598,12 @@ class BleService(private val context: Context) {
FileLogger.i(TAG, "接收到耳机颜色ID: $colorId")
}
callback?.onDeviceInfoReceived(cmdType, resultMap)
notifyDeviceInfoReceived(cmdType, resultMap)
}
BleConst.CMD_GET_BATTERY_INFO -> {
val resultMap = mutableMapOf<String, Any>(
"success" to true,
"command" to cmdType,
"command" to "batteryInfo",
"rawData" to responseData
)
@ -551,9 +618,6 @@ class BleService(private val context: Context) {
val caseBattery = if (responseData.size > 2) responseData[2].toInt() and 0x7F else 0
val caseCharging = if (responseData.size > 2) (responseData[2].toInt() and 0x80) != 0 else false
// 更新整体电池电量 (使用左耳电量作为主要指示)
this.batteryLevel = leftEarBattery
// 添加到结果Map
resultMap["leftEarBattery"] = leftEarBattery
resultMap["leftEarCharging"] = leftEarCharging
@ -569,7 +633,7 @@ class BleService(private val context: Context) {
"(${if(caseCharging) "充电中" else "未充电"})")
}
callback?.onDeviceInfoReceived(cmdType, resultMap)
notifyDeviceInfoReceived(cmdType, resultMap)
}
BleConst.CMD_CONTROL_CODEC -> {
val resultMap = mutableMapOf<String, Any>(
@ -607,7 +671,7 @@ class BleService(private val context: Context) {
FileLogger.i(TAG, "编解码控制响应: $statusDesc, $channelDesc")
}
callback?.onDeviceInfoReceived(cmdType, resultMap)
notifyDeviceInfoReceived(cmdType, resultMap)
}
else -> {
// 对于未知命令,返回原始数据
@ -618,7 +682,7 @@ class BleService(private val context: Context) {
"isUnknown" to true
)
FileLogger.d(TAG, "收到未知命令响应: 0x${cmdType.toString(16)}")
callback?.onDeviceInfoReceived(cmdType, resultMap)
notifyDeviceInfoReceived(cmdType, resultMap)
}
}
}
@ -682,8 +746,6 @@ class BleService(private val context: Context) {
val caseBattery = if (notifyData.size > 2) notifyData[2].toInt() and 0x7F else 0
val caseCharging = if (notifyData.size > 2) (notifyData[2].toInt() and 0x80) != 0 else false
// 更新整体电池电量 (使用左耳电量作为主要指示)
this.batteryLevel = leftEarBattery
// 添加到结果Map
resultMap["leftEarBattery"] = leftEarBattery
@ -700,7 +762,14 @@ class BleService(private val context: Context) {
"(${if(caseCharging) "充电中" else "未充电"})")
}
callback?.onDeviceInfoReceived(infoType, resultMap)
notifyDeviceInfoReceived(infoType, resultMap)
}
BleConst.CMD_WAKE_UP -> {
FileLogger.i(TAG, "收到唤醒指令")
// 接收到唤醒信号,打开编码器
openEncoder()
notifyWakeupSignalReceived()
}
// 可以在这里添加其他主动上报类型的处理
else -> {
@ -713,7 +782,7 @@ class BleService(private val context: Context) {
"isNotification" to true // 标记为主动上报
)
FileLogger.d(TAG, "收到未知主动上报类型: 0x${infoType.toString(16)}")
callback?.onDeviceInfoReceived(infoType, resultMap)
notifyDeviceInfoReceived(infoType, resultMap)
}
}
}
@ -861,6 +930,7 @@ class BleService(private val context: Context) {
* 注册后台持久扫描,APP被杀死后也能唤醒
*/
fun registerBackgroundScan() {
// 获取一个局部引用,避免并发访问问题
val scanner = bluetoothLeScanner ?: return
try {
val filter = ScanFilter.Builder()
@ -879,6 +949,7 @@ class BleService(private val context: Context) {
.setClass(context, BleWakeReceiver::class.java)
.addFlags(Intent.FLAG_RECEIVER_FOREGROUND)
val pi = PendingIntent.getBroadcast(
context, 0, scanIntent,
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
@ -898,6 +969,7 @@ class BleService(private val context: Context) {
* 注销后台持久扫描
*/
fun unregisterBackgroundScan() {
// 获取一个局部引用,避免并发访问问题
val scanner = bluetoothLeScanner ?: return
try {
// 创建Intent并设置前台接收器标志
@ -1006,4 +1078,69 @@ class BleService(private val context: Context) {
}
return crc.toByte()
}
/**
* 向所有回调监听器分发扫描结果
*/
private fun notifyScanResult(devices: List<Map<String, Any>>) {
for (callback in callbacks) {
try {
callback.onScanResult(devices)
} catch (e: Exception) {
FileLogger.e(TAG, "分发扫描结果回调异常", e)
}
}
}
/**
* 向所有回调监听器分发连接状态变化
*/
private fun notifyConnectionStateChanged(state: Int) {
for (callback in callbacks) {
try {
callback.onConnectionStateChanged(state)
} catch (e: Exception) {
FileLogger.e(TAG, "分发连接状态回调异常", e)
}
}
}
/**
* 向所有回调监听器分发音频数据
*/
private fun notifyAudioDataReceived(data: ByteArray) {
for (callback in callbacks) {
try {
callback.onAudioDataReceived(data)
} catch (e: Exception) {
FileLogger.e(TAG, "分发音频数据回调异常", e)
}
}
}
/**
* 向所有回调监听器分发唤醒信号
*/
private fun notifyWakeupSignalReceived() {
for (callback in callbacks) {
try {
callback.onWakeupSignalReceived()
} catch (e: Exception) {
FileLogger.e(TAG, "分发唤醒信号回调异常", e)
}
}
}
/**
* 向所有回调监听器分发设备信息
*/
private fun notifyDeviceInfoReceived(infoType: Int, infoData: Map<String, Any>) {
for (callback in callbacks) {
try {
callback.onDeviceInfoReceived(infoType, infoData)
} catch (e: Exception) {
FileLogger.e(TAG, "分发设备信息回调异常", e)
}
}
}
}

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

@ -37,9 +37,6 @@ class BleServicePlugin : FlutterPlugin, MethodCallHandler, BleService.Callback {
// 应用上下文
private lateinit var context: Context
// BleManager包装器
private val bleManager = BleManagerWrapper.instance
// 事件接收器 - 只保留数据和状态通道
private var dataEventSink: EventChannel.EventSink? = null
private var statusEventSink: EventChannel.EventSink? = null
@ -57,14 +54,11 @@ class BleServicePlugin : FlutterPlugin, MethodCallHandler, BleService.Callback {
context = flutterPluginBinding.applicationContext
// 初始化BleManagerWrapper
try {
bleManager.init(context)
bleManager.addCallback(this)
FileLogger.d(TAG, "BleManagerWrapper initialized successfully")
} catch (e: Exception) {
FileLogger.e(TAG, "Failed to initialize BleManagerWrapper: ${e.message}", e)
}
// 初始化BleService
BleService.initialize(context)
BleService.addCallback(this)
}
// 设置事件通道 - 简化版
@ -122,11 +116,12 @@ class BleServicePlugin : FlutterPlugin, MethodCallHandler, BleService.Callback {
methodChannel.setMethodCallHandler(null)
// 移除回调
bleManager.removeCallback(this)
BleService.removeCallback(this)
// 解除事件通道
dataEventChannel?.setStreamHandler(null)
statusEventChannel?.setStreamHandler(null)
}
override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
@ -134,47 +129,62 @@ class BleServicePlugin : FlutterPlugin, MethodCallHandler, BleService.Callback {
when (call.method) {
"isConnected" -> {
val connected = bleManager.isConnected()
val connected = BleService.isConnected()
result.success(connected)
}
"getBatteryLevel" -> {
val level = bleManager.getBatteryLevel()
"getBatteryInfo" -> {
val level = BleService.getBatteryInfo()
result.success(level)
}
"getFirmwareVersion" -> {
val version = bleManager.getFirmwareVersion()
result.success(version)
}
"getDeviceVersion" -> {
val success = bleManager.getDeviceVersion()
val success = BleService.getDeviceVersion()
result.success(success)
}
"getBatteryInfo" -> {
val success = bleManager.getBatteryInfo()
val success = BleService.getBatteryInfo()
result.success(success)
}
"openEncoder" -> {
val success = bleManager.openEncoder()
val success = BleService.openEncoder()
result.success(success)
}
"openDecoder" -> {
val success = bleManager.openDecoder()
val success = BleService.openDecoder()
result.success(success)
}
"closeCodec" -> {
val success = bleManager.closeCodec()
val success = BleService.closeCodec()
result.success(success)
}
"playA2DP" -> {
val success = bleManager.playA2DP()
val success = BleService.playA2DP()
result.success(success)
}
"registerBackgroundScan" -> {
try {
BleService.registerBackgroundScan()
FileLogger.d(TAG, "后台扫描已注册")
result.success(true)
} catch (e: Exception) {
FileLogger.e(TAG, "注册后台扫描失败", e)
result.error("BLE_SERVICE_ERROR", "注册后台扫描失败: ${e.message}", null)
}
}
"unregisterBackgroundScan" -> {
try {
BleService.unregisterBackgroundScan()
FileLogger.d(TAG, "后台扫描已注销")
result.success(true)
} catch (e: Exception) {
FileLogger.e(TAG, "注销后台扫描失败", e)
result.error("BLE_SERVICE_ERROR", "注销后台扫描失败: ${e.message}", null)
}
}
"startBleService" -> {
try {
// 从Flutter获取配置参数
val configParams = call.arguments as? Map<String, Any> ?: mapOf()
FileLogger.i(TAG, "启动BLE服务,配置参数: $configParams")
// 启动BLE服务并传递配置参数
startBleServiceWithConfig(configParams)
result.success(true)
@ -218,7 +228,7 @@ class BleServicePlugin : FlutterPlugin, MethodCallHandler, BleService.Callback {
val stateMap = mapOf(
"type" to "connectionStateChanged",
"state" to state,
"stateStr" to stateStr
)
sendEvent(statusEventSink, stateMap, "发送连接状态异常")
@ -229,6 +239,7 @@ class BleServicePlugin : FlutterPlugin, MethodCallHandler, BleService.Callback {
}
override fun onWakeupSignalReceived() {
// 将唤醒事件发送到Flutter
sendEvent(statusEventSink, mapOf("type" to "wakeup"), "发送唤醒信号异常")
}
@ -248,12 +259,9 @@ class BleServicePlugin : FlutterPlugin, MethodCallHandler, BleService.Callback {
*/
private fun startBleServiceWithConfig(configParams: Map<String, Any>) {
try {
// 初始化BleManagerWrapper
val bleManager = BleManagerWrapper.instance
bleManager.init(context)
// 启动BLE前台服务
val serviceIntent = Intent(context, BleForegroundService::class.java)
val serviceIntent = Intent(context, BleMediaService::class.java)
// 如果有额外配置需要传递给服务,可以通过Intent传递
configParams.forEach { (key, value) ->
@ -283,7 +291,7 @@ class BleServicePlugin : FlutterPlugin, MethodCallHandler, BleService.Callback {
*/
private fun stopBleService() {
try {
context.stopService(Intent(context, BleForegroundService::class.java))
context.stopService(Intent(context, BleMediaService::class.java))
FileLogger.d(TAG, "成功停止BLE服务")
} catch (e: Exception) {
FileLogger.e(TAG, "停止BLE服务失败", e)

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

@ -3,14 +3,13 @@ package com.yunqiinnovation.ble_service
import android.content.*
import android.bluetooth.le.*
import android.os.Build
import android.view.KeyEvent
import android.media.AudioManager
import com.yunqiinnovation.deepvoice_utils.FileLogger
import android.util.Log
import android.os.Handler
import android.os.Looper
import com.yunqiinnovation.ble_service.BleService
/**
* BLE唤醒接收器
* 当耳机发送含特定UUID广播时,系统会触发此接收器
* 接收器会解析MAC地址并启动前台服务进行连接
*/
class BleWakeReceiver : BroadcastReceiver() {
@ -19,72 +18,60 @@ class BleWakeReceiver : BroadcastReceiver() {
}
override fun onReceive(ctx: Context, intent: Intent) {
// 获取PendingResult以防止接收器过早结束
val pendingResult = goAsync()
FileLogger.init(ctx)
try {
FileLogger.init(ctx)
FileLogger.d(TAG, "onReceive — 收到唤醒广播: action=${intent.action}, ${BleConst.ACTION_SCAN_RESULT}")
if (intent.action != BleConst.ACTION_SCAN_RESULT) {
pendingResult.finish()
return
}
val list = intent.getParcelableArrayListExtra<ScanResult>(
BluetoothLeScanner.EXTRA_LIST_SCAN_RESULT
)?: run {
pendingResult.finish()
return
}
) ?: return
if (list.isEmpty()) {
FileLogger.d(TAG, "扫描结果为空")
pendingResult.finish()
return
}
val mac = list.first().device.address
FileLogger.d(TAG, "被系统唤醒,发现设备 $mac")
// 使用Handler确保在主线程上下文中启动服务
Handler(Looper.getMainLooper()).post {
try {
// 首先尝试启动普通服务
val svc = Intent(ctx, BleForegroundService::class.java).apply {
putExtra("mac", mac)
action = BleConst.ACTION_SCAN_RESULT
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// 对于Android 8及以上,优先尝试普通服务启动,如果APP在前台可以成功
try {
ctx.startService(svc)
FileLogger.d(TAG, "使用普通服务启动成功")
} catch (e: Exception) {
FileLogger.d(TAG, "普通服务启动失败,尝试前台服务: ${e.message}")
// 尝试前台服务启动
try {
ctx.startForegroundService(svc)
FileLogger.d(TAG, "前台服务启动成功")
} catch (e: Exception) {
FileLogger.e(TAG, "前台服务启动也失败: ${e.message}", e)
}
}
} else {
ctx.startService(svc)
}
} catch (e: Exception) {
FileLogger.e(TAG, "启动服务异常: ${e.message}", e)
} finally {
// 确保完成pendingResult
pendingResult.finish()
}
// FileLogger.d(TAG, "被系统唤醒,发现设备 $mac")
if(!BleMediaService.isServiceRunning()) {
val am = ctx.getSystemService(Context.AUDIO_SERVICE) as AudioManager
val keyEvent = KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE)
am.dispatchMediaKeyEvent(keyEvent)
// val mediaIntent = Intent(Intent.ACTION_MEDIA_BUTTON)
// mediaIntent.putExtra(Intent.EXTRA_KEY_EVENT, keyEvent)
// mediaIntent.putExtra("mac", mac) // 额外传递MAC地址
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// // 通过AudioManager发送媒体按键
// am.dispatchMediaKeyEvent(keyEvent)
// // 发送UP事件
// // am.dispatchMediaKeyEvent(KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE))
// } else {
// // 通过广播发送媒体按钮
// mediaIntent.component = ComponentName(ctx.packageName, "androidx.media.session.MediaButtonReceiver")
// ctx.sendBroadcast(mediaIntent)
// }
}
} catch (e: Exception) {
FileLogger.e(TAG, "处理BLE广播异常: ${e.message}", e)
pendingResult.finish()
if (!BleService.isConnected()) {
val serviceIntent = Intent(ctx, BleMediaService::class.java).apply {
action = BleConst.ACTION_SCAN_RESULT
putExtra("mac", mac)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
ctx.startForegroundService(serviceIntent)
} else {
ctx.startService(serviceIntent)
}
return
}
}
catch (e: Exception) {
FileLogger.e(TAG, "接收器异常: ${e}")
}
}
}

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

@ -12,8 +12,8 @@ class BootReceiver : BroadcastReceiver() {
override fun onReceive(ctx: Context, i: Intent) {
if (i.action == Intent.ACTION_BOOT_COMPLETED) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
ctx.startForegroundService(Intent(ctx, BleForegroundService::class.java))
else ctx.startService(Intent(ctx, BleForegroundService::class.java))
ctx.startForegroundService(Intent(ctx, BleMediaService::class.java))
else ctx.startService(Intent(ctx, BleMediaService::class.java))
}
}
}

162
local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/ChatAgent.kt

@ -0,0 +1,162 @@
package com.yunqiinnovation.ble_service
import android.content.Context
import com.yunqiinnovation.agent_service.AgentService
import com.yunqiinnovation.agent_service.AgentServiceListener
import com.yunqiinnovation.deepvoice_utils.FileLogger
/**
* 蓝牙语音交互代理
* 连接BleService和AgentService,响应蓝牙设备唤醒信号,启动语音识别和交互
*/
object ChatAgent : BleService.Callback, AgentServiceListener {
private const val TAG = "ChatAgent"
// 是否已初始化
private var isInitialized = false
// 是否已注册BLE回调
private var isRegisteredToBle = false
/**
* 初始化语音代理
* @param context 上下文
* @param config 语音服务配置
*/
fun initialize(context: Context, config: Map<String, Any>): Boolean {
if (isInitialized) return true
try {
FileLogger.d(TAG, "初始化语音代理..., $config")
// 1. 初始化AgentService
if (!AgentService.initialize(context, config)) {
FileLogger.e(TAG, "语音服务初始化失败")
return false
}
// 2. 初始化BleService并注册回调
if (BleService.initialize(context)) {
if (BleService.addCallback(this)) {
isRegisteredToBle = true
FileLogger.d(TAG, "已注册BLE回调")
} else {
FileLogger.w(TAG, "BLE回调注册失败")
}
} else {
FileLogger.e(TAG, "BLE服务初始化失败")
return false
}
isInitialized = true
FileLogger.d(TAG, "语音代理初始化成功")
return true
} catch (e: Exception) {
FileLogger.e(TAG, "初始化语音代理异常", e)
return false
}
}
/**
* 手动启动语音识别
*/
fun startListening(): Boolean {
if (!isInitialized) {
FileLogger.e(TAG, "语音代理未初始化")
return false
}
return AgentService.startRecognition()
}
/**
* 停止语音识别
*/
fun stopListening() {
AgentService.stopRecognition()
}
/**
* 处理文本输入
*/
fun processTextInput(text: String, speakResponse: Boolean = false): Boolean {
return AgentService.processTextInput(text, speakResponse)
}
/**
* 停止TTS
*/
fun stopTts() {
AgentService.stopTts()
}
/**
* 清理资源
*/
fun dispose() {
// 取消BLE回调注册
if (isRegisteredToBle) {
BleService.removeCallback(this)
isRegisteredToBle = false
}
// 释放AgentService资源
AgentService.dispose()
isInitialized = false
FileLogger.d(TAG, "语音代理资源已释放")
}
//============================================================================================
// BleService.Callback 接口实现
//============================================================================================
override fun onScanResult(devices: List<Map<String, Any>>) {
// 不处理扫描结果
}
override fun onConnectionStateChanged(state: Int) {
// 可选:处理连接状态变化
if (state == BleConst.STATE_CONNECTED) {
FileLogger.d(TAG, "Ble设备已连接,准备好语音交互")
} else if (state == BleConst.STATE_DISCONNECTED) {
FileLogger.d(TAG, "Ble设备已断开,停止语音服务")
// 设备断开连接,停止语音服务
// stopListening()
}
}
override fun onAudioDataReceived(data: ByteArray) {
// 可选:处理音频数据
}
/**
* 处理唤醒信号
* 在收到唤醒信号时启动语音识别
*/
override fun onWakeupSignalReceived() {
FileLogger.d(TAG, "收到唤醒信号,启动语音识别")
// 停止当前TTS,避免冲突
AgentService.interruptCurrentResponse()
AgentService.speakText("我在!")
// 启动语音识别
if (AgentService.isRecognitionActive) {
// 如果已经在识别,则重新启动
stopListening()
}
startListening()
}
override fun onDeviceInfoReceived(infoType: Int, infoData: Map<String, Any>) {
// 不处理设备信息
}
//============================================================================================
// AgentServiceListener 接口实现
//============================================================================================
override fun onEvent(eventName: String, data: Map<String, Any>) {
}
}

144
local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/utils.kt

@ -0,0 +1,144 @@
package com.yunqiinnovation.ble_service
import android.content.Context
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey
import com.yunqiinnovation.deepvoice_utils.FileLogger
/**
* 通用工具类,包含安全存储等功能
*/
object Utils {
private const val TAG = "Utils"
private const val SECURE_PREFS_FILENAME = "deep_voice_secure_prefs"
private const val KEY_SESSION_ID = "session_id"
/**
* 安全存储数据
* @param context 上下文
* @param dataMap 要存储的键值对字典
* @return 是否成功存储
*/
fun saveSecureData(context: Context, dataMap: Map<String, String?>): Boolean {
return try {
// 创建或获取主密钥
val masterKey = MasterKey.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()
// 创建加密SharedPreferences
val sharedPreferences = EncryptedSharedPreferences.create(
context,
SECURE_PREFS_FILENAME,
masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
// 获取或生成会话ID
var sessionId = sharedPreferences.getString(KEY_SESSION_ID, "") ?: ""
if (sessionId.isEmpty()) {
sessionId = java.util.UUID.randomUUID().toString()
}
// 开始编辑会话
val editor = sharedPreferences.edit()
// 保存会话ID
editor.putString(KEY_SESSION_ID, sessionId)
// 保存所有键值对
for ((key, value) in dataMap) {
if (value != null) {
editor.putString(key, value)
} else {
editor.remove(key)
}
}
// 提交更改
editor.apply()
FileLogger.d(TAG, "数据已安全保存到加密存储中")
true
} catch (e: Exception) {
FileLogger.e(TAG, "保存数据到加密存储失败", e)
false
}
}
/**
* 从安全存储中读取所有数据
* @param context 上下文
* @return 包含所有数据的键值对字典
*/
fun loadAllSecureData(context: Context): Map<String, String> {
return try {
val resultMap = mutableMapOf<String, String>()
// 创建或获取主密钥
val masterKey = MasterKey.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()
// 创建加密SharedPreferences
val sharedPreferences = EncryptedSharedPreferences.create(
context,
SECURE_PREFS_FILENAME,
masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
// 读取所有数据
for ((key, value) in sharedPreferences.all) {
if (value is String) {
resultMap[key] = value
}
}
FileLogger.d(TAG, "已从加密存储加载所有数据")
resultMap
} catch (e: Exception) {
FileLogger.e(TAG, "从加密存储加载所有数据失败", e)
emptyMap()
}
}
/**
* 清除所有安全存储的数据
* @param context 上下文
* @return 是否成功清除
*/
fun clearAllSecureData(context: Context): Boolean {
return try {
// 创建或获取主密钥
val masterKey = MasterKey.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()
// 创建加密SharedPreferences
val sharedPreferences = EncryptedSharedPreferences.create(
context,
SECURE_PREFS_FILENAME,
masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
// 生成新的会话ID
val sessionId = java.util.UUID.randomUUID().toString()
// 清除所有数据
sharedPreferences.edit().clear()
.putString(KEY_SESSION_ID, sessionId)
.apply()
FileLogger.d(TAG, "已清除所有安全存储的数据")
true
} catch (e: Exception) {
FileLogger.e(TAG, "清除安全存储数据失败", e)
false
}
}
}

BIN
local_plugins/ble_service/android/src/main/res/raw/silence.mp3

Binary file not shown.

103
local_plugins/ble_service/lib/ble_service.dart

@ -163,9 +163,104 @@ class BleService {
}
}
/// 获取已连接设备地址 - 使用其他方式获取设备地址,不再直接调用此方法
Future<String?> getConnectedDeviceAddress() async {
// 简化版处理,返回空值
return null;
/// 注册BLE后台扫描
///
/// 注册后,即使应用被杀死,系统也会在检测到设备广播时唤醒应用
/// 这是确保设备可以在应用后台时被自动连接的关键功能
Future<bool> registerBackgroundScan() async {
try {
final result = await _methodChannel.invokeMethod<bool>('registerBackgroundScan');
return result ?? false;
} catch (e) {
print('注册后台扫描失败: $e');
return false;
}
}
/// 注销BLE后台扫描
///
/// 当不再需要后台扫描功能时调用,例如用户主动断开连接时
Future<bool> unregisterBackgroundScan() async {
try {
final result = await _methodChannel.invokeMethod<bool>('unregisterBackgroundScan');
return result ?? false;
} catch (e) {
print('注销后台扫描失败: $e');
return false;
}
}
//========================================================================
// 以下是ChatAgent相关功能
//========================================================================
/// 初始化语音交互代理
///
/// [config] 配置参数,必须包含:
/// - azureSpeechKey: Azure语音服务API密钥
/// - azureSpeechRegion: Azure语音服务区域
/// - openaiApiKey: OpenAI API密钥
/// - openaiBaseUrl: OpenAI API基础URL
/// - openaiModel: OpenAI模型名称,默认为'gpt-3.5-turbo'
Future<bool> initializeChatAgent(Map<String, dynamic> config) async {
try {
final result = await _methodChannel.invokeMethod<bool>('initializeChatAgent', config);
return result ?? false;
} catch (e) {
print('初始化语音交互代理失败: $e');
return false;
}
}
/// 启动语音识别
///
/// 用于手动启动语音识别,通常不需要直接调用,因为在唤醒时会自动启动
Future<bool> startListening() async {
try {
final result = await _methodChannel.invokeMethod<bool>('startListening');
return result ?? false;
} catch (e) {
print('启动语音识别失败: $e');
return false;
}
}
/// 停止语音识别
Future<bool> stopListening() async {
try {
final result = await _methodChannel.invokeMethod<bool>('stopListening');
return result ?? false;
} catch (e) {
print('停止语音识别失败: $e');
return false;
}
}
/// 处理文本输入
///
/// [text] 用户输入的文本
/// [speakResponse] 是否使用TTS朗读回复,默认为false
Future<bool> processTextInput(String text, {bool speakResponse = false}) async {
try {
final result = await _methodChannel.invokeMethod<bool>('processTextInput', {
'text': text,
'speakResponse': speakResponse,
});
return result ?? false;
} catch (e) {
print('处理文本输入失败: $e');
return false;
}
}
/// 停止TTS播放
Future<bool> stopTts() async {
try {
final result = await _methodChannel.invokeMethod<bool>('stopTts');
return result ?? false;
} catch (e) {
print('停止TTS播放失败: $e');
return false;
}
}
}

2
local_plugins/bytedance_speech/android/build.gradle.kts

@ -8,7 +8,7 @@ plugins {
android {
namespace = "com.yunqiinnovation.ble_service"
compileSdk = 34
compileSdk = 35
defaultConfig {
minSdk = 26

2
local_plugins/classic_bluetooth/android/build.gradle.kts

@ -11,7 +11,7 @@ android {
namespace = "com.yunqiinnovation.classic_bluetooth"
// 目标 SDK 版本
compileSdk = 33
compileSdk = 35
defaultConfig {
// 最低 SDK 版本

4
local_plugins/deepvoice_utils/android/build.gradle.kts

@ -11,12 +11,12 @@ android {
namespace = "com.yunqiinnovation.deepvoice_utils"
// 目标 SDK 版本
compileSdk = 33
compileSdk = 35
defaultConfig {
// 最低 SDK 版本
minSdk = 21
targetSdk = 33
targetSdk = 34
}
// Java 语言级别兼容配置

2
local_plugins/jieli/android/build.gradle.kts

@ -11,7 +11,7 @@ android {
namespace = "com.yunqiinnovation.jieli"
// 目标 SDK 版本
compileSdk = 33
compileSdk = 35
defaultConfig {
// 最低 SDK 版本

2
local_plugins/jl_opus/android/build.gradle.kts

@ -18,7 +18,7 @@ android {
namespace = "com.yunqiinnovation.jl_opus"
// 目标 SDK 版本
compileSdk = 33
compileSdk = 35
defaultConfig {
// 最低 SDK 版本

2
local_plugins/open_ai_service/android/build.gradle.kts

@ -12,7 +12,7 @@ android {
namespace = "com.yunqiinnovation.open_ai_service"
// 目标 SDK 版本
compileSdk = 33
compileSdk = 35
defaultConfig {
// 最低 SDK 版本

76
local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/MCPClient.kt

@ -33,19 +33,8 @@ import okhttp3.Interceptor
import io.ktor.client.request.*
import io.ktor.http.*
import com.yunqiinnovation.open_ai_service.mcp.CustomSseClientTransport
import io.ktor.util.valuesOf
import okio.Buffer
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
@Serializable
data class ServerConfig(
val servername: String,
val url: String,
val tools: List<String>? = null
)
/**
* 工具类型枚举
*/
@ -72,13 +61,13 @@ interface FunctionHandler {
*/
class MCPSubClient(
private val serverId: String,
private val config: ServerConfig,
serverUrl: String,
private val customClient: HttpClient? = null
) : AutoCloseable {
private val TAG = "MCPSubClient[$serverId]"
// 保存服务器URL
private val serverUrl: String = config.url
private val serverUrl: String = serverUrl
init {
Log.d(TAG, "MCPSubClient初始化,URL: $serverUrl")
@ -91,6 +80,7 @@ class MCPSubClient(
version = "1.0.0"
)
)
// 服务器提供的工具列表
private var tools: List<ToolUnion> = emptyList()
@ -168,8 +158,7 @@ class MCPSubClient(
private fun processToolsList(toolsResult: ListToolsResult?) {
try {
// 将MCP工具列表转换为ToolUnion列表
tools = toolsResult?.tools?.filter { tool -> config.tools.isNullOrEmpty() || config.tools.contains(tool.name)
}?.map { tool ->
tools = toolsResult?.tools?.map { tool ->
// 转换为ToolUnion格式
ToolUnion.ofTool(
Tool.builder()
@ -178,26 +167,26 @@ class MCPSubClient(
.inputSchema(
Tool.InputSchema.builder()
.type(JsonValue.from(tool.inputSchema?.type ?: "object"))
.properties(if (tool.inputSchema?.properties is JsonObject)
(tool.inputSchema.properties as JsonObject).toJsonValue()
else JsonValue.from(emptyMap<String, String>()))
.properties(if (tool.inputSchema?.properties is JsonObject)
(tool.inputSchema.properties as JsonObject).toJsonValue()
else JsonValue.from(emptyMap<String, String>()))
.putAdditionalProperty("required", JsonValue.from(tool.inputSchema?.required ?: emptyList<String>()))
.build()
)
.build()
)
} ?: emptyList()
// 更新工具映射
toolMapping.clear()
tools.forEach { toolUnion ->
val tool = toolUnion.tool().get()
toolMapping[tool.name()] = toolUnion
}
// 更新工具Map列表
updateToolMaps()
Log.d(TAG, "成功处理 ${tools.size} 个工具")
} catch (e: Exception) {
Log.e(TAG, "处理工具列表失败: ${e.message}", e)
@ -222,7 +211,7 @@ class MCPSubClient(
// 使用toString()获取完整JSON字符串
val schemaStr = inputSchema.toString()
// Log.d(TAG, "工具 ${tool.name()} 的Schema: $schemaStr")
Log.d(TAG, "工具 ${tool.name()} 的Schema: $schemaStr")
// 解析properties和required
val propertiesMap = HashMap<String, Any>()
@ -278,7 +267,7 @@ class MCPSubClient(
propertiesMap[fieldName] = fieldMap
}
// Log.d(TAG, "成功解析属性: $propertiesMap")
Log.d(TAG, "成功解析属性: $propertiesMap")
}
// 解析required
@ -299,7 +288,7 @@ class MCPSubClient(
}
}
requiredList.addAll(fieldNames)
// Log.d(TAG, "解析到required字段: $requiredList")
Log.d(TAG, "解析到required字段: $requiredList")
}
}
} catch (e: Exception) {
@ -406,6 +395,7 @@ class MCPSubClient(
name = name,
arguments = arguments
)
// 处理工具调用结果
val context = result?.content?.joinToString("\n") { content ->
when (content) {
@ -414,9 +404,12 @@ class MCPSubClient(
}
}
JSONObject().apply { put("name",name);put("meta",result?._meta); put("context",context) }
} catch (e: Exception) {
Log.e(TAG, "调用MCP工具失败: ${e.message}", e)
"调用MCP工具失败: ${e.message}"
JSONObject().apply { put("context", "调用MCP工具失败: ${e.message}")}
}
}
@ -498,7 +491,7 @@ class MCPClient(private val context: Context? = null) : AutoCloseable {
// 清除现有连接
closeAllConnections()
val serverMap = mutableMapOf<String, ServerConfig>()
val serverMap = mutableMapOf<String, String>()
var configLoaded = false
// 尝试解析传入的JSON配置
@ -506,19 +499,17 @@ class MCPClient(private val context: Context? = null) : AutoCloseable {
if (mcpConfigJson.isNotEmpty()) {
val mapper = ObjectMapper()
try {
Log.d(TAG, "传入JSON:${mcpConfigJson}")
val configMap: Map<String, ServerConfig> = Json.decodeFromString(mcpConfigJson)
// val config = mapper.readValue(mcpConfigJson, Map::class.java)
val config = mapper.readValue(mcpConfigJson, Map::class.java)
// 处理mcpServers格式
@Suppress("UNCHECKED_CAST")
// val mcpServers = config["mcpServers"] as? Map<String, Map<String, String>>
if (configMap.isNotEmpty()) {
val mcpServers = config["mcpServers"] as? Map<String, Map<String, String>>
if (mcpServers != null && mcpServers.isNotEmpty()) {
// 从服务器配置中提取URL
configMap.forEach { (serverId, serverConfig) ->
val url = serverConfig.url
mcpServers.forEach { (serverId, serverConfig) ->
val url = serverConfig["url"]
if (!url.isNullOrEmpty()) {
serverMap[serverId] = serverConfig
serverMap[serverId] = url
Log.d(TAG, "从配置中添加服务器 [$serverId]: $url")
}
}
@ -537,10 +528,10 @@ class MCPClient(private val context: Context? = null) : AutoCloseable {
// 创建并连接所有子客户端
val connectionResults = mutableMapOf<String, Boolean>()
for ((serverId, serverConfig) in serverMap) {
for ((serverId, serverUrl) in serverMap) {
try {
Log.d(TAG, "为服务器 [$serverId],[${serverConfig.url}] 创建子客户端")
val subClient = MCPSubClient(serverId, serverConfig, sharedHttpClient)
Log.d(TAG, "为服务器 [$serverId],[$serverUrl] 创建子客户端")
val subClient = MCPSubClient(serverId, serverUrl, sharedHttpClient)
val connected = subClient.connect()
if (connected) {
@ -710,7 +701,7 @@ class MCPClient(private val context: Context? = null) : AutoCloseable {
subClients.forEach { (serverId, client) ->
val clientTools = client.getToolMaps()
allToolMaps.addAll(clientTools)
Log.d(TAG, "添加子客户端 [$serverId] 的 ${clientTools.size} 个工具")
// Log.d(TAG, "添加子客户端 [$serverId] 的 ${clientTools.size} 个工具")
}
return allToolMaps
@ -751,7 +742,7 @@ class MCPClient(private val context: Context? = null) : AutoCloseable {
}
null -> {
Log.e(TAG, "未找到工具: $name")
null
JSONObject().apply { put("context","未找到工具: $name") }
}
}
}
@ -763,16 +754,15 @@ class MCPClient(private val context: Context? = null) : AutoCloseable {
return try {
val handler = localFunctions[name]
if (handler != null) {
val context = handler.handle(arguments)
val context = handler.handle(arguments)
JSONObject().apply { put("context", context) }
} else {
Log.e(TAG, "未找到本地函数: $name")
null
JSONObject().apply { put("context","未找到本地函数: $name") }
}
} catch (e: Exception) {
Log.e(TAG, "调用本地函数失败: ${e.message}", e)
JSONObject().apply { put("context", "调用本地函数失败: ${e.message}") }
JSONObject().apply { put("context","调用本地函数失败: ${e.message}") }
}
}
@ -782,7 +772,7 @@ class MCPClient(private val context: Context? = null) : AutoCloseable {
private suspend fun callMcpTool(name: String, arguments: Map<String, Any>): JSONObject? {
if (!isConnected) {
Log.e(TAG, "未连接到MCP服务器", null)
return JSONObject().apply { put("context", "未连接到MCP服务器") }
return JSONObject().apply { put("context","未连接到MCP服务器") }
}
// 查找包含此工具的子客户端

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

@ -42,6 +42,10 @@ class OpenAIService(private val context: Context? = null) {
// 是否自动处理MCP工具调用
private var autoHandleMcpTools = true
// 添加成员变量以跟踪当前请求
private var currentStreamCall: Call? = null
private var isCanceled = false
/**
* 构建curl命令用于测试
*/
@ -81,6 +85,16 @@ class OpenAIService(private val context: Context? = null) {
}
}
/**
* 创建系统消息
*/
fun createSystemMessage(content: String): JSONObject {
return JSONObject().apply {
put("role", "system")
put("content", content)
}
}
/**
* 创建助手消息
*/
@ -166,7 +180,7 @@ class OpenAIService(private val context: Context? = null) {
*/
suspend fun handleMcpToolCall(functionCall: JSONObject): JSONObject? {
if (mcpClient == null || !isMcpInitialized) {
return JSONObject().apply { put("context", "MCP客户端未初始化") }
return JSONObject().apply { put("context","MCP客户端未初始化") }
}
try {
@ -181,7 +195,7 @@ class OpenAIService(private val context: Context? = null) {
return mcpClient?.callTool(name, arguments)
} catch (e: Exception) {
Log.e(TAG, "处理MCP工具调用失败: ${e.message}", e)
return JSONObject().apply { put("context", "处理MCP工具调用失败: ${e.message}") }
return JSONObject().apply { put("context","处理MCP工具调用失败: ${e.message}") }
}
}
@ -226,17 +240,12 @@ class OpenAIService(private val context: Context? = null) {
// 调用工具(统一通过MCPClient处理)
val result = mcpClient?.callTool(name, arguments)
var context = "工具调用失败";
if (result != null){
callback.onFunctionCall(result)
context = "${result["context"]}"
}
callback.onFunctionCallResult(functionCall, result?: JSONObject().apply { put("context","工具调用失败") })
// 不论是本地函数还是MCP工具,都直接将结果发送回OpenAI
sendFunctionCallResult(
messages = messages,
functionCall = functionCall,
functionResult = context,
functionResult = result?.getString("context")?: "工具调用失败",
callback = callback
)
} catch (e: Exception) {
@ -359,6 +368,9 @@ class OpenAIService(private val context: Context? = null) {
return
}
// 重置取消状态
isCanceled = false
val requestBody = JSONObject().apply {
put("model", model)
put("messages", messages)
@ -398,12 +410,25 @@ class OpenAIService(private val context: Context? = null) {
// val curlCommand = buildCurlCommand(request, requestBody.toString())
// Log.d(TAG, "curl command: $curlCommand")
client.newCall(request).enqueue(object : Callback {
val call = client.newCall(request)
// 保存当前Call对象以便后续取消
currentStreamCall = call
call.enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
// 如果是由于取消导致的失败,不报告错误
if (isCanceled) {
return
}
callback.onError(OpenAIException(e.message ?: "请求失败"))
}
override fun onResponse(call: Call, response: Response) {
// 检查请求是否被取消
if (isCanceled) {
response.body?.close()
return
}
if (!response.isSuccessful) {
callback.onError(OpenAIException("API调用失败: ${response.code}"))
@ -422,7 +447,7 @@ class OpenAIService(private val context: Context? = null) {
// 逐块读取数据,而不是一次性预取
val BUFFER_SIZE = 8192L // 8KB的块大小
while (!Thread.currentThread().isInterrupted) {
while (!Thread.currentThread().isInterrupted && !isCanceled) {
// 读取一小块数据
val bytesRead = source.read(buffer, BUFFER_SIZE)
if (bytesRead == -1L) break // 流结束
@ -434,12 +459,13 @@ class OpenAIService(private val context: Context? = null) {
if (line.startsWith("data:")) {
val data = line.substring(5).trim()
Log.d("OpenAIService", "$data")
// Log.d("OpenAIService", "$data")
// 处理[DONE]消息
if (data == "[DONE]" || data == "[\"DONE\"]") {
Log.d(TAG, "收到[DONE]消息")
val hasToolCalls = processToolCalls(finalToolCalls, callback, messages)
// Log.d(TAG, "收到[DONE]消息, $hasToolCalls")
// 只有在没有工具调用时才认为对话真正完成
if (!hasToolCalls) {
callback.onComplete()
@ -504,7 +530,24 @@ class OpenAIService(private val context: Context? = null) {
}
}
}
// 如果是因为取消导致循环结束,不继续处理
if (isCanceled) {
return
}
// 处理可能尚未处理的工具调用
val hasToolCalls = processToolCalls(finalToolCalls, callback, messages)
// 只有在没有工具调用时才认为对话真正完成
if (!hasToolCalls) {
callback.onComplete()
}
} catch (e: Exception) {
// 如果是由于取消导致的异常,不报告错误
if (isCanceled) {
return
}
callback.onError(OpenAIException("处理响应流时出错: ${e.message}"))
} finally {
responseBody.close()
@ -556,7 +599,6 @@ class OpenAIService(private val context: Context? = null) {
put("content", functionResult)
put("tool_call_id", functionCall.optString("id", "call_${System.currentTimeMillis()}"))
})
// 发送完整对话
sendMessageStream(fullMessages, callback)
@ -581,12 +623,16 @@ class OpenAIService(private val context: Context? = null) {
put("arguments", firstToolCall.arguments)
put("id", firstToolCall.id)
}
Log.d(TAG, "工具调用: $functionCall")
// 在协程中处理工具调用
if (messages != null) {
kotlinx.coroutines.GlobalScope.launch {
try {
//通知上层回调事件,但不关心结果
callback.onFunctionCall(functionCall)
autoHandleMcpToolCall(functionCall, messages, callback)
} catch (e: Exception) {
Log.e(TAG, "工具调用处理失败: ${e.message}", e)
}
@ -618,6 +664,34 @@ class OpenAIService(private val context: Context? = null) {
fun onComplete()
fun onError(e: Exception)
fun onFunctionCall(functionCall: JSONObject)
fun onFunctionCallResult(functionCall: JSONObject, functionCallResult: JSONObject)
}
/**
* 取消当前流式请求
*
* @return 是否成功取消
*/
fun cancelCurrentStream(): Boolean {
try {
// 标记已取消
isCanceled = true
// 取消当前Call对象
val call = currentStreamCall
if (call != null && !call.isCanceled()) {
call.cancel()
Log.d(TAG, "已取消当前流式请求")
return true
}
return false
} catch (e: Exception) {
Log.e(TAG, "取消流式请求失败: ${e.message}", e)
return false
} finally {
// 清除当前Call引用
currentStreamCall = null
}
}
}

146
local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/OpenAIServicePlugin.kt

@ -83,47 +83,6 @@ class OpenAIServicePlugin : FlutterPlugin, MethodCallHandler, StreamHandler {
}
"registerFunction" -> {
val name = call.argument<String>("name") ?: ""
val description = call.argument<String>("description") ?: ""
val parameters = call.argument<Map<String, Any>>("parameters")
if (name.isEmpty() || parameters == null) {
result.error("INVALID_ARGUMENT", "函数注册参数无效", null)
return
}
val parametersJson = JSONObject(parameters)
val registered = openAIService.registerFunction(name, description, parametersJson)
result.success(registered)
}
"sendMessage" -> {
val messagesRaw = call.argument<List<Map<String, Any>>>("messages") ?: emptyList()
// 转换消息格式
val messages = JSONArray()
for (message in messagesRaw) {
messages.put(JSONObject(message))
}
// 对于需要在后台执行的同步阻塞操作,仍然使用executor
executor.execute {
try {
val response = openAIService.sendMessage(messages)
// 在主线程返回结果
mainHandler.post {
result.success(response)
}
} catch (e: Exception) {
// 在主线程返回错误
mainHandler.post {
result.error("OPENAI_ERROR", e.message, null)
}
}
}
}
"sendMessageStream" -> {
val messagesRaw = call.argument<List<Map<String, Any>>>("messages") ?: emptyList()
// 检查事件接收器
@ -165,102 +124,19 @@ class OpenAIServicePlugin : FlutterPlugin, MethodCallHandler, StreamHandler {
}
override fun onFunctionCall(functionCall: JSONObject) {
Log.e("OpenAIServicePlugin", "context:${functionCall.toString()}")
val context = functionCall["context"]
if (functionCall.has("meta")) {
val meta = functionCall.get("meta")
val metaJsonObject:JsonObject = meta as JsonObject
Log.e("OpenAIServicePlugin", "工具调用结果 context:${context} meta:${meta.toString()}")
// 发送函数调用事件
mainHandler.post {
eventSink?.success(mapOf("type" to "functionCall", "content" to context,"meta" to meta.toString()))
}
if (metaJsonObject.containsKey("card_spotify") || metaJsonObject.containsKey("card_music")) {
mainHandler.post {
eventSink?.success(mapOf("type" to "complete"))
}
}
}
// val meta: Map<String, Any>? = functionCall.optJSONObject("meta")?.let { metaJson ->
// metaJson.keys().asSequence().associateWith { key ->
// metaJson.get(key) // 或 opt(key),更安全
// }
// } ?: mapOf(
// // 如果 null,就用默认值
// "card_spotify" to {
// "url" to "spotify:track:58kNJana4w5BIjlZE2wq5m"
// },
// )
}
}
)
// 请求已开始,直接返回结果
result.success(true)
} catch (e: Exception) {
// 直接在当前线程处理错误
result.error("OPENAI_ERROR", e.message, null)
}
}
"sendFunctionCallResult" -> {
val messagesRaw = call.argument<List<Map<String, Any>>>("messages") ?: emptyList()
val functionCallRaw = call.argument<Map<String, Any>>("functionCall") ?: emptyMap()
val functionResult = call.argument<String>("functionResult") ?: ""
// 检查事件接收器
if (eventSink == null) {
result.error("NO_EVENT_SINK", "没有可用的事件流接收器", null)
return
}
// 转换消息格式
val messages = JSONArray()
for (message in messagesRaw) {
messages.put(JSONObject(message))
}
// 转换函数调用
val functionCall = JSONObject(functionCallRaw)
// 不再在后台线程包装OkHttp的异步调用
try {
openAIService.sendFunctionCallResult(
messages = messages,
functionCall = functionCall,
functionResult = functionResult,
callback = object : OpenAIService.StreamCallback {
override fun onToken(token: String) {
// 发送token事件
mainHandler.post {
eventSink?.success(mapOf("type" to "token", "content" to token))
}
}
override fun onComplete() {
// 发送完成事件
mainHandler.post {
eventSink?.success(mapOf("type" to "complete"))
eventSink?.success(mapOf("type" to "function_call", "content" to functionCall.toString()))
}
}
override fun onError(e: Exception) {
// 发送错误事件
override fun onFunctionCallResult(functionCall: JSONObject, functionCallResult: JSONObject) {
mainHandler.post {
eventSink?.success(mapOf("type" to "error", "content" to e.message))
eventSink?.success(
mapOf("type" to "function_call_result"
,"function_call" to functionCall.toString()
, "result" to functionCallResult.toString()))
}
}
override fun onFunctionCall(nestedFunctionCall: JSONObject) {
Log.e("OpenAIServicePlugin", "工具调用结果: ${nestedFunctionCall.toString()}")
// 发送函数调用事件
// mainHandler.post {
// val functionCallMap = nestedFunctionCall.toMap()
// eventSink?.success(mapOf("type" to "functionCall", "content" to functionCallMap))
// }
}
}
)
@ -271,6 +147,7 @@ class OpenAIServicePlugin : FlutterPlugin, MethodCallHandler, StreamHandler {
result.error("OPENAI_ERROR", e.message, null)
}
}
"createUserMessage" -> {
val content = call.argument<String>("content") ?: ""
@ -283,12 +160,7 @@ class OpenAIServicePlugin : FlutterPlugin, MethodCallHandler, StreamHandler {
val message = openAIService.createAssistantMessage(content)
result.success(message.toMap())
}
"initializeMcpClient" -> initializeMcpClient(call, result)
"isMcpInitialized" -> isMcpInitialized(call, result)
"closeMcpClient" -> closeMcpClient(call, result)
"handleMcpToolCall" -> handleMcpToolCall(call, result)
else -> {
result.notImplemented()
}

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

@ -3,11 +3,20 @@ package com.yunqiinnovation.open_ai_service
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.location.Location
import android.location.LocationListener
import android.location.LocationManager
import android.net.Uri
import android.os.Bundle
import android.provider.ContactsContract
import android.provider.AlarmClock
import android.provider.CalendarContract
import android.provider.Settings
import android.telephony.SmsManager
import android.util.Log
import org.json.JSONObject
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlin.coroutines.resume
/**
* 系统功能处理器 - 负责调用系统API执行各种功能以及注册系统功能
@ -39,6 +48,15 @@ class SystemFunctionHandler(private val context: Context) {
// 注册打开应用函数
registerOpenAppFunction(client)
// 注册设置提醒函数
registerSetReminderFunction(client)
// 注册获取当前时间函数
registerGetCurrentTimeFunction(client)
// 注册获取当前位置函数
registerGetCurrentLocationFunction(client)
Log.d(TAG, "所有系统功能注册完成")
} catch (e: Exception) {
Log.e(TAG, "注册系统功能失败: ${e.message}", e)
@ -52,7 +70,7 @@ class SystemFunctionHandler(private val context: Context) {
private fun registerExitInteractionFunction(client: MCPClient) {
client.registerLocalFunction(
"exit_interaction",
"退出当前语音交互",
"结束当前交互",
JSONObject("""
{
"type": "object",
@ -63,9 +81,9 @@ class SystemFunctionHandler(private val context: Context) {
object : FunctionHandler {
override suspend fun handle(arguments: Map<String, Any>): String {
// 调用发送广播的函数
sendExitInteractionBroadcast()
// sendExitInteractionBroadcast()
return "{\"result\": \"已退出语音交互\"}"
return "{\"result\": \"已结束当前交互\"}"
}
}
)
@ -191,6 +209,100 @@ class SystemFunctionHandler(private val context: Context) {
Log.d(TAG, "已注册打开应用功能")
}
/**
* 注册设置提醒函数
*/
private fun registerSetReminderFunction(client: MCPClient) {
client.registerLocalFunction(
"set_reminder",
"设置提醒事项",
JSONObject("""
{
"type": "object",
"properties": {
"title": {
"type": "string",
"description": "提醒标题"
},
"content": {
"type": "string",
"description": "提醒内容"
},
"time": {
"type": "string",
"description": "提醒时间,格式为'yyyy-MM-dd HH:mm',如'2023-12-31 14:30'"
}
},
"required": ["title", "time"]
}
"""),
object : FunctionHandler {
override suspend fun handle(arguments: Map<String, Any>): String {
val title = arguments["title"] as? String ?: ""
val content = arguments["content"] as? String ?: ""
val time = arguments["time"] as? String ?: ""
val result = setReminder(title, content, time)
return "{\"result\": \"${result.message}\"}"
}
}
)
Log.d(TAG, "已注册设置提醒功能")
}
/**
* 注册获取当前时间函数
*/
private fun registerGetCurrentTimeFunction(client: MCPClient) {
client.registerLocalFunction(
"get_current_time",
"获取当前日期和时间",
JSONObject("""
{
"type": "object",
"properties": {
"format": {
"type": "string",
"description": "时间格式,可选,默认为标准格式"
}
},
"required": []
}
"""),
object : FunctionHandler {
override suspend fun handle(arguments: Map<String, Any>): String {
val format = arguments["format"] as? String
val result = getCurrentTime(format)
return "{\"result\": \"${result.message}\", \"time\": \"${result.message}\"}"
}
}
)
Log.d(TAG, "已注册获取当前时间功能")
}
/**
* 注册获取当前位置函数
*/
private fun registerGetCurrentLocationFunction(client: MCPClient) {
client.registerLocalFunction(
"get_current_location",
"获取当前地理位置",
JSONObject("""
{
"type": "object",
"properties": {},
"required": []
}
"""),
object : FunctionHandler {
override suspend fun handle(arguments: Map<String, Any>): String {
val result = getCurrentLocation()
return "{\"result\": \"${result.message}\", \"success\": ${result.success}}"
}
}
)
Log.d(TAG, "已注册获取当前位置功能")
}
/**
* 发送退出交互广播
@ -412,6 +524,347 @@ class SystemFunctionHandler(private val context: Context) {
return ""
}
/**
* 设置提醒
* @param title 提醒标题
* @param content 提醒内容(可选)
* @param timeStr 提醒时间,格式为"yyyy-MM-dd HH:mm"
* @return 操作结果
*/
fun setReminder(title: String, content: String, timeStr: String): Result {
Log.d(TAG, "设置提醒: $title, 时间: $timeStr")
try {
// 解析时间字符串
val time = try {
val dateFormat = java.text.SimpleDateFormat("yyyy-MM-dd HH:mm", java.util.Locale.getDefault())
dateFormat.parse(timeStr)?.time ?: System.currentTimeMillis()
} catch (e: Exception) {
Log.e(TAG, "解析时间失败: $timeStr, ${e.message}")
return Result(
success = false,
message = "提醒时间格式无效,请使用'yyyy-MM-dd HH:mm'格式"
)
}
// 使用系统日历应用创建提醒
val intent = Intent(Intent.ACTION_INSERT).apply {
data = CalendarContract.Events.CONTENT_URI
putExtra(CalendarContract.Events.TITLE, title)
putExtra(CalendarContract.Events.DESCRIPTION, content)
putExtra(CalendarContract.EXTRA_EVENT_BEGIN_TIME, time)
putExtra(CalendarContract.EXTRA_EVENT_END_TIME, time + 3600000) // 默认1小时
putExtra(CalendarContract.Events.HAS_ALARM, 1)
putExtra(CalendarContract.Events.AVAILABILITY, CalendarContract.Events.AVAILABILITY_BUSY)
flags = Intent.FLAG_ACTIVITY_NEW_TASK
}
// 检查是否有处理该Intent的应用
val calendarActivity = intent.resolveActivity(context.packageManager)
if (calendarActivity != null) {
context.startActivity(intent)
return Result(
success = true,
message = "已创建提醒: $title, 时间: $timeStr"
)
} else {
// 备选方案:如果没有日历应用,尝试创建闹钟
try {
// 解析小时和分钟
val timeParts = timeStr.split(" ")[1].split(":")
val hour = timeParts[0].toInt()
val minute = timeParts[1].toInt()
val alarmIntent = Intent(AlarmClock.ACTION_SET_ALARM)
alarmIntent.putExtra(AlarmClock.EXTRA_MESSAGE, "$title: $content")
alarmIntent.putExtra(AlarmClock.EXTRA_HOUR, hour)
alarmIntent.putExtra(AlarmClock.EXTRA_MINUTES, minute)
alarmIntent.putExtra(AlarmClock.EXTRA_SKIP_UI, false)
alarmIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
if (alarmIntent.resolveActivity(context.packageManager) != null) {
context.startActivity(alarmIntent)
return Result(
success = true,
message = "已创建闹钟提醒: $title, 时间: $timeStr"
)
}
} catch (e: Exception) {
Log.e(TAG, "创建闹钟失败: ${e.message}", e)
}
return Result(
success = false,
message = "未找到可用的日历或闹钟应用"
)
}
} catch (e: Exception) {
Log.e(TAG, "设置提醒失败: ${e.message}", e)
return Result(
success = false,
message = "设置提醒失败: ${e.message}"
)
}
}
/**
* 获取当前时间
* @param format 时间格式(可选)
* @return 操作结果
*/
fun getCurrentTime(format: String? = null): Result {
try {
val calendar = java.util.Calendar.getInstance()
val date = calendar.time
val timeStr = when (format?.lowercase(java.util.Locale.getDefault())) {
"short" -> {
val timeFormat = java.text.SimpleDateFormat("HH:mm", java.util.Locale.getDefault())
timeFormat.format(date)
}
"date" -> {
val dateFormat = java.text.SimpleDateFormat("yyyy年MM月dd日", java.util.Locale.getDefault())
dateFormat.format(date)
}
"time" -> {
val timeFormat = java.text.SimpleDateFormat("HH:mm:ss", java.util.Locale.getDefault())
timeFormat.format(date)
}
"full" -> {
val fullFormat = java.text.SimpleDateFormat("yyyy年MM月dd日 EEEE HH:mm:ss", java.util.Locale.getDefault())
fullFormat.format(date)
}
"iso" -> {
val isoFormat = java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", java.util.Locale.getDefault())
isoFormat.format(date)
}
else -> {
val defaultFormat = java.text.SimpleDateFormat("yyyy年MM月dd日 HH:mm", java.util.Locale.getDefault())
defaultFormat.format(date)
}
}
// 获取星期几
val weekDay = when (calendar.get(java.util.Calendar.DAY_OF_WEEK)) {
java.util.Calendar.SUNDAY -> "星期日"
java.util.Calendar.MONDAY -> "星期一"
java.util.Calendar.TUESDAY -> "星期二"
java.util.Calendar.WEDNESDAY -> "星期三"
java.util.Calendar.THURSDAY -> "星期四"
java.util.Calendar.FRIDAY -> "星期五"
java.util.Calendar.SATURDAY -> "星期六"
else -> ""
}
// 如果不是返回短格式,则添加星期几(除非format已经包含星期)
val resultTime = when {
format == "short" || format == "time" -> timeStr
format == "full" -> timeStr // full格式已经包含了星期
else -> "$timeStr $weekDay"
}
Log.d(TAG, "获取当前时间成功: $resultTime")
return Result(
success = true,
message = resultTime
)
} catch (e: Exception) {
Log.e(TAG, "获取当前时间失败: ${e.message}", e)
return Result(
success = false,
message = "获取当前时间失败: ${e.message}"
)
}
}
/**
* 获取当前位置
* @return 操作结果
*/
suspend fun getCurrentLocation(): Result = suspendCancellableCoroutine { continuation ->
Log.d(TAG, "开始获取当前位置")
try {
val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
// 检查位置权限
val fineLocationPermission = android.Manifest.permission.ACCESS_FINE_LOCATION
val coarseLocationPermission = android.Manifest.permission.ACCESS_COARSE_LOCATION
val hasFineLocationPermission = context.checkPermission(
fineLocationPermission, android.os.Process.myPid(), android.os.Process.myUid()
) == PackageManager.PERMISSION_GRANTED
val hasCoarseLocationPermission = context.checkPermission(
coarseLocationPermission, android.os.Process.myPid(), android.os.Process.myUid()
) == PackageManager.PERMISSION_GRANTED
if (!hasFineLocationPermission && !hasCoarseLocationPermission) {
Log.d(TAG, "没有位置权限")
// 如果没有权限,返回需要授权的结果
continuation.resume(Result(
success = false,
message = "需要位置权限才能获取当前位置"
))
return@suspendCancellableCoroutine
}
// 检查位置服务是否开启
if (!locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER) &&
!locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)) {
Log.d(TAG, "位置服务未启用")
// 提醒用户打开位置服务
try {
val settingsIntent = Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK
}
context.startActivity(settingsIntent)
} catch (e: Exception) {
Log.e(TAG, "无法打开位置设置: ${e.message}")
}
continuation.resume(Result(
success = false,
message = "位置服务未启用,请打开GPS或网络定位"
))
return@suspendCancellableCoroutine
}
// 尝试先获取最后已知位置
var bestLocation: Location? = null
try {
// 检查网络提供者最后已知位置
if (locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)) {
val networkLocation = locationManager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER)
if (networkLocation != null) {
bestLocation = networkLocation
Log.d(TAG, "获取到网络位置: ${networkLocation.latitude}, ${networkLocation.longitude}")
}
}
// 检查GPS提供者最后已知位置
if (locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) {
val gpsLocation = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER)
if (gpsLocation != null && (bestLocation == null || gpsLocation.accuracy < bestLocation.accuracy)) {
bestLocation = gpsLocation
Log.d(TAG, "获取到GPS位置: ${gpsLocation.latitude}, ${gpsLocation.longitude}")
}
}
} catch (e: Exception) {
Log.e(TAG, "获取最后已知位置失败: ${e.message}")
}
// 如果有最后已知位置,直接返回结果
if (bestLocation != null) {
val message = "当前位置: 经度 ${bestLocation.longitude}, 纬度 ${bestLocation.latitude}"
Log.d(TAG, message)
continuation.resume(Result(
success = true,
message = message
))
return@suspendCancellableCoroutine
}
// 如果没有最后已知位置,尝试请求更新位置(设置10秒超时)
val locationListener = object : LocationListener {
override fun onLocationChanged(location: Location) {
// 收到位置更新
val message = "当前位置: 经度 ${location.longitude}, 纬度 ${location.latitude}"
Log.d(TAG, "位置更新: $message")
locationManager.removeUpdates(this)
if (continuation.isActive) {
continuation.resume(Result(
success = true,
message = message
))
}
}
@Deprecated("Deprecated in Java")
override fun onStatusChanged(provider: String?, status: Int, extras: Bundle?) {
// 不需要处理
}
override fun onProviderEnabled(provider: String) {
// 不需要处理
}
override fun onProviderDisabled(provider: String) {
// 如果提供者被禁用,检查是否还有其他可用提供者
if (!locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER) &&
!locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)) {
locationManager.removeUpdates(this)
if (continuation.isActive) {
continuation.resume(Result(
success = false,
message = "位置服务已被禁用"
))
}
}
}
}
// 注册位置更新请求
if (locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)) {
locationManager.requestLocationUpdates(
LocationManager.NETWORK_PROVIDER,
0L,
0f,
locationListener
)
}
if (locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) {
locationManager.requestLocationUpdates(
LocationManager.GPS_PROVIDER,
0L,
0f,
locationListener
)
}
// 设置超时取消
continuation.invokeOnCancellation {
try {
locationManager.removeUpdates(locationListener)
} catch (e: Exception) {
Log.e(TAG, "取消位置更新失败: ${e.message}")
}
}
// 启动超时处理
android.os.Handler(android.os.Looper.getMainLooper()).postDelayed({
if (continuation.isActive) {
try {
locationManager.removeUpdates(locationListener)
continuation.resume(Result(
success = false,
message = "获取位置超时,请稍后重试"
))
} catch (e: Exception) {
Log.e(TAG, "超时处理失败: ${e.message}")
}
}
}, 10000L) // 10秒超时
} catch (e: Exception) {
Log.e(TAG, "获取位置过程中发生异常: ${e.message}")
if (continuation.isActive) {
continuation.resume(Result(
success = false,
message = "获取位置失败: ${e.message}"
))
}
}
}
/**
* 操作结果数据类
*/

2
local_plugins/opus/android/build.gradle.kts

@ -12,7 +12,7 @@ android {
namespace = "com.yunqiinnovation.opus"
// 目标 SDK 版本
compileSdk = 31
compileSdk = 35
defaultConfig {
// 最低 SDK 版本

2
local_plugins/spotify_sdk/android/build.gradle

@ -32,7 +32,7 @@ android {
namespace 'de.minimalme.spotify_sdk'
}
compileSdk 34
compileSdk 35
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17

2
local_plugins/volcano_speech/android/build.gradle.kts

@ -27,7 +27,7 @@ plugins {
// 配置android扩展
configure<LibraryExtension> {
namespace = "com.yunqiinnovation.volcano_speech"
compileSdkVersion(33)
compileSdkVersion(35)
defaultConfig {
minSdk = 21

2
pubspec.yaml

@ -84,6 +84,8 @@ dependencies:
path: local_plugins/deepvoice_utils
classic_bluetooth:
path: local_plugins/classic_bluetooth
agent_service:
path: local_plugins/agent_service
dev_dependencies:

4
test.json

@ -13,4 +13,6 @@ adb shell am broadcast \
adb shell dumpsys package com.yunqiinnovation.deepsound | grep BleWakeReceiver
adb shell dumpsys package com.yunqiinnovation.deepsound | grep receiver
adb shell dumpsys package com.yunqiinnovation.deepsound | grep receiver
adb logcat -v time | grep -i BleForegroundService

101
test2.json

@ -0,0 +1,101 @@
curl -v -X POST -H 'Content-Type: application/json' -H 'Authorization: Bearer 168deb3d-fd0c-4912-b9f1-aaee5c6743e6' -H 'Accept: text/event-stream' -d '{
"model": "doubao-1-5-pro-32k-250115",
"messages": [
[],
{
"role": "user",
"content": "你好,你能听到我在说话吗?"
}
],
"temperature": 0.7,
"max_tokens": 2000,
"stream": true,
"tools": [
{
"type": "function",
"function": {
"name": "exit_interaction",
"description": "退出当前语音交互",
"parameters": {
"type": "object",
"properties": {},
"required": []
}
}
},
{
"type": "function",
"function": {
"name": "enter_translation_mode",
"description": "用户请求进入实时翻译模式时,启动实时翻译功能",
"parameters": {
"type": "object",
"properties": {},
"required": []
}
}
},
{
"type": "function",
"function": {
"name": "send_text_message",
"description": "发送短信",
"parameters": {
"type": "object",
"properties": {
"contact": {
"type": "string",
"description": "联系人姓名或电话号码"
},
"message": {
"type": "string",
"description": "短信内容"
}
},
"required": [
"contact",
"message"
]
}
}
},
{
"type": "function",
"function": {
"name": "make_phone_call",
"description": "拨打电话",
"parameters": {
"type": "object",
"properties": {
"contact": {
"type": "string",
"description": "联系人姓名或电话号码"
}
},
"required": [
"contact"
]
}
}
},
{
"type": "function",
"function": {
"name": "open_app",
"description": "当用户请求打开特定应用时,启动相应应用",
"parameters": {
"type": "object",
"properties": {
"app_name": {
"type": "string",
"description": "应用名称"
}
},
"required": [
"app_name"
]
}
}
}
]
}' 'https: //ark.cn-beijing.volces.com/api/v3/chat/completions'
Loading…
Cancel
Save