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