wolfplus 1 year ago
parent
commit
e91609fe4c
  1. 249
      lib/modules/agent/controllers/agent_controller.dart
  2. 258
      lib/modules/agent/views/agent_view.dart
  3. 4
      lib/modules/agent/views/message_bubble.dart
  4. 2
      lib/modules/pairing/controllers/pairing_controller.dart
  5. 3
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt
  6. 6
      local_plugins/music_service/android/src/main/AndroidManifest.xml
  7. 2
      local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt
  8. 1
      local_plugins/open_ai_service/android/build.gradle.kts
  9. 4
      local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/OpenAIService.kt
  10. 33
      local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/SystemFunctionHandler.kt

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

@ -8,6 +8,9 @@ import 'package:agent_service/agent_service.dart';
import 'package:logger/logger.dart';
import 'package:chat_storage/chat_storage.dart';
import 'package:image_picker/image_picker.dart';
import 'package:get_storage/get_storage.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:app_settings/app_settings.dart';
class Message {
final bool isUser;
@ -51,6 +54,8 @@ class AgentController extends GetxController {
final isListening = false.obs;
final isSpeaking = false.obs;
final isProcessing = false.obs;
// TTS启用状态
final isTtsEnabled = true.obs;
// 图片处理状态
final isImageProcessing = false.obs;
@ -81,6 +86,7 @@ class AgentController extends GetxController {
@override
void onInit() {
super.onInit();
_loadTtsPreference();
_loadChatHistory();
_subscribeToEvents();
}
@ -95,6 +101,36 @@ class AgentController extends GetxController {
super.onClose();
}
// 加载TTS偏好设置
Future<void> _loadTtsPreference() async {
try {
final storage = GetStorage();
isTtsEnabled.value = storage.read('agent_tts_enabled') ?? true;
} catch (e) {
// 如果读取失败,使用默认值
logger.e('加载TTS偏好设置失败: $e');
isTtsEnabled.value = true;
}
}
// 切换TTS状态
Future<void> toggleTts() async {
isTtsEnabled.toggle();
// 如果关闭TTS,停止当前语音
if (!isTtsEnabled.value && isSpeaking.value) {
await AgentService.stopTts();
}
// 保存偏好设置
try {
final storage = GetStorage();
await storage.write('agent_tts_enabled', isTtsEnabled.value);
} catch (e) {
logger.e('保存TTS偏好设置失败: $e');
}
}
// 加载聊天历史记录
Future<void> _loadChatHistory() async {
try {
@ -134,12 +170,8 @@ class AgentController extends GetxController {
_addWelcomeMessage();
} finally {
isHistoryLoaded.value = true;
messages.refresh();
// 在加载完成后直接定位到底部,无需动画
WidgetsBinding.instance.addPostFrameCallback((_) {
scrollController.jumpTo(scrollController.position.maxScrollExtent);
});
// 使用reverse:true时不需要手动滚动到底部
}
}
@ -199,7 +231,7 @@ class AgentController extends GetxController {
));
}
messages.refresh();
_scrollToBottom();
// 使用reverse:true时通常不需要手动滚动
}
break;
@ -229,8 +261,7 @@ class AgentController extends GetxController {
isProcessing.value = true;
// 标记为新的AI回复
_isNewAssistantResponse = true;
// 滚动到底部
_scrollToBottom();
// 使用reverse:true时通常不需要手动滚动
} else if (index >= 0) {
// 如果识别结果为空,移除临时的识别消息
messages.removeAt(index);
@ -282,8 +313,7 @@ class AgentController extends GetxController {
);
}
}
// 滚动到底部
_scrollToBottom();
// 使用reverse:true时通常不需要手动滚动
}
break;
@ -334,8 +364,7 @@ class AgentController extends GetxController {
// 发送此消息到AI处理
isProcessing.value = true;
_isNewAssistantResponse = true;
AgentService.processTextInput(messages[index].text,
speakResponse: true);
_handleAutoStop(index);
}
}
break;
@ -351,33 +380,39 @@ class AgentController extends GetxController {
final result = event.data['result'] ?? '';
final functionCall = event.data['function_call'] ?? '';
logger.i('Flutter 结束调用 mcp: $result, $functionCall');
if (result.isNotEmpty) {
final parsedResult = jsonDecode(result);
final meta = parsedResult["meta"] ?? "";
final metaResult = jsonDecode(meta);
logger.i('Flutter 结束调用 mcp metaResult: $metaResult');
// 判断是否为新的回复或响应ID是否改变
if (_isNewAssistantResponse || _currentResponseId != responseId) {
// 添加新消息
messages.add(Message(isUser: false, text: "", meta: metaResult));
_isNewAssistantResponse = false;
_currentResponseId = responseId;
} else {
// 更新最后一条消息
final lastIndex = messages.length - 1;
if (lastIndex >= 0 && !messages[lastIndex].isUser) {
messages[lastIndex] = Message(
isUser: false,
text: messages[lastIndex].text,
meta: meta,
timestamp: messages[lastIndex].timestamp,
);
final meta = parsedResult["meta"];
Map<String, dynamic> metaResult = {};
if (meta != null && meta is String && meta.isNotEmpty) {
try {
metaResult = jsonDecode(meta);
} catch (e) {
print('解析meta数据失败: $e');
}
}
if(metaResult.isNotEmpty){
// 判断是否为新的回复或响应ID是否改变
if (_isNewAssistantResponse || _currentResponseId != responseId) {
// 添加新消息
messages.add(Message(isUser: false, text: "", meta: metaResult));
_isNewAssistantResponse = false;
_currentResponseId = responseId;
} else {
// 更新最后一条消息
final lastIndex = messages.length - 1;
if (lastIndex >= 0 && !messages[lastIndex].isUser) {
messages[lastIndex] = Message(
isUser: false,
text: messages[lastIndex].text,
meta: meta,
timestamp: messages[lastIndex].timestamp,
);
}
}
}
// 滚动到底部
_scrollToBottom();
}
break;
default:
@ -388,11 +423,13 @@ class AgentController extends GetxController {
// 滚动到底部
void _scrollToBottom() {
// 使用reverse:true时,此方法可大部分场景下不再需要
// 保留此方法但简化实现,仅用于特殊情况下的手动滚动
WidgetsBinding.instance.addPostFrameCallback((_) {
if (scrollController.hasClients) {
try {
scrollController.animateTo(
scrollController.position.maxScrollExtent,
0, // 使用reverse:true时,0表示列表底部
duration: Duration(milliseconds: 300),
curve: Curves.easeOut,
);
@ -423,14 +460,14 @@ class AgentController extends GetxController {
messages.add(message);
textController.clear();
// 滚动到底部
_scrollToBottom();
// 使用reverse:true时通常不需要手动滚动
try {
isProcessing.value = true;
// 标记为新的AI回复
_isNewAssistantResponse = true;
await AgentService.processTextInput(text, speakResponse: true);
// 根据TTS状态决定是否朗读
await AgentService.processTextInput(text, speakResponse: isTtsEnabled.value);
} catch (e) {
logger.e('发送文本消息失败: $e');
isProcessing.value = false;
@ -459,8 +496,7 @@ class AgentController extends GetxController {
// 清空文本输入
textController.clear();
// 滚动到底部
_scrollToBottom();
// 使用reverse:true时通常不需要手动滚动
try {
isProcessing.value = true;
@ -468,11 +504,11 @@ class AgentController extends GetxController {
// 标记为新的AI回复
_isNewAssistantResponse = true;
// 调用Agent Service处理图片
// 调用Agent Service处理图片,考虑TTS状态
await AgentService.processImageInput(
imagePath,
text: text ?? '',
speakResponse: true,
speakResponse: isTtsEnabled.value,
);
} catch (e) {
logger.e('处理图片失败: $e');
@ -481,10 +517,108 @@ class AgentController extends GetxController {
}
}
// 切换输入模式
void toggleInputMode() async {
// 如果当前在图片输入模式,先清除
if (isImageInputActive.value) {
clearImageInput();
return;
}
// 如果要切换到语音模式,需要先检查麦克风权限
if (isTextInputMode.value) {
final hasPermission = await _checkMicrophonePermission();
if (!hasPermission) {
return; // 权限未获取,保持文本模式
}
}
isTextInputMode.toggle();
// 切换到语音模式时,直接开始语音输入
if (!isTextInputMode.value) {
startVoiceInput();
}
// 切换到文本模式时,直接停止语音输入
else {
stopVoiceInput();
}
}
// 检查麦克风权限
Future<bool> _checkMicrophonePermission() async {
try {
PermissionStatus status = await Permission.microphone.status;
// 如果已授权,直接返回 true
if (status.isGranted) {
return true;
}
// 如果被永久拒绝,提示用户前往设置页面
if (status.isPermanentlyDenied) {
_showPermissionDialog(true);
return false;
}
// 如果之前拒绝但不是永久拒绝,或首次请求,尝试请求权限
if (status.isDenied) {
status = await Permission.microphone.request();
if (status.isGranted) {
return true;
} else {
_showPermissionDialog(false);
return false;
}
}
return false;
} catch (e) {
logger.e('检查麦克风权限失败: $e');
return false;
}
}
// 显示权限提示对话框
void _showPermissionDialog(bool isPermanent) {
Get.dialog(
AlertDialog(
title: const Text('需要麦克风权限'),
content: Text(isPermanent
? '语音功能需要麦克风权限。请前往设置页面手动开启权限。'
: '语音功能需要麦克风权限。请允许使用麦克风权限以继续使用语音功能。'),
actions: [
TextButton(
onPressed: () => Get.back(),
child: const Text('取消'),
),
TextButton(
onPressed: () {
Get.back();
if (isPermanent) {
AppSettings.openAppSettings();
} else {
Permission.microphone.request();
}
},
child: Text(isPermanent ? '前往设置' : '授权'),
),
],
),
);
}
// 开始语音输入
Future<void> startVoiceInput() async {
if (isListening.value) return;
// 检查麦克风权限
final hasPermission = await _checkMicrophonePermission();
if (!hasPermission) {
isTextInputMode.value = true; // 如果没有权限,切回文本模式
return;
}
try {
await AgentService.startConversation();
} catch (e) {
@ -521,26 +655,6 @@ class AgentController extends GetxController {
}
}
// 切换输入模式
void toggleInputMode() {
// 如果当前在图片输入模式,先清除
if (isImageInputActive.value) {
clearImageInput();
return;
}
isTextInputMode.toggle();
// 切换到语音模式时,直接开始语音输入
if (!isTextInputMode.value) {
startVoiceInput();
}
// 切换到文本模式时,直接停止语音输入
else {
stopVoiceInput();
}
}
// 从相册选择图片
Future<void> pickImage() async {
try {
@ -645,4 +759,13 @@ class AgentController extends GetxController {
_musiceManager.resume();
isPlaying.value = true;
}
// 修改自动停止处理,遵循TTS设置
void _handleAutoStop(int index) {
// 发送此消息到AI处理
isProcessing.value = true;
_isNewAssistantResponse = true;
AgentService.processTextInput(messages[index].text,
speakResponse: isTtsEnabled.value);
}
}

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

@ -10,42 +10,64 @@ class AgentView extends StatelessWidget {
@override
Widget build(BuildContext context) {
final controller = Get.find<AgentController>();
final theme = Theme.of(context);
final primaryColor = theme.primaryColor;
final backgroundColor = theme.scaffoldBackgroundColor;
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
return Scaffold(
appBar: AppBar(
elevation: 1,
backgroundColor: backgroundColor,
leading: IconButton(
icon: Icon(Icons.arrow_back_ios,
color: isDarkMode ? Colors.white : Colors.black87,
size: 20),
onPressed: () => Get.back(),
),
titleSpacing: 0,
title: Row(
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: Theme.of(context).primaryColor,
color: primaryColor,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 3,
offset: const Offset(0, 1),
),
],
),
child: const Icon(
Icons.smart_toy_outlined,
color: Colors.white,
size: 22,
),
),
const SizedBox(width: 10),
const SizedBox(width: 12),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
Text(
'DeepVoice AI',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: isDarkMode ? Colors.white : Colors.black87,
),
),
Row(
children: [
Container(
width: 8,
height: 8,
decoration: BoxDecoration(
width: 6,
height: 6,
decoration: const BoxDecoration(
color: Colors.green,
borderRadius: BorderRadius.circular(4),
shape: BoxShape.circle,
),
),
const SizedBox(width: 4),
@ -54,6 +76,7 @@ class AgentView extends StatelessWidget {
style: TextStyle(
fontSize: 12,
color: Colors.green,
fontWeight: FontWeight.w500,
),
),
],
@ -63,14 +86,62 @@ class AgentView extends StatelessWidget {
],
),
actions: [
PopupMenuButton(
icon: const Icon(Icons.more_vert),
itemBuilder: (context) => [
PopupMenuItem(
child: const Text('清除聊天记录'),
onTap: () => controller.clearChat(),
// TTS开关
Obx(() => IconButton(
icon: Icon(
controller.isTtsEnabled.value
? Icons.volume_up
: Icons.volume_off,
color: controller.isTtsEnabled.value
? primaryColor
: isDarkMode
? Colors.grey[400]
: Colors.grey,
),
onPressed: () => controller.toggleTts(),
)),
// 更多选项
Container(
width: 34,
height: 34,
margin: const EdgeInsets.only(right: 8),
decoration: BoxDecoration(
color: isDarkMode ? Colors.white.withOpacity(0.1) : Colors.grey[100],
shape: BoxShape.circle,
boxShadow: [
if (!isDarkMode)
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 2,
spreadRadius: 0.5,
),
],
),
child: PopupMenuButton(
icon: Icon(Icons.more_vert,
color: isDarkMode ? Colors.white : Colors.black87,
size: 20),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
],
color: isDarkMode ? Colors.grey[800] : Colors.white,
itemBuilder: (context) => [
PopupMenuItem(
child: Row(
children: [
Icon(Icons.delete_outline, color: primaryColor, size: 20),
const SizedBox(width: 8),
const Text('清除聊天记录', style: TextStyle(fontSize: 14)),
],
),
onTap: () => Future.delayed(
const Duration(milliseconds: 100),
() => controller.clearChat(),
),
),
],
),
),
],
),
@ -81,19 +152,22 @@ class AgentView extends StatelessWidget {
child: Obx(
() => ListView.builder(
controller: controller.scrollController,
reverse: false,
padding: const EdgeInsets.all(16),
reverse: true,
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 16),
itemCount: controller.messages.length,
itemBuilder: (context, index) {
final message = controller.messages[index];
return MessageBubble(
isUser: message.isUser,
message: message.text,
timestamp: message.timestamp,
isRecognizing: message.isRecognizing,
hasImage: message.hasImage,
imagePath: message.imagePath,
mate: message.meta,
final message = controller.messages[controller.messages.length - 1 - index];
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: MessageBubble(
isUser: message.isUser,
message: message.text,
timestamp: message.timestamp,
isRecognizing: message.isRecognizing,
hasImage: message.hasImage,
imagePath: message.imagePath,
mate: message.meta,
),
);
},
),
@ -102,16 +176,22 @@ class AgentView extends StatelessWidget {
// 输入区域
Container(
padding: const EdgeInsets.all(16),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color: Theme.of(context).scaffoldBackgroundColor,
color: backgroundColor,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 5,
color: Colors.black.withOpacity(0.06),
blurRadius: 8,
offset: const Offset(0, -1),
),
],
border: Border(
top: BorderSide(
color: Colors.grey.withOpacity(0.15),
width: 1,
),
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
@ -141,12 +221,12 @@ class AgentView extends StatelessWidget {
child: GestureDetector(
onTap: () => controller.clearImageInput(),
child: Container(
padding: EdgeInsets.all(4),
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.6),
shape: BoxShape.circle,
),
child: Icon(
child: const Icon(
Icons.close,
color: Colors.white,
size: 16,
@ -158,7 +238,7 @@ class AgentView extends StatelessWidget {
),
// 预设提示选项
SizedBox(height: 8),
const SizedBox(height: 12),
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
@ -168,13 +248,13 @@ class AgentView extends StatelessWidget {
label: '总结图片内容',
onTap: () => controller.summarizeImage(),
),
SizedBox(width: 8),
const SizedBox(width: 8),
_buildPromptChip(
context: context,
label: '抽取图像文字',
onTap: () => controller.extractTextFromImage(),
),
SizedBox(width: 8),
const SizedBox(width: 8),
_buildPromptChip(
context: context,
label: '翻译图像文字',
@ -183,11 +263,11 @@ class AgentView extends StatelessWidget {
],
),
),
SizedBox(height: 12),
const SizedBox(height: 12),
],
);
} else {
return SizedBox.shrink();
return const SizedBox.shrink();
}
}),
@ -202,15 +282,16 @@ class AgentView extends StatelessWidget {
controller.toggleInputMode();
},
child: Container(
width: 48,
height: 48,
width: 44,
height: 44,
decoration: BoxDecoration(
color: Colors.grey[200],
borderRadius: BorderRadius.circular(24),
borderRadius: BorderRadius.circular(22),
),
child: Icon(
isTextMode ? Icons.mic_none : Icons.keyboard,
color: Colors.grey[600],
color: Colors.grey[700],
size: 22,
),
),
);
@ -229,7 +310,11 @@ class AgentView extends StatelessWidget {
padding: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
color: Colors.grey[200],
borderRadius: BorderRadius.circular(24),
borderRadius: BorderRadius.circular(22),
border: Border.all(
color: Colors.grey.withOpacity(0.1),
width: 1.0,
),
),
child: Row(
children: [
@ -243,10 +328,18 @@ class AgentView extends StatelessWidget {
controller: controller.textController,
maxLines: isImageActive ? 2 : 1,
minLines: isImageActive ? 2 : 1,
style: TextStyle(
fontSize: 15,
color: isDarkMode ? Colors.white : Colors.black87,
),
decoration: InputDecoration(
hintText: isImageActive
? '添加对图片的问题或描述...'
: '输入消息...',
hintStyle: TextStyle(
color: Colors.grey[500],
fontSize: 15,
),
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(
vertical: isImageActive ? 12 : 0),
@ -258,9 +351,13 @@ class AgentView extends StatelessWidget {
),
GestureDetector(
onTap: () => controller.sendTextMessage(),
child: Icon(
Icons.send,
color: Theme.of(context).primaryColor,
child: Container(
padding: const EdgeInsets.all(6),
child: Icon(
Icons.send,
color: primaryColor,
size: 22,
),
),
),
],
@ -277,12 +374,16 @@ class AgentView extends StatelessWidget {
}
},
child: Container(
height: 48,
height: 44,
padding:
const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
color: Colors.grey[200],
borderRadius: BorderRadius.circular(24),
borderRadius: BorderRadius.circular(22),
border: Border.all(
color: Colors.grey.withOpacity(0.1),
width: 1.0,
),
),
child: Row(
children: [
@ -306,19 +407,18 @@ class AgentView extends StatelessWidget {
Icons.mic,
size: 14,
color: isListening
? Theme.of(context)
.primaryColor
: Colors.grey,
? primaryColor
: Colors.grey[500],
),
const SizedBox(width: 4),
Text(
'正在聆听',
style: TextStyle(
fontSize: 12,
fontSize: 13,
fontWeight: FontWeight.w500,
color: isListening
? Theme.of(context)
.primaryColor
: Colors.grey,
? primaryColor
: Colors.grey[500],
),
),
],
@ -339,18 +439,22 @@ class AgentView extends StatelessWidget {
// 图片选择按钮
Container(
width: 48,
height: 48,
width: 44,
height: 44,
decoration: BoxDecoration(
color: Colors.grey[200],
borderRadius: BorderRadius.circular(24),
borderRadius: BorderRadius.circular(22),
),
child: PopupMenuButton<String>(
icon: Icon(
Icons.add_photo_alternate_outlined,
color: Colors.grey[600],
color: Colors.grey[700],
size: 22,
),
padding: EdgeInsets.zero,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
onSelected: (value) async {
if (value == 'camera') {
await controller.takePhoto();
@ -364,9 +468,9 @@ class AgentView extends StatelessWidget {
child: Row(
children: [
Icon(Icons.camera_alt,
color: Theme.of(context).primaryColor),
SizedBox(width: 8),
Text('拍照'),
color: primaryColor, size: 20),
const SizedBox(width: 8),
const Text('拍照', style: TextStyle(fontSize: 14)),
],
),
),
@ -375,9 +479,9 @@ class AgentView extends StatelessWidget {
child: Row(
children: [
Icon(Icons.photo_library,
color: Theme.of(context).primaryColor),
SizedBox(width: 8),
Text('从相册选择'),
color: primaryColor, size: 20),
const SizedBox(width: 8),
const Text('从相册选择', style: TextStyle(fontSize: 14)),
],
),
),
@ -400,22 +504,26 @@ class AgentView extends StatelessWidget {
required String label,
required VoidCallback onTap,
}) {
final theme = Theme.of(context);
final primaryColor = theme.primaryColor;
return GestureDetector(
onTap: onTap,
child: Container(
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 6),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: Theme.of(context).primaryColor.withOpacity(0.1),
color: primaryColor.withOpacity(0.08),
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: Theme.of(context).primaryColor.withOpacity(0.3),
color: primaryColor.withOpacity(0.2),
width: 1,
),
),
child: Text(
label,
style: TextStyle(
fontSize: 13,
color: Theme.of(context).primaryColor,
color: primaryColor,
fontWeight: FontWeight.w500,
),
),
@ -463,7 +571,7 @@ class _SoundBarState extends State<_SoundBar>
duration: const Duration(milliseconds: 500),
);
_animation = Tween<double>(begin: 3, end: 15).animate(
_animation = Tween<double>(begin: 3, end: 18).animate(
CurvedAnimation(
parent: _controller,
curve: Curves.easeInOut,
@ -493,7 +601,7 @@ class _SoundBarState extends State<_SoundBar>
} else {
_controller.reset();
// 设置为小高度的静态状态
_controller.animateTo(0.1, duration: Duration(milliseconds: 300));
_controller.animateTo(0.1, duration: const Duration(milliseconds: 300));
}
}
@ -505,13 +613,17 @@ class _SoundBarState extends State<_SoundBar>
@override
Widget build(BuildContext context) {
final primaryColor = Theme.of(context).primaryColor;
return Container(
width: 2,
width: 2.5,
height: widget.isActive ? _animation.value : 3,
margin: const EdgeInsets.symmetric(horizontal: 1),
decoration: BoxDecoration(
color:
widget.isActive ? Theme.of(context).primaryColor : Colors.grey[400],
borderRadius: BorderRadius.circular(1),
color: widget.isActive
? primaryColor.withOpacity(0.8)
: Colors.grey.withOpacity(0.4),
borderRadius: BorderRadius.circular(1.5),
),
);
}

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

@ -197,7 +197,7 @@ class MessageBubble extends GetView<AgentController> {
// 判断消息内容,显示特殊卡片
if (!isUser && mate != null) {
if (mate!.keys.contains('card_music')) {
final musicId = mate!['card_music']['id'] as String;
final musicId = mate!['card_music']['id'].toString();
final musicName = mate!['card_music']['name'] as String;
final musicUrl = mate!['card_music']['url'] as String;
final musicSinger = mate!['card_music']['sgener'] as String;
@ -326,7 +326,7 @@ class MessageBubble extends GetView<AgentController> {
...playlist.asMap().entries.map((entry) {
final index = entry.key;
final item = entry.value;
final id = item["id"] ?? "";
final id = item["id"]?.toString() ?? "";
final name = item["name"] ?? "未知歌曲";
final url = item["url"] ?? "";
final singer = item["sgener"] ?? "未知歌手";

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

@ -186,7 +186,7 @@ class PairingController extends GetxController {
if (Platform.isAndroid) {
permissionsToRequest.add(Permission.locationWhenInUse);
permissionsToRequest.add(Permission.microphone);
// permissionsToRequest.add(Permission.microphone);
if (await _getAndroidSdkVersion() >= 31) {
permissionsToRequest.add(Permission.bluetoothScan);
permissionsToRequest.add(Permission.bluetoothConnect);

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

@ -666,12 +666,11 @@ object AgentService : CoroutineScope {
override fun onFunctionCallResult(functionCall: JSONObject, functionCallResult: JSONObject) {
audioPlayer?.stopAudio()
FileLogger.d(TAG, "mcp调用结果: $functionCallResult")
sendEvent("function_call_result", mapOf(
"function_call" to functionCall.toString(),
"result" to functionCallResult.toString(),
))
FileLogger.d(TAG, "mcp调用结果: $functionCallResult")
FileLogger.d(TAG, "mcp调用结果: $functionCallResult")
metadata = autoPlayMusic(functionCallResult);
}
}

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

@ -7,7 +7,9 @@
<application>
<service
android:name=".MusicService"
android:exported="true">
android:exported="true"
android:foregroundServiceType="mediaPlayback"
>
<!-- 让系统知道这是媒体浏览服务 -->
<intent-filter>
<action android:name="android.media.browse.MediaBrowserService" />
@ -20,7 +22,7 @@
<receiver
android:name="androidx.media.session.MediaButtonReceiver"
android:exported="false">
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter>

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

@ -55,6 +55,7 @@ class MusicService : MediaBrowserServiceCompat() {
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
FileLogger.i(TAG, "onStartCommand: $intent")
intent?.action?.takeIf { it == Intent.ACTION_MEDIA_BUTTON }?.let {
// 分发给 MediaSession 处理媒体按钮
MediaButtonReceiver.handleIntent(mediaSession, intent)
@ -62,7 +63,6 @@ class MusicService : MediaBrowserServiceCompat() {
}
val command = intent?.getStringExtra("command") ?: return START_NOT_STICKY
FileLogger.i(TAG, "liwei------------------- onStartCommand: $command")
when (command) {
"play" -> intent.getSerializableExtra("song")?.let { url ->
val songMap = intent.getSerializableExtra("song") as? Map<String, String>

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

@ -60,4 +60,5 @@ dependencies {
implementation("io.ktor:ktor-client-logging:3.1.2")
implementation("com.google.code.gson:gson:2.10.1")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0")
implementation("androidx.media:media:1.6.0")
}

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

@ -528,7 +528,7 @@ class OpenAIService(private val context: Context? = null) : CoroutineScope {
val call = client.newCall(request)
// 保存当前Call对象以便后续取消
currentStreamCall = call
Log.d(TAG, "发送消息流式输出: request=$request")
// Log.d(TAG, "发送消息流式输出: request=$request")
call.enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
Log.d(TAG, "发送消息流式输出失败: e=$e")
@ -638,7 +638,7 @@ class OpenAIService(private val context: Context? = null) : CoroutineScope {
// 处理普通文本内容
if (delta.has("content")) {
val content = delta.getString("content")
Log.d(TAG, "处理流式JSON数据: content=$content")
// Log.d(TAG, "处理流式JSON数据: content=$content")
if (!isCanceled) {
callback.onToken(content)
}

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

@ -17,7 +17,7 @@ import android.util.Log
import org.json.JSONObject
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlin.coroutines.resume
import android.view.KeyEvent
/**
* 系统功能处理器 - 负责调用系统API执行各种功能以及注册系统功能
*/
@ -430,27 +430,22 @@ class SystemFunctionHandler(private val context: Context) {
)
flags = Intent.FLAG_RECEIVER_FOREGROUND
}
context.sendOrderedBroadcast(keyIntent, null)
keyIntent.setPackage("com.yunqiinnovation.deep_voice")
context.sendBroadcast(keyIntent)
return Result(
success = true,
message = "已开始播放媒体"
)
}
"pause" -> {
// 尝试发送暂停广播
intent.action = "android.intent.action.MEDIA_PAUSE"
context.sendBroadcast(intent)
// 也尝试使用媒体按键广播
val keyIntent = Intent(Intent.ACTION_MEDIA_BUTTON).apply {
putExtra(
Intent.EXTRA_KEY_EVENT,
android.view.KeyEvent(android.view.KeyEvent.ACTION_DOWN, android.view.KeyEvent.KEYCODE_MEDIA_PAUSE)
)
flags = Intent.FLAG_RECEIVER_FOREGROUND
}
context.sendOrderedBroadcast(keyIntent, null)
return Result(
// 发送完整的按键事件序列
val downEvent = KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PAUSE)
val keyIntent = Intent(Intent.ACTION_MEDIA_BUTTON)
keyIntent.putExtra(Intent.EXTRA_KEY_EVENT, downEvent)
keyIntent.setPackage("com.yunqiinnovation.deep_voice")
context.sendBroadcast(keyIntent)
return Result(
success = true,
message = "已暂停媒体播放"
)
@ -468,7 +463,8 @@ class SystemFunctionHandler(private val context: Context) {
)
flags = Intent.FLAG_RECEIVER_FOREGROUND
}
context.sendOrderedBroadcast(keyIntent, null)
keyIntent.setPackage("com.yunqiinnovation.deep_voice")
context.sendBroadcast(keyIntent)
return Result(
success = true,
message = "已切换到上一首"
@ -487,7 +483,8 @@ class SystemFunctionHandler(private val context: Context) {
)
flags = Intent.FLAG_RECEIVER_FOREGROUND
}
context.sendOrderedBroadcast(keyIntent, null)
keyIntent.setPackage("com.yunqiinnovation.deep_voice")
context.sendBroadcast(keyIntent)
return Result(
success = true,
message = "已切换到下一首"

Loading…
Cancel
Save