Browse Source

Merge branch 'new_dev' of https://github.com/deepcloud2048/deep_voice into new_dev

weicu
tanlongsheng 1 year ago
parent
commit
3b1e2c1f77
  1. 10
      ios/Podfile.lock
  2. 24
      ios/Runner/Info.plist
  3. 2
      lib/data/models/appconfig_model.dart
  4. 2
      lib/data/models/appconfig_model.g.dart
  5. 217
      lib/modules/agent/controllers/agent_controller.dart
  6. 3
      lib/modules/agent/controllers/report_controller.dart
  7. 5
      lib/modules/agent/views/agent_view.dart
  8. 8
      lib/modules/agent/views/message_bubble.dart
  9. 16
      lib/modules/agent/views/weather_card.dart
  10. 58
      lib/modules/pairing/controllers/pairing_controller.dart
  11. 132
      lib/modules/pairing/views/pairing_view.dart
  12. 131
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt
  13. 42
      local_plugins/amap_service/ios/Classes/AmapHelper.swift
  14. 165
      local_plugins/amap_service/ios/Classes/AmapService.swift
  15. 34
      local_plugins/amap_service/ios/Classes/AmapServicePlugin.swift
  16. 231
      local_plugins/amap_service/ios/Classes/AmapViewController.swift
  17. 16
      local_plugins/amap_service/ios/amap_service.podspec
  18. 32
      local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiService.kt
  19. 6
      local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/CustomSseClientTransport.kt
  20. 5
      local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/MCPClient.kt
  21. 125
      local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/MCPSubClient.kt
  22. 12
      local_plugins/chat_api/ios/chat_api/Sources/chat_api/SystemFunctionHandler.swift

10
ios/Podfile.lock

@ -1,6 +1,10 @@
PODS:
- amap_service (0.0.1):
- AMapNavi (~> 10.0.900)
- Flutter
- AMapFoundation (1.8.2)
- AMapNavi (10.0.900):
- AMapFoundation (>= 1.8.2)
- audio_waveforms (0.0.1):
- Flutter
- classic_bluetooth (0.0.1):
@ -102,6 +106,8 @@ DEPENDENCIES:
SPEC REPOS:
trunk:
- AMapFoundation
- AMapNavi
- FBAEMKit
- FBSDKCoreKit
- FBSDKCoreKit_Basics
@ -152,7 +158,9 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/tencentcloud_cos_sdk_plugin/ios"
SPEC CHECKSUMS:
amap_service: 3110d959589c6d49570ebe9162418d7b046df30d
amap_service: 1c3e7e07f51cbbdff03719a4a079c60d015c1c0b
AMapFoundation: 9885c48fc3a78fdfb84a0299a2293e56ea3c9fec
AMapNavi: fceb4715f9a3b5a25f330facebee2aa2bdba42dd
audio_waveforms: a6dde7fe7c0ea05f06ffbdb0f7c1b2b2ba6cedcf
classic_bluetooth: c146e1618e867a47fa7ab79a3bf262e7d69d557d
device_info_plus: 71ffc6ab7634ade6267c7a93088ed7e4f74e5896

24
ios/Runner/Info.plist

@ -76,10 +76,6 @@
<array>
<string>EFAB</string>
</array>
<key>NSAccessorySetupKitSupports</key>
<array>
<string>Bluetooth</string>
</array>
<key>NSAccessorySetupDescriptions</key>
<array>
<dict>
@ -89,7 +85,10 @@
<string>语音助理耳机</string>
</dict>
</array>
<key>NSAccessorySetupKitSupports</key>
<array>
<string>Bluetooth</string>
</array>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
@ -98,7 +97,7 @@
<true/>
<key>NSExceptionDomains</key>
<dict>
<key>mcp.ideapsound.com</key>
<key>mcp.amap.com</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
@ -107,7 +106,7 @@
<key>NSIncludesSubdomains</key>
<true/>
</dict>
<key>mcp.amap.com</key>
<key>mcp.ideapsound.com</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
@ -122,24 +121,27 @@
<string>需要蓝牙权限用于连接和管理音频设备</string>
<key>NSBluetoothPeripheralUsageDescription</key>
<string>需要蓝牙权限用于连接和管理音频设备</string>
<key>NSCalendarsUsageDescription</key>
<string>需要日历权限用于安排和管理您的音频会议和录音任务</string>
<key>NSCameraUsageDescription</key>
<string>需要相机权限用于扫描二维码和视频通话功能</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>语音助手需要位置权限来为您提供基于位置的信息和服务</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>语音助手需要位置权限来为您提供基于位置的信息和服务</string>
<key>NSMicrophoneUsageDescription</key>
<string>需要麦克风权限用于语音识别和录音功能</string>
<key>NSCalendarsUsageDescription</key>
<string>需要日历权限用于安排和管理您的音频会议和录音任务</string>
<key>NSMotionUsageDescription</key>
<string>我们需要访问您的运动数据以提供更精准的导航服务</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>需要相册权限用于保存录音文件和导入音频文件</string>
<key>NSCameraUsageDescription</key>
<string>需要相机权限用于扫描二维码和视频通话功能</string>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UIBackgroundModes</key>
<array>
<string>bluetooth-central</string>
<string>audio</string>
<string>location</string>
</array>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>

2
lib/data/models/appconfig_model.dart

@ -52,7 +52,7 @@ class DBAgent {
class DBMCPServer {
final String servername;
final String url;
final String? tools;
final String tools;
DBMCPServer({
required this.servername,
required this.url,

2
lib/data/models/appconfig_model.g.dart

@ -57,7 +57,7 @@ Map<String, dynamic> _$DBAgentToJson(DBAgent instance) => <String, dynamic>{
DBMCPServer _$DBMCPServerFromJson(Map<String, dynamic> json) => DBMCPServer(
servername: json['servername'] as String,
url: json['url'] as String,
tools: json['tools'] as String?,
tools: json['tools'] as String? ?? '',
);
Map<String, dynamic> _$DBMCPServerToJson(DBMCPServer instance) =>

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

@ -4,7 +4,6 @@ import 'dart:io';
import 'package:amap_service/amap_service.dart';
import 'package:deep_voice/data/services/music_manager.dart';
import 'package:deep_voice/data/services/navigation_manager.dart';
import 'package:deep_voice/routes/app_routes.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:agent_service/agent_service.dart';
@ -12,32 +11,11 @@ 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';
import 'package:deep_voice/core/utils/logger.dart';
import 'package:path_provider/path_provider.dart';
import 'package:photo_view/photo_view.dart';
import '../../../core/utils/permission_util.dart';
class Message {
final bool isUser; //标识信息是否来自用户还是AI
final String text; //信息文本内容
final DateTime timestamp; // 消息时间戳
final bool isRecognizing; // 是否为语音识别中的临时消息
final bool hasImage; // 是否包含图片
final String? imagePath; // 图片路径
final Map? meta; //卡片数据
Message({
// 构造函数
required this.isUser,
required this.text,
DateTime? timestamp,
this.isRecognizing = false,
this.hasImage = false,
this.imagePath,
this.meta,
}) : this.timestamp = timestamp ?? DateTime.now();
}
import '../../../data/models/message_model.dart';
class AgentController extends GetxController {
static AgentController get to => Get.find(); //单例访问模式
@ -214,7 +192,7 @@ class AgentController extends GetxController {
}
}
// 添加欢迎消息
// 添加初始欢迎消息
void _addWelcomeMessage() {
messages.add(Message(
isUser: false,
@ -418,7 +396,7 @@ class AgentController extends GetxController {
break;
case AgentServiceEventType.functionCallResult:
final responseId = event.data['responseId'] ?? '';
final responseId = event.data['responseId'] ?? '';
// final result = event.data['result'];
final metaStr = event.data['meta'] ?? '';
// final functionCall = event.data['function_call'] ?? '';
@ -521,8 +499,10 @@ class AgentController extends GetxController {
}
// 发送消息(包括所有支持的消息类型)
Future<void> sendTextMessage() async {
// 如果图片输入是活跃的,使用选中的图片和文本框的内容
//场景1:用户在文本输入框中输入文本,点击发送按钮,调用sendMessage()方法。
//场景2:当用户选择图片后在文本框输入内容并发送时,调用sendMessage()转发给sendImageMessage()。
Future<void> sendMessage() async {
// 1. 处理图片+文本的混合输入
if (isImageInputActive.value && selectedImagePath.value != null) {
final text = textController.text.trim();
final imagePath = selectedImagePath.value!; // 使用!强制断言非空
@ -533,6 +513,7 @@ class AgentController extends GetxController {
return;
}
// 2. 处理纯文本消息
final text = textController.text.trim();
if (text.isEmpty) return;
@ -541,7 +522,7 @@ class AgentController extends GetxController {
textController.clear();
// 使用reverse:true时通常不需要手动滚动
// 3. 调用AI服务处理
try {
isProcessing.value = true;
// 标记为新的AI回复
@ -636,36 +617,6 @@ class AgentController extends GetxController {
}
}
// 保存消息到持久化存储
// Future<void> _saveMessageToStorage(Message message) async {
// try {
// // 准备元数据,包含图片信息
// Map<String, dynamic> metadata = {};
// if (message.meta != null) {
// metadata = Map<String, dynamic>.from(message.meta!);
// }
// // 添加图片相关信息到元数据
// if (message.hasImage) {
// metadata['hasImage'] = true;
// metadata['imagePath'] = message.imagePath;
// }
// // 保存消息
// await ChatStorage.saveMessage(
// sessionId: sessionId,
// sender: message.isUser ? 'user' : 'assistant',
// message: message.text,
// metadata: metadata.isNotEmpty ? jsonEncode(metadata) : null,
// //timestamp: message.timestamp.toIso8601String(),
// );
// Logger.i(TAG, '消息已保存到持久化存储');
// } catch (e) {
// Logger.e(TAG, '保存消息到持久化存储失败: $e');
// }
// }
// 切换输入模式
void toggleInputMode() async {
// 如果当前在图片输入模式,先清除
@ -703,156 +654,6 @@ class AgentController extends GetxController {
}
}
// 检查麦克风权限,返回true表示已授权,false表示未授权
Future<bool> _checkMicrophonePermission() async {
try {
// 1.先获取当前麦克风权限状态
final micStatus = await Permission.microphone.status;
// 2.如果麦克风权限已授予,返回true
if (micStatus.isGranted) {
return true;
}
// 3.创建待请求权限列表(这里只请求麦克风权限)
List<Permission> permissionsToRequest = [];
permissionsToRequest.add(Permission.microphone);
// 4.弹出权限说明对话框(首次拒绝/还没授权时展示)
if (micStatus.isPermanentlyDenied) {
_showPermissionDialog(true);
} else {
_showPermissionDialog(false);
}
// 5.请求权限,并获取返回的权限状态集合
final statuses = await permissionsToRequest.request();
// 6.用于记录被拒绝的权限(便于后续判断)
Permission? deniedPermission;
// 7.遍历权限状态集合,检查是否有任何权限被拒绝
statuses.forEach((permission, status) {
if (!status.isGranted) {
deniedPermission = permission;
Logger.e(TAG, '权限 ${permission.toString()} 未被授予: $status');
}
});
// // 8. 如果是“永久拒绝”(用户勾选了“不再询问”)
// if (micStatus.isPermanentlyDenied) {
// _showPermissionDialog(true);
// return false;
// }
// // 9. 普通的拒绝情况,再次弹出说明对话框
// if (deniedPermission != null) {
// _showPermissionDialog(false);
// }
// 10. 如果权限未被授予,返回 false
return false;
} catch (e) {
Logger.e(TAG, '检查麦克风权限失败: $e');
return false;
}
}
// 显示权限提示对话框
void _showPermissionDialog(bool isPermanent) {
Get.generalDialog(
barrierDismissible: false,
barrierLabel: '权限提示',
transitionBuilder: (context, anim1, anim2, child) {
return SlideTransition(
position: Tween<Offset>(
begin: Offset(0, -1), // 从上方滑入
end: Offset(0, 0),
).animate(CurvedAnimation(parent: anim1, curve: Curves.easeOut)),
child: child,
);
},
pageBuilder: (context, anim1, anim2) {
return SafeArea(
child: Align(
alignment: Alignment.topCenter, // 关键:设置为顶部对齐
child: Padding(
padding: const EdgeInsets.only(top: 80, left: 24, right: 24),
child: Material(
borderRadius: BorderRadius.circular(12),
color: Colors.white,
child: Padding(
padding: const EdgeInsets.all(16),
child: ConstrainedBox(
constraints: BoxConstraints(maxWidth: 400),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('需要麦克风权限',
style: TextStyle(
fontSize: 18, fontWeight: FontWeight.bold)),
const SizedBox(height: 12),
Text(isPermanent
? '语音功能需要麦克风权限。请前往设置页面手动开启权限。'
: '语音功能需要麦克风权限。请允许使用麦克风权限以继续使用语音功能。'),
const SizedBox(height: 16),
const Text('权限使用说明:',
style: TextStyle(fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
const Text('• 麦克风权限:用于录制您的语音,实现语音翻译、语音助手对话等核心功能'),
const SizedBox(height: 8),
const Text('• 我们仅在您主动使用语音功能时访问麦克风'),
const SizedBox(height: 8),
const Text('• 您可以随时在系统设置中关闭权限'),
const SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () => Get.back(),
child: const Text('取消'),
),
const SizedBox(width: 8),
TextButton(
onPressed: () {
Get.back();
if (isPermanent) {
AppSettings.openAppSettings();
} else {
Permission.microphone.request();
}
},
child: Text(isPermanent ? '前往设置' : '授权'),
),
],
),
],
),
),
),
),
),
),
);
},
);
}
// 获取Android SDK版本
Future<int> _getAndroidSdkVersion() async {
if (Platform.isAndroid) {
try {
// 这里我们简单返回一个较高的值,确保请求所有可能需要的权限
// 在实际应用中,你可能需要使用platform channel获取真实的SDK版本
return 31; // 对应Android 12
} catch (e) {
Logger.e(TAG, '获取Android SDK版本失败: $e');
}
}
return 0;
}
// 开始语音输入
Future<void> startVoiceInput() async {
if (isListening.value) return;

3
lib/modules/agent/controllers/report_controller.dart

@ -1,7 +1,8 @@
import 'package:get/get.dart';
import 'agent_controller.dart'; // 确保导入Message模型
//import 'agent_controller.dart'; // 确保导入Message模型
import 'package:flutter/material.dart';
import '../../../data/services/network/api.dart';
import '../../../data/models/message_model.dart';
class ReportController extends GetxController {
// 当前举报的消息

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

@ -8,6 +8,7 @@ import 'package:flutter/services.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../../../core/theme/app_colors.dart';
import 'package:haptic_feedback/haptic_feedback.dart';
import '../../../data/models/message_model.dart';
class AgentView extends StatelessWidget {
const AgentView({Key? key}) : super(key: key);
@ -384,12 +385,12 @@ class AgentView extends StatelessWidget {
vertical: isImageActive ? 12 : 0),
),
onSubmitted: (_) =>
controller.sendTextMessage(),
controller.sendMessage(),
);
}),
),
GestureDetector(
onTap: () => controller.sendTextMessage(),
onTap: () => controller.sendMessage(),
child: Container(
padding: const EdgeInsets.all(6),
child: Icon(

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

@ -249,12 +249,12 @@ class MessageBubble extends GetView<AgentController> {
rawList.map((e) => Map<String, dynamic>.from(e)).toList();
//音乐播放列表
return buildMusicContent(context, playlist);
} else if (meta!.keys.contains('card_weather')) {
final city = meta!['card_weather']['city'] as String;
final rawList = meta!['card_weather']['dayweather'] as List;
} else if (meta!.keys.contains('maps_weather')) {
final city = meta!['maps_weather']['city'] as String;
final rawList = meta!['maps_weather']['forecasts'] as List;
final daylist =
rawList.map((e) => Map<String, dynamic>.from(e)).toList();
final now = meta!['card_weather']['day'] as Map<String, dynamic>;
final now = daylist[0] as Map<String, dynamic>;
//天气
return buildWeatherContent(
context,

16
lib/modules/agent/views/weather_card.dart

@ -62,7 +62,7 @@ Widget buildWeatherContent(BuildContext context, String city,
),
SizedBox(height: 4),
Text(
now["weatherCondition"],
now["dayweather"],
style: TextStyle(
fontSize: 14,
color: Colors.white.withOpacity(0.9),
@ -72,7 +72,7 @@ Widget buildWeatherContent(BuildContext context, String city,
Row(
children: [
Text(
"${now["temperature"]}°",
"${now["daytemp"]}°",
style: const TextStyle(
fontSize: 36,
fontWeight: FontWeight.bold,
@ -84,14 +84,14 @@ Widget buildWeatherContent(BuildContext context, String city,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'最高: ${now["maxTemperature"]}°',
'最高: ${now["daytemp"]}°',
style: TextStyle(
fontSize: 12,
color: Colors.white.withOpacity(0.9),
),
),
Text(
'最低: ${now["minTemperature"]}°',
'最低: ${now["nighttemp"]}°',
style: TextStyle(
fontSize: 12,
color: Colors.white.withOpacity(0.9),
@ -103,8 +103,8 @@ Widget buildWeatherContent(BuildContext context, String city,
),
],
),
const Icon(
Icons.wb_sunny,
Icon(
getWeatherIcon(now['dayweather']),
size: 48,
color: Colors.white,
),
@ -133,10 +133,10 @@ Widget buildWeatherContent(BuildContext context, String city,
String dateStr = days[index]['date'];
DateTime? date = DateUtil.getDateTime(dateStr);
IconData weatherIcon =
getWeatherIcon(days[index]['weatherCondition']);
getWeatherIcon(days[index]['dayweather']);
String weekday = DateUtil.getWeekday(date,
languageCode: "zh", short: true);
String temp = "${days[index]['temperature']}°";
String temp = "${days[index]['daytemp']}°";
return _buildForecastDay(
weekday,
weatherIcon,

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

@ -36,6 +36,10 @@ class PairingController extends GetxController {
final _state = PairingState.init.obs;
PairingState get state => _state.value;
// ==================== 手动模式控制 ====================
final isManualMode = true.obs;
final isReadyToStart = false.obs;
// ==================== 状态数据 ====================
final statusDescription = ''.obs;
final actionButtonText = ''.obs;
@ -78,12 +82,32 @@ class PairingController extends GetxController {
_transition(PairingState.init);
}
/// 手动开始配对
Future<void> startManualPairing() async {
if (!isReadyToStart.value) return;
Logger.i(_tag, '用户手动启动配对流程');
isManualMode.value = false;
isReadyToStart.value = false;
// 开始执行配对流程
await _executeInitFlow();
}
/// 重新开始配对流程
Future<void> restart() async {
_cancelAllOperations();
_transition(PairingState.init);
}
/// 重置到手动模式
void resetToManualMode() {
_cancelAllOperations();
isManualMode.value = true;
isReadyToStart.value = true;
_transition(PairingState.init);
}
/// 处理按钮点击
void handleActionButtonTap() {
switch (state) {
@ -174,10 +198,24 @@ class PairingController extends GetxController {
// ==================== 状态处理方法 ====================
/// INIT: 初始化,检查权限
/// INIT: 初始化状态
Future<void> _enterInit() async {
Logger.i(_tag, '开始执行初始化流程');
Logger.i(_tag, '进入初始化状态');
if (isManualMode.value) {
// 手动模式:仅显示待机状态,不执行任何操作
isLoading.value = false;
statusDescription.value = '准备就绪';
isReadyToStart.value = true;
return;
}
// 自动模式:执行原有的初始化逻辑
await _executeInitFlow();
}
/// 执行初始化流程
Future<void> _executeInitFlow() async {
isLoading.value = true;
statusDescription.value = '正在初始化...';
@ -529,12 +567,6 @@ class PairingController extends GetxController {
}
// Android权限检查 - 使用 PermissionUtil
// final context = Get.context;
// if (context == null) {
// Logger.e(_tag, '无法获取有效上下文,跳过权限请求');
// return false;
// }
// 根据 Android 版本确定需要的权限
List<Permission> requiredPermissions = [
Permission.locationWhenInUse,
@ -551,7 +583,6 @@ class PairingController extends GetxController {
Logger.d(_tag, '正在请求 $permissionName 权限');
final granted = await PermissionUtil.instance.requestPermission(
// context: context,
permissionType: permission,
permissionName: permissionName,
explanationText: _getExplanationText(permission),
@ -646,7 +677,11 @@ class PairingController extends GetxController {
// 根据状态更新UI文本
switch (state) {
case PairingState.init:
statusDescription.value = '初始化中...';
if (isManualMode.value && isReadyToStart.value) {
statusDescription.value = '准备就绪';
} else {
statusDescription.value = '初始化中...';
}
break;
case PairingState.btReady:
statusDescription.value = '蓝牙已就绪';
@ -718,7 +753,8 @@ class PairingController extends GetxController {
/// 是否可以跳过配对
bool get canSkip =>
!isLoading.value &&
(state == PairingState.exitNoClassic ||
((isManualMode.value && isReadyToStart.value) || // 手动模式待机状态可以跳过
state == PairingState.exitNoClassic ||
state == PairingState.exitNotMatch ||
state == PairingState.exitTimeout);

132
lib/modules/pairing/views/pairing_view.dart

@ -99,11 +99,13 @@ class PairingView extends GetView<PairingController> {
return Obx(() {
final state = controller.state;
final isLoading = controller.isLoading.value;
final isReadyToStart = controller.isReadyToStart.value;
final isManualMode = controller.isManualMode.value;
// 根据状态决定动画颜色
Color primaryColor = Color(0xFF0066CC);
Color secondaryColor = Color(0xFF5E5CE6);
// 错误状态使用红色
if (state.name.startsWith('exit')) {
primaryColor = Colors.red;
@ -114,7 +116,54 @@ class PairingView extends GetView<PairingController> {
primaryColor = Colors.green;
secondaryColor = Colors.greenAccent;
}
// 手动模式待机状态的特殊处理
if (isManualMode && isReadyToStart) {
return Container(
width: 240.w,
height: 240.w,
child: Stack(
alignment: Alignment.center,
children: [
// 静态背景圆圈
Container(
width: 200.w,
height: 200.w,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
primaryColor.withOpacity(0.1),
secondaryColor.withOpacity(0.1),
],
),
shape: BoxShape.circle,
),
),
// 设备图标
Container(
width: 120.w,
height: 120.w,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [primaryColor, secondaryColor],
),
shape: BoxShape.circle,
),
child: Icon(
Icons.headphones,
color: Colors.white,
size: 60.w,
),
),
],
),
);
}
return Container(
width: 240.w,
height: 240.w,
@ -239,11 +288,11 @@ class PairingView extends GetView<PairingController> {
),
// 当前连接设备(有设备时才显示)
if (device != null &&
(state == PairingState.classicOk ||
state == PairingState.serverVerify ||
state == PairingState.pairing ||
state == PairingState.paired)) ...[
if (device != null &&
(state == PairingState.classicOk ||
state == PairingState.serverVerify ||
state == PairingState.pairing ||
state == PairingState.paired)) ...[
SizedBox(height: 24.h),
_buildConnectedDevice(device, isDarkMode),
],
@ -254,10 +303,14 @@ class PairingView extends GetView<PairingController> {
// 根据状态获取副标题
String _getSubtitleForState(PairingState state) {
if (controller.isManualMode.value && controller.isReadyToStart.value) {
return '点击下方按钮开始配对您的DeapSound设备';
}
switch (state) {
case PairingState.init:
case PairingState.btReady:
return '正在准备配对环境...';
return controller.isManualMode.value ? '准备开始配对' : '正在准备配对环境...';
case PairingState.classicOk:
return '请先通过系统蓝牙连接DeapSound设备';
case PairingState.serverVerify:
@ -364,17 +417,50 @@ class PairingView extends GetView<PairingController> {
final showActionButton = controller.showActionButton;
final canSkip = controller.canSkip;
final actionButtonText = controller.actionButtonText.value;
final isReadyToStart = controller.isReadyToStart.value;
final isManualMode = controller.isManualMode.value;
return Container(
width: double.infinity,
child: Column(
children: [
// 主按钮 - 根据状态显示不同的按钮
if (showActionButton)
// 手动模式:显示开始配对按钮
if (isManualMode && isReadyToStart)
ElevatedButton(
onPressed: () => controller.startManualPairing(),
style: ElevatedButton.styleFrom(
backgroundColor: Color(0xFF0066CC),
foregroundColor: Colors.white,
padding: EdgeInsets.symmetric(vertical: 16.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16.r),
),
elevation: 0,
minimumSize: Size(double.infinity, 56.h),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.bluetooth_searching, size: 20.w),
SizedBox(width: 8.w),
Text(
'开始配对设备',
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
),
),
],
),
),
// 原有的状态按钮(错误处理等)
if (showActionButton && !isManualMode)
ElevatedButton(
onPressed: () => controller.handleActionButtonTap(),
style: ElevatedButton.styleFrom(
backgroundColor: _getButtonColor(controller.state, isDarkMode),
backgroundColor:
_getButtonColor(controller.state, isDarkMode),
foregroundColor: Colors.white,
padding: EdgeInsets.symmetric(vertical: 16.h),
shape: RoundedRectangleBorder(
@ -392,7 +478,7 @@ class PairingView extends GetView<PairingController> {
),
),
// 只在可以跳过时显示跳过按钮
// 跳过按钮和重置按钮
if (canSkip) ...[
SizedBox(height: 12.h),
TextButton(
@ -411,6 +497,24 @@ class PairingView extends GetView<PairingController> {
),
),
],
// 添加重置按钮(在错误状态下)
if (!isManualMode &&
!controller.isLoading.value &&
controller.state.name.startsWith('exit')) ...[
SizedBox(height: 8.h),
TextButton(
onPressed: () => controller.resetToManualMode(),
style: TextButton.styleFrom(
foregroundColor: Colors.grey,
padding: EdgeInsets.symmetric(vertical: 8.h),
),
child: Text(
'重新配对',
style: TextStyle(fontSize: 14.sp),
),
),
],
],
),
);

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

@ -673,7 +673,7 @@ object AgentService : CoroutineScope {
override fun onComplete() {
// 视情况决定是否朗读回复
if (speakResponse) {
if (speakResponse && !nobroadcast) {
ttsService?.flushStream()
}
val response = responseBuilder.toString()
@ -731,13 +731,16 @@ object AgentService : CoroutineScope {
override fun onFunctionCallResult(functionCall: JSONObject, functionCallResult: JSONObject) {
audioPlayer?.stopAudio()
val name = functionCall.get("name") as String;
val (metestr, broadcast)= autoHandleFcunCallResult(name,functionCallResult);
aiMetadata = metestr;
nobroadcast = broadcast
sendEvent("function_call_result", mapOf(
"function_call" to functionCall.toString(),
"result" to functionCallResult.toString(),
"meta" to metestr,
))
val (metestr, broadcast)= autoHandleFcunCallResult(functionCallResult);
aiMetadata = metestr;
nobroadcast = broadcast
}
}
)
@ -1102,55 +1105,86 @@ object AgentService : CoroutineScope {
}
}
}
/*
/*
* 自动播放音乐
* */
fun autoHandleFcunCallResult(functionCallResult: JSONObject): Pair<String, Boolean>{
val metaStr = functionCallResult.optString("meta")
var broadcast = false;
if (metaStr.isNotEmpty()) {
val meta = JSONObject(metaStr)
// Log.d(TAG, "mcp调用结果: $metaStr")
if (meta.has("card_music")) { //音乐卡片
val cardMusic = meta.getJSONObject("card_music")
val id = cardMusic.optString("id", "")
val url = cardMusic.optString("url", "")
val name = cardMusic.optString("name", "")
val sgener = cardMusic.optString("sgener", "")
val image = cardMusic.optString("image", "")
processMusicPlay(mapOf("id" to id,"url" to url, "title" to name, "artist" to sgener,"coverUrl" to image))
broadcast = true;
}else if (meta.has("card_musiclist")) { //音乐列表
val cardMusiclist = meta.getJSONObject("card_musiclist")
val musics = cardMusiclist.optJSONArray("musics")
if (musics != null) {
val playlist = mutableListOf<Map<String, String>>()
for (i in 0 until musics.length()) {
val item = musics.optJSONObject(i) ?: continue
val id = item.optString("id", "")
val url = item.optString("url", "")
val name = item.optString("name", "")
val sgener = item.optString("sgener", "")
val image = item.optString("image", "")
playlist.add(mapOf("id" to id,"url" to url, "title" to name, "artist" to sgener,"coverUrl" to image))
}
if (playlist.isNotEmpty()) {
broadcast = true;
processMusicPlayList(playlist)
fun autoHandleFcunCallResult(toolname:String,functionCallResult: JSONObject): Pair<String, Boolean>{
println("协议工具返回数据 检查是否存在卡片! functionCallResult: ${functionCallResult}")
try {
val contentStr = functionCallResult.optString("context")
println("协议工具返回数据 检查是否存在卡片! context: ${contentStr}")
val content = JSONObject(contentStr)
val textStr = content.optString("text")
println("协议工具返回数据 检查是否存在卡片! textStr: ${textStr}")
val metadata: MutableMap<String, Any> = mutableMapOf()
var broadcast = false;
println("协议工具返回数据 检查是否存在卡片!")
if (textStr.isNotEmpty()) {
val meta = JSONObject(textStr)
metadata[toolname] = meta // 现在可以赋值
val metaStr = JSONObject(metadata as Map<*, *>).toString()
// Log.d(TAG, "mcp调用结果: $metaStr")
if (meta.has("card_music")) { //音乐卡片
val cardMusic = meta.getJSONObject("card_music")
val id = cardMusic.optString("id", "")
val url = cardMusic.optString("url", "")
val name = cardMusic.optString("name", "")
val sgener = cardMusic.optString("sgener", "")
val image = cardMusic.optString("image", "")
processMusicPlay(
mapOf(
"id" to id,
"url" to url,
"title" to name,
"artist" to sgener,
"coverUrl" to image
)
)
broadcast = true;
} else if (meta.has("card_musiclist")) { //音乐列表
val cardMusiclist = meta.getJSONObject("card_musiclist")
val musics = cardMusiclist.optJSONArray("musics")
if (musics != null) {
val playlist = mutableListOf<Map<String, String>>()
for (i in 0 until musics.length()) {
val item = musics.optJSONObject(i) ?: continue
val id = item.optString("id", "")
val url = item.optString("url", "")
val name = item.optString("name", "")
val sgener = item.optString("sgener", "")
val image = item.optString("image", "")
playlist.add(
mapOf(
"id" to id,
"url" to url,
"title" to name,
"artist" to sgener,
"coverUrl" to image
)
)
}
if (playlist.isNotEmpty()) {
broadcast = true;
processMusicPlayList(playlist)
} else {
Log.w(TAG, "card_musiclist 中没有有效的音乐条目")
}
} else {
Log.w(TAG, "card_musiclist 中没有有效的音乐条目")
Log.w(TAG, "card_musiclist.musics 不是一个数组")
}
} else {
Log.w(TAG, "card_musiclist.musics 不是一个数组")
} else if (meta.has("card_navigation")) { //导航服务
val cardNavigation = meta.getJSONObject("card_navigation")
val start = cardNavigation.optString("start", "")
val end = cardNavigation.optString("end", "")
broadcast = true;
processNavigation(start, end);
}
}else if (meta.has("card_navigation")) { //导航服务
val cardNavigation = meta.getJSONObject("card_navigation")
val start = cardNavigation.optString("start", "")
val end = cardNavigation.optString("end", "")
broadcast = true;
processNavigation(start,end);
return Pair(metaStr, broadcast);
}
return Pair(metaStr, broadcast);
} catch (e: Exception) {
// 捕获其他类型的异常(可选)
println("为解析到卡片数据!")
}
return Pair("", false);
}
@ -1167,6 +1201,7 @@ object AgentService : CoroutineScope {
*/
fun processMusicPlay(song: Map<String,String>){
// 在其他 Service、BroadcastReceiver 或 Application 中调用
Log.i(TAG, "播放音乐: ${song}")
MusicServiceStarter.startServiceWithCommand(context, command = "play", song = song)
}
@ -1184,7 +1219,7 @@ object AgentService : CoroutineScope {
if (songs.isNotEmpty()) {
MusicServiceStarter.startServiceWithPlaylist(context, command = "playlist", songs = songs)
} else {
Log.i(TAG, "音乐列表为空,未启动播放服务")
Log.e(TAG, "音乐列表为空,未启动播放服务")
}
}

42
local_plugins/amap_service/ios/Classes/AmapHelper.swift

@ -0,0 +1,42 @@
import Foundation
import NotificationCenter
extension Notification.Name {
static let navigationdEvent = Notification.Name("navigationdEvent") //导航事件
}
@objc class AmapHelper: NSObject {
private static var isInitialized = false
// MARK: - 初始化监听器(你可以在服务底层初始化时调用这个方法)
@objc static func initializeListener() {
guard !isInitialized else {
print("⚠️ AmapHelper 已经初始化过了,忽略重复调用")
return
}
isInitialized = true
print("✅ AmapHelper 初始化监听导航事件")
NotificationCenter.default.addObserver(
self,
selector: #selector(handleNavigationEvent(_:)),
name: .navigationdEvent, // 自定义事件名
object: nil
)
}
// MARK: - 处理导航事件
@objc static func handleNavigationEvent(_ notification: Notification) {
guard let userInfo = notification.userInfo else {
print("⚠️ 导航事件未携带 userInfo")
return
}
print("📍 收到导航事件 userInfo: \(userInfo)")
// 示例处理:读取起点和终点信息
if let start = userInfo["start"] as? String,
let end = userInfo["end"] as? String {
print("🚗 导航从 \(start) 到 \(end)")
// TODO: 调用实际导航逻辑
}
}
}

165
local_plugins/amap_service/ios/Classes/AmapService.swift

@ -0,0 +1,165 @@
import Foundation
import AMapNaviKit
import AVFoundation
public class AmapService: NSObject, AMapNaviDriveManagerDelegate {
public static let shared = AmapService()
private var driveManager: AMapNaviDriveManager?
private var startPoint: AMapNaviPoint?
private var endPoint: AMapNaviPoint?
private var isNavigating = false
private override init() {
super.init()
setupDriveManager()
}
private func setupDriveManager() {
// 使用单例模式初始化
driveManager = AMapNaviDriveManager.sharedInstance()
driveManager?.delegate = self
driveManager?.isUseInternalTTS = true
// 设置高德地图服务
// 1. 设置隐私合规
AMapServices.shared()?.enableHTTPS = true
AMapServices.shared()?.apiKey = "aa60a03be9f6531bcc64029e60f95d03"
}
public func startNavigation(from start: String, to end: String) {
guard !isNavigating else {
print("导航已在进行中")
return
}
print("开始导航")
// 解析坐标
let startCoords = start.split(separator: ",").compactMap { Double($0.trimmingCharacters(in: .whitespaces)) }
let endCoords = end.split(separator: ",").compactMap { Double($0.trimmingCharacters(in: .whitespaces)) }
guard startCoords.count == 2, endCoords.count == 2 else {
print("坐标格式错误,应为'经度,纬度'")
return
}
// 创建导航点(注意:高德地图使用经纬度顺序)
startPoint = AMapNaviPoint.location(withLatitude: CGFloat(startCoords[1]),
longitude: CGFloat(startCoords[0]))
endPoint = AMapNaviPoint.location(withLatitude: CGFloat(endCoords[1]),
longitude: CGFloat(endCoords[0]))
if let start = startPoint, let end = endPoint {
isNavigating = true
// 使用推荐的默认策略(避让拥堵+速度优先+避免收费)
let strategy: AMapNaviDrivingStrategy = .drivingStrategySingleDefault
// 调用最新API(注意参数是数组)
let success = driveManager?.calculateDriveRoute(withStart: [start],
end: [end],
wayPoints: nil,
drivingStrategy: strategy) ?? false
if !success {
isNavigating = false
print("路径规划参数校验失败")
}
}
}
public func stopNavigation() {
driveManager?.stopNavi()
isNavigating = false
}
// MARK: - AMapNaviDriveManagerDelegate
public func driveManager(onCalculateRouteSuccess driveManager: AMapNaviDriveManager) {
print("Route calculation succeeded")
driveManager.startEmulatorNavi() // 使用模拟导航(测试用)
// 实际使用时改为:
// driveManager.startGPSNavi()
}
public func driveManager(_ driveManager: AMapNaviDriveManager, onCalculateRouteFailure error: Error) {
isNavigating = false
print("Route calculation failed: \(error.localizedDescription)")
}
public func driveManager(_ driveManager: AMapNaviDriveManager, playNaviSound soundString: String, soundStringType: AMapNaviSoundType) {
// 过滤空内容和不需要处理的播报类型
guard !soundString.isEmpty else { return }
switch soundStringType {
case .default:
// 默认导航提示(如"前方500米右转"等)
playSystemTTS(soundString)
case .navigationStart:
// 开始导航提示("开始导航"等)
playSystemTTS("准备出发," + soundString)
case .navigationEnd:
// 到达目的地提示
playSystemTTS(soundString + ",导航结束")
case .yaw:
// 偏航提示("您已偏航,正在重新规划路线"等)
playSystemTTS("注意:" + soundString)
case .interrupt:
// 重要打断播报(如急转弯警告等)
interruptCurrentSpeech()
playSystemTTS(soundString)
default:
// 其他未明确处理的类型使用默认播报
playSystemTTS(soundString)
}
}
public func playSystemTTS(_ text: String) {
let utterance = AVSpeechUtterance(string: text)
utterance.voice = AVSpeechSynthesisVoice(language: "zh-CN")
utterance.rate = 0.5 // 适当放慢语速
let synthesizer = AVSpeechSynthesizer()
synthesizer.speak(utterance)
}
public func interruptCurrentSpeech() {
AVSpeechSynthesizer().stopSpeaking(at: .immediate)
}
public func driveManager(_ driveManager: AMapNaviDriveManager, update naviInfo: AMapNaviInfo?) {
guard let naviInfo = naviInfo else { return }
let currentRoad = naviInfo.currentRoadName ?? "未知道路"
let nextRoad = naviInfo.nextRoadName ?? "未知道路"
let distance = naviInfo.routeRemainDistance
let time = naviInfo.routeRemainTime
print("""
当前道路: \(currentRoad)
下条道路: \(nextRoad)
剩余距离: \(distance)米
剩余时间: \(time)秒
""")
}
public func driveManager(_ driveManager: AMapNaviDriveManager, error: Error) {
isNavigating = false
print("Navigation error occurred: \(error.localizedDescription)")
}
public func driveManagerDidEndEmulatorNavi(_ driveManager: AMapNaviDriveManager) {
isNavigating = false
print("Emulator navigation ended")
}
deinit {
driveManager?.stopNavi()
driveManager?.delegate = nil
AMapNaviDriveManager.destroyInstance()
}
}

34
local_plugins/amap_service/ios/Classes/AmapServicePlugin.swift

@ -1,17 +1,45 @@
import Flutter
import UIKit
import AMapNaviKit
public class AmapServicePlugin: NSObject, FlutterPlugin {
private static var channel: FlutterMethodChannel?
public static func register(with registrar: FlutterPluginRegistrar) {
let channel = FlutterMethodChannel(name: "amap_service", binaryMessenger: registrar.messenger())
let channel = FlutterMethodChannel(name: "com.yunqiinnovation.amap_service", binaryMessenger: registrar.messenger())
let instance = AmapServicePlugin()
// 必须先设置隐私合规
DispatchQueue.main.async {
// 2. API Key
AMapServices.shared()?.apiKey = "aa60a03be9f6531bcc64029e60f95d03"
AMapServices.shared().enableHTTPS = true
// 导航SDK隐私合规(如果使用导航功能)
AMapNaviManagerConfig.shared().updatePrivacyShow(.didShow, privacyInfo: .didContain)
AMapNaviManagerConfig.shared().updatePrivacyAgree(.didAgree)
// 3. 打印验证(调试用)
print("高德SDK初始化完成")
}
registrar.addMethodCallDelegate(instance, channel: channel)
self.channel = channel
}
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
switch call.method {
case "getPlatformVersion":
result("iOS " + UIDevice.current.systemVersion)
case "open":
print("liwei------------------ 打开导航页面")
if let args = call.arguments as? [String: Any],
let startpos = args["startpos"] as? String,
let endpos = args["endpos"] as? String,
let ntype = args["ntype"] as? String{
print("liwei------------------ 打开导航页面1 \(startpos) \(endpos)")
DispatchQueue.main.async {
let navVC = AmapViewController(startpos: startpos, endpos: endpos)
navVC.modalPresentationStyle = .fullScreen
UIApplication.shared.keyWindow?.rootViewController?.present(navVC, animated: true)
}
result(nil)
} else {
result(FlutterError(code: "INVALID_ARGUMENT", message: "Missing 'open' map", details: nil))
}
default:
result(FlutterMethodNotImplemented)
}

231
local_plugins/amap_service/ios/Classes/AmapViewController.swift

@ -0,0 +1,231 @@
import UIKit
import AVFAudio
import AMapNaviKit
class AmapViewController: UIViewController {
// MARK: - 必须声明为强引用
private var driveManager: AMapNaviDriveManager!
private var driveView: AMapNaviDriveView!
private let speechSynthesizer = AVSpeechSynthesizer()
private var startPoint: AMapNaviPoint!
private var endPoint: AMapNaviPoint!
// 自定义初始化方法
init(startpos: String, endpos: String) {
super.init(nibName: nil, bundle: nil)
self.startPoint = AmapViewController.parseCoord(from: startpos)
self.endPoint = AmapViewController.parseCoord(from: endpos)
print("导航---------------------: \(startPoint) \(endPoint)")
}
// 解析 "lat,lng" -> AMapNaviPoint
private static func parseCoord(from string: String) -> AMapNaviPoint? {
let components = string.split(separator: ",")
guard components.count == 2,
let lng = Double(components[0]),
let lat = Double(components[1]) else {
print("❌ 无效坐标格式: \(string)")
return nil
}
return AMapNaviPoint.location(withLatitude: lat, longitude: lng)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
setupAMapComponents()
}
private func setupAMapComponents() {
// 1. 初始化 DriveView(必须在主线程)
print("导航------------------ setupAMapComponents_1")
DispatchQueue.main.async {
print("导航------------------ setupAMapComponents_2")
self.driveView = AMapNaviDriveView(frame: self.view.bounds)
print("导航------------------ setupAMapComponents_3")
self.driveView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
print("导航------------------ setupAMapComponents_4")
self.driveView.delegate = self
print("导航------------------ setupAMapComponents_5")
self.view.addSubview(self.driveView)
print("导航------------------ setupAMapComponents_6")
// 2. 初始化 DriveManager
self.driveManager = AMapNaviDriveManager.sharedInstance()
print("导航------------------ setupAMapComponents_7")
self.driveManager.delegate = self
print("导航------------------ setupAMapComponents_8")
self.driveManager.allowsBackgroundLocationUpdates = true
print("导航------------------ setupAMapComponents_9")
// 3. 确保对象非空再关联
if let manager = self.driveManager, let view = self.driveView {
print("导航------------------ setupAMapComponents_10")
manager.addDataRepresentative(view)
print("导航------------------ setupAMapComponents_11")
self.startNavigation()
print("导航------------------ setupAMapComponents_12")
} else {
print("Error: driveManager or driveView is nil")
}
print("导航------------------ setupAMapComponents_13")
}
}
private func startNavigation() {
// print("导航------------------ startNavigation_1")
// let startPoint = AMapNaviPoint.location(withLatitude: 39.989614, longitude: 116.481763)!
// print("导航------------------ startNavigation_2")
// let endPoint = AMapNaviPoint.location(withLatitude: 39.983456, longitude: 116.315495)!
// print("导航------------------ startNavigation_3")
driveManager.calculateDriveRoute(
withStart: [startPoint],
end: [endPoint],
wayPoints: nil,
drivingStrategy: .drivingStrategySingleDefault
)
print("导航------------------ startNavigation_4")
}
@objc private func exitNavigation() {
print("导航------------------ 退出导航,清理资源")
// 停止导航
driveManager.stopNavi()
driveManager.removeDataRepresentative(driveView)
driveManager.delegate = nil
// 移除导航视图
driveView.delegate = nil
driveView.removeFromSuperview()
driveView = nil
// 销毁导航管理器
driveManager = nil
// 关闭当前页面,视情况而定
// 如果你用的是导航控制器,可以pop
navigationController?.popViewController(animated: true)
// 或者如果是present出来的,dismiss
// dismiss(animated: true, completion: nil)
}
}
// MARK: - 代理方法
extension AmapViewController: AMapNaviDriveViewDelegate, AMapNaviDriveManagerDelegate {
//View回调
func driveViewCloseButtonClicked(_ driveView: AMapNaviDriveView) {
print("导航界面退出按钮被点击")
// 停止导航并清理
driveManager.stopNavi()
driveManager.removeDataRepresentative(driveView)
driveManager.delegate = nil
driveView.delegate = nil
driveView.removeFromSuperview()
// 返回到 Flutter 页面
self.dismiss(animated: true, completion: nil)
}
func driveView(_ driveView: AMapNaviDriveView, didChange showMode: AMapNaviDriveViewShowMode) {
print("导航界面显示模式改变:\(showMode.rawValue)")
}
func driveView(_ driveView: AMapNaviDriveView, didChange trackMode: AMapNaviViewTrackingMode) {
print("导航界面跟随模式改变:\(trackMode.rawValue)")
}
func driveView(_ driveView: AMapNaviDriveView, didChangeOrientation isLandscape: Bool) {
print("导航界面横竖屏切换:isLandscape = \(isLandscape)")
}
func driveView(_ driveView: AMapNaviDriveView, didChangeDayNightType showStandardNightType: Bool) {
print("导航界面白天/黑夜模式切换:isNight = \(showStandardNightType)")
}
func driveViewEdgePadding(_ driveView: AMapNaviDriveView) -> UIEdgeInsets {
// 你可以根据自己 UI 布局返回合适的内边距,防止遮挡
let padding = UIEdgeInsets(top: 100, left: 50, bottom: 80, right: 60)
print("请求导航界面可视区域边距:\(padding)")
return padding
}
//导航回调
func driveManager(_ driveManager: AMapNaviDriveManager, error: Error) {
print("导航------------------ ❌ 发生错误: \(error.localizedDescription)")
}
func driveManager(onCalculateRouteSuccess driveManager: AMapNaviDriveManager){
print("导航------------------ ✅ 路线规划成功(旧方法)")
driveManager.startGPSNavi()
}
func driveManager(_ driveManager: AMapNaviDriveManager, onCalculateRouteSuccessWith type: AMapNaviRoutePlanType) {
print("导航------------------ 路径规划成功,类型: \(type.rawValue)")
driveManager.startGPSNavi()
}
func driveManager(_ driveManager: AMapNaviDriveManager, onCalculateRouteFailure error: Error) {
print("导航------------------ ❌ 路线规划失败: \(error.localizedDescription)")
}
func driveManager(_ driveManager: AMapNaviDriveManager, onCalculateRouteFailure error: Error, routePlanType type: AMapNaviRoutePlanType) {
print("导航------------------ ❌ 路线规划失败(新方法): \(error.localizedDescription),类型: \(type.rawValue)")
}
func driveManager(_ driveManager: AMapNaviDriveManager, didStartNavi naviMode: AMapNaviMode) {
print("导航------------------ 🚗 已开始导航,模式: \(naviMode.rawValue)")
}
func driveManagerNeedRecalculateRoute(forYaw driveManager: AMapNaviDriveManager) {
print("导航------------------ 出现偏航,需要重新计算路径")
}
func driveManagerNeedRecalculateRoute(forTrafficJam driveManager: AMapNaviDriveManager) {
print("导航------------------ 遇到拥堵,重新规划路线")
}
func driveManager(_ driveManager: AMapNaviDriveManager, onArrivedWayPoint wayPointIndex: Int) {
print("导航------------------ 到达途经点索引:\(wayPointIndex)")
}
func driveManagerIsNaviSoundPlaying(_ driveManager: AMapNaviDriveManager) -> Bool {
print("导航------------------ 🔊 正在判断是否在播报语音")
return false // 如果你有 TTS 系统播报时需返回 true
}
func driveManager(_ driveManager: AMapNaviDriveManager, playNaviSound soundString: String, soundStringType: AMapNaviSoundType) {
print("导航------------------ 📢 导航播报: \(soundString),类型: \(soundStringType.rawValue)")
// 示例:使用 AVSpeechSynthesizer 播放语音
let utterance = AVSpeechUtterance(string: soundString)
utterance.voice = AVSpeechSynthesisVoice(language: "zh-CN")
speechSynthesizer.speak(utterance)
}
func driveManagerDidEndEmulatorNavi(_ driveManager: AMapNaviDriveManager) {
print("导航------------------ 🛑 模拟导航结束")
}
func driveManager(onArrivedDestination driveManager: AMapNaviDriveManager) {
print("导航------------------ 🎯 到达目的地")
}
func driveManager(_ driveManager: AMapNaviDriveManager, onNaviPlay ringType: AMapNaviRingType) {
print("导航------------------ 🔔 播放提示音,类型: \(ringType.rawValue)")
}
func driveManager(_ driveManager: AMapNaviDriveManager, update gpsSignalStrength: AMapNaviGPSSignalStrength) {
print("导航------------------ 📡 GPS信号强度: \(gpsSignalStrength.rawValue)")
}
func driveManager(_ driveManager: AMapNaviDriveManager, postRouteNotification notifyData: AMapNaviRouteNotifyData) {
print("导航------------------ 📌 路线通知信息: \(notifyData)")
}
func driveManager(_ driveManager: AMapNaviDriveManager, onSuggestChangeMainNaviRoute suggestChangeMainNaviRouteInfo: AMapNaviSuggestChangeMainNaviRouteInfo) {
print("导航------------------ 🔄 建议切换主导航路线: \(suggestChangeMainNaviRouteInfo)")
}
}

16
local_plugins/amap_service/ios/amap_service.podspec

@ -15,15 +15,13 @@ A new Flutter project.
s.source = { :path => '.' }
s.source_files = 'Classes/**/*'
s.dependency 'Flutter'
s.dependency 'AMapNavi', '~> 10.0.900'
s.static_framework = true
s.pod_target_xcconfig = {
'DEFINES_MODULE' => 'YES',
'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386'
}
s.platform = :ios, '12.0'
# Flutter.framework does not contain a i386 slice.
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' }
s.swift_version = '5.0'
# If your plugin requires a privacy manifest, for example if it uses any
# required reason APIs, update the PrivacyInfo.xcprivacy file to describe your
# plugin's privacy impact, and then uncomment this line. For more information,
# see https://developer.apple.com/documentation/bundleresources/privacy_manifest_files
# s.resource_bundles = {'amap_service_privacy' => ['Resources/PrivacyInfo.xcprivacy']}
end

32
local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiService.kt

@ -333,7 +333,7 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
chatsFlow.collect { result ->
if (isCanceled) return@collect
val choice = result.choices?.firstOrNull()
val choice = result.choices.firstOrNull()
if (choice == null) {
Log.e("ChatApiService", "choice为null")
return@collect
@ -352,33 +352,35 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
// 收集工具调用信息
delta.toolCalls?.forEach { toolCall ->
val id = toolCall.id ?: run {
Log.e("ChatApiService", "toolCall.id 为空,跳过本次处理")
return@forEach
}
Log.e("ChatApiService", "AI发起工具调用: ${toolCall.id} ${toolCall.index} ${toolCall.function?.name} ${toolCall.function?.arguments}")
val index = toolCall.index
// 创建或获取现有的工具调用信息
val toolCallInfo = toolCalls.getOrPut(index) { ToolCallInfo() }
// 安全处理工具调用ID
try {
toolCall.id?.let { id ->
toolCallInfo.id = id.toString()
toolCall.id?.let { i ->
toolCallInfo.id = i.toString()
}
} catch (e: Exception) {
Log.e("ChatApiService", "处理工具调用ID异常: ${e.message}")
}
// 安全处理函数信息
try {
toolCall.function?.let { function ->
try {
function.name?.let { name ->
function.name.let { name ->
toolCallInfo.name = name
}
} catch (e: Exception) {
Log.e("ChatApiService", "处理工具调用函数名称异常: ${e.message}")
Log.e("ChatApiService", "处理工具调用函数名称异常! function:${toolCall.function} err:${e.message}")
}
try {
function.arguments?.let { args ->
function.arguments.let { args ->
toolCallInfo.arguments += args
}
} catch (e: Exception) {
@ -417,11 +419,9 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
Log.e("ChatApiService", "查找有效工具调用异常: ${e.message}")
null
}
if (firstToolCall == null) {
return false
}
// 创建函数调用字典
val functionCall = try {
mapOf(
@ -433,10 +433,8 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
Log.e("ChatApiService", "创建函数调用字典异常: ${e.message}")
return false
}
// 通知上层工具调用事件
streamCallback?.onFunctionCall(convertMapToJsonObject(functionCall))
// 在后台队列处理工具调用
launch {
try {
@ -444,14 +442,13 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
// 通过MCP客户端处理工具调用
val functionName = firstToolCall.name
val argumentsJson = firstToolCall.arguments
val result = if (_mcpClient?.hasToolWithName(functionName) == true) {
// 解析参数
val arguments = _mcpClient?.parseJsonArguments(argumentsJson) ?: emptyMap()
// 调用MCP工具
val toolResult = _mcpClient?.callTool(functionName, arguments)
Log.e("ChatApiService", "AI调用工具结果: $functionName}:$arguments} toolResult:${toolResult}")
// 处理结果
if (toolResult != null) {
if (toolResult["isError"] == true) {
@ -478,14 +475,12 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
// 工具不存在
mapOf("context" to "Tool not found: $functionName")
}
if (!isCanceled) {
// 处理结果
streamCallback?.onFunctionCallResult(
convertMapToJsonObject(functionCall),
convertMapToJsonObject(result)
)
// 将结果发送回OpenAI继续对话
sendFunctionCallResultInternal(
messages = currentMessages,
@ -855,12 +850,11 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
nameOrNull = name,
argumentsOrNull = arguments
)
val toolCall = ToolCall.Function(
id = ToolId(id),
function = functionCall
)
toolCallsList.add(toolCall)
}
}

6
local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/CustomSseClientTransport.kt

@ -107,7 +107,9 @@ class CustomSseClientTransport(
"open" -> {
// SSE连接已打开
}
"ping" -> {
// 心跳
}
"endpoint" -> {
try {
val eventData = event.data ?: ""
@ -234,7 +236,7 @@ class CustomSseClientTransport(
if (!response.status.isSuccess()) {
val text = response.bodyAsText()
Log.e(TAG, "发送消息失败: HTTP ${response.status}, $text")
Log.e(TAG, "发送消息失败:URL:${urlString} HTTP ${response.status}, $text")
error("Error POSTing to endpoint (HTTP ${response.status}): $text")
}
} catch (e: Exception) {

5
local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/MCPClient.kt

@ -91,9 +91,9 @@ class MCPClient(private val context: Context? = null) : AutoCloseable {
val serverConfig = mcpServers.optJSONObject(serverId) ?: continue
val url = serverConfig.optString("url", "")
val tools = serverConfig.optString("tools", "")
if (url.isEmpty()) continue
val subClient = MCPSubClient(serverId, url, sharedHttpClient)
val subClient = MCPSubClient(serverId, url,tools, sharedHttpClient)
if (subClient.connect()) {
subClients[serverId] = subClient
@ -369,4 +369,5 @@ class MCPClient(private val context: Context? = null) : AutoCloseable {
return list
}
}

125
local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/MCPSubClient.kt

@ -1,6 +1,7 @@
package com.yunqiinnovation.chat_api
import android.util.Log
import com.google.gson.GsonBuilder
import io.ktor.client.*
import io.modelcontextprotocol.kotlin.sdk.*
import io.modelcontextprotocol.kotlin.sdk.client.*
@ -21,13 +22,23 @@ import kotlin.collections.mutableListOf
class MCPSubClient(
private val serverId: String,
private val serverUrl: String,
private val filterTools: String,
private val httpClient: HttpClient? = null
) : AutoCloseable {
companion object {
private const val TAG = "MCPSubClient"
}
// 心跳检测
private var heartbeatJob: Job? = null
private val heartbeatInterval = 30000L // 30秒
// 重连配置
private val maxRetryAttempts = 3
private val initialReconnectDelay = 1000L // 初始重连延迟1秒
private val maxReconnectDelay = 30000L // 最大重连延迟30秒
private var currentReconnectDelay = initialReconnectDelay
private var retryCount = 0
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
private val connectionMutex = Mutex()
private var mcpClient: Client? = null
@ -73,7 +84,11 @@ class MCPSubClient(
val toolsResult = client.listTools()
if (toolsResult != null) {
availableTools.clear()
availableTools.addAll(toolsResult.tools)
val filtered = toolsResult.tools.filter { tool ->
filterTools.isEmpty() || filterTools.contains(tool.name)
}
Log.w(TAG, "[$serverId] [${filterTools}] 获取工具列表: ${filtered} 原始列表:${toolsResult.tools}")
availableTools.addAll(filtered)
}
} catch (e: Exception) {
Log.w(TAG, "[$serverId] 获取工具列表失败: ${e.message}")
@ -82,6 +97,11 @@ class MCPSubClient(
mcpClient = client
isConnected = true
retryCount = 0
currentReconnectDelay = initialReconnectDelay
// 启动心跳检测
// startHeartbeat()
true
} catch (e: Exception) {
@ -129,8 +149,15 @@ class MCPSubClient(
* 调用MCP工具
*/
suspend fun callTool(name: String, arguments: Map<String, Any>): Map<String, Any>? {
val client = mcpClient ?: return null
// 先检查连接状态
if (!checkConnection()) {
Log.e(TAG, "无法连接到服务器,工具调用失败")
return null
}
if (!containsTool(name)) {
Log.w(TAG, "此客户端不包含工具: $name")
return null
}
return try {
// 创建工具调用请求 - 将Map转换为JsonObject
val argumentsJson = kotlinx.serialization.json.buildJsonObject {
@ -150,7 +177,7 @@ class MCPSubClient(
)
// 调用工具
val result = client.callTool(request)
val result = mcpClient?.callTool(request)
result?.let { callResult ->
// 将结果转换为统一格式
@ -158,7 +185,7 @@ class MCPSubClient(
// 根据不同的内容类型处理
mapOf(
"type" to "text",
"text" to (contentItem.toString())
"text" to (contentItem.toJsonString())
)
}
@ -180,6 +207,20 @@ class MCPSubClient(
}
}
// 外部扩展函数
fun PromptMessageContent.toJsonString(indent: Int = 0): String {
val gson = GsonBuilder()
.setPrettyPrinting()
.create()
return if (indent > 0) {
gson.toJson(this)
} else {
// 移除空格和换行符
gson.toJson(this).replace("\\s+".toRegex(), "")
}
}
/**
* 将Tool.Input转换为Map格式,供OpenAI使用
*/
@ -275,7 +316,77 @@ class MCPSubClient(
false
}
}
/**
* 检查连接状态并自动重连
*/
suspend fun checkConnection(): Boolean {
if (!isConnected) {
Log.d(TAG, "当前未连接,尝试重新连接...")
return connect()
}
try {
// 简单ping测试连接状态
mcpClient?.ping()
return true
} catch (e: Exception) {
Log.e(TAG, "连接检查失败: ${e.message}")
isConnected = false
return false
}
}
/**
* 停止心跳检测
*/
private fun stopHeartbeat() {
heartbeatJob?.cancel()
heartbeatJob = null
}
/**
* 启动心跳检测
*/
private fun startHeartbeat() {
heartbeatJob?.cancel()
heartbeatJob = CoroutineScope(Dispatchers.IO).launch {
while (isActive && isConnected) {
try {
delay(heartbeatInterval)
// 发送心跳请求
val startTime = System.currentTimeMillis()
val response = mcpClient?.ping()
val latency = System.currentTimeMillis() - startTime
Log.d(TAG, "心跳检测成功,延迟: ${latency}ms")
} catch (e: Exception) {
Log.e(TAG, "心跳检测失败: ${e.message}")
handleConnectionError(e)
break
}
}
}
}
/**
* 处理连接错误并尝试重连
*/
private suspend fun handleConnectionError(e: Exception) {
isConnected = false
stopHeartbeat()
if (retryCount < maxRetryAttempts) {
retryCount++
currentReconnectDelay = minOf(currentReconnectDelay * 2, maxReconnectDelay)
Log.w(TAG, "连接失败,将在 ${currentReconnectDelay}ms 后尝试重连 (尝试 $retryCount/$maxRetryAttempts)")
delay(currentReconnectDelay)
connect()
} else {
Log.e(TAG, "已达到最大重试次数($maxRetryAttempts),停止重连")
// 可以在这里添加通知或回调,告知上层连接彻底失败
}
}
/**
* 关闭连接
*/

12
local_plugins/chat_api/ios/chat_api/Sources/chat_api/SystemFunctionHandler.swift

@ -606,18 +606,18 @@ private class GetCurrentTimeHandler: FunctionHandler {
/// 获取当前位置处理器
private class GetCurrentLocationHandler: FunctionHandler {
private weak var systemHandler: SystemFunctionHandler?
private let systemHandler: SystemFunctionHandler
init(systemHandler: SystemFunctionHandler) {
self.systemHandler = systemHandler
}
func handle(arguments: [String: Any]) async throws -> String {
guard let systemHandler = systemHandler else {
throw ChatApiException("系统处理器未初始化")
}
// guard let systemHandler = systemHandler else {
// throw ChatApiException("系统处理器未初始化")
// }
let result = await systemHandler.getCurrentLocation()
let result: Result = await systemHandler.getCurrentLocation()
return "{\"result\": \"\(result.message)\", \"success\": \(result.success)}"
}
}
@ -666,4 +666,4 @@ private class OpenRecorderHandler: FunctionHandler {
return "{\"result\": \"无法打开录音机应用\"}"
}
}
}

Loading…
Cancel
Save