Browse Source

Merge commit 'c4d1ee03583f3f2eeeb45408385490872114e9a3' into lxm

newdev_shunjiawei
lxm 1 year ago
parent
commit
810906242f
  1. 4
      lib/data/models/appconfig.dart
  2. 4
      lib/data/services/asr_service.dart
  3. 16
      lib/data/services/ble_manager.dart
  4. 21
      lib/data/services/music_manager.dart
  5. 9
      lib/data/services/network/api.dart
  6. 7
      lib/data/services/speech_impl/azure_asr_service.dart
  7. 3
      lib/data/services/speech_impl/flutter_asr_service.dart
  8. 8
      lib/data/services/speech_impl/volcano_asr_api_service.dart
  9. 10
      lib/data/services/speech_impl/volcano_asr_service.dart
  10. 3
      lib/data/services/speech_impl/xunfei_asr_service.dart
  11. 207
      lib/data/services/voice_interaction_impl/ios_voice_interaction_service.dart
  12. 50
      lib/modules/agent/controllers/agent_controller.dart
  13. 142
      lib/modules/agent/views/message_bubble.dart
  14. 14
      lib/modules/chat/controllers/chat_controller.dart
  15. 3
      lib/modules/meeting/controllers/meeting_record_controller.dart
  16. 9
      lib/modules/ota_upgrade/bindings/ota_upgrade_binding.dart
  17. 213
      lib/modules/ota_upgrade/controllers/ota_upgrade_controller.dart
  18. 54
      lib/modules/ota_upgrade/ota.dart
  19. 109
      lib/modules/ota_upgrade/views/ota_upgrade_view.dart
  20. 171
      lib/modules/pairing/controllers/pairing_controller.dart
  21. 28
      lib/modules/settings/views/settings_view.dart
  22. 150
      lib/modules/speech_test/controllers/speech_test_controller.dart
  23. 44
      lib/modules/translation/controllers/translation_controller.dart
  24. 14
      lib/routes/app_pages.dart
  25. 2
      lib/routes/app_routes.dart
  26. 1
      local_plugins/agent_service/android/build.gradle.kts
  27. 12
      local_plugins/agent_service/android/src/main/AndroidManifest.xml
  28. 209
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt
  29. 1
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt
  30. 2
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleAgent.kt
  31. 134
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleMediaService.kt
  32. 85
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleWakeReceiver.kt
  33. 19
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BootReceiver.kt
  34. 0
      local_plugins/agent_service/android/src/main/res/raw/silence.mp3
  35. BIN
      local_plugins/agent_service/android/src/main/res/raw/start.mp3
  36. BIN
      local_plugins/agent_service/android/src/main/res/raw/stop.mp3
  37. BIN
      local_plugins/agent_service/android/src/main/res/raw/stop2.mp3
  38. 5
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt
  39. 32
      local_plugins/ble_service/android/src/main/AndroidManifest.xml
  40. 481
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleCompanionManager.kt
  41. 73
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleCompanionService.kt
  42. 67
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt
  43. 127
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleServicePlugin.kt
  44. 69
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleWakeReceiver.kt
  45. 26
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BootReceiver.kt
  46. 179
      local_plugins/ble_service/lib/ble_service.dart
  47. 42
      local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt
  48. 15
      local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicServicePlugin.kt
  49. 11
      local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicServiceStarter.kt
  50. 7
      local_plugins/music_service/lib/music_service.dart
  51. 2
      local_plugins/music_service/pubspec.yaml
  52. 131
      local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/OpenAIService.kt
  53. BIN
      local_plugins/opus/.gradle/8.10/fileHashes/fileHashes.lock
  54. BIN
      local_plugins/opus/.gradle/buildOutputCleanup/buildOutputCleanup.lock
  55. 2
      local_plugins/opus/.gradle/buildOutputCleanup/cache.properties

4
lib/data/models/appconfig.dart

@ -21,8 +21,8 @@ class AppConfig {
}
//智能体配置
static String agent(String agentId) {
static String? agentsystemPrompt(String agentId) {
final foundAgent = config.agents.firstWhere((agent) => agent.id == agentId);
return jsonEncode((foundAgent));
return foundAgent.systemPrompt;
}
}

4
lib/data/services/asr_service.dart

@ -19,7 +19,9 @@ abstract class AsrService {
/// 开始连续语音识别
///
/// 返回一个包含识别事件的流
Future<Stream<RecognitionEvent>> startContinuousRecognition();
Future<Stream<RecognitionEvent>> startContinuousRecognition(
bool audioSourceType,
);
/// 停止连续语音识别
Future<bool> stopContinuousRecognition();

16
lib/data/services/ble_manager.dart

@ -404,6 +404,8 @@ class BleManager extends GetxService {
final openaiBaseUrl =
AppConfig.env('OPENAI_BASE_URL') ?? 'https://api.openai.com/v1';
final openaiModel = AppConfig.env('OPENAI_MODEL') ?? 'gpt-3.5-turbo';
final systemPrompt = AppConfig.agentsystemPrompt("personal_assistant");
// 从Flutter资源包中加载.mcp.json
final jsonData = AppConfig.mcpConfig();
@ -414,6 +416,7 @@ class BleManager extends GetxService {
'openaiApiKey': openaiApiKey,
'openaiBaseUrl': openaiBaseUrl,
'openaiModel': openaiModel,
'systemPrompt': systemPrompt,
'mcpServer': jsonEncode(jsonData),
};
@ -483,9 +486,6 @@ class BleManager extends GetxService {
}
}
registerBackgroundScan() {
_bleService.registerBackgroundScan();
}
/// 获取电池电量
Future<int> getBatteryLevel() async {
@ -537,6 +537,16 @@ class BleManager extends GetxService {
}
}
/// 打开解码器
Future<bool> openA2DPDecoder() async {
try {
return await _bleService.openA2DPDecoder();
} catch (e) {
Logger.error('打开解码器失败: $e');
return false;
}
}
/// 关闭编解码器
Future<bool> closeCodec() async {
try {

21
lib/data/services/music_manager.dart

@ -22,6 +22,23 @@ class MusicManager extends GetxService {
super.onClose();
}
//播放 音乐
Future<void> play(String url) async {}
//播放
Future<void> play(String url) async {
await _musicService.playMusic(url);
}
//暂停
Future<void> pause() async {
await _musicService.pauseMusic();
}
//继续
Future<void> resume() async {
await _musicService.resumeMusic();
}
//状态
Future<Map<String, dynamic>> getStatus() async {
return await _musicService.getStatus();
}
}

9
lib/data/services/network/api.dart

@ -73,4 +73,13 @@ class Api {
params: params,
);
}
//app获取设备信息
static getequipmentinfo([params]) {
return DioManager().request(
NWMethod.post,
'/api/home/user_getequipmentinfo',
params: params,
);
}
}

7
lib/data/services/speech_impl/azure_asr_service.dart

@ -150,7 +150,8 @@ class AzureAsrService extends GetxService implements AsrService {
}
@override
Future<Stream<RecognitionEvent>> startContinuousRecognition() async {
Future<Stream<RecognitionEvent>> startContinuousRecognition(
bool audioSourceType) async {
if (!_isInitialized) {
await initialize();
}
@ -164,7 +165,9 @@ class AzureAsrService extends GetxService implements AsrService {
// 开始连续识别
final bool result =
await _channel.invokeMethod('startContinuousRecognition');
await _channel.invokeMethod('startContinuousRecognition', {
'audioSourceType': audioSourceType,
});
if (!result) {
_cleanupEventStream();

3
lib/data/services/speech_impl/flutter_asr_service.dart

@ -363,7 +363,8 @@ class FlutterAsrService extends GetxService implements AsrService {
}
@override
Future<Stream<RecognitionEvent>> startContinuousRecognition() async {
Future<Stream<RecognitionEvent>> startContinuousRecognition(
bool audioSourceType) async {
try {
if (!_isAvailable.value) {
final controller = StreamController<RecognitionEvent>.broadcast();

8
lib/data/services/speech_impl/volcano_asr_api_service.dart

@ -36,7 +36,8 @@ class VolcanoAsrApiService implements AsrService {
final int _bitsPerSample = 16;
final int _channels = 1;
final String _encoding = 'pcm';
// 音频输入源
bool _audioSourceType = false;
// 连接相关
WebSocketChannel? _channel;
Completer<void>? _connectionCompleter;
@ -113,7 +114,7 @@ class VolcanoAsrApiService implements AsrService {
final resultCompleter = Completer<RecognitionEvent>();
// 开始连续识别流程
final stream = await startContinuousRecognition();
final stream = await startContinuousRecognition(_audioSourceType);
// 监听结果
StreamSubscription? subscription;
@ -156,7 +157,8 @@ class VolcanoAsrApiService implements AsrService {
}
@override
Future<Stream<RecognitionEvent>> startContinuousRecognition() async {
Future<Stream<RecognitionEvent>> startContinuousRecognition(
bool audioSourceType) async {
if (!_isInitialized) {
await initialize();
}

10
lib/data/services/speech_impl/volcano_asr_service.dart

@ -265,7 +265,8 @@ class VolcanoAsrService extends GetxService implements AsrService {
/// 开始连续语音识别
@override
Future<Stream<RecognitionEvent>> startContinuousRecognition() async {
Future<Stream<RecognitionEvent>> startContinuousRecognition(
bool audioSourceType) async {
if (!await _ensureInitialized()) {
_eventStreamController.add(RecognitionEvent(
type: RecognitionEventType.error,
@ -275,9 +276,10 @@ class VolcanoAsrService extends GetxService implements AsrService {
}
try {
final success =
await _channel.invokeMethod<bool>('startContinuousRecognition') ??
false;
final success = await _channel.invokeMethod<bool>('audioSourceType', {
'audioSourceType': audioSourceType,
}) ??
false;
if (success) {
_isContinuousRecognitionActive = true;

3
lib/data/services/speech_impl/xunfei_asr_service.dart

@ -75,7 +75,8 @@ class XunfeiAsrService extends GetxService implements AsrService {
}
@override
Future<Stream<RecognitionEvent>> startContinuousRecognition() async {
Future<Stream<RecognitionEvent>> startContinuousRecognition(
bool audioSourceType) async {
if (!_isInitialized) {
await initialize();
}

207
lib/data/services/voice_interaction_impl/ios_voice_interaction_service.dart

@ -9,8 +9,8 @@ import '../../models/events/voice_interaction_event.dart';
import '../../../core/utils/logger.dart';
enum VoiceInteractionState {
idle, // 等待唤醒
active, // 活跃状态 - 可以同时识别和播放
idle, // 等待唤醒
active, // 活跃状态 - 可以同时识别和播放
}
class IosVoiceInteractionService extends GetxService {
@ -18,45 +18,47 @@ class IosVoiceInteractionService extends GetxService {
final TtsService _ttsService = Get.find<TtsService>();
final VolcanoAIService _aiService = Get.find<VolcanoAIService>();
final ChatHistoryService _chatHistoryService = Get.find<ChatHistoryService>();
// 当前状态
final Rx<VoiceInteractionState> state = VoiceInteractionState.idle.obs;
// 是否识别到用户语音
final RxBool isSpeechDetected = false.obs;
// 是否正在处理AI响应
final RxBool isProcessingAI = false.obs;
// 是否正在播放TTS
final RxBool isSpeaking = false.obs;
// 对话历史记录
final List<Map<String, String>> _messageHistory = [];
// 最大历史记录数
static const int _maxHistorySize = 10;
// 超时计时器
Timer? _inactivityTimer;
// 流订阅
StreamSubscription? _recognitionSubscription;
// 当前识别的文本
String _currentRecognizedText = '';
// 音频输入源
bool _audioSourceType = false;
// 事件流控制器
final StreamController<VoiceInteractionEvent> _eventStreamController =
final StreamController<VoiceInteractionEvent> _eventStreamController =
StreamController<VoiceInteractionEvent>.broadcast();
// 获取事件流
@override
Stream<VoiceInteractionEvent> get eventStream => _eventStreamController.stream;
Stream<VoiceInteractionEvent> get eventStream =>
_eventStreamController.stream;
// 标识符,用于存储聊天历史
final String _agentId = 'ios_assistant';
/// 初始化服务
@override
Future<IosVoiceInteractionService> initialize() async {
@ -66,10 +68,10 @@ class IosVoiceInteractionService extends GetxService {
} catch (e) {
Logger.error('iOS语音交互服务初始化失败: $e');
}
return this;
}
// 处理唤醒事件
void onWakeup() {
_ttsService.speakOnce('我在呢!');
@ -79,63 +81,61 @@ class IosVoiceInteractionService extends GetxService {
_activateInteraction();
}
}
// 激活交互
void _activateInteraction() {
// 设置状态为活跃
state.value = VoiceInteractionState.active;
// 开始连续监听
_startContinuousListening();
// 启动不活动计时器
_startInactivityTimer();
// 发送识别开始事件
_sendRecognitionStartedEvent();
}
// 开始连续监听
Future<void> _startContinuousListening() async {
// 停止之前的监听
_recognitionSubscription?.cancel();
try {
// 开始连续识别
final recognitionStream = await _asrService.startContinuousRecognition();
final recognitionStream =
await _asrService.startContinuousRecognition(_audioSourceType);
// 监听识别结果
_recognitionSubscription = recognitionStream.listen(
_handleRecognitionEvent,
onError: (error) {
Logger.error('语音识别错误: $error');
_startContinuousListening(); // 尝试重新启动
}
);
_recognitionSubscription =
recognitionStream.listen(_handleRecognitionEvent, onError: (error) {
Logger.error('语音识别错误: $error');
_startContinuousListening(); // 尝试重新启动
});
Logger.info('开始连续语音识别');
} catch (e) {
Logger.error('启动语音识别失败: $e');
}
}
// 处理识别事件
void _handleRecognitionEvent(RecognitionEvent event) {
// 更新活动时间
_updateActivityTime();
switch (event.type) {
case RecognitionEventType.intermediateResult:
// 检测用户是否开始说话
if (event.text.isNotEmpty && !isSpeechDetected.value) {
isSpeechDetected.value = true;
// 用户开始讲话时,立即中断当前响应
_interruptCurrentResponse("检测到用户开始讲话,中断当前响应");
}
break;
case RecognitionEventType.finalResult:
if (event.text.isNotEmpty) {
// 最终结果,处理用户输入
@ -144,16 +144,16 @@ class IosVoiceInteractionService extends GetxService {
// 重置语音检测状态
isSpeechDetected.value = false;
break;
case RecognitionEventType.sessionStarted:
Logger.info('语音识别会话开始');
break;
case RecognitionEventType.sessionStopped:
Logger.info('语音识别会话结束');
break;
case RecognitionEventType.error:
case RecognitionEventType.canceled:
Logger.error('语音识别错误: ${event.error}');
@ -161,11 +161,11 @@ class IosVoiceInteractionService extends GetxService {
break;
}
}
// 中断当前响应
void _interruptCurrentResponse(String reason) {
Logger.info(reason);
// 如果正在活跃状态,需要中断当前操作
if (state.value == VoiceInteractionState.active) {
// 停止TTS播放
@ -174,40 +174,40 @@ class IosVoiceInteractionService extends GetxService {
isProcessingAI.value = false;
}
}
// 处理用户输入
Future<void> _processUserInput(String text) async {
Logger.info('处理用户输入: $text');
// 设置为处理状态
isProcessingAI.value = true;
// 添加用户消息到历史记录
final userMessage = {'role': 'user', 'content': text};
_addToHistory(userMessage);
// 创建用户消息对象
final userMsg = Message(
role: 'user',
content: text,
timestamp: DateTime.now(),
);
// AI响应处理标志
bool isProcessingCancelled = false;
try {
// 构建消息历史
final messageHistory = _buildMessageHistory();
// 调用AI服务获取响应
final responseStream = _aiService.sendMessageStream(
messages: messageHistory,
systemPrompt: "你是一个智能助手,请简明扼要地回答问题。",
);
String aiResponse = '';
// 等待AI响应
await for (final chunk in responseStream) {
// 检查是否被用户打断
@ -217,49 +217,49 @@ class IosVoiceInteractionService extends GetxService {
Logger.info('AI响应生成过程中被用户打断');
break;
}
// 如果状态已改变(可能由其他原因导致),停止处理
if (state.value != VoiceInteractionState.active) {
isProcessingCancelled = true;
break;
}
aiResponse += chunk;
// 立即播放当前文本块,实现边生成边播放
_speakStreamResponse(chunk);
}
// 如果处理被取消,不继续后续操作
if (isProcessingCancelled) {
isProcessingAI.value = false;
return;
}
// 确保完整响应被处理
if (aiResponse.isNotEmpty) {
// 将AI响应添加到历史记录
final assistantMessage = {'role': 'assistant', 'content': aiResponse};
_addToHistory(assistantMessage);
// 创建助手消息对象
final assistantMsg = Message(
role: 'assistant',
content: aiResponse,
timestamp: DateTime.now(),
);
// 保存聊天历史
await _saveChatHistory(userMsg, assistantMsg);
// 发送聊天历史事件
_sendChatHistoryEvent(userMsg.content, assistantMsg.content);
// 刷新TTS流,确保所有文本都被播放
await _ttsService.flushStream();
isSpeaking.value = false;
}
// 处理完成
isProcessingAI.value = false;
} catch (e) {
@ -268,59 +268,59 @@ class IosVoiceInteractionService extends GetxService {
isProcessingAI.value = false;
}
}
// 构建用于AI服务的消息历史
List<Map<String, String>> _buildMessageHistory() {
return List<Map<String, String>>.from(_messageHistory);
}
// 添加消息到历史记录
void _addToHistory(Map<String, String> message) {
_messageHistory.add(message);
// 限制历史记录大小
while (_messageHistory.length > _maxHistorySize) {
_messageHistory.removeAt(0);
}
}
// 播放流式响应
void _speakStreamResponse(String chunk) {
if (chunk.isEmpty) return;
// 更新播放状态
isSpeaking.value = true;
// 使用流式TTS播放
_ttsService.speakStream(chunk);
// 更新活动时间
_updateActivityTime();
}
// 停止TTS
void _stopTts() {
_ttsService.stop();
isSpeaking.value = false;
}
// 更新最后活动时间
DateTime _lastActivityTime = DateTime.now();
void _updateActivityTime() {
_lastActivityTime = DateTime.now();
}
// 启动不活动计时器(长时间无交互会切换到空闲状态)
void _startInactivityTimer() {
_cancelInactivityTimer();
_inactivityTimer = Timer.periodic(Duration(seconds: 5), (timer) {
// 计算空闲时间
final idleTime = DateTime.now().difference(_lastActivityTime).inSeconds;
// 如果空闲超过30秒,且不在播放或检测到语音,切换到空闲
if (idleTime > 8 &&
!isSpeaking.value &&
if (idleTime > 8 &&
!isSpeaking.value &&
!isProcessingAI.value &&
!isSpeechDetected.value) {
_resetToIdle();
@ -328,37 +328,37 @@ class IosVoiceInteractionService extends GetxService {
}
});
}
// 取消不活动计时器
void _cancelInactivityTimer() {
_inactivityTimer?.cancel();
_inactivityTimer = null;
}
// 重置到空闲状态
void _resetToIdle() {
_ttsService.speakOnce('没有听到声音,暂停对话,双击耳机唤醒!');
_cancelInactivityTimer();
// 停止TTS
_stopTts();
// 停止语音识别
_recognitionSubscription?.cancel();
_asrService.stopContinuousRecognition();
// 重置状态
state.value = VoiceInteractionState.idle;
isSpeechDetected.value = false;
isProcessingAI.value = false;
isSpeaking.value = false;
// 清空历史记录
_messageHistory.clear();
Logger.info('语音交互已重置为空闲状态');
}
// 暂停语音交互
@override
void pauseVoiceInteraction() {
@ -366,53 +366,53 @@ class IosVoiceInteractionService extends GetxService {
_stopTts();
_recognitionSubscription?.cancel();
_asrService.stopContinuousRecognition();
// 重置所有状态
state.value = VoiceInteractionState.idle;
isSpeechDetected.value = false;
isProcessingAI.value = false;
isSpeaking.value = false;
// 清空历史记录
_messageHistory.clear();
}
}
// 恢复语音交互
void resumeVoiceInteraction() {
if (state.value == VoiceInteractionState.idle) {
_activateInteraction();
}
}
// 手动停止交互
void stopInteraction() {
_resetToIdle();
}
// 获取当前状态
VoiceInteractionState getCurrentState() {
return state.value;
}
// 是否处于活跃状态
@override
bool isActive() {
return state.value != VoiceInteractionState.idle;
}
// 获取当前对话历史
@override
List<Map<String, String>> getMessageHistory() {
return List<Map<String, String>>.from(_messageHistory);
}
// 获取当前服务ID
@override
String getServiceId() {
return _agentId;
}
// 释放资源
@override
void onClose() {
@ -422,25 +422,25 @@ class IosVoiceInteractionService extends GetxService {
_eventStreamController.close();
super.onClose();
}
// 保存聊天历史
Future<void> _saveChatHistory(Message userMsg, Message assistantMsg) async {
try {
// 加载现有历史记录
final existingMessages = await _chatHistoryService.loadHistory(_agentId);
// 添加新消息
existingMessages.addAll([userMsg, assistantMsg]);
// 保存更新后的历史记录
await _chatHistoryService.saveHistory(_agentId, existingMessages);
Logger.info('保存聊天历史记录成功');
} catch (e) {
Logger.error('保存聊天历史记录失败: $e');
}
}
// 发送聊天历史事件
void _sendChatHistoryEvent(String userMessage, String assistantMessage) {
final event = ChatHistoryEvent(
@ -449,19 +449,18 @@ class IosVoiceInteractionService extends GetxService {
assistantMessage: assistantMessage,
timestamp: DateTime.now().millisecondsSinceEpoch,
);
_eventStreamController.add(event);
Logger.info('已发送聊天历史事件');
}
// 发送识别开始事件
void _sendRecognitionStartedEvent() {
final event = RecognitionStartedEvent(
timestamp: DateTime.now().millisecondsSinceEpoch,
);
_eventStreamController.add(event);
Logger.info('已发送识别开始事件');
}
}

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

@ -1,6 +1,7 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:deep_voice/data/services/music_manager.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:agent_service/agent_service.dart';
@ -40,6 +41,11 @@ class AgentController extends GetxController {
final TextEditingController textController = TextEditingController();
final ScrollController scrollController = ScrollController();
//音乐播放管理
final _musiceManager = Get.find<MusicManager>();
var isPlaying = false.obs;
var currentUrl = "".obs;
// 状态标志
final isInitialized = false.obs;
final isListening = false.obs;
@ -101,13 +107,17 @@ class AgentController extends GetxController {
if (result.messages.isNotEmpty) {
// 转换并添加消息到列表
final historyMessages = result.messages
.map((msg) => Message(
isUser: msg.sender == 'user',
text: msg.message,
timestamp: DateTime.parse(msg.timestamp),
))
.toList();
final historyMessages = result.messages.map((msg) {
final meta = msg.metadata != null && msg.metadata != ""
? jsonDecode(msg.metadata!)
: null; // 如果为空,使用空对象
return Message(
isUser: msg.sender == 'user',
text: msg.message,
meta: meta,
timestamp: DateTime.parse(msg.timestamp),
);
}).toList();
// 消息按时间排序,最新的消息在底部
historyMessages.sort((a, b) => a.timestamp.compareTo(b.timestamp));
@ -607,4 +617,30 @@ class AgentController extends GetxController {
void translateImageText() {
useImagePrompt('请翻译这张图片中的文字');
}
// 播放音乐
void musicStatus() async {
var status = await _musiceManager.getStatus();
logger.i('获取当前音乐播放状态 $status');
isPlaying.value = status['isPlaying'];
currentUrl.value = status['url'];
}
// 播放音乐
void musicPlay(String url) {
_musiceManager.play(url);
isPlaying.value = true;
currentUrl.value = url;
}
// 播放音乐
void musicPause() {
_musiceManager.pause();
isPlaying.value = false;
}
void musicResume() {
_musiceManager.resume();
isPlaying.value = true;
}
}

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

@ -1,9 +1,13 @@
import 'package:common_utils/common_utils.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
import 'dart:io';
import '../controllers/agent_controller.dart';
/// 消息气泡组件
class MessageBubble extends StatelessWidget {
class MessageBubble extends GetView<AgentController> {
final bool isUser;
final String message;
final DateTime timestamp;
@ -199,11 +203,28 @@ class MessageBubble extends StatelessWidget {
//音乐卡片
return _buildMusicItem(context, musicName, musicUrl, musicSinger);
} else if (mate!.keys.contains('card_musiclist')) {
final rawList = mate!['card_musiclist']['musics'] as List;
final playlist =
rawList.map((e) => Map<String, dynamic>.from(e)).toList();
// final playlist =
// mate!['card_musiclist']['musics'] as List<Map<String, String>>;
//音乐播放列表
return _buildMusicContent(context);
return _buildMusicContent(context, playlist);
} else if (mate!.keys.contains('card_weather')) {
final city = mate!['card_weather']['city'] as String;
final rawList = mate!['card_weather']['dayweather'] as List;
final daylist =
rawList.map((e) => Map<String, dynamic>.from(e)).toList();
final now = daylist[0];
//天气
return _buildWeatherContent(context);
return _buildWeatherContent(
context,
city,
now,
daylist,
);
}
// if (message.contains('播放列表') || message.contains('音乐')) {
@ -250,11 +271,12 @@ class MessageBubble extends StatelessWidget {
}
/// 构建音乐内容卡片
Widget _buildMusicContent(BuildContext context) {
Widget _buildMusicContent(
BuildContext context, List<Map<String, dynamic>> playlist) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
const Text(
'以下是一些推荐的音乐:',
style: TextStyle(
color: Colors.black87,
@ -301,18 +323,22 @@ class MessageBubble extends StatelessWidget {
),
const SizedBox(height: 10),
// 歌曲示例
_buildMusicItem(
context,
'雨声与钢琴曲',
"",
'纯音乐',
),
Divider(height: 1, thickness: 1, color: Colors.grey[100]),
_buildMusicItem(context, 'Rainy Day', "", 'Coldplay'),
Divider(height: 1, thickness: 1, color: Colors.grey[100]),
_buildMusicItem(context, '雨天的爵士咖啡馆', "", '爵士合集'),
// 动态生成音乐条目
...playlist.asMap().entries.map((entry) {
final index = entry.key;
final item = entry.value;
final name = item["name"] ?? "未知歌曲";
final url = item["url"] ?? "";
final singer = item["sgener"] ?? "未知歌手";
return Column(
children: [
_buildMusicItem(context, name, url, singer),
if (index != playlist.length - 1)
Divider(height: 1, thickness: 1, color: Colors.grey[100]),
],
);
}).toList(),
const SizedBox(height: 8),
Center(
child: TextButton(
@ -507,6 +533,8 @@ class MessageBubble extends StatelessWidget {
/// 构建音乐项目
Widget _buildMusicItem(
BuildContext context, String name, String url, String singer) {
final controller = Get.find<AgentController>(); // 获取控制器
controller.musicStatus(); //刷新状态
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
@ -550,14 +578,35 @@ class MessageBubble extends StatelessWidget {
],
),
),
IconButton(
onPressed: () {},
icon:
Icon(Icons.play_circle, color: Theme.of(context).primaryColor),
padding: EdgeInsets.zero,
constraints: BoxConstraints(),
iconSize: 24,
),
Obx(() {
final isThisPlaying = controller.isPlaying.value &&
controller.currentUrl.value == url;
return IconButton(
onPressed: () {
if (controller.isPlaying.value) {
if (controller.currentUrl.value == url) {
controller.musicPause();
} else {
controller.musicPlay(url);
}
} else {
if (controller.currentUrl.value == url) {
controller.musicResume();
} else {
controller.musicPlay(url);
}
}
},
icon: Icon(
isThisPlaying ? Icons.pause_circle : Icons.play_circle,
color: Theme.of(context).primaryColor,
),
padding: EdgeInsets.zero,
constraints: BoxConstraints(),
iconSize: 24,
);
}),
],
),
);
@ -622,7 +671,8 @@ class MessageBubble extends StatelessWidget {
}
/// 构建天气内容卡片
Widget _buildWeatherContent(BuildContext context) {
Widget _buildWeatherContent(BuildContext context, String city,
Map<String, dynamic> now, List<Map<String, dynamic>> days) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@ -664,7 +714,7 @@ class MessageBubble extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'北京',
city,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
@ -673,7 +723,7 @@ class MessageBubble extends StatelessWidget {
),
SizedBox(height: 4),
Text(
'晴朗',
now["weatherCondition"],
style: TextStyle(
fontSize: 14,
color: Colors.white.withOpacity(0.9),
@ -683,7 +733,7 @@ class MessageBubble extends StatelessWidget {
Row(
children: [
Text(
'25°',
"${now["temperature"]}°",
style: TextStyle(
fontSize: 36,
fontWeight: FontWeight.bold,
@ -695,14 +745,14 @@ class MessageBubble extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'最高: 29°',
'最高: ${now["maxTemperature"]}°',
style: TextStyle(
fontSize: 12,
color: Colors.white.withOpacity(0.9),
),
),
Text(
'最低: 18°',
'最低: ${now["minTemperature"]}°',
style: TextStyle(
fontSize: 12,
color: Colors.white.withOpacity(0.9),
@ -736,15 +786,31 @@ class MessageBubble extends StatelessWidget {
),
),
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°'),
// ],
// ),
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°'),
],
children: List.generate(
days.length,
(index) {
String dateStr = days[index]['date'];
DateTime? date = DateUtil.getDateTime(dateStr);
String weekday = DateUtil.getWeekday(date);
return _buildForecastDay(
weekday,
Icons.wb_sunny,
days[index]['temperature'],
);
},
),
),
SizedBox(height: 16),
Container(
@ -753,7 +819,7 @@ class MessageBubble extends StatelessWidget {
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(8),
),
child: Row(
child: const Row(
children: [
Icon(
Icons.info_outline,

14
lib/modules/chat/controllers/chat_controller.dart

@ -78,6 +78,9 @@ class ChatController extends GetxController {
// 取消标志
bool _shouldCancelAiResponse = false;
// 音频输入源
bool _audioSourceType = false;
// 自动停止语音识别计时器
Timer? _autoStopTimer;
@ -184,7 +187,6 @@ class ChatController extends GetxController {
// 自动停止计时器会在startVoiceRecognition中启动
}
_getLocation();
player.playerStateStream.listen((state) async {
@ -211,8 +213,6 @@ class ChatController extends GetxController {
_amapFlutter.startLocation();
}
@override
void onClose() {
Logger.info('ChatController onClose');
@ -295,7 +295,6 @@ class ChatController extends GetxController {
Future<void> sendMessage(String text) async {
if (text.trim().isEmpty) return;
// 中断当前响应
_interruptCurrentResponse("发送新消息,中断当前响应");
@ -558,12 +557,11 @@ class ChatController extends GetxController {
Timer(const Duration(milliseconds: 260), () async {
if (isDebounce) return;
// 中断当前响应
_interruptCurrentResponse("发送新消息,中断当前响应");
try {
final recognitionStream =
await _asrService.startContinuousRecognition();
await _asrService.startContinuousRecognition(_audioSourceType);
await Future.delayed(const Duration(milliseconds: 260));
isDebounce = true;
recognizedText.value = '';
@ -619,11 +617,11 @@ class ChatController extends GetxController {
Future<void> startVoiceRecognition() async {
if (isVoiceRecognitionActive.value) return;
try {
// 启动连续语音识别
// 注意:在UI更新后,使用模拟波形动画替代实际麦克风输入的波形数据
final recognitionStream = await _asrService.startContinuousRecognition();
final recognitionStream =
await _asrService.startContinuousRecognition(_audioSourceType);
isVoiceRecognitionActive.value = true;
isListening.value = true;

3
lib/modules/meeting/controllers/meeting_record_controller.dart

@ -111,7 +111,8 @@ class MeetingRecordController extends GetxController
//开始录音
Future<void> _startRecorder() async {
final recognitionStream = await _asrService.startContinuousRecognition();
final recognitionStream =
await _asrService.startContinuousRecognition(false);
_recognitionSubscription = recognitionStream.listen(
_handleRecognitionEvent,
);

9
lib/modules/ota_upgrade/bindings/ota_upgrade_binding.dart

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

213
lib/modules/ota_upgrade/controllers/ota_upgrade_controller.dart

@ -0,0 +1,213 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'dart:async';
import 'dart:convert';
import 'package:get_storage/get_storage.dart';
import '../../../data/services/network/api.dart';
import '../ota.dart';
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:path_provider/path_provider.dart';
import 'package:json_annotation/json_annotation.dart';
class OtaUpgradeController extends GetxController {
final Dio _dio = Dio();
final RxString latestVersion = ''.obs;
final RxString currentVersion = ''.obs;
String updatepackageaddress = '';
final RxDouble downloadProgress = 0.0.obs;
final RxBool isUpdating = false.obs;
final isUpdateAvailable = true.obs;
final RxDouble progress = 0.0.obs;
@override
void onInit() {
super.onInit();
_getCurrentVersion();
}
// 获取当前版本信息
Future<void> _getCurrentVersion() async {
//发送ble获取固件版本
var firmwareVersion = "1.1.0";
currentVersion.value = firmwareVersion;
//发送ble获取产品id
var id = "a2";
//发送api请求
final response = await Api.getequipmentinfo({
"id": id,
});
if (response['info'] != '') {
print('getLoginToken:进来了');
// 根据API设计调整
final resp = OtaConfigResp.fromJson(response);
print('成功解析: ${resp.id}, ${resp.factory}');
latestVersion.value = resp.systemversion!;
updatepackageaddress = resp.updatepackageaddress!;
if (_isNewVersionAvailable(resp.systemversion!)) {
//需要升级
print('需要更新');
isUpdateAvailable.value = false;
} else {
isUpdateAvailable.value = true;
//已经是最新版本更新ui,按钮隐藏
}
} else {
throw const FormatException('API 响应缺少 "info" 字段');
}
}
// 版本比较方法优化
bool _isNewVersionAvailable(String latest) {
try {
List<int> currentParts = currentVersion.value
.split('.')
.map((e) => int.parse(e.trim()))
.toList();
List<int> latestParts =
latest.split('.').map((e) => int.parse(e.trim())).toList();
for (int i = 0; i < latestParts.length; i++) {
int current = (i < currentParts.length) ? currentParts[i] : 0;
if (latestParts[i] > current) return true;
if (latestParts[i] < current) return false;
}
return false;
} catch (e) {
print('版本比较错误: $e');
return false;
}
}
// 版本更新
Future<void> checkUpdate() async {
try {
if (latestVersion.value != "")
_showUpdateDialog(latestVersion.value);
else {
Get.snackbar('错误', '获取最新版本信息失败');
}
} catch (e) {
Get.snackbar('错误', '获取版本信息失败');
}
}
// 显示更新对话框
void _showUpdateDialog(String latest) {
Get.dialog(
AlertDialog(
title: Text('发现新版本 $latest'),
content: Text('是否立即更新?'),
actions: [
TextButton(
onPressed: () => Get.back(),
child: Text('取消'),
),
TextButton(
onPressed: () {
Get.back();
startDownload();
},
child: Text('更新'),
),
],
),
);
}
// 开始下载
Future<void> startDownload() async {
isUpdating.value = true;
// 查找最后一个 '/' 的位置
int lastIndex = updatepackageaddress.lastIndexOf('/');
// 截取最后一个 '/' 后面的字符串
String fileName = updatepackageaddress.substring(lastIndex + 1);
String directoryName = "ota_upgrades";
final dio = Dio(BaseOptions(
connectTimeout: const Duration(seconds: 5),
receiveTimeout: Duration.zero,
sendTimeout: const Duration(seconds: 5),
headers: {'Accept-Encoding': 'gzip'},
));
Get.snackbar('升级命令已发送', '开始下载...');
try {
final Directory? downloadsDir = await getDownloadsDirectory();
if (downloadsDir == null) {
throw FileSystemException('无法访问下载目录');
}
// 创建子目录:Download/APP
final targetDir = Directory('${downloadsDir.path}/$directoryName');
if (!await targetDir.exists()) {
await targetDir.create(recursive: true);
}
final savePath = '${targetDir.path}/$fileName';
print('文件保存路径: $savePath'); // 关键日志
final options = Options(
receiveTimeout: Duration.zero,
followRedirects: true,
maxRedirects: 5,
);
DateTime lastUpdate = DateTime.now();
await dio.download(
updatepackageaddress,
savePath,
options: options,
onReceiveProgress: (received, total) {
final now = DateTime.now();
if (now.difference(lastUpdate).inMilliseconds > 1000) {
lastUpdate = now;
String progressMsg = total != -1
? '${(received / total * 100).toStringAsFixed(1)}%'
: '已下载:${(received / 1024 / 1024).toStringAsFixed(1)}MB';
Get.snackbar(
'升级进度',
progressMsg,
duration: const Duration(seconds: 1),
);
}
},
);
// 下载完成后验证文件存在性
File file = File(savePath);
if (await file.exists()) {
Get.snackbar('下载完成', '文件已保存至: $savePath'); // 明确提示路径
} else {
Get.snackbar('错误', '文件未找到,请检查写入权限');
}
isUpdating.value = false;
//下载完成,开始升级
_installApp(savePath);
} on DioException catch (e) {
Get.snackbar('下载失败', '错误: ${e.message}');
} on FileSystemException catch (e) {
Get.snackbar('存储失败', '无法写入文件: ${e.message}');
} catch (e) {
Get.snackbar('未知错误', e.toString());
}
}
// 开始升级
void _installApp(String path) async {
// final File file = File(path);
// if (await file.exists()) {
// // OpenResult result = await OpenFile.open(path);
// // if (result.type != ResultType.done) {
// // Get.snackbar('错误', '安装失败: ${result.message}');
// // }
// }
}
}

54
lib/modules/ota_upgrade/ota.dart

@ -0,0 +1,54 @@
class OtaConfigResp {
// 单例实例
static OtaConfigResp? _instance;
// 单例访问器(增加空安全保护)
static OtaConfigResp get instance {
if (_instance == null) {
throw Exception("User must be initialized first");
}
return _instance!;
}
// 最终属性(不可变)
final String? id;
final String? factory;
final String? equipmentname;
final String? systemversion;
final String? updatepackageaddress;
// 私有构造函数
OtaConfigResp._({
this.id,
this.factory,
this.equipmentname,
this.systemversion,
this.updatepackageaddress,
});
factory OtaConfigResp.fromJson(Map<String, dynamic> json) {
print("数据获取fronTokenJson${json}");
// 修复1:安全获取嵌套的 user 字段
final infoData = json['info'] as Map<String, dynamic>?; // 允许空值
// 更新单例实例
return _instance = OtaConfigResp._(
id: (infoData?['id'] as String?) ?? '',
factory: (infoData?['factory'] as String?) ?? '',
equipmentname: (infoData?['equipmentname'] as String?) ?? '',
systemversion: (infoData?['systemversion'] as String?) ?? '',
updatepackageaddress:
(infoData?['updatepackageaddress'] as String?) ?? '',
);
}
// 完整的JSON序列化
Map<String, dynamic> toJson() => {
'user': {
'id': instance.id,
'factory': instance.factory,
'equipmentname': instance.equipmentname,
'systemversion': instance.systemversion,
'updatepackageaddress': instance.updatepackageaddress,
}
};
}

109
lib/modules/ota_upgrade/views/ota_upgrade_view.dart

@ -0,0 +1,109 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../controllers/ota_upgrade_controller.dart';
class OtaUpgradeView extends GetView<OtaUpgradeController> {
const OtaUpgradeView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
// 添加外层Center
child: SingleChildScrollView(
// 添加滚动防止小屏溢出
child: Padding(
padding: EdgeInsets.all(20),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center, // 水平居中
mainAxisSize: MainAxisSize.min, // 紧凑布局
children: [
// 设备图标
CircleAvatar(
radius: 60,
backgroundColor: Colors.blue[100],
child: Icon(
Icons.phone_iphone,
size: 60,
color: Colors.blue,
),
),
SizedBox(height: 30),
// 版本信息
Column(
children: [
Text(
'当前版本',
style: TextStyle(
fontSize: 18,
color: Colors.grey,
),
),
SizedBox(height: 8),
Text(
'v${controller.currentVersion}',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.blue,
),
),
],
),
SizedBox(height: 40),
// 更新按钮/进度条
Obx(() {
if (controller.isUpdating.value) {
return Column(
children: [
SizedBox(
width: 200, // 限制进度条宽度
child: LinearProgressIndicator(
value: controller.downloadProgress.value,
backgroundColor: Colors.grey[200],
valueColor:
AlwaysStoppedAnimation<Color>(Colors.blue),
minHeight: 12,
borderRadius: BorderRadius.circular(10),
),
),
SizedBox(height: 20),
Text(
'${(controller.downloadProgress.value * 100).toStringAsFixed(1)}%',
style: TextStyle(color: Colors.grey),
)
],
);
} else {
return ElevatedButton(
onPressed: controller.isUpdateAvailable.value
? null
: controller.checkUpdate,
style: ElevatedButton.styleFrom(
backgroundColor: controller.isUpdateAvailable.value
? Colors.grey
: Colors.blue,
foregroundColor: Colors.white,
padding:
EdgeInsets.symmetric(horizontal: 40, vertical: 15),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(30),
),
),
child: Text(
controller.isUpdateAvailable.value ? '已是最新版本' : '立即更新',
style: TextStyle(fontSize: 16),
),
);
}
}),
],
),
),
),
),
);
}
}

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

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

28
lib/modules/settings/views/settings_view.dart

@ -555,11 +555,32 @@ class SettingsView extends GetView<SettingsController> {
title: '语音测试',
subtitle: '测试语音识别和合成功能,支持多种服务实现',
icon: Icons.record_voice_over,
iconBgColor: isDarkMode ? Colors.orange[900]!.withOpacity(0.3) : Colors.orange[100]!,
iconColor: isDarkMode ? Colors.orange[300]! : Colors.orange[600]!,
iconBgColor: isDarkMode
? Colors.orange[900]!.withOpacity(0.3)
: Colors.orange[100]!,
iconColor:
isDarkMode ? Colors.orange[300]! : Colors.orange[600]!,
onTap: () => Get.toNamed(Routes.speechTest),
isDarkMode: isDarkMode,
),
Divider(
height: 1,
color: isDarkMode
? Colors.white.withOpacity(0.1)
: Colors.grey[200]),
// 测试ble耳机固件版本升级
_buildSimpleNavigationSetting(
title: 'ota升级',
subtitle: '测试ble耳机固件版本升级',
icon: Icons.record_voice_over,
iconBgColor: isDarkMode
? Colors.orange[900]!.withOpacity(0.3)
: Colors.orange[100]!,
iconColor:
isDarkMode ? Colors.orange[300]! : Colors.orange[600]!,
onTap: () => Get.toNamed(Routes.OTA_UPGRADE),
isDarkMode: isDarkMode,
),
Divider(
height: 1,
color: isDarkMode
@ -1191,8 +1212,7 @@ class SettingsView extends GetView<SettingsController> {
Obx(() {
final serviceType = controller.speechServiceType.value;
final serviceName = controller.availableSpeechServices
.firstWhere(
(service) => service['type'] == serviceType,
.firstWhere((service) => service['type'] == serviceType,
orElse: () => {'name': 'unknown'})['name'];
return Container(
constraints: BoxConstraints(maxWidth: 120.w), // 限制最大宽度

150
lib/modules/speech_test/controllers/speech_test_controller.dart

@ -11,47 +11,48 @@ import '../../../core/utils/logger.dart';
class SpeechTestController extends GetxController {
// 语音服务工厂
final SpeechFactory _speechFactory = Get.find<SpeechFactory>();
// 当前使用的TTS服务
late TtsService _ttsService;
// 当前使用的ASR服务
late AsrService _asrService;
// 状态变量
final RxBool isInitialized = false.obs;
final RxBool isProcessing = false.obs;
final RxBool isListening = false.obs;
final RxBool isSpeaking = false.obs;
// 识别结果
final RxString intermediateResult = ''.obs; // 中间结果
final RxString finalResult = ''.obs; // 最终结果
final RxString finalResult = ''.obs; // 最终结果
final RxString recognitionStatus = '准备就绪'.obs;
final RxString detectedLanguage = ''.obs; // 检测到的语言
final RxString detectedLanguage = ''.obs; // 检测到的语言
// TTS事件
final RxString ttsStatus = '准备就绪'.obs;
// 当前选择的语音服务类型
final Rx<SpeechServiceType> selectedServiceType = SpeechServiceType.azure.obs;
// 音频输入源
bool _audioSourceType = false;
// 连续识别结果流
StreamSubscription<RecognitionEvent>? _recognitionSubscription;
// TTS事件流
StreamSubscription<TtsEvent>? _ttsEventSubscription;
// 文本控制器
final TextEditingController textController = TextEditingController();
// 连续播放的测试句子
final List<String> testSentences = [
'这是第一句测试语音。',
'这是第二句测试语音,语音合成支持连续播放。',
'这是第三句测试语音,将在连续模式下播放完毕。'
];
@override
void onInit() {
super.onInit();
@ -59,23 +60,23 @@ class SpeechTestController extends GetxController {
textController.text = '欢迎使用语音测试,这是一段默认的测试文本。您可以修改此文本,然后点击播放按钮测试语音合成效果。';
_initServices();
}
/// 初始化服务
Future<void> _initServices() async {
try {
isProcessing.value = true;
// 获取服务实例
_ttsService = _speechFactory.getTtsService();
_asrService = _speechFactory.getAsrService();
// 初始化服务
await _ttsService.initialize();
await _asrService.initialize();
// 监听TTS事件
_setupTtsEventListener();
isInitialized.value = true;
ttsStatus.value = '准备就绪';
recognitionStatus.value = '准备就绪';
@ -87,12 +88,12 @@ class SpeechTestController extends GetxController {
isProcessing.value = false;
}
}
/// 设置TTS事件监听
void _setupTtsEventListener() {
// 取消旧的订阅
_ttsEventSubscription?.cancel();
// 设置新的订阅
_ttsEventSubscription = _ttsService.onEvent.listen((event) {
switch (event.type) {
@ -115,18 +116,18 @@ class SpeechTestController extends GetxController {
}
});
}
/// 设置ASR事件监听
void _setupAsrEventListener(Stream<RecognitionEvent> stream) {
// 取消旧的订阅
_recognitionSubscription?.cancel();
// 重置识别结果
intermediateResult.value = '';
finalResult.value = '';
detectedLanguage.value = '';
recognitionStatus.value = '正在等待语音输入...';
// 设置新的订阅
_recognitionSubscription = stream.listen((event) {
switch (event.type) {
@ -134,14 +135,14 @@ class SpeechTestController extends GetxController {
isListening.value = true;
recognitionStatus.value = '开始聆听...';
break;
case RecognitionEventType.sessionStopped:
isListening.value = false;
recognitionStatus.value = '聆听已结束';
// 清空中间结果,保留最终结果
intermediateResult.value = '';
break;
case RecognitionEventType.intermediateResult:
// 更新中间结果,显示为斜体或不同颜色
intermediateResult.value = event.text;
@ -150,7 +151,7 @@ class SpeechTestController extends GetxController {
}
recognitionStatus.value = '正在识别...';
break;
case RecognitionEventType.finalResult:
// 更新最终结果,显示为正常文本
finalResult.value = event.text;
@ -160,12 +161,12 @@ class SpeechTestController extends GetxController {
}
recognitionStatus.value = '识别完成';
break;
case RecognitionEventType.error:
isListening.value = false;
recognitionStatus.value = '识别错误: ${event.error}';
break;
case RecognitionEventType.canceled:
isListening.value = false;
recognitionStatus.value = '识别已取消';
@ -179,21 +180,21 @@ class SpeechTestController extends GetxController {
intermediateResult.value = '';
});
}
/// 切换语音服务类型
void switchServiceType(SpeechServiceType type) async {
if (selectedServiceType.value == type) return;
try {
isProcessing.value = true;
// 停止当前的所有服务状态
await stopAll();
// 切换服务类型
_speechFactory.setServiceType(type);
selectedServiceType.value = type;
// 重新初始化服务
await _initServices();
} catch (e) {
@ -202,40 +203,40 @@ class SpeechTestController extends GetxController {
isProcessing.value = false;
}
}
/// 停止所有活动
Future<void> stopAll() async {
// 停止ASR
if (isListening.value) {
await stopContinuousRecognition();
}
// 停止TTS
if (isSpeaking.value) {
await stopSpeaking();
}
// 取消所有订阅
await _recognitionSubscription?.cancel();
_recognitionSubscription = null;
}
/// 单次播放文本
Future<void> speakOnce() async {
if (!isInitialized.value || isProcessing.value) return;
final text = textController.text.trim();
if (text.isEmpty) {
ttsStatus.value = '请输入要播放的文本';
return;
}
try {
isProcessing.value = true;
ttsStatus.value = '准备播放...';
final result = await _ttsService.speakOnce(text);
if (!result) {
ttsStatus.value = '播放失败';
}
@ -246,15 +247,15 @@ class SpeechTestController extends GetxController {
isProcessing.value = false;
}
}
/// 连续播放测试
Future<void> speakContinuously() async {
if (!isInitialized.value || isProcessing.value) return;
try {
isProcessing.value = true;
ttsStatus.value = '准备连续播放...';
// 为避免阻塞,使用一个句子接一个句子的方式添加到队列
for (final sentence in testSentences) {
if (!await _ttsService.speakStream(sentence)) {
@ -264,10 +265,9 @@ class SpeechTestController extends GetxController {
// 给前一个句子一点处理时间
await Future.delayed(const Duration(milliseconds: 50));
}
// 刷新队列,确保播放完成
await _ttsService.flushStream();
} catch (e) {
ttsStatus.value = '连续播放异常: $e';
Logger.error('连续播放异常: $e');
@ -275,11 +275,11 @@ class SpeechTestController extends GetxController {
isProcessing.value = false;
}
}
/// 停止播放
Future<void> stopSpeaking() async {
if (!isInitialized.value) return;
try {
await _ttsService.stop();
} catch (e) {
@ -287,39 +287,39 @@ class SpeechTestController extends GetxController {
Logger.error('停止播放异常: $e');
}
}
/// 一次性识别
Future<void> recognizeOnce() async {
if (!isInitialized.value || isProcessing.value || isListening.value) return;
try {
isProcessing.value = true;
recognitionStatus.value = '开始识别...';
// 清空之前的结果
intermediateResult.value = '';
finalResult.value = '';
detectedLanguage.value = '';
// 显示正在进行的状态
recognitionStatus.value = '正在聆听...';
// 执行一次性识别 - 这是同步等待结果的操作
final result = await _asrService.recognizeOnce();
if (result.isError) {
recognitionStatus.value = '识别错误: ${result.error}';
Logger.error('一次性识别错误: ${result.error}');
} else {
// 更新最终结果
finalResult.value = result.text;
// 更新语言信息
if (result.detectedLanguage.isNotEmpty) {
detectedLanguage.value = result.detectedLanguage;
Logger.debug('检测到语言: ${result.detectedLanguage}');
}
recognitionStatus.value = '识别完成';
}
} catch (e) {
@ -329,26 +329,26 @@ class SpeechTestController extends GetxController {
isProcessing.value = false;
}
}
/// 开始连续识别
Future<void> startContinuousRecognition() async {
if (!isInitialized.value || isProcessing.value || isListening.value) return;
try {
isProcessing.value = true;
// 清空之前的结果
intermediateResult.value = '';
finalResult.value = '';
detectedLanguage.value = '';
recognitionStatus.value = '准备连续识别...';
// 获取识别事件流 - 这启动了异步识别过程
final stream = await _asrService.startContinuousRecognition();
final stream =
await _asrService.startContinuousRecognition(_audioSourceType);
// 设置事件监听 - 异步处理识别结果
_setupAsrEventListener(stream);
} catch (e) {
recognitionStatus.value = '启动连续识别异常: $e';
Logger.error('启动连续识别异常: $e');
@ -359,19 +359,19 @@ class SpeechTestController extends GetxController {
isProcessing.value = false;
}
}
/// 停止连续识别
Future<void> stopContinuousRecognition() async {
if (!isInitialized.value || !isListening.value) return;
try {
// 首先更新状态,防止重复调用
isListening.value = false;
recognitionStatus.value = '正在停止识别...';
// 停止识别过程
await _asrService.stopContinuousRecognition();
// 更新UI状态
recognitionStatus.value = '连续识别已停止';
intermediateResult.value = '';
@ -380,16 +380,16 @@ class SpeechTestController extends GetxController {
Logger.error('停止连续识别异常: $e');
}
}
// 获取识别结果(用于UI显示)
String getDisplayResult() {
// 如果有最终结果,优先显示
String displayText = '';
if (finalResult.value.isNotEmpty) {
displayText = finalResult.value;
}
// 如果有中间结果,添加到最终结果后(可在UI中使用不同样式显示)
if (intermediateResult.value.isNotEmpty) {
if (displayText.isNotEmpty) {
@ -398,19 +398,19 @@ class SpeechTestController extends GetxController {
displayText = '正在识别: ${intermediateResult.value}';
}
}
return displayText;
}
@override
void onClose() {
// 取消所有订阅
_recognitionSubscription?.cancel();
_ttsEventSubscription?.cancel();
// 释放控制器
textController.dispose();
super.onClose();
}
}
}

44
lib/modules/translation/controllers/translation_controller.dart

@ -9,28 +9,9 @@ import '../../../data/services/language_manager.dart';
import '../../../core/utils/logger.dart';
import '../../../data/models/events/voice_interaction_event.dart';
import '../../../data/services/asr_service.dart';
import 'package:jieli/jieli.dart';
import '../../../data/services/ble_manager.dart';
class TranslationController extends GetxController {
late Jieli jieli;
// 事件订阅
StreamSubscription? _scanSubscription;
StreamSubscription? _connectionSubscription;
StreamSubscription? _wakeupSubscription;
StreamSubscription? _audioSubscription;
StreamSubscription? _commandResultSubscription;
// 状态变量
var isScanning = false.obs;
var deviceInfo = '未获取'.obs;
var connectStatus = '未连接'.obs;
var currentDevice = Rx<BleDevice?>(null);
var scannedDevices = RxList<BleDevice>([]);
var wakeupReceived = false.obs;
var audioDataSize = 0.obs;
var receivedFrames = 0.obs;
var audioDataReceiving = false.obs;
var volumeCommandResult = ''.obs;
// 服务
final AsrService _asrService = Get.find<AsrService>();
final VolcanoTranslationService _translationService =
@ -38,7 +19,10 @@ class TranslationController extends GetxController {
final TtsService _ttsService = Get.find<TtsService>();
final LanguageManager _languageManager = Get.find<LanguageManager>();
final GetStorage _storage = GetStorage();
// 蓝牙服务
final _bleManager = Get.find<BleManager>();
// 音频输入源
bool _audioSourceType = false;
// 存储相关
static const String _historyKey = 'translation_history';
static const String _sourceLanguageKey = 'translation_source_language';
@ -104,7 +88,7 @@ class TranslationController extends GetxController {
@override
void onInit() {
super.onInit();
jieli = Jieli();
//_setupEventListeners();
// 加载上次保存的语言设置
_loadSavedLanguages();
@ -240,19 +224,23 @@ class TranslationController extends GetxController {
// // 添加到历史记录
// translationHistory.add(newItem);
// 开始连续语音识别
if (currentMode.value == "audioVideo") {
_bleManager.openDecoder();
// 发送ble音乐或者通话远端声音
_audioSourceType = true;
Logger.info('发送ble音乐或者通话远端声音');
} else if (currentMode.value == "call") {
// 发送ble系统mic和dac(音乐或者通话远端)声音
_bleManager.openA2DPDecoder();
_audioSourceType = true;
Logger.info('发送ble系统mic和dac(音乐或者通话远端)声音');
} else {
// 开始连续语音识别
_audioSourceType = false;
}
// 开始连续语音识别
final recognitionStream = await _asrService.startContinuousRecognition();
final recognitionStream =
await _asrService.startContinuousRecognition(_audioSourceType);
isRecognizing.value = true;
// 监听识别结果

14
lib/routes/app_pages.dart

@ -41,6 +41,8 @@ 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';
import '../modules/ota_upgrade/views/ota_upgrade_view.dart';
import '../modules/ota_upgrade/bindings/ota_upgrade_binding.dart';
abstract class AppPages {
static final pages = [
@ -91,6 +93,16 @@ abstract class AppPages {
transition: Transition.rightToLeft,
transitionDuration: Duration(milliseconds: 250),
),
GetPage(
name: Routes.recording,
page: () => const ChatView(),
binding: ChatBinding(),
),
GetPage(
name: Routes.OTA_UPGRADE,
page: () => const OtaUpgradeView(),
binding: OtaUpgradeBinding(),
),
GetPage(
name: Routes.profile,
page: () => const ProfileView(),
@ -182,7 +194,6 @@ abstract class AppPages {
transition: Transition.rightToLeft,
transitionDuration: Duration(milliseconds: 250),
),
GetPage(
name: Routes.speechTest,
page: () => const SpeechTestView(),
@ -190,7 +201,6 @@ abstract class AppPages {
transition: Transition.rightToLeft,
transitionDuration: Duration(milliseconds: 250),
),
GetPage(
name: Routes.agent,
page: () => const AgentView(),

2
lib/routes/app_routes.dart

@ -6,6 +6,8 @@ abstract class Routes {
static const home = '/home';
static const explore = '/explore';
static const chat = '/chat';
static const OTA_UPGRADE = '/ota_upgrade';
static const recording = '/recording';
static const profile = '/profile';
static const meeting = '/meeting';
static const meetingRecord = '/meetingRecord';

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

@ -48,5 +48,4 @@ dependencies {
add("compileOnly", project(":deepvoice_utils"))
add("compileOnly", project(":chat_storage"))
add("compileOnly", project(":ble_service"))
add("compileOnly", project(":music_service"))
}

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

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

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

@ -14,6 +14,8 @@ import kotlinx.coroutines.*
import java.util.Collections
import kotlin.coroutines.CoroutineContext
import com.yunqiinnovation.ble_service.BleService
import android.media.MediaPlayer
import com.yunqiinnovation.agent_service.R
/**
* 代理服务事件监听接口
*/
@ -69,6 +71,9 @@ object AgentService : CoroutineScope {
// 系统提示词
private var systemPrompt = ""
// 音频播放器
private var audioPlayer: AudioPlayer? = null // 初始化音频播放器
// 状态
private var isInitialized = false
var isRecognitionActive = false
@ -121,6 +126,8 @@ object AgentService : CoroutineScope {
this.context = appContext.applicationContext
this.openAIService = OpenAIService(context)
this.chatStorageHelper = ChatStorageHelper(context)
this.audioPlayer = AudioPlayer(context)
// 从配置中获取参数
azureSpeechKey = config["azureSpeechKey"] as? String ?: ""
@ -132,6 +139,7 @@ object AgentService : CoroutineScope {
// 自定义系统提示词
val customSystemPrompt = config["systemPrompt"] as? String
FileLogger.i(TAG, "初始化 系统提示词:$config")
if (!customSystemPrompt.isNullOrBlank()) {
systemPrompt = customSystemPrompt
} else {
@ -289,11 +297,11 @@ object AgentService : CoroutineScope {
// 检查录音权限
// if (!isExternalActive||!checkRecordAudioPermission()) {
// FileLogger.e(TAG, "需要录音权限,请在设置中授予权限")
// sendEvent("error", mapOf("code" to "PERMISSION_DENIED", "message" to "需要录音权限"))
// return false
// }
if (!isExternalActive&&!checkRecordAudioPermission()) {
FileLogger.e(TAG, "需要录音权限,请在设置中授予权限")
sendEvent("error", mapOf("code" to "PERMISSION_DENIED", "message" to "需要录音权限"))
return false
}
isRecognitionActive = true
hasSpeechDetected = false
@ -354,12 +362,14 @@ object AgentService : CoroutineScope {
sendEvent("recognition_started", mapOf("status" to "started"))
// 启动空闲检测
startIdleCheck()
audioPlayer?.playAudio(R.raw.start)
}
override fun onSessionStopped() {
isRecognitionActive = false
stopIdleCheck()
sendEvent("recognition_stopped", mapOf("status" to "stopped"))
audioPlayer?.playAudio(R.raw.stop)
}
override fun onCanceled(reason: String, errorDetails: String) {
@ -439,24 +449,25 @@ object AgentService : CoroutineScope {
*/
private fun stopAiStream() {
if (isAiStreaming) {
// 取消当前AI生成任务
currentAiJob?.cancel()
currentAiJob = null
// 通知OpenAI服务终止当前流式请求
openAIService.cancelCurrentStream()
// 更新状态
isAiStreaming = false
// 记录日志
FileLogger.d(TAG, "AI流输出已停止")
// // 向客户端发送取消事件,表明AI回复已被终止
// sendEvent("assistant_stream_canceled", mapOf(
// "status" to "canceled",
// "timestamp" to System.currentTimeMillis()
// ))
try {
// 先更新状态,避免回调时的状态不一致
isAiStreaming = false
// 取消当前AI生成任务
currentAiJob?.cancel()
currentAiJob = null
// 通知OpenAI服务终止当前流式请求
openAIService.cancelCurrentStream()
// 记录日志
FileLogger.d(TAG, "AI流输出已停止")
} catch (e: Exception) {
FileLogger.e(TAG, "停止AI流输出异常", e)
// 确保状态被重置,即使发生异常
isAiStreaming = false
currentAiJob = null
}
}
}
@ -559,6 +570,7 @@ object AgentService : CoroutineScope {
// 使用历史记录作为上下文发送到OpenAI
val responseBuilder = StringBuilder()
var metadata:String = ""
// 添加系统提示词到历史消息的副本中
val messagesWithSystemPrompt = JSONArray()
@ -637,7 +649,7 @@ object AgentService : CoroutineScope {
addToHistoryMessages(openAIService.createAssistantMessage(response))
// 保存聊天记录
saveChatMessage(displayText, response)
saveChatMessage(displayText, response,metadata)
}
// 标记AI流式输出已完成
@ -658,12 +670,17 @@ object AgentService : CoroutineScope {
}
override fun onFunctionCall(call: JSONObject) {
sendEvent("function_call", mapOf(
"name" to call.getString("name"),
"arguments" to call.toString()
))
if(call.getString("name") == "exit_interaction") {
stopRecognition()
try {
val name = call.getString("name")
sendEvent("function_call", mapOf(
"name" to name,
"arguments" to call.toString()
))
if(name == "exit_interaction") {
stopRecognition()
}
} catch (e: Exception) {
FileLogger.e(TAG, "处理函数调用事件失败", e)
}
}
@ -673,19 +690,6 @@ object AgentService : CoroutineScope {
"result" to functionCallResult.toString(),
))
FileLogger.d(TAG, "mcp调用结果: $functionCallResult")
val metaStr = functionCallResult.optString("meta")
if (metaStr.isNotEmpty()) {
val meta = JSONObject(metaStr)
FileLogger.d(TAG, "mcp调用结果: $metaStr")
if (meta.has("card_music")) {
val cardMusic = meta.getJSONObject("card_music")
FileLogger.d(TAG, "检查到音乐卡片: $cardMusic")
val musicUrl = cardMusic.optString("url", "")
val name = cardMusic.optString("name", "")
val sgener = cardMusic.optString("sgener", "")
processMusicPlay(musicUrl, name, sgener)
}
}
}
}
)
@ -784,7 +788,7 @@ object AgentService : CoroutineScope {
/**
* 保存聊天消息
*/
private fun saveChatMessage(userMessage: String, assistantMessage: String) {
private fun saveChatMessage(userMessage: String, assistantMessage: String,metadata : String) {
launch(Dispatchers.IO) {
try {
// 保存用户消息
@ -799,7 +803,8 @@ object AgentService : CoroutineScope {
val assistantMessageId = chatStorageHelper.saveMessage(
sessionId = sessionId,
message = assistantMessage,
sender = "assistant"
sender = "assistant",
metadata = metadata
)
// if (assistantMessageId != -1L) {
@ -864,25 +869,43 @@ object AgentService : CoroutineScope {
}
/**
* 释放资源
* 释放所有资源
*/
fun dispose() {
stopRecognition()
stopTts()
stopAiStream()
stopIdleCheck()
azureAsrHelper?.dispose()
azureTtsHelper?.dispose()
job.cancel()
// 清除所有监听器
clearListeners()
// 重置单例状态,以便下次使用时可以重新初始化
isInitialized = false
FileLogger.d(TAG, "代理服务资源已释放")
try {
// 停止语音识别
stopRecognition()
// 停止TTS播放
stopTts()
// 停止AI流输出
stopAiStream()
// 停止空闲检测
stopIdleCheck()
// 释放音频播放器资源
audioPlayer?.release()
audioPlayer = null
// 释放Azure服务
azureAsrHelper?.dispose()
azureTtsHelper?.dispose()
// 取消所有协程
job.cancel()
// 清除监听器
clearListeners()
// 标记为未初始化
isInitialized = false
FileLogger.d(TAG, "代理服务资源已释放")
} catch (e: Exception) {
FileLogger.e(TAG, "释放资源异常", e)
}
}
/**
@ -964,17 +987,57 @@ object AgentService : CoroutineScope {
}
/**
* 处理音乐播放
* 将ai返回的音乐连接 在后台启动播放
*
* @param musicUrl 图片文件路径
* @param name 歌曲名称,默认为空
* @param singer 歌手,默认为false
* @return 是否成功开始处理
* 简单音频播放器,用于播放提示音等
*/
fun processMusicPlay(musicUrl:String,name: String = "",singer:String = ""){
// 在其他 Service、BroadcastReceiver 或 Application 中调用
FileLogger.i(TAG, "启动音乐服务 播放音乐 $musicUrl")
MusicServiceStarter.startServiceWithCommand(context, "play", url = musicUrl)
class AudioPlayer(private val context: Context) {
private var mediaPlayer: MediaPlayer? = null
/**
* 播放音频资源
* @param resId 资源ID
*/
fun playAudio(resId: Int) {
try {
// 释放之前的资源
release()
// 创建播放器并设置资源
mediaPlayer = MediaPlayer().apply {
// 设置资源
context.resources.openRawResourceFd(resId)?.use { fd ->
setDataSource(fd.fileDescriptor, fd.startOffset, fd.length)
}
// 播放完成后自动释放资源
setOnCompletionListener {
release()
}
// 准备并播放
prepare()
start()
}
FileLogger.d(TAG, "开始播放音频资源")
} catch (e: Exception) {
FileLogger.e(TAG, "播放音频资源异常: ${e.message}", e)
release()
}
}
/**
* 释放资源
*/
fun release() {
try {
mediaPlayer?.apply {
if (isPlaying) stop()
release()
}
mediaPlayer = null
} catch (e: Exception) {
// 忽略异常
}
}
}
}

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

@ -57,6 +57,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream
config["openaiApiKey"]?.toString()?.let { intent.putExtra("openaiApiKey", it) }
config["openaiBaseUrl"]?.toString()?.let { intent.putExtra("openaiBaseUrl", it) }
config["openaiModel"]?.toString()?.let { intent.putExtra("openaiModel", it) }
config["systemPrompt"]?.toString()?.let { intent.putExtra("systemPrompt", it) }
config["mcpServer"]?.toString()?.let { intent.putExtra("mcpServer", it) }
// 启动前台服务

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

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

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

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

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

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

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

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

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

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

@ -172,7 +172,7 @@ class AzureSpeechPlugin: FlutterPlugin, CoroutineScope {
}
}
"startContinuousRecognition" -> {
val useExternalAudio = call.argument<Boolean>("useExternalAudio") ?: false
val useExternalAudio = call.argument<Boolean>("audioSourceType") ?: false
// 确保事件通道已准备好
if (asrEventSink == null) {
result.error("EVENT_CHANNEL_NOT_READY", "事件通道未准备好,无法开始连续识别", null)
@ -187,6 +187,7 @@ class AzureSpeechPlugin: FlutterPlugin, CoroutineScope {
} else {
AzureAsrHelper.AudioSourceType.MICROPHONE
}
val success = azureAsrHelper.startContinuousRecognition(object : AzureAsrHelper.ContinuousRecognizeCallback {
override fun onResult(text: String, detectedLanguage: String) {
sendAsrEvent(mapOf(
@ -261,7 +262,7 @@ class AzureSpeechPlugin: FlutterPlugin, CoroutineScope {
"initialize" -> {
val subscriptionKey = call.argument<String>("subscriptionKey") ?: ""
val region = call.argument<String>("region") ?: ""
val useCustomAudioOutput = call.argument<Boolean>("useCustomAudioOutput") ?: true // 默认使用自定义播放器
val useCustomAudioOutput = call.argument<Boolean>("useCustomAudioOutput") ?: false // 默认使用自定义播放器
// 先释放现有资源
azureTtsHelper.dispose()

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

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

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

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

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

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

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

@ -29,6 +29,7 @@ import com.jieli.jl_audio_decode.callback.OnDecodeStreamCallback
import com.jieli.jl_audio_decode.callback.OnStateCallback
import com.jieli.jl_audio_decode.opus.OpusManager
import com.jieli.jl_audio_decode.exceptions.OpusException
import com.yunqiinnovation.ble_service.BleCompanionManager
/**
* BLE服务类:提供蓝牙低功耗设备的扫描、连接和通信功能
@ -317,6 +318,33 @@ object BleService {
// ======================================================================================================
// 设备连接与通信
// ======================================================================================================
/**
* 自动连接已配对的BLE设备
* 如果没有找到已配对的MAC地址,将不会进行连接
*/
fun connect() {
try {
// 创建 BleCompanionManager 实例
val companionManager = BleCompanionManager(context)
// 获取已配对的 MAC 地址
val pairedMac = companionManager.getPairedMacAddress()
if (pairedMac.isNullOrEmpty()) {
FileLogger.w(TAG, "未找到已配对的设备 MAC 地址")
return
}
FileLogger.i(TAG, "检测到已配对设备,MAC: $pairedMac,尝试连接")
// 调用现有的 connect 方法
connect(pairedMac)
} catch (e: Exception) {
FileLogger.e(TAG, "自动连接已配对设备失败: ${e.message}", e)
updateConnectionState(BleConst.STATE_ERROR)
}
}
/**
* 连接到指定地址的BLE设备
* @param address 设备MAC地址
@ -999,8 +1027,45 @@ object BleService {
* 控制编解码 - 打开解码
*/
fun openDecoder(): Boolean {
FileLogger.i(TAG, "打开编码 (设备侧)...")
val opusStarted = startOpusStreamDecoding(
hasHeader = false, // BLE传输的Opus数据通常无文件头
channel = 1, // 单声道
sampleRate = 16000,// Opus推荐16kHz或更高
packetSize = 40 // Opus常用帧长,如20ms, 40ms, 60ms。 (16000Hz * 1channel * 2bytes/sample * 0.04s = 1280 bytes/frame if PCM)
// packetSize for Opus is typically in ms, e.g. 20, 40, 60.
// The library might expect this in a different unit or derive it.
// Given JlOpusPlugin.kt, packetSize refers to Opus encoded frame duration in ms.
)
FileLogger.i(TAG, "打开解码...")
return sendCommand(BleConst.CMD_CONTROL_CODEC.toByte(), byteArrayOf(BleConst.CODEC_CONTROL_DECODE_ON.toByte(),
BleConst.AUDIO_CHANNEL_RIGHT.toByte()))
}
/**
* 控制编解码 - 打开解码
*/
fun openA2DPDecoder(): Boolean {
FileLogger.i(TAG, "打开编码 (设备侧)...")
val opusStarted = startOpusStreamDecoding(
hasHeader = false, // BLE传输的Opus数据通常无文件头
channel = 1, // 单声道
sampleRate = 16000,// Opus推荐16kHz或更高
packetSize = 40 // Opus常用帧长,如20ms, 40ms, 60ms。 (16000Hz * 1channel * 2bytes/sample * 0.04s = 1280 bytes/frame if PCM)
// packetSize for Opus is typically in ms, e.g. 20, 40, 60.
// The library might expect this in a different unit or derive it.
// Given JlOpusPlugin.kt, packetSize refers to Opus encoded frame duration in ms.
)
FileLogger.i(TAG, "打开解码...")
return sendCommand(BleConst.CMD_CONTROL_CODEC.toByte(), byteArrayOf(BleConst.CODEC_CONTROL_DECODE_ON.toByte()))
return sendCommand(BleConst.CMD_CONTROL_CODEC.toByte(), byteArrayOf(BleConst.CODEC_CONTROL_A2DP_PLAY.toByte(),
BleConst.AUDIO_CHANNEL_RIGHT.toByte()))
}
/**

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

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

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

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

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

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

179
local_plugins/ble_service/lib/ble_service.dart

@ -15,32 +15,35 @@ class BleConnectionState {
/// BLE服务插件
class BleService {
static final BleService _instance = BleService._internal();
/// 获取单例
factory BleService() => _instance;
// 方法通道
final MethodChannel _methodChannel = const MethodChannel('com.yunqiinnovation.ble_service');
final MethodChannel _methodChannel =
const MethodChannel('com.yunqiinnovation.ble_service');
// 事件通道 - 简化为只使用数据和状态通道
late final EventChannel _dataEventChannel;
late final EventChannel _statusEventChannel;
// 事件流
late final Stream<dynamic> _dataStream;
late final Stream<dynamic> _statusStream;
// 获取事件流
Stream<dynamic> get onDataReceived => _dataStream;
// 状态流直接传递原始数据
Stream<dynamic> get onStatusUpdated => _statusStream;
// 内部构造函数
BleService._internal() {
_dataEventChannel = const EventChannel('com.yunqiinnovation.ble_service/data_events');
_statusEventChannel = const EventChannel('com.yunqiinnovation.ble_service/status_events');
_dataEventChannel =
const EventChannel('com.yunqiinnovation.ble_service/data_events');
_statusEventChannel =
const EventChannel('com.yunqiinnovation.ble_service/status_events');
// 初始化事件流
_dataStream = _dataEventChannel.receiveBroadcastStream();
_statusStream = _statusEventChannel.receiveBroadcastStream();
@ -56,7 +59,7 @@ class BleService {
return false;
}
}
/// 获取电池电量
Future<int> getBatteryLevel() async {
try {
@ -67,18 +70,19 @@ class BleService {
return 0;
}
}
/// 获取设备版本信息
Future<bool> getDeviceVersion() async {
try {
final result = await _methodChannel.invokeMethod<bool>('getDeviceVersion');
final result =
await _methodChannel.invokeMethod<bool>('getDeviceVersion');
return result ?? false;
} catch (e) {
print('获取设备版本失败: $e');
return false;
}
}
/// 获取电池信息
Future<bool> getBatteryInfo() async {
try {
@ -89,7 +93,7 @@ class BleService {
return false;
}
}
/// 打开编码器
Future<bool> openEncoder() async {
try {
@ -100,7 +104,7 @@ class BleService {
return false;
}
}
/// 打开解码器
Future<bool> openDecoder() async {
try {
@ -111,7 +115,18 @@ class BleService {
return false;
}
}
/// 打开解码器
Future<bool> openA2DPDecoder() async {
try {
final result = await _methodChannel.invokeMethod<bool>('openA2DPDecoder');
return result ?? false;
} catch (e) {
print('打开解码器失败: $e');
return false;
}
}
/// 关闭编解码器
Future<bool> closeCodec() async {
try {
@ -122,7 +137,7 @@ class BleService {
return false;
}
}
/// 播放A2DP
Future<bool> playA2DP() async {
try {
@ -133,7 +148,7 @@ class BleService {
return false;
}
}
/// 启动BLE后台服务
/// [config] 配置参数,可包含:
/// - scan_interval: 扫描间隔时间(毫秒)
@ -144,14 +159,15 @@ class BleService {
/// - device_name: 设备名称
Future<bool> startBleService(Map<String, dynamic> config) async {
try {
final result = await _methodChannel.invokeMethod<bool>('startBleService', config);
final result =
await _methodChannel.invokeMethod<bool>('startBleService', config);
return result ?? false;
} catch (e) {
print('启动BLE后台服务失败: $e');
return false;
}
}
/// 停止BLE后台服务
Future<bool> stopBleService() async {
try {
@ -163,26 +179,93 @@ class BleService {
}
}
/// 注册BLE后台扫描
//========================================================================
// 以下是Companion Device相关功能
//========================================================================
/// 检查设备是否支持Companion Device Manager
///
/// 返回设备是否支持Companion Device Manager API
/// 仅在Android 8.0及以上版本可用
Future<bool> isCompanionSupported() async {
try {
final result = await _methodChannel.invokeMethod<bool>('isCompanionSupported');
return result ?? false;
} catch (e) {
print('检查Companion设备支持失败: $e');
return false;
}
}
/// 启动设备关联流程
///
/// 弹出系统设备选择对话框,用户选择后将建立持久化关联
/// 关联成功后,应用将获得后台扫描和启动前台服务的特权
///
/// [deviceNamePattern] 设备名称正则表达式,例如"MyHeadset.*"
/// [serviceUuid] 可选的服务UUID
/// [macAddress] 可选的MAC地址
///
/// 返回是否成功启动关联流程
Future<bool> startDeviceAssociation({
String? deviceNamePattern,
String? serviceUuid,
String? macAddress,
}) async {
try {
final result = await _methodChannel.invokeMethod<bool>(
'startDeviceAssociation',
{
'deviceNamePattern': deviceNamePattern,
'serviceUuid': serviceUuid,
'macAddress': macAddress,
},
);
return result ?? false;
} catch (e) {
print('启动设备关联失败: $e');
return false;
}
}
/// 获取已关联的设备MAC地址
///
/// 返回已关联的设备MAC地址,如果没有则返回null
Future<String?> getPairedMacAddress() async {
try {
final result = await _methodChannel.invokeMethod<String>('getPairedMacAddress');
return result;
} catch (e) {
print('获取配对MAC地址失败: $e');
return null;
}
}
/// 注册BLE后台扫描
///
/// 注册后,即使应用被杀死,系统也会在检测到设备广播时唤醒应用
/// 这是确保设备可以在应用后台时被自动连接的关键功能
Future<bool> registerBackgroundScan() async {
///
/// [macAddress] 可选的设备MAC地址,如果不提供则使用已关联的设备
Future<bool> registerBackgroundScan({String? macAddress}) async {
try {
final result = await _methodChannel.invokeMethod<bool>('registerBackgroundScan');
final result =
await _methodChannel.invokeMethod<bool>('registerBackgroundScan');
return result ?? false;
} catch (e) {
print('注册后台扫描失败: $e');
return false;
}
}
/// 注销BLE后台扫描
///
///
/// 当不再需要后台扫描功能时调用,例如用户主动断开连接时
Future<bool> unregisterBackgroundScan() async {
try {
final result = await _methodChannel.invokeMethod<bool>('unregisterBackgroundScan');
final result =
await _methodChannel.invokeMethod<bool>('unregisterBackgroundScan');
return result ?? false;
} catch (e) {
print('注销后台扫描失败: $e');
@ -190,12 +273,25 @@ class BleService {
}
}
/// 清除所有设备关联
///
/// 用于重置设备关联状态,通常在用户主动解除关联或调试时使用
Future<bool> clearAssociations() async {
try {
final result = await _methodChannel.invokeMethod<bool>('clearAssociations');
return result ?? false;
} catch (e) {
print('清除设备关联失败: $e');
return false;
}
}
//========================================================================
// 以下是ChatAgent相关功能
//========================================================================
/// 初始化语音交互代理
///
///
/// [config] 配置参数,必须包含:
/// - azureSpeechKey: Azure语音服务API密钥
/// - azureSpeechRegion: Azure语音服务区域
@ -204,16 +300,17 @@ class BleService {
/// - openaiModel: OpenAI模型名称,默认为'gpt-3.5-turbo'
Future<bool> initializeChatAgent(Map<String, dynamic> config) async {
try {
final result = await _methodChannel.invokeMethod<bool>('initializeChatAgent', config);
final result = await _methodChannel.invokeMethod<bool>(
'initializeChatAgent', config);
return result ?? false;
} catch (e) {
print('初始化语音交互代理失败: $e');
return false;
}
}
/// 启动语音识别
///
///
/// 用于手动启动语音识别,通常不需要直接调用,因为在唤醒时会自动启动
Future<bool> startListening() async {
try {
@ -224,7 +321,7 @@ class BleService {
return false;
}
}
/// 停止语音识别
Future<bool> stopListening() async {
try {
@ -235,14 +332,16 @@ class BleService {
return false;
}
}
/// 处理文本输入
///
///
/// [text] 用户输入的文本
/// [speakResponse] 是否使用TTS朗读回复,默认为false
Future<bool> processTextInput(String text, {bool speakResponse = false}) async {
Future<bool> processTextInput(String text,
{bool speakResponse = false}) async {
try {
final result = await _methodChannel.invokeMethod<bool>('processTextInput', {
final result =
await _methodChannel.invokeMethod<bool>('processTextInput', {
'text': text,
'speakResponse': speakResponse,
});
@ -252,7 +351,7 @@ class BleService {
return false;
}
}
/// 停止TTS播放
Future<bool> stopTts() async {
try {
@ -263,4 +362,4 @@ class BleService {
return false;
}
}
}
}

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

@ -20,13 +20,18 @@ class MusicService : Service() {
private const val NOTIFICATION_ID = 1002
private val isRunning = AtomicBoolean(false)
private var currentStatus: Boolean = false
private var currentUrlInternal: String? = null
fun isServiceRunning(): Boolean = isRunning.get()
fun isPlaying(): Boolean = currentStatus
fun getCurrentUrl(): String? = currentUrlInternal
}
private var mediaPlayer: MediaPlayer? = null
private val playlist = mutableListOf<String>() // 可选:初始化为预置歌曲
private var currentIndex = 0
private var isPlaying = false
override fun onCreate() {
super.onCreate()
@ -40,7 +45,7 @@ class MusicService : Service() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val command = intent?.getStringExtra("command") ?: return START_NOT_STICKY
Log.i(TAG, "onStartCommand: $command")
Log.i(TAG, "onStartCommand: $command" )
when (command) {
"play" -> {
@ -49,6 +54,18 @@ class MusicService : Service() {
currentIndex = playlist.indexOf(url)
play(url)
}
"playlist" -> {
val urls = intent.getStringArrayListExtra("urls")?.filter { it.isNotEmpty() } ?: return START_NOT_STICKY
Log.i(TAG, "后台服务播放 playlist: $urls" )
if (urls.isNotEmpty()) {
playlist.clear()
playlist.addAll(urls)
currentIndex = 0
play(playlist[currentIndex])
} else {
Log.w(TAG, "playlist 命令收到空列表")
}
}
"pause" -> pause()
"resume" -> resume()
"stop" -> stopSelf()
@ -76,24 +93,31 @@ class MusicService : Service() {
private fun play(url: String) {
Log.i(TAG, "播放音乐: $url")
releaseMediaPlayer()
currentStatus = true;
currentUrlInternal = url;
mediaPlayer = MediaPlayer().apply {
setDataSource(url)
setAudioStreamType(AudioManager.STREAM_MUSIC)
setOnPreparedListener {
start()
this@MusicService.isPlaying = true
currentStatus = true
updateNotification("正在播放")
Log.i(TAG, "开始播放")
}
setOnCompletionListener {
Log.i(TAG, "播放完成")
this@MusicService.isPlaying = false
stopSelf()
// 播放下一首
if (currentIndex + 1 < playlist.size) {
currentIndex++
play(playlist[currentIndex]) // 播放下一首
} else {
currentStatus = false
stopSelf() // 如果没有更多歌曲了,可以停止服务
}
}
setOnErrorListener { _, what, extra ->
Log.e(TAG, "播放错误: what=$what extra=$extra")
this@MusicService.isPlaying = false
currentStatus = false
stopSelf()
true
}
@ -105,7 +129,7 @@ class MusicService : Service() {
mediaPlayer?.let {
if (it.isPlaying) {
it.pause()
this@MusicService.isPlaying = false
currentStatus= false
updateNotification("已暂停")
Log.i(TAG, "暂停播放")
}
@ -116,7 +140,7 @@ class MusicService : Service() {
mediaPlayer?.let {
if (!it.isPlaying) {
it.start()
this@MusicService.isPlaying = true
currentStatus = true
updateNotification("继续播放")
Log.i(TAG, "继续播放")
}

15
local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicServicePlugin.kt

@ -18,7 +18,7 @@ class MusicServicePlugin: FlutterPlugin, MethodCallHandler {
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
context = flutterPluginBinding.applicationContext
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "music_service")
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "com.yunqiinnovation.music_service")
channel.setMethodCallHandler(this)
}
@ -30,6 +30,10 @@ class MusicServicePlugin: FlutterPlugin, MethodCallHandler {
val url = call.argument<String>("url")
MusicServiceStarter.startServiceWithCommand(context, "play", url)
}
"playlist" -> {
val urls = call.argument<List<String>>("urls")
MusicServiceStarter.startServiceWithPlaylist(context, "playlist", urls)
}
"pause" -> MusicServiceStarter.startServiceWithCommand(context, "pause")
"resume" -> MusicServiceStarter.startServiceWithCommand(context, "resume")
"stop" -> MusicServiceStarter.startServiceWithCommand(context, "stop")
@ -37,6 +41,15 @@ class MusicServicePlugin: FlutterPlugin, MethodCallHandler {
val volume = call.argument<Double>("volume")?.toFloat()
MusicServiceStarter.startServiceWithCommand(context, "setVolume", volume = volume)
}
"getStatus" -> {
val isPlaying = MusicService.isPlaying()
val currentUrl = MusicService.getCurrentUrl()
val status = mapOf(
"isPlaying" to isPlaying,
"url" to currentUrl
)
result.success(status)
}
else -> result.notImplemented()
}
result.success(null)

11
local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicServiceStarter.kt

@ -7,6 +7,7 @@ import androidx.core.content.ContextCompat
object MusicServiceStarter {
private val TAG = "MusicServiceStarter"
//播放单曲
fun startServiceWithCommand(context: Context, command: String, url: String? = null, volume: Float? = null) {
Log.i(TAG, "startServiceWithCommand: $command $url")
val intent = Intent(context, MusicService::class.java).apply {
@ -16,4 +17,14 @@ object MusicServiceStarter {
}
ContextCompat.startForegroundService(context, intent)
}
//启动播放列表
fun startServiceWithPlaylist(context: Context,command: String, urls: List<String>?,volume: Float? = null) {
Log.i(TAG, "startServiceWithPlaylist: $urls")
val intent = Intent(context, MusicService::class.java).apply {
putExtra("command", command)
putStringArrayListExtra("urls", ArrayList(urls!!)) // 注意类型是 ArrayList<String>
}
ContextCompat.startForegroundService(context, intent)
}
}

7
local_plugins/music_service/lib/music_service.dart

@ -52,4 +52,11 @@ class MusicService {
'setVolume',
{'volume': volume},
); // volume: 0.0~1.0
Future<Map<String, dynamic>> getStatus() async {
final result = await _methodChannel.invokeMethod<Map>('getStatus');
return {
'isPlaying': result?['isPlaying'] ?? false,
'url': result?['url'] ?? "",
};
}
}

2
local_plugins/music_service/pubspec.yaml

@ -4,7 +4,7 @@ version: 0.0.1
homepage:
environment:
sdk: ^3.7.2
sdk: '>=3.7.0 <4.0.0'
flutter: '>=3.3.0'
dependencies:

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

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

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

Binary file not shown.

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

Binary file not shown.

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

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

Loading…
Cancel
Save