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

24
ios/Runner/Info.plist

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

2
lib/data/models/appconfig_model.dart

@ -52,7 +52,7 @@ class DBAgent {
class DBMCPServer { class DBMCPServer {
final String servername; final String servername;
final String url; final String url;
final String? tools; final String tools;
DBMCPServer({ DBMCPServer({
required this.servername, required this.servername,
required this.url, 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( DBMCPServer _$DBMCPServerFromJson(Map<String, dynamic> json) => DBMCPServer(
servername: json['servername'] as String, servername: json['servername'] as String,
url: json['url'] as String, url: json['url'] as String,
tools: json['tools'] as String?, tools: json['tools'] as String? ?? '',
); );
Map<String, dynamic> _$DBMCPServerToJson(DBMCPServer instance) => 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:amap_service/amap_service.dart';
import 'package:deep_voice/data/services/music_manager.dart'; import 'package:deep_voice/data/services/music_manager.dart';
import 'package:deep_voice/data/services/navigation_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:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:agent_service/agent_service.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:image_picker/image_picker.dart';
import 'package:get_storage/get_storage.dart'; import 'package:get_storage/get_storage.dart';
import 'package:permission_handler/permission_handler.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:deep_voice/core/utils/logger.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:photo_view/photo_view.dart'; import 'package:photo_view/photo_view.dart';
import '../../../core/utils/permission_util.dart'; import '../../../core/utils/permission_util.dart';
import '../../../data/models/message_model.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();
}
class AgentController extends GetxController { class AgentController extends GetxController {
static AgentController get to => Get.find(); //单例访问模式 static AgentController get to => Get.find(); //单例访问模式
@ -214,7 +192,7 @@ class AgentController extends GetxController {
} }
} }
// 添加欢迎消息 // 添加初始欢迎消息
void _addWelcomeMessage() { void _addWelcomeMessage() {
messages.add(Message( messages.add(Message(
isUser: false, isUser: false,
@ -418,7 +396,7 @@ class AgentController extends GetxController {
break; break;
case AgentServiceEventType.functionCallResult: case AgentServiceEventType.functionCallResult:
final responseId = event.data['responseId'] ?? ''; final responseId = event.data['responseId'] ?? '';
// final result = event.data['result']; // final result = event.data['result'];
final metaStr = event.data['meta'] ?? ''; final metaStr = event.data['meta'] ?? '';
// final functionCall = event.data['function_call'] ?? ''; // 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) { if (isImageInputActive.value && selectedImagePath.value != null) {
final text = textController.text.trim(); final text = textController.text.trim();
final imagePath = selectedImagePath.value!; // 使用!强制断言非空 final imagePath = selectedImagePath.value!; // 使用!强制断言非空
@ -533,6 +513,7 @@ class AgentController extends GetxController {
return; return;
} }
// 2. 处理纯文本消息
final text = textController.text.trim(); final text = textController.text.trim();
if (text.isEmpty) return; if (text.isEmpty) return;
@ -541,7 +522,7 @@ class AgentController extends GetxController {
textController.clear(); textController.clear();
// 使用reverse:true时通常不需要手动滚动 // 使用reverse:true时通常不需要手动滚动
// 3. 调用AI服务处理
try { try {
isProcessing.value = true; isProcessing.value = true;
// 标记为新的AI回复 // 标记为新的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 { 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 { Future<void> startVoiceInput() async {
if (isListening.value) return; if (isListening.value) return;

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

@ -1,7 +1,8 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'agent_controller.dart'; // 确保导入Message模型 //import 'agent_controller.dart'; // 确保导入Message模型
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../../../data/services/network/api.dart'; import '../../../data/services/network/api.dart';
import '../../../data/models/message_model.dart';
class ReportController extends GetxController { 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 'package:flutter_screenutil/flutter_screenutil.dart';
import '../../../core/theme/app_colors.dart'; import '../../../core/theme/app_colors.dart';
import 'package:haptic_feedback/haptic_feedback.dart'; import 'package:haptic_feedback/haptic_feedback.dart';
import '../../../data/models/message_model.dart';
class AgentView extends StatelessWidget { class AgentView extends StatelessWidget {
const AgentView({Key? key}) : super(key: key); const AgentView({Key? key}) : super(key: key);
@ -384,12 +385,12 @@ class AgentView extends StatelessWidget {
vertical: isImageActive ? 12 : 0), vertical: isImageActive ? 12 : 0),
), ),
onSubmitted: (_) => onSubmitted: (_) =>
controller.sendTextMessage(), controller.sendMessage(),
); );
}), }),
), ),
GestureDetector( GestureDetector(
onTap: () => controller.sendTextMessage(), onTap: () => controller.sendMessage(),
child: Container( child: Container(
padding: const EdgeInsets.all(6), padding: const EdgeInsets.all(6),
child: Icon( 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(); rawList.map((e) => Map<String, dynamic>.from(e)).toList();
//音乐播放列表 //音乐播放列表
return buildMusicContent(context, playlist); return buildMusicContent(context, playlist);
} else if (meta!.keys.contains('card_weather')) { } else if (meta!.keys.contains('maps_weather')) {
final city = meta!['card_weather']['city'] as String; final city = meta!['maps_weather']['city'] as String;
final rawList = meta!['card_weather']['dayweather'] as List; final rawList = meta!['maps_weather']['forecasts'] as List;
final daylist = final daylist =
rawList.map((e) => Map<String, dynamic>.from(e)).toList(); 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( return buildWeatherContent(
context, context,

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

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

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

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

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

@ -99,11 +99,13 @@ class PairingView extends GetView<PairingController> {
return Obx(() { return Obx(() {
final state = controller.state; final state = controller.state;
final isLoading = controller.isLoading.value; final isLoading = controller.isLoading.value;
final isReadyToStart = controller.isReadyToStart.value;
final isManualMode = controller.isManualMode.value;
// 根据状态决定动画颜色 // 根据状态决定动画颜色
Color primaryColor = Color(0xFF0066CC); Color primaryColor = Color(0xFF0066CC);
Color secondaryColor = Color(0xFF5E5CE6); Color secondaryColor = Color(0xFF5E5CE6);
// 错误状态使用红色 // 错误状态使用红色
if (state.name.startsWith('exit')) { if (state.name.startsWith('exit')) {
primaryColor = Colors.red; primaryColor = Colors.red;
@ -114,7 +116,54 @@ class PairingView extends GetView<PairingController> {
primaryColor = Colors.green; primaryColor = Colors.green;
secondaryColor = Colors.greenAccent; 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( return Container(
width: 240.w, width: 240.w,
height: 240.w, height: 240.w,
@ -239,11 +288,11 @@ class PairingView extends GetView<PairingController> {
), ),
// 当前连接设备(有设备时才显示) // 当前连接设备(有设备时才显示)
if (device != null && if (device != null &&
(state == PairingState.classicOk || (state == PairingState.classicOk ||
state == PairingState.serverVerify || state == PairingState.serverVerify ||
state == PairingState.pairing || state == PairingState.pairing ||
state == PairingState.paired)) ...[ state == PairingState.paired)) ...[
SizedBox(height: 24.h), SizedBox(height: 24.h),
_buildConnectedDevice(device, isDarkMode), _buildConnectedDevice(device, isDarkMode),
], ],
@ -254,10 +303,14 @@ class PairingView extends GetView<PairingController> {
// 根据状态获取副标题 // 根据状态获取副标题
String _getSubtitleForState(PairingState state) { String _getSubtitleForState(PairingState state) {
if (controller.isManualMode.value && controller.isReadyToStart.value) {
return '点击下方按钮开始配对您的DeapSound设备';
}
switch (state) { switch (state) {
case PairingState.init: case PairingState.init:
case PairingState.btReady: case PairingState.btReady:
return '正在准备配对环境...'; return controller.isManualMode.value ? '准备开始配对' : '正在准备配对环境...';
case PairingState.classicOk: case PairingState.classicOk:
return '请先通过系统蓝牙连接DeapSound设备'; return '请先通过系统蓝牙连接DeapSound设备';
case PairingState.serverVerify: case PairingState.serverVerify:
@ -364,17 +417,50 @@ class PairingView extends GetView<PairingController> {
final showActionButton = controller.showActionButton; final showActionButton = controller.showActionButton;
final canSkip = controller.canSkip; final canSkip = controller.canSkip;
final actionButtonText = controller.actionButtonText.value; final actionButtonText = controller.actionButtonText.value;
final isReadyToStart = controller.isReadyToStart.value;
final isManualMode = controller.isManualMode.value;
return Container( return Container(
width: double.infinity, width: double.infinity,
child: Column( child: Column(
children: [ 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( ElevatedButton(
onPressed: () => controller.handleActionButtonTap(), onPressed: () => controller.handleActionButtonTap(),
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: _getButtonColor(controller.state, isDarkMode), backgroundColor:
_getButtonColor(controller.state, isDarkMode),
foregroundColor: Colors.white, foregroundColor: Colors.white,
padding: EdgeInsets.symmetric(vertical: 16.h), padding: EdgeInsets.symmetric(vertical: 16.h),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
@ -392,7 +478,7 @@ class PairingView extends GetView<PairingController> {
), ),
), ),
// 只在可以跳过时显示跳过按钮 // 跳过按钮和重置按钮
if (canSkip) ...[ if (canSkip) ...[
SizedBox(height: 12.h), SizedBox(height: 12.h),
TextButton( 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() { override fun onComplete() {
// 视情况决定是否朗读回复 // 视情况决定是否朗读回复
if (speakResponse) { if (speakResponse && !nobroadcast) {
ttsService?.flushStream() ttsService?.flushStream()
} }
val response = responseBuilder.toString() val response = responseBuilder.toString()
@ -731,13 +731,16 @@ object AgentService : CoroutineScope {
override fun onFunctionCallResult(functionCall: JSONObject, functionCallResult: JSONObject) { override fun onFunctionCallResult(functionCall: JSONObject, functionCallResult: JSONObject) {
audioPlayer?.stopAudio() audioPlayer?.stopAudio()
val name = functionCall.get("name") as String;
val (metestr, broadcast)= autoHandleFcunCallResult(name,functionCallResult);
aiMetadata = metestr;
nobroadcast = broadcast
sendEvent("function_call_result", mapOf( sendEvent("function_call_result", mapOf(
"function_call" to functionCall.toString(), "function_call" to functionCall.toString(),
"result" to functionCallResult.toString(), "result" to functionCallResult.toString(),
"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>{ fun autoHandleFcunCallResult(toolname:String,functionCallResult: JSONObject): Pair<String, Boolean>{
val metaStr = functionCallResult.optString("meta") println("协议工具返回数据 检查是否存在卡片! functionCallResult: ${functionCallResult}")
var broadcast = false; try {
if (metaStr.isNotEmpty()) { val contentStr = functionCallResult.optString("context")
val meta = JSONObject(metaStr) println("协议工具返回数据 检查是否存在卡片! context: ${contentStr}")
// Log.d(TAG, "mcp调用结果: $metaStr") val content = JSONObject(contentStr)
if (meta.has("card_music")) { //音乐卡片 val textStr = content.optString("text")
val cardMusic = meta.getJSONObject("card_music") println("协议工具返回数据 检查是否存在卡片! textStr: ${textStr}")
val id = cardMusic.optString("id", "") val metadata: MutableMap<String, Any> = mutableMapOf()
val url = cardMusic.optString("url", "") var broadcast = false;
val name = cardMusic.optString("name", "") println("协议工具返回数据 检查是否存在卡片!")
val sgener = cardMusic.optString("sgener", "") if (textStr.isNotEmpty()) {
val image = cardMusic.optString("image", "") val meta = JSONObject(textStr)
processMusicPlay(mapOf("id" to id,"url" to url, "title" to name, "artist" to sgener,"coverUrl" to image)) metadata[toolname] = meta // 现在可以赋值
broadcast = true; val metaStr = JSONObject(metadata as Map<*, *>).toString()
}else if (meta.has("card_musiclist")) { //音乐列表 // Log.d(TAG, "mcp调用结果: $metaStr")
val cardMusiclist = meta.getJSONObject("card_musiclist") if (meta.has("card_music")) { //音乐卡片
val musics = cardMusiclist.optJSONArray("musics") val cardMusic = meta.getJSONObject("card_music")
if (musics != null) { val id = cardMusic.optString("id", "")
val playlist = mutableListOf<Map<String, String>>() val url = cardMusic.optString("url", "")
for (i in 0 until musics.length()) { val name = cardMusic.optString("name", "")
val item = musics.optJSONObject(i) ?: continue val sgener = cardMusic.optString("sgener", "")
val id = item.optString("id", "") val image = cardMusic.optString("image", "")
val url = item.optString("url", "") processMusicPlay(
val name = item.optString("name", "") mapOf(
val sgener = item.optString("sgener", "") "id" to id,
val image = item.optString("image", "") "url" to url,
playlist.add(mapOf("id" to id,"url" to url, "title" to name, "artist" to sgener,"coverUrl" to image)) "title" to name,
} "artist" to sgener,
if (playlist.isNotEmpty()) { "coverUrl" to image
broadcast = true; )
processMusicPlayList(playlist) )
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 { } else {
Log.w(TAG, "card_musiclist 中没有有效的音乐条目") Log.w(TAG, "card_musiclist.musics 不是一个数组")
} }
} else { } else if (meta.has("card_navigation")) { //导航服务
Log.w(TAG, "card_musiclist.musics 不是一个数组") 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")) { //导航服务 return Pair(metaStr, broadcast);
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); } catch (e: Exception) {
// 捕获其他类型的异常(可选)
println("为解析到卡片数据!")
} }
return Pair("", false); return Pair("", false);
} }
@ -1167,6 +1201,7 @@ object AgentService : CoroutineScope {
*/ */
fun processMusicPlay(song: Map<String,String>){ fun processMusicPlay(song: Map<String,String>){
// 在其他 Service、BroadcastReceiver 或 Application 中调用 // 在其他 Service、BroadcastReceiver 或 Application 中调用
Log.i(TAG, "播放音乐: ${song}")
MusicServiceStarter.startServiceWithCommand(context, command = "play", song = song) MusicServiceStarter.startServiceWithCommand(context, command = "play", song = song)
} }
@ -1184,7 +1219,7 @@ object AgentService : CoroutineScope {
if (songs.isNotEmpty()) { if (songs.isNotEmpty()) {
MusicServiceStarter.startServiceWithPlaylist(context, command = "playlist", songs = songs) MusicServiceStarter.startServiceWithPlaylist(context, command = "playlist", songs = songs)
} else { } 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 Flutter
import UIKit import UIKit
import AMapNaviKit
public class AmapServicePlugin: NSObject, FlutterPlugin { public class AmapServicePlugin: NSObject, FlutterPlugin {
private static var channel: FlutterMethodChannel?
public static func register(with registrar: FlutterPluginRegistrar) { 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() 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) registrar.addMethodCallDelegate(instance, channel: channel)
self.channel = channel
} }
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
switch call.method { switch call.method {
case "getPlatformVersion": case "open":
result("iOS " + UIDevice.current.systemVersion) 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: default:
result(FlutterMethodNotImplemented) 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 = { :path => '.' }
s.source_files = 'Classes/**/*' s.source_files = 'Classes/**/*'
s.dependency 'Flutter' 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' 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' 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 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 -> chatsFlow.collect { result ->
if (isCanceled) return@collect if (isCanceled) return@collect
val choice = result.choices?.firstOrNull() val choice = result.choices.firstOrNull()
if (choice == null) { if (choice == null) {
Log.e("ChatApiService", "choice为null") Log.e("ChatApiService", "choice为null")
return@collect return@collect
@ -352,33 +352,35 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
// 收集工具调用信息 // 收集工具调用信息
delta.toolCalls?.forEach { toolCall -> 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 index = toolCall.index
// 创建或获取现有的工具调用信息 // 创建或获取现有的工具调用信息
val toolCallInfo = toolCalls.getOrPut(index) { ToolCallInfo() } val toolCallInfo = toolCalls.getOrPut(index) { ToolCallInfo() }
// 安全处理工具调用ID // 安全处理工具调用ID
try { try {
toolCall.id?.let { id -> toolCall.id?.let { i ->
toolCallInfo.id = id.toString() toolCallInfo.id = i.toString()
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e("ChatApiService", "处理工具调用ID异常: ${e.message}") Log.e("ChatApiService", "处理工具调用ID异常: ${e.message}")
} }
// 安全处理函数信息 // 安全处理函数信息
try { try {
toolCall.function?.let { function -> toolCall.function?.let { function ->
try { try {
function.name?.let { name -> function.name.let { name ->
toolCallInfo.name = name toolCallInfo.name = name
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e("ChatApiService", "处理工具调用函数名称异常: ${e.message}") Log.e("ChatApiService", "处理工具调用函数名称异常! function:${toolCall.function} err:${e.message}")
} }
try { try {
function.arguments?.let { args -> function.arguments.let { args ->
toolCallInfo.arguments += args toolCallInfo.arguments += args
} }
} catch (e: Exception) { } catch (e: Exception) {
@ -417,11 +419,9 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
Log.e("ChatApiService", "查找有效工具调用异常: ${e.message}") Log.e("ChatApiService", "查找有效工具调用异常: ${e.message}")
null null
} }
if (firstToolCall == null) { if (firstToolCall == null) {
return false return false
} }
// 创建函数调用字典 // 创建函数调用字典
val functionCall = try { val functionCall = try {
mapOf( mapOf(
@ -433,10 +433,8 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
Log.e("ChatApiService", "创建函数调用字典异常: ${e.message}") Log.e("ChatApiService", "创建函数调用字典异常: ${e.message}")
return false return false
} }
// 通知上层工具调用事件 // 通知上层工具调用事件
streamCallback?.onFunctionCall(convertMapToJsonObject(functionCall)) streamCallback?.onFunctionCall(convertMapToJsonObject(functionCall))
// 在后台队列处理工具调用 // 在后台队列处理工具调用
launch { launch {
try { try {
@ -444,14 +442,13 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
// 通过MCP客户端处理工具调用 // 通过MCP客户端处理工具调用
val functionName = firstToolCall.name val functionName = firstToolCall.name
val argumentsJson = firstToolCall.arguments val argumentsJson = firstToolCall.arguments
val result = if (_mcpClient?.hasToolWithName(functionName) == true) { val result = if (_mcpClient?.hasToolWithName(functionName) == true) {
// 解析参数 // 解析参数
val arguments = _mcpClient?.parseJsonArguments(argumentsJson) ?: emptyMap() val arguments = _mcpClient?.parseJsonArguments(argumentsJson) ?: emptyMap()
// 调用MCP工具 // 调用MCP工具
val toolResult = _mcpClient?.callTool(functionName, arguments) val toolResult = _mcpClient?.callTool(functionName, arguments)
Log.e("ChatApiService", "AI调用工具结果: $functionName}:$arguments} toolResult:${toolResult}")
// 处理结果 // 处理结果
if (toolResult != null) { if (toolResult != null) {
if (toolResult["isError"] == true) { 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") mapOf("context" to "Tool not found: $functionName")
} }
if (!isCanceled) { if (!isCanceled) {
// 处理结果 // 处理结果
streamCallback?.onFunctionCallResult( streamCallback?.onFunctionCallResult(
convertMapToJsonObject(functionCall), convertMapToJsonObject(functionCall),
convertMapToJsonObject(result) convertMapToJsonObject(result)
) )
// 将结果发送回OpenAI继续对话 // 将结果发送回OpenAI继续对话
sendFunctionCallResultInternal( sendFunctionCallResultInternal(
messages = currentMessages, messages = currentMessages,
@ -855,12 +850,11 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
nameOrNull = name, nameOrNull = name,
argumentsOrNull = arguments argumentsOrNull = arguments
) )
val toolCall = ToolCall.Function( val toolCall = ToolCall.Function(
id = ToolId(id), id = ToolId(id),
function = functionCall function = functionCall
) )
toolCallsList.add(toolCall) 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" -> { "open" -> {
// SSE连接已打开 // SSE连接已打开
} }
"ping" -> {
// 心跳
}
"endpoint" -> { "endpoint" -> {
try { try {
val eventData = event.data ?: "" val eventData = event.data ?: ""
@ -234,7 +236,7 @@ class CustomSseClientTransport(
if (!response.status.isSuccess()) { if (!response.status.isSuccess()) {
val text = response.bodyAsText() 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") error("Error POSTing to endpoint (HTTP ${response.status}): $text")
} }
} catch (e: Exception) { } 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 serverConfig = mcpServers.optJSONObject(serverId) ?: continue
val url = serverConfig.optString("url", "") val url = serverConfig.optString("url", "")
val tools = serverConfig.optString("tools", "")
if (url.isEmpty()) continue if (url.isEmpty()) continue
val subClient = MCPSubClient(serverId, url,tools, sharedHttpClient)
val subClient = MCPSubClient(serverId, url, sharedHttpClient)
if (subClient.connect()) { if (subClient.connect()) {
subClients[serverId] = subClient subClients[serverId] = subClient
@ -369,4 +369,5 @@ class MCPClient(private val context: Context? = null) : AutoCloseable {
return list 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 package com.yunqiinnovation.chat_api
import android.util.Log import android.util.Log
import com.google.gson.GsonBuilder
import io.ktor.client.* import io.ktor.client.*
import io.modelcontextprotocol.kotlin.sdk.* import io.modelcontextprotocol.kotlin.sdk.*
import io.modelcontextprotocol.kotlin.sdk.client.* import io.modelcontextprotocol.kotlin.sdk.client.*
@ -21,13 +22,23 @@ import kotlin.collections.mutableListOf
class MCPSubClient( class MCPSubClient(
private val serverId: String, private val serverId: String,
private val serverUrl: String, private val serverUrl: String,
private val filterTools: String,
private val httpClient: HttpClient? = null private val httpClient: HttpClient? = null
) : AutoCloseable { ) : AutoCloseable {
companion object { companion object {
private const val TAG = "MCPSubClient" 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 scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
private val connectionMutex = Mutex() private val connectionMutex = Mutex()
private var mcpClient: Client? = null private var mcpClient: Client? = null
@ -73,7 +84,11 @@ class MCPSubClient(
val toolsResult = client.listTools() val toolsResult = client.listTools()
if (toolsResult != null) { if (toolsResult != null) {
availableTools.clear() 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) { } catch (e: Exception) {
Log.w(TAG, "[$serverId] 获取工具列表失败: ${e.message}") Log.w(TAG, "[$serverId] 获取工具列表失败: ${e.message}")
@ -82,6 +97,11 @@ class MCPSubClient(
mcpClient = client mcpClient = client
isConnected = true isConnected = true
retryCount = 0
currentReconnectDelay = initialReconnectDelay
// 启动心跳检测
// startHeartbeat()
true true
} catch (e: Exception) { } catch (e: Exception) {
@ -129,8 +149,15 @@ class MCPSubClient(
* 调用MCP工具 * 调用MCP工具
*/ */
suspend fun callTool(name: String, arguments: Map<String, Any>): Map<String, Any>? { 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 { return try {
// 创建工具调用请求 - 将Map转换为JsonObject // 创建工具调用请求 - 将Map转换为JsonObject
val argumentsJson = kotlinx.serialization.json.buildJsonObject { 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 -> result?.let { callResult ->
// 将结果转换为统一格式 // 将结果转换为统一格式
@ -158,7 +185,7 @@ class MCPSubClient(
// 根据不同的内容类型处理 // 根据不同的内容类型处理
mapOf( mapOf(
"type" to "text", "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使用 * 将Tool.Input转换为Map格式,供OpenAI使用
*/ */
@ -275,7 +316,77 @@ class MCPSubClient(
false 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 class GetCurrentLocationHandler: FunctionHandler {
private weak var systemHandler: SystemFunctionHandler? private let systemHandler: SystemFunctionHandler
init(systemHandler: SystemFunctionHandler) { init(systemHandler: SystemFunctionHandler) {
self.systemHandler = systemHandler self.systemHandler = systemHandler
} }
func handle(arguments: [String: Any]) async throws -> String { func handle(arguments: [String: Any]) async throws -> String {
guard let systemHandler = systemHandler else { // guard let systemHandler = systemHandler else {
throw ChatApiException("系统处理器未初始化") // throw ChatApiException("系统处理器未初始化")
} // }
let result = await systemHandler.getCurrentLocation() let result: Result = await systemHandler.getCurrentLocation()
return "{\"result\": \"\(result.message)\", \"success\": \(result.success)}" return "{\"result\": \"\(result.message)\", \"success\": \(result.success)}"
} }
} }
@ -666,4 +666,4 @@ private class OpenRecorderHandler: FunctionHandler {
return "{\"result\": \"无法打开录音机应用\"}" return "{\"result\": \"无法打开录音机应用\"}"
} }
} }

Loading…
Cancel
Save