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.Handler
import android.os.Looper import android.os.Looper
import com.yunqiinnovation.deepvoice_utils.FileLogger import com.yunqiinnovation.deepvoice_utils.FileLogger
import com.yunqiinnovation.ble_service.BleMediaService
class MainActivity: FlutterActivity() { class MainActivity: FlutterActivity() {
private val TAG = "MainActivity" 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 'package:deep_voice/data/models/appconfig_model.dart';
import 'dart:convert'; import 'dart:convert';
import 'package:flutter/services.dart' show rootBundle; import 'package:flutter/services.dart' show rootBundle;
import 'package:agent_service/agent_service.dart';
/// 命令类型常量(与原生层保持一致) /// 命令类型常量(与原生层保持一致)
class BleCommands { class BleCommands {
@ -381,7 +382,7 @@ class BleManager extends GetxService {
} }
/// 启动BLE服务 /// 启动BLE服务
Future<bool> startBleService() async { Future<bool> startAgentService() async {
try { try {
// 从.env文件直接读取环境变量 // 从.env文件直接读取环境变量
// final azureSpeechKey = dotenv.env['AZURE_SPEECH_KEY'] ?? ''; // 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) { if (result) {
Logger.info('BLE服务启动成功'); Logger.info('BLE服务启动成功');
@ -463,9 +464,9 @@ class BleManager extends GetxService {
} }
/// 停止BLE服务 /// 停止BLE服务
Future<bool> stopBleService() async { Future<bool> stopAgentService() async {
try { try {
final result = await _bleService.stopBleService(); final result = await AgentService.stopAgentService();
if (result) { if (result) {
Logger.info('BLE服务停止成功'); 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 '../../../data/services/bluetooth_manager.dart';
import 'package:classic_bluetooth/classic_bluetooth.dart' show BluetoothDevice; import 'package:classic_bluetooth/classic_bluetooth.dart' show BluetoothDevice;
import '../../../data/services/ble_manager.dart'; import '../../../data/services/ble_manager.dart';
import 'package:agent_service/agent_service.dart';
/// 配对控制器,负责检查蓝牙权限、状态,并处理配对流程 /// 配对控制器,负责检查蓝牙权限、状态,并处理配对流程
class PairingController extends GetxController { class PairingController extends GetxController {
static PairingController get to => Get.find(); static PairingController get to => Get.find();
@ -87,7 +88,7 @@ class PairingController extends GetxController {
statusDescription.value = 'searchingDevice'.tr; statusDescription.value = 'searchingDevice'.tr;
// 设备连接时启动BLE后台服务 // 设备连接时启动BLE后台服务
_startBleService(); _startAgentService();
await refreshConnectedDevices(); await refreshConnectedDevices();
} catch (e) { } catch (e) {
@ -241,7 +242,7 @@ class PairingController extends GetxController {
} }
/// 启动BLE后台服务 /// 启动BLE后台服务
Future<void> _startBleService() async { Future<void> _startAgentService() async {
try { try {
// if (!isDeviceConnected) return; // if (!isDeviceConnected) return;
@ -256,7 +257,7 @@ class PairingController extends GetxController {
} }
// 启动BLE服务,无需传递配置参数,将从.env读取 // 启动BLE服务,无需传递配置参数,将从.env读取
final result = await BleManager.to.startBleService(); final result = await BleManager.to.startAgentService();
if (result) { if (result) {
Logger.info('BLE后台服务启动成功'); Logger.info('BLE后台服务启动成功');
@ -269,9 +270,9 @@ class PairingController extends GetxController {
} }
/// 停止BLE后台服务 /// 停止BLE后台服务
Future<void> _stopBleService() async { Future<void> _stopAgentService() async {
try { try {
final result = await BleManager.to.stopBleService(); final result = await BleManager.to.stopAgentService();
if (result) { if (result) {
Logger.info('BLE后台服务停止成功'); Logger.info('BLE后台服务停止成功');

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

@ -34,13 +34,18 @@ android {
} }
dependencies { dependencies {
add("implementation", "org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.6.10") implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.6.10")
add("implementation", "androidx.core:core-ktx:1.8.0") implementation("androidx.core:core-ktx:1.8.0")
add("implementation", "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4") 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(":azure_speech"))
add("compileOnly", project(":open_ai_service")) add("compileOnly", project(":open_ai_service"))
add("compileOnly", project(":deepvoice_utils")) add("compileOnly", project(":deepvoice_utils"))
add("compileOnly", project(":chat_storage")) 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"> package="com.yunqiinnovation.agent_service">
<uses-permission android:name="android.permission.RECORD_AUDIO" /> <uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.INTERNET" /> <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> </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 kotlinx.coroutines.*
import java.util.Collections import java.util.Collections
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
import com.yunqiinnovation.ble_service.BleService
/** /**
* 代理服务事件监听接口 * 代理服务事件监听接口
*/ */
@ -401,6 +401,7 @@ object AgentService : CoroutineScope {
FileLogger.d(TAG, "停止语音识别") FileLogger.d(TAG, "停止语音识别")
try { try {
BleService.closeCodec()
azureAsrHelper?.stopContinuousRecognition() azureAsrHelper?.stopContinuousRecognition()
isRecognitionActive = false isRecognitionActive = false
stopIdleCheck() 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 org.json.JSONObject
import kotlinx.coroutines.MainScope import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import android.content.Intent
import android.os.Build
class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.StreamHandler, AgentServiceListener { class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.StreamHandler, AgentServiceListener {
private val TAG = "AgentServicePlugin" private val TAG = "AgentServicePlugin"
@ -36,7 +38,50 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream
override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) { override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
when (call.method) { 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" -> { "startConversation" -> {
try { try {
val isStarted = AgentService.startRecognition(false) 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 android.content.Context
import com.yunqiinnovation.agent_service.AgentService import com.yunqiinnovation.agent_service.AgentService
import com.yunqiinnovation.agent_service.AgentServiceListener import com.yunqiinnovation.agent_service.AgentServiceListener
import com.yunqiinnovation.deepvoice_utils.FileLogger import com.yunqiinnovation.deepvoice_utils.FileLogger
import com.yunqiinnovation.ble_service.BleService
import com.yunqiinnovation.ble_service.BleConst
/** /**
* 蓝牙语音交互代理 * 蓝牙语音交互代理
* 连接BleService和AgentService,响应蓝牙设备唤醒信号,启动语音识别和交互 * 连接BleService和AgentService,响应蓝牙设备唤醒信号,启动语音识别和交互
*/ */
object ChatAgent : BleService.Callback, AgentServiceListener { object BleAgent : BleService.Callback, AgentServiceListener {
private const val TAG = "ChatAgent" private const val TAG = "BleAgent"
// 是否已初始化 // 是否已初始化
private var isInitialized = false private var isInitialized = false

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.Notification
import android.app.NotificationChannel import android.app.NotificationChannel
import android.app.NotificationManager import android.app.NotificationManager
import android.app.PendingIntent import android.app.PendingIntent
import android.app.Service 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.ComponentName
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
@ -28,6 +31,8 @@ import android.view.KeyEvent
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job 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 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()) { if(config.isNotEmpty()) {
// 初始化ChatAgent // 初始化ChatAgent
FileLogger.i(TAG, "开始初始化ChatAgent") FileLogger.i(TAG, "开始初始化ChatAgent")
val success = ChatAgent.initialize(applicationContext, config) val success = BleAgent.initialize(applicationContext, config)
if (success) { if (success) {
isChatAgentInitialized = true isChatAgentInitialized = true
FileLogger.i(TAG, "语音交互代理初始化成功") FileLogger.i(TAG, "语音交互代理初始化成功")
@ -364,7 +358,7 @@ class BleMediaService : Service(), CoroutineScope {
val currentTime = System.currentTimeMillis() val currentTime = System.currentTimeMillis()
if (currentTime - lastActionTime >= ACTION_DEBOUNCE_MS) { if (currentTime - lastActionTime >= ACTION_DEBOUNCE_MS) {
lastActionTime = currentTime lastActionTime = currentTime
ChatAgent.onWakeupSignalReceived() BleAgent.onWakeupSignalReceived()
} else { } else {
FileLogger.i(TAG, "媒体操作防抖: 忽略${ACTION_DEBOUNCE_MS}ms内的重复请求") FileLogger.i(TAG, "媒体操作防抖: 忽略${ACTION_DEBOUNCE_MS}ms内的重复请求")
} }
@ -386,7 +380,66 @@ class BleMediaService : Service(), CoroutineScope {
FileLogger.e(TAG, "连接设备失败: ${e.message}", e) 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.content.*
import android.bluetooth.le.* import android.bluetooth.le.*
@ -7,6 +7,7 @@ import android.view.KeyEvent
import android.media.AudioManager import android.media.AudioManager
import com.yunqiinnovation.deepvoice_utils.FileLogger import com.yunqiinnovation.deepvoice_utils.FileLogger
import com.yunqiinnovation.ble_service.BleService import com.yunqiinnovation.ble_service.BleService
import com.yunqiinnovation.ble_service.BleConst
/** /**
* BLE唤醒接收器 * BLE唤醒接收器
* 当耳机发送含特定UUID广播时,系统会触发此接收器 * 当耳机发送含特定UUID广播时,系统会触发此接收器
@ -18,6 +19,7 @@ class BleWakeReceiver : BroadcastReceiver() {
} }
override fun onReceive(ctx: Context, intent: Intent) { override fun onReceive(ctx: Context, intent: Intent) {
FileLogger.d(TAG, "onReceive, intent: $intent, ${BleConst.ACTION_SCAN_RESULT}")
FileLogger.init(ctx) FileLogger.init(ctx)
try { try {
@ -26,15 +28,21 @@ class BleWakeReceiver : BroadcastReceiver() {
} }
val list = intent.getParcelableArrayListExtra<ScanResult>( val list = intent.getParcelableArrayListExtra<ScanResult>(
BluetoothLeScanner.EXTRA_LIST_SCAN_RESULT BluetoothLeScanner.EXTRA_LIST_SCAN_RESULT
) ?: return )
// FileLogger.d(TAG, "list: $list")
if (list.isEmpty()) { if (list?.isEmpty() == true || list == null) {
FileLogger.d(TAG, "扫描结果为空") FileLogger.d(TAG, "扫描结果为空")
return return
} }
val mac = list.first().device.address val mac = list.firstOrNull()?.device?.address
// FileLogger.d(TAG, "被系统唤醒,发现设备 $mac") if (mac == null) {
FileLogger.d(TAG, "没有获取到设备MAC地址")
return
}
FileLogger.d(TAG, "被系统唤醒,发现设备 $mac")
if(!BleMediaService.isServiceRunning()) { if(!BleMediaService.isServiceRunning()) {
val am = ctx.getSystemService(Context.AUDIO_SERVICE) as AudioManager 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.content.*
import android.os.Build 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 android.content.Context
import androidx.security.crypto.EncryptedSharedPreferences 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 语音合成辅助类 /// Azure 语音合成辅助类
class AzureTtsHelper: NSObject { 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 synthesizer: SPXSpeechSynthesizer?
private var speechConfig: SPXSpeechConfig? private var speechConfig: SPXSpeechConfiguration?
private var audioConfig: SPXAudioConfig? private var audioConfig: SPXAudioConfiguration?
private var initialized = false private var initialized = false
private var speaking = false private var speaking = false
// 音频输出类型
enum AudioOutputType {
case speaker // 扬声器
case earpiece // 听筒
case auto // 自动选择
}
// 当前设置 // 当前设置
private var currentVoiceName = "zh-CN-XiaoxiaoNeural" private var currentVoice = "zh-CN-XiaoxiaoNeural"
private var currentSpeechRate = 0 private var currentRate = "0%"
private var currentPitch = 0 private var currentPitch = "0%"
private var currentVolume = 100 private var currentVolume = "100%"
private var currentAudioOutputType: AudioOutputType = .auto
// 音频会话管理 // 音频会话管理
private let audioSession = AVAudioSession.sharedInstance() 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 引擎 /// 初始化 TTS 引擎
///
/// - Parameters: /// - Parameters:
/// - speechSubscriptionKey: Azure 语音服务订阅密钥 /// - subscriptionKey: Azure 语音服务订阅密钥
/// - serviceRegion: Azure 语音服务区域 /// - region: Azure 语音服务区域
/// - language: 可选,默认语言,默认为 "zh-CN" /// - callback: 可选,全局TTS回调接口
/// - Returns: 是否初始化成功 /// - Returns: 是否初始化成功
func initialize(speechSubscriptionKey: String, serviceRegion: String, language: String = "zh-CN") -> Bool { func initialize(
print("[AzureTtsHelper] 初始化 Azure 语音服务") subscriptionKey: String,
region: String,
callback: TtsCallback? = nil
) -> Bool {
print("\(tag): 初始化 Azure 语音服务")
// 检查配置是否为空 // 检查配置是否为空
if speechSubscriptionKey.isEmpty || serviceRegion.isEmpty { if subscriptionKey.isEmpty || region.isEmpty {
print("[AzureTtsHelper] 错误: Azure 配置信息不完整") print("\(tag): 错误: Azure 配置信息不完整")
return false return false
} }
// 释放之前的资源 // 释放之前的资源
dispose() dispose()
// 保存全局回调
ttsCallback = callback
do { do {
// 创建语音配置 // 创建语音配置
speechConfig = try SPXSpeechConfig(subscription: speechSubscriptionKey, region: serviceRegion) speechConfig = try SPXSpeechConfiguration(subscription: subscriptionKey, region: region)
// 设置语音合成输出格式为高质量音频 // 设置语音合成输出格式为高质量音频
speechConfig?.setSpeechSynthesisOutputFormat(.riff24Khz16BitMonoPcm) 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!) synthesizer = try SPXSpeechSynthesizer(speechConfiguration: speechConfig!, audioConfiguration: audioConfig!)
initialized = true
// 设置默认音频输出类型为自动 // 设置事件监听
setAudioOutputType(outputType: .auto) setupEventListeners()
print("[AzureTtsHelper] TTS 引擎初始化成功") initialized = true
print("\(tag): TTS 引擎初始化成功")
return true return true
} catch { } catch {
print("[AzureTtsHelper] TTS 引擎初始化失败: \(error.localizedDescription)") print("\(tag): TTS 引擎初始化失败: \(error.localizedDescription)")
return false return false
} }
} }
/// 设置音频输出设备类型 /// 设置事件监听器
/// private func setupEventListeners() {
/// - Parameter outputType: 音频输出设备类型 guard let synthesizer = synthesizer else { return }
/// - Returns: 是否设置成功
func setAudioOutputType(outputType: AudioOutputType) -> Bool { // 合成开始事件
if !initialized { synthesizer.addSynthesisStarted { [weak self] _, _ in
print("[AzureTtsHelper] TTS 引擎尚未初始化") guard let self = self else { return }
return false 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 { do {
currentAudioOutputType = outputType // 保存引用
customAudioOutputStream = outputStream
switch outputType {
case .speaker: // 如果已初始化,需要重新创建合成器以应用新的音频输出流
// 使用扬声器 if initialized {
try audioSession.setCategory(.playback, mode: .default) recreateSynthesizer()
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] 已设置音频输出设备为听筒")
}
} }
try audioSession.setActive(true)
return true return true
} catch { } catch {
print("[AzureTtsHelper] 设置音频输出设备失败: \(error.localizedDescription)") print("\(tag): 设置自定义音频输出流失败: \(error.localizedDescription)")
return false 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" /// - Parameter voiceName: 语音名称,例如 "zh-CN-XiaoxiaoNeural"
/// - Returns: 是否设置成功 /// - Returns: 是否设置成功
func setVoice(voiceName: String) -> Bool { func setVoice(voiceName: String) -> Bool {
if !initialized { if !initialized {
print("[AzureTtsHelper] TTS 引擎尚未初始化") print("\(tag): TTS 引擎尚未初始化")
return false return false
} }
if voiceName == currentVoiceName { if voiceName == currentVoice {
print("[AzureTtsHelper] 已设置语音: \(voiceName)") print("\(tag): 已设置语音: \(voiceName)")
return true return true
} }
do { do {
currentVoiceName = voiceName currentVoice = voiceName
speechConfig?.setSpeechSynthesisVoiceName(voiceName) speechConfig?.setSpeechSynthesisVoiceName(voiceName)
// 重新创建合成器 // 重新创建合成器
synthesizer = try SPXSpeechSynthesizer(speechConfig: speechConfig!, audioConfig: audioConfig!) recreateSynthesizer()
print("[AzureTtsHelper] 已设置语音: \(voiceName)") print("\(tag): 已设置语音: \(voiceName)")
return true return true
} catch { } catch {
print("[AzureTtsHelper] 设置语音失败: \(error.localizedDescription)") print("\(tag): 设置语音失败: \(error.localizedDescription)")
return false return false
} }
} }
/// 设置语音合成参数 /// 设置语音合成参数
///
/// - Parameters: /// - Parameters:
/// - rate: 语速,范围 -100 到 100,默认为 0 /// - rate: 语速,范围 -100 到 100,默认为 0
/// - pitch: 音调,范围 -100 到 100,默认为 0 /// - pitch: 音调,范围 -100 到 100,默认为 0
@ -171,58 +239,36 @@ class AzureTtsHelper: NSObject {
/// - Returns: 是否设置成功 /// - Returns: 是否设置成功
func setSpeechParams(rate: Int = 0, pitch: Int = 0, volume: Int = 100) -> Bool { func setSpeechParams(rate: Int = 0, pitch: Int = 0, volume: Int = 100) -> Bool {
if !initialized { if !initialized {
print("[AzureTtsHelper] TTS 引擎尚未初始化") print("\(tag): TTS 引擎尚未初始化")
return false 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 { do {
print("[AzureTtsHelper] 开始合成文本: \(text)") currentRate = formatPercentage(rate)
currentPitch = formatPercentage(pitch)
currentVolume = "\(min(max(volume, 0), 100))%"
// 生成 SSML print("\(tag): 已设置语音参数: 语速=\(rate), 音调=\(pitch), 音量=\(volume)")
let ssml = generateSsml(text: text) return true
// 使用 SSML 合成语音
speakSsml(ssml: ssml, completion: completion)
} catch { } catch {
print("[AzureTtsHelper] 语音合成异常: \(error.localizedDescription)") print("\(tag): 设置语音参数失败: \(error.localizedDescription)")
completion(false, "语音合成异常: \(error.localizedDescription)") return false
} }
} }
/// 格式化百分比值
private func formatPercentage(_ value: Int) -> String {
return value >= 0 ? "+\(value)%" : "\(value)%"
}
/// 生成 SSML 文本 /// 生成 SSML 文本
///
/// - Parameter text: 要转换的文本 /// - Parameter text: 要转换的文本
/// - Returns: SSML 格式的文本 /// - Returns: SSML 格式的文本
private func generateSsml(text: String) -> String { 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 """ 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"> <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)"> <voice name="\(currentVoice)">
<prosody rate="\(rateParam)" pitch="\(pitchParam)" volume="\(volumeParam)"> <prosody rate="\(currentRate)" pitch="\(currentPitch)" volume="\(currentVolume)">
\(text) \(text)
</prosody> </prosody>
</voice> </voice>
@ -230,73 +276,133 @@ class AzureTtsHelper: NSObject {
""" """
} }
/// 合成 SSML 为语音并播放 /// 合成文本为语音并播放
/// /// - Parameter text: 要合成的文本
/// - Parameters: /// - Returns: 是否成功开始合成
/// - ssml: SSML 格式的文本 func speakText(_ text: String) -> Bool {
/// - completion: 完成回调,返回是否成功和可能的错误信息
private func speakSsml(ssml: String, completion: @escaping (Bool, String?) -> Void) {
if !initialized { if !initialized {
print("[AzureTtsHelper] TTS 引擎尚未初始化") print("\(tag): TTS 引擎尚未初始化")
completion(false, "TTS 引擎尚未初始化") return false
return
} }
do { do {
print("[AzureTtsHelper] 开始合成 SSML") print("\(tag): 开始合成文本: \(text)")
// 生成 SSML
let ssml = generateSsml(text: text)
// 标记为正在播放 // 标记为正在播放
speaking = true speaking = true
// 激活音频会话
try audioSession.setActive(true)
// 异步合成语音 // 异步合成语音
let result = try synthesizer!.speakSsml(ssml) try synthesizer?.speakSsmlAsync(ssml)
switch result.reason { return true
case .synthesizingAudioCompleted: } catch {
print("[AzureTtsHelper] 语音合成完成") print("\(tag): 语音合成异常: \(error.localizedDescription)")
speaking = false speaking = false
completion(true, "语音合成完成") return false
case .canceled: }
if let cancelDetails = try? SPXSpeechSynthesisCancellationDetails(fromResult: result) { }
print("[AzureTtsHelper] 语音合成取消: \(cancelDetails.errorDetails ?? "未知错误")")
speaking = false /// 处理流式文本并在遇到标点符号时播放
completion(false, "语音合成取消: \(cancelDetails.errorDetails ?? "未知错误")") /// - Parameter text: 收到的文本流片段
} else { /// - Returns: 是否成功处理
print("[AzureTtsHelper] 语音合成取消") func speakStream(_ text: String) -> Bool {
speaking = false if !initialized || text.isEmpty {
completion(false, "语音合成取消") 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 { } catch {
print("[AzureTtsHelper] 语音合成异常: \(error.localizedDescription)") print("\(tag): 流式语音合成失败: \(error.localizedDescription)")
speaking = false return false
completion(false, "语音合成异常: \(error.localizedDescription)") }
}
/// 播放剩余的流式文本
/// - 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: 是否停止成功 /// - Returns: 是否停止成功
func stopSpeaking() -> Bool { func stopSpeaking() -> Bool {
if !initialized { if !initialized {
print("[AzureTtsHelper] TTS 引擎尚未初始化") print("\(tag): TTS 引擎尚未初始化")
return false return false
} }
do { do {
try synthesizer?.stopSpeaking() // 清空流缓冲区
streamBuffer = ""
try synthesizer?.stopSpeakingAsync()
speaking = false speaking = false
print("[AzureTtsHelper] 已停止语音合成") print("\(tag): 已停止语音合成")
return true return true
} catch { } catch {
print("[AzureTtsHelper] 停止语音合成失败: \(error.localizedDescription)") print("\(tag): 停止语音合成失败: \(error.localizedDescription)")
return false return false
} }
} }
@ -309,20 +415,30 @@ class AzureTtsHelper: NSObject {
// 恢复音频会话 // 恢复音频会话
try audioSession.setActive(false, options: .notifyOthersOnDeactivation) try audioSession.setActive(false, options: .notifyOthersOnDeactivation)
// 关闭自定义音频输出流
customAudioOutputStream = nil
// 清空流缓冲区
streamBuffer = ""
// 清理资源
synthesizer = nil synthesizer = nil
speechConfig = nil speechConfig = nil
audioConfig = nil audioConfig = nil
ttsCallback = nil
initialized = false initialized = false
speaking = false speaking = false
print("[AzureTtsHelper] TTS 引擎已释放") print("\(tag): TTS 引擎已释放")
} catch { } catch {
print("[AzureTtsHelper] 释放 TTS 引擎失败: \(error.localizedDescription)") print("\(tag): 释放 TTS 引擎失败: \(error.localizedDescription)")
// 确保重置状态
initialized = false
speaking = false
} }
} }
/// 检查当前是否正在播放语音 /// 检查当前是否正在播放语音
///
/// - Returns: 是否正在播放语音 /// - Returns: 是否正在播放语音
func isSpeaking() -> Bool { func isSpeaking() -> Bool {
return speaking return speaking

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

@ -28,13 +28,7 @@ android {
dependencies { dependencies {
implementation("androidx.core:core-ktx:1.13.1") 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(":deepvoice_utils"))
implementation(project(":agent_service"))
implementation(project(":azure_speech")) implementation(project(":azure_speech"))
implementation(project(":jl_opus")) 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.MEDIA_CONTENT_CONTROL" />
<uses-permission android:name="android.permission.RECEIVE_MEDIA_BUTTON" /> <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> </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 if (isScanning) return false
FileLogger.i(TAG, "开始主动扫描BLE设备...") FileLogger.i(TAG, "开始主动扫描BLE设备...")
unregisterBackgroundScan() // 避免双重扫描
scanResults.clear() scanResults.clear()
val filter = ScanFilter.Builder() val filter = ScanFilter.Builder()
@ -223,8 +222,6 @@ object BleService {
scanHandler.removeCallbacksAndMessages(null) scanHandler.removeCallbacksAndMessages(null)
isScanning = false isScanning = false
// 重新注册后台扫描,保证唤醒能力
registerBackgroundScan()
return true return true
} catch (e: Exception) { } catch (e: Exception) {
@ -1017,74 +1014,7 @@ object BleService {
return bluetoothAdapter 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() val success = BleService.playA2DP()
result.success(success) 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 -> { else -> {
result.notImplemented() result.notImplemented()
} }
@ -254,48 +213,4 @@ class BleServicePlugin : FlutterPlugin, MethodCallHandler, BleService.Callback {
sendEvent(statusEventSink, resultMap, "发送设备信息异常") 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| Pod::Spec.new do |s|
s.name = 'chat_storage' s.name = 'chat_storage'
s.version = '0.0.1' s.version = '0.0.1'
s.summary = 'Chat Storage plugin' s.summary = '聊天记录存储插件'
s.description = <<-DESC s.description = <<-DESC
A Flutter plugin for chat storage services. 用于存储聊天记录的Flutter插件,支持SQLite数据库操作。
DESC DESC
s.homepage = 'http://example.com' s.homepage = 'http://yunqiinnovation.com'
s.license = { :file => '../LICENSE' } s.license = { :file => '../LICENSE' }
s.author = { 'Your Company' => 'email@example.com' } s.author = { 'YunQi Innovation' => 'support@yunqiinnovation.com' }
s.source = { :path => '.' } s.source = { :path => '.' }
s.source_files = 'Classes/**/*' s.source_files = 'Classes/**/*'
s.dependency 'Flutter' s.dependency 'Flutter'
@ -20,4 +20,7 @@ A Flutter plugin for chat storage services.
# Flutter.framework does not contain a i386 slice. # Flutter.framework does not contain a i386 slice.
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' }
s.swift_version = '5.0' s.swift_version = '5.0'
# 添加SQLite库依赖
s.library = 'sqlite3'
end 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: platforms:
android: android:
package: com.yunqiinnovation.classic_bluetooth package: com.yunqiinnovation.classic_bluetooth
pluginClass: ClassicBluetoothPlugin
ios:
pluginClass: ClassicBluetoothPlugin 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 Foundation
import UIKit
/// OpenAI服务异常 /// OpenAI服务异常
public class OpenAIError: Error { 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原生实现 /// OpenAI服务iOS原生实现
public class OpenAIService { public class OpenAIService {
private let TAG = "OpenAIService" private let TAG = "OpenAIService"
@ -27,6 +37,7 @@ public class OpenAIService {
private var apiKey: String = "" private var apiKey: String = ""
private var isInitialized = false private var isInitialized = false
private var model: String = "doubao-1-5-lite-32k-250115" // 默认模型 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]] = [] private var registeredFunctions: [[String: Any]] = []
@ -34,6 +45,17 @@ public class OpenAIService {
// URL会话 // URL会话
private let session: URLSession 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() { public init() {
// 创建URL会话配置 // 创建URL会话配置
let config = URLSessionConfiguration.default let config = URLSessionConfiguration.default
@ -42,18 +64,181 @@ public class OpenAIService {
session = URLSession(configuration: config) 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] { public func createUserMessage(content: String) -> [String: Any] {
return ["role": "user", "content": content] return ["role": "user", "content": content]
} }
/// 创建系统消息
public func createSystemMessage(content: String) -> [String: Any] {
return ["role": "system", "content": content]
}
/// 创建助手消息 /// 创建助手消息
public func createAssistantMessage(content: String) -> [String: Any] { public func createAssistantMessage(content: String) -> [String: Any] {
return ["role": "assistant", "content": content] 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服务 /// 初始化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 self.apiKey = apiKey
if !baseUrl.isEmpty { if !baseUrl.isEmpty {
self.baseUrl = baseUrl self.baseUrl = baseUrl
@ -61,6 +246,12 @@ public class OpenAIService {
if !model.isEmpty { if !model.isEmpty {
self.model = model self.model = model
} }
// 初始化MCP客户端
if !mcpServer.isEmpty {
initializeMcpClient(mcpServer)
}
isInitialized = !apiKey.isEmpty isInitialized = !apiKey.isEmpty
return isInitialized 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 { public func sendMessage(messages: [[String: Any]], systemPrompt: String) throws -> String {
guard isInitialized, !apiKey.isEmpty else { guard isInitialized, !apiKey.isEmpty else {
@ -239,7 +500,7 @@ public class OpenAIService {
/// 发送消息(流式输出) /// 发送消息(流式输出)
public func sendMessageStream( public func sendMessageStream(
messages: [[String: Any]], messages: [[String: Any]],
systemPrompt: String, systemPrompt: String = "",
callback: @escaping StreamCallback callback: @escaping StreamCallback
) { ) {
guard isInitialized, !apiKey.isEmpty else { guard isInitialized, !apiKey.isEmpty else {
@ -247,34 +508,36 @@ public class OpenAIService {
return 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] = [ var requestBody: [String: Any] = [
"model": model, "model": currentModel,
"temperature": 0.7, "temperature": 0.7,
"max_tokens": 2000, "max_tokens": 2000,
"stream": true "stream": true
] ]
// 构建完整消息数组,添加系统提示 // 构建完整消息数组,添加系统提示
var fullMessages: [[String: Any]] = [ var fullMessages: [[String: Any]] = []
["role": "system", "content": systemPrompt] if !systemPrompt.isEmpty {
] fullMessages.append(["role": "system", "content": systemPrompt])
}
fullMessages.append(contentsOf: messages) fullMessages.append(contentsOf: messages)
requestBody["messages"] = fullMessages requestBody["messages"] = fullMessages
// 添加工具列表 // 添加工具列表
if let toolMaps = mcpClient?.getToolMaps(), !toolMaps.isEmpty { if let toolMaps = mcpClient?.getToolMaps(), !toolMaps.isEmpty {
var tools: [[String: Any]] = [] requestBody["tools"] = toolMaps
for toolMap in toolMaps {
if let tool = toolMap as? [String: Any] {
tools.append(tool)
}
}
if !tools.isEmpty {
requestBody["tools"] = tools
}
} else if !registeredFunctions.isEmpty { } else if !registeredFunctions.isEmpty {
var tools: [[String: Any]] = [] var tools: [[String: Any]] = []
@ -311,9 +574,25 @@ public class OpenAIService {
request.httpBody = jsonData request.httpBody = jsonData
// 创建流式会话任务 // 创建流式会话任务
let delegate = SSEStreamDelegate(callback: callback) let delegate = SSEStreamDelegate(
let session = URLSession(configuration: .default, delegate: delegate, delegateQueue: nil) 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) let task = session.dataTask(with: request)
currentStreamTask = task
task.resume() task.resume()
} }
@ -327,10 +606,12 @@ public class OpenAIService {
) { ) {
do { do {
// 构建完整消息数组 // 构建完整消息数组
var fullMessages: [[String: Any]] = [ var fullMessages: [[String: Any]] = []
// 添加系统提示
["role": "system", "content": systemPrompt] // 添加系统提示
] if !systemPrompt.isEmpty {
fullMessages.append(["role": "system", "content": systemPrompt])
}
// 添加用户消息 // 添加用户消息
fullMessages.append(contentsOf: messages) fullMessages.append(contentsOf: messages)
@ -347,7 +628,7 @@ public class OpenAIService {
// 添加函数调用消息 // 添加函数调用消息
fullMessages.append([ fullMessages.append([
"role": "assistant", "role": "assistant",
"content": NSNull(), "content": nil as Any?,
"tool_calls": [ "tool_calls": [
[ [
"id": id, "id": id,
@ -378,11 +659,20 @@ public class OpenAIService {
/// SSE流委托实现 /// SSE流委托实现
private class SSEStreamDelegate: NSObject, URLSessionDataDelegate { private class SSEStreamDelegate: NSObject, URLSessionDataDelegate {
let callback: StreamCallback let callback: StreamCallback
let autoHandleMcpTools: Bool
let autoToolHandler: ([String: Any]) -> Void
private var buffer = Data() private var buffer = Data()
private var finalToolCalls: [Int: ToolCallInfo] = [:] 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.callback = callback
self.autoHandleMcpTools = autoHandleMcpTools
self.autoToolHandler = autoToolHandler
super.init() super.init()
} }
@ -471,7 +761,7 @@ public class OpenAIService {
} }
} }
} catch { } catch {
NSLog("解析JSON出错: \(error.localizedDescription)") print("解析JSON出错: \(error.localizedDescription)")
// 忽略解析错误,继续处理其他行 // 忽略解析错误,继续处理其他行
} }
} }
@ -482,50 +772,45 @@ public class OpenAIService {
if finalToolCalls.isEmpty { return false } if finalToolCalls.isEmpty { return false }
// 只处理第一个工具调用 // 只处理第一个工具调用
guard let firstToolCall = finalToolCalls.values.first, firstToolCall.isValid else { return false } for (_, toolCall) in finalToolCalls {
if toolCall.isValid {
// 创建函数调用字典 // 创建函数调用字典
let functionCall: [String: Any] = [ let functionCall: [String: Any] = [
"name": firstToolCall.name, "name": toolCall.name,
"arguments": firstToolCall.arguments, "arguments": toolCall.arguments,
"id": firstToolCall.id "id": toolCall.id
] ]
// 调用回调
callback.onFunctionCall(functionCall)
// 如果需要自动处理工具调用
if autoHandleMcpTools {
autoToolHandler(functionCall)
}
return true
}
}
// 回调 return false
callback.onFunctionCall(functionCall)
return true
} }
// 处理完成 // 处理完成
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
if let 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 apiKey = args["apiKey"] as? String {
let baseUrl = args["baseUrl"] as? String ?? "" let baseUrl = args["baseUrl"] as? String ?? ""
let model = args["model"] 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) result(initialized)
} else { } else {
result(FlutterError(code: "INVALID_ARGUMENT", message: "初始化参数无效", details: nil)) result(FlutterError(code: "INVALID_ARGUMENT", message: "初始化参数无效", details: nil))
@ -46,12 +47,13 @@ public class OpenAIServicePlugin: NSObject, FlutterPlugin, FlutterStreamHandler
case "sendMessage": case "sendMessage":
guard let args = call.arguments as? [String: Any], guard let args = call.arguments as? [String: Any],
let messagesRaw = args["messages"] as? [[String: Any]], let messagesRaw = args["messages"] as? [[String: Any]] else {
let systemPrompt = args["systemPrompt"] as? String else {
result(FlutterError(code: "INVALID_ARGUMENT", message: "发送消息参数无效", details: nil)) result(FlutterError(code: "INVALID_ARGUMENT", message: "发送消息参数无效", details: nil))
return return
} }
let systemPrompt = args["systemPrompt"] as? String ?? ""
// 在后台线程执行 // 在后台线程执行
DispatchQueue.global(qos: .userInitiated).async { DispatchQueue.global(qos: .userInitiated).async {
do { do {
@ -70,12 +72,13 @@ public class OpenAIServicePlugin: NSObject, FlutterPlugin, FlutterStreamHandler
case "sendMessageStream": case "sendMessageStream":
guard let args = call.arguments as? [String: Any], guard let args = call.arguments as? [String: Any],
let messagesRaw = args["messages"] as? [[String: Any]], let messagesRaw = args["messages"] as? [[String: Any]] else {
let systemPrompt = args["systemPrompt"] as? String else {
result(FlutterError(code: "INVALID_ARGUMENT", message: "发送消息参数无效", details: nil)) result(FlutterError(code: "INVALID_ARGUMENT", message: "发送消息参数无效", details: nil))
return return
} }
let systemPrompt = args["systemPrompt"] as? String ?? ""
// 检查事件接收器 // 检查事件接收器
guard let eventSink = self.eventSink else { guard let eventSink = self.eventSink else {
result(FlutterError(code: "NO_EVENT_SINK", message: "没有可用的事件流接收器", details: nil)) result(FlutterError(code: "NO_EVENT_SINK", message: "没有可用的事件流接收器", details: nil))
@ -106,7 +109,35 @@ public class OpenAIServicePlugin: NSObject, FlutterPlugin, FlutterStreamHandler
onFunctionCall: { functionCall in onFunctionCall: { functionCall in
// 发送函数调用事件 // 发送函数调用事件
DispatchQueue.main.async { 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": case "sendFunctionCallResult":
guard let args = call.arguments as? [String: Any], guard let args = call.arguments as? [String: Any],
let messagesRaw = args["messages"] as? [[String: Any]], let messagesRaw = args["messages"] as? [[String: Any]],
let systemPrompt = args["systemPrompt"] as? String,
let functionCallRaw = args["functionCall"] as? [String: Any], let functionCallRaw = args["functionCall"] as? [String: Any],
let functionResult = args["functionResult"] as? String else { let functionResult = args["functionResult"] as? String else {
result(FlutterError(code: "INVALID_ARGUMENT", message: "发送函数调用结果参数无效", details: nil)) result(FlutterError(code: "INVALID_ARGUMENT", message: "发送函数调用结果参数无效", details: nil))
return return
} }
let systemPrompt = args["systemPrompt"] as? String ?? ""
// 检查事件接收器 // 检查事件接收器
guard let eventSink = self.eventSink else { guard let eventSink = self.eventSink else {
result(FlutterError(code: "NO_EVENT_SINK", message: "没有可用的事件流接收器", details: nil)) result(FlutterError(code: "NO_EVENT_SINK", message: "没有可用的事件流接收器", details: nil))
@ -159,7 +191,35 @@ public class OpenAIServicePlugin: NSObject, FlutterPlugin, FlutterStreamHandler
onFunctionCall: { functionCall in onFunctionCall: { functionCall in
// 发送函数调用事件 // 发送函数调用事件
DispatchQueue.main.async { 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)) 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: default:
result(FlutterMethodNotImplemented) 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