Browse Source

Merge commit 'f956c31897bacc3cddb78d388226301679a4fb41' into lxm

weicu
lxm 1 year ago
parent
commit
cee2ec2639
  1. 1
      android/app/src/main/kotlin/com/yunqiinnovation/deepsound/MainActivity.kt
  2. 9
      lib/data/services/ble_manager.dart
  3. 11
      lib/modules/pairing/controllers/pairing_controller.dart
  4. 13
      local_plugins/agent_service/android/build.gradle.kts
  5. 37
      local_plugins/agent_service/android/src/main/AndroidManifest.xml
  6. 3
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt
  7. 47
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt
  8. 9
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleAgent.kt
  9. 85
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleMediaService.kt
  10. 18
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleWakeReceiver.kt
  11. 2
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BootReceiver.kt
  12. 2
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/utils.kt
  13. 994
      local_plugins/agent_service/ios/Classes/AgentService.swift
  14. 11
      local_plugins/agent_service/ios/Classes/AgentServiceListener.swift
  15. 4
      local_plugins/agent_service/ios/Classes/AgentServicePlugin.h
  16. 12
      local_plugins/agent_service/ios/Classes/AgentServicePlugin.m
  17. 259
      local_plugins/agent_service/ios/Classes/SwiftAgentServicePlugin.swift
  18. 36
      local_plugins/agent_service/lib/agent_service.dart
  19. 873
      local_plugins/azure_speech/ios/Classes/AzureAsrHelper.swift
  20. 454
      local_plugins/azure_speech/ios/Classes/AzureTtsHelper.swift
  21. 6
      local_plugins/ble_service/android/build.gradle.kts
  22. 36
      local_plugins/ble_service/android/src/main/AndroidManifest.xml
  23. 72
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt
  24. 87
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleServicePlugin.kt
  25. 15
      local_plugins/ble_service/ios/Assets/ble_service-Info.plist
  26. 90
      local_plugins/ble_service/ios/Classes/BleConst.swift
  27. 25
      local_plugins/ble_service/ios/Classes/BleDelegate.swift
  28. 963
      local_plugins/ble_service/ios/Classes/BleService.swift
  29. 4
      local_plugins/ble_service/ios/Classes/BleServicePlugin.h
  30. 12
      local_plugins/ble_service/ios/Classes/BleServicePlugin.m
  31. 203
      local_plugins/ble_service/ios/Classes/SwiftBleServicePlugin.swift
  32. 23
      local_plugins/ble_service/ios/ble_service.podspec
  33. 410
      local_plugins/chat_storage/ios/Classes/ChatStorageHelper.swift
  34. 4
      local_plugins/chat_storage/ios/Classes/ChatStoragePlugin.h
  35. 12
      local_plugins/chat_storage/ios/Classes/ChatStoragePlugin.m
  36. 182
      local_plugins/chat_storage/ios/Classes/ChatStoragePlugin.swift
  37. 11
      local_plugins/chat_storage/ios/Classes/SwiftChatStoragePlugin.swift
  38. 64
      local_plugins/chat_storage/ios/README.md
  39. 11
      local_plugins/chat_storage/ios/chat_storage.podspec
  40. 197
      local_plugins/classic_bluetooth/ios/Classes/ClassicBluetoothHelper.swift
  41. 4
      local_plugins/classic_bluetooth/ios/Classes/ClassicBluetoothPlugin.h
  42. 12
      local_plugins/classic_bluetooth/ios/Classes/ClassicBluetoothPlugin.m
  43. 108
      local_plugins/classic_bluetooth/ios/Classes/ClassicBluetoothPlugin.swift
  44. 136
      local_plugins/classic_bluetooth/ios/Classes/FileLogger.swift
  45. 11
      local_plugins/classic_bluetooth/ios/Classes/SwiftClassicBluetoothPlugin.swift
  46. 44
      local_plugins/classic_bluetooth/ios/README.md
  47. 22
      local_plugins/classic_bluetooth/ios/classic_bluetooth.podspec
  48. 2
      local_plugins/classic_bluetooth/pubspec.yaml
  49. 4
      local_plugins/deepvoice_utils/ios/Classes/DeepvoiceUtilsPlugin.h
  50. 12
      local_plugins/deepvoice_utils/ios/Classes/DeepvoiceUtilsPlugin.m
  51. 30
      local_plugins/deepvoice_utils/ios/Classes/DeepvoiceUtilsPlugin.swift
  52. 286
      local_plugins/deepvoice_utils/ios/Classes/FileLogger.swift
  53. 11
      local_plugins/deepvoice_utils/ios/Classes/SwiftDeepvoiceUtilsPlugin.swift
  54. 54
      local_plugins/deepvoice_utils/ios/README.md
  55. 19
      local_plugins/deepvoice_utils/ios/deepvoice_utils.podspec
  56. 249
      local_plugins/open_ai_service/ios/Classes/MCPClient.swift
  57. 411
      local_plugins/open_ai_service/ios/Classes/OpenAIService.swift
  58. 146
      local_plugins/open_ai_service/ios/Classes/OpenAIServicePlugin.swift
  59. 514
      local_plugins/open_ai_service/ios/Classes/SystemFunctionHandler.swift

1
android/app/src/main/kotlin/com/yunqiinnovation/deepsound/MainActivity.kt

@ -13,7 +13,6 @@ import android.content.Context
import android.os.Handler
import android.os.Looper
import com.yunqiinnovation.deepvoice_utils.FileLogger
import com.yunqiinnovation.ble_service.BleMediaService
class MainActivity: FlutterActivity() {
private val TAG = "MainActivity"

9
lib/data/services/ble_manager.dart

@ -8,6 +8,7 @@ import 'package:get_storage/get_storage.dart';
import 'package:deep_voice/data/models/appconfig_model.dart';
import 'dart:convert';
import 'package:flutter/services.dart' show rootBundle;
import 'package:agent_service/agent_service.dart';
/// 命令类型常量(与原生层保持一致)
class BleCommands {
@ -381,7 +382,7 @@ class BleManager extends GetxService {
}
/// 启动BLE服务
Future<bool> startBleService() async {
Future<bool> startAgentService() async {
try {
// 从.env文件直接读取环境变量
// final azureSpeechKey = dotenv.env['AZURE_SPEECH_KEY'] ?? '';
@ -431,7 +432,7 @@ class BleManager extends GetxService {
}
}
final result = await _bleService.startBleService(config);
final result = await AgentService.startAgentService(config);
if (result) {
Logger.info('BLE服务启动成功');
@ -463,9 +464,9 @@ class BleManager extends GetxService {
}
/// 停止BLE服务
Future<bool> stopBleService() async {
Future<bool> stopAgentService() async {
try {
final result = await _bleService.stopBleService();
final result = await AgentService.stopAgentService();
if (result) {
Logger.info('BLE服务停止成功');

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

@ -8,6 +8,7 @@ import 'dart:io';
import '../../../data/services/bluetooth_manager.dart';
import 'package:classic_bluetooth/classic_bluetooth.dart' show BluetoothDevice;
import '../../../data/services/ble_manager.dart';
import 'package:agent_service/agent_service.dart';
/// 配对控制器,负责检查蓝牙权限、状态,并处理配对流程
class PairingController extends GetxController {
static PairingController get to => Get.find();
@ -87,7 +88,7 @@ class PairingController extends GetxController {
statusDescription.value = 'searchingDevice'.tr;
// 设备连接时启动BLE后台服务
_startBleService();
_startAgentService();
await refreshConnectedDevices();
} catch (e) {
@ -241,7 +242,7 @@ class PairingController extends GetxController {
}
/// 启动BLE后台服务
Future<void> _startBleService() async {
Future<void> _startAgentService() async {
try {
// if (!isDeviceConnected) return;
@ -256,7 +257,7 @@ class PairingController extends GetxController {
}
// 启动BLE服务,无需传递配置参数,将从.env读取
final result = await BleManager.to.startBleService();
final result = await BleManager.to.startAgentService();
if (result) {
Logger.info('BLE后台服务启动成功');
@ -269,9 +270,9 @@ class PairingController extends GetxController {
}
/// 停止BLE后台服务
Future<void> _stopBleService() async {
Future<void> _stopAgentService() async {
try {
final result = await BleManager.to.stopBleService();
final result = await BleManager.to.stopAgentService();
if (result) {
Logger.info('BLE后台服务停止成功');

13
local_plugins/agent_service/android/build.gradle.kts

@ -34,13 +34,18 @@ android {
}
dependencies {
add("implementation", "org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.6.10")
add("implementation", "androidx.core:core-ktx:1.8.0")
add("implementation", "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4")
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.6.10")
implementation("androidx.core:core-ktx:1.8.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.0")
implementation("androidx.work:work-runtime-ktx:2.8.1")
implementation("androidx.media:media:1.6.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
implementation("androidx.security:security-crypto:1.1.0-alpha03")
// 依赖本地其他插件
add("compileOnly", project(":azure_speech"))
add("compileOnly", project(":open_ai_service"))
add("compileOnly", project(":deepvoice_utils"))
add("compileOnly", project(":chat_storage"))
add("compileOnly", project(":ble_service"))
}

37
local_plugins/agent_service/android/src/main/AndroidManifest.xml

@ -2,4 +2,41 @@
package="com.yunqiinnovation.agent_service">
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<application>
<!-- 媒体前台服务 -->
<service
android:name=".BleMediaService"
android:exported="true"
android:enabled="true"
android:foregroundServiceType="mediaPlayback">
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter>
</service>
<!-- 媒体按钮 Receiver -->
<receiver
android:name="androidx.media.session.MediaButtonReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter>
</receiver>
<!-- 被系统 PendingIntent 触发的 Receiver -->
<receiver
android:name=".BleWakeReceiver"
android:exported="true"
android:enabled="true">
<intent-filter>
<action android:name="com.yunqiinnovation.ble_service.ACTION_BLE_SCAN" />
</intent-filter>
</receiver>
</application>
</manifest>

3
local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt

@ -12,7 +12,7 @@ import io.flutter.plugin.common.MethodChannel
import kotlinx.coroutines.*
import java.util.Collections
import kotlin.coroutines.CoroutineContext
import com.yunqiinnovation.ble_service.BleService
/**
* 代理服务事件监听接口
*/
@ -401,6 +401,7 @@ object AgentService : CoroutineScope {
FileLogger.d(TAG, "停止语音识别")
try {
BleService.closeCodec()
azureAsrHelper?.stopContinuousRecognition()
isRecognitionActive = false
stopIdleCheck()

47
local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt

@ -12,6 +12,8 @@ import com.yunqiinnovation.deepvoice_utils.FileLogger
import org.json.JSONObject
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
import android.content.Intent
import android.os.Build
class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.StreamHandler, AgentServiceListener {
private val TAG = "AgentServicePlugin"
@ -36,7 +38,50 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream
override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
when (call.method) {
"startAgentService" -> {
try {
val config = call.argument<Map<String, Any>>("config")
if (config == null) {
result.error("INVALID_ARGUMENTS", "配置参数不能为空", null)
return
}
FileLogger.d(TAG, "启动BleMediaService,配置参数: $config")
// 创建Intent启动BleMediaService
val intent = Intent(context, BleMediaService::class.java)
// 将参数添加到Intent
config["azureSpeechKey"]?.toString()?.let { intent.putExtra("azureSpeechKey", it) }
config["azureSpeechRegion"]?.toString()?.let { intent.putExtra("azureSpeechRegion", it) }
config["openaiApiKey"]?.toString()?.let { intent.putExtra("openaiApiKey", it) }
config["openaiBaseUrl"]?.toString()?.let { intent.putExtra("openaiBaseUrl", it) }
config["openaiModel"]?.toString()?.let { intent.putExtra("openaiModel", it) }
config["mcpServer"]?.toString()?.let { intent.putExtra("mcpServer", it) }
// 启动前台服务
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(intent)
} else {
context.startService(intent)
}
result.success(true)
} catch (e: Exception) {
FileLogger.e(TAG, "启动AgentService失败", e)
result.error("SERVICE_START_ERROR", "启动AgentService失败: ${e.message}", null)
}
}
"stopAgentService" -> {
try {
val intent = Intent(context, BleMediaService::class.java)
context.stopService(intent)
result.success(true)
} catch (e: Exception) {
FileLogger.e(TAG, "停止AgentService失败", e)
result.error("SERVICE_STOP_ERROR", "停止AgentService失败: ${e.message}", null)
}
}
"startConversation" -> {
try {
val isStarted = AgentService.startRecognition(false)

9
local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/ChatAgent.kt → local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleAgent.kt

@ -1,17 +1,18 @@
package com.yunqiinnovation.ble_service
package com.yunqiinnovation.agent_service
import android.content.Context
import com.yunqiinnovation.agent_service.AgentService
import com.yunqiinnovation.agent_service.AgentServiceListener
import com.yunqiinnovation.deepvoice_utils.FileLogger
import com.yunqiinnovation.ble_service.BleService
import com.yunqiinnovation.ble_service.BleConst
/**
* 蓝牙语音交互代理
* 连接BleService和AgentService,响应蓝牙设备唤醒信号,启动语音识别和交互
*/
object ChatAgent : BleService.Callback, AgentServiceListener {
private const val TAG = "ChatAgent"
object BleAgent : BleService.Callback, AgentServiceListener {
private const val TAG = "BleAgent"
// 是否已初始化
private var isInitialized = false

85
local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleMediaService.kt → local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleMediaService.kt

@ -1,10 +1,13 @@
package com.yunqiinnovation.ble_service
package com.yunqiinnovation.agent_service
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.bluetooth.BluetoothManager
import android.bluetooth.le.ScanFilter
import android.bluetooth.le.ScanSettings
import android.content.ComponentName
import android.content.Context
import android.content.Intent
@ -28,6 +31,8 @@ import android.view.KeyEvent
import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import com.yunqiinnovation.ble_service.BleConst
import com.yunqiinnovation.ble_service.BleService
/**
* 语音助手媒体服务
* 职责:
@ -149,18 +154,7 @@ class BleMediaService : Service(), CoroutineScope {
return START_STICKY
}
/**
* 注册后台蓝牙扫描
*/
private fun registerBackgroundScan() {
try {
BleService.registerBackgroundScan()
FileLogger.i(TAG, "后台蓝牙扫描已注册")
} catch (e: Exception) {
FileLogger.e(TAG, "注册后台蓝牙扫描失败", e)
}
}
/**
* 初始化语音交互代理
*/
@ -194,7 +188,7 @@ class BleMediaService : Service(), CoroutineScope {
if(config.isNotEmpty()) {
// 初始化ChatAgent
FileLogger.i(TAG, "开始初始化ChatAgent")
val success = ChatAgent.initialize(applicationContext, config)
val success = BleAgent.initialize(applicationContext, config)
if (success) {
isChatAgentInitialized = true
FileLogger.i(TAG, "语音交互代理初始化成功")
@ -364,7 +358,7 @@ class BleMediaService : Service(), CoroutineScope {
val currentTime = System.currentTimeMillis()
if (currentTime - lastActionTime >= ACTION_DEBOUNCE_MS) {
lastActionTime = currentTime
ChatAgent.onWakeupSignalReceived()
BleAgent.onWakeupSignalReceived()
} else {
FileLogger.i(TAG, "媒体操作防抖: 忽略${ACTION_DEBOUNCE_MS}ms内的重复请求")
}
@ -386,7 +380,66 @@ class BleMediaService : Service(), CoroutineScope {
FileLogger.e(TAG, "连接设备失败: ${e.message}", e)
}
}
// ======================================================================================================
// 后台扫描与唤醒
// ======================================================================================================
/**
* 注册后台持久扫描,APP被杀死后也能唤醒
*/
fun registerBackgroundScan() {
val scanner = (getSystemService(Context.BLUETOOTH_SERVICE) as? BluetoothManager)?.adapter?.bluetoothLeScanner ?: return
try {
val filter = ScanFilter.Builder()
.setManufacturerData(0xEFAB, byteArrayOf(0xA0.toByte(), 0x01))
.build()
val settings = ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_POWER)
.setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES)
.build()
// 创建Intent并设置前台接收器标志
val scanIntent = Intent(BleConst.ACTION_SCAN_RESULT)
.setClass(this, BleWakeReceiver::class.java)
.addFlags(Intent.FLAG_RECEIVER_FOREGROUND)
val pi = PendingIntent.getBroadcast(
this, 0, scanIntent,
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
} else {
PendingIntent.FLAG_UPDATE_CURRENT
}
)
scanner.startScan(listOf(filter), settings, pi)
FileLogger.i(TAG, "后台持久扫描已注册")
} catch (e: Exception) {
FileLogger.e(TAG, "注册后台扫描失败: ${e.message}", e)
}
}
/**
* 注销后台持久扫描
*/
fun unregisterBackgroundScan() {
// 获取一个局部引用,避免并发访问问题
val scanner = (getSystemService(Context.BLUETOOTH_SERVICE) as? BluetoothManager)?.adapter?.bluetoothLeScanner ?: return
try {
// 创建Intent并设置前台接收器标志
val scanIntent = Intent(BleConst.ACTION_SCAN_RESULT)
.setClass(this, BleWakeReceiver::class.java)
.addFlags(Intent.FLAG_RECEIVER_FOREGROUND)
val pi = PendingIntent.getBroadcast(
this, 0, scanIntent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
scanner.stopScan(pi)
FileLogger.i(TAG, "后台持久扫描已注销")
} catch (e: Exception) {
FileLogger.e(TAG, "注销后台扫描失败: ${e.message}", e)
}
}
/**
* 更新通知内容
*/

18
local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleWakeReceiver.kt → local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleWakeReceiver.kt

@ -1,4 +1,4 @@
package com.yunqiinnovation.ble_service
package com.yunqiinnovation.agent_service
import android.content.*
import android.bluetooth.le.*
@ -7,6 +7,7 @@ import android.view.KeyEvent
import android.media.AudioManager
import com.yunqiinnovation.deepvoice_utils.FileLogger
import com.yunqiinnovation.ble_service.BleService
import com.yunqiinnovation.ble_service.BleConst
/**
* BLE唤醒接收器
* 当耳机发送含特定UUID广播时,系统会触发此接收器
@ -18,6 +19,7 @@ class BleWakeReceiver : BroadcastReceiver() {
}
override fun onReceive(ctx: Context, intent: Intent) {
FileLogger.d(TAG, "onReceive, intent: $intent, ${BleConst.ACTION_SCAN_RESULT}")
FileLogger.init(ctx)
try {
@ -26,15 +28,21 @@ class BleWakeReceiver : BroadcastReceiver() {
}
val list = intent.getParcelableArrayListExtra<ScanResult>(
BluetoothLeScanner.EXTRA_LIST_SCAN_RESULT
) ?: return
)
// FileLogger.d(TAG, "list: $list")
if (list.isEmpty()) {
if (list?.isEmpty() == true || list == null) {
FileLogger.d(TAG, "扫描结果为空")
return
}
val mac = list.first().device.address
// FileLogger.d(TAG, "被系统唤醒,发现设备 $mac")
val mac = list.firstOrNull()?.device?.address
if (mac == null) {
FileLogger.d(TAG, "没有获取到设备MAC地址")
return
}
FileLogger.d(TAG, "被系统唤醒,发现设备 $mac")
if(!BleMediaService.isServiceRunning()) {
val am = ctx.getSystemService(Context.AUDIO_SERVICE) as AudioManager

2
local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BootReceiver.kt → local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BootReceiver.kt

@ -1,4 +1,4 @@
package com.yunqiinnovation.ble_service
package com.yunqiinnovation.agent_service
import android.content.*
import android.os.Build

2
local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/utils.kt → local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/utils.kt

@ -1,4 +1,4 @@
package com.yunqiinnovation.ble_service
package com.yunqiinnovation.agent_service
import android.content.Context
import androidx.security.crypto.EncryptedSharedPreferences

994
local_plugins/agent_service/ios/Classes/AgentService.swift

@ -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)
}
}

11
local_plugins/agent_service/ios/Classes/AgentServiceListener.swift

@ -0,0 +1,11 @@
import Foundation
/// 代理服务事件监听器协议
/// 用于接收代理服务发送的各类事件
protocol AgentServiceListener: AnyObject {
/// 事件回调
/// - Parameters:
/// - eventName: 事件名称
/// - data: 事件数据
func onEvent(eventName: String, data: [String: Any])
}

4
local_plugins/agent_service/ios/Classes/AgentServicePlugin.h

@ -0,0 +1,4 @@
#import <Flutter/Flutter.h>
@interface AgentServicePlugin : NSObject<FlutterPlugin>
@end

12
local_plugins/agent_service/ios/Classes/AgentServicePlugin.m

@ -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

259
local_plugins/agent_service/ios/Classes/SwiftAgentServicePlugin.swift

@ -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)")
}
}
}

36
local_plugins/agent_service/lib/agent_service.dart

@ -189,6 +189,42 @@ class AgentService {
}
}
/// 启动语音助手服务
///
/// 启动常驻的语音助手服务,支持后台蓝牙设备唤醒和媒体按钮唤醒
/// [config] 配置参数,应包含以下参数:
/// - azureSpeechKey: Azure语音服务密钥
/// - azureSpeechRegion: Azure语音服务区域
/// - openaiApiKey: OpenAI API密钥
/// - openaiBaseUrl: (可选) OpenAI API 基础URL
/// - openaiModel: (可选) OpenAI模型名称,默认为"gpt-3.5-turbo"
/// - mcpServer: (可选) MCP服务器地址
///
/// 返回是否成功启动服务
static Future<bool> startAgentService(Map<String, dynamic> config) async {
try {
final bool result = await _channel.invokeMethod('startAgentService', {
'config': config,
});
return result;
} on PlatformException catch (e) {
throw AgentServiceException(e.code, e.message ?? '启动语音助手服务失败', e.details);
}
}
/// 停止语音助手服务
///
/// 停止常驻的语音助手服务
/// 返回是否成功停止服务
static Future<bool> stopAgentService() async {
try {
final bool result = await _channel.invokeMethod('stopAgentService');
return result;
} on PlatformException catch (e) {
throw AgentServiceException(e.code, e.message ?? '停止语音助手服务失败', e.details);
}
}
/// 开始对话
///
/// 启动语音识别,开始监听用户语音输入

873
local_plugins/azure_speech/ios/Classes/AzureAsrHelper.swift

File diff suppressed because it is too large

454
local_plugins/azure_speech/ios/Classes/AzureTtsHelper.swift

@ -4,166 +4,234 @@ import MicrosoftCognitiveServicesSpeech
/// Azure 语音合成辅助类
class AzureTtsHelper: NSObject {
// 常量定义
private let tag = "AzureTtsHelper"
private let DEFAULT_LANGUAGE = "zh-CN"
private let DEFAULT_VOICE = "zh-CN-XiaoxiaoNeural"
// 核心组件
private var synthesizer: SPXSpeechSynthesizer?
private var speechConfig: SPXSpeechConfig?
private var audioConfig: SPXAudioConfig?
private var speechConfig: SPXSpeechConfiguration?
private var audioConfig: SPXAudioConfiguration?
private var initialized = false
private var speaking = false
// 音频输出类型
enum AudioOutputType {
case speaker // 扬声器
case earpiece // 听筒
case auto // 自动选择
}
// 当前设置
private var currentVoiceName = "zh-CN-XiaoxiaoNeural"
private var currentSpeechRate = 0
private var currentPitch = 0
private var currentVolume = 100
private var currentAudioOutputType: AudioOutputType = .auto
private var currentVoice = "zh-CN-XiaoxiaoNeural"
private var currentRate = "0%"
private var currentPitch = "0%"
private var currentVolume = "100%"
// 音频会话管理
private let audioSession = AVAudioSession.sharedInstance()
// 自定义音频输出流
private var customAudioOutputStream: SPXPushAudioOutputStream?
// 流式文本处理的缓冲区
private var streamBuffer = ""
private var lastSpeakTime: TimeInterval = 0
// 应用上下文
private let context: Any
// 全局回调
private var ttsCallback: TtsCallback?
/// TTS回调接口
protocol TtsCallback {
/// 合成开始
func onSynthesisStarted()
/// 合成中
func onSynthesizing()
/// 合成完成
func onSynthesisCompleted()
/// 合成取消
func onSynthesisCanceled()
}
/// 初始化
/// - Parameter context: 应用上下文
init(_ context: Any) {
self.context = context
super.init()
}
/// 初始化 TTS 引擎
///
/// - Parameters:
/// - speechSubscriptionKey: Azure 语音服务订阅密钥
/// - serviceRegion: Azure 语音服务区域
/// - language: 可选,默认语言,默认为 "zh-CN"
/// - subscriptionKey: Azure 语音服务订阅密钥
/// - region: Azure 语音服务区域
/// - callback: 可选,全局TTS回调接口
/// - Returns: 是否初始化成功
func initialize(speechSubscriptionKey: String, serviceRegion: String, language: String = "zh-CN") -> Bool {
print("[AzureTtsHelper] 初始化 Azure 语音服务")
func initialize(
subscriptionKey: String,
region: String,
callback: TtsCallback? = nil
) -> Bool {
print("\(tag): 初始化 Azure 语音服务")
// 检查配置是否为空
if speechSubscriptionKey.isEmpty || serviceRegion.isEmpty {
print("[AzureTtsHelper] 错误: Azure 配置信息不完整")
if subscriptionKey.isEmpty || region.isEmpty {
print("\(tag): 错误: Azure 配置信息不完整")
return false
}
// 释放之前的资源
dispose()
// 保存全局回调
ttsCallback = callback
do {
// 创建语音配置
speechConfig = try SPXSpeechConfig(subscription: speechSubscriptionKey, region: serviceRegion)
speechConfig = try SPXSpeechConfiguration(subscription: subscriptionKey, region: region)
// 设置语音合成输出格式为高质量音频
speechConfig?.setSpeechSynthesisOutputFormat(.riff24Khz16BitMonoPcm)
// 设置默认语言
speechConfig?.setSpeechSynthesisLanguage(language)
// 设置默认语音
speechConfig?.setSpeechSynthesisVoiceName(currentVoiceName)
speechConfig?.setSpeechSynthesisVoiceName(currentVoice)
// 创建音频配置 - 使用默认扬声器
audioConfig = SPXAudioConfig.default()
// 创建音频配置
if let customOutput = customAudioOutputStream {
// 使用自定义音频输出流
audioConfig = try SPXAudioConfiguration(streamOutput: customOutput)
} else {
// 使用默认扬声器
audioConfig = SPXAudioConfiguration()
}
// 创建语音合成器
synthesizer = try SPXSpeechSynthesizer(speechConfig: speechConfig!, audioConfig: audioConfig!)
initialized = true
synthesizer = try SPXSpeechSynthesizer(speechConfiguration: speechConfig!, audioConfiguration: audioConfig!)
// 设置默认音频输出类型为自动
setAudioOutputType(outputType: .auto)
// 设置事件监听
setupEventListeners()
print("[AzureTtsHelper] TTS 引擎初始化成功")
initialized = true
print("\(tag): TTS 引擎初始化成功")
return true
} catch {
print("[AzureTtsHelper] TTS 引擎初始化失败: \(error.localizedDescription)")
print("\(tag): TTS 引擎初始化失败: \(error.localizedDescription)")
return false
}
}
/// 设置音频输出设备类型
///
/// - Parameter outputType: 音频输出设备类型
/// - Returns: 是否设置成功
func setAudioOutputType(outputType: AudioOutputType) -> Bool {
if !initialized {
print("[AzureTtsHelper] TTS 引擎尚未初始化")
return false
/// 设置事件监听器
private func setupEventListeners() {
guard let synthesizer = synthesizer else { return }
// 合成开始事件
synthesizer.addSynthesisStarted { [weak self] _, _ in
guard let self = self else { return }
self.ttsCallback?.onSynthesisStarted()
}
// 合成中事件
synthesizer.addSynthesizing { [weak self] _, _ in
guard let self = self else { return }
self.ttsCallback?.onSynthesizing()
}
// 合成完成事件
synthesizer.addSynthesisCompleted { [weak self] _, _ in
guard let self = self else { return }
self.speaking = false
self.ttsCallback?.onSynthesisCompleted()
}
// 合成取消事件
synthesizer.addSynthesisCanceled { [weak self] _, _ in
guard let self = self else { return }
print("\(self.tag): 语音合成取消")
self.speaking = false
self.ttsCallback?.onSynthesisCanceled()
}
}
/// 设置TTS回调
/// - Parameter callback: TTS回调接口
func setTtsCallback(_ callback: TtsCallback?) {
ttsCallback = callback
}
/// 设置自定义音频输出流
/// - Parameter outputStream: 自定义音频输出流,如果为nil则使用默认音频输出
/// - Returns: 是否设置成功
func setCustomAudioOutputStream(_ outputStream: SPXPushAudioOutputStream?) -> Bool {
do {
currentAudioOutputType = outputType
switch outputType {
case .speaker:
// 使用扬声器
try audioSession.setCategory(.playback, mode: .default)
try audioSession.overrideOutputAudioPort(.speaker)
print("[AzureTtsHelper] 已设置音频输出设备为扬声器")
case .earpiece:
// 使用听筒
try audioSession.setCategory(.playback, mode: .voiceChat)
try audioSession.overrideOutputAudioPort(.none)
print("[AzureTtsHelper] 已设置音频输出设备为听筒")
case .auto:
// 检查是否有耳机连接
let outputs = audioSession.currentRoute.outputs
let hasHeadphones = outputs.contains { output in
return output.portType == .headphones || output.portType == .bluetoothA2DP || output.portType == .bluetoothHFP
}
if hasHeadphones {
// 有耳机,使用耳机
try audioSession.setCategory(.playback, mode: .default)
try audioSession.overrideOutputAudioPort(.none)
print("[AzureTtsHelper] 已设置音频输出设备为耳机")
} else {
// 无耳机,使用听筒
try audioSession.setCategory(.playback, mode: .voiceChat)
try audioSession.overrideOutputAudioPort(.none)
print("[AzureTtsHelper] 已设置音频输出设备为听筒")
}
// 保存引用
customAudioOutputStream = outputStream
// 如果已初始化,需要重新创建合成器以应用新的音频输出流
if initialized {
recreateSynthesizer()
}
try audioSession.setActive(true)
return true
} catch {
print("[AzureTtsHelper] 设置音频输出设备失败: \(error.localizedDescription)")
print("\(tag): 设置自定义音频输出流失败: \(error.localizedDescription)")
return false
}
}
/// 重新创建合成器
private func recreateSynthesizer() {
do {
// 关闭现有合成器
synthesizer = nil
// 创建音频配置
if let customOutput = customAudioOutputStream {
// 使用自定义音频输出流
audioConfig = try SPXAudioConfiguration(streamOutput: customOutput)
} else {
// 使用默认扬声器
audioConfig = SPXAudioConfiguration()
}
// 使用新的音频配置创建合成器
synthesizer = try SPXSpeechSynthesizer(speechConfiguration: speechConfig!, audioConfiguration: audioConfig!)
// 重新设置事件监听
setupEventListeners()
} catch {
print("\(tag): 重新创建合成器失败: \(error.localizedDescription)")
}
}
/// 设置语音
///
/// - Parameter voiceName: 语音名称,例如 "zh-CN-XiaoxiaoNeural"
/// - Returns: 是否设置成功
func setVoice(voiceName: String) -> Bool {
if !initialized {
print("[AzureTtsHelper] TTS 引擎尚未初始化")
print("\(tag): TTS 引擎尚未初始化")
return false
}
if voiceName == currentVoiceName {
print("[AzureTtsHelper] 已设置语音: \(voiceName)")
if voiceName == currentVoice {
print("\(tag): 已设置语音: \(voiceName)")
return true
}
do {
currentVoiceName = voiceName
currentVoice = voiceName
speechConfig?.setSpeechSynthesisVoiceName(voiceName)
// 重新创建合成器
synthesizer = try SPXSpeechSynthesizer(speechConfig: speechConfig!, audioConfig: audioConfig!)
recreateSynthesizer()
print("[AzureTtsHelper] 已设置语音: \(voiceName)")
print("\(tag): 已设置语音: \(voiceName)")
return true
} catch {
print("[AzureTtsHelper] 设置语音失败: \(error.localizedDescription)")
print("\(tag): 设置语音失败: \(error.localizedDescription)")
return false
}
}
/// 设置语音合成参数
///
/// - Parameters:
/// - rate: 语速,范围 -100 到 100,默认为 0
/// - pitch: 音调,范围 -100 到 100,默认为 0
@ -171,58 +239,36 @@ class AzureTtsHelper: NSObject {
/// - Returns: 是否设置成功
func setSpeechParams(rate: Int = 0, pitch: Int = 0, volume: Int = 100) -> Bool {
if !initialized {
print("[AzureTtsHelper] TTS 引擎尚未初始化")
print("\(tag): TTS 引擎尚未初始化")
return false
}
currentSpeechRate = rate
currentPitch = pitch
currentVolume = volume
print("[AzureTtsHelper] 已设置语音参数: 语速=\(rate), 音调=\(pitch), 音量=\(volume)")
return true
}
/// 合成文本为语音并播放
///
/// - Parameters:
/// - text: 要合成的文本
/// - completion: 完成回调,返回是否成功和可能的错误信息
func speakText(text: String, completion: @escaping (Bool, String?) -> Void) {
if !initialized {
print("[AzureTtsHelper] TTS 引擎尚未初始化")
completion(false, "TTS 引擎尚未初始化")
return
}
do {
print("[AzureTtsHelper] 开始合成文本: \(text)")
currentRate = formatPercentage(rate)
currentPitch = formatPercentage(pitch)
currentVolume = "\(min(max(volume, 0), 100))%"
// 生成 SSML
let ssml = generateSsml(text: text)
// 使用 SSML 合成语音
speakSsml(ssml: ssml, completion: completion)
print("\(tag): 已设置语音参数: 语速=\(rate), 音调=\(pitch), 音量=\(volume)")
return true
} catch {
print("[AzureTtsHelper] 语音合成异常: \(error.localizedDescription)")
completion(false, "语音合成异常: \(error.localizedDescription)")
print("\(tag): 设置语音参数失败: \(error.localizedDescription)")
return false
}
}
/// 格式化百分比值
private func formatPercentage(_ value: Int) -> String {
return value >= 0 ? "+\(value)%" : "\(value)%"
}
/// 生成 SSML 文本
///
/// - Parameter text: 要转换的文本
/// - Returns: SSML 格式的文本
private func generateSsml(text: String) -> String {
// 计算 SSML 参数
let rateParam = currentSpeechRate == 0 ? "0%" : (currentSpeechRate < 0 ? "\(Int(Double(currentSpeechRate) * 0.9))%" : "\(currentSpeechRate)%")
let pitchParam = currentPitch == 0 ? "0%" : "\(Int(Double(currentPitch) * 0.5))%"
let volumeParam = "\(min(max(currentVolume, 0), 100))%"
return """
<speak version="1.0" xmlns="http://www.w3.org/2001/10/synthesis" xmlns:mstts="https://www.w3.org/2001/mstts" xml:lang="zh-CN">
<voice name="\(currentVoiceName)">
<prosody rate="\(rateParam)" pitch="\(pitchParam)" volume="\(volumeParam)">
<voice name="\(currentVoice)">
<prosody rate="\(currentRate)" pitch="\(currentPitch)" volume="\(currentVolume)">
\(text)
</prosody>
</voice>
@ -230,73 +276,133 @@ class AzureTtsHelper: NSObject {
"""
}
/// 合成 SSML 为语音并播放
///
/// - Parameters:
/// - ssml: SSML 格式的文本
/// - completion: 完成回调,返回是否成功和可能的错误信息
private func speakSsml(ssml: String, completion: @escaping (Bool, String?) -> Void) {
/// 合成文本为语音并播放
/// - Parameter text: 要合成的文本
/// - Returns: 是否成功开始合成
func speakText(_ text: String) -> Bool {
if !initialized {
print("[AzureTtsHelper] TTS 引擎尚未初始化")
completion(false, "TTS 引擎尚未初始化")
return
print("\(tag): TTS 引擎尚未初始化")
return false
}
do {
print("[AzureTtsHelper] 开始合成 SSML")
print("\(tag): 开始合成文本: \(text)")
// 生成 SSML
let ssml = generateSsml(text: text)
// 标记为正在播放
speaking = true
// 激活音频会话
try audioSession.setActive(true)
// 异步合成语音
let result = try synthesizer!.speakSsml(ssml)
switch result.reason {
case .synthesizingAudioCompleted:
print("[AzureTtsHelper] 语音合成完成")
speaking = false
completion(true, "语音合成完成")
case .canceled:
if let cancelDetails = try? SPXSpeechSynthesisCancellationDetails(fromResult: result) {
print("[AzureTtsHelper] 语音合成取消: \(cancelDetails.errorDetails ?? "未知错误")")
speaking = false
completion(false, "语音合成取消: \(cancelDetails.errorDetails ?? "未知错误")")
} else {
print("[AzureTtsHelper] 语音合成取消")
speaking = false
completion(false, "语音合成取消")
try synthesizer?.speakSsmlAsync(ssml)
return true
} catch {
print("\(tag): 语音合成异常: \(error.localizedDescription)")
speaking = false
return false
}
}
/// 处理流式文本并在遇到标点符号时播放
/// - Parameter text: 收到的文本流片段
/// - Returns: 是否成功处理
func speakStream(_ text: String) -> Bool {
if !initialized || text.isEmpty {
return false
}
do {
// 添加新文本到缓冲区
streamBuffer.append(text)
// 增加500ms防抖逻辑
let currentTime = Date().timeIntervalSince1970
if currentTime - lastSpeakTime < 0.5 {
return true
}
lastSpeakTime = currentTime
let currentText = streamBuffer
// 定义标点符号列表
let punctuationMarks: [Character] = [".", "。", "!", "!", "?", "?", ";", ";", ",", ",", ":", ":"]
// 查找最后一个标点符号的位置
var lastPunctuationIndex = -1
for i in currentText.indices.reversed() {
if punctuationMarks.contains(currentText[i]) {
lastPunctuationIndex = currentText.distance(from: currentText.startIndex, to: i)
break
}
default:
print("[AzureTtsHelper] 语音合成失败: \(result.reason)")
speaking = false
completion(false, "语音合成失败: \(result.reason)")
}
// 如果找到标点符号,则播放到该标点符号
if lastPunctuationIndex >= 0 {
// 提取要播放的文本(包含标点符号)
let textToSpeak = String(currentText.prefix(lastPunctuationIndex + 1))
// 剩余的文本保存在缓冲区中
streamBuffer = String(currentText.dropFirst(lastPunctuationIndex + 1))
// 播放提取的文本
return speakText(textToSpeak)
}
// 如果没有找到标点符号,则等待更多文本
return true
} catch {
print("[AzureTtsHelper] 语音合成异常: \(error.localizedDescription)")
speaking = false
completion(false, "语音合成异常: \(error.localizedDescription)")
print("\(tag): 流式语音合成失败: \(error.localizedDescription)")
return false
}
}
/// 播放剩余的流式文本
/// - Returns: 是否成功播放剩余文本
func flushStream() -> Bool {
if !initialized {
return false
}
do {
// 获取缓冲区中剩余的文本
let remainingText = streamBuffer
// 清空缓冲区
streamBuffer = ""
// 如果缓冲区为空,直接返回成功
if remainingText.isEmpty {
return true
}
// 播放剩余文本
return speakText(remainingText)
} catch {
print("\(tag): 刷新流式文本失败: \(error.localizedDescription)")
return false
}
}
/// 停止当前语音合成
///
/// - Returns: 是否停止成功
func stopSpeaking() -> Bool {
if !initialized {
print("[AzureTtsHelper] TTS 引擎尚未初始化")
print("\(tag): TTS 引擎尚未初始化")
return false
}
do {
try synthesizer?.stopSpeaking()
// 清空流缓冲区
streamBuffer = ""
try synthesizer?.stopSpeakingAsync()
speaking = false
print("[AzureTtsHelper] 已停止语音合成")
print("\(tag): 已停止语音合成")
return true
} catch {
print("[AzureTtsHelper] 停止语音合成失败: \(error.localizedDescription)")
print("\(tag): 停止语音合成失败: \(error.localizedDescription)")
return false
}
}
@ -309,20 +415,30 @@ class AzureTtsHelper: NSObject {
// 恢复音频会话
try audioSession.setActive(false, options: .notifyOthersOnDeactivation)
// 关闭自定义音频输出流
customAudioOutputStream = nil
// 清空流缓冲区
streamBuffer = ""
// 清理资源
synthesizer = nil
speechConfig = nil
audioConfig = nil
ttsCallback = nil
initialized = false
speaking = false
print("[AzureTtsHelper] TTS 引擎已释放")
print("\(tag): TTS 引擎已释放")
} catch {
print("[AzureTtsHelper] 释放 TTS 引擎失败: \(error.localizedDescription)")
print("\(tag): 释放 TTS 引擎失败: \(error.localizedDescription)")
// 确保重置状态
initialized = false
speaking = false
}
}
/// 检查当前是否正在播放语音
///
/// - Returns: 是否正在播放语音
func isSpeaking() -> Bool {
return speaking

6
local_plugins/ble_service/android/build.gradle.kts

@ -28,13 +28,7 @@ android {
dependencies {
implementation("androidx.core:core-ktx:1.13.1")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.0")
implementation("androidx.work:work-runtime-ktx:2.8.1")
implementation("androidx.media:media:1.6.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
implementation("androidx.security:security-crypto:1.1.0-alpha03")
implementation(project(":deepvoice_utils"))
implementation(project(":agent_service"))
implementation(project(":azure_speech"))
implementation(project(":jl_opus"))
}

36
local_plugins/ble_service/android/src/main/AndroidManifest.xml

@ -18,39 +18,5 @@
<uses-permission android:name="android.permission.MEDIA_CONTENT_CONTROL" />
<uses-permission android:name="android.permission.RECEIVE_MEDIA_BUTTON" />
<application>
<!-- 媒体前台服务 -->
<service
android:name=".BleMediaService"
android:exported="true"
android:enabled="true"
android:foregroundServiceType="mediaPlayback">
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter>
</service>
<!-- 媒体按钮 Receiver -->
<receiver
android:name="androidx.media.session.MediaButtonReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter>
</receiver>
<!-- 被系统 PendingIntent 触发的 Receiver -->
<receiver
android:name=".BleWakeReceiver"
android:exported="true"
android:enabled="true">
<intent-filter>
<action android:name="com.yunqiinnovation.ble_service.ACTION_BLE_SCAN" />
</intent-filter>
</receiver>
</application>
</manifest>

72
local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt

@ -175,7 +175,6 @@ object BleService {
if (isScanning) return false
FileLogger.i(TAG, "开始主动扫描BLE设备...")
unregisterBackgroundScan() // 避免双重扫描
scanResults.clear()
val filter = ScanFilter.Builder()
@ -223,8 +222,6 @@ object BleService {
scanHandler.removeCallbacksAndMessages(null)
isScanning = false
// 重新注册后台扫描,保证唤醒能力
registerBackgroundScan()
return true
} catch (e: Exception) {
@ -1017,74 +1014,7 @@ object BleService {
return bluetoothAdapter
}
// ======================================================================================================
// 后台扫描与唤醒
// ======================================================================================================
/**
* 注册后台持久扫描,APP被杀死后也能唤醒
*/
fun registerBackgroundScan() {
// 获取一个局部引用,避免并发访问问题
val scanner = bluetoothLeScanner ?: return
try {
val filter = ScanFilter.Builder()
// .setServiceUuid(ParcelUuid(BleConst.PRIMARY_SERVICE_UUID))
.setManufacturerData(0xEFAB, byteArrayOf(0xA0.toByte(), 0x01))
.build()
val settings = ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_POWER)
// .setCallbackType(ScanSettings.CALLBACK_TYPE_FIRST_MATCH)
.setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES)
.build()
// 创建Intent并设置前台接收器标志
val scanIntent = Intent(BleConst.ACTION_SCAN_RESULT)
.setClass(context, BleWakeReceiver::class.java)
.addFlags(Intent.FLAG_RECEIVER_FOREGROUND)
val pi = PendingIntent.getBroadcast(
context, 0, scanIntent,
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
} else {
PendingIntent.FLAG_UPDATE_CURRENT
}
)
scanner.startScan(listOf(filter), settings, pi)
FileLogger.i(TAG, "后台持久扫描已注册")
} catch (e: Exception) {
FileLogger.e(TAG, "注册后台扫描失败: ${e.message}", e)
}
}
/**
* 注销后台持久扫描
*/
fun unregisterBackgroundScan() {
// 获取一个局部引用,避免并发访问问题
val scanner = bluetoothLeScanner ?: return
try {
// 创建Intent并设置前台接收器标志
val scanIntent = Intent(BleConst.ACTION_SCAN_RESULT)
.setClass(context, BleWakeReceiver::class.java)
.addFlags(Intent.FLAG_RECEIVER_FOREGROUND)
val pi = PendingIntent.getBroadcast(
context, 0, scanIntent,
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
} else {
PendingIntent.FLAG_UPDATE_CURRENT
}
)
scanner.stopScan(pi)
FileLogger.i(TAG, "后台持久扫描已注销")
} catch (e: Exception) {
FileLogger.e(TAG, "注销后台扫描失败: ${e.message}", e)
}
}
// ======================================================================================================
// 资源清理

87
local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleServicePlugin.kt

@ -160,48 +160,7 @@ class BleServicePlugin : FlutterPlugin, MethodCallHandler, BleService.Callback {
val success = BleService.playA2DP()
result.success(success)
}
"registerBackgroundScan" -> {
try {
BleService.registerBackgroundScan()
FileLogger.d(TAG, "后台扫描已注册")
result.success(true)
} catch (e: Exception) {
FileLogger.e(TAG, "注册后台扫描失败", e)
result.error("BLE_SERVICE_ERROR", "注册后台扫描失败: ${e.message}", null)
}
}
"unregisterBackgroundScan" -> {
try {
BleService.unregisterBackgroundScan()
FileLogger.d(TAG, "后台扫描已注销")
result.success(true)
} catch (e: Exception) {
FileLogger.e(TAG, "注销后台扫描失败", e)
result.error("BLE_SERVICE_ERROR", "注销后台扫描失败: ${e.message}", null)
}
}
"startBleService" -> {
try {
// 从Flutter获取配置参数
val configParams = call.arguments as? Map<String, Any> ?: mapOf()
FileLogger.i(TAG, "启动BLE服务,配置参数: $configParams")
// 启动BLE服务并传递配置参数
startBleServiceWithConfig(configParams)
result.success(true)
} catch (e: Exception) {
FileLogger.e(TAG, "启动BLE服务失败", e)
result.error("BLE_SERVICE_ERROR", "启动BLE服务失败: ${e.message}", null)
}
}
"stopBleService" -> {
try {
stopBleService()
result.success(true)
} catch (e: Exception) {
FileLogger.e(TAG, "停止BLE服务失败", e)
result.error("BLE_SERVICE_ERROR", "停止BLE服务失败: ${e.message}", null)
}
}
else -> {
result.notImplemented()
}
@ -254,48 +213,4 @@ class BleServicePlugin : FlutterPlugin, MethodCallHandler, BleService.Callback {
sendEvent(statusEventSink, resultMap, "发送设备信息异常")
}
/**
* 使用配置参数启动BLE服务
*/
private fun startBleServiceWithConfig(configParams: Map<String, Any>) {
try {
// 启动BLE前台服务
val serviceIntent = Intent(context, BleMediaService::class.java)
// 如果有额外配置需要传递给服务,可以通过Intent传递
configParams.forEach { (key, value) ->
when (value) {
is String -> serviceIntent.putExtra(key, value)
is Boolean -> serviceIntent.putExtra(key, value)
is Int -> serviceIntent.putExtra(key, value)
is Double -> serviceIntent.putExtra(key, value.toFloat())
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(serviceIntent)
} else {
context.startService(serviceIntent)
}
FileLogger.d(TAG, "成功启动BLE服务,配置参数: $configParams")
} catch (e: Exception) {
FileLogger.e(TAG, "BLE服务启动失败", e)
throw e
}
}
/**
* 停止BLE服务
*/
private fun stopBleService() {
try {
context.stopService(Intent(context, BleMediaService::class.java))
FileLogger.d(TAG, "成功停止BLE服务")
} catch (e: Exception) {
FileLogger.e(TAG, "停止BLE服务失败", e)
throw e
}
}
}

15
local_plugins/ble_service/ios/Assets/ble_service-Info.plist

@ -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>

90
local_plugins/ble_service/ios/Classes/BleConst.swift

@ -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
}

25
local_plugins/ble_service/ios/Classes/BleDelegate.swift

@ -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])
}

963
local_plugins/ble_service/ios/Classes/BleService.swift

@ -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])
}

4
local_plugins/ble_service/ios/Classes/BleServicePlugin.h

@ -0,0 +1,4 @@
#import <Flutter/Flutter.h>
@interface BleServicePlugin : NSObject<FlutterPlugin>
@end

12
local_plugins/ble_service/ios/Classes/BleServicePlugin.m

@ -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

203
local_plugins/ble_service/ios/Classes/SwiftBleServicePlugin.swift

@ -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
}
}

23
local_plugins/ble_service/ios/ble_service.podspec

@ -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

410
local_plugins/chat_storage/ios/Classes/ChatStorageHelper.swift

@ -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
}
}

4
local_plugins/chat_storage/ios/Classes/ChatStoragePlugin.h

@ -0,0 +1,4 @@
#import <Flutter/Flutter.h>
@interface ChatStoragePlugin : NSObject<FlutterPlugin>
@end

12
local_plugins/chat_storage/ios/Classes/ChatStoragePlugin.m

@ -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

182
local_plugins/chat_storage/ios/Classes/ChatStoragePlugin.swift

@ -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))
}
}
}

11
local_plugins/chat_storage/ios/Classes/SwiftChatStoragePlugin.swift

@ -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)
}
}

64
local_plugins/chat_storage/ios/README.md

@ -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 端的实现,并确保在版本升级时提供适当的数据库迁移策略。
数据库迁移逻辑目前实现为重建表,在生产环境中应该改为保留用户数据的方式。

11
local_plugins/chat_storage/ios/chat_storage.podspec

@ -5,13 +5,13 @@
Pod::Spec.new do |s|
s.name = 'chat_storage'
s.version = '0.0.1'
s.summary = 'Chat Storage plugin'
s.summary = '聊天记录存储插件'
s.description = <<-DESC
A Flutter plugin for chat storage services.
用于存储聊天记录的Flutter插件,支持SQLite数据库操作。
DESC
s.homepage = 'http://example.com'
s.homepage = 'http://yunqiinnovation.com'
s.license = { :file => '../LICENSE' }
s.author = { 'Your Company' => 'email@example.com' }
s.author = { 'YunQi Innovation' => 'support@yunqiinnovation.com' }
s.source = { :path => '.' }
s.source_files = 'Classes/**/*'
s.dependency 'Flutter'
@ -20,4 +20,7 @@ A Flutter plugin for chat storage services.
# 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'
# 添加SQLite库依赖
s.library = 'sqlite3'
end

197
local_plugins/classic_bluetooth/ios/Classes/ClassicBluetoothHelper.swift

@ -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])
}
}

4
local_plugins/classic_bluetooth/ios/Classes/ClassicBluetoothPlugin.h

@ -0,0 +1,4 @@
#import <Flutter/Flutter.h>
@interface ClassicBluetoothPlugin : NSObject<FlutterPlugin>
@end

12
local_plugins/classic_bluetooth/ios/Classes/ClassicBluetoothPlugin.m

@ -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

108
local_plugins/classic_bluetooth/ios/Classes/ClassicBluetoothPlugin.swift

@ -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
}
}

136
local_plugins/classic_bluetooth/ios/Classes/FileLogger.swift

@ -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
}
}

11
local_plugins/classic_bluetooth/ios/Classes/SwiftClassicBluetoothPlugin.swift

@ -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)
}
}

44
local_plugins/classic_bluetooth/ios/README.md

@ -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系统对蓝牙状态变化的通知可能有延迟
- 请确保用户已经授权蓝牙权限

22
local_plugins/classic_bluetooth/ios/classic_bluetooth.podspec

@ -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

2
local_plugins/classic_bluetooth/pubspec.yaml

@ -17,4 +17,6 @@ flutter:
platforms:
android:
package: com.yunqiinnovation.classic_bluetooth
pluginClass: ClassicBluetoothPlugin
ios:
pluginClass: ClassicBluetoothPlugin

4
local_plugins/deepvoice_utils/ios/Classes/DeepvoiceUtilsPlugin.h

@ -0,0 +1,4 @@
#import <Flutter/Flutter.h>
@interface DeepvoiceUtilsPlugin : NSObject<FlutterPlugin>
@end

12
local_plugins/deepvoice_utils/ios/Classes/DeepvoiceUtilsPlugin.m

@ -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

30
local_plugins/deepvoice_utils/ios/Classes/DeepvoiceUtilsPlugin.swift

@ -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()
}
}

286
local_plugins/deepvoice_utils/ios/Classes/FileLogger.swift

@ -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
}
}

11
local_plugins/deepvoice_utils/ios/Classes/SwiftDeepvoiceUtilsPlugin.swift

@ -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)
}
}

54
local_plugins/deepvoice_utils/ios/README.md

@ -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)
```

19
local_plugins/deepvoice_utils/ios/deepvoice_utils.podspec

@ -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

249
local_plugins/open_ai_service/ios/Classes/MCPClient.swift

@ -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()
}
}

411
local_plugins/open_ai_service/ios/Classes/OpenAIService.swift

@ -1,4 +1,5 @@
import Foundation
import UIKit
/// OpenAI服务异常
public class OpenAIError: Error {
@ -20,6 +21,15 @@ private class ToolCallInfo {
}
}
/// OpenAI服务流回调类型定义
public typealias StreamCallback = (
onToken: (String) -> Void,
onComplete: () -> Void,
onError: (Error) -> Void,
onFunctionCall: ([String: Any]) -> Void,
onFunctionCallResult: ([String: Any], [String: Any]) -> Void
)
/// OpenAI服务iOS原生实现
public class OpenAIService {
private let TAG = "OpenAIService"
@ -27,6 +37,7 @@ public class OpenAIService {
private var apiKey: String = ""
private var isInitialized = false
private var model: String = "doubao-1-5-lite-32k-250115" // 默认模型
private var visionModel: String = "doubao-1-5-vision-pro-32k-250115" // 默认视觉模型
// 用于存储注册的函数
private var registeredFunctions: [[String: Any]] = []
@ -34,6 +45,17 @@ public class OpenAIService {
// URL会话
private let session: URLSession
// 当前流式请求任务
private var currentStreamTask: URLSessionDataTask?
private var isCanceled = false
// MCP客户端
private var mcpClient: MCPClient?
private var isMcpInitialized = false
// 是否自动处理MCP工具调用
private var autoHandleMcpTools = true
public init() {
// 创建URL会话配置
let config = URLSessionConfiguration.default
@ -42,18 +64,181 @@ public class OpenAIService {
session = URLSession(configuration: config)
}
/// 初始化MCP客户端
/// - Parameter serverUrl: MCP服务器地址
/// - Returns: 是否成功开始初始化
public func initializeMcpClient(_ serverUrl: String) -> Bool {
if mcpClient != nil {
mcpClient?.close()
}
mcpClient = MCPClient()
// 开始异步连接
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
guard let self = self, let mcpClient = self.mcpClient else { return }
let result = mcpClient.connectToSSE(serverUrl)
self.isMcpInitialized = result
print("\(self.TAG) MCP客户端初始化\(result ? "成功" : "失败")")
}
return true // 立即返回,实际连接在后台进行
}
/// 检查MCP客户端是否已初始化
/// - Returns: 是否已初始化
public func isMcpInitialized() -> Bool {
return isMcpInitialized && mcpClient?.isConnected() == true
}
/// 关闭MCP客户端
/// - Returns: 是否成功关闭
public func closeMcpClient() -> Bool {
mcpClient?.close()
mcpClient = nil
isMcpInitialized = false
return true
}
/// 处理MCP工具调用
/// - Parameter functionCall: 函数调用信息
/// - Returns: 处理结果
public func handleMcpToolCall(_ functionCall: [String: Any]) async -> String {
if mcpClient == nil || !isMcpInitialized {
return "MCP客户端未初始化"
}
do {
// 获取函数名称
guard let name = functionCall["name"] as? String,
let argumentsJson = functionCall["arguments"] as? String else {
return "函数调用信息不完整"
}
// 解析参数
let arguments = mcpClient?.parseJsonArguments(argumentsJson) ?? [:]
// 调用工具
let result = await mcpClient?.callTool(name: name, arguments: arguments)
return result?["context"] as? String ?? "处理MCP工具调用失败"
} catch {
print("\(TAG) 处理MCP工具调用失败: \(error.localizedDescription)")
return "处理MCP工具调用失败: \(error.localizedDescription)"
}
}
/// 创建用户消息
public func createUserMessage(content: String) -> [String: Any] {
return ["role": "user", "content": content]
}
/// 创建系统消息
public func createSystemMessage(content: String) -> [String: Any] {
return ["role": "system", "content": content]
}
/// 创建助手消息
public func createAssistantMessage(content: String) -> [String: Any] {
return ["role": "assistant", "content": content]
}
/// 创建带图片的用户消息
public func createUserMessageWithImage(text: String, imageBase64: String) -> [String: Any] {
// 创建包含文本和图片的内容数组
var contentArray: [[String: Any]] = []
// 添加文本部分
if !text.isEmpty {
contentArray.append([
"type": "text",
"text": text
])
}
// 添加图片部分
contentArray.append([
"type": "image_url",
"image_url": [
"url": "data:image/jpeg;base64,\(imageBase64)"
]
])
return ["role": "user", "content": contentArray]
}
/// 将文件转换为Base64字符串
public func fileToBase64(_ filePath: String, maxSizeKB: Int = 20480) -> String? {
do {
let fileURL = URL(fileURLWithPath: filePath)
// 检查文件是否存在
guard FileManager.default.fileExists(atPath: filePath) else {
print("\(TAG) 文件不存在: \(filePath)")
return nil
}
// 读取图片
guard let originalImage = UIImage(contentsOfFile: filePath) else {
print("\(TAG) 无法读取图片: \(filePath)")
return nil
}
// 检查图片尺寸,限制最大为1024*1024
var processedImage = originalImage
let maxDimension: CGFloat = 1024
if originalImage.size.width > maxDimension || originalImage.size.height > maxDimension {
print("\(TAG) 图片尺寸超过限制,进行缩放: \(originalImage.size) -> \(maxDimension)")
// 计算缩放比例,保持纵横比
let widthRatio = maxDimension / originalImage.size.width
let heightRatio = maxDimension / originalImage.size.height
let ratio = min(widthRatio, heightRatio)
let newWidth = originalImage.size.width * ratio
let newHeight = originalImage.size.height * ratio
let newSize = CGSize(width: newWidth, height: newHeight)
UIGraphicsBeginImageContextWithOptions(newSize, false, 1.0)
originalImage.draw(in: CGRect(origin: .zero, size: newSize))
if let resizedImage = UIGraphicsGetImageFromCurrentImageContext() {
processedImage = resizedImage
}
UIGraphicsEndImageContext()
print("\(TAG) 缩放后图片尺寸: \(newSize)")
}
// 压缩图片
var imageData = processedImage.jpegData(compressionQuality: 0.9)
// 检查文件大小,如果超出限制,继续压缩
var compressionQuality: CGFloat = 0.9
while let data = imageData, data.count > maxSizeKB * 1024 && compressionQuality > 0.1 {
compressionQuality -= 0.1
imageData = processedImage.jpegData(compressionQuality: compressionQuality)
}
guard let finalImageData = imageData else {
print("\(TAG) 无法压缩图片")
return nil
}
// 检查最终大小
if finalImageData.count > maxSizeKB * 1024 {
print("\(TAG) 压缩后图片仍然超出大小限制: \(finalImageData.count / 1024)KB > \(maxSizeKB)KB")
}
// 转为Base64
return finalImageData.base64EncodedString()
} catch {
print("\(TAG) 转换文件到Base64失败: \(error.localizedDescription)")
return nil
}
}
/// 初始化OpenAI服务
public func initialize(apiKey: String, baseUrl: String = "", model: String = "") -> Bool {
public func initialize(apiKey: String, baseUrl: String = "", model: String = "", mcpServer: String = "") -> Bool {
self.apiKey = apiKey
if !baseUrl.isEmpty {
self.baseUrl = baseUrl
@ -61,6 +246,12 @@ public class OpenAIService {
if !model.isEmpty {
self.model = model
}
// 初始化MCP客户端
if !mcpServer.isEmpty {
initializeMcpClient(mcpServer)
}
isInitialized = !apiKey.isEmpty
return isInitialized
}
@ -89,6 +280,76 @@ public class OpenAIService {
}
}
/// 取消当前流式请求
public func cancelCurrentStream() {
isCanceled = true
currentStreamTask?.cancel()
currentStreamTask = nil
}
/// 自动处理MCP工具调用
private func autoHandleMcpToolCall(
functionCall: [String: Any],
messages: [[String: Any]],
systemPrompt: String,
callback: @escaping StreamCallback
) {
Task {
do {
// 获取函数名称
guard let name = functionCall["name"] as? String,
let argumentsJson = functionCall["arguments"] as? String else {
callback.onError(OpenAIError("函数调用信息不完整"))
return
}
// 解析参数
let arguments = mcpClient?.parseJsonArguments(argumentsJson) ?? [:]
// 调用工具
if let result = await mcpClient?.callTool(name: name, arguments: arguments) {
let functionResult = result["context"] as? String ?? "工具调用失败"
// 创建结果对象,格式需要与Android匹配
let functionCallResult: [String: Any] = ["result": functionResult]
// 发送函数调用结果回调
callback.onFunctionCallResult(functionCall, functionCallResult)
// 发送函数调用结果
sendFunctionCallResult(
messages: messages,
systemPrompt: systemPrompt,
functionCall: functionCall,
functionResult: functionResult,
callback: callback
)
} else {
callback.onError(OpenAIError("工具调用失败"))
}
} catch {
print("\(TAG) 自动处理工具调用失败: \(error.localizedDescription)")
// 失败时返回错误给回调函数
let errorMessage = "工具调用失败: \(error.localizedDescription)"
// 创建错误结果对象
let errorResult: [String: Any] = ["error": errorMessage]
// 发送函数调用结果
callback.onFunctionCallResult(functionCall, errorResult)
sendFunctionCallResult(
messages: messages,
systemPrompt: systemPrompt,
functionCall: functionCall,
functionResult: errorMessage,
callback: callback
)
}
}
}
/// 发送消息(非流式输出)
public func sendMessage(messages: [[String: Any]], systemPrompt: String) throws -> String {
guard isInitialized, !apiKey.isEmpty else {
@ -239,7 +500,7 @@ public class OpenAIService {
/// 发送消息(流式输出)
public func sendMessageStream(
messages: [[String: Any]],
systemPrompt: String,
systemPrompt: String = "",
callback: @escaping StreamCallback
) {
guard isInitialized, !apiKey.isEmpty else {
@ -247,34 +508,36 @@ public class OpenAIService {
return
}
// 重置取消状态
isCanceled = false
// 检查最后一条消息内容中是否包含图片,决定使用哪个模型
var currentModel = model
if let lastMessage = messages.last,
let content = lastMessage["content"] as? [[String: Any]],
content.contains(where: { ($0["type"] as? String) == "image_url" }) {
currentModel = visionModel
}
// 构建请求体
var requestBody: [String: Any] = [
"model": model,
"model": currentModel,
"temperature": 0.7,
"max_tokens": 2000,
"stream": true
]
// 构建完整消息数组,添加系统提示
var fullMessages: [[String: Any]] = [
["role": "system", "content": systemPrompt]
]
var fullMessages: [[String: Any]] = []
if !systemPrompt.isEmpty {
fullMessages.append(["role": "system", "content": systemPrompt])
}
fullMessages.append(contentsOf: messages)
requestBody["messages"] = fullMessages
// 添加工具列表
if let toolMaps = mcpClient?.getToolMaps(), !toolMaps.isEmpty {
var tools: [[String: Any]] = []
for toolMap in toolMaps {
if let tool = toolMap as? [String: Any] {
tools.append(tool)
}
}
if !tools.isEmpty {
requestBody["tools"] = tools
}
requestBody["tools"] = toolMaps
} else if !registeredFunctions.isEmpty {
var tools: [[String: Any]] = []
@ -311,9 +574,25 @@ public class OpenAIService {
request.httpBody = jsonData
// 创建流式会话任务
let delegate = SSEStreamDelegate(callback: callback)
let session = URLSession(configuration: .default, delegate: delegate, delegateQueue: nil)
let delegate = SSEStreamDelegate(
callback: callback,
autoHandleMcpTools: autoHandleMcpTools,
autoToolHandler: { [weak self] functionCall in
// 如果需要自动处理工具调用,调用处理方法
guard let self = self, self.autoHandleMcpTools else { return }
self.autoHandleMcpToolCall(
functionCall: functionCall,
messages: messages,
systemPrompt: systemPrompt,
callback: callback
)
}
)
let sessionConfig = URLSessionConfiguration.default
let session = URLSession(configuration: sessionConfig, delegate: delegate, delegateQueue: nil)
let task = session.dataTask(with: request)
currentStreamTask = task
task.resume()
}
@ -327,10 +606,12 @@ public class OpenAIService {
) {
do {
// 构建完整消息数组
var fullMessages: [[String: Any]] = [
// 添加系统提示
["role": "system", "content": systemPrompt]
]
var fullMessages: [[String: Any]] = []
// 添加系统提示
if !systemPrompt.isEmpty {
fullMessages.append(["role": "system", "content": systemPrompt])
}
// 添加用户消息
fullMessages.append(contentsOf: messages)
@ -347,7 +628,7 @@ public class OpenAIService {
// 添加函数调用消息
fullMessages.append([
"role": "assistant",
"content": NSNull(),
"content": nil as Any?,
"tool_calls": [
[
"id": id,
@ -378,11 +659,20 @@ public class OpenAIService {
/// SSE流委托实现
private class SSEStreamDelegate: NSObject, URLSessionDataDelegate {
let callback: StreamCallback
let autoHandleMcpTools: Bool
let autoToolHandler: ([String: Any]) -> Void
private var buffer = Data()
private var finalToolCalls: [Int: ToolCallInfo] = [:]
init(callback: @escaping StreamCallback) {
init(
callback: @escaping StreamCallback,
autoHandleMcpTools: Bool = true,
autoToolHandler: @escaping ([String: Any]) -> Void
) {
self.callback = callback
self.autoHandleMcpTools = autoHandleMcpTools
self.autoToolHandler = autoToolHandler
super.init()
}
@ -471,7 +761,7 @@ public class OpenAIService {
}
}
} catch {
NSLog("解析JSON出错: \(error.localizedDescription)")
print("解析JSON出错: \(error.localizedDescription)")
// 忽略解析错误,继续处理其他行
}
}
@ -482,50 +772,45 @@ public class OpenAIService {
if finalToolCalls.isEmpty { return false }
// 只处理第一个工具调用
guard let firstToolCall = finalToolCalls.values.first, firstToolCall.isValid else { return false }
// 创建函数调用字典
let functionCall: [String: Any] = [
"name": firstToolCall.name,
"arguments": firstToolCall.arguments,
"id": firstToolCall.id
]
for (_, toolCall) in finalToolCalls {
if toolCall.isValid {
// 创建函数调用字典
let functionCall: [String: Any] = [
"name": toolCall.name,
"arguments": toolCall.arguments,
"id": toolCall.id
]
// 调用回调
callback.onFunctionCall(functionCall)
// 如果需要自动处理工具调用
if autoHandleMcpTools {
autoToolHandler(functionCall)
}
return true
}
}
// 回调
callback.onFunctionCall(functionCall)
return true
return false
}
// 处理完成
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
if let error = error {
callback.onError(OpenAIError("请求失败: \(error.localizedDescription)"))
if (error as NSError).code == NSURLErrorCancelled {
// 请求被取消,无需处理
return
}
callback.onError(error)
} else {
// 如果没有处理任何工具调用且没有错误,则完成
if finalToolCalls.isEmpty {
callback.onComplete()
}
}
}
}
/// 流式输出回调协议
public typealias StreamCallback = (onToken: (String) -> Void,
onComplete: () -> Void,
onError: (Error) -> Void,
onFunctionCall: ([String: Any]) -> Void)
/// 处理工具调用(函数调用)并回调
private func processToolCalls(_ toolCalls: [Int: ToolCallInfo], callback: StreamCallback) -> Bool {
if toolCalls.isEmpty { return false }
// 只处理第一个工具调用
guard let firstToolCall = toolCalls.values.first, firstToolCall.isValid else { return false }
// 创建函数调用字典
let functionCall: [String: Any] = [
"name": firstToolCall.name,
"arguments": firstToolCall.arguments,
"id": firstToolCall.id
]
// 回调
callback.onFunctionCall(functionCall)
return true
}
}

146
local_plugins/open_ai_service/ios/Classes/OpenAIServicePlugin.swift

@ -26,7 +26,8 @@ public class OpenAIServicePlugin: NSObject, FlutterPlugin, FlutterStreamHandler
let apiKey = args["apiKey"] as? String {
let baseUrl = args["baseUrl"] as? String ?? ""
let model = args["model"] as? String ?? ""
let initialized = openAIService.initialize(apiKey: apiKey, baseUrl: baseUrl, model: model)
let mcpServer = args["mcpServer"] as? String ?? ""
let initialized = openAIService.initialize(apiKey: apiKey, baseUrl: baseUrl, model: model, mcpServer: mcpServer)
result(initialized)
} else {
result(FlutterError(code: "INVALID_ARGUMENT", message: "初始化参数无效", details: nil))
@ -46,12 +47,13 @@ public class OpenAIServicePlugin: NSObject, FlutterPlugin, FlutterStreamHandler
case "sendMessage":
guard let args = call.arguments as? [String: Any],
let messagesRaw = args["messages"] as? [[String: Any]],
let systemPrompt = args["systemPrompt"] as? String else {
let messagesRaw = args["messages"] as? [[String: Any]] else {
result(FlutterError(code: "INVALID_ARGUMENT", message: "发送消息参数无效", details: nil))
return
}
let systemPrompt = args["systemPrompt"] as? String ?? ""
// 在后台线程执行
DispatchQueue.global(qos: .userInitiated).async {
do {
@ -70,12 +72,13 @@ public class OpenAIServicePlugin: NSObject, FlutterPlugin, FlutterStreamHandler
case "sendMessageStream":
guard let args = call.arguments as? [String: Any],
let messagesRaw = args["messages"] as? [[String: Any]],
let systemPrompt = args["systemPrompt"] as? String else {
let messagesRaw = args["messages"] as? [[String: Any]] else {
result(FlutterError(code: "INVALID_ARGUMENT", message: "发送消息参数无效", details: nil))
return
}
let systemPrompt = args["systemPrompt"] as? String ?? ""
// 检查事件接收器
guard let eventSink = self.eventSink else {
result(FlutterError(code: "NO_EVENT_SINK", message: "没有可用的事件流接收器", details: nil))
@ -106,7 +109,35 @@ public class OpenAIServicePlugin: NSObject, FlutterPlugin, FlutterStreamHandler
onFunctionCall: { functionCall in
// 发送函数调用事件
DispatchQueue.main.async {
eventSink(["type": "functionCall", "content": functionCall])
// 转换为字符串
do {
let data = try JSONSerialization.data(withJSONObject: functionCall)
if let jsonString = String(data: data, encoding: .utf8) {
eventSink(["type": "function_call", "content": jsonString])
} else {
eventSink(["type": "error", "content": "函数调用数据序列化失败"])
}
} catch {
eventSink(["type": "error", "content": "函数调用数据序列化失败: \(error.localizedDescription)"])
}
}
},
onFunctionCallResult: { functionCall, functionCallResult in
// 发送函数调用结果事件
DispatchQueue.main.async {
do {
let functionCallData = try JSONSerialization.data(withJSONObject: functionCall)
let resultData = try JSONSerialization.data(withJSONObject: functionCallResult)
if let functionCallString = String(data: functionCallData, encoding: .utf8),
let resultString = String(data: resultData, encoding: .utf8) {
eventSink(["type": "function_call_result", "function_call": functionCallString, "result": resultString])
} else {
eventSink(["type": "error", "content": "函数调用结果数据序列化失败"])
}
} catch {
eventSink(["type": "error", "content": "函数调用结果数据序列化失败: \(error.localizedDescription)"])
}
}
}
)
@ -122,13 +153,14 @@ public class OpenAIServicePlugin: NSObject, FlutterPlugin, FlutterStreamHandler
case "sendFunctionCallResult":
guard let args = call.arguments as? [String: Any],
let messagesRaw = args["messages"] as? [[String: Any]],
let systemPrompt = args["systemPrompt"] as? String,
let functionCallRaw = args["functionCall"] as? [String: Any],
let functionResult = args["functionResult"] as? String else {
result(FlutterError(code: "INVALID_ARGUMENT", message: "发送函数调用结果参数无效", details: nil))
return
}
let systemPrompt = args["systemPrompt"] as? String ?? ""
// 检查事件接收器
guard let eventSink = self.eventSink else {
result(FlutterError(code: "NO_EVENT_SINK", message: "没有可用的事件流接收器", details: nil))
@ -159,7 +191,35 @@ public class OpenAIServicePlugin: NSObject, FlutterPlugin, FlutterStreamHandler
onFunctionCall: { functionCall in
// 发送函数调用事件
DispatchQueue.main.async {
eventSink(["type": "functionCall", "content": functionCall])
// 转换为字符串
do {
let data = try JSONSerialization.data(withJSONObject: functionCall)
if let jsonString = String(data: data, encoding: .utf8) {
eventSink(["type": "function_call", "content": jsonString])
} else {
eventSink(["type": "error", "content": "函数调用数据序列化失败"])
}
} catch {
eventSink(["type": "error", "content": "函数调用数据序列化失败: \(error.localizedDescription)"])
}
}
},
onFunctionCallResult: { functionCall, functionCallResult in
// 发送函数调用结果事件
DispatchQueue.main.async {
do {
let functionCallData = try JSONSerialization.data(withJSONObject: functionCall)
let resultData = try JSONSerialization.data(withJSONObject: functionCallResult)
if let functionCallString = String(data: functionCallData, encoding: .utf8),
let resultString = String(data: resultData, encoding: .utf8) {
eventSink(["type": "function_call_result", "function_call": functionCallString, "result": resultString])
} else {
eventSink(["type": "error", "content": "函数调用结果数据序列化失败"])
}
} catch {
eventSink(["type": "error", "content": "函数调用结果数据序列化失败: \(error.localizedDescription)"])
}
}
}
)
@ -196,6 +256,76 @@ public class OpenAIServicePlugin: NSObject, FlutterPlugin, FlutterStreamHandler
result(FlutterError(code: "INVALID_ARGUMENT", message: "创建助手消息参数无效", details: nil))
}
case "createUserMessageWithImage":
if let args = call.arguments as? [String: Any],
let text = args["text"] as? String,
let imageBase64 = args["imageBase64"] as? String {
let message = openAIService.createUserMessageWithImage(text: text, imageBase64: imageBase64)
result(message)
} else {
result(FlutterError(code: "INVALID_ARGUMENT", message: "创建图片消息参数无效", details: nil))
}
case "fileToBase64":
if let args = call.arguments as? [String: Any],
let filePath = args["filePath"] as? String {
let maxSizeKB = args["maxSizeKB"] as? Int ?? 20480
let base64 = openAIService.fileToBase64(filePath, maxSizeKB: maxSizeKB)
if let base64 = base64 {
result(base64)
} else {
result(FlutterError(code: "FILE_ERROR", message: "文件转换失败", details: nil))
}
} else {
result(FlutterError(code: "INVALID_ARGUMENT", message: "文件转换参数无效", details: nil))
}
case "initializeMcpClient":
if let args = call.arguments as? [String: Any],
let serverUrl = args["serverUrl"] as? String {
let success = openAIService.initializeMcpClient(serverUrl)
result(success)
} else {
result(FlutterError(code: "INVALID_ARGUMENT", message: "初始化MCP客户端参数无效", details: nil))
}
case "isMcpInitialized":
result(openAIService.isMcpInitialized())
case "closeMcpClient":
result(openAIService.closeMcpClient())
case "handleMcpToolCall":
if let args = call.arguments as? [String: Any],
let functionCallJson = args["functionCall"] as? String {
// 解析函数调用JSON
guard let data = functionCallJson.data(using: .utf8),
let functionCall = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
result(FlutterError(code: "INVALID_ARGUMENT", message: "无效的函数调用JSON", details: nil))
return
}
// 在后台线程执行
Task {
do {
let toolResult = await openAIService.handleMcpToolCall(functionCall)
// 在主线程返回结果
DispatchQueue.main.async {
result(toolResult)
}
} catch {
// 在主线程返回错误
DispatchQueue.main.async {
result(FlutterError(code: "MCP_TOOL_ERROR", message: "处理MCP工具调用失败: \(error.localizedDescription)", details: nil))
}
}
}
} else {
result(FlutterError(code: "INVALID_ARGUMENT", message: "处理MCP工具调用参数无效", details: nil))
}
default:
result(FlutterMethodNotImplemented)
}

514
local_plugins/open_ai_service/ios/Classes/SystemFunctionHandler.swift

@ -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…
Cancel
Save