From 38c5ab1bd4fa76df4fb5cf10b24e2e6b8bfe5c48 Mon Sep 17 00:00:00 2001 From: liwei1dao Date: Thu, 12 Jun 2025 12:54:13 +0800 Subject: [PATCH 1/6] =?UTF-8?q?=E4=B8=8A=E4=BC=A0iOS=E5=AF=BC=E8=88=AA?= =?UTF-8?q?=E6=9C=8D=E5=8A=A1=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ios/Podfile.lock | 10 +- ios/Runner/Info.plist | 24 +- .../amap_service/ios/Classes/AmapHelper.swift | 42 ++++ .../ios/Classes/AmapService.swift | 165 +++++++++++++ .../ios/Classes/AmapServicePlugin.swift | 34 ++- .../ios/Classes/AmapViewController.swift | 231 ++++++++++++++++++ .../amap_service/ios/amap_service.podspec | 16 +- .../chat_api/SystemFunctionHandler.swift | 12 +- 8 files changed, 504 insertions(+), 30 deletions(-) create mode 100644 local_plugins/amap_service/ios/Classes/AmapHelper.swift create mode 100644 local_plugins/amap_service/ios/Classes/AmapService.swift create mode 100644 local_plugins/amap_service/ios/Classes/AmapViewController.swift diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 890b9e1a7..dda0379b0 100644 --- a/ios/Podfile.lock +++ b/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 diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index b7e7fc98f..bc3f4a793 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -76,10 +76,6 @@ EFAB - NSAccessorySetupKitSupports - - Bluetooth - NSAccessorySetupDescriptions @@ -89,7 +85,10 @@ 语音助理耳机 - + NSAccessorySetupKitSupports + + Bluetooth + NSAppTransportSecurity NSAllowsArbitraryLoads @@ -98,7 +97,7 @@ NSExceptionDomains - mcp.ideapsound.com + mcp.amap.com NSExceptionAllowsInsecureHTTPLoads @@ -107,7 +106,7 @@ NSIncludesSubdomains - mcp.amap.com + mcp.ideapsound.com NSExceptionAllowsInsecureHTTPLoads @@ -122,24 +121,27 @@ 需要蓝牙权限用于连接和管理音频设备 NSBluetoothPeripheralUsageDescription 需要蓝牙权限用于连接和管理音频设备 + NSCalendarsUsageDescription + 需要日历权限用于安排和管理您的音频会议和录音任务 + NSCameraUsageDescription + 需要相机权限用于扫描二维码和视频通话功能 NSLocationAlwaysAndWhenInUseUsageDescription 语音助手需要位置权限来为您提供基于位置的信息和服务 NSLocationWhenInUseUsageDescription 语音助手需要位置权限来为您提供基于位置的信息和服务 NSMicrophoneUsageDescription 需要麦克风权限用于语音识别和录音功能 - NSCalendarsUsageDescription - 需要日历权限用于安排和管理您的音频会议和录音任务 + NSMotionUsageDescription + 我们需要访问您的运动数据以提供更精准的导航服务 NSPhotoLibraryUsageDescription 需要相册权限用于保存录音文件和导入音频文件 - NSCameraUsageDescription - 需要相机权限用于扫描二维码和视频通话功能 UIApplicationSupportsIndirectInputEvents UIBackgroundModes bluetooth-central audio + location UILaunchStoryboardName LaunchScreen diff --git a/local_plugins/amap_service/ios/Classes/AmapHelper.swift b/local_plugins/amap_service/ios/Classes/AmapHelper.swift new file mode 100644 index 000000000..02b36ec9d --- /dev/null +++ b/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: 调用实际导航逻辑 + } + } +} diff --git a/local_plugins/amap_service/ios/Classes/AmapService.swift b/local_plugins/amap_service/ios/Classes/AmapService.swift new file mode 100644 index 000000000..30f6d1308 --- /dev/null +++ b/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() + } +} diff --git a/local_plugins/amap_service/ios/Classes/AmapServicePlugin.swift b/local_plugins/amap_service/ios/Classes/AmapServicePlugin.swift index e6e555729..196285394 100644 --- a/local_plugins/amap_service/ios/Classes/AmapServicePlugin.swift +++ b/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) } diff --git a/local_plugins/amap_service/ios/Classes/AmapViewController.swift b/local_plugins/amap_service/ios/Classes/AmapViewController.swift new file mode 100644 index 000000000..d2408f7cb --- /dev/null +++ b/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)") + } +} diff --git a/local_plugins/amap_service/ios/amap_service.podspec b/local_plugins/amap_service/ios/amap_service.podspec index ad4b9dc1f..5e90480f9 100644 --- a/local_plugins/amap_service/ios/amap_service.podspec +++ b/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 diff --git a/local_plugins/chat_api/ios/chat_api/Sources/chat_api/SystemFunctionHandler.swift b/local_plugins/chat_api/ios/chat_api/Sources/chat_api/SystemFunctionHandler.swift index bc9fc061e..610ccdc00 100644 --- a/local_plugins/chat_api/ios/chat_api/Sources/chat_api/SystemFunctionHandler.swift +++ b/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\": \"无法打开录音机应用\"}" } -} \ No newline at end of file +} From ce4377911487f0b153a0f5dea4f3b86ad7b07eed Mon Sep 17 00:00:00 2001 From: liwei1dao Date: Thu, 12 Jun 2025 14:26:36 +0800 Subject: [PATCH 2/6] =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E5=A4=A9=E6=B0=94?= =?UTF-8?q?=E9=A2=84=E6=8A=A5=E5=8D=A1=E7=89=87=E8=AF=9D=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/modules/agent/views/message_bubble.dart | 8 ++++---- lib/modules/agent/views/weather_card.dart | 16 ++++++++-------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/lib/modules/agent/views/message_bubble.dart b/lib/modules/agent/views/message_bubble.dart index ee6af73c5..c72c22ee5 100644 --- a/lib/modules/agent/views/message_bubble.dart +++ b/lib/modules/agent/views/message_bubble.dart @@ -249,12 +249,12 @@ class MessageBubble extends GetView { rawList.map((e) => Map.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.from(e)).toList(); - final now = meta!['card_weather']['day'] as Map; + final now = daylist[0] as Map; //天气 return buildWeatherContent( context, diff --git a/lib/modules/agent/views/weather_card.dart b/lib/modules/agent/views/weather_card.dart index aba39038e..323a4a483 100644 --- a/lib/modules/agent/views/weather_card.dart +++ b/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, From f2a49b995670de6b9d304f7f2a7ae8708f203864 Mon Sep 17 00:00:00 2001 From: lxm <1274844347@qq.com> Date: Thu, 12 Jun 2025 14:52:25 +0800 Subject: [PATCH 3/6] =?UTF-8?q?feat(=E9=85=8D=E5=AF=B9):=20=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E6=89=8B=E5=8A=A8=E9=85=8D=E5=AF=B9=E6=A8=A1=E5=BC=8F?= =?UTF-8?q?=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增手动配对模式控制变量 isManualMode 和 isReadyToStart - 添加手动开始配对方法 startManualPairing 和重置方法 resetToManualMode - 分离初始化逻辑到 _executeInitFlow 方法 - 更新配对视图以支持手动模式UI展示 - 添加开始配对按钮和重置按钮 --- .../controllers/pairing_controller.dart | 58 ++++++-- lib/modules/pairing/views/pairing_view.dart | 132 ++++++++++++++++-- 2 files changed, 165 insertions(+), 25 deletions(-) diff --git a/lib/modules/pairing/controllers/pairing_controller.dart b/lib/modules/pairing/controllers/pairing_controller.dart index 2c7debbe5..35a858a4f 100644 --- a/lib/modules/pairing/controllers/pairing_controller.dart +++ b/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 startManualPairing() async { + if (!isReadyToStart.value) return; + + Logger.i(_tag, '用户手动启动配对流程'); + isManualMode.value = false; + isReadyToStart.value = false; + + // 开始执行配对流程 + await _executeInitFlow(); + } + /// 重新开始配对流程 Future 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 _enterInit() async { - Logger.i(_tag, '开始执行初始化流程'); + Logger.i(_tag, '进入初始化状态'); + if (isManualMode.value) { + // 手动模式:仅显示待机状态,不执行任何操作 + isLoading.value = false; + statusDescription.value = '准备就绪'; + isReadyToStart.value = true; + return; + } + + // 自动模式:执行原有的初始化逻辑 + await _executeInitFlow(); + } + + /// 执行初始化流程 + Future _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 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); diff --git a/lib/modules/pairing/views/pairing_view.dart b/lib/modules/pairing/views/pairing_view.dart index f15cf8f34..abc48189e 100644 --- a/lib/modules/pairing/views/pairing_view.dart +++ b/lib/modules/pairing/views/pairing_view.dart @@ -99,11 +99,13 @@ class PairingView extends GetView { 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 { 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 { ), // 当前连接设备(有设备时才显示) - 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 { // 根据状态获取副标题 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 { 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 { ), ), - // 只在可以跳过时显示跳过按钮 + // 跳过按钮和重置按钮 if (canSkip) ...[ SizedBox(height: 12.h), TextButton( @@ -411,6 +497,24 @@ class PairingView extends GetView { ), ), ], + + // 添加重置按钮(在错误状态下) + 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), + ), + ), + ], ], ), ); From 78a762477a96c2365d1a78b09be9f9c7f005d691 Mon Sep 17 00:00:00 2001 From: liwei1dao Date: Thu, 12 Jun 2025 17:03:35 +0800 Subject: [PATCH 4/6] =?UTF-8?q?=E4=BC=98=E5=8C=96android=20mcp=20=E9=87=8D?= =?UTF-8?q?=E8=BF=9E=E5=92=8C=E5=BF=83=E8=B7=B3=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/data/models/appconfig_model.dart | 2 +- lib/data/models/appconfig_model.g.dart | 2 +- .../agent_service/AgentService.kt | 131 +++++++++++------- .../chat_api/CustomSseClientTransport.kt | 2 +- .../com/yunqiinnovation/chat_api/MCPClient.kt | 5 +- .../yunqiinnovation/chat_api/MCPSubClient.kt | 129 +++++++++++++++-- 6 files changed, 209 insertions(+), 62 deletions(-) diff --git a/lib/data/models/appconfig_model.dart b/lib/data/models/appconfig_model.dart index b95b5fae2..602abf4dd 100644 --- a/lib/data/models/appconfig_model.dart +++ b/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, diff --git a/lib/data/models/appconfig_model.g.dart b/lib/data/models/appconfig_model.g.dart index 6d47874e6..c4ac4cc90 100644 --- a/lib/data/models/appconfig_model.g.dart +++ b/lib/data/models/appconfig_model.g.dart @@ -57,7 +57,7 @@ Map _$DBAgentToJson(DBAgent instance) => { DBMCPServer _$DBMCPServerFromJson(Map json) => DBMCPServer( servername: json['servername'] as String, url: json['url'] as String, - tools: json['tools'] as String?, + tools: json['tools'] as String? ?? '', ); Map _$DBMCPServerToJson(DBMCPServer instance) => diff --git a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt index 90e318861..c3854b568 100644 --- a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt +++ b/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{ - 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>() - 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{ + 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 = 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>() + 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){ // 在其他 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, "音乐列表为空,未启动播放服务") } } diff --git a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/CustomSseClientTransport.kt b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/CustomSseClientTransport.kt index 2e6fa877e..14777d02a 100644 --- a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/CustomSseClientTransport.kt +++ b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/CustomSseClientTransport.kt @@ -234,7 +234,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) { diff --git a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/MCPClient.kt b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/MCPClient.kt index 7231f4999..23c538115 100644 --- a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/MCPClient.kt +++ b/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 } + } \ No newline at end of file diff --git a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/MCPSubClient.kt b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/MCPSubClient.kt index 60eacea0a..3290c2297 100644 --- a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/MCPSubClient.kt +++ b/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 @@ -66,14 +77,18 @@ class MCPSubClient( } // 连接到服务器 - client.connect(transport) + client?.connect(transport) // 获取可用工具列表 try { - val toolsResult = client.listTools() + 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): Map? { - 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),停止重连") + // 可以在这里添加通知或回调,告知上层连接彻底失败 + } + } /** * 关闭连接 */ From 254d86d0b2379dffde19eab2b5c8785c64c4fd8e Mon Sep 17 00:00:00 2001 From: lxm <1274844347@qq.com> Date: Thu, 12 Jun 2025 18:51:59 +0800 Subject: [PATCH 5/6] =?UTF-8?q?refactor(agent):=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E6=B6=88=E6=81=AF=E6=A8=A1=E5=9E=8B=E4=B8=BA=E7=8B=AC=E7=AB=8B?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E5=B9=B6=E7=BB=9F=E4=B8=80=E5=8F=91=E9=80=81?= =?UTF-8?q?=E6=96=B9=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将Message类从agent_controller.dart移出到独立的message_model.dart文件,提高代码可维护性 将sendTextMessage()重命名为sendMessage()以支持多种消息类型发送 移除未使用的导入和注释掉的代码 --- .../agent/controllers/agent_controller.dart | 217 +----------------- .../agent/controllers/report_controller.dart | 3 +- lib/modules/agent/views/agent_view.dart | 5 +- 3 files changed, 14 insertions(+), 211 deletions(-) diff --git a/lib/modules/agent/controllers/agent_controller.dart b/lib/modules/agent/controllers/agent_controller.dart index 93960f0d8..f1a51375c 100644 --- a/lib/modules/agent/controllers/agent_controller.dart +++ b/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 sendTextMessage() async { - // 如果图片输入是活跃的,使用选中的图片和文本框的内容 + //场景1:用户在文本输入框中输入文本,点击发送按钮,调用sendMessage()方法。 + //场景2:当用户选择图片后在文本框输入内容并发送时,调用sendMessage()转发给sendImageMessage()。 + Future 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 _saveMessageToStorage(Message message) async { - // try { - // // 准备元数据,包含图片信息 - // Map metadata = {}; - // if (message.meta != null) { - // metadata = Map.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 _checkMicrophonePermission() async { - try { - // 1.先获取当前麦克风权限状态 - final micStatus = await Permission.microphone.status; - - // 2.如果麦克风权限已授予,返回true - if (micStatus.isGranted) { - return true; - } - - // 3.创建待请求权限列表(这里只请求麦克风权限) - List 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( - 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 _getAndroidSdkVersion() async { - if (Platform.isAndroid) { - try { - // 这里我们简单返回一个较高的值,确保请求所有可能需要的权限 - // 在实际应用中,你可能需要使用platform channel获取真实的SDK版本 - return 31; // 对应Android 12 - } catch (e) { - Logger.e(TAG, '获取Android SDK版本失败: $e'); - } - } - return 0; - } - // 开始语音输入 Future startVoiceInput() async { if (isListening.value) return; diff --git a/lib/modules/agent/controllers/report_controller.dart b/lib/modules/agent/controllers/report_controller.dart index 0bae0106a..816d8277d 100644 --- a/lib/modules/agent/controllers/report_controller.dart +++ b/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 { // 当前举报的消息 diff --git a/lib/modules/agent/views/agent_view.dart b/lib/modules/agent/views/agent_view.dart index 15044c63c..6ba38309e 100644 --- a/lib/modules/agent/views/agent_view.dart +++ b/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( From 7b9f19d863d8af2e1daa8ddcb5feac063c37594b Mon Sep 17 00:00:00 2001 From: liwei1dao Date: Thu, 12 Jun 2025 21:39:36 +0800 Subject: [PATCH 6/6] =?UTF-8?q?=E4=B8=8A=E4=BC=A0android=20=E7=9A=84mcp?= =?UTF-8?q?=E8=BF=9E=E6=8E=A5=E5=92=8C=E5=BC=82=E5=B8=B8=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chat_api/ChatApiService.kt | 32 ++++++++----------- .../chat_api/CustomSseClientTransport.kt | 4 ++- .../yunqiinnovation/chat_api/MCPSubClient.kt | 6 ++-- 3 files changed, 19 insertions(+), 23 deletions(-) diff --git a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiService.kt b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiService.kt index 190e2a1f0..150321d7e 100644 --- a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiService.kt +++ b/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) } } diff --git a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/CustomSseClientTransport.kt b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/CustomSseClientTransport.kt index 14777d02a..f4bf2de9b 100644 --- a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/CustomSseClientTransport.kt +++ b/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 ?: "" diff --git a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/MCPSubClient.kt b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/MCPSubClient.kt index 3290c2297..a332689e2 100644 --- a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/MCPSubClient.kt +++ b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/MCPSubClient.kt @@ -77,11 +77,11 @@ class MCPSubClient( } // 连接到服务器 - client?.connect(transport) + client.connect(transport) // 获取可用工具列表 try { - val toolsResult = client?.listTools() + val toolsResult = client.listTools() if (toolsResult != null) { availableTools.clear() val filtered = toolsResult.tools.filter { tool -> @@ -101,7 +101,7 @@ class MCPSubClient( retryCount = 0 currentReconnectDelay = initialReconnectDelay // 启动心跳检测 - startHeartbeat() +// startHeartbeat() true } catch (e: Exception) {