59 changed files with 6578 additions and 804 deletions
@ -1,17 +1,18 @@ |
|||||
package com.yunqiinnovation.ble_service |
package com.yunqiinnovation.agent_service |
||||
|
|
||||
import android.content.Context |
import android.content.Context |
||||
import com.yunqiinnovation.agent_service.AgentService |
import com.yunqiinnovation.agent_service.AgentService |
||||
import com.yunqiinnovation.agent_service.AgentServiceListener |
import com.yunqiinnovation.agent_service.AgentServiceListener |
||||
import com.yunqiinnovation.deepvoice_utils.FileLogger |
import com.yunqiinnovation.deepvoice_utils.FileLogger |
||||
|
import com.yunqiinnovation.ble_service.BleService |
||||
|
import com.yunqiinnovation.ble_service.BleConst |
||||
|
|
||||
/** |
/** |
||||
* 蓝牙语音交互代理 |
* 蓝牙语音交互代理 |
||||
* 连接BleService和AgentService,响应蓝牙设备唤醒信号,启动语音识别和交互 |
* 连接BleService和AgentService,响应蓝牙设备唤醒信号,启动语音识别和交互 |
||||
*/ |
*/ |
||||
object ChatAgent : BleService.Callback, AgentServiceListener { |
object BleAgent : BleService.Callback, AgentServiceListener { |
||||
private const val TAG = "ChatAgent" |
private const val TAG = "BleAgent" |
||||
|
|
||||
// 是否已初始化 |
// 是否已初始化 |
||||
private var isInitialized = false |
private var isInitialized = false |
||||
@ -1,4 +1,4 @@ |
|||||
package com.yunqiinnovation.ble_service |
package com.yunqiinnovation.agent_service |
||||
|
|
||||
import android.content.* |
import android.content.* |
||||
import android.os.Build |
import android.os.Build |
||||
@ -1,4 +1,4 @@ |
|||||
package com.yunqiinnovation.ble_service |
package com.yunqiinnovation.agent_service |
||||
|
|
||||
import android.content.Context |
import android.content.Context |
||||
import androidx.security.crypto.EncryptedSharedPreferences |
import androidx.security.crypto.EncryptedSharedPreferences |
||||
@ -0,0 +1,994 @@ |
|||||
|
import Foundation |
||||
|
import MicrosoftCognitiveServicesSpeech |
||||
|
import open_ai_service |
||||
|
import chat_storage |
||||
|
|
||||
|
/// 代理服务 - 处理语音识别、TTS和AI对话相关逻辑 |
||||
|
/// |
||||
|
/// 负责集成Azure语音服务、OpenAI服务和本地存储服务, |
||||
|
/// 提供语音识别、语音合成、AI对话等功能 |
||||
|
class AgentService { |
||||
|
// 常量定义 |
||||
|
private let TAG = "AgentService" |
||||
|
|
||||
|
// 上下文和监听器 |
||||
|
private var listeners = NSHashTable<AnyObject>.weakObjects() |
||||
|
|
||||
|
// 配置参数 |
||||
|
private var azureSpeechKey: String = "" |
||||
|
private var azureSpeechRegion: String = "" |
||||
|
private var openaiApiKey: String = "" |
||||
|
private var openaiBaseUrl: String = "" |
||||
|
private var openaiModel: String = "gpt-3.5-turbo" |
||||
|
private var mcpServer: String = "" |
||||
|
|
||||
|
// Azure服务 |
||||
|
private var azureAsrHelper: AzureAsrHelper? |
||||
|
private var azureTtsHelper: AzureTtsHelper? |
||||
|
|
||||
|
// OpenAI服务 |
||||
|
private var openAIService: OpenAIServiceBridge? |
||||
|
|
||||
|
// 聊天存储服务 |
||||
|
private var chatStorageHelper: ChatStorageHelperBridge? |
||||
|
|
||||
|
// 会话ID,用于区分不同聊天上下文 |
||||
|
private let sessionId = "default_agent" |
||||
|
|
||||
|
// 历史聊天消息缓存 |
||||
|
private var historyMessages = [[String: Any]]() |
||||
|
|
||||
|
// 系统提示词 |
||||
|
private var systemPrompt = "" |
||||
|
|
||||
|
// 状态 |
||||
|
private var isInitialized = false |
||||
|
private var isRecognitionActive = false |
||||
|
private var isTtsSpeaking = false |
||||
|
private var hasSpeechDetected = false |
||||
|
private var isAiStreaming = false |
||||
|
|
||||
|
// AI流生成相关 |
||||
|
private var currentAiTask: DispatchWorkItem? |
||||
|
|
||||
|
// 空闲检测相关 |
||||
|
private var idleCheckTimer: Timer? |
||||
|
private let maxIdleSeconds: TimeInterval = 10 // 最大空闲秒数 |
||||
|
|
||||
|
/// 初始化 |
||||
|
init() { |
||||
|
// 创建OpenAI服务桥接器 |
||||
|
openAIService = OpenAIServiceBridge() |
||||
|
|
||||
|
// 创建聊天存储桥接器 |
||||
|
chatStorageHelper = ChatStorageHelperBridge() |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 初始化系统提示词 |
||||
|
*/ |
||||
|
private func initSystemPrompt() { |
||||
|
systemPrompt = """ |
||||
|
你是一个友好、专业的语音助手,名叫"小语"。你的目标是通过对话为用户提供帮助、解答问题和完成任务。 |
||||
|
遵循以下指导原则: |
||||
|
1. 保持简短精炼的回答,因为用户是通过语音与你交流 |
||||
|
2. 优先使用中文回复,除非用户明确要求使用其他语言 |
||||
|
3. 当用户问题不明确时,礼貌地请求更多信息 |
||||
|
4. 避免过长的列表,尽量将信息分成小段 |
||||
|
5. 不要使用需要视觉展示的元素(如表格、图表或代码块) |
||||
|
6. 记住用户之前的对话内容,保持对话连贯 |
||||
|
7. 如果用户发送了图片,请根据图片内容和文字要求回答问题 |
||||
|
你不仅可以回答知识性问题,还可以帮助用户设置提醒、提供建议,或进行轻松愉快的对话。 |
||||
|
无论遇到什么问题,都要尽力以温暖、贴心的语气提供最佳帮助。 |
||||
|
""" |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 初始化 |
||||
|
* |
||||
|
* - Parameter config: 配置参数,包含所需的所有API密钥和设置 |
||||
|
* - Returns: 是否初始化成功 |
||||
|
*/ |
||||
|
func initialize(config: [String: Any]) -> Bool { |
||||
|
if isInitialized { return true } |
||||
|
|
||||
|
do { |
||||
|
// 从配置中获取参数 |
||||
|
if let azureKey = config["azureSpeechKey"] as? String { |
||||
|
azureSpeechKey = azureKey |
||||
|
} |
||||
|
|
||||
|
if let azureRegion = config["azureSpeechRegion"] as? String { |
||||
|
azureSpeechRegion = azureRegion |
||||
|
} |
||||
|
|
||||
|
if let openaiKey = config["openaiApiKey"] as? String { |
||||
|
openaiApiKey = openaiKey |
||||
|
} |
||||
|
|
||||
|
if let baseUrl = config["openaiBaseUrl"] as? String { |
||||
|
openaiBaseUrl = baseUrl |
||||
|
} |
||||
|
|
||||
|
if let model = config["openaiModel"] as? String { |
||||
|
openaiModel = model |
||||
|
} |
||||
|
|
||||
|
if let server = config["mcpServer"] as? String { |
||||
|
mcpServer = server |
||||
|
} |
||||
|
|
||||
|
// 自定义系统提示词 |
||||
|
if let customSystemPrompt = config["systemPrompt"] as? String, !customSystemPrompt.isEmpty { |
||||
|
systemPrompt = customSystemPrompt |
||||
|
} else { |
||||
|
// 使用默认系统提示词 |
||||
|
initSystemPrompt() |
||||
|
} |
||||
|
|
||||
|
// 检查必要参数 |
||||
|
if azureSpeechKey.isEmpty || azureSpeechRegion.isEmpty || openaiApiKey.isEmpty { |
||||
|
print("\(TAG): 初始化失败:关键配置参数缺失") |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
// 初始化OpenAI服务 |
||||
|
openAIService?.initialize(apiKey: openaiApiKey, baseUrl: openaiBaseUrl, model: openaiModel, mcpServer: mcpServer) |
||||
|
|
||||
|
// 初始化Azure ASR |
||||
|
azureAsrHelper = AzureAsrHelper(self) |
||||
|
let asrInitResult = azureAsrHelper?.initialize( |
||||
|
subscriptionKey: azureSpeechKey, |
||||
|
region: azureSpeechRegion, |
||||
|
audioSourceType: .microphone |
||||
|
) ?? false |
||||
|
|
||||
|
if !asrInitResult { |
||||
|
print("\(TAG): Azure ASR初始化失败") |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
// 初始化Azure TTS |
||||
|
azureTtsHelper = AzureTtsHelper(self) |
||||
|
let ttsInitResult = azureTtsHelper?.initialize( |
||||
|
subscriptionKey: azureSpeechKey, |
||||
|
region: azureSpeechRegion, |
||||
|
callback: self |
||||
|
) ?? false |
||||
|
|
||||
|
if !ttsInitResult { |
||||
|
print("\(TAG): Azure TTS初始化失败") |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
// 加载最近的聊天记录 |
||||
|
loadChatHistory() |
||||
|
|
||||
|
isInitialized = true |
||||
|
print("\(TAG): 代理服务初始化成功") |
||||
|
return true |
||||
|
} catch { |
||||
|
print("\(TAG): 初始化失败:\(error.localizedDescription)") |
||||
|
return false |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 添加事件监听器 |
||||
|
* |
||||
|
* - Parameter listener: 要添加的监听器 |
||||
|
*/ |
||||
|
func addListener(_ listener: AgentServiceListener) { |
||||
|
listeners.add(listener as AnyObject) |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 设置事件监听器(替换现有监听器) |
||||
|
* |
||||
|
* - Parameter listener: 要设置的监听器 |
||||
|
*/ |
||||
|
func setListener(_ listener: AgentServiceListener) { |
||||
|
listeners.removeAllObjects() |
||||
|
listeners.add(listener as AnyObject) |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 移除事件监听器 |
||||
|
* |
||||
|
* - Parameter listener: 要移除的监听器 |
||||
|
*/ |
||||
|
func removeListener(_ listener: AgentServiceListener) { |
||||
|
listeners.remove(listener as AnyObject) |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 移除所有事件监听器 |
||||
|
*/ |
||||
|
func clearListeners() { |
||||
|
listeners.removeAllObjects() |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 启动空闲检测 |
||||
|
*/ |
||||
|
private func startIdleCheck() { |
||||
|
stopIdleCheck() // 先停止现有的检查 |
||||
|
|
||||
|
if !isRecognitionActive { return } |
||||
|
|
||||
|
// 创建定时器 |
||||
|
idleCheckTimer = Timer.scheduledTimer(withTimeInterval: maxIdleSeconds, repeats: false) { [weak self] _ in |
||||
|
guard let self = self else { return } |
||||
|
|
||||
|
// 如果状态仍然是空闲的,则停止识别 |
||||
|
if self.isRecognitionActive && !self.hasSpeechDetected && !self.isTtsSpeaking && !self.isAiStreaming { |
||||
|
self.stopRecognition() |
||||
|
self.sendEvent("auto_stop", ["reason": "idle_timeout", "seconds": self.maxIdleSeconds]) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 停止空闲检测 |
||||
|
*/ |
||||
|
private func stopIdleCheck() { |
||||
|
idleCheckTimer?.invalidate() |
||||
|
idleCheckTimer = nil |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 重启空闲检测 |
||||
|
* 当状态发生变化时调用 |
||||
|
*/ |
||||
|
private func restartIdleCheck() { |
||||
|
if isRecognitionActive { |
||||
|
startIdleCheck() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 开始语音识别 |
||||
|
* |
||||
|
* - Returns: 是否成功开始识别 |
||||
|
*/ |
||||
|
func startRecognition() -> Bool { |
||||
|
if !isInitialized { |
||||
|
print("\(TAG): 服务未初始化") |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
if isRecognitionActive { return true } |
||||
|
|
||||
|
isRecognitionActive = true |
||||
|
hasSpeechDetected = false |
||||
|
|
||||
|
do { |
||||
|
// 创建连续识别回调 |
||||
|
class ContinuousRecognizeCallbackImpl: AzureAsrHelper.ContinuousRecognizeCallback { |
||||
|
private weak var service: AgentService? |
||||
|
|
||||
|
init(_ service: AgentService) { |
||||
|
self.service = service |
||||
|
} |
||||
|
|
||||
|
func onRecognizing(recognizing: String, detectedLanguage: String) { |
||||
|
guard let service = service else { return } |
||||
|
|
||||
|
if !recognizing.isEmpty { |
||||
|
// 检测到语音,更新状态 |
||||
|
let previousHasSpeech = service.hasSpeechDetected |
||||
|
service.hasSpeechDetected = true |
||||
|
|
||||
|
// 状态发生变化时重启空闲检测 |
||||
|
if !previousHasSpeech { |
||||
|
service.restartIdleCheck() |
||||
|
} |
||||
|
|
||||
|
service.sendEvent("recognizing", [ |
||||
|
"text": recognizing, |
||||
|
"language": detectedLanguage |
||||
|
]) |
||||
|
|
||||
|
// 如果TTS正在播放或AI正在生成,则触发打断 |
||||
|
if service.isTtsSpeaking || service.isAiStreaming { |
||||
|
service.interruptCurrentResponse() |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func onResult(text: String, detectedLanguage: String) { |
||||
|
guard let service = service else { return } |
||||
|
|
||||
|
if !text.isEmpty { |
||||
|
service.sendEvent("recognition_result", [ |
||||
|
"text": text, |
||||
|
"language": detectedLanguage |
||||
|
]) |
||||
|
|
||||
|
service.processWithOpenAI(text: text) |
||||
|
} |
||||
|
|
||||
|
// 重置状态,继续识别 |
||||
|
let previousHasSpeech = service.hasSpeechDetected |
||||
|
service.hasSpeechDetected = false |
||||
|
|
||||
|
// 状态发生变化时重启空闲检测 |
||||
|
if previousHasSpeech { |
||||
|
service.restartIdleCheck() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func onSessionStarted() { |
||||
|
guard let service = service else { return } |
||||
|
|
||||
|
service.sendEvent("recognition_started", ["status": "started"]) |
||||
|
// 启动空闲检测 |
||||
|
service.startIdleCheck() |
||||
|
} |
||||
|
|
||||
|
func onSessionStopped() { |
||||
|
guard let service = service else { return } |
||||
|
|
||||
|
service.isRecognitionActive = false |
||||
|
service.stopIdleCheck() |
||||
|
service.sendEvent("recognition_stopped", ["status": "stopped"]) |
||||
|
} |
||||
|
|
||||
|
func onCanceled(reason: String, errorDetails: String) { |
||||
|
guard let service = service else { return } |
||||
|
|
||||
|
service.isRecognitionActive = false |
||||
|
service.stopIdleCheck() |
||||
|
service.sendEvent("recognition_canceled", [ |
||||
|
"reason": reason, |
||||
|
"details": errorDetails |
||||
|
]) |
||||
|
} |
||||
|
|
||||
|
func onError(error: String) { |
||||
|
guard let service = service else { return } |
||||
|
|
||||
|
service.isRecognitionActive = false |
||||
|
service.stopIdleCheck() |
||||
|
print("\(service.TAG): 语音识别出错: \(error)") |
||||
|
service.sendEvent("error", [ |
||||
|
"code": "RECOGNITION_ERROR", |
||||
|
"message": error |
||||
|
]) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
let callback = ContinuousRecognizeCallbackImpl(self) |
||||
|
let result = azureAsrHelper?.startContinuousRecognition(callback) ?? false |
||||
|
|
||||
|
if !result { |
||||
|
isRecognitionActive = false |
||||
|
print("\(TAG): 启动语音识别失败") |
||||
|
sendEvent("error", [ |
||||
|
"code": "RECOGNITION_START_ERROR", |
||||
|
"message": "启动语音识别失败" |
||||
|
]) |
||||
|
} |
||||
|
|
||||
|
return result |
||||
|
} catch { |
||||
|
isRecognitionActive = false |
||||
|
print("\(TAG): 启动语音识别失败: \(error.localizedDescription)") |
||||
|
sendEvent("error", [ |
||||
|
"code": "RECOGNITION_START_ERROR", |
||||
|
"message": error.localizedDescription |
||||
|
]) |
||||
|
return false |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 停止语音识别 |
||||
|
*/ |
||||
|
func stopRecognition() { |
||||
|
if !isRecognitionActive { return } |
||||
|
|
||||
|
print("\(TAG): 停止语音识别") |
||||
|
|
||||
|
// 停止识别 |
||||
|
let _ = azureAsrHelper?.stopContinuousRecognition() |
||||
|
isRecognitionActive = false |
||||
|
stopIdleCheck() |
||||
|
print("\(TAG): 语音识别已停止") |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 打断当前响应 |
||||
|
* 停止TTS播放和AI流输出 |
||||
|
*/ |
||||
|
func interruptCurrentResponse() { |
||||
|
if isAiStreaming || isTtsSpeaking { |
||||
|
// 停止TTS播放 |
||||
|
stopTts() |
||||
|
|
||||
|
// 停止AI流输出 |
||||
|
stopAiStream() |
||||
|
|
||||
|
// 发送打断事件 |
||||
|
sendEvent("response_interrupted", ["status": "interrupted"]) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 停止AI流输出 |
||||
|
*/ |
||||
|
private func stopAiStream() { |
||||
|
if isAiStreaming { |
||||
|
// 取消当前AI生成任务 |
||||
|
currentAiTask?.cancel() |
||||
|
currentAiTask = nil |
||||
|
|
||||
|
// 通知OpenAI服务终止当前流式请求 |
||||
|
openAIService?.cancelCurrentStream() |
||||
|
|
||||
|
// 更新状态 |
||||
|
isAiStreaming = false |
||||
|
|
||||
|
// 记录日志 |
||||
|
print("\(TAG): AI流输出已停止") |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 处理文本输入 |
||||
|
* 作为语音输入的补充,直接处理文本并通过事件返回结果 |
||||
|
* |
||||
|
* - Parameters: |
||||
|
* - text: 用户输入文本 |
||||
|
* - speakResponse: 是否朗读回复,默认为false |
||||
|
* - Returns: 是否成功开始处理 |
||||
|
*/ |
||||
|
func processTextInput(text: String, speakResponse: Bool = false) -> Bool { |
||||
|
if !isInitialized { |
||||
|
print("\(TAG): 服务未初始化") |
||||
|
sendEvent("error", ["code": "NOT_INITIALIZED", "message": "服务未初始化"]) |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
if text.isEmpty { |
||||
|
print("\(TAG): 文本输入不能为空") |
||||
|
sendEvent("error", ["code": "EMPTY_TEXT", "message": "文本输入不能为空"]) |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
// 使用OpenAI处理文本 |
||||
|
processWithOpenAI(text: text, speakResponse: speakResponse) |
||||
|
return true |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 使用OpenAI处理语音识别结果 |
||||
|
* |
||||
|
* - Parameter text: 用户输入文本 |
||||
|
*/ |
||||
|
private func processWithOpenAI(text: String) { |
||||
|
processWithOpenAI(text: text, speakResponse: true) |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 使用OpenAI处理文本消息 |
||||
|
* |
||||
|
* - Parameters: |
||||
|
* - text: 用户输入文本 |
||||
|
* - speakResponse: 是否使用TTS朗读回复 |
||||
|
*/ |
||||
|
private func processWithOpenAI(text: String, speakResponse: Bool = true) { |
||||
|
print("\(TAG): 用户问题: \(text)") |
||||
|
|
||||
|
// 创建用户文本消息并处理 |
||||
|
guard let userMessage = openAIService?.createUserMessage(text: text) else { |
||||
|
print("\(TAG): 创建用户消息失败") |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
processWithOpenAIInternal(userMessage: userMessage, displayText: text, speakResponse: speakResponse) |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 使用OpenAI处理图片 |
||||
|
* |
||||
|
* - Parameters: |
||||
|
* - imagePath: 图片文件路径 |
||||
|
* - text: 可选的文本描述或问题 |
||||
|
* - speakResponse: 是否朗读回复 |
||||
|
* - Returns: 是否成功开始处理 |
||||
|
*/ |
||||
|
func processImageInput(imagePath: String, text: String = "", speakResponse: Bool = false) -> Bool { |
||||
|
if !isInitialized { |
||||
|
print("\(TAG): 服务未初始化") |
||||
|
sendEvent("error", ["code": "NOT_INITIALIZED", "message": "服务未初始化"]) |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
if imagePath.isEmpty { |
||||
|
print("\(TAG): 图片路径不能为空") |
||||
|
sendEvent("error", ["code": "EMPTY_IMAGE_PATH", "message": "图片路径不能为空"]) |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
// 通知开始处理图片 |
||||
|
sendEvent("image_processing", [ |
||||
|
"status": "processing", |
||||
|
"imagePath": imagePath |
||||
|
]) |
||||
|
|
||||
|
// 异步处理图片 |
||||
|
DispatchQueue.global(qos: .userInitiated).async { [weak self] in |
||||
|
guard let self = self else { return } |
||||
|
|
||||
|
// 将图片转换为Base64格式 |
||||
|
guard let imageBase64 = self.openAIService?.fileToBase64(filePath: imagePath) else { |
||||
|
DispatchQueue.main.async { |
||||
|
print("\(self.TAG): 图片转换失败: \(imagePath)") |
||||
|
self.sendEvent("error", [ |
||||
|
"code": "IMAGE_CONVERSION_FAILED", |
||||
|
"message": "图片转换失败" |
||||
|
]) |
||||
|
} |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
// 通知图片准备完成 |
||||
|
DispatchQueue.main.async { |
||||
|
self.sendEvent("image_ready", [ |
||||
|
"status": "ready", |
||||
|
"imagePath": imagePath |
||||
|
]) |
||||
|
|
||||
|
// 处理包含图片的消息 |
||||
|
self.processImageWithOpenAI(imageBase64: imageBase64, text: text, speakResponse: speakResponse) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return true |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 使用OpenAI处理图片 |
||||
|
* |
||||
|
* - Parameters: |
||||
|
* - imageBase64: Base64编码的图片数据 |
||||
|
* - text: 可选的文本描述或问题 |
||||
|
* - speakResponse: 是否朗读回复 |
||||
|
*/ |
||||
|
private func processImageWithOpenAI(imageBase64: String, text: String = "", speakResponse: Bool = false) { |
||||
|
print("\(TAG): 处理图片输入: \(text.isEmpty ? "无附加文本" : "附带文本: \(text)")") |
||||
|
|
||||
|
// 创建带图片的用户消息并处理 |
||||
|
guard let userMessage = openAIService?.createUserMessageWithImage(text: text, imageBase64: imageBase64) else { |
||||
|
print("\(TAG): 创建带图片的用户消息失败") |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
// 图片描述用于存储 |
||||
|
let displayText = text.isEmpty ? "[图片]" : text |
||||
|
processWithOpenAIInternal(userMessage: userMessage, displayText: displayText, speakResponse: speakResponse, hasImage: true) |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 内部方法:通用的OpenAI处理逻辑 |
||||
|
* |
||||
|
* - Parameters: |
||||
|
* - userMessage: 用户消息(可以是文本或图片格式) |
||||
|
* - displayText: 用于显示和存储的文本 |
||||
|
* - speakResponse: 是否朗读回复 |
||||
|
* - hasImage: 是否包含图片 |
||||
|
*/ |
||||
|
private func processWithOpenAIInternal(userMessage: [String: Any], displayText: String, speakResponse: Bool = true, hasImage: Bool = false) { |
||||
|
// 如果有正在进行的AI流式输出,先停止它 |
||||
|
stopAiStream() |
||||
|
|
||||
|
// 创建AI任务 |
||||
|
let workItem = DispatchWorkItem { [weak self] in |
||||
|
guard let self = self else { return } |
||||
|
|
||||
|
// 设置状态为正在流式输出 |
||||
|
self.isAiStreaming = true |
||||
|
|
||||
|
// 使用历史记录作为上下文发送到OpenAI |
||||
|
var responseBuilder = "" |
||||
|
|
||||
|
// 添加系统提示词到历史消息的副本中 |
||||
|
var messagesWithSystemPrompt: [[String: Any]] = [] |
||||
|
|
||||
|
// 先添加系统提示词 |
||||
|
if !self.systemPrompt.isEmpty { |
||||
|
if let systemMessage = self.openAIService?.createSystemMessage(text: self.systemPrompt) { |
||||
|
messagesWithSystemPrompt.append(systemMessage) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 再添加历史消息 |
||||
|
messagesWithSystemPrompt.append(contentsOf: self.historyMessages) |
||||
|
messagesWithSystemPrompt.append(userMessage) |
||||
|
|
||||
|
// 将用户消息添加到历史记录 |
||||
|
self.addToHistoryMessages(userMessage) |
||||
|
|
||||
|
// 流式回调 |
||||
|
class StreamCallbackBridge: NSObject, OpenAIStreamCallback { |
||||
|
private weak var service: AgentService? |
||||
|
private var responseBuilder: String |
||||
|
private let speakResponse: Bool |
||||
|
private let displayText: String |
||||
|
private let hasImage: Bool |
||||
|
|
||||
|
init(_ service: AgentService, responseBuilder: String = "", speakResponse: Bool, displayText: String, hasImage: Bool) { |
||||
|
self.service = service |
||||
|
self.responseBuilder = responseBuilder |
||||
|
self.speakResponse = speakResponse |
||||
|
self.displayText = displayText |
||||
|
self.hasImage = hasImage |
||||
|
super.init() |
||||
|
} |
||||
|
|
||||
|
func onToken(token: String) { |
||||
|
guard let service = service else { return } |
||||
|
|
||||
|
responseBuilder.append(token) |
||||
|
if speakResponse { |
||||
|
_ = service.azureTtsHelper?.speakStream(token) |
||||
|
} |
||||
|
// 发送流式回复token |
||||
|
service.sendEvent("assistant_token", ["token": token]) |
||||
|
} |
||||
|
|
||||
|
func onComplete() { |
||||
|
guard let service = service else { return } |
||||
|
|
||||
|
// 视情况决定是否朗读回复 |
||||
|
if speakResponse { |
||||
|
_ = service.azureTtsHelper?.flushStream() |
||||
|
} |
||||
|
|
||||
|
if !responseBuilder.isEmpty { |
||||
|
// 发送完整回复,包含是否有图片的标记 |
||||
|
var responseData: [String: Any] = [ |
||||
|
"text": responseBuilder, |
||||
|
"userInput": displayText |
||||
|
] |
||||
|
|
||||
|
if hasImage { |
||||
|
responseData["hasImage"] = true |
||||
|
} |
||||
|
|
||||
|
service.sendEvent("assistant_response", responseData) |
||||
|
|
||||
|
// 添加AI回复到历史记录 |
||||
|
if let assistantMessage = service.openAIService?.createAssistantMessage(text: responseBuilder) { |
||||
|
service.addToHistoryMessages(assistantMessage) |
||||
|
} |
||||
|
|
||||
|
// 保存聊天记录 |
||||
|
service.saveChatMessage(userMessage: displayText, assistantMessage: responseBuilder) |
||||
|
} |
||||
|
|
||||
|
// 标记AI流式输出已完成 |
||||
|
service.isAiStreaming = false |
||||
|
service.currentAiTask = nil |
||||
|
} |
||||
|
|
||||
|
func onError(error: Error) { |
||||
|
guard let service = service else { return } |
||||
|
|
||||
|
print("\(service.TAG): AI处理出错: \(error.localizedDescription)") |
||||
|
service.sendEvent("error", [ |
||||
|
"code": "AI_ERROR", |
||||
|
"message": error.localizedDescription |
||||
|
]) |
||||
|
|
||||
|
// 标记AI流式输出已完成 |
||||
|
service.isAiStreaming = false |
||||
|
service.currentAiTask = nil |
||||
|
} |
||||
|
|
||||
|
func onFunctionCall(call: [String: Any]) { |
||||
|
guard let service = service else { return } |
||||
|
|
||||
|
if let name = call["name"] as? String { |
||||
|
service.sendEvent("function_call", [ |
||||
|
"name": name, |
||||
|
"arguments": call |
||||
|
]) |
||||
|
|
||||
|
if name == "exit_interaction" { |
||||
|
service.stopRecognition() |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func onFunctionCallResult(functionCall: [String: Any], functionCallResult: [String: Any]) { |
||||
|
guard let service = service else { return } |
||||
|
|
||||
|
service.sendEvent("function_call_result", [ |
||||
|
"function_call": functionCall, |
||||
|
"result": functionCallResult, |
||||
|
]) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
let callback = StreamCallbackBridge(self, responseBuilder: responseBuilder, speakResponse: speakResponse, displayText: displayText, hasImage: hasImage) |
||||
|
|
||||
|
self.openAIService?.sendMessageStream(messages: messagesWithSystemPrompt, callback: callback) |
||||
|
} |
||||
|
|
||||
|
// 保存任务引用并在全局队列中执行 |
||||
|
currentAiTask = workItem |
||||
|
DispatchQueue.global(qos: .userInitiated).async(execute: workItem) |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 加载最近的聊天历史记录 |
||||
|
*/ |
||||
|
private func loadChatHistory() { |
||||
|
// 清空当前历史记录 |
||||
|
historyMessages.removeAll() |
||||
|
|
||||
|
// 使用ChatStorageHelper直接获取最近消息 |
||||
|
guard let recentMessages = chatStorageHelper?.getRecentMessages(sessionId: sessionId, count: 10) else { |
||||
|
print("\(TAG): 没有找到历史记录") |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
// 将消息添加到历史记录 |
||||
|
for message in recentMessages { |
||||
|
if let sender = message["sender"] as? String, |
||||
|
let content = message["message"] as? String { |
||||
|
|
||||
|
if sender == "user" { |
||||
|
if let userMessage = openAIService?.createUserMessage(text: content) { |
||||
|
addToHistoryMessages(userMessage) |
||||
|
} |
||||
|
} else if sender == "assistant" { |
||||
|
if let assistantMessage = openAIService?.createAssistantMessage(text: content) { |
||||
|
addToHistoryMessages(assistantMessage) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
print("\(TAG): 已加载\(recentMessages.count)条历史记录") |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 添加消息到历史记录,保持最近10条 |
||||
|
*/ |
||||
|
private func addToHistoryMessages(_ message: [String: Any]) { |
||||
|
// 添加新消息 |
||||
|
historyMessages.append(message) |
||||
|
|
||||
|
// 如果超过10条,删除最早的消息 |
||||
|
while historyMessages.count > 10 { |
||||
|
historyMessages.removeFirst() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* TTS播放函数 |
||||
|
* |
||||
|
* - Parameter text: 要播放的文本 |
||||
|
*/ |
||||
|
func speakText(text: String) { |
||||
|
if text.isEmpty { return } |
||||
|
|
||||
|
// 更新状态 |
||||
|
isTtsSpeaking = true |
||||
|
restartIdleCheck() // 状态变化,重启检测 |
||||
|
|
||||
|
// 播放文本 |
||||
|
_ = azureTtsHelper?.speakText(text) |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 停止TTS播放 |
||||
|
*/ |
||||
|
func stopTts() { |
||||
|
if isTtsSpeaking { |
||||
|
_ = azureTtsHelper?.stopSpeaking() |
||||
|
isTtsSpeaking = false |
||||
|
restartIdleCheck() // 状态变化,重启检测 |
||||
|
sendEvent("tts_stopped", ["status": "stopped"]) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 保存聊天消息 |
||||
|
*/ |
||||
|
private func saveChatMessage(userMessage: String, assistantMessage: String) { |
||||
|
DispatchQueue.global(qos: .background).async { [weak self] in |
||||
|
guard let self = self else { return } |
||||
|
|
||||
|
// 保存用户消息 |
||||
|
let userMessageId = self.chatStorageHelper?.saveMessage( |
||||
|
sessionId: self.sessionId, |
||||
|
message: userMessage, |
||||
|
sender: "user" |
||||
|
) ?? -1 |
||||
|
|
||||
|
if userMessageId != -1 { |
||||
|
// 保存AI回复 |
||||
|
let assistantMessageId = self.chatStorageHelper?.saveMessage( |
||||
|
sessionId: self.sessionId, |
||||
|
message: assistantMessage, |
||||
|
sender: "assistant" |
||||
|
) ?? -1 |
||||
|
|
||||
|
if assistantMessageId == -1 { |
||||
|
print("\(self.TAG): 保存助手消息失败") |
||||
|
} |
||||
|
} else { |
||||
|
print("\(self.TAG): 保存用户消息失败") |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 清除聊天历史 |
||||
|
*/ |
||||
|
func clearChatHistory(completion: @escaping (Bool) -> Void) { |
||||
|
DispatchQueue.global(qos: .background).async { [weak self] in |
||||
|
guard let self = self else { |
||||
|
DispatchQueue.main.async { |
||||
|
completion(false) |
||||
|
} |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
// 清除指定会话的所有消息 |
||||
|
let success = self.chatStorageHelper?.deleteMessages(sessionId: self.sessionId) ?? false |
||||
|
|
||||
|
if success { |
||||
|
// 清空内存中的历史记录 |
||||
|
self.historyMessages.removeAll() |
||||
|
print("\(self.TAG): 聊天历史已清除") |
||||
|
} else { |
||||
|
print("\(self.TAG): 清除聊天历史失败") |
||||
|
} |
||||
|
|
||||
|
DispatchQueue.main.async { |
||||
|
completion(success) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 发送事件 |
||||
|
*/ |
||||
|
private func sendEvent(_ eventName: String, _ data: [String: Any]) { |
||||
|
// 向所有监听器发送事件 |
||||
|
let allListeners = listeners.allObjects |
||||
|
for case let listener as AgentServiceListener in allListeners { |
||||
|
listener.onEvent(eventName: eventName, data: data) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 释放资源 |
||||
|
*/ |
||||
|
func dispose() { |
||||
|
stopRecognition() |
||||
|
stopTts() |
||||
|
stopAiStream() |
||||
|
stopIdleCheck() |
||||
|
|
||||
|
azureAsrHelper?.dispose() |
||||
|
azureTtsHelper?.dispose() |
||||
|
|
||||
|
// 清除所有监听器 |
||||
|
clearListeners() |
||||
|
|
||||
|
// 重置单例状态,以便下次使用时可以重新初始化 |
||||
|
isInitialized = false |
||||
|
|
||||
|
print("\(TAG): 代理服务资源已释放") |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// MARK: - AzureTtsHelper.TtsCallback |
||||
|
extension AgentService: AzureTtsHelper.TtsCallback { |
||||
|
func onSynthesisStarted() { |
||||
|
isTtsSpeaking = true |
||||
|
// 状态变化,重置空闲检测 |
||||
|
restartIdleCheck() |
||||
|
sendEvent("tts_started", ["status": "started"]) |
||||
|
} |
||||
|
|
||||
|
func onSynthesizing() { |
||||
|
// 可以在此添加TTS合成中的处理逻辑 |
||||
|
} |
||||
|
|
||||
|
func onSynthesisCompleted() { |
||||
|
isTtsSpeaking = false |
||||
|
// 状态变化,重启空闲检测 |
||||
|
restartIdleCheck() |
||||
|
sendEvent("tts_completed", ["status": "completed"]) |
||||
|
} |
||||
|
|
||||
|
func onSynthesisCanceled() { |
||||
|
isTtsSpeaking = false |
||||
|
// 状态变化,重启空闲检测 |
||||
|
restartIdleCheck() |
||||
|
sendEvent("tts_canceled", ["status": "canceled"]) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// MARK: - 外部插件桥接器 |
||||
|
|
||||
|
/// 桥接OpenAIService插件 |
||||
|
class OpenAIServiceBridge { |
||||
|
private let service = OpenAIService() |
||||
|
|
||||
|
/// 初始化服务 |
||||
|
func initialize(apiKey: String, baseUrl: String, model: String, mcpServer: String) { |
||||
|
service.initialize(apiKey: apiKey, baseUrl: baseUrl, model: model, mcpServer: mcpServer) |
||||
|
} |
||||
|
|
||||
|
/// 创建系统消息 |
||||
|
func createSystemMessage(text: String) -> [String: Any] { |
||||
|
return service.createSystemMessage(text) |
||||
|
} |
||||
|
|
||||
|
/// 创建用户消息 |
||||
|
func createUserMessage(text: String) -> [String: Any] { |
||||
|
return service.createUserMessage(text) |
||||
|
} |
||||
|
|
||||
|
/// 创建助手消息 |
||||
|
func createAssistantMessage(text: String) -> [String: Any] { |
||||
|
return service.createAssistantMessage(text) |
||||
|
} |
||||
|
|
||||
|
/// 创建带图片的用户消息 |
||||
|
func createUserMessageWithImage(text: String, imageBase64: String) -> [String: Any]? { |
||||
|
return service.createUserMessageWithImage(text: text, imageBase64: imageBase64) |
||||
|
} |
||||
|
|
||||
|
/// 文件转Base64 |
||||
|
func fileToBase64(filePath: String) -> String? { |
||||
|
return service.fileToBase64(filePath: filePath) |
||||
|
} |
||||
|
|
||||
|
/// 发送流式消息 |
||||
|
func sendMessageStream(messages: [[String: Any]], callback: OpenAIStreamCallback) { |
||||
|
service.sendMessageStream(messages: messages, callback: callback) |
||||
|
} |
||||
|
|
||||
|
/// 取消当前流 |
||||
|
func cancelCurrentStream() { |
||||
|
service.cancelCurrentStream() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// OpenAI流式回调协议 |
||||
|
@objc protocol OpenAIStreamCallback { |
||||
|
func onToken(token: String) |
||||
|
func onComplete() |
||||
|
func onError(error: Error) |
||||
|
func onFunctionCall(call: [String: Any]) |
||||
|
func onFunctionCallResult(functionCall: [String: Any], functionCallResult: [String: Any]) |
||||
|
} |
||||
|
|
||||
|
/// 桥接ChatStorageHelper插件 |
||||
|
class ChatStorageHelperBridge { |
||||
|
private let helper = ChatStorageHelper() |
||||
|
|
||||
|
/// 保存消息 |
||||
|
func saveMessage(sessionId: String, message: String, sender: String) -> Int64 { |
||||
|
return helper.saveMessage(sessionId: sessionId, message: message, sender: sender) |
||||
|
} |
||||
|
|
||||
|
/// 获取最近消息 |
||||
|
func getRecentMessages(sessionId: String, count: Int) -> [[String: Any]]? { |
||||
|
return helper.getRecentMessages(sessionId: sessionId, count: count) |
||||
|
} |
||||
|
|
||||
|
/// 删除消息 |
||||
|
func deleteMessages(sessionId: String) -> Bool { |
||||
|
return helper.deleteMessages(sessionId: sessionId) |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,11 @@ |
|||||
|
import Foundation |
||||
|
|
||||
|
/// 代理服务事件监听器协议 |
||||
|
/// 用于接收代理服务发送的各类事件 |
||||
|
protocol AgentServiceListener: AnyObject { |
||||
|
/// 事件回调 |
||||
|
/// - Parameters: |
||||
|
/// - eventName: 事件名称 |
||||
|
/// - data: 事件数据 |
||||
|
func onEvent(eventName: String, data: [String: Any]) |
||||
|
} |
||||
@ -0,0 +1,4 @@ |
|||||
|
#import <Flutter/Flutter.h> |
||||
|
|
||||
|
@interface AgentServicePlugin : NSObject<FlutterPlugin> |
||||
|
@end |
||||
@ -0,0 +1,12 @@ |
|||||
|
#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 { |
||||
|
[SwiftAgentServicePlugin registerWithRegistrar:registrar]; |
||||
|
} |
||||
|
@end |
||||
@ -0,0 +1,259 @@ |
|||||
|
import Flutter |
||||
|
import UIKit |
||||
|
import open_ai_service |
||||
|
import chat_storage |
||||
|
|
||||
|
/// Swift版Agent Service插件 |
||||
|
/// 处理Flutter与原生层的通信 |
||||
|
public class SwiftAgentServicePlugin: NSObject, FlutterPlugin { |
||||
|
// Flutter方法通道 |
||||
|
private var methodChannel: FlutterMethodChannel? |
||||
|
|
||||
|
// Flutter事件通道 |
||||
|
private var eventChannel: FlutterEventChannel? |
||||
|
|
||||
|
// 事件sink,用于向Flutter发送事件 |
||||
|
private var eventSink: FlutterEventSink? |
||||
|
|
||||
|
// 核心服务实例 |
||||
|
private var agentService: AgentService? |
||||
|
|
||||
|
/// 注册插件 |
||||
|
public static func register(with registrar: FlutterPluginRegistrar) { |
||||
|
let instance = SwiftAgentServicePlugin() |
||||
|
|
||||
|
// 初始化方法通道 |
||||
|
let methodChannel = FlutterMethodChannel(name: "com.yunqiinnovation.agent_service", binaryMessenger: registrar.messenger()) |
||||
|
registrar.addMethodCallDelegate(instance, channel: methodChannel) |
||||
|
instance.methodChannel = methodChannel |
||||
|
|
||||
|
// 初始化事件通道 |
||||
|
let eventChannel = FlutterEventChannel(name: "com.yunqiinnovation.agent_service/events", binaryMessenger: registrar.messenger()) |
||||
|
eventChannel.setStreamHandler(instance) |
||||
|
instance.eventChannel = eventChannel |
||||
|
} |
||||
|
|
||||
|
/// 处理方法调用 |
||||
|
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { |
||||
|
switch call.method { |
||||
|
case "initialize": |
||||
|
handleInitialize(call, result) |
||||
|
|
||||
|
case "startConversation": |
||||
|
handleStartConversation(result) |
||||
|
|
||||
|
case "stopConversation": |
||||
|
handleStopConversation(result) |
||||
|
|
||||
|
case "processTextInput": |
||||
|
handleProcessTextInput(call, result) |
||||
|
|
||||
|
case "speakText": |
||||
|
handleSpeakText(call, result) |
||||
|
|
||||
|
case "stopTts": |
||||
|
handleStopTts(result) |
||||
|
|
||||
|
case "clearChatHistory": |
||||
|
handleClearChatHistory(result) |
||||
|
|
||||
|
case "interruptCurrentResponse": |
||||
|
handleInterruptCurrentResponse(result) |
||||
|
|
||||
|
case "processImageInput": |
||||
|
handleProcessImageInput(call, result) |
||||
|
|
||||
|
case "dispose": |
||||
|
handleDispose(result) |
||||
|
|
||||
|
default: |
||||
|
result(FlutterMethodNotImplemented) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// MARK: - 方法处理 |
||||
|
|
||||
|
/// 初始化 |
||||
|
private func handleInitialize(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { |
||||
|
guard let args = call.arguments as? [String: Any], |
||||
|
let config = args["config"] as? [String: Any] else { |
||||
|
result(FlutterError(code: "INVALID_ARGUMENTS", message: "缺少配置参数", details: nil)) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
// 初始化代理服务 |
||||
|
if agentService == nil { |
||||
|
agentService = AgentService() |
||||
|
|
||||
|
// 设置事件监听器 |
||||
|
agentService?.setListener(self) |
||||
|
} |
||||
|
|
||||
|
// 执行初始化 |
||||
|
let success = agentService?.initialize(config: config) ?? false |
||||
|
result(success) |
||||
|
} |
||||
|
|
||||
|
/// 开始对话 |
||||
|
private func handleStartConversation(_ result: @escaping FlutterResult) { |
||||
|
guard let service = agentService else { |
||||
|
result(FlutterError(code: "NOT_INITIALIZED", message: "AgentService尚未初始化", details: nil)) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
let success = service.startRecognition() |
||||
|
result(success) |
||||
|
} |
||||
|
|
||||
|
/// 停止对话 |
||||
|
private func handleStopConversation(_ result: @escaping FlutterResult) { |
||||
|
guard let service = agentService else { |
||||
|
result(FlutterError(code: "NOT_INITIALIZED", message: "AgentService尚未初始化", details: nil)) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
service.stopRecognition() |
||||
|
result(true) |
||||
|
} |
||||
|
|
||||
|
/// 处理文本输入 |
||||
|
private func handleProcessTextInput(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { |
||||
|
guard let service = agentService else { |
||||
|
result(FlutterError(code: "NOT_INITIALIZED", message: "AgentService尚未初始化", details: nil)) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
guard let args = call.arguments as? [String: Any], |
||||
|
let text = args["text"] as? String else { |
||||
|
result(FlutterError(code: "INVALID_ARGUMENTS", message: "无效的文本参数", details: nil)) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
let speakResponse = (args["speakResponse"] as? Bool) ?? false |
||||
|
let success = service.processTextInput(text: text, speakResponse: speakResponse) |
||||
|
result(success) |
||||
|
} |
||||
|
|
||||
|
/// 朗读文本 |
||||
|
private func handleSpeakText(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { |
||||
|
guard let service = agentService else { |
||||
|
result(FlutterError(code: "NOT_INITIALIZED", message: "AgentService尚未初始化", details: nil)) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
guard let args = call.arguments as? [String: Any], |
||||
|
let text = args["text"] as? String else { |
||||
|
result(FlutterError(code: "INVALID_ARGUMENTS", message: "无效的文本参数", details: nil)) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
service.speakText(text: text) |
||||
|
result(true) |
||||
|
} |
||||
|
|
||||
|
/// 停止语音合成 |
||||
|
private func handleStopTts(_ result: @escaping FlutterResult) { |
||||
|
guard let service = agentService else { |
||||
|
result(FlutterError(code: "NOT_INITIALIZED", message: "AgentService尚未初始化", details: nil)) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
service.stopTts() |
||||
|
result(true) |
||||
|
} |
||||
|
|
||||
|
/// 清除聊天历史 |
||||
|
private func handleClearChatHistory(_ result: @escaping FlutterResult) { |
||||
|
guard let service = agentService else { |
||||
|
result(FlutterError(code: "NOT_INITIALIZED", message: "AgentService尚未初始化", details: nil)) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
service.clearChatHistory { success in |
||||
|
result(success) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 中断当前响应 |
||||
|
private func handleInterruptCurrentResponse(_ result: @escaping FlutterResult) { |
||||
|
guard let service = agentService else { |
||||
|
result(FlutterError(code: "NOT_INITIALIZED", message: "AgentService尚未初始化", details: nil)) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
service.interruptCurrentResponse() |
||||
|
result(true) |
||||
|
} |
||||
|
|
||||
|
/// 处理图片输入 |
||||
|
private func handleProcessImageInput(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { |
||||
|
guard let service = agentService else { |
||||
|
result(FlutterError(code: "NOT_INITIALIZED", message: "AgentService尚未初始化", details: nil)) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
guard let args = call.arguments as? [String: Any], |
||||
|
let imagePath = args["imagePath"] as? String else { |
||||
|
result(FlutterError(code: "INVALID_ARGUMENTS", message: "无效的图片路径参数", details: nil)) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
let text = (args["text"] as? String) ?? "" |
||||
|
let speakResponse = (args["speakResponse"] as? Bool) ?? false |
||||
|
|
||||
|
let success = service.processImageInput(imagePath: imagePath, text: text, speakResponse: speakResponse) |
||||
|
result(success) |
||||
|
} |
||||
|
|
||||
|
/// 释放资源 |
||||
|
private func handleDispose(_ result: @escaping FlutterResult) { |
||||
|
guard let service = agentService else { |
||||
|
result(true) // 已经是nil,直接返回成功 |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
service.dispose() |
||||
|
agentService = nil |
||||
|
|
||||
|
result(true) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// MARK: - FlutterStreamHandler |
||||
|
extension SwiftAgentServicePlugin: FlutterStreamHandler { |
||||
|
public func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { |
||||
|
self.eventSink = events |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
public func onCancel(withArguments arguments: Any?) -> FlutterError? { |
||||
|
self.eventSink = nil |
||||
|
return nil |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// MARK: - AgentServiceListener |
||||
|
extension SwiftAgentServicePlugin: AgentServiceListener { |
||||
|
func onEvent(eventName: String, data: [String: Any]) { |
||||
|
guard let sink = eventSink else { return } |
||||
|
|
||||
|
do { |
||||
|
// 构建事件JSON |
||||
|
let eventMap: [String: Any] = [ |
||||
|
"event": eventName, |
||||
|
"data": data |
||||
|
] |
||||
|
|
||||
|
// 将事件转换为JSON字符串 |
||||
|
let jsonData = try JSONSerialization.data(withJSONObject: eventMap, options: []) |
||||
|
if let jsonString = String(data: jsonData, encoding: .utf8) { |
||||
|
// 发送到Flutter |
||||
|
DispatchQueue.main.async { |
||||
|
sink(jsonString) |
||||
|
} |
||||
|
} |
||||
|
} catch { |
||||
|
print("事件序列化失败: \(error.localizedDescription)") |
||||
|
} |
||||
|
} |
||||
|
} |
||||
File diff suppressed because it is too large
@ -0,0 +1,15 @@ |
|||||
|
<?xml version="1.0" encoding="UTF-8"?> |
||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> |
||||
|
<plist version="1.0"> |
||||
|
<dict> |
||||
|
<key>NSBluetoothAlwaysUsageDescription</key> |
||||
|
<string>需要蓝牙权限与设备进行通信和传输音频数据</string> |
||||
|
<key>NSBluetoothPeripheralUsageDescription</key> |
||||
|
<string>需要蓝牙权限与设备进行通信和传输音频数据</string> |
||||
|
<key>UIBackgroundModes</key> |
||||
|
<array> |
||||
|
<string>bluetooth-central</string> |
||||
|
<string>audio</string> |
||||
|
</array> |
||||
|
</dict> |
||||
|
</plist> |
||||
@ -0,0 +1,90 @@ |
|||||
|
import Foundation |
||||
|
import CoreBluetooth |
||||
|
|
||||
|
/// BLE服务常量 |
||||
|
struct BleConst { |
||||
|
// ================== 协议相关UUID ================== |
||||
|
/// 主服务UUID - 0000ABC0-0000-1111-2222-123456789ABC |
||||
|
static let PRIMARY_SERVICE_UUID = CBUUID(string: "0000abc0-0000-1111-2222-123456789abc") |
||||
|
|
||||
|
/// 音频服务UUID - 00001801-0000-1000-8000-00805f9b34fb |
||||
|
static let AUDIO_SERVICE_UUID = CBUUID(string: "0000ae00-0000-1000-8000-00805f9b34fb") |
||||
|
|
||||
|
/// 接收音频特征UUID - 0000ABC1-0000-1111-2222-123456789ABC |
||||
|
static let RECEIVE_AUDIO_CHAR_UUID = CBUUID(string: "0000ae02-0000-1000-8000-00805f9b34fb") |
||||
|
|
||||
|
/// 写入特征UUID - 0000ABC1-0000-1111-2222-123456789ABC |
||||
|
static let WRITE_CHAR_UUID = CBUUID(string: "0000abc1-0000-1111-2222-123456789abc") |
||||
|
|
||||
|
/// 通知特征UUID - 0000ABC2-0000-1111-2222-123456789ABC |
||||
|
static let NOTIFY_CHAR_UUID = CBUUID(string: "0000abc2-0000-1111-2222-123456789abc") |
||||
|
|
||||
|
/// 客户端特征配置描述符UUID |
||||
|
static let CLIENT_CHAR_CONFIG_UUID = CBUUID(string: "0000fd7b-0000-1000-8000-00805f9b34fb") |
||||
|
|
||||
|
/// BLE扫描Action |
||||
|
static let ACTION_SCAN_RESULT = "com.yunqiinnovation.ble_service.ACTION_BLE_SCAN" |
||||
|
|
||||
|
// ================== 连接状态 ================== |
||||
|
static let STATE_DISCONNECTED = 0 |
||||
|
static let STATE_CONNECTING = 1 |
||||
|
static let STATE_CONNECTED = 2 |
||||
|
static let STATE_DISCONNECTING = 3 |
||||
|
static let STATE_ERROR = 4 |
||||
|
|
||||
|
// ================== 广播厂商信息 ================== |
||||
|
/// 厂商ID - 0xABEF |
||||
|
static let MANUFACTURER_ID: UInt16 = 0xABEF |
||||
|
/// 客户ID - 0xA001 |
||||
|
static let CUSTOMER_ID: UInt16 = 0xA001 |
||||
|
/// 产品ID - 0xA001 |
||||
|
static let PRODUCT_ID: UInt16 = 0xA001 |
||||
|
/// 蓝牙状态-未连接 |
||||
|
static let BT_STATE_DISCONNECTED: UInt8 = 0x00 |
||||
|
/// 蓝牙状态-已连接 |
||||
|
static let BT_STATE_CONNECTED: UInt8 = 0x0C |
||||
|
|
||||
|
// ================== 协议格式 ================== |
||||
|
/// APP请求帧头 |
||||
|
static let APP_REQUEST_HEADER: UInt8 = 0xAA |
||||
|
/// 设备响应帧头 |
||||
|
static let DEVICE_RESPONSE_HEADER: UInt8 = 0xBB |
||||
|
/// 设备主动上报帧头 |
||||
|
static let DEVICE_NOTIFY_HEADER: UInt8 = 0xCC |
||||
|
|
||||
|
// ================== 协议指令 (CMD) ================== |
||||
|
/// 获取耳机版本号 |
||||
|
static let CMD_GET_VERSION: UInt8 = 0x01 |
||||
|
/// 获取耳机产品ID |
||||
|
static let CMD_GET_PRODUCT_ID: UInt8 = 0x02 |
||||
|
/// 获取耳机颜色ID |
||||
|
static let CMD_GET_COLOR_ID: UInt8 = 0x03 |
||||
|
/// 获取耳机电量ID |
||||
|
static let CMD_GET_BATTERY_INFO: UInt8 = 0x04 |
||||
|
/// APP控制编解码指令 |
||||
|
static let CMD_CONTROL_CODEC: UInt8 = 0x05 |
||||
|
/// 音量增大 |
||||
|
static let CMD_VOLUME_UP: UInt8 = 0x06 |
||||
|
/// 音量减小 |
||||
|
static let CMD_VOLUME_DOWN: UInt8 = 0x07 |
||||
|
/// A2DP播放 |
||||
|
static let CMD_PLAY_A2DP: UInt8 = 0x08 |
||||
|
/// 唤醒指令 |
||||
|
static let CMD_WAKE_UP: UInt8 = 0x11 |
||||
|
|
||||
|
// ================== 编解码控制值 ================== |
||||
|
/// 关闭编解码指令 |
||||
|
static let CODEC_CONTROL_CLOSE: UInt8 = 0x00 |
||||
|
/// 打开解码指令 |
||||
|
static let CODEC_CONTROL_DECODE_ON: UInt8 = 0xA1 |
||||
|
/// 通过A2DP播放 |
||||
|
static let CODEC_CONTROL_A2DP_PLAY: UInt8 = 0xA2 |
||||
|
/// 打开编码指令 |
||||
|
static let CODEC_CONTROL_ENCODE_ON: UInt8 = 0xB1 |
||||
|
/// 左声道 |
||||
|
static let AUDIO_CHANNEL_LEFT: UInt8 = 0x01 |
||||
|
/// 右声道 |
||||
|
static let AUDIO_CHANNEL_RIGHT: UInt8 = 0x02 |
||||
|
/// 立体声 |
||||
|
static let AUDIO_CHANNEL_STEREO: UInt8 = 0x03 |
||||
|
} |
||||
@ -0,0 +1,25 @@ |
|||||
|
import Foundation |
||||
|
|
||||
|
/// BLE服务回调代理协议 |
||||
|
protocol BleDelegate: AnyObject { |
||||
|
/// 扫描结果回调 |
||||
|
/// - Parameter devices: 设备列表,每个设备包含id、name、rssi和uuids等信息 |
||||
|
func onScanResult(devices: [[String: Any]]) |
||||
|
|
||||
|
/// 连接状态改变回调 |
||||
|
/// - Parameter state: 连接状态,参考BleConst中的STATE_*常量 |
||||
|
func onConnectionStateChanged(state: Int) |
||||
|
|
||||
|
/// 音频数据接收回调 |
||||
|
/// - Parameter data: 接收到的音频数据 |
||||
|
func onAudioDataReceived(data: Data) |
||||
|
|
||||
|
/// 唤醒信号接收回调 |
||||
|
func onWakeupSignalReceived() |
||||
|
|
||||
|
/// 设备信息接收回调 |
||||
|
/// - Parameters: |
||||
|
/// - infoType: 信息类型 |
||||
|
/// - infoData: 信息数据 |
||||
|
func onDeviceInfoReceived(infoType: Int, infoData: [String: Any]) |
||||
|
} |
||||
@ -0,0 +1,963 @@ |
|||||
|
import Foundation |
||||
|
import CoreBluetooth |
||||
|
import os.log |
||||
|
|
||||
|
/// BLE服务类 - 单例实现 |
||||
|
actor BleService { |
||||
|
// MARK: - 单例实现 |
||||
|
static let shared = BleService() |
||||
|
|
||||
|
// MARK: - 常量 |
||||
|
|
||||
|
/// 用于状态恢复的唯一标识符 |
||||
|
private let restoreIdentifier = "com.yunqiinnovation.ble_service.restore" |
||||
|
|
||||
|
private init() { |
||||
|
// 初始化连接状态 |
||||
|
connectionState = BleConst.STATE_DISCONNECTED |
||||
|
|
||||
|
// 将通知处理移到非actor方法中 |
||||
|
setupNotifications() |
||||
|
} |
||||
|
|
||||
|
deinit { |
||||
|
// 移除通知观察者 |
||||
|
for observer in notificationObservers { |
||||
|
NotificationCenter.default.removeObserver(observer) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// MARK: - 属性 |
||||
|
|
||||
|
/// 中央管理器 |
||||
|
private var centralManager: CBCentralManager? |
||||
|
|
||||
|
/// 当前连接的外设 |
||||
|
private var peripheral: CBPeripheral? |
||||
|
|
||||
|
/// 主服务特征 |
||||
|
private var writeCharacteristic: CBCharacteristic? |
||||
|
private var notifyCharacteristic: CBCharacteristic? |
||||
|
private var audioCharacteristic: CBCharacteristic? |
||||
|
|
||||
|
/// 连接状态 |
||||
|
private(set) var connectionState: Int { |
||||
|
didSet { |
||||
|
if oldValue != connectionState { |
||||
|
Task { await notifyConnectionStateChanged(state: connectionState) } |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 是否正在扫描 |
||||
|
private(set) var isScanning = false |
||||
|
|
||||
|
/// 扫描结果缓存 |
||||
|
private var scanResults = [CBPeripheral: [String: Any]]() |
||||
|
|
||||
|
/// 扫描超时任务 |
||||
|
private var scanTimeoutTask: Task<Void, Never>? |
||||
|
|
||||
|
/// 是否已初始化 |
||||
|
private var isInitialized = false |
||||
|
|
||||
|
/// 回调代理列表 - 使用actor保证线程安全 |
||||
|
private var delegates = [BleDelegate]() |
||||
|
|
||||
|
/// 日志 |
||||
|
private let logger = OSLog(subsystem: "com.yunqiinnovation.ble_service", category: "BleService") |
||||
|
|
||||
|
/// 通知观察者 |
||||
|
private var notificationObservers = [NSObjectProtocol]() |
||||
|
|
||||
|
// MARK: - 通知设置 |
||||
|
|
||||
|
/// 设置应用生命周期通知 |
||||
|
private nonisolated func setupNotifications() { |
||||
|
// 使用nonisolated函数以避免actor隔离问题 |
||||
|
let becomeActiveObserver = NotificationCenter.default.addObserver( |
||||
|
forName: UIApplication.didBecomeActiveNotification, |
||||
|
object: nil, |
||||
|
queue: .main |
||||
|
) { [weak self] _ in |
||||
|
guard let self = self else { return } |
||||
|
Task { |
||||
|
await self.handleAppDidBecomeActive() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
let enterBackgroundObserver = NotificationCenter.default.addObserver( |
||||
|
forName: UIApplication.didEnterBackgroundNotification, |
||||
|
object: nil, |
||||
|
queue: .main |
||||
|
) { [weak self] _ in |
||||
|
guard let self = self else { return } |
||||
|
Task { |
||||
|
await self.handleAppDidEnterBackground() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 存储观察者以便后续移除 |
||||
|
notificationObservers = [becomeActiveObserver, enterBackgroundObserver] |
||||
|
} |
||||
|
|
||||
|
// MARK: - 公共方法 |
||||
|
|
||||
|
/// 初始化BLE服务 |
||||
|
/// - Returns: 是否初始化成功 |
||||
|
func initialize() -> Bool { |
||||
|
if isInitialized { |
||||
|
return true |
||||
|
} |
||||
|
|
||||
|
do { |
||||
|
// 初始化中央管理器 - 配置状态恢复选项 |
||||
|
let options: [String: Any] = [ |
||||
|
CBCentralManagerOptionRestoreIdentifierKey: restoreIdentifier, |
||||
|
CBCentralManagerOptionShowPowerAlertKey: true |
||||
|
] |
||||
|
|
||||
|
centralManager = CBCentralManager( |
||||
|
delegate: CBManagerHandler.shared, |
||||
|
queue: nil, |
||||
|
options: options |
||||
|
) |
||||
|
|
||||
|
CBManagerHandler.shared.bleService = self |
||||
|
|
||||
|
isInitialized = true |
||||
|
os_log("BLE服务初始化成功", log: logger, type: .info) |
||||
|
return true |
||||
|
} catch { |
||||
|
os_log("BLE服务初始化失败: %{public}@", log: logger, type: .error, error.localizedDescription) |
||||
|
return false |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 添加回调代理 |
||||
|
/// - Parameter delegate: 要添加的代理 |
||||
|
/// - Returns: 是否添加成功 |
||||
|
func addDelegate(_ delegate: BleDelegate) -> Bool { |
||||
|
if !delegates.contains(where: { $0 === delegate }) { |
||||
|
delegates.append(delegate) |
||||
|
os_log("已添加BLE回调, 当前回调数: %d", log: logger, type: .debug, delegates.count) |
||||
|
return true |
||||
|
} |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
/// 移除回调代理 |
||||
|
/// - Parameter delegate: 要移除的代理 |
||||
|
/// - Returns: 是否移除成功 |
||||
|
func removeDelegate(_ delegate: BleDelegate) -> Bool { |
||||
|
if let index = delegates.firstIndex(where: { $0 === delegate }) { |
||||
|
delegates.remove(at: index) |
||||
|
os_log("已移除BLE回调, 当前回调数: %d", log: logger, type: .debug, delegates.count) |
||||
|
return true |
||||
|
} |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
/// 清除所有回调代理 |
||||
|
func clearDelegates() { |
||||
|
delegates.removeAll() |
||||
|
os_log("已清除所有BLE回调", log: logger, type: .debug) |
||||
|
} |
||||
|
|
||||
|
/// 检查是否已连接 |
||||
|
/// - Returns: 是否已连接 |
||||
|
func isConnected() -> Bool { |
||||
|
return connectionState == BleConst.STATE_CONNECTED |
||||
|
} |
||||
|
|
||||
|
// MARK: - 扫描方法 |
||||
|
|
||||
|
/// 开始主动扫描BLE设备 |
||||
|
/// - Parameter timeout: 扫描超时时间(秒) |
||||
|
/// - Returns: 是否成功开始扫描 |
||||
|
func startScan(timeout: TimeInterval = 15.0) -> Bool { |
||||
|
if !isInitialized { |
||||
|
os_log("BLE服务未初始化", log: logger, type: .error) |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
if isScanning { |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
guard let manager = centralManager, manager.state == .poweredOn else { |
||||
|
os_log("蓝牙未开启", log: logger, type: .error) |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
os_log("开始主动扫描BLE设备...", log: logger, type: .info) |
||||
|
unregisterBackgroundScan() // 避免双重扫描 |
||||
|
scanResults.removeAll() |
||||
|
|
||||
|
// 扫描选项 - 允许重复结果,以获取最新的RSSI值 |
||||
|
let options: [String: Any] = [ |
||||
|
CBCentralManagerScanOptionAllowDuplicatesKey: true |
||||
|
] |
||||
|
|
||||
|
// 设置过滤服务UUID |
||||
|
let scanFilters = [ |
||||
|
// 如果需要过滤,添加CBUUID |
||||
|
// CBUUID(string: BleConst.PRIMARY_SERVICE_UUID.uuidString) |
||||
|
] |
||||
|
|
||||
|
// 使用过滤器开始扫描 |
||||
|
manager.scanForPeripherals( |
||||
|
withServices: scanFilters.isEmpty ? nil : scanFilters, |
||||
|
options: options |
||||
|
) |
||||
|
|
||||
|
isScanning = true |
||||
|
|
||||
|
// 设置扫描超时 |
||||
|
scanTimeoutTask?.cancel() |
||||
|
scanTimeoutTask = Task { [weak self] in |
||||
|
guard let self = self else { return } |
||||
|
|
||||
|
do { |
||||
|
try await Task.sleep(nanoseconds: UInt64(timeout * 1_000_000_000)) |
||||
|
if !Task.isCancelled { |
||||
|
await self.stopScan() |
||||
|
} |
||||
|
} catch { |
||||
|
// 任务被取消,忽略错误 |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return true |
||||
|
} |
||||
|
|
||||
|
/// 停止主动扫描 |
||||
|
/// - Returns: 是否成功停止扫描 |
||||
|
func stopScan() -> Bool { |
||||
|
if !isScanning { |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
os_log("停止主动扫描", log: logger, type: .info) |
||||
|
|
||||
|
centralManager?.stopScan() |
||||
|
scanTimeoutTask?.cancel() |
||||
|
scanTimeoutTask = nil |
||||
|
isScanning = false |
||||
|
|
||||
|
// 重新注册后台扫描,保证唤醒能力 |
||||
|
registerBackgroundScan() |
||||
|
|
||||
|
return true |
||||
|
} |
||||
|
|
||||
|
/// 注册后台扫描 |
||||
|
/// - Returns: 是否成功注册 |
||||
|
func registerBackgroundScan() -> Bool { |
||||
|
// iOS中无法直接实现与Android相同的后台扫描唤醒功能 |
||||
|
os_log("iOS不支持直接的后台扫描唤醒,将使用状态保存和恢复功能", log: logger, type: .info) |
||||
|
return true |
||||
|
} |
||||
|
|
||||
|
/// 注销后台扫描 |
||||
|
/// - Returns: 是否成功注销 |
||||
|
func unregisterBackgroundScan() -> Bool { |
||||
|
// iOS没有直接的后台扫描注销机制 |
||||
|
os_log("iOS不支持直接的后台扫描注销", log: logger, type: .info) |
||||
|
return true |
||||
|
} |
||||
|
|
||||
|
// MARK: - 连接方法 |
||||
|
|
||||
|
/// 连接到指定地址的BLE设备 |
||||
|
/// - Parameter address: 设备UUID字符串 |
||||
|
func connect(address: String) async { |
||||
|
if connectionState == BleConst.STATE_CONNECTED { |
||||
|
os_log("已经连接中,忽略连接请求", log: logger, type: .warning) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
guard let central = centralManager, central.state == .poweredOn else { |
||||
|
updateConnectionState(BleConst.STATE_ERROR) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
// 关闭现有连接 |
||||
|
disconnectGatt() |
||||
|
|
||||
|
// 查找对应的外设 |
||||
|
if let uuid = UUID(uuidString: address), |
||||
|
let targetPeripheral = scanResults.keys.first(where: { $0.identifier == uuid }) { |
||||
|
// 如果在扫描结果中找到,直接连接 |
||||
|
os_log("从扫描结果中找到目标设备,开始连接...", log: logger, type: .debug) |
||||
|
await connectToPeripheral(targetPeripheral) |
||||
|
} else { |
||||
|
// 如果没有找到,尝试从已知设备中恢复 |
||||
|
os_log("尝试使用UUID恢复设备连接: %{public}@", log: logger, type: .debug, address) |
||||
|
if let uuid = UUID(uuidString: address) { |
||||
|
let peripherals = central.retrievePeripherals(withIdentifiers: [uuid]) |
||||
|
if let targetPeripheral = peripherals.first { |
||||
|
await connectToPeripheral(targetPeripheral) |
||||
|
} else { |
||||
|
os_log("未找到指定UUID的设备: %{public}@", log: logger, type: .error, address) |
||||
|
updateConnectionState(BleConst.STATE_ERROR) |
||||
|
} |
||||
|
} else { |
||||
|
os_log("无效的UUID格式: %{public}@", log: logger, type: .error, address) |
||||
|
updateConnectionState(BleConst.STATE_ERROR) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 连接到指定外设 |
||||
|
/// - Parameter peripheral: 要连接的外设 |
||||
|
private func connectToPeripheral(_ peripheral: CBPeripheral) async { |
||||
|
self.peripheral = peripheral |
||||
|
peripheral.delegate = CBPeripheralHandler.shared |
||||
|
CBPeripheralHandler.shared.bleService = self |
||||
|
updateConnectionState(BleConst.STATE_CONNECTING) |
||||
|
centralManager?.connect(peripheral, options: nil) |
||||
|
} |
||||
|
|
||||
|
/// 断开当前连接 |
||||
|
func disconnect() async { |
||||
|
guard let peripheral = peripheral, let central = centralManager else { |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
updateConnectionState(BleConst.STATE_DISCONNECTING) |
||||
|
central.cancelPeripheralConnection(peripheral) |
||||
|
} |
||||
|
|
||||
|
/// 安全关闭GATT连接 |
||||
|
private func disconnectGatt() { |
||||
|
if let peripheral = peripheral, let central = centralManager { |
||||
|
central.cancelPeripheralConnection(peripheral) |
||||
|
} |
||||
|
|
||||
|
peripheral = nil |
||||
|
writeCharacteristic = nil |
||||
|
notifyCharacteristic = nil |
||||
|
audioCharacteristic = nil |
||||
|
} |
||||
|
|
||||
|
/// 更新连接状态并通知回调 |
||||
|
private func updateConnectionState(_ state: Int) { |
||||
|
connectionState = state |
||||
|
} |
||||
|
|
||||
|
// MARK: - 设备通信方法 |
||||
|
|
||||
|
/// 获取设备版本号 |
||||
|
/// - Returns: 是否成功发送请求 |
||||
|
func getDeviceVersion() -> Bool { |
||||
|
return sendCommand(BleConst.CMD_GET_VERSION) |
||||
|
} |
||||
|
|
||||
|
/// 获取电池信息 |
||||
|
/// - Returns: 是否成功发送请求 |
||||
|
func getBatteryInfo() -> Bool { |
||||
|
return sendCommand(BleConst.CMD_GET_BATTERY_INFO) |
||||
|
} |
||||
|
|
||||
|
/// 打开编码器 |
||||
|
/// - Returns: 是否成功发送请求 |
||||
|
func openEncoder() -> Bool { |
||||
|
return sendCodecControl(BleConst.CODEC_CONTROL_ENCODE_ON) |
||||
|
} |
||||
|
|
||||
|
/// 打开解码器 |
||||
|
/// - Returns: 是否成功发送请求 |
||||
|
func openDecoder() -> Bool { |
||||
|
return sendCodecControl(BleConst.CODEC_CONTROL_DECODE_ON) |
||||
|
} |
||||
|
|
||||
|
/// 关闭编解码器 |
||||
|
/// - Returns: 是否成功发送请求 |
||||
|
func closeCodec() -> Bool { |
||||
|
return sendCodecControl(BleConst.CODEC_CONTROL_CLOSE) |
||||
|
} |
||||
|
|
||||
|
/// 播放A2DP |
||||
|
/// - Returns: 是否成功发送请求 |
||||
|
func playA2DP() -> Bool { |
||||
|
return sendCodecControl(BleConst.CODEC_CONTROL_A2DP_PLAY) |
||||
|
} |
||||
|
|
||||
|
/// 发送编解码控制指令 |
||||
|
/// - Parameter control: 控制值 |
||||
|
/// - Returns: 是否成功发送 |
||||
|
private func sendCodecControl(_ control: UInt8) -> Bool { |
||||
|
let data = Data([BleConst.APP_REQUEST_HEADER, BleConst.CMD_CONTROL_CODEC, control]) |
||||
|
return writeData(data) |
||||
|
} |
||||
|
|
||||
|
/// 发送命令 |
||||
|
/// - Parameter command: 命令字 |
||||
|
/// - Returns: 是否成功发送 |
||||
|
private func sendCommand(_ command: UInt8) -> Bool { |
||||
|
let data = Data([BleConst.APP_REQUEST_HEADER, command]) |
||||
|
return writeData(data) |
||||
|
} |
||||
|
|
||||
|
/// 写入数据 |
||||
|
/// - Parameter data: 要写入的数据 |
||||
|
/// - Returns: 是否成功写入 |
||||
|
private func writeData(_ data: Data) -> Bool { |
||||
|
guard let peripheral = peripheral, |
||||
|
let characteristic = writeCharacteristic, |
||||
|
connectionState == BleConst.STATE_CONNECTED else { |
||||
|
os_log("无法写入数据,设备未连接或特征未找到", log: logger, type: .error) |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
let hexString = data.map { String(format: "0x%02X", $0) }.joined(separator: " ") |
||||
|
os_log("写入数据: %{public}@", log: logger, type: .debug, hexString) |
||||
|
|
||||
|
peripheral.writeValue(data, for: characteristic, type: .withoutResponse) |
||||
|
return true |
||||
|
} |
||||
|
|
||||
|
// MARK: - 数据处理方法 |
||||
|
|
||||
|
/// 处理音频数据 |
||||
|
/// - Parameter data: 音频数据 |
||||
|
func processAudioData(_ data: Data) async { |
||||
|
os_log("收到音频数据: %d字节", log: logger, type: .debug, data.count) |
||||
|
await notifyAudioDataReceived(data: data) |
||||
|
} |
||||
|
|
||||
|
/// 处理设备响应 |
||||
|
/// - Parameter data: 设备响应数据 |
||||
|
func processDeviceResponse(_ data: Data) async { |
||||
|
guard data.count >= 2 else { return } |
||||
|
|
||||
|
let cmd = data[1] |
||||
|
os_log("收到设备响应: CMD=0x%02X", log: logger, type: .debug, cmd) |
||||
|
|
||||
|
switch cmd { |
||||
|
case BleConst.CMD_GET_VERSION: |
||||
|
if data.count >= 3 { |
||||
|
// 解析版本号 |
||||
|
let version = String(format: "%d.%d.%d", data[2], data.count > 3 ? data[3] : 0, data.count > 4 ? data[4] : 0) |
||||
|
let infoData: [String: Any] = ["version": version] |
||||
|
await notifyDeviceInfoReceived(infoType: Int(cmd), infoData: infoData) |
||||
|
} |
||||
|
case BleConst.CMD_GET_BATTERY_INFO: |
||||
|
if data.count >= 3 { |
||||
|
// 解析电池电量 |
||||
|
let batteryLevel = Int(data[2]) |
||||
|
let infoData: [String: Any] = ["level": batteryLevel] |
||||
|
await notifyDeviceInfoReceived(infoType: Int(cmd), infoData: infoData) |
||||
|
} |
||||
|
default: |
||||
|
os_log("未处理的响应命令: 0x%02X", log: logger, type: .debug, cmd) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 处理设备通知 |
||||
|
/// - Parameter data: 设备通知数据 |
||||
|
func processDeviceNotification(_ data: Data) async { |
||||
|
guard data.count >= 2 else { return } |
||||
|
|
||||
|
let notifyType = data[1] |
||||
|
os_log("收到设备通知: Type=0x%02X", log: logger, type: .debug, notifyType) |
||||
|
|
||||
|
switch notifyType { |
||||
|
case BleConst.CMD_WAKE_UP: |
||||
|
// 设备唤醒信号 |
||||
|
await notifyWakeupSignalReceived() |
||||
|
default: |
||||
|
os_log("未处理的通知类型: 0x%02X", log: logger, type: .debug, notifyType) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// MARK: - 应用状态处理 |
||||
|
|
||||
|
/// 处理应用进入活跃状态 |
||||
|
private func handleAppDidBecomeActive() async { |
||||
|
os_log("应用进入活跃状态", log: logger, type: .debug) |
||||
|
// 重新初始化蓝牙管理器,如果需要 |
||||
|
if centralManager == nil { |
||||
|
_ = initialize() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 处理应用进入后台 |
||||
|
private func handleAppDidEnterBackground() async { |
||||
|
os_log("应用进入后台", log: logger, type: .debug) |
||||
|
// 在iOS中,后台蓝牙操作有一定限制,这里可以做一些清理工作 |
||||
|
} |
||||
|
|
||||
|
// MARK: - 回调通知方法 |
||||
|
|
||||
|
/// 通知扫描结果 |
||||
|
/// - Parameter devices: 设备列表 |
||||
|
func notifyScanResult(devices: [[String: Any]]) async { |
||||
|
let currentDelegates = delegates |
||||
|
|
||||
|
for delegate in currentDelegates { |
||||
|
await MainActor.run { |
||||
|
delegate.onScanResult(devices: devices) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 通知连接状态变化 |
||||
|
/// - Parameter state: 连接状态 |
||||
|
func notifyConnectionStateChanged(state: Int) async { |
||||
|
let currentDelegates = delegates |
||||
|
|
||||
|
for delegate in currentDelegates { |
||||
|
await MainActor.run { |
||||
|
delegate.onConnectionStateChanged(state: state) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 通知音频数据接收 |
||||
|
/// - Parameter data: 音频数据 |
||||
|
func notifyAudioDataReceived(data: Data) async { |
||||
|
let currentDelegates = delegates |
||||
|
|
||||
|
for delegate in currentDelegates { |
||||
|
await MainActor.run { |
||||
|
delegate.onAudioDataReceived(data: data) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 通知唤醒信号接收 |
||||
|
func notifyWakeupSignalReceived() async { |
||||
|
let currentDelegates = delegates |
||||
|
|
||||
|
for delegate in currentDelegates { |
||||
|
await MainActor.run { |
||||
|
delegate.onWakeupSignalReceived() |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 通知设备信息接收 |
||||
|
/// - Parameters: |
||||
|
/// - infoType: 信息类型 |
||||
|
/// - infoData: 信息数据 |
||||
|
func notifyDeviceInfoReceived(infoType: Int, infoData: [String: Any]) async { |
||||
|
let currentDelegates = delegates |
||||
|
|
||||
|
for delegate in currentDelegates { |
||||
|
await MainActor.run { |
||||
|
delegate.onDeviceInfoReceived(infoType: infoType, infoData: infoData) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 处理扫描到的设备 |
||||
|
func handleDiscoveredPeripheral(_ peripheral: CBPeripheral, advertisementData: [String: Any], rssi: NSNumber) async { |
||||
|
// 打印外设信息 |
||||
|
os_log("扫描到设备: 名称=%{public}@, ID=%{public}@, RSSI=%d", log: logger, type: .debug, |
||||
|
peripheral.name ?? "未知", peripheral.identifier.uuidString, rssi.intValue) |
||||
|
|
||||
|
// 提取服务UUID |
||||
|
var uuids: [String] = [] |
||||
|
if let serviceUUIDs = advertisementData[CBAdvertisementDataServiceUUIDsKey] as? [CBUUID] { |
||||
|
uuids = serviceUUIDs.map { $0.uuidString } |
||||
|
} |
||||
|
|
||||
|
// 创建设备信息字典 |
||||
|
let deviceInfo: [String: Any] = [ |
||||
|
"id": peripheral.identifier.uuidString, |
||||
|
"name": peripheral.name ?? "未知", |
||||
|
"rssi": rssi.intValue, |
||||
|
"uuids": uuids |
||||
|
] |
||||
|
|
||||
|
// 更新扫描缓存 |
||||
|
scanResults[peripheral] = deviceInfo |
||||
|
|
||||
|
// 通知扫描结果 |
||||
|
await notifyScanResult(devices: [deviceInfo]) |
||||
|
} |
||||
|
|
||||
|
/// 处理连接结果 |
||||
|
func handleConnectionResult(peripheral: CBPeripheral, success: Bool, error: Error?) async { |
||||
|
if success { |
||||
|
os_log("已连接到设备: %{public}@", log: logger, type: .info, peripheral.identifier.uuidString) |
||||
|
updateConnectionState(BleConst.STATE_CONNECTED) |
||||
|
|
||||
|
// 开始服务发现 |
||||
|
peripheral.discoverServices([BleConst.PRIMARY_SERVICE_UUID, BleConst.AUDIO_SERVICE_UUID]) |
||||
|
} else { |
||||
|
os_log("连接设备失败: %{public}@", log: logger, type: .error, error?.localizedDescription ?? "未知错误") |
||||
|
updateConnectionState(BleConst.STATE_ERROR) |
||||
|
|
||||
|
// 清理资源 |
||||
|
self.peripheral = nil |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 处理断开连接 |
||||
|
func handleDisconnection(peripheral: CBPeripheral, error: Error?) async { |
||||
|
if let error = error { |
||||
|
os_log("设备连接断开(错误): %{public}@", log: logger, type: .error, error.localizedDescription) |
||||
|
updateConnectionState(BleConst.STATE_ERROR) |
||||
|
} else { |
||||
|
os_log("设备连接断开(正常)", log: logger, type: .info) |
||||
|
updateConnectionState(BleConst.STATE_DISCONNECTED) |
||||
|
} |
||||
|
|
||||
|
// 清理资源 |
||||
|
if self.peripheral?.identifier == peripheral.identifier { |
||||
|
self.peripheral = nil |
||||
|
self.writeCharacteristic = nil |
||||
|
self.notifyCharacteristic = nil |
||||
|
self.audioCharacteristic = nil |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 处理发现服务 |
||||
|
func handleDiscoveredServices(peripheral: CBPeripheral, error: Error?) async { |
||||
|
if let error = error { |
||||
|
os_log("发现服务失败: %{public}@", log: logger, type: .error, error.localizedDescription) |
||||
|
updateConnectionState(BleConst.STATE_ERROR) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
guard let services = peripheral.services else { |
||||
|
os_log("没有发现任何服务", log: logger, type: .error) |
||||
|
updateConnectionState(BleConst.STATE_ERROR) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
// 发现所有服务的特征 |
||||
|
for service in services { |
||||
|
os_log("发现服务: %{public}@", log: logger, type: .debug, service.uuid.uuidString) |
||||
|
peripheral.discoverCharacteristics(nil, for: service) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 处理发现的特征 |
||||
|
func handleDiscoveredCharacteristics(for service: CBService, peripheral: CBPeripheral, error: Error?) async { |
||||
|
if let error = error { |
||||
|
os_log("发现特征失败: %{public}@", log: logger, type: .error, error.localizedDescription) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
guard let characteristics = service.characteristics else { |
||||
|
os_log("服务没有任何特征: %{public}@", log: logger, type: .warning, service.uuid.uuidString) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
// 根据UUID识别和保存特征 |
||||
|
for characteristic in characteristics { |
||||
|
os_log("发现特征: %{public}@", log: logger, type: .debug, characteristic.uuid.uuidString) |
||||
|
|
||||
|
// 主服务特征 |
||||
|
if service.uuid == BleConst.PRIMARY_SERVICE_UUID { |
||||
|
if characteristic.uuid == BleConst.WRITE_CHAR_UUID { |
||||
|
writeCharacteristic = characteristic |
||||
|
os_log("找到写入特征", log: logger, type: .debug) |
||||
|
} else if characteristic.uuid == BleConst.NOTIFY_CHAR_UUID { |
||||
|
notifyCharacteristic = characteristic |
||||
|
os_log("找到通知特征", log: logger, type: .debug) |
||||
|
// 设置通知 |
||||
|
await setupNotification(for: characteristic, peripheral: peripheral) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 音频服务特征 |
||||
|
if service.uuid == BleConst.AUDIO_SERVICE_UUID { |
||||
|
if characteristic.uuid == BleConst.RECEIVE_AUDIO_CHAR_UUID { |
||||
|
audioCharacteristic = characteristic |
||||
|
os_log("找到音频特征", log: logger, type: .debug) |
||||
|
// 设置通知 |
||||
|
await setupNotification(for: characteristic, peripheral: peripheral) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 检查是否找到所有必要的特征 |
||||
|
if writeCharacteristic == nil || notifyCharacteristic == nil { |
||||
|
os_log("未找到所有必要的特征", log: logger, type: .error) |
||||
|
updateConnectionState(BleConst.STATE_ERROR) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 设置特征的通知 |
||||
|
/// - Parameters: |
||||
|
/// - characteristic: 特征 |
||||
|
/// - peripheral: 外设 |
||||
|
private func setupNotification(for characteristic: CBCharacteristic, peripheral: CBPeripheral) async { |
||||
|
if characteristic.properties.contains(.notify) { |
||||
|
peripheral.setNotifyValue(true, for: characteristic) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 处理特征值更新 |
||||
|
func handleValueUpdate(for characteristic: CBCharacteristic, peripheral: CBPeripheral, error: Error?) async { |
||||
|
if let error = error { |
||||
|
os_log("特征值更新失败: %{public}@", log: logger, type: .error, error.localizedDescription) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
guard let data = characteristic.value else { |
||||
|
os_log("特征没有值", log: logger, type: .warning) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
let hexString = data.map { String(format: "%02X", $0) }.joined(separator: " ") |
||||
|
os_log("收到特征数据: %{public}@ - %{public}@", log: logger, type: .debug, characteristic.uuid.uuidString, hexString) |
||||
|
|
||||
|
// 根据特征UUID处理不同类型的数据 |
||||
|
if characteristic.uuid == BleConst.RECEIVE_AUDIO_CHAR_UUID { |
||||
|
// 音频数据处理 |
||||
|
await processAudioData(data) |
||||
|
} else if characteristic.uuid == BleConst.NOTIFY_CHAR_UUID { |
||||
|
// 通知特征处理 |
||||
|
if !data.isEmpty { |
||||
|
switch data[0] { |
||||
|
case BleConst.DEVICE_RESPONSE_HEADER: |
||||
|
// 设备响应 |
||||
|
await processDeviceResponse(data) |
||||
|
case BleConst.DEVICE_NOTIFY_HEADER: |
||||
|
// 设备通知 |
||||
|
await processDeviceNotification(data) |
||||
|
default: |
||||
|
os_log("未知数据类型: %d", log: logger, type: .debug, data[0]) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// MARK: - 状态恢复处理 |
||||
|
|
||||
|
/// 处理蓝牙状态恢复 |
||||
|
func handleStateRestoration(dict: [String: Any]) async { |
||||
|
os_log("正在恢复蓝牙状态...", log: logger, type: .info) |
||||
|
|
||||
|
// 1. 恢复之前连接的外设 |
||||
|
if let peripherals = dict[CBCentralManagerRestoredStatePeripheralsKey] as? [CBPeripheral] { |
||||
|
os_log("恢复 %d 个已连接的外设", log: logger, type: .debug, peripherals.count) |
||||
|
|
||||
|
for peripheral in peripherals { |
||||
|
self.peripheral = peripheral |
||||
|
peripheral.delegate = CBPeripheralHandler.shared |
||||
|
CBPeripheralHandler.shared.bleService = self |
||||
|
|
||||
|
// 根据外设当前状态采取不同操作 |
||||
|
if peripheral.state == .connected { |
||||
|
os_log("外设 %{public}@ 已连接", log: logger, type: .debug, peripheral.identifier.uuidString) |
||||
|
updateConnectionState(BleConst.STATE_CONNECTED) |
||||
|
|
||||
|
// 重新发现服务 |
||||
|
peripheral.discoverServices([BleConst.PRIMARY_SERVICE_UUID, BleConst.AUDIO_SERVICE_UUID]) |
||||
|
} else if peripheral.state == .connecting { |
||||
|
os_log("外设 %{public}@ 正在连接中", log: logger, type: .debug, peripheral.identifier.uuidString) |
||||
|
updateConnectionState(BleConst.STATE_CONNECTING) |
||||
|
} else { |
||||
|
os_log("外设 %{public}@ 状态异常: %d", log: logger, type: .debug, peripheral.identifier.uuidString, peripheral.state.rawValue) |
||||
|
// 尝试重新连接 |
||||
|
updateConnectionState(BleConst.STATE_CONNECTING) |
||||
|
centralManager?.connect(peripheral, options: nil) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 2. 恢复扫描状态 |
||||
|
if let scanServices = dict[CBCentralManagerRestoredStateScanServicesKey] as? [CBUUID] { |
||||
|
os_log("恢复扫描任务,服务UUID数: %d", log: logger, type: .debug, scanServices.count) |
||||
|
|
||||
|
let scanOptions = dict[CBCentralManagerRestoredStateScanOptionsKey] as? [String: Any] ?? [ |
||||
|
CBCentralManagerScanOptionAllowDuplicatesKey: true |
||||
|
] |
||||
|
|
||||
|
// 重新启动扫描任务 |
||||
|
centralManager?.scanForPeripherals( |
||||
|
withServices: scanServices.isEmpty ? nil : scanServices, |
||||
|
options: scanOptions |
||||
|
) |
||||
|
|
||||
|
isScanning = true |
||||
|
|
||||
|
// 设置新的扫描超时(默认15秒) |
||||
|
scanTimeoutTask?.cancel() |
||||
|
scanTimeoutTask = Task { [weak self] in |
||||
|
guard let self = self else { return } |
||||
|
|
||||
|
do { |
||||
|
try await Task.sleep(nanoseconds: UInt64(15.0 * 1_000_000_000)) |
||||
|
if !Task.isCancelled { |
||||
|
await self.stopScan() |
||||
|
} |
||||
|
} catch { |
||||
|
// 任务被取消,忽略错误 |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 通知外部:BLE状态已恢复 |
||||
|
await notifyStateRestored() |
||||
|
} |
||||
|
|
||||
|
/// 通知状态恢复 |
||||
|
private func notifyStateRestored() async { |
||||
|
let currentDelegates = delegates |
||||
|
|
||||
|
for delegate in currentDelegates { |
||||
|
if let stateDelegate = delegate as? BleStateRestorationDelegate { |
||||
|
await MainActor.run { |
||||
|
stateDelegate.onBleStateRestored() |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// MARK: - 中央管理器委托处理器 |
||||
|
|
||||
|
class CBManagerHandler: NSObject, CBCentralManagerDelegate { |
||||
|
static let shared = CBManagerHandler() |
||||
|
weak var bleService: BleService? |
||||
|
|
||||
|
func centralManagerDidUpdateState(_ central: CBCentralManager) { |
||||
|
Task { |
||||
|
guard let service = bleService else { return } |
||||
|
|
||||
|
switch central.state { |
||||
|
case .poweredOn: |
||||
|
await MainActor.run { |
||||
|
os_log("蓝牙已开启", log: OSLog(subsystem: "com.yunqiinnovation.ble_service", category: "BleService"), type: .info) |
||||
|
} |
||||
|
case .poweredOff: |
||||
|
await MainActor.run { |
||||
|
os_log("蓝牙已关闭", log: OSLog(subsystem: "com.yunqiinnovation.ble_service", category: "BleService"), type: .warning) |
||||
|
} |
||||
|
await service.updateConnectionState(BleConst.STATE_DISCONNECTED) |
||||
|
case .unauthorized, .unsupported: |
||||
|
await MainActor.run { |
||||
|
os_log("蓝牙不可用: %d", log: OSLog(subsystem: "com.yunqiinnovation.ble_service", category: "BleService"), type: .error, central.state.rawValue) |
||||
|
} |
||||
|
await service.updateConnectionState(BleConst.STATE_ERROR) |
||||
|
default: |
||||
|
await MainActor.run { |
||||
|
os_log("蓝牙状态变化: %d", log: OSLog(subsystem: "com.yunqiinnovation.ble_service", category: "BleService"), type: .debug, central.state.rawValue) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) { |
||||
|
Task { |
||||
|
await bleService?.handleDiscoveredPeripheral(peripheral, advertisementData: advertisementData, rssi: RSSI) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) { |
||||
|
Task { |
||||
|
await bleService?.handleConnectionResult(peripheral: peripheral, success: true, error: nil) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) { |
||||
|
Task { |
||||
|
await bleService?.handleConnectionResult(peripheral: peripheral, success: false, error: error) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) { |
||||
|
Task { |
||||
|
await bleService?.handleDisconnection(peripheral: peripheral, error: error) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 状态恢复处理 |
||||
|
func centralManager(_ central: CBCentralManager, willRestoreState dict: [String : Any]) { |
||||
|
os_log("蓝牙状态恢复被触发", log: OSLog(subsystem: "com.yunqiinnovation.ble_service", category: "BleService"), type: .info) |
||||
|
|
||||
|
// 打印恢复信息 |
||||
|
if let peripherals = dict[CBCentralManagerRestoredStatePeripheralsKey] as? [CBPeripheral] { |
||||
|
os_log("恢复外设数量: %d", log: OSLog(subsystem: "com.yunqiinnovation.ble_service", category: "BleService"), type: .debug, peripherals.count) |
||||
|
for (i, peripheral) in peripherals.enumerated() { |
||||
|
os_log("恢复外设[%d]: %{public}@", log: OSLog(subsystem: "com.yunqiinnovation.ble_service", category: "BleService"), type: .debug, i, peripheral.identifier.uuidString) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if let services = dict[CBCentralManagerRestoredStateScanServicesKey] as? [CBUUID] { |
||||
|
os_log("恢复扫描服务数量: %d", log: OSLog(subsystem: "com.yunqiinnovation.ble_service", category: "BleService"), type: .debug, services.count) |
||||
|
} |
||||
|
|
||||
|
// 异步处理状态恢复 |
||||
|
Task { |
||||
|
if let service = bleService { |
||||
|
await service.handleStateRestoration(dict: dict) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// MARK: - 外设委托处理器 |
||||
|
|
||||
|
class CBPeripheralHandler: NSObject, CBPeripheralDelegate { |
||||
|
static let shared = CBPeripheralHandler() |
||||
|
weak var bleService: BleService? |
||||
|
|
||||
|
func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) { |
||||
|
Task { |
||||
|
await bleService?.handleDiscoveredServices(peripheral: peripheral, error: error) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) { |
||||
|
Task { |
||||
|
await bleService?.handleDiscoveredCharacteristics(for: service, peripheral: peripheral, error: error) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) { |
||||
|
Task { |
||||
|
await bleService?.handleValueUpdate(for: characteristic, peripheral: peripheral, error: error) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) { |
||||
|
if let error = error { |
||||
|
os_log("写入特征值失败: %{public}@", log: OSLog(subsystem: "com.yunqiinnovation.ble_service", category: "BleService"), type: .error, error.localizedDescription) |
||||
|
} else { |
||||
|
os_log("写入特征值成功", log: OSLog(subsystem: "com.yunqiinnovation.ble_service", category: "BleService"), type: .debug) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?) { |
||||
|
if let error = error { |
||||
|
os_log("设置通知状态失败: %{public}@", log: OSLog(subsystem: "com.yunqiinnovation.ble_service", category: "BleService"), type: .error, error.localizedDescription) |
||||
|
} else { |
||||
|
os_log("设置通知状态成功: %{public}@", log: OSLog(subsystem: "com.yunqiinnovation.ble_service", category: "BleService"), type: .debug, characteristic.uuid.uuidString) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// MARK: - BLE状态恢复代理 |
||||
|
/// 蓝牙状态恢复代理 |
||||
|
protocol BleStateRestorationDelegate: AnyObject { |
||||
|
/// 当蓝牙状态被系统恢复时调用 |
||||
|
func onBleStateRestored() |
||||
|
} |
||||
|
|
||||
|
// MARK: - BLE代理 |
||||
|
/// 蓝牙事件代理 |
||||
|
protocol BleDelegate: AnyObject { |
||||
|
/// 扫描结果回调 |
||||
|
func onScanResult(devices: [[String: Any]]) |
||||
|
|
||||
|
/// 连接状态变化回调 |
||||
|
func onConnectionStateChanged(state: Int) |
||||
|
|
||||
|
/// 音频数据接收回调 |
||||
|
func onAudioDataReceived(data: Data) |
||||
|
|
||||
|
/// 唤醒信号接收回调 |
||||
|
func onWakeupSignalReceived() |
||||
|
|
||||
|
/// 设备信息接收回调 |
||||
|
func onDeviceInfoReceived(infoType: Int, infoData: [String: Any]) |
||||
|
} |
||||
@ -0,0 +1,4 @@ |
|||||
|
#import <Flutter/Flutter.h> |
||||
|
|
||||
|
@interface BleServicePlugin : NSObject<FlutterPlugin> |
||||
|
@end |
||||
@ -0,0 +1,12 @@ |
|||||
|
#import "BleServicePlugin.h" |
||||
|
#if __has_include(<ble_service/ble_service-Swift.h>) |
||||
|
#import <ble_service/ble_service-Swift.h> |
||||
|
#else |
||||
|
#import "ble_service-Swift.h" |
||||
|
#endif |
||||
|
|
||||
|
@implementation BleServicePlugin |
||||
|
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar { |
||||
|
[SwiftBleServicePlugin registerWithRegistrar:registrar]; |
||||
|
} |
||||
|
@end |
||||
@ -0,0 +1,203 @@ |
|||||
|
import Flutter |
||||
|
import UIKit |
||||
|
|
||||
|
public class SwiftBleServicePlugin: NSObject, FlutterPlugin, BleDelegate { |
||||
|
private let TAG = "BleServicePlugin" |
||||
|
|
||||
|
// 方法通道 |
||||
|
private var methodChannel: FlutterMethodChannel? |
||||
|
|
||||
|
// 事件通道 |
||||
|
private var dataEventSink: FlutterEventSink? |
||||
|
private var statusEventSink: FlutterEventSink? |
||||
|
|
||||
|
// BLE服务实例 |
||||
|
private let bleService = BleService.shared |
||||
|
|
||||
|
public static func register(with registrar: FlutterPluginRegistrar) { |
||||
|
let methodChannel = FlutterMethodChannel(name: "com.yunqiinnovation.ble_service", binaryMessenger: registrar.messenger()) |
||||
|
|
||||
|
// 创建插件实例 |
||||
|
let instance = SwiftBleServicePlugin() |
||||
|
instance.methodChannel = methodChannel |
||||
|
|
||||
|
// 设置方法通道处理器 |
||||
|
registrar.addMethodCallDelegate(instance, channel: methodChannel) |
||||
|
|
||||
|
// 设置事件通道 |
||||
|
instance.setupEventChannels(with: registrar) |
||||
|
|
||||
|
// 初始化BLE服务 |
||||
|
_ = instance.bleService.initialize() |
||||
|
|
||||
|
// 添加自身作为BLE回调 |
||||
|
_ = instance.bleService.addDelegate(instance) |
||||
|
} |
||||
|
|
||||
|
/// 设置事件通道 |
||||
|
private func setupEventChannels(with registrar: FlutterPluginRegistrar) { |
||||
|
// 数据接收事件通道 |
||||
|
let dataEventChannel = FlutterEventChannel(name: "com.yunqiinnovation.ble_service/data_events", binaryMessenger: registrar.messenger()) |
||||
|
dataEventChannel.setStreamHandler(StreamHandler { [weak self] sink in |
||||
|
self?.dataEventSink = sink |
||||
|
} onCancel: { [weak self] in |
||||
|
self?.dataEventSink = nil |
||||
|
}) |
||||
|
|
||||
|
// 状态更新事件通道 |
||||
|
let statusEventChannel = FlutterEventChannel(name: "com.yunqiinnovation.ble_service/status_events", binaryMessenger: registrar.messenger()) |
||||
|
statusEventChannel.setStreamHandler(StreamHandler { [weak self] sink in |
||||
|
self?.statusEventSink = sink |
||||
|
} onCancel: { [weak self] in |
||||
|
self?.statusEventSink = nil |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
/// 发送事件到Flutter |
||||
|
private func sendEvent(_ eventSink: FlutterEventSink?, _ data: Any) { |
||||
|
DispatchQueue.main.async { |
||||
|
eventSink?(data) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { |
||||
|
NSLog("Method call: \(call.method)") |
||||
|
|
||||
|
switch call.method { |
||||
|
case "isConnected": |
||||
|
result(bleService.isConnected()) |
||||
|
|
||||
|
case "getBatteryInfo": |
||||
|
result(bleService.getBatteryInfo()) |
||||
|
|
||||
|
case "getDeviceVersion": |
||||
|
result(bleService.getDeviceVersion()) |
||||
|
|
||||
|
case "openEncoder": |
||||
|
result(bleService.openEncoder()) |
||||
|
|
||||
|
case "openDecoder": |
||||
|
result(bleService.openDecoder()) |
||||
|
|
||||
|
case "closeCodec": |
||||
|
result(bleService.closeCodec()) |
||||
|
|
||||
|
case "playA2DP": |
||||
|
result(bleService.playA2DP()) |
||||
|
|
||||
|
case "registerBackgroundScan": |
||||
|
result(bleService.registerBackgroundScan()) |
||||
|
|
||||
|
case "unregisterBackgroundScan": |
||||
|
result(bleService.unregisterBackgroundScan()) |
||||
|
|
||||
|
case "startBleService": |
||||
|
guard let config = call.arguments as? [String: Any] else { |
||||
|
result(FlutterError(code: "INVALID_ARGUMENTS", message: "配置参数无效", details: nil)) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
// 处理配置参数 |
||||
|
if let address = config["mac"] as? String { |
||||
|
// 如果提供了设备地址,尝试连接 |
||||
|
bleService.connect(address: address) |
||||
|
} |
||||
|
|
||||
|
// 开始扫描,寻找设备 |
||||
|
let scanSuccess = bleService.startScan() |
||||
|
result(scanSuccess) |
||||
|
|
||||
|
case "stopBleService": |
||||
|
// 停止扫描和断开连接 |
||||
|
_ = bleService.stopScan() |
||||
|
if bleService.isConnected() { |
||||
|
bleService.disconnect() |
||||
|
} |
||||
|
result(true) |
||||
|
|
||||
|
default: |
||||
|
result(FlutterMethodNotImplemented) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// MARK: - BleDelegate Methods |
||||
|
|
||||
|
func onScanResult(devices: [[String: Any]]) { |
||||
|
// 目前Flutter层没有直接使用此事件,可以选择不转发 |
||||
|
} |
||||
|
|
||||
|
func onConnectionStateChanged(state: Int) { |
||||
|
// 将连接状态变化通过状态通道发送 |
||||
|
let stateStr: String |
||||
|
switch state { |
||||
|
case BleConst.STATE_DISCONNECTED: |
||||
|
stateStr = "Disconnected" |
||||
|
case BleConst.STATE_CONNECTING: |
||||
|
stateStr = "Connecting" |
||||
|
case BleConst.STATE_CONNECTED: |
||||
|
stateStr = "Connected" |
||||
|
case BleConst.STATE_DISCONNECTING: |
||||
|
stateStr = "Disconnecting" |
||||
|
case BleConst.STATE_ERROR: |
||||
|
stateStr = "Connection Error" |
||||
|
default: |
||||
|
stateStr = "Unknown State(\(state))" |
||||
|
} |
||||
|
|
||||
|
let stateMap: [String: Any] = [ |
||||
|
"type": "connectionStateChanged", |
||||
|
"state": state, |
||||
|
"stateStr": stateStr |
||||
|
] |
||||
|
|
||||
|
sendEvent(statusEventSink, stateMap) |
||||
|
} |
||||
|
|
||||
|
func onAudioDataReceived(data: Data) { |
||||
|
// 将音频数据通过数据通道发送 |
||||
|
let dataMap: [String: Any] = [ |
||||
|
"type": "audioData", |
||||
|
"data": FlutterStandardTypedData(bytes: data) |
||||
|
] |
||||
|
|
||||
|
sendEvent(dataEventSink, dataMap) |
||||
|
} |
||||
|
|
||||
|
func onWakeupSignalReceived() { |
||||
|
// 将唤醒事件通过状态通道发送 |
||||
|
sendEvent(statusEventSink, ["type": "wakeup"]) |
||||
|
} |
||||
|
|
||||
|
func onDeviceInfoReceived(infoType: Int, infoData: [String: Any]) { |
||||
|
// 将设备信息通过状态通道发送 |
||||
|
var resultMap = infoData |
||||
|
resultMap["type"] = "deviceInfo" |
||||
|
resultMap["result"] = infoType |
||||
|
|
||||
|
sendEvent(statusEventSink, resultMap) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// MARK: - 事件处理器类 |
||||
|
|
||||
|
/// 简化的事件处理器类 |
||||
|
class StreamHandler: NSObject, FlutterStreamHandler { |
||||
|
private let onListen: (FlutterEventSink) -> Void |
||||
|
private let onCancel: () -> Void |
||||
|
|
||||
|
init(onListen: @escaping (FlutterEventSink) -> Void, onCancel: @escaping () -> Void) { |
||||
|
self.onListen = onListen |
||||
|
self.onCancel = onCancel |
||||
|
super.init() |
||||
|
} |
||||
|
|
||||
|
func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { |
||||
|
onListen(events) |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
func onCancel(withArguments arguments: Any?) -> FlutterError? { |
||||
|
onCancel() |
||||
|
return nil |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,23 @@ |
|||||
|
# |
||||
|
# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. |
||||
|
# Run `pod lib lint ble_service.podspec` to validate before publishing. |
||||
|
# |
||||
|
Pod::Spec.new do |s| |
||||
|
s.name = 'ble_service' |
||||
|
s.version = '0.0.1' |
||||
|
s.summary = 'BLE服务Flutter插件' |
||||
|
s.description = <<-DESC |
||||
|
BLE服务Flutter插件,提供蓝牙设备扫描、连接和通信功能。 |
||||
|
DESC |
||||
|
s.homepage = 'http://example.com' |
||||
|
s.license = { :file => '../LICENSE' } |
||||
|
s.author = { 'Your Company' => 'email@example.com' } |
||||
|
s.source = { :path => '.' } |
||||
|
s.source_files = 'Classes/**/*' |
||||
|
s.dependency 'Flutter' |
||||
|
s.platform = :ios, '11.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' |
||||
|
end |
||||
@ -0,0 +1,410 @@ |
|||||
|
import Foundation |
||||
|
import SQLite3 |
||||
|
|
||||
|
/// ChatStorageHelper - iOS版本的聊天存储助手 |
||||
|
/// 负责SQLite数据库操作和业务逻辑 |
||||
|
class ChatStorageHelper { |
||||
|
// MARK: - Constants |
||||
|
private let TAG = "ChatStorageHelper" |
||||
|
|
||||
|
// 数据库常量 |
||||
|
private let DATABASE_NAME = "chat_messages.db" |
||||
|
private let DATABASE_VERSION = 1 |
||||
|
|
||||
|
// 表和列名 |
||||
|
private let TABLE_MESSAGES = "messages" |
||||
|
private let COLUMN_ID = "id" |
||||
|
private let COLUMN_SESSION_ID = "session_id" |
||||
|
private let COLUMN_MESSAGE = "message" |
||||
|
private let COLUMN_SENDER = "sender" |
||||
|
private let COLUMN_TIMESTAMP = "timestamp" |
||||
|
private let COLUMN_METADATA = "metadata" |
||||
|
|
||||
|
// MARK: - Properties |
||||
|
private var db: OpaquePointer? |
||||
|
private let dbPath: String |
||||
|
|
||||
|
// MARK: - Initialization |
||||
|
init(_ context: Any) { |
||||
|
// 获取数据库文件路径 |
||||
|
let fileManager = FileManager.default |
||||
|
if let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first { |
||||
|
let databaseDirectory = documentsDirectory.appendingPathComponent("Databases") |
||||
|
|
||||
|
// 确保目录存在 |
||||
|
try? fileManager.createDirectory(at: databaseDirectory, withIntermediateDirectories: true) |
||||
|
|
||||
|
dbPath = databaseDirectory.appendingPathComponent(DATABASE_NAME).path |
||||
|
|
||||
|
// 打开数据库连接 |
||||
|
openDatabase() |
||||
|
} else { |
||||
|
dbPath = "" |
||||
|
print("\(TAG) 无法获取文档目录路径") |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
deinit { |
||||
|
closeDatabase() |
||||
|
} |
||||
|
|
||||
|
// MARK: - Database Operations |
||||
|
|
||||
|
/// 打开数据库连接 |
||||
|
private func openDatabase() { |
||||
|
if sqlite3_open(dbPath, &db) == SQLITE_OK { |
||||
|
createTables() |
||||
|
print("\(TAG) 数据库已成功打开") |
||||
|
} else { |
||||
|
print("\(TAG) 无法打开数据库: \(String(describing: errorMessage))") |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 关闭数据库连接 |
||||
|
private func closeDatabase() { |
||||
|
if db != nil { |
||||
|
sqlite3_close(db) |
||||
|
db = nil |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 创建必要的数据库表 |
||||
|
private func createTables() { |
||||
|
// 创建消息表 |
||||
|
let createTableSQL = """ |
||||
|
CREATE TABLE IF NOT EXISTS \(TABLE_MESSAGES) ( |
||||
|
\(COLUMN_ID) INTEGER PRIMARY KEY AUTOINCREMENT, |
||||
|
\(COLUMN_SESSION_ID) TEXT NOT NULL, |
||||
|
\(COLUMN_MESSAGE) TEXT NOT NULL, |
||||
|
\(COLUMN_SENDER) TEXT NOT NULL, |
||||
|
\(COLUMN_TIMESTAMP) TEXT NOT NULL, |
||||
|
\(COLUMN_METADATA) TEXT |
||||
|
); |
||||
|
""" |
||||
|
|
||||
|
if executeUpdate(sql: createTableSQL) { |
||||
|
// 创建索引以加快查询速度 |
||||
|
executeUpdate(sql: "CREATE INDEX IF NOT EXISTS idx_session_id ON \(TABLE_MESSAGES) (\(COLUMN_SESSION_ID))") |
||||
|
executeUpdate(sql: "CREATE INDEX IF NOT EXISTS idx_timestamp ON \(TABLE_MESSAGES) (\(COLUMN_TIMESTAMP))") |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 获取SQLite错误消息 |
||||
|
private var errorMessage: String { |
||||
|
if let errorPointer = sqlite3_errmsg(db) { |
||||
|
let errorMessage = String(cString: errorPointer) |
||||
|
return errorMessage |
||||
|
} else { |
||||
|
return "未知错误" |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 执行SQL更新操作(INSERT, UPDATE, DELETE, CREATE等) |
||||
|
private func executeUpdate(sql: String, parameters: [Any]? = nil) -> Bool { |
||||
|
var statement: OpaquePointer? |
||||
|
|
||||
|
if sqlite3_prepare_v2(db, sql, -1, &statement, nil) == SQLITE_OK { |
||||
|
// 绑定参数 |
||||
|
if let params = parameters { |
||||
|
for (index, param) in params.enumerated() { |
||||
|
bindParameter(statement: statement, index: index + 1, value: param) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 执行语句 |
||||
|
let result = sqlite3_step(statement) |
||||
|
sqlite3_finalize(statement) |
||||
|
|
||||
|
return result == SQLITE_DONE |
||||
|
} else { |
||||
|
print("\(TAG) 准备SQL语句失败: \(errorMessage)") |
||||
|
return false |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 绑定SQL参数 |
||||
|
private func bindParameter(statement: OpaquePointer?, index: Int, value: Any) { |
||||
|
if let value = value as? Int { |
||||
|
sqlite3_bind_int(statement, Int32(index), Int32(value)) |
||||
|
} else if let value = value as? Double { |
||||
|
sqlite3_bind_double(statement, Int32(index), value) |
||||
|
} else if let value = value as? String { |
||||
|
sqlite3_bind_text(statement, Int32(index), (value as NSString).utf8String, -1, nil) |
||||
|
} else if value is NSNull { |
||||
|
sqlite3_bind_null(statement, Int32(index)) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 查询数据并处理结果 |
||||
|
private func executeQuery(sql: String, parameters: [Any]? = nil, handler: (OpaquePointer) -> Void) { |
||||
|
var statement: OpaquePointer? |
||||
|
|
||||
|
if sqlite3_prepare_v2(db, sql, -1, &statement, nil) == SQLITE_OK { |
||||
|
// 绑定参数 |
||||
|
if let params = parameters { |
||||
|
for (index, param) in params.enumerated() { |
||||
|
bindParameter(statement: statement, index: index + 1, value: param) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 处理结果 |
||||
|
while sqlite3_step(statement) == SQLITE_ROW { |
||||
|
handler(statement!) |
||||
|
} |
||||
|
} else { |
||||
|
print("\(TAG) 执行查询失败: \(errorMessage)") |
||||
|
} |
||||
|
|
||||
|
sqlite3_finalize(statement) |
||||
|
} |
||||
|
|
||||
|
// MARK: - Public Methods |
||||
|
|
||||
|
/// 保存消息 |
||||
|
/// - Parameters: |
||||
|
/// - sessionId: 会话ID |
||||
|
/// - message: 消息内容 |
||||
|
/// - sender: 发送者 |
||||
|
/// - metadata: 元数据 |
||||
|
/// - Returns: 成功返回消息ID,失败返回-1 |
||||
|
func saveMessage(sessionId: String, message: String, sender: String, metadata: String? = nil) -> Int64 { |
||||
|
let dateFormatter = DateFormatter() |
||||
|
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss" |
||||
|
let timestamp = dateFormatter.string(from: Date()) |
||||
|
|
||||
|
let sql = """ |
||||
|
INSERT INTO \(TABLE_MESSAGES) (\(COLUMN_SESSION_ID), \(COLUMN_MESSAGE), \(COLUMN_SENDER), \(COLUMN_TIMESTAMP), \(COLUMN_METADATA)) |
||||
|
VALUES (?, ?, ?, ?, ?) |
||||
|
""" |
||||
|
|
||||
|
var parameters: [Any] = [sessionId, message, sender, timestamp] |
||||
|
if let metadata = metadata { |
||||
|
parameters.append(metadata) |
||||
|
} else { |
||||
|
parameters.append(NSNull()) |
||||
|
} |
||||
|
|
||||
|
if executeUpdate(sql: sql, parameters: parameters) { |
||||
|
return sqlite3_last_insert_rowid(db) |
||||
|
} else { |
||||
|
print("\(TAG) 保存消息失败") |
||||
|
return -1 |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 获取指定会话的消息 |
||||
|
/// - Parameters: |
||||
|
/// - sessionId: 会话ID |
||||
|
/// - page: 页码 |
||||
|
/// - pageSize: 每页大小 |
||||
|
/// - Returns: JSON字符串,包含消息列表和分页信息 |
||||
|
func getMessages(sessionId: String, page: Int = 1, pageSize: Int = 20) -> [String: Any] { |
||||
|
var messages: [[String: Any]] = [] |
||||
|
let offset = (page - 1) * pageSize |
||||
|
|
||||
|
// 查询消息 |
||||
|
let sql = """ |
||||
|
SELECT * FROM \(TABLE_MESSAGES) |
||||
|
WHERE \(COLUMN_SESSION_ID) = ? |
||||
|
ORDER BY \(COLUMN_TIMESTAMP) DESC |
||||
|
LIMIT ? OFFSET ? |
||||
|
""" |
||||
|
|
||||
|
executeQuery(sql: sql, parameters: [sessionId, pageSize, offset]) { statement in |
||||
|
var message: [String: Any] = [:] |
||||
|
|
||||
|
// 获取ID |
||||
|
message["id"] = Int(sqlite3_column_int(statement, 0)) |
||||
|
|
||||
|
// 获取会话ID |
||||
|
if let sessionId = sqlite3_column_text(statement, 1) { |
||||
|
message["sessionId"] = String(cString: sessionId) |
||||
|
} |
||||
|
|
||||
|
// 获取消息内容 |
||||
|
if let messageText = sqlite3_column_text(statement, 2) { |
||||
|
message["message"] = String(cString: messageText) |
||||
|
} |
||||
|
|
||||
|
// 获取发送者 |
||||
|
if let sender = sqlite3_column_text(statement, 3) { |
||||
|
message["sender"] = String(cString: sender) |
||||
|
} |
||||
|
|
||||
|
// 获取时间戳 |
||||
|
if let timestamp = sqlite3_column_text(statement, 4) { |
||||
|
message["timestamp"] = String(cString: timestamp) |
||||
|
} |
||||
|
|
||||
|
// 获取元数据(如果有) |
||||
|
if sqlite3_column_type(statement, 5) != SQLITE_NULL { |
||||
|
if let metadata = sqlite3_column_text(statement, 5) { |
||||
|
message["metadata"] = String(cString: metadata) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
messages.append(message) |
||||
|
} |
||||
|
|
||||
|
// 获取总记录数 |
||||
|
var totalCount = 0 |
||||
|
let countSql = "SELECT COUNT(*) FROM \(TABLE_MESSAGES) WHERE \(COLUMN_SESSION_ID) = ?" |
||||
|
|
||||
|
executeQuery(sql: countSql, parameters: [sessionId]) { statement in |
||||
|
totalCount = Int(sqlite3_column_int(statement, 0)) |
||||
|
} |
||||
|
|
||||
|
// 计算总页数 |
||||
|
let totalPages = (totalCount + pageSize - 1) / pageSize |
||||
|
|
||||
|
return [ |
||||
|
"messages": messages, |
||||
|
"page": page, |
||||
|
"pageSize": pageSize, |
||||
|
"totalCount": totalCount, |
||||
|
"totalPages": totalPages |
||||
|
] |
||||
|
} |
||||
|
|
||||
|
/// 获取会话列表 |
||||
|
/// - Returns: 会话列表数据 |
||||
|
func getSessions() -> [[String: Any]] { |
||||
|
var sessions: [[String: Any]] = [] |
||||
|
|
||||
|
// 查询不同的会话ID及其最新消息 |
||||
|
let sql = """ |
||||
|
SELECT s1.\(COLUMN_SESSION_ID), s1.\(COLUMN_MESSAGE), s1.\(COLUMN_SENDER), s1.\(COLUMN_TIMESTAMP) |
||||
|
FROM \(TABLE_MESSAGES) s1 |
||||
|
INNER JOIN ( |
||||
|
SELECT \(COLUMN_SESSION_ID), MAX(\(COLUMN_TIMESTAMP)) as max_timestamp |
||||
|
FROM \(TABLE_MESSAGES) |
||||
|
GROUP BY \(COLUMN_SESSION_ID) |
||||
|
) s2 ON s1.\(COLUMN_SESSION_ID) = s2.\(COLUMN_SESSION_ID) AND s1.\(COLUMN_TIMESTAMP) = s2.max_timestamp |
||||
|
ORDER BY s1.\(COLUMN_TIMESTAMP) DESC |
||||
|
""" |
||||
|
|
||||
|
executeQuery(sql: sql) { statement in |
||||
|
var session: [String: Any] = [:] |
||||
|
|
||||
|
// 获取会话ID |
||||
|
if let sessionId = sqlite3_column_text(statement, 0) { |
||||
|
let sessionIdStr = String(cString: sessionId) |
||||
|
session["sessionId"] = sessionIdStr |
||||
|
|
||||
|
// 获取该会话的消息数量 |
||||
|
var messageCount = 0 |
||||
|
let countSql = "SELECT COUNT(*) FROM \(TABLE_MESSAGES) WHERE \(COLUMN_SESSION_ID) = ?" |
||||
|
|
||||
|
executeQuery(sql: countSql, parameters: [sessionIdStr]) { countStmt in |
||||
|
messageCount = Int(sqlite3_column_int(countStmt, 0)) |
||||
|
} |
||||
|
|
||||
|
session["messageCount"] = messageCount |
||||
|
} |
||||
|
|
||||
|
// 获取最后一条消息 |
||||
|
if let message = sqlite3_column_text(statement, 1) { |
||||
|
session["lastMessage"] = String(cString: message) |
||||
|
} |
||||
|
|
||||
|
// 获取最后发送者 |
||||
|
if let sender = sqlite3_column_text(statement, 2) { |
||||
|
session["lastSender"] = String(cString: sender) |
||||
|
} |
||||
|
|
||||
|
// 获取时间戳 |
||||
|
if let timestamp = sqlite3_column_text(statement, 3) { |
||||
|
session["timestamp"] = String(cString: timestamp) |
||||
|
} |
||||
|
|
||||
|
sessions.append(session) |
||||
|
} |
||||
|
|
||||
|
return sessions |
||||
|
} |
||||
|
|
||||
|
/// 删除消息 |
||||
|
/// - Parameters: |
||||
|
/// - sessionId: 会话ID,如果提供则删除整个会话 |
||||
|
/// - messageIds: 消息ID列表,如果提供则删除指定消息 |
||||
|
/// - Returns: 成功返回true,失败返回false |
||||
|
func deleteMessages(sessionId: String? = nil, messageIds: [Int]? = nil) -> Bool { |
||||
|
if let messageIds = messageIds, !messageIds.isEmpty { |
||||
|
// 删除指定ID的消息 |
||||
|
let placeholders = Array(repeating: "?", count: messageIds.count).joined(separator: ",") |
||||
|
let sql = "DELETE FROM \(TABLE_MESSAGES) WHERE \(COLUMN_ID) IN (\(placeholders))" |
||||
|
|
||||
|
return executeUpdate(sql: sql, parameters: messageIds.map { $0 }) |
||||
|
} else if let sessionId = sessionId, !sessionId.isEmpty { |
||||
|
// 删除会话中的所有消息 |
||||
|
let sql = "DELETE FROM \(TABLE_MESSAGES) WHERE \(COLUMN_SESSION_ID) = ?" |
||||
|
return executeUpdate(sql: sql, parameters: [sessionId]) |
||||
|
} |
||||
|
|
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
/// 清空数据库 |
||||
|
/// - Returns: 成功返回true,失败返回false |
||||
|
func clearDatabase() -> Bool { |
||||
|
let sql = "DELETE FROM \(TABLE_MESSAGES)" |
||||
|
return executeUpdate(sql: sql) |
||||
|
} |
||||
|
|
||||
|
/// 获取指定会话的最近消息 |
||||
|
/// - Parameters: |
||||
|
/// - sessionId: 会话ID |
||||
|
/// - limit: 限制数量 |
||||
|
/// - Returns: 消息列表,按时间正序排列 |
||||
|
func getRecentMessages(sessionId: String, limit: Int) -> [[String: Any]] { |
||||
|
var messages: [[String: Any]] = [] |
||||
|
|
||||
|
// 查询语句 - 按时间倒序获取最近的N条,然后在结果中再按时间正序 |
||||
|
let sql = """ |
||||
|
SELECT * FROM ( |
||||
|
SELECT * FROM \(TABLE_MESSAGES) |
||||
|
WHERE \(COLUMN_SESSION_ID) = ? |
||||
|
ORDER BY \(COLUMN_TIMESTAMP) DESC |
||||
|
LIMIT ? |
||||
|
) tmp ORDER BY \(COLUMN_TIMESTAMP) ASC |
||||
|
""" |
||||
|
|
||||
|
executeQuery(sql: sql, parameters: [sessionId, limit]) { statement in |
||||
|
var message: [String: Any] = [:] |
||||
|
|
||||
|
// 获取ID |
||||
|
message["id"] = Int(sqlite3_column_int(statement, 0)) |
||||
|
|
||||
|
// 获取会话ID |
||||
|
if let sessionId = sqlite3_column_text(statement, 1) { |
||||
|
message["sessionId"] = String(cString: sessionId) |
||||
|
} |
||||
|
|
||||
|
// 获取消息内容 |
||||
|
if let messageText = sqlite3_column_text(statement, 2) { |
||||
|
message["message"] = String(cString: messageText) |
||||
|
} |
||||
|
|
||||
|
// 获取发送者 |
||||
|
if let sender = sqlite3_column_text(statement, 3) { |
||||
|
message["sender"] = String(cString: sender) |
||||
|
} |
||||
|
|
||||
|
// 获取时间戳 |
||||
|
if let timestamp = sqlite3_column_text(statement, 4) { |
||||
|
message["timestamp"] = String(cString: timestamp) |
||||
|
} |
||||
|
|
||||
|
// 获取元数据(如果有) |
||||
|
if sqlite3_column_type(statement, 5) != SQLITE_NULL { |
||||
|
if let metadata = sqlite3_column_text(statement, 5) { |
||||
|
message["metadata"] = String(cString: metadata) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
messages.append(message) |
||||
|
} |
||||
|
|
||||
|
return messages |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,4 @@ |
|||||
|
#import <Flutter/Flutter.h> |
||||
|
|
||||
|
@interface ChatStoragePlugin : NSObject<FlutterPlugin> |
||||
|
@end |
||||
@ -0,0 +1,12 @@ |
|||||
|
#import "ChatStoragePlugin.h" |
||||
|
#if __has_include(<chat_storage/chat_storage-Swift.h>) |
||||
|
#import <chat_storage/chat_storage-Swift.h> |
||||
|
#else |
||||
|
#import "chat_storage-Swift.h" |
||||
|
#endif |
||||
|
|
||||
|
@implementation ChatStoragePlugin |
||||
|
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar { |
||||
|
[SwiftChatStoragePlugin registerWithRegistrar:registrar]; |
||||
|
} |
||||
|
@end |
||||
@ -0,0 +1,182 @@ |
|||||
|
import Flutter |
||||
|
import UIKit |
||||
|
|
||||
|
public class ChatStoragePlugin: NSObject, FlutterPlugin { |
||||
|
private let TAG = "ChatStoragePlugin" |
||||
|
private var chatStorageHelper: ChatStorageHelper? |
||||
|
|
||||
|
public static func register(with registrar: FlutterPluginRegistrar) { |
||||
|
let channel = FlutterMethodChannel(name: "chat_storage", binaryMessenger: registrar.messenger()) |
||||
|
let instance = ChatStoragePlugin() |
||||
|
registrar.addMethodCallDelegate(instance, channel: channel) |
||||
|
} |
||||
|
|
||||
|
override init() { |
||||
|
super.init() |
||||
|
chatStorageHelper = ChatStorageHelper(()) |
||||
|
print("\(TAG) 聊天存储插件已加载") |
||||
|
} |
||||
|
|
||||
|
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { |
||||
|
guard let helper = chatStorageHelper else { |
||||
|
result(FlutterError(code: "NOT_INITIALIZED", message: "存储助手未初始化", details: nil)) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
switch call.method { |
||||
|
case "saveMessage": |
||||
|
handleSaveMessage(call, result, helper) |
||||
|
|
||||
|
case "getMessages": |
||||
|
handleGetMessages(call, result, helper) |
||||
|
|
||||
|
case "getSessions": |
||||
|
handleGetSessions(call, result, helper) |
||||
|
|
||||
|
case "deleteMessages": |
||||
|
handleDeleteMessages(call, result, helper) |
||||
|
|
||||
|
case "clearDatabase": |
||||
|
handleClearDatabase(call, result, helper) |
||||
|
|
||||
|
case "getRecentMessages": |
||||
|
handleGetRecentMessages(call, result, helper) |
||||
|
|
||||
|
default: |
||||
|
result(FlutterMethodNotImplemented) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// MARK: - Method handlers |
||||
|
|
||||
|
private func handleSaveMessage(_ call: FlutterMethodCall, _ result: @escaping FlutterResult, _ helper: ChatStorageHelper) { |
||||
|
guard let arguments = call.arguments as? [String: Any] else { |
||||
|
result(FlutterError(code: "INVALID_ARGS", message: "参数无效", details: nil)) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
guard let sessionId = arguments["sessionId"] as? String, |
||||
|
let message = arguments["message"] as? String, |
||||
|
let sender = arguments["sender"] as? String else { |
||||
|
result(FlutterError(code: "MISSING_ARGS", message: "缺少必要参数", details: nil)) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
let metadata = arguments["metadata"] as? String |
||||
|
|
||||
|
do { |
||||
|
let id = helper.saveMessage(sessionId: sessionId, message: message, sender: sender, metadata: metadata) |
||||
|
|
||||
|
if id != -1 { |
||||
|
result(["success": true, "id": id]) |
||||
|
} else { |
||||
|
result(FlutterError(code: "DB_ERROR", message: "插入消息失败", details: nil)) |
||||
|
} |
||||
|
} catch { |
||||
|
print("\(TAG) 保存消息时发生错误: \(error.localizedDescription)") |
||||
|
result(FlutterError(code: "EXCEPTION", message: "保存消息时发生错误: \(error.localizedDescription)", details: nil)) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private func handleGetMessages(_ call: FlutterMethodCall, _ result: @escaping FlutterResult, _ helper: ChatStorageHelper) { |
||||
|
guard let arguments = call.arguments as? [String: Any] else { |
||||
|
result(FlutterError(code: "INVALID_ARGS", message: "参数无效", details: nil)) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
guard let sessionId = arguments["sessionId"] as? String else { |
||||
|
result(FlutterError(code: "MISSING_ARGS", message: "缺少会话ID", details: nil)) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
let page = arguments["page"] as? Int ?? 1 |
||||
|
let pageSize = arguments["pageSize"] as? Int ?? 20 |
||||
|
|
||||
|
do { |
||||
|
let messagesData = helper.getMessages(sessionId: sessionId, page: page, pageSize: pageSize) |
||||
|
|
||||
|
// 将结果转换为JSON字符串 |
||||
|
if let jsonData = try? JSONSerialization.data(withJSONObject: messagesData), |
||||
|
let jsonString = String(data: jsonData, encoding: .utf8) { |
||||
|
result(jsonString) |
||||
|
} else { |
||||
|
result(FlutterError(code: "JSON_ERROR", message: "无法序列化结果", details: nil)) |
||||
|
} |
||||
|
} catch { |
||||
|
print("\(TAG) 获取消息时发生错误: \(error.localizedDescription)") |
||||
|
result(FlutterError(code: "EXCEPTION", message: "获取消息时发生错误: \(error.localizedDescription)", details: nil)) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private func handleGetSessions(_ call: FlutterMethodCall, _ result: @escaping FlutterResult, _ helper: ChatStorageHelper) { |
||||
|
do { |
||||
|
let sessions = helper.getSessions() |
||||
|
|
||||
|
// 将结果转换为JSON字符串 |
||||
|
if let jsonData = try? JSONSerialization.data(withJSONObject: sessions), |
||||
|
let jsonString = String(data: jsonData, encoding: .utf8) { |
||||
|
result(jsonString) |
||||
|
} else { |
||||
|
result(FlutterError(code: "JSON_ERROR", message: "无法序列化结果", details: nil)) |
||||
|
} |
||||
|
} catch { |
||||
|
print("\(TAG) 获取会话列表时发生错误: \(error.localizedDescription)") |
||||
|
result(FlutterError(code: "EXCEPTION", message: "获取会话列表时发生错误: \(error.localizedDescription)", details: nil)) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private func handleDeleteMessages(_ call: FlutterMethodCall, _ result: @escaping FlutterResult, _ helper: ChatStorageHelper) { |
||||
|
guard let arguments = call.arguments as? [String: Any] else { |
||||
|
result(FlutterError(code: "INVALID_ARGS", message: "参数无效", details: nil)) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
let sessionId = arguments["sessionId"] as? String |
||||
|
let messageIds = arguments["messageIds"] as? [Int] |
||||
|
|
||||
|
do { |
||||
|
let success = helper.deleteMessages(sessionId: sessionId, messageIds: messageIds) |
||||
|
|
||||
|
if success { |
||||
|
result(["success": true]) |
||||
|
} else { |
||||
|
result(FlutterError(code: "INVALID_ARGS", message: "必须提供sessionId或messageIds", details: nil)) |
||||
|
} |
||||
|
} catch { |
||||
|
print("\(TAG) 删除消息时发生错误: \(error.localizedDescription)") |
||||
|
result(FlutterError(code: "EXCEPTION", message: "删除消息时发生错误: \(error.localizedDescription)", details: nil)) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private func handleClearDatabase(_ call: FlutterMethodCall, _ result: @escaping FlutterResult, _ helper: ChatStorageHelper) { |
||||
|
do { |
||||
|
let success = helper.clearDatabase() |
||||
|
result(["success": success]) |
||||
|
} catch { |
||||
|
print("\(TAG) 清空数据库时发生错误: \(error.localizedDescription)") |
||||
|
result(FlutterError(code: "EXCEPTION", message: "清空数据库时发生错误: \(error.localizedDescription)", details: nil)) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private func handleGetRecentMessages(_ call: FlutterMethodCall, _ result: @escaping FlutterResult, _ helper: ChatStorageHelper) { |
||||
|
guard let arguments = call.arguments as? [String: Any] else { |
||||
|
result(FlutterError(code: "INVALID_ARGS", message: "参数无效", details: nil)) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
guard let sessionId = arguments["sessionId"] as? String else { |
||||
|
result(FlutterError(code: "MISSING_ARGS", message: "缺少会话ID", details: nil)) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
let limit = arguments["limit"] as? Int ?? 10 |
||||
|
|
||||
|
do { |
||||
|
let messages = helper.getRecentMessages(sessionId: sessionId, limit: limit) |
||||
|
result(messages) |
||||
|
} catch { |
||||
|
print("\(TAG) 获取最近消息时发生错误: \(error.localizedDescription)") |
||||
|
result(FlutterError(code: "EXCEPTION", message: "获取最近消息时发生错误: \(error.localizedDescription)", details: nil)) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,11 @@ |
|||||
|
import Flutter |
||||
|
import UIKit |
||||
|
|
||||
|
// 这个类是为了满足Flutter插件的命名约定 |
||||
|
// 实际实现在ChatStoragePlugin.swift中 |
||||
|
public class SwiftChatStoragePlugin: NSObject, FlutterPlugin { |
||||
|
public static func register(with registrar: FlutterPluginRegistrar) { |
||||
|
// 调用实际实现类的注册方法 |
||||
|
ChatStoragePlugin.register(with: registrar) |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,64 @@ |
|||||
|
# Chat Storage 插件 - iOS 实现 |
||||
|
|
||||
|
## 概述 |
||||
|
|
||||
|
Chat Storage 插件的 iOS 实现,为 Flutter 应用提供本地 SQLite 数据库支持,用于存储和管理聊天记录。 |
||||
|
|
||||
|
## 功能特性 |
||||
|
|
||||
|
- 基于 SQLite 的聊天记录存储 |
||||
|
- 支持按会话分组消息 |
||||
|
- 提供分页查询功能 |
||||
|
- 支持元数据存储 |
||||
|
- 完整的增删改查 API |
||||
|
|
||||
|
## 技术实现 |
||||
|
|
||||
|
本插件使用原生 Swift 实现,通过 SQLite C API 直接操作数据库,不依赖第三方 SQLite 封装库,以保持最小的依赖要求和最佳性能。 |
||||
|
|
||||
|
### 核心组件 |
||||
|
|
||||
|
- `ChatStorageHelper`: 负责 SQLite 数据库操作,提供完整的数据操作 API |
||||
|
- `ChatStoragePlugin`: 插件主类,处理与 Flutter 的通信 |
||||
|
|
||||
|
### 数据库结构 |
||||
|
|
||||
|
数据库包含一个 `messages` 表,结构如下: |
||||
|
|
||||
|
```sql |
||||
|
CREATE TABLE messages ( |
||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT, |
||||
|
session_id TEXT NOT NULL, |
||||
|
message TEXT NOT NULL, |
||||
|
sender TEXT NOT NULL, |
||||
|
timestamp TEXT NOT NULL, |
||||
|
metadata TEXT |
||||
|
); |
||||
|
|
||||
|
-- 索引 |
||||
|
CREATE INDEX idx_session_id ON messages (session_id); |
||||
|
CREATE INDEX idx_timestamp ON messages (timestamp); |
||||
|
``` |
||||
|
|
||||
|
## 使用注意事项 |
||||
|
|
||||
|
### SQLite 多线程访问 |
||||
|
|
||||
|
本插件所有数据库操作均在主线程执行,以避免 SQLite 多线程访问的复杂性。在处理大量数据时,建议在 Flutter 层面使用 `compute` 函数或 `Isolate` 进行异步处理。 |
||||
|
|
||||
|
### 数据格式 |
||||
|
|
||||
|
- 时间戳使用 `yyyy-MM-dd HH:mm:ss` 格式的字符串 |
||||
|
- JSON 转换遵循 Swift/Objective-C 的类型映射规则 |
||||
|
|
||||
|
## 调试与故障排除 |
||||
|
|
||||
|
- 日志前缀为 `ChatStoragePlugin` 和 `ChatStorageHelper` |
||||
|
- 可通过 Console.app 查看日志输出 |
||||
|
- 数据库文件存储在应用的 Documents/Databases 目录下 |
||||
|
|
||||
|
## 开发者须知 |
||||
|
|
||||
|
如果需要扩展此插件的功能,比如添加新的表或字段,应同时更新 Android 和 iOS 端的实现,并确保在版本升级时提供适当的数据库迁移策略。 |
||||
|
|
||||
|
数据库迁移逻辑目前实现为重建表,在生产环境中应该改为保留用户数据的方式。 |
||||
@ -0,0 +1,197 @@ |
|||||
|
import Foundation |
||||
|
import ExternalAccessory |
||||
|
import CoreBluetooth |
||||
|
|
||||
|
/// 蓝牙事件类型 |
||||
|
enum BluetoothEventType { |
||||
|
case stateChanged |
||||
|
case deviceConnected |
||||
|
case deviceDisconnected |
||||
|
} |
||||
|
|
||||
|
/// 蓝牙状态枚举 |
||||
|
enum BluetoothState: String { |
||||
|
case unknown = "unknown" |
||||
|
case off = "off" |
||||
|
case on = "on" |
||||
|
case turningOn = "turningOn" |
||||
|
case turningOff = "turningOff" |
||||
|
} |
||||
|
|
||||
|
/// 经典蓝牙助手类,负责iOS上的蓝牙设备管理 |
||||
|
class ClassicBluetoothHelper: NSObject { |
||||
|
private let TAG = "ClassicBluetoothHelper" |
||||
|
|
||||
|
// 状态监控相关 |
||||
|
private var isMonitoring = false |
||||
|
private var stateCallback: ((BluetoothEventType, [String: Any]) -> Void)? |
||||
|
|
||||
|
// 用于监控蓝牙系统状态 |
||||
|
private var centralManager: CBCentralManager? |
||||
|
|
||||
|
// 通知观察者 |
||||
|
private var accessoryConnectionObserver: NSObjectProtocol? |
||||
|
private var accessoryDisconnectionObserver: NSObjectProtocol? |
||||
|
|
||||
|
override init() { |
||||
|
super.init() |
||||
|
centralManager = CBCentralManager(delegate: nil, queue: nil, options: [CBCentralManagerOptionShowPowerAlertKey: false]) |
||||
|
} |
||||
|
|
||||
|
deinit { |
||||
|
stopBluetoothStateMonitoring() |
||||
|
} |
||||
|
|
||||
|
/// 检查蓝牙是否启用 |
||||
|
func isBluetoothEnabled() -> Bool { |
||||
|
guard let centralManager = centralManager else { return false } |
||||
|
return centralManager.state == .poweredOn |
||||
|
} |
||||
|
|
||||
|
/// 获取已连接的蓝牙设备 |
||||
|
/// - Parameter completion: 完成回调 |
||||
|
func getConnectedDevices(completion: @escaping ([[String: String]], Error?) -> Void) { |
||||
|
// 获取所有已连接的外部配件 |
||||
|
let accessoryManager = EAAccessoryManager.shared() |
||||
|
let connectedAccessories = accessoryManager.connectedAccessories |
||||
|
|
||||
|
// 将配件转换为设备信息数组 |
||||
|
let devices = connectedAccessories.map { accessory -> [String: String] in |
||||
|
return [ |
||||
|
"name": accessory.name, |
||||
|
"address": accessory.serialNumber // iOS使用序列号作为唯一标识 |
||||
|
] |
||||
|
} |
||||
|
|
||||
|
// 返回结果 |
||||
|
completion(devices, nil) |
||||
|
} |
||||
|
|
||||
|
/// 开始监听蓝牙状态变化 |
||||
|
/// - Parameter callback: 状态变化回调 |
||||
|
func startBluetoothStateMonitoring(callback: @escaping (BluetoothEventType, [String: Any]) -> Void) { |
||||
|
self.stateCallback = callback |
||||
|
|
||||
|
if !isMonitoring { |
||||
|
isMonitoring = true |
||||
|
|
||||
|
// 设置蓝牙中央管理器代理 |
||||
|
centralManager?.delegate = self |
||||
|
|
||||
|
// 注册蓝牙配件连接/断开通知 |
||||
|
let notificationCenter = NotificationCenter.default |
||||
|
|
||||
|
// 观察配件连接 |
||||
|
accessoryConnectionObserver = notificationCenter.addObserver( |
||||
|
forName: NSNotification.Name.EAAccessoryDidConnect, |
||||
|
object: nil, |
||||
|
queue: nil |
||||
|
) { [weak self] notification in |
||||
|
guard let self = self, |
||||
|
let accessory = notification.userInfo?[EAAccessoryKey] as? EAAccessory else { |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
let device: [String: String] = [ |
||||
|
"name": accessory.name, |
||||
|
"address": accessory.serialNumber |
||||
|
] |
||||
|
|
||||
|
self.stateCallback?(.deviceConnected, ["device": device]) |
||||
|
} |
||||
|
|
||||
|
// 观察配件断开 |
||||
|
accessoryDisconnectionObserver = notificationCenter.addObserver( |
||||
|
forName: NSNotification.Name.EAAccessoryDidDisconnect, |
||||
|
object: nil, |
||||
|
queue: nil |
||||
|
) { [weak self] notification in |
||||
|
guard let self = self, |
||||
|
let accessory = notification.userInfo?[EAAccessoryKey] as? EAAccessory else { |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
let device: [String: String] = [ |
||||
|
"name": accessory.name, |
||||
|
"address": accessory.serialNumber |
||||
|
] |
||||
|
|
||||
|
self.stateCallback?(.deviceDisconnected, ["device": device]) |
||||
|
} |
||||
|
|
||||
|
// 开始监视配件变化 |
||||
|
EAAccessoryManager.shared().registerForLocalNotifications() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 停止监听蓝牙状态变化 |
||||
|
func stopBluetoothStateMonitoring() { |
||||
|
if isMonitoring { |
||||
|
isMonitoring = false |
||||
|
|
||||
|
// 移除蓝牙中央管理器代理 |
||||
|
centralManager?.delegate = nil |
||||
|
|
||||
|
// 注销配件通知观察者 |
||||
|
if let observer = accessoryConnectionObserver { |
||||
|
NotificationCenter.default.removeObserver(observer) |
||||
|
accessoryConnectionObserver = nil |
||||
|
} |
||||
|
|
||||
|
if let observer = accessoryDisconnectionObserver { |
||||
|
NotificationCenter.default.removeObserver(observer) |
||||
|
accessoryDisconnectionObserver = nil |
||||
|
} |
||||
|
|
||||
|
// 停止监视配件变化 |
||||
|
EAAccessoryManager.shared().unregisterForLocalNotifications() |
||||
|
} |
||||
|
|
||||
|
// 清除回调 |
||||
|
stateCallback = nil |
||||
|
} |
||||
|
|
||||
|
/// 报告当前蓝牙状态 |
||||
|
func reportCurrentBluetoothState() { |
||||
|
guard let centralManager = centralManager else { return } |
||||
|
|
||||
|
let state: BluetoothState |
||||
|
|
||||
|
switch centralManager.state { |
||||
|
case .poweredOn: |
||||
|
state = .on |
||||
|
case .poweredOff: |
||||
|
state = .off |
||||
|
case .resetting: |
||||
|
state = .turningOff // 最接近的近似值 |
||||
|
case .unauthorized, .unsupported: |
||||
|
state = .off // 功能上等同于关闭 |
||||
|
default: |
||||
|
state = .unknown |
||||
|
} |
||||
|
|
||||
|
stateCallback?(.stateChanged, ["state": state.rawValue]) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// MARK: - CBCentralManagerDelegate |
||||
|
extension ClassicBluetoothHelper: CBCentralManagerDelegate { |
||||
|
func centralManagerDidUpdateState(_ central: CBCentralManager) { |
||||
|
let state: BluetoothState |
||||
|
|
||||
|
switch central.state { |
||||
|
case .poweredOn: |
||||
|
state = .on |
||||
|
case .poweredOff: |
||||
|
state = .off |
||||
|
case .resetting: |
||||
|
state = .turningOff |
||||
|
case .unauthorized, .unsupported: |
||||
|
state = .off |
||||
|
default: |
||||
|
state = .unknown |
||||
|
} |
||||
|
|
||||
|
stateCallback?(.stateChanged, ["state": state.rawValue]) |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,4 @@ |
|||||
|
#import <Flutter/Flutter.h> |
||||
|
|
||||
|
@interface ClassicBluetoothPlugin : NSObject<FlutterPlugin> |
||||
|
@end |
||||
@ -0,0 +1,12 @@ |
|||||
|
#import "ClassicBluetoothPlugin.h" |
||||
|
#if __has_include(<classic_bluetooth/classic_bluetooth-Swift.h>) |
||||
|
#import <classic_bluetooth/classic_bluetooth-Swift.h> |
||||
|
#else |
||||
|
#import "classic_bluetooth-Swift.h" |
||||
|
#endif |
||||
|
|
||||
|
@implementation ClassicBluetoothPlugin |
||||
|
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar { |
||||
|
[SwiftClassicBluetoothPlugin registerWithRegistrar:registrar]; |
||||
|
} |
||||
|
@end |
||||
@ -0,0 +1,108 @@ |
|||||
|
import Flutter |
||||
|
import UIKit |
||||
|
import ExternalAccessory |
||||
|
|
||||
|
public class ClassicBluetoothPlugin: NSObject, FlutterPlugin { |
||||
|
private let TAG = "ClassicBluetoothPlugin" |
||||
|
private var methodChannel: FlutterMethodChannel? |
||||
|
private var eventChannel: FlutterEventChannel? |
||||
|
private var eventSink: FlutterEventSink? |
||||
|
private let bluetoothHelper = ClassicBluetoothHelper() |
||||
|
|
||||
|
public static func register(with registrar: FlutterPluginRegistrar) { |
||||
|
let channel = FlutterMethodChannel(name: "com.yunqiinnovation.classic_bluetooth/methods", binaryMessenger: registrar.messenger()) |
||||
|
let instance = ClassicBluetoothPlugin() |
||||
|
registrar.addMethodCallDelegate(instance, channel: channel) |
||||
|
|
||||
|
// 设置事件通道(用于蓝牙状态监控) |
||||
|
let eventChannel = FlutterEventChannel(name: "com.yunqiinnovation.classic_bluetooth/events", binaryMessenger: registrar.messenger()) |
||||
|
eventChannel.setStreamHandler(instance) |
||||
|
|
||||
|
instance.methodChannel = channel |
||||
|
instance.eventChannel = eventChannel |
||||
|
} |
||||
|
|
||||
|
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { |
||||
|
switch call.method { |
||||
|
case "getConnectedDevices": |
||||
|
getConnectedDevices(result: result) |
||||
|
case "isBluetoothEnabled": |
||||
|
result(bluetoothHelper.isBluetoothEnabled()) |
||||
|
default: |
||||
|
result(FlutterMethodNotImplemented) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private func getConnectedDevices(result: @escaping FlutterResult) { |
||||
|
if !bluetoothHelper.isBluetoothEnabled() { |
||||
|
result([]) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
bluetoothHelper.getConnectedDevices { devices, error in |
||||
|
if let error = error { |
||||
|
result(FlutterError(code: "BLUETOOTH_ERROR", message: error.localizedDescription, details: nil)) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
result(devices) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 发送事件到Flutter |
||||
|
private func sendEvent(_ event: [String: Any]) { |
||||
|
if let eventSink = eventSink { |
||||
|
eventSink(event) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// MARK: - FlutterStreamHandler |
||||
|
extension ClassicBluetoothPlugin: FlutterStreamHandler { |
||||
|
public func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { |
||||
|
self.eventSink = events |
||||
|
|
||||
|
// 开始监听蓝牙状态变化 |
||||
|
bluetoothHelper.startBluetoothStateMonitoring { [weak self] eventType, data in |
||||
|
guard let self = self else { return } |
||||
|
|
||||
|
switch eventType { |
||||
|
case .stateChanged: |
||||
|
if let state = data["state"] as? String { |
||||
|
self.sendEvent([ |
||||
|
"type": "stateChanged", |
||||
|
"state": state, |
||||
|
"timestamp": Int(Date().timeIntervalSince1970 * 1000) |
||||
|
]) |
||||
|
} |
||||
|
case .deviceConnected: |
||||
|
if let device = data["device"] as? [String: String] { |
||||
|
self.sendEvent([ |
||||
|
"type": "deviceConnected", |
||||
|
"device": device, |
||||
|
"timestamp": Int(Date().timeIntervalSince1970 * 1000) |
||||
|
]) |
||||
|
} |
||||
|
case .deviceDisconnected: |
||||
|
if let device = data["device"] as? [String: String] { |
||||
|
self.sendEvent([ |
||||
|
"type": "deviceDisconnected", |
||||
|
"device": device, |
||||
|
"timestamp": Int(Date().timeIntervalSince1970 * 1000) |
||||
|
]) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 报告当前蓝牙状态 |
||||
|
bluetoothHelper.reportCurrentBluetoothState() |
||||
|
|
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
public func onCancel(withArguments arguments: Any?) -> FlutterError? { |
||||
|
eventSink = nil |
||||
|
bluetoothHelper.stopBluetoothStateMonitoring() |
||||
|
return nil |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,136 @@ |
|||||
|
import Foundation |
||||
|
import os.log |
||||
|
|
||||
|
/// iOS端的文件日志助手类,提供与Android端FileLogger类似的功能 |
||||
|
class FileLogger { |
||||
|
|
||||
|
private static let logQueue = DispatchQueue(label: "com.yunqiinnovation.classic_bluetooth.logger", qos: .background) |
||||
|
private static let dateFormatter: DateFormatter = { |
||||
|
let formatter = DateFormatter() |
||||
|
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss.SSS" |
||||
|
return formatter |
||||
|
}() |
||||
|
|
||||
|
/// 日志类型 |
||||
|
enum LogLevel: String { |
||||
|
case debug = "DEBUG" |
||||
|
case info = "INFO" |
||||
|
case warning = "WARNING" |
||||
|
case error = "ERROR" |
||||
|
} |
||||
|
|
||||
|
/// 输出调试日志 |
||||
|
/// - Parameters: |
||||
|
/// - tag: 标签 |
||||
|
/// - message: 日志消息 |
||||
|
static func d(_ tag: String, _ message: String) { |
||||
|
log(level: .debug, tag: tag, message: message) |
||||
|
} |
||||
|
|
||||
|
/// 输出信息日志 |
||||
|
/// - Parameters: |
||||
|
/// - tag: 标签 |
||||
|
/// - message: 日志消息 |
||||
|
static func i(_ tag: String, _ message: String) { |
||||
|
log(level: .info, tag: tag, message: message) |
||||
|
} |
||||
|
|
||||
|
/// 输出警告日志 |
||||
|
/// - Parameters: |
||||
|
/// - tag: 标签 |
||||
|
/// - message: 日志消息 |
||||
|
static func w(_ tag: String, _ message: String) { |
||||
|
log(level: .warning, tag: tag, message: message) |
||||
|
} |
||||
|
|
||||
|
/// 输出错误日志 |
||||
|
/// - Parameters: |
||||
|
/// - tag: 标签 |
||||
|
/// - message: 日志消息 |
||||
|
/// - error: 可选的错误对象 |
||||
|
static func e(_ tag: String, _ message: String, _ error: Error? = nil) { |
||||
|
var fullMessage = message |
||||
|
if let error = error { |
||||
|
fullMessage += " - Error: \(error.localizedDescription)" |
||||
|
} |
||||
|
log(level: .error, tag: tag, message: fullMessage) |
||||
|
} |
||||
|
|
||||
|
/// 输出日志 |
||||
|
/// - Parameters: |
||||
|
/// - level: 日志级别 |
||||
|
/// - tag: 标签 |
||||
|
/// - message: 日志消息 |
||||
|
private static func log(level: LogLevel, tag: String, message: String) { |
||||
|
let timestamp = dateFormatter.string(from: Date()) |
||||
|
let logMessage = "[\(timestamp)] [\(level.rawValue)] [\(tag)]: \(message)" |
||||
|
|
||||
|
// 输出到控制台 |
||||
|
print(logMessage) |
||||
|
|
||||
|
// 异步写入日志文件 |
||||
|
logQueue.async { |
||||
|
writeToLogFile(logMessage) |
||||
|
} |
||||
|
|
||||
|
// 使用系统日志 |
||||
|
if #available(iOS 10.0, *) { |
||||
|
let osLog = OSLog(subsystem: "com.yunqiinnovation.classic_bluetooth", category: tag) |
||||
|
let osLogType: OSLogType |
||||
|
|
||||
|
switch level { |
||||
|
case .debug: |
||||
|
osLogType = .debug |
||||
|
case .info: |
||||
|
osLogType = .info |
||||
|
case .warning: |
||||
|
osLogType = .default |
||||
|
case .error: |
||||
|
osLogType = .error |
||||
|
} |
||||
|
|
||||
|
os_log("%{public}@", log: osLog, type: osLogType, message) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 将日志写入文件 |
||||
|
/// - Parameter message: 日志消息 |
||||
|
private static func writeToLogFile(_ message: String) { |
||||
|
guard let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
let logDirectoryURL = documentsDirectory.appendingPathComponent("Logs") |
||||
|
let dateString = DateFormatter().with { $0.dateFormat = "yyyy-MM-dd" }.string(from: Date()) |
||||
|
let logFileURL = logDirectoryURL.appendingPathComponent("bluetooth_\(dateString).log") |
||||
|
|
||||
|
do { |
||||
|
// 创建日志文件夹 |
||||
|
try FileManager.default.createDirectory(at: logDirectoryURL, withIntermediateDirectories: true) |
||||
|
|
||||
|
// 如果文件不存在,创建文件 |
||||
|
if !FileManager.default.fileExists(atPath: logFileURL.path) { |
||||
|
FileManager.default.createFile(atPath: logFileURL.path, contents: nil) |
||||
|
} |
||||
|
|
||||
|
// 追加日志内容 |
||||
|
if let fileHandle = FileHandle(forWritingAtPath: logFileURL.path) { |
||||
|
fileHandle.seekToEndOfFile() |
||||
|
if let data = "\(message)\n".data(using: .utf8) { |
||||
|
fileHandle.write(data) |
||||
|
} |
||||
|
fileHandle.closeFile() |
||||
|
} |
||||
|
} catch { |
||||
|
print("写入日志文件失败: \(error.localizedDescription)") |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// MARK: - DateFormatter 扩展 |
||||
|
extension DateFormatter { |
||||
|
func with(_ block: (DateFormatter) -> Void) -> DateFormatter { |
||||
|
block(self) |
||||
|
return self |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,11 @@ |
|||||
|
import Flutter |
||||
|
import UIKit |
||||
|
|
||||
|
// 这个文件是为了满足Flutter插件的命名约定 |
||||
|
// 实际实现在ClassicBluetoothPlugin.swift中 |
||||
|
public class SwiftClassicBluetoothPlugin: NSObject, FlutterPlugin { |
||||
|
public static func register(with registrar: FlutterPluginRegistrar) { |
||||
|
// 调用真正的实现 |
||||
|
ClassicBluetoothPlugin.register(with: registrar) |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,44 @@ |
|||||
|
# iOS蓝牙配置说明 |
||||
|
|
||||
|
## 所需权限 |
||||
|
|
||||
|
要在iOS中使用经典蓝牙功能,需要在应用的`Info.plist`中添加以下配置: |
||||
|
|
||||
|
### 1. 外部附件协议支持 |
||||
|
|
||||
|
在Info.plist中添加`UISupportedExternalAccessoryProtocols`数组,并添加你的设备支持的协议。如果不确定,可以先添加常见的协议: |
||||
|
|
||||
|
```xml |
||||
|
<key>UISupportedExternalAccessoryProtocols</key> |
||||
|
<array> |
||||
|
<string>com.example.protocol</string> |
||||
|
<!-- 添加其他你设备支持的协议 --> |
||||
|
</array> |
||||
|
``` |
||||
|
|
||||
|
### 2. 蓝牙使用说明 |
||||
|
|
||||
|
在iOS 13及以上版本中,需要添加蓝牙使用说明: |
||||
|
|
||||
|
```xml |
||||
|
<key>NSBluetoothAlwaysUsageDescription</key> |
||||
|
<string>我们需要使用蓝牙来连接您的音频设备</string> |
||||
|
``` |
||||
|
|
||||
|
对于iOS 13以下版本: |
||||
|
|
||||
|
```xml |
||||
|
<key>NSBluetoothPeripheralUsageDescription</key> |
||||
|
<string>我们需要使用蓝牙来连接您的音频设备</string> |
||||
|
``` |
||||
|
|
||||
|
## 常见问题 |
||||
|
|
||||
|
1. 如果看不到任何设备,请确保: |
||||
|
- 设备已配对并连接到iOS设备 |
||||
|
- 设备是MFi认证的(Made for iPhone/iPad),因为iOS对经典蓝牙有限制 |
||||
|
- 添加了正确的协议字符串 |
||||
|
|
||||
|
2. 蓝牙状态变化不及时: |
||||
|
- iOS系统对蓝牙状态变化的通知可能有延迟 |
||||
|
- 请确保用户已经授权蓝牙权限 |
||||
@ -0,0 +1,22 @@ |
|||||
|
# |
||||
|
# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. |
||||
|
# |
||||
|
Pod::Spec.new do |s| |
||||
|
s.name = 'classic_bluetooth' |
||||
|
s.version = '0.0.1' |
||||
|
s.summary = '经典蓝牙插件' |
||||
|
s.description = <<-DESC |
||||
|
一个用于管理经典蓝牙设备的Flutter插件。 |
||||
|
DESC |
||||
|
s.homepage = 'http://yunqiinnovation.com' |
||||
|
s.license = { :file => '../LICENSE' } |
||||
|
s.author = { 'YunQi Innovation' => 'support@yunqiinnovation.com' } |
||||
|
s.source = { :path => '.' } |
||||
|
s.source_files = 'Classes/**/*' |
||||
|
|
||||
|
s.ios.deployment_target = '11.0' |
||||
|
s.swift_version = '5.0' |
||||
|
|
||||
|
# 添加CoreBluetooth和ExternalAccessory框架依赖 |
||||
|
s.framework = 'CoreBluetooth', 'ExternalAccessory' |
||||
|
end |
||||
@ -0,0 +1,4 @@ |
|||||
|
#import <Flutter/Flutter.h> |
||||
|
|
||||
|
@interface DeepvoiceUtilsPlugin : NSObject<FlutterPlugin> |
||||
|
@end |
||||
@ -0,0 +1,12 @@ |
|||||
|
#import "DeepvoiceUtilsPlugin.h" |
||||
|
#if __has_include(<deepvoice_utils/deepvoice_utils-Swift.h>) |
||||
|
#import <deepvoice_utils/deepvoice_utils-Swift.h> |
||||
|
#else |
||||
|
#import "deepvoice_utils-Swift.h" |
||||
|
#endif |
||||
|
|
||||
|
@implementation DeepvoiceUtilsPlugin |
||||
|
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar { |
||||
|
[SwiftDeepvoiceUtilsPlugin registerWithRegistrar:registrar]; |
||||
|
} |
||||
|
@end |
||||
@ -0,0 +1,30 @@ |
|||||
|
import Flutter |
||||
|
import UIKit |
||||
|
|
||||
|
public class DeepvoiceUtilsPlugin: NSObject, FlutterPlugin { |
||||
|
private static let TAG = "DeepvoiceUtilsPlugin" |
||||
|
|
||||
|
public static func register(with registrar: FlutterPluginRegistrar) { |
||||
|
let channel = FlutterMethodChannel(name: "deepvoice_utils", binaryMessenger: registrar.messenger()) |
||||
|
let instance = DeepvoiceUtilsPlugin() |
||||
|
registrar.addMethodCallDelegate(instance, channel: channel) |
||||
|
|
||||
|
// 初始化文件日志 |
||||
|
FileLogger.initialize() |
||||
|
FileLogger.d(TAG, "DeepvoiceUtilsPlugin 已初始化") |
||||
|
} |
||||
|
|
||||
|
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { |
||||
|
switch call.method { |
||||
|
case "getPlatformVersion": |
||||
|
result("iOS \(UIDevice.current.systemVersion)") |
||||
|
default: |
||||
|
result(FlutterMethodNotImplemented) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public func detachFromEngine(for registrar: FlutterPluginRegistrar) { |
||||
|
// 关闭日志系统 |
||||
|
FileLogger.shutdown() |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,286 @@ |
|||||
|
import Foundation |
||||
|
import os.log |
||||
|
|
||||
|
/** |
||||
|
* 文件日志工具 |
||||
|
* 将日志同时输出到控制台和文件系统 |
||||
|
*/ |
||||
|
@objc public class FileLogger: NSObject { |
||||
|
|
||||
|
private static let TAG = "FileLogger" |
||||
|
private static var instance: FileLogger? |
||||
|
|
||||
|
private let logDir: URL |
||||
|
private var currentLogFile: URL? |
||||
|
private let logQueue = DispatchQueue(label: "com.yunqiinnovation.deepvoice_utils.logger", qos: .utility) |
||||
|
|
||||
|
private let dateFormatter: DateFormatter = { |
||||
|
let formatter = DateFormatter() |
||||
|
formatter.dateFormat = "yyyy-MM-dd" |
||||
|
return formatter |
||||
|
}() |
||||
|
|
||||
|
private let timeFormatter: DateFormatter = { |
||||
|
let formatter = DateFormatter() |
||||
|
formatter.dateFormat = "HH:mm:ss.SSS" |
||||
|
return formatter |
||||
|
}() |
||||
|
|
||||
|
/** |
||||
|
* 初始化日志系统 |
||||
|
*/ |
||||
|
private init(directory: URL) { |
||||
|
self.logDir = directory.appendingPathComponent("logs", isDirectory: true) |
||||
|
super.init() |
||||
|
|
||||
|
// 创建日志目录 |
||||
|
try? FileManager.default.createDirectory(at: logDir, withIntermediateDirectories: true) |
||||
|
|
||||
|
// 创建当天日志文件 |
||||
|
updateLogFile() |
||||
|
|
||||
|
// 记录初始化完成 |
||||
|
self.d(FileLogger.TAG, "文件日志系统已初始化") |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 更新日志文件为当天日期 |
||||
|
*/ |
||||
|
private func updateLogFile() { |
||||
|
let today = dateFormatter.string(from: Date()) |
||||
|
let logFile = logDir.appendingPathComponent("log_\(today).txt") |
||||
|
|
||||
|
if currentLogFile == nil || currentLogFile?.path != logFile.path { |
||||
|
currentLogFile = logFile |
||||
|
|
||||
|
// 如果是新文件,添加文件头 |
||||
|
if !FileManager.default.fileExists(atPath: logFile.path) { |
||||
|
let headerDate = DateFormatter() |
||||
|
headerDate.dateFormat = "yyyy-MM-dd HH:mm:ss" |
||||
|
let headerLine = "===== 日志开始: \(headerDate.string(from: Date())) =====\n" |
||||
|
|
||||
|
try? headerLine.write(to: logFile, atomically: true, encoding: .utf8) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 将日志写入文件 |
||||
|
*/ |
||||
|
private func logToFile(level: String, tag: String, message: String) { |
||||
|
logQueue.async { [weak self] in |
||||
|
guard let self = self else { return } |
||||
|
|
||||
|
do { |
||||
|
// 检查是否需要更新日志文件 |
||||
|
self.updateLogFile() |
||||
|
|
||||
|
guard let logFile = self.currentLogFile, |
||||
|
FileManager.default.fileExists(atPath: logFile.path) else { |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
// 格式化日志内容 |
||||
|
let timestamp = self.timeFormatter.string(from: Date()) |
||||
|
let logLine = "\(timestamp) \(level)/\(tag): \(message)\n" |
||||
|
|
||||
|
// 获取文件句柄并追加内容 |
||||
|
if let fileHandle = try? FileHandle(forWritingTo: logFile) { |
||||
|
fileHandle.seekToEndOfFile() |
||||
|
if let data = logLine.data(using: .utf8) { |
||||
|
fileHandle.write(data) |
||||
|
} |
||||
|
fileHandle.closeFile() |
||||
|
} else { |
||||
|
// 如果无法获取文件句柄,尝试直接写入 |
||||
|
let existingContent = try? String(contentsOf: logFile, encoding: .utf8) |
||||
|
let newContent = (existingContent ?? "") + logLine |
||||
|
try newContent.write(to: logFile, atomically: true, encoding: .utf8) |
||||
|
} |
||||
|
} catch { |
||||
|
print("写入日志文件失败: \(error.localizedDescription)") |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 调试级别日志 |
||||
|
*/ |
||||
|
private func d(_ tag: String, _ message: String) { |
||||
|
// 输出到控制台 |
||||
|
print("D/\(tag): \(message)") |
||||
|
|
||||
|
// 写入文件 |
||||
|
logToFile(level: "D", tag: tag, message: message) |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 信息级别日志 |
||||
|
*/ |
||||
|
private func i(_ tag: String, _ message: String) { |
||||
|
// 输出到控制台 |
||||
|
print("I/\(tag): \(message)") |
||||
|
|
||||
|
// 写入文件 |
||||
|
logToFile(level: "I", tag: tag, message: message) |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 警告级别日志 |
||||
|
*/ |
||||
|
private func w(_ tag: String, _ message: String) { |
||||
|
// 输出到控制台 |
||||
|
print("W/\(tag): \(message)") |
||||
|
|
||||
|
// 写入文件 |
||||
|
logToFile(level: "W", tag: tag, message: message) |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 错误级别日志 |
||||
|
*/ |
||||
|
private func e(_ tag: String, _ message: String, _ error: Error? = nil) { |
||||
|
var fullMessage = message |
||||
|
|
||||
|
// 如果有错误,添加错误信息 |
||||
|
if let error = error { |
||||
|
fullMessage += "\n\(error.localizedDescription)" |
||||
|
} |
||||
|
|
||||
|
// 输出到控制台 |
||||
|
print("E/\(tag): \(fullMessage)") |
||||
|
|
||||
|
// 写入文件 |
||||
|
logToFile(level: "E", tag: tag, message: fullMessage) |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 清理旧日志文件 |
||||
|
*/ |
||||
|
private func cleanOldLogs(daysToKeep: Int = 7) { |
||||
|
logQueue.async { |
||||
|
do { |
||||
|
let currentDate = Date() |
||||
|
let fileManager = FileManager.default |
||||
|
|
||||
|
let logFiles = try fileManager.contentsOfDirectory(at: self.logDir, includingPropertiesForKeys: [.creationDateKey], options: []) |
||||
|
|
||||
|
for fileURL in logFiles where fileURL.pathExtension == "txt" { |
||||
|
if let fileAttributes = try? fileManager.attributesOfItem(atPath: fileURL.path), |
||||
|
let creationDate = fileAttributes[.creationDate] as? Date { |
||||
|
|
||||
|
let age = Calendar.current.dateComponents([.day], from: creationDate, to: currentDate) |
||||
|
|
||||
|
if let days = age.day, days > daysToKeep { |
||||
|
try? fileManager.removeItem(at: fileURL) |
||||
|
print("D/\(FileLogger.TAG): 已删除旧日志文件: \(fileURL.lastPathComponent)") |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} catch { |
||||
|
print("E/\(FileLogger.TAG): 清理旧日志失败: \(error.localizedDescription)") |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// MARK: - 公共静态方法 |
||||
|
|
||||
|
/** |
||||
|
* 初始化日志系统 |
||||
|
*/ |
||||
|
@objc public static func initialize() { |
||||
|
if instance == nil { |
||||
|
let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] |
||||
|
instance = FileLogger(directory: documentsDirectory) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 初始化日志系统(使用自定义目录) |
||||
|
*/ |
||||
|
@objc public static func initialize(withDirectory directory: URL) { |
||||
|
if instance == nil { |
||||
|
instance = FileLogger(directory: directory) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 调试日志 |
||||
|
*/ |
||||
|
@objc public static func d(_ tag: String, _ message: String) { |
||||
|
// 如果实例不存在,先使用默认目录初始化 |
||||
|
if instance == nil { |
||||
|
initialize() |
||||
|
} |
||||
|
|
||||
|
instance?.d(tag, message) |
||||
|
|
||||
|
// 使用系统日志 |
||||
|
if #available(iOS 10.0, *) { |
||||
|
os_log("%{public}@", log: OSLog(subsystem: "com.yunqiinnovation.deepvoice_utils", category: tag), type: .debug, message) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 信息日志 |
||||
|
*/ |
||||
|
@objc public static func i(_ tag: String, _ message: String) { |
||||
|
if instance == nil { |
||||
|
initialize() |
||||
|
} |
||||
|
|
||||
|
instance?.i(tag, message) |
||||
|
|
||||
|
// 使用系统日志 |
||||
|
if #available(iOS 10.0, *) { |
||||
|
os_log("%{public}@", log: OSLog(subsystem: "com.yunqiinnovation.deepvoice_utils", category: tag), type: .info, message) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 警告日志 |
||||
|
*/ |
||||
|
@objc public static func w(_ tag: String, _ message: String) { |
||||
|
if instance == nil { |
||||
|
initialize() |
||||
|
} |
||||
|
|
||||
|
instance?.w(tag, message) |
||||
|
|
||||
|
// 使用系统日志 |
||||
|
if #available(iOS 10.0, *) { |
||||
|
os_log("%{public}@", log: OSLog(subsystem: "com.yunqiinnovation.deepvoice_utils", category: tag), type: .default, message) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 错误日志 |
||||
|
*/ |
||||
|
@objc public static func e(_ tag: String, _ message: String, _ error: Error? = nil) { |
||||
|
if instance == nil { |
||||
|
initialize() |
||||
|
} |
||||
|
|
||||
|
instance?.e(tag, message, error) |
||||
|
|
||||
|
// 使用系统日志 |
||||
|
if #available(iOS 10.0, *) { |
||||
|
let errorMessage = error != nil ? "\(message): \(error!.localizedDescription)" : message |
||||
|
os_log("%{public}@", log: OSLog(subsystem: "com.yunqiinnovation.deepvoice_utils", category: tag), type: .error, errorMessage) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 清理旧日志 |
||||
|
*/ |
||||
|
@objc public static func cleanOldLogs(daysToKeep: Int = 7) { |
||||
|
instance?.cleanOldLogs(daysToKeep: daysToKeep) |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 关闭日志系统 |
||||
|
*/ |
||||
|
@objc public static func shutdown() { |
||||
|
instance = nil |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,11 @@ |
|||||
|
import Flutter |
||||
|
import UIKit |
||||
|
|
||||
|
// 这个类是为了满足Flutter插件的命名约定 |
||||
|
// 实际实现在DeepvoiceUtilsPlugin.swift中 |
||||
|
public class SwiftDeepvoiceUtilsPlugin: NSObject, FlutterPlugin { |
||||
|
public static func register(with registrar: FlutterPluginRegistrar) { |
||||
|
// 调用真正的实现 |
||||
|
DeepvoiceUtilsPlugin.register(with: registrar) |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,54 @@ |
|||||
|
# DeepVoice Utils iOS 实现 |
||||
|
|
||||
|
## 简介 |
||||
|
|
||||
|
本模块是 `deepvoice_utils` 插件的 iOS 实现部分,主要提供以下功能: |
||||
|
|
||||
|
- 文件日志记录(FileLogger):将日志记录到文件,方便调试和问题排查 |
||||
|
|
||||
|
## 使用说明 |
||||
|
|
||||
|
### 日志系统 |
||||
|
|
||||
|
该插件自动初始化日志系统,开发者无需手动调用初始化方法。日志文件默认保存在应用的 Documents 目录下的 logs 文件夹中,按日期分文件存储。 |
||||
|
|
||||
|
## 配置要求 |
||||
|
|
||||
|
- iOS 11.0 或更高版本 |
||||
|
- Swift 5.0 |
||||
|
- 确保应用拥有文件写入权限 |
||||
|
|
||||
|
## 开发说明 |
||||
|
|
||||
|
### 文件结构 |
||||
|
|
||||
|
- `DeepvoiceUtilsPlugin.swift`:插件主类,负责与 Flutter 交互 |
||||
|
- `FileLogger.swift`:日志系统实现,提供日志记录功能 |
||||
|
- `SwiftDeepvoiceUtilsPlugin.swift`:命名约定桥接类 |
||||
|
- `DeepvoiceUtilsPlugin.h/.m`:Objective-C 桥接文件 |
||||
|
|
||||
|
### 自定义日志目录 |
||||
|
|
||||
|
如需使用自定义日志目录,可以在应用启动时手动调用初始化方法: |
||||
|
|
||||
|
```swift |
||||
|
import deepvoice_utils |
||||
|
|
||||
|
// 在 AppDelegate 中 |
||||
|
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { |
||||
|
// 自定义日志目录 |
||||
|
let customDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0].appendingPathComponent("custom_logs") |
||||
|
FileLogger.initialize(withDirectory: customDirectory) |
||||
|
|
||||
|
return true |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
### 清理旧日志 |
||||
|
|
||||
|
默认情况下,日志系统不会自动清理旧日志。如果需要清理,可以手动调用: |
||||
|
|
||||
|
```swift |
||||
|
// 清理7天前的日志 |
||||
|
FileLogger.cleanOldLogs(daysToKeep: 7) |
||||
|
``` |
||||
@ -0,0 +1,19 @@ |
|||||
|
# |
||||
|
# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. |
||||
|
# |
||||
|
Pod::Spec.new do |s| |
||||
|
s.name = 'deepvoice_utils' |
||||
|
s.version = '0.0.1' |
||||
|
s.summary = '深语工具库' |
||||
|
s.description = <<-DESC |
||||
|
提供文件日志记录等通用功能的Flutter插件。 |
||||
|
DESC |
||||
|
s.homepage = 'http://yunqiinnovation.com' |
||||
|
s.license = { :file => '../LICENSE' } |
||||
|
s.author = { 'YunQi Innovation' => 'support@yunqiinnovation.com' } |
||||
|
s.source = { :path => '.' } |
||||
|
s.source_files = 'Classes/**/*' |
||||
|
|
||||
|
s.ios.deployment_target = '11.0' |
||||
|
s.swift_version = '5.0' |
||||
|
end |
||||
@ -0,0 +1,249 @@ |
|||||
|
import Foundation |
||||
|
|
||||
|
/// 工具类型枚举 |
||||
|
enum ToolType { |
||||
|
case localFunction // 本地函数 |
||||
|
case mcpTool // MCP工具 |
||||
|
} |
||||
|
|
||||
|
/// 函数处理器协议 |
||||
|
protocol FunctionHandler { |
||||
|
/// 处理函数调用 |
||||
|
/// - Parameters: |
||||
|
/// - arguments: 函数参数,Dictionary格式 |
||||
|
/// - Returns: 函数执行结果,字符串 |
||||
|
func handle(arguments: [String: Any]) async throws -> String |
||||
|
} |
||||
|
|
||||
|
/// MCPClient - iOS版本,负责连接到Model Context Protocol服务器和管理本地函数 |
||||
|
public class MCPClient { |
||||
|
private let TAG = "MCPClient" |
||||
|
|
||||
|
// 本地函数Map,函数名 -> 处理器 |
||||
|
private var localFunctions = [String: FunctionHandler]() |
||||
|
|
||||
|
// 工具定义Map |
||||
|
private var toolDefinitions = [[String: Any]]() |
||||
|
|
||||
|
// 是否已连接 |
||||
|
private var isConnected = false |
||||
|
|
||||
|
// 系统功能处理器 |
||||
|
private var systemFunctionHandler: SystemFunctionHandler? |
||||
|
|
||||
|
// SSE会话 |
||||
|
private var sseSession: URLSession? |
||||
|
private var sseTask: URLSessionDataTask? |
||||
|
|
||||
|
/// 初始化MCPClient |
||||
|
/// - Parameter context: 可选的应用上下文 |
||||
|
init(_ context: Any? = nil) { |
||||
|
if let appContext = context { |
||||
|
systemFunctionHandler = SystemFunctionHandler(context: appContext) |
||||
|
|
||||
|
// 初始化语音功能 |
||||
|
initializeVoiceFunctions() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 初始化语音功能处理器 |
||||
|
private func initializeVoiceFunctions() { |
||||
|
do { |
||||
|
// 使用SystemFunctionHandler注册所有功能 |
||||
|
systemFunctionHandler?.registerAllFunctions(client: self) |
||||
|
|
||||
|
print("\(TAG) 语音功能初始化完成") |
||||
|
} catch { |
||||
|
print("\(TAG) 语音功能初始化失败: \(error.localizedDescription)") |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 注册本地函数 |
||||
|
/// - Parameters: |
||||
|
/// - name: 函数名称 |
||||
|
/// - description: 函数描述 |
||||
|
/// - parameters: 函数参数定义 |
||||
|
/// - handler: 函数处理器 |
||||
|
/// - Returns: 是否注册成功 |
||||
|
func registerLocalFunction(name: String, description: String, parameters: [String: Any], handler: FunctionHandler) -> Bool { |
||||
|
do { |
||||
|
// 存储函数处理器 |
||||
|
localFunctions[name] = handler |
||||
|
|
||||
|
// 创建函数定义 |
||||
|
let functionDef: [String: Any] = [ |
||||
|
"name": name, |
||||
|
"description": description, |
||||
|
"parameters": parameters |
||||
|
] |
||||
|
|
||||
|
// 添加到工具定义列表 |
||||
|
let toolDef: [String: Any] = [ |
||||
|
"type": "function", |
||||
|
"function": functionDef |
||||
|
] |
||||
|
|
||||
|
// 更新或添加工具定义 |
||||
|
if let index = toolDefinitions.firstIndex(where: { |
||||
|
guard let function = $0["function"] as? [String: Any], |
||||
|
let functionName = function["name"] as? String else { |
||||
|
return false |
||||
|
} |
||||
|
return functionName == name |
||||
|
}) { |
||||
|
toolDefinitions[index] = toolDef |
||||
|
} else { |
||||
|
toolDefinitions.append(toolDef) |
||||
|
} |
||||
|
|
||||
|
print("\(TAG) 成功注册本地函数: \(name)") |
||||
|
return true |
||||
|
} catch { |
||||
|
print("\(TAG) 注册函数失败: \(error.localizedDescription)") |
||||
|
return false |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 连接到MCP服务器(通过SSE) |
||||
|
/// - Parameter mcpServer: 服务器URL |
||||
|
/// - Returns: 是否连接成功 |
||||
|
func connectToSSE(_ mcpServer: String) -> Bool { |
||||
|
// 关闭现有连接 |
||||
|
closeConnection() |
||||
|
|
||||
|
// 如果URL为空,返回失败 |
||||
|
if mcpServer.isEmpty { |
||||
|
print("\(TAG) MCP服务器URL为空") |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
// 解析服务器URL |
||||
|
guard let serverURL = URL(string: mcpServer) else { |
||||
|
print("\(TAG) 无效的MCP服务器URL: \(mcpServer)") |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
print("\(TAG) 正在连接到MCP服务器: \(mcpServer)") |
||||
|
|
||||
|
// 创建会话配置 |
||||
|
let config = URLSessionConfiguration.default |
||||
|
config.timeoutIntervalForRequest = 30 |
||||
|
config.timeoutIntervalForResource = 60 |
||||
|
|
||||
|
// 创建会话 |
||||
|
let session = URLSession(configuration: config) |
||||
|
self.sseSession = session |
||||
|
|
||||
|
// 创建请求 |
||||
|
var request = URLRequest(url: serverURL) |
||||
|
request.httpMethod = "GET" |
||||
|
request.addValue("text/event-stream", forHTTPHeaderField: "Accept") |
||||
|
|
||||
|
// 创建并启动SSE任务 |
||||
|
let task = session.dataTask(with: request) { [weak self] data, response, error in |
||||
|
guard let self = self else { return } |
||||
|
|
||||
|
if let error = error { |
||||
|
print("\(self.TAG) MCP服务器连接失败: \(error.localizedDescription)") |
||||
|
self.isConnected = false |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
if let httpResponse = response as? HTTPURLResponse { |
||||
|
if httpResponse.statusCode == 200 { |
||||
|
print("\(self.TAG) 成功连接到MCP服务器") |
||||
|
self.isConnected = true |
||||
|
|
||||
|
// 处理服务器响应数据 |
||||
|
if let data = data, let responseString = String(data: data, encoding: .utf8) { |
||||
|
print("\(self.TAG) 收到初始响应: \(responseString)") |
||||
|
|
||||
|
// 在这里解析可用工具列表,暂时模拟 |
||||
|
// 实际情况下,需要实现SSE流解析 |
||||
|
} |
||||
|
} else { |
||||
|
print("\(self.TAG) MCP服务器连接失败,状态码: \(httpResponse.statusCode)") |
||||
|
self.isConnected = false |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
task.resume() |
||||
|
self.sseTask = task |
||||
|
|
||||
|
// 在iOS中,连接是异步的,此处返回true表示连接已开始 |
||||
|
// 实际连接状态通过isConnected属性检查 |
||||
|
return true |
||||
|
} |
||||
|
|
||||
|
/// 检查是否已连接 |
||||
|
/// - Returns: 是否已连接 |
||||
|
func isConnected() -> Bool { |
||||
|
return isConnected |
||||
|
} |
||||
|
|
||||
|
/// 关闭连接 |
||||
|
func closeConnection() { |
||||
|
sseTask?.cancel() |
||||
|
sseTask = nil |
||||
|
isConnected = false |
||||
|
} |
||||
|
|
||||
|
/// 获取工具定义列表,用于OpenAI请求 |
||||
|
/// - Returns: 工具定义列表 |
||||
|
func getToolMaps() -> [[String: Any]] { |
||||
|
return toolDefinitions |
||||
|
} |
||||
|
|
||||
|
/// 解析JSON参数 |
||||
|
/// - Parameter argumentsJson: JSON字符串 |
||||
|
/// - Returns: 解析后的参数字典 |
||||
|
func parseJsonArguments(_ argumentsJson: String) -> [String: Any] { |
||||
|
guard !argumentsJson.isEmpty, |
||||
|
let data = argumentsJson.data(using: .utf8), |
||||
|
let parsedArgs = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { |
||||
|
return [:] |
||||
|
} |
||||
|
return parsedArgs |
||||
|
} |
||||
|
|
||||
|
/// 调用工具 |
||||
|
/// - Parameters: |
||||
|
/// - name: 工具名称 |
||||
|
/// - arguments: 工具参数 |
||||
|
/// - Returns: 工具调用结果 |
||||
|
func callTool(name: String, arguments: [String: Any]) async -> [String: Any] { |
||||
|
do { |
||||
|
// 检查是否为本地函数 |
||||
|
if let handler = localFunctions[name] { |
||||
|
let result = try await handler.handle(arguments: arguments) |
||||
|
return [ |
||||
|
"name": name, |
||||
|
"context": result |
||||
|
] |
||||
|
} |
||||
|
|
||||
|
// 如果不是本地函数,尝试作为MCP工具调用 |
||||
|
if isConnected { |
||||
|
// 这里应该实现真正的MCP工具调用 |
||||
|
// 暂时返回一个模拟结果 |
||||
|
return [ |
||||
|
"name": name, |
||||
|
"context": "模拟MCP工具调用结果: \(name)(\(arguments))" |
||||
|
] |
||||
|
} |
||||
|
|
||||
|
return ["context": "无法处理工具调用:找不到工具 \(name) 或MCP未连接"] |
||||
|
} catch { |
||||
|
print("\(TAG) 调用工具失败: \(error.localizedDescription)") |
||||
|
return ["context": "工具调用失败: \(error.localizedDescription)"] |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 关闭MCP客户端 |
||||
|
func close() { |
||||
|
closeConnection() |
||||
|
localFunctions.removeAll() |
||||
|
toolDefinitions.removeAll() |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,514 @@ |
|||||
|
import Foundation |
||||
|
import UIKit |
||||
|
import CoreLocation |
||||
|
import Contacts |
||||
|
import EventKit |
||||
|
|
||||
|
/// 系统功能处理器 |
||||
|
class SystemFunctionHandler { |
||||
|
private let TAG = "SystemFunctionHandler" |
||||
|
private let context: Any |
||||
|
static let ACTION_EXIT_INTERACTION = "com.yunqiinnovation.deepsound.action.EXIT_INTERACTION" |
||||
|
|
||||
|
/// 初始化系统功能处理器 |
||||
|
/// - Parameter context: 应用上下文 |
||||
|
init(context: Any) { |
||||
|
self.context = context |
||||
|
} |
||||
|
|
||||
|
/// 注册所有系统功能 |
||||
|
/// - Parameter client: MCP客户端 |
||||
|
func registerAllFunctions(client: MCPClient) { |
||||
|
// 注册退出交互函数 |
||||
|
registerExitInteractionFunction(client) |
||||
|
|
||||
|
// 注册翻译模式函数 |
||||
|
registerTranslationModeFunction(client) |
||||
|
|
||||
|
// 注册发送短信函数 |
||||
|
registerSendTextMessageFunction(client) |
||||
|
|
||||
|
// 注册拨打电话函数 |
||||
|
registerMakePhoneCallFunction(client) |
||||
|
|
||||
|
// 注册打开应用函数 |
||||
|
registerOpenAppFunction(client) |
||||
|
|
||||
|
// 注册设置提醒函数 |
||||
|
registerSetReminderFunction(client) |
||||
|
|
||||
|
// 注册获取当前时间函数 |
||||
|
registerGetCurrentTimeFunction(client) |
||||
|
|
||||
|
// 注册获取当前位置函数 |
||||
|
registerGetCurrentLocationFunction(client) |
||||
|
|
||||
|
print("\(TAG) 所有系统功能注册完成") |
||||
|
} |
||||
|
|
||||
|
/// 注册退出交互函数 |
||||
|
private func registerExitInteractionFunction(_ client: MCPClient) { |
||||
|
// 定义函数参数 |
||||
|
let parameters: [String: Any] = [ |
||||
|
"type": "object", |
||||
|
"properties": [:], |
||||
|
"required": [] |
||||
|
] |
||||
|
|
||||
|
// 创建函数处理器 |
||||
|
let handler = ExitInteractionHandler() |
||||
|
|
||||
|
// 注册函数 |
||||
|
client.registerLocalFunction( |
||||
|
name: "exit_interaction", |
||||
|
description: "结束当前交互", |
||||
|
parameters: parameters, |
||||
|
handler: handler |
||||
|
) |
||||
|
print("\(TAG) 已注册退出交互功能") |
||||
|
} |
||||
|
|
||||
|
/// 注册翻译模式函数 |
||||
|
private func registerTranslationModeFunction(_ client: MCPClient) { |
||||
|
// 定义函数参数 |
||||
|
let parameters: [String: Any] = [ |
||||
|
"type": "object", |
||||
|
"properties": [:], |
||||
|
"required": [] |
||||
|
] |
||||
|
|
||||
|
// 创建函数处理器 |
||||
|
let handler = TranslationModeHandler() |
||||
|
|
||||
|
// 注册函数 |
||||
|
client.registerLocalFunction( |
||||
|
name: "enter_translation_mode", |
||||
|
description: "用户请求进入实时翻译模式时,启动实时翻译功能", |
||||
|
parameters: parameters, |
||||
|
handler: handler |
||||
|
) |
||||
|
print("\(TAG) 已注册翻译模式功能") |
||||
|
} |
||||
|
|
||||
|
/// 注册发送短信函数 |
||||
|
private func registerSendTextMessageFunction(_ client: MCPClient) { |
||||
|
// 定义函数参数 |
||||
|
let parameters: [String: Any] = [ |
||||
|
"type": "object", |
||||
|
"properties": [ |
||||
|
"contact": [ |
||||
|
"type": "string", |
||||
|
"description": "联系人姓名或电话号码" |
||||
|
], |
||||
|
"message": [ |
||||
|
"type": "string", |
||||
|
"description": "短信内容" |
||||
|
] |
||||
|
], |
||||
|
"required": ["contact", "message"] |
||||
|
] |
||||
|
|
||||
|
// 创建函数处理器 |
||||
|
let handler = SendTextMessageHandler() |
||||
|
|
||||
|
// 注册函数 |
||||
|
client.registerLocalFunction( |
||||
|
name: "send_text_message", |
||||
|
description: "发送短信", |
||||
|
parameters: parameters, |
||||
|
handler: handler |
||||
|
) |
||||
|
print("\(TAG) 已注册发送短信功能") |
||||
|
} |
||||
|
|
||||
|
/// 注册拨打电话函数 |
||||
|
private func registerMakePhoneCallFunction(_ client: MCPClient) { |
||||
|
// 定义函数参数 |
||||
|
let parameters: [String: Any] = [ |
||||
|
"type": "object", |
||||
|
"properties": [ |
||||
|
"contact": [ |
||||
|
"type": "string", |
||||
|
"description": "联系人姓名或电话号码" |
||||
|
] |
||||
|
], |
||||
|
"required": ["contact"] |
||||
|
] |
||||
|
|
||||
|
// 创建函数处理器 |
||||
|
let handler = MakePhoneCallHandler() |
||||
|
|
||||
|
// 注册函数 |
||||
|
client.registerLocalFunction( |
||||
|
name: "make_phone_call", |
||||
|
description: "拨打电话", |
||||
|
parameters: parameters, |
||||
|
handler: handler |
||||
|
) |
||||
|
print("\(TAG) 已注册拨打电话功能") |
||||
|
} |
||||
|
|
||||
|
/// 注册打开应用函数 |
||||
|
private func registerOpenAppFunction(_ client: MCPClient) { |
||||
|
// 定义函数参数 |
||||
|
let parameters: [String: Any] = [ |
||||
|
"type": "object", |
||||
|
"properties": [ |
||||
|
"app_name": [ |
||||
|
"type": "string", |
||||
|
"description": "应用名称" |
||||
|
] |
||||
|
], |
||||
|
"required": ["app_name"] |
||||
|
] |
||||
|
|
||||
|
// 创建函数处理器 |
||||
|
let handler = OpenAppHandler() |
||||
|
|
||||
|
// 注册函数 |
||||
|
client.registerLocalFunction( |
||||
|
name: "open_app", |
||||
|
description: "当用户请求打开特定应用时,启动相应应用", |
||||
|
parameters: parameters, |
||||
|
handler: handler |
||||
|
) |
||||
|
print("\(TAG) 已注册打开应用功能") |
||||
|
} |
||||
|
|
||||
|
/// 注册设置提醒函数 |
||||
|
private func registerSetReminderFunction(_ client: MCPClient) { |
||||
|
// 定义函数参数 |
||||
|
let parameters: [String: Any] = [ |
||||
|
"type": "object", |
||||
|
"properties": [ |
||||
|
"title": [ |
||||
|
"type": "string", |
||||
|
"description": "提醒标题" |
||||
|
], |
||||
|
"content": [ |
||||
|
"type": "string", |
||||
|
"description": "提醒内容" |
||||
|
], |
||||
|
"time": [ |
||||
|
"type": "string", |
||||
|
"description": "提醒时间,格式为'yyyy-MM-dd HH:mm',如'2023-12-31 14:30'" |
||||
|
] |
||||
|
], |
||||
|
"required": ["title", "time"] |
||||
|
] |
||||
|
|
||||
|
// 创建函数处理器 |
||||
|
let handler = SetReminderHandler() |
||||
|
|
||||
|
// 注册函数 |
||||
|
client.registerLocalFunction( |
||||
|
name: "set_reminder", |
||||
|
description: "设置提醒事项", |
||||
|
parameters: parameters, |
||||
|
handler: handler |
||||
|
) |
||||
|
print("\(TAG) 已注册设置提醒功能") |
||||
|
} |
||||
|
|
||||
|
/// 注册获取当前时间函数 |
||||
|
private func registerGetCurrentTimeFunction(_ client: MCPClient) { |
||||
|
// 定义函数参数 |
||||
|
let parameters: [String: Any] = [ |
||||
|
"type": "object", |
||||
|
"properties": [ |
||||
|
"format": [ |
||||
|
"type": "string", |
||||
|
"description": "时间格式,可选,默认为标准格式" |
||||
|
] |
||||
|
], |
||||
|
"required": [] |
||||
|
] |
||||
|
|
||||
|
// 创建函数处理器 |
||||
|
let handler = GetCurrentTimeHandler() |
||||
|
|
||||
|
// 注册函数 |
||||
|
client.registerLocalFunction( |
||||
|
name: "get_current_time", |
||||
|
description: "获取当前日期和时间", |
||||
|
parameters: parameters, |
||||
|
handler: handler |
||||
|
) |
||||
|
print("\(TAG) 已注册获取当前时间功能") |
||||
|
} |
||||
|
|
||||
|
/// 注册获取当前位置函数 |
||||
|
private func registerGetCurrentLocationFunction(_ client: MCPClient) { |
||||
|
// 定义函数参数 |
||||
|
let parameters: [String: Any] = [ |
||||
|
"type": "object", |
||||
|
"properties": [:], |
||||
|
"required": [] |
||||
|
] |
||||
|
|
||||
|
// 创建函数处理器 |
||||
|
let handler = GetCurrentLocationHandler() |
||||
|
|
||||
|
// 注册函数 |
||||
|
client.registerLocalFunction( |
||||
|
name: "get_current_location", |
||||
|
description: "获取当前地理位置", |
||||
|
parameters: parameters, |
||||
|
handler: handler |
||||
|
) |
||||
|
print("\(TAG) 已注册获取当前位置功能") |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 退出交互处理器 |
||||
|
class ExitInteractionHandler: FunctionHandler { |
||||
|
func handle(arguments: [String: Any]) async throws -> String { |
||||
|
// 发送通知和广播 |
||||
|
NotificationCenter.default.post( |
||||
|
name: Notification.Name(SystemFunctionHandler.ACTION_EXIT_INTERACTION), |
||||
|
object: nil, |
||||
|
userInfo: ["timestamp": Date().timeIntervalSince1970] |
||||
|
) |
||||
|
|
||||
|
return "{\"result\": \"已结束当前交互\"}" |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 翻译模式处理器 |
||||
|
class TranslationModeHandler: FunctionHandler { |
||||
|
func handle(arguments: [String: Any]) async throws -> String { |
||||
|
// 发送通知 |
||||
|
NotificationCenter.default.post( |
||||
|
name: Notification.Name("com.yunqiinnovation.deepsound.ACTION_ENTER_TRANSLATION_MODE"), |
||||
|
object: nil, |
||||
|
userInfo: ["timestamp": Date().timeIntervalSince1970] |
||||
|
) |
||||
|
|
||||
|
return "{\"result\": \"已进入实时翻译模式\"}" |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 发送短信处理器 |
||||
|
class SendTextMessageHandler: FunctionHandler { |
||||
|
func handle(arguments: [String: Any]) async throws -> String { |
||||
|
guard let contact = arguments["contact"] as? String, |
||||
|
let message = arguments["message"] as? String else { |
||||
|
return "{\"result\": \"发送短信失败:缺少联系人或消息内容\"}" |
||||
|
} |
||||
|
|
||||
|
// iOS不允许应用直接发送短信,但可以打开短信应用 |
||||
|
if let url = URL(string: "sms:\(contact)&body=\(message.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "")") { |
||||
|
DispatchQueue.main.async { |
||||
|
UIApplication.shared.open(url, options: [:], completionHandler: nil) |
||||
|
} |
||||
|
return "{\"result\": \"已打开短信应用,收件人:\(contact)\"}" |
||||
|
} |
||||
|
|
||||
|
return "{\"result\": \"发送短信失败:无法打开短信应用\"}" |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 拨打电话处理器 |
||||
|
class MakePhoneCallHandler: FunctionHandler { |
||||
|
func handle(arguments: [String: Any]) async throws -> String { |
||||
|
guard let contact = arguments["contact"] as? String else { |
||||
|
return "{\"result\": \"拨打电话失败:缺少联系人\"}" |
||||
|
} |
||||
|
|
||||
|
let phoneNumber = contact.replacingOccurrences(of: "[^+0-9]", with: "", options: .regularExpression) |
||||
|
|
||||
|
if let url = URL(string: "tel://\(phoneNumber)") { |
||||
|
DispatchQueue.main.async { |
||||
|
UIApplication.shared.open(url, options: [:], completionHandler: nil) |
||||
|
} |
||||
|
return "{\"result\": \"正在拨打\(contact)的电话\"}" |
||||
|
} |
||||
|
|
||||
|
return "{\"result\": \"拨打电话失败:无法拨打电话\"}" |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 打开应用处理器 |
||||
|
class OpenAppHandler: FunctionHandler { |
||||
|
func handle(arguments: [String: Any]) async throws -> String { |
||||
|
guard let appName = arguments["app_name"] as? String else { |
||||
|
return "{\"result\": \"打开应用失败:缺少应用名称\"}" |
||||
|
} |
||||
|
|
||||
|
// iOS没有直接打开其他应用的API,只能通过URL Scheme |
||||
|
// 这里只是模拟返回成功,实际上无法在iOS上实现这个功能 |
||||
|
return "{\"result\": \"打开应用功能在iOS上受限,无法直接打开\(appName)\"}" |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 设置提醒处理器 |
||||
|
class SetReminderHandler: FunctionHandler { |
||||
|
func handle(arguments: [String: Any]) async throws -> String { |
||||
|
guard let title = arguments["title"] as? String, |
||||
|
let timeStr = arguments["time"] as? String else { |
||||
|
return "{\"result\": \"设置提醒失败:缺少标题或时间\"}" |
||||
|
} |
||||
|
|
||||
|
let content = arguments["content"] as? String ?? "" |
||||
|
|
||||
|
// 解析时间 |
||||
|
let dateFormatter = DateFormatter() |
||||
|
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm" |
||||
|
guard let date = dateFormatter.date(from: timeStr) else { |
||||
|
return "{\"result\": \"设置提醒失败:时间格式无效,请使用'yyyy-MM-dd HH:mm'格式\"}" |
||||
|
} |
||||
|
|
||||
|
// 使用EventKit添加提醒 |
||||
|
let eventStore = EKEventStore() |
||||
|
let event = EKEvent(eventStore: eventStore) |
||||
|
event.title = title |
||||
|
event.notes = content |
||||
|
event.startDate = date |
||||
|
event.endDate = date.addingTimeInterval(3600) // 1小时后 |
||||
|
event.calendar = eventStore.defaultCalendarForNewEvents |
||||
|
|
||||
|
do { |
||||
|
try eventStore.save(event, span: .thisEvent) |
||||
|
return "{\"result\": \"已创建提醒: \(title), 时间: \(timeStr)\"}" |
||||
|
} catch { |
||||
|
return "{\"result\": \"设置提醒失败:\(error.localizedDescription)\"}" |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 获取当前时间处理器 |
||||
|
class GetCurrentTimeHandler: FunctionHandler { |
||||
|
func handle(arguments: [String: Any]) async throws -> String { |
||||
|
let format = arguments["format"] as? String |
||||
|
|
||||
|
let calendar = Calendar.current |
||||
|
let date = Date() |
||||
|
|
||||
|
let dateFormatter = DateFormatter() |
||||
|
dateFormatter.locale = Locale(identifier: "zh_CN") |
||||
|
|
||||
|
let timeStr: String |
||||
|
switch format?.lowercased() { |
||||
|
case "short": |
||||
|
dateFormatter.dateFormat = "HH:mm" |
||||
|
timeStr = dateFormatter.string(from: date) |
||||
|
case "date": |
||||
|
dateFormatter.dateFormat = "yyyy年MM月dd日" |
||||
|
timeStr = dateFormatter.string(from: date) |
||||
|
case "time": |
||||
|
dateFormatter.dateFormat = "HH:mm:ss" |
||||
|
timeStr = dateFormatter.string(from: date) |
||||
|
case "full": |
||||
|
dateFormatter.dateFormat = "yyyy年MM月dd日 EEEE HH:mm:ss" |
||||
|
timeStr = dateFormatter.string(from: date) |
||||
|
case "iso": |
||||
|
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss" |
||||
|
timeStr = dateFormatter.string(from: date) |
||||
|
default: |
||||
|
dateFormatter.dateFormat = "yyyy年MM月dd日 HH:mm" |
||||
|
timeStr = dateFormatter.string(from: date) |
||||
|
} |
||||
|
|
||||
|
// 获取星期几 |
||||
|
let weekDay: String |
||||
|
if format == "full" { |
||||
|
weekDay = "" // 已经包含在dateFormatter里 |
||||
|
} else { |
||||
|
let weekDayNum = calendar.component(.weekday, from: date) |
||||
|
switch weekDayNum { |
||||
|
case 1: weekDay = "星期日" |
||||
|
case 2: weekDay = "星期一" |
||||
|
case 3: weekDay = "星期二" |
||||
|
case 4: weekDay = "星期三" |
||||
|
case 5: weekDay = "星期四" |
||||
|
case 6: weekDay = "星期五" |
||||
|
case 7: weekDay = "星期六" |
||||
|
default: weekDay = "" |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 格式化结果 |
||||
|
let resultTime: String |
||||
|
if format == "short" || format == "time" { |
||||
|
resultTime = timeStr |
||||
|
} else if format == "full" { |
||||
|
resultTime = timeStr // already contains weekday |
||||
|
} else { |
||||
|
resultTime = "\(timeStr) \(weekDay)" |
||||
|
} |
||||
|
|
||||
|
return "{\"result\": \"\(resultTime)\", \"time\": \"\(resultTime)\"}" |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 获取当前位置处理器 |
||||
|
class GetCurrentLocationHandler: FunctionHandler { |
||||
|
private let locationManager = CLLocationManager() |
||||
|
private var locationCompletionHandler: ((String, Bool) -> Void)? |
||||
|
|
||||
|
func handle(arguments: [String: Any]) async throws -> String { |
||||
|
return await withCheckedContinuation { continuation in |
||||
|
self.getLocation { message, success in |
||||
|
continuation.resume(returning: "{\"result\": \"\(message)\", \"success\": \(success)}") |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private func getLocation(completion: @escaping (String, Bool) -> Void) { |
||||
|
locationCompletionHandler = completion |
||||
|
|
||||
|
// 检查权限 |
||||
|
let authStatus = CLLocationManager.authorizationStatus() |
||||
|
if authStatus == .denied || authStatus == .restricted { |
||||
|
completion("需要位置权限才能获取当前位置", false) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
// 检查位置服务是否开启 |
||||
|
if !CLLocationManager.locationServicesEnabled() { |
||||
|
completion("位置服务未启用,请打开GPS或网络定位", false) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
// 如果有最后的位置,使用它 |
||||
|
if let lastLocation = locationManager.location { |
||||
|
let message = "当前位置: 经度 \(lastLocation.coordinate.longitude), 纬度 \(lastLocation.coordinate.latitude)" |
||||
|
completion(message, true) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
// 否则请求更新 |
||||
|
locationManager.delegate = self |
||||
|
locationManager.desiredAccuracy = kCLLocationAccuracyBest |
||||
|
locationManager.requestWhenInUseAuthorization() |
||||
|
locationManager.startUpdatingLocation() |
||||
|
|
||||
|
// 设置超时 |
||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 10) { [weak self] in |
||||
|
if let handler = self?.locationCompletionHandler { |
||||
|
self?.locationManager.stopUpdatingLocation() |
||||
|
handler("获取位置超时,请稍后重试", false) |
||||
|
self?.locationCompletionHandler = nil |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 扩展以接收位置更新 |
||||
|
extension GetCurrentLocationHandler: CLLocationManagerDelegate { |
||||
|
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { |
||||
|
guard let location = locations.last, let handler = locationCompletionHandler else { return } |
||||
|
|
||||
|
manager.stopUpdatingLocation() |
||||
|
|
||||
|
let message = "当前位置: 经度 \(location.coordinate.longitude), 纬度 \(location.coordinate.latitude)" |
||||
|
handler(message, true) |
||||
|
locationCompletionHandler = nil |
||||
|
} |
||||
|
|
||||
|
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) { |
||||
|
manager.stopUpdatingLocation() |
||||
|
locationCompletionHandler?("获取位置失败: \(error.localizedDescription)", false) |
||||
|
locationCompletionHandler = nil |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue