17 changed files with 328 additions and 1558 deletions
File diff suppressed because it is too large
@ -1,4 +0,0 @@ |
|||||
#import <Flutter/Flutter.h> |
|
||||
|
|
||||
@interface AgentServicePlugin : NSObject<FlutterPlugin> |
|
||||
@end |
|
||||
@ -1,12 +0,0 @@ |
|||||
#import "AgentServicePlugin.h" |
|
||||
#if __has_include(<agent_service/agent_service-Swift.h>) |
|
||||
#import <agent_service/agent_service-Swift.h> |
|
||||
#else |
|
||||
#import "agent_service-Swift.h" |
|
||||
#endif |
|
||||
|
|
||||
@implementation AgentServicePlugin |
|
||||
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar { |
|
||||
[AgentServicePlugin registerWithRegistrar:registrar]; |
|
||||
} |
|
||||
@end |
|
||||
@ -1,160 +0,0 @@ |
|||||
import Flutter |
|
||||
import UIKit |
|
||||
import Foundation |
|
||||
import os.log |
|
||||
|
|
||||
/// 代理服务Flutter插件,负责将Flutter调用桥接到原生实现 |
|
||||
public class AgentServicePlugin: NSObject, FlutterPlugin { |
|
||||
|
|
||||
/// 插件注册方法,Flutter引擎调用 |
|
||||
public static func register(with registrar: FlutterPluginRegistrar) { |
|
||||
let channel = FlutterMethodChannel(name: "com.yunqiinnovation.agent_service", binaryMessenger: registrar.messenger()) |
|
||||
let eventChannel = FlutterEventChannel(name: "com.yunqiinnovation.agent_service/events", binaryMessenger: registrar.messenger()) |
|
||||
let instance = AgentServicePlugin() |
|
||||
|
|
||||
registrar.addMethodCallDelegate(instance, channel: channel) |
|
||||
eventChannel.setStreamHandler(instance.eventHandler) |
|
||||
} |
|
||||
|
|
||||
// MARK: - 属性 |
|
||||
|
|
||||
private let impl = AgentServiceImpl.shared |
|
||||
private lazy var eventHandler = AgentServiceEventHandler(impl: impl) |
|
||||
|
|
||||
// MARK: - FlutterPlugin 实现 |
|
||||
|
|
||||
/// 处理Flutter发来的方法调用 |
|
||||
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { |
|
||||
switch call.method { |
|
||||
case "startAgentService": |
|
||||
guard let arguments = call.arguments as? [String: Any], |
|
||||
let config = arguments["config"] as? [String: Any] else { |
|
||||
result(FlutterError(code: "INVALID_ARGUMENTS", message: "配置参数不能为空", details: nil)) |
|
||||
return |
|
||||
} |
|
||||
|
|
||||
let success = impl.initialize(config: config) |
|
||||
result(success) |
|
||||
|
|
||||
case "stopAgentService": |
|
||||
let success = impl.dispose() |
|
||||
result(success) |
|
||||
|
|
||||
case "startConversation": |
|
||||
impl.startRecognition() |
|
||||
result(true) |
|
||||
|
|
||||
case "stopConversation": |
|
||||
impl.stopRecognition() |
|
||||
result(true) |
|
||||
|
|
||||
case "processTextInput": |
|
||||
guard let arguments = call.arguments as? [String: Any], |
|
||||
let text = arguments["text"] as? String else { |
|
||||
result(FlutterError(code: "INVALID_ARGUMENTS", message: "文本不能为空", details: nil)) |
|
||||
return |
|
||||
} |
|
||||
|
|
||||
let speakResponse = arguments["speakResponse"] as? Bool ?? false |
|
||||
let success = impl.processTextInput(text, speakResponse: speakResponse) |
|
||||
result(success) |
|
||||
|
|
||||
case "processImageInput": |
|
||||
guard let arguments = call.arguments as? [String: Any], |
|
||||
let imagePath = arguments["imagePath"] as? String else { |
|
||||
result(FlutterError(code: "INVALID_ARGUMENTS", message: "图片路径不能为空", details: nil)) |
|
||||
return |
|
||||
} |
|
||||
|
|
||||
let text = arguments["text"] as? String ?? "" |
|
||||
let speakResponse = arguments["speakResponse"] as? Bool ?? false |
|
||||
let success = impl.processImageInput(imagePath: imagePath, text: text, speakResponse: speakResponse) |
|
||||
result(success) |
|
||||
|
|
||||
case "speakText": |
|
||||
guard let arguments = call.arguments as? [String: Any], |
|
||||
let text = arguments["text"] as? String else { |
|
||||
result(FlutterError(code: "INVALID_ARGUMENTS", message: "文本不能为空", details: nil)) |
|
||||
return |
|
||||
} |
|
||||
|
|
||||
let success = impl.speakText(text) |
|
||||
result(success) |
|
||||
|
|
||||
case "stopTts": |
|
||||
let success = impl.stopTts() |
|
||||
result(success) |
|
||||
|
|
||||
case "clearChatHistory": |
|
||||
let success = impl.clearChatHistory() |
|
||||
result(success) |
|
||||
|
|
||||
case "interruptCurrentResponse": |
|
||||
let success = impl.interruptCurrentResponse() |
|
||||
result(success) |
|
||||
|
|
||||
case "dispose": |
|
||||
let success = impl.dispose() |
|
||||
result(success) |
|
||||
|
|
||||
default: |
|
||||
result(FlutterMethodNotImplemented) |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/// 代理服务事件处理器,负责将原生事件传递给Flutter |
|
||||
class AgentServiceEventHandler: NSObject, FlutterStreamHandler { |
|
||||
private let impl: AgentServiceImpl |
|
||||
private var eventSink: FlutterEventSink? |
|
||||
private let logger = OSLog(subsystem: "com.yunqiinnovation.agent_service", category: "AgentServiceEventHandler") |
|
||||
|
|
||||
init(impl: AgentServiceImpl) { |
|
||||
self.impl = impl |
|
||||
super.init() |
|
||||
} |
|
||||
|
|
||||
/// 监听开始时调用 |
|
||||
func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { |
|
||||
self.eventSink = events |
|
||||
|
|
||||
// 添加实现的事件监听器 |
|
||||
impl.addListener(self) |
|
||||
return nil |
|
||||
} |
|
||||
|
|
||||
/// 监听取消时调用 |
|
||||
func onCancel(withArguments arguments: Any?) -> FlutterError? { |
|
||||
impl.removeListener(self) |
|
||||
self.eventSink = nil |
|
||||
return nil |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
// 扩展事件处理器以实现AgentServiceListener接口 |
|
||||
extension AgentServiceEventHandler: AgentServiceListener { |
|
||||
func onEvent(eventName: String, data: [String: Any]) { |
|
||||
// 确保在主线程上调用 |
|
||||
DispatchQueue.main.async { [weak self] in |
|
||||
guard let self = self else { return } |
|
||||
|
|
||||
// 将事件数据格式化为与Android版本一致的格式 |
|
||||
let eventData: [String: Any] = [ |
|
||||
"event": eventName, |
|
||||
"data": data |
|
||||
] |
|
||||
|
|
||||
// 转换为JSON字符串发送,与Android版本保持一致 |
|
||||
do { |
|
||||
let jsonData = try JSONSerialization.data(withJSONObject: eventData, options: []) |
|
||||
if let jsonString = String(data: jsonData, encoding: .utf8) { |
|
||||
self.eventSink?(jsonString) |
|
||||
} else { |
|
||||
os_log("无法创建JSON字符串", log: self.logger, type: .error) |
|
||||
} |
|
||||
} catch { |
|
||||
os_log("JSON序列化失败: %{public}@", log: self.logger, type: .error, error.localizedDescription) |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,240 +0,0 @@ |
|||||
import AVFoundation |
|
||||
import os.log |
|
||||
|
|
||||
public enum AudioIntent { |
|
||||
case voice |
|
||||
case playback |
|
||||
} |
|
||||
|
|
||||
public class AudioSessionHub { |
|
||||
|
|
||||
public static let shared = AudioSessionHub() |
|
||||
|
|
||||
private let logger = OSLog(subsystem: "com.yunqiinnovation.agent_service", category: "AudioSessionHub") |
|
||||
private var currentIntent: AudioIntent? |
|
||||
private let audioSession = AVAudioSession.sharedInstance() |
|
||||
private var isInterrupted = false |
|
||||
|
|
||||
private init() { |
|
||||
setupNotifications() |
|
||||
} |
|
||||
|
|
||||
public func begin(_ intent: AudioIntent) { |
|
||||
if intent == currentIntent && audioSession.isOtherAudioPlaying == false { |
|
||||
return |
|
||||
} |
|
||||
|
|
||||
currentIntent = intent |
|
||||
|
|
||||
do { |
|
||||
switch intent { |
|
||||
|
|
||||
case .voice: |
|
||||
try audioSession.setCategory(.playAndRecord, |
|
||||
mode: .voiceChat, |
|
||||
options: [.allowBluetooth, .mixWithOthers]) |
|
||||
|
|
||||
// 如果有蓝牙HFP设备,强制设置为首选输入 |
|
||||
if let bluetoothInput = audioSession.availableInputs?.first(where: { $0.portType == .bluetoothHFP }) { |
|
||||
try audioSession.setPreferredInput(bluetoothInput) |
|
||||
os_log("已设置蓝牙HFP为首选输入: %{public}@", log: logger, type: .info, bluetoothInput.portName) |
|
||||
} |
|
||||
|
|
||||
case .playback: |
|
||||
try audioSession.setCategory(.playback, |
|
||||
mode: .default, |
|
||||
options: [.allowBluetooth, .mixWithOthers]) |
|
||||
|
|
||||
} |
|
||||
|
|
||||
try audioSession.setActive(true, options: .notifyOthersOnDeactivation) |
|
||||
|
|
||||
os_log("音频会话已设置为:%{public}@", log: logger, type: .debug, String(describing: intent)) |
|
||||
|
|
||||
// 输出当前音频路由信息 |
|
||||
let hasBluetoothDevice = hasBluetoothAudioDevice() |
|
||||
let hasBluetoothMic = hasBluetoothMicrophone() |
|
||||
let currentInput = getCurrentInputDevice() |
|
||||
|
|
||||
os_log("蓝牙音频设备状态: %{public}@", log: logger, type: .info, hasBluetoothDevice ? "已连接" : "未连接") |
|
||||
os_log("蓝牙麦克风状态: %{public}@", log: logger, type: .info, hasBluetoothMic ? "已连接" : "未连接") |
|
||||
os_log("当前输入设备: %{public}@", log: logger, type: .info, currentInput) |
|
||||
|
|
||||
if hasBluetoothDevice { |
|
||||
os_log("%{public}@", log: logger, type: .debug, getCurrentAudioRoute()) |
|
||||
} |
|
||||
} catch { |
|
||||
os_log("设置音频会话失败:%{public}@", log: logger, type: .error, error.localizedDescription) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public func appWillEnterForeground() { |
|
||||
if let intent = currentIntent { |
|
||||
begin(intent) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public func appDidEnterBackground() { |
|
||||
do { |
|
||||
try audioSession.setActive(false, options: .notifyOthersOnDeactivation) |
|
||||
os_log("音频会话已释放(应用进入后台)", log: logger, type: .debug) |
|
||||
} catch { |
|
||||
os_log("释放音频会话失败:%{public}@", log: logger, type: .error, error.localizedDescription) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/// 获取当前音频路由信息 |
|
||||
public func getCurrentAudioRoute() -> String { |
|
||||
let currentRoute = audioSession.currentRoute |
|
||||
var routeInfo = "当前音频路由:\n" |
|
||||
|
|
||||
// 输出设备 |
|
||||
routeInfo += "输出设备:\n" |
|
||||
for output in currentRoute.outputs { |
|
||||
routeInfo += " - \(output.portName) (\(output.portType.rawValue))\n" |
|
||||
} |
|
||||
|
|
||||
// 输入设备 |
|
||||
routeInfo += "输入设备:\n" |
|
||||
for input in currentRoute.inputs { |
|
||||
routeInfo += " - \(input.portName) (\(input.portType.rawValue))\n" |
|
||||
} |
|
||||
|
|
||||
return routeInfo |
|
||||
} |
|
||||
|
|
||||
/// 检查是否有蓝牙音频设备连接 |
|
||||
public func hasBluetoothAudioDevice() -> Bool { |
|
||||
let currentRoute = audioSession.currentRoute |
|
||||
|
|
||||
// 检查输出设备 |
|
||||
for output in currentRoute.outputs { |
|
||||
if output.portType == .bluetoothA2DP || |
|
||||
output.portType == .bluetoothHFP || |
|
||||
output.portType == .bluetoothLE { |
|
||||
return true |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
// 检查输入设备(这是关键!) |
|
||||
for input in currentRoute.inputs { |
|
||||
if input.portType == .bluetoothHFP { |
|
||||
return true |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
return false |
|
||||
} |
|
||||
|
|
||||
/// 检查是否有蓝牙麦克风作为输入设备 |
|
||||
public func hasBluetoothMicrophone() -> Bool { |
|
||||
let currentRoute = audioSession.currentRoute |
|
||||
|
|
||||
for input in currentRoute.inputs { |
|
||||
if input.portType == .bluetoothHFP { |
|
||||
return true |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
return false |
|
||||
} |
|
||||
|
|
||||
/// 获取当前输入设备信息 |
|
||||
public func getCurrentInputDevice() -> String { |
|
||||
let currentRoute = audioSession.currentRoute |
|
||||
|
|
||||
if let input = currentRoute.inputs.first { |
|
||||
return "\(input.portName) (\(input.portType.rawValue))" |
|
||||
} |
|
||||
|
|
||||
return "无输入设备" |
|
||||
} |
|
||||
|
|
||||
private func setupNotifications() { |
|
||||
NotificationCenter.default.addObserver( |
|
||||
self, |
|
||||
selector: #selector(handleAudioSessionInterruption), |
|
||||
name: AVAudioSession.interruptionNotification, |
|
||||
object: nil |
|
||||
) |
|
||||
|
|
||||
NotificationCenter.default.addObserver( |
|
||||
self, |
|
||||
selector: #selector(handleAudioRouteChange), |
|
||||
name: AVAudioSession.routeChangeNotification, |
|
||||
object: nil |
|
||||
) |
|
||||
} |
|
||||
|
|
||||
/// 处理音频中断 |
|
||||
@objc private func handleAudioSessionInterruption(notification: Notification) { |
|
||||
guard let userInfo = notification.userInfo, |
|
||||
let typeValue = userInfo[AVAudioSessionInterruptionTypeKey] as? UInt, |
|
||||
let type = AVAudioSession.InterruptionType(rawValue: typeValue) else { |
|
||||
return |
|
||||
} |
|
||||
|
|
||||
switch type { |
|
||||
case .began: |
|
||||
// 中断开始 |
|
||||
isInterrupted = true |
|
||||
os_log("音频会话被中断", log: logger, type: .debug) |
|
||||
|
|
||||
case .ended: |
|
||||
// 中断结束 |
|
||||
isInterrupted = false |
|
||||
|
|
||||
// 检查是否应该恢复 |
|
||||
if let optionsValue = userInfo[AVAudioSessionInterruptionOptionKey] as? UInt { |
|
||||
let options = AVAudioSession.InterruptionOptions(rawValue: optionsValue) |
|
||||
if options.contains(.shouldResume), let intent = currentIntent { |
|
||||
// 恢复之前的音频意图 |
|
||||
begin(intent) |
|
||||
os_log("音频会话中断结束,已恢复", log: logger, type: .debug) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
@unknown default: |
|
||||
os_log("未知的音频中断类型: %{public}@", log: logger, type: .error, "\(type)") |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/// 处理音频路由变化 |
|
||||
@objc private func handleAudioRouteChange(notification: Notification) { |
|
||||
guard let userInfo = notification.userInfo, |
|
||||
let reasonValue = userInfo[AVAudioSessionRouteChangeReasonKey] as? UInt, |
|
||||
let reason = AVAudioSession.RouteChangeReason(rawValue: reasonValue) else { |
|
||||
return |
|
||||
} |
|
||||
|
|
||||
switch reason { |
|
||||
case .newDeviceAvailable: |
|
||||
os_log("新音频设备已连接", log: logger, type: .info) |
|
||||
os_log("%{public}@", log: logger, type: .debug, getCurrentAudioRoute()) |
|
||||
|
|
||||
// 重新应用当前意图以确保正确路由 |
|
||||
if let intent = currentIntent { |
|
||||
begin(intent) |
|
||||
} |
|
||||
|
|
||||
case .oldDeviceUnavailable: |
|
||||
os_log("音频设备已断开", log: logger, type: .info) |
|
||||
os_log("%{public}@", log: logger, type: .debug, getCurrentAudioRoute()) |
|
||||
|
|
||||
// 重新应用当前意图以确保正确路由 |
|
||||
if let intent = currentIntent { |
|
||||
begin(intent) |
|
||||
} |
|
||||
|
|
||||
case .categoryChange: |
|
||||
os_log("音频类别已变化", log: logger, type: .info) |
|
||||
|
|
||||
default: |
|
||||
break |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
deinit { |
|
||||
NotificationCenter.default.removeObserver(self) |
|
||||
} |
|
||||
} |
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading…
Reference in new issue