Browse Source

add

newdev_shunjiawei
wolfplus 2 years ago
parent
commit
78ea4618cf
  1. 65
      .cursorrules
  2. 1
      android/app/build.gradle.kts
  3. 456
      android/app/src/main/kotlin/com/yunqiinnovation/deepsound/VoiceInteractionHandler copy.kt.bak
  4. 1
      android/build.gradle.kts
  5. 4
      android/settings.gradle.kts
  6. 5
      lib/core/bindings/initial_binding.dart
  7. 89
      lib/core/utils/jieli_constants.dart
  8. 97
      lib/data/services/function_service.dart
  9. 21
      lib/data/services/open_ai_service_adapter.dart
  10. 14
      lib/data/services/voice_interaction_service.dart
  11. 11
      lib/modules/jieli_test/bindings/jieli_test_binding.dart
  12. 298
      lib/modules/jieli_test/controllers/jieli_test_controller.dart
  13. 494
      lib/modules/jieli_test/views/jieli_test_view.dart
  14. 4
      lib/modules/pairing/controllers/pairing_controller.dart
  15. 22
      lib/modules/settings/views/settings_view.dart
  16. 7
      lib/routes/app_pages.dart
  17. 1
      lib/routes/app_routes.dart
  18. 42
      local_plugins/azure_speech/android/build.gradle.kts
  19. 80
      local_plugins/chat_storage_sqlite/README.md
  20. 60
      local_plugins/chat_storage_sqlite/android/build.gradle.kts
  21. 1
      local_plugins/chat_storage_sqlite/android/settings.gradle.kts
  22. 8
      local_plugins/chat_storage_sqlite/android/src/main/AndroidManifest.xml
  23. 236
      local_plugins/chat_storage_sqlite/android/src/main/kotlin/com/yunqiinnovation/chat_storage_sqlite/ChatStorageSqlitePlugin.kt
  24. 155
      local_plugins/chat_storage_sqlite/lib/chat_storage_sqlite.dart
  25. 30
      local_plugins/chat_storage_sqlite/pubspec.yaml
  26. 144
      local_plugins/jieli/README.md
  27. 55
      local_plugins/jieli/android/build.gradle.kts
  28. 1
      local_plugins/jieli/android/settings.gradle.kts
  29. 22
      local_plugins/jieli/android/src/main/AndroidManifest.xml
  30. 460
      local_plugins/jieli/android/src/main/kotlin/com/yunqiinnovation/jieli/BleService.kt
  31. 269
      local_plugins/jieli/android/src/main/kotlin/com/yunqiinnovation/jieli/JieliPlugin.kt
  32. 188
      local_plugins/jieli/lib/jieli.dart
  33. 23
      local_plugins/jieli/pubspec.yaml
  34. 12
      local_plugins/open_ai_service/android/AndroidManifest.xml
  35. 24
      local_plugins/open_ai_service/android/build.gradle.kts
  36. 496
      local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/MCPClient.kt
  37. 228
      local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/OpenAIService.kt
  38. 65
      local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/OpenAIServicePlugin.kt
  39. 422
      local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/SystemFunctionHandler.kt
  40. 101
      local_plugins/open_ai_service/lib/open_ai_service.dart
  41. 4
      local_plugins/volcano_speech/android/build.gradle.kts
  42. 39
      local_plugins/volcano_speech/android/src/main/kotlin/com/yunqiinnovation/volcano_speech/VolcanoAsrHelper.kt
  43. 57
      local_plugins/volcano_speech/android/src/main/kotlin/com/yunqiinnovation/volcano_speech/VolcanoTtsHelper.kt
  44. 5
      pubspec.yaml
  45. 58
      test.json

65
.cursorrules

@ -7,4 +7,67 @@
7. 尽可能避免打印日志,仅在出错时记录日志
8. 请使用较小的代码量改动,最安全的方法进行代码编写和优化
9. 请尽可能使用中文回复问题
10. 重要:不要过度设计!!
10. 重要:不要过度设计!!
11. 本项目使用 Kotlin DSL 进行开发
# 项目架构概述
DeepSound是一个基于Flutter和GetX构建的多平台语音应用,具有语音合成、语音识别、语音聊天等功能。
## 总体架构
- 采用GetX状态管理框架
- 遵循模块化设计,各功能模块相对独立
- 使用依赖注入实现组件解耦
- 多语言支持,使用GetX国际化方案
## 主要目录结构
- lib/core: 核心组件、工具和通用功能
- lib/data: 数据模型、服务和提供者
- lib/modules: 功能模块,按业务划分
- lib/routes: 路由定义和管理
- local_plugins: 本地插件,提供原生功能支持
# 核心模块说明
## 语音处理
- 火山语音服务: 提供语音识别和合成核心能力
- Azure语音服务: 提供备选的语音识别和合成能力
- 离线TTS支持: 在线语音合成失败时的备选方案
## 聊天功能
- 基于OpenAI的对话功能
- 支持语音输入、文字输入和语音输出
- 支持多种角色和语音类型
## 用户系统
- 支持多种登录方式:Firebase、Google、Facebook
- 用户资料管理和设置
- 主题切换和语言设置
## 蓝牙配对
- 支持蓝牙设备发现和连接
- 提供简化的配对流程
- 设备管理和状态监控
## 音频处理
- 音频录制和播放
- 支持波形显示
- 音量控制和音频质量设置
# 技术栈
- Flutter: UI框架
- GetX: 状态管理和依赖注入
- Firebase: 用户认证和后端服务
- WebSocket: 实时通信
- SQLite: 本地数据存储
- 原生插件: 语音识别、合成和蓝牙功能
# 插件说明
- jieli: 杰理蓝牙设备连接和控制插件
- volcano_speech: 火山语音服务插件,提供语音识别和合成
- azure_speech: 微软Azure语音服务插件
- open_ai_service: OpenAI服务插件,提供AI对话能力
- speech_to_text: 通用语音识别插件
# 环境配置
- 环境变量通过.env文件管理
- 支持不同环境的配置切换
- 敏感信息不应直接硬编码在代码中

1
android/app/build.gradle.kts

@ -108,6 +108,7 @@ dependencies {
implementation(project(":azure_speech"))
implementation(project(":open_ai_service"))
implementation(project(":volcano_speech"))
implementation(project(":chat_storage_sqlite")) // 添加新创建的SQLite聊天存储插件
// 添加OkHttp依赖
implementation("com.squareup.okhttp3:okhttp:4.9.3")

456
android/app/src/main/kotlin/com/yunqiinnovation/deepsound/VoiceInteractionHandler copy.kt.bak

@ -1,456 +0,0 @@
package com.yunqiinnovation.deepsound
import android.content.Context
import org.json.JSONArray
import org.json.JSONObject
import android.util.Log
import com.yunqiinnovation.deepsound.core.utils.FileLogger
import com.yunqiinnovation.azure_speech.AzureAsrHelper
import com.yunqiinnovation.volcano_speech.VolcanoTtsHelper
import com.yunqiinnovation.open_ai_service.OpenAIService
/**
* 语音交互处理器 - 处理语音识别、TTS和AI对话相关逻辑
*/
class VoiceInteractionHandler(
private val context: Context,
private val azureSpeechKey: String,
private val azureSpeechRegion: String,
private val openaiApiKey: String,
private val openaiBaseUrl: String = "",
private val openaiModel: String = "",
private val volcanoSpeechAppId: String,
private val volcanoSpeechAppToken: String
) {
private val TAG = "VoiceInteractionHandler"
// Azure服务
private var azureAsrHelper: AzureAsrHelper? = null
private var volcanoTtsHelper: VolcanoTtsHelper? = null
// OpenAI服务
private val openAIService = OpenAIService()
// 语音功能处理
private val voiceFunctionHandler = VoiceFunctionHandler(openAIService, context)
// 当前用户输入
private var currentUserInput = ""
// 状态
private var isInitialized = false
var isRecognitionActive = false
private set
var isTtsSpeaking = false
private set
var hasSpeechDetected = false
private set
// 回调
private var callback: InteractionCallback? = null
/**
* 初始化
*/
fun initialize(): Boolean {
if (isInitialized) return true
try {
// 初始化Azure ASR
azureAsrHelper = AzureAsrHelper(context).apply {
initialize(azureSpeechKey, azureSpeechRegion)
}
// 初始化Volcano TTS (使用大模型TTS)
volcanoTtsHelper = VolcanoTtsHelper(context).apply {
// 这里需要替换为实际的Volcano SDK初始化参数
// 暂时使用假参数,实际使用时需要替换为真实值
initialize(volcanoSpeechAppId, volcanoSpeechAppToken, "volc.bigasr.sauc.duration")
}
// 初始化OpenAI服务
openAIService.initialize(openaiApiKey, openaiBaseUrl, openaiModel)
// 初始化语音功能处理器
voiceFunctionHandler.initialize()
isInitialized = true
return true
} catch (e: Exception) {
FileLogger.e(TAG, "初始化失败: ${e.message}", e)
return false
}
}
/**
* 设置回调
*/
fun setCallback(callback: InteractionCallback) {
this.callback = callback
}
/**
* 开始语音识别
*/
fun startRecognition() {
if (isRecognitionActive) return
// 检查录音权限
if (!checkRecordAudioPermission()) {
callback?.onError("需要录音权限,请在设置中授予权限")
return
}
isRecognitionActive = true
hasSpeechDetected = false
notifyStateChanged()
try {
azureAsrHelper?.startContinuousRecognition(object : AzureAsrHelper.ContinuousRecognizeCallback {
override fun onRecognizing(recognizing: String, detectedLanguage: String) {
if (recognizing.isNotEmpty()) {
hasSpeechDetected = true
stopTts()
notifyStateChanged()
}
}
override fun onResult(result: String, detectedLanguage: String) {
if (result.isNotEmpty()) {
notifyStateChanged()
processWithOpenAI(result)
}
// 重置状态,继续识别
hasSpeechDetected = false
}
override fun onSessionStarted() {
notifyStateChanged()
}
override fun onSessionStopped() {
isRecognitionActive = false
notifyStateChanged()
}
override fun onCanceled(reason: String, errorDetails: String) {
isRecognitionActive = false
notifyStateChanged()
}
override fun onError(error: String) {
isRecognitionActive = false
callback?.onError("语音识别出错")
notifyStateChanged()
}
override fun onSuccess(message: String) {
// 处理成功事件
}
})
} catch (e: Exception) {
isRecognitionActive = false
FileLogger.e(TAG, "启动语音识别失败: ${e.message}", e)
callback?.onError("启动语音识别失败")
notifyStateChanged()
}
}
/**
* 停止语音识别
*/
fun stopRecognition() {
if (!isRecognitionActive) return
FileLogger.d(TAG, "停止语音识别")
try {
azureAsrHelper?.stopContinuousRecognition(object : AzureAsrHelper.ContinuousRecognizeCallback {
override fun onResult(result: String, detectedLanguage: String) {}
override fun onRecognizing(recognizing: String, detectedLanguage: String) {}
override fun onSessionStarted() {}
override fun onSessionStopped() {
isRecognitionActive = false
FileLogger.d(TAG, "语音识别会话已停止")
notifyStateChanged()
}
override fun onCanceled(reason: String, errorDetails: String) {
isRecognitionActive = false
FileLogger.d(TAG, "语音识别已取消: $reason")
notifyStateChanged()
}
override fun onError(error: String) {
isRecognitionActive = false
FileLogger.e(TAG, "停止语音识别时出错: $error")
notifyStateChanged()
}
override fun onSuccess(message: String) {
isRecognitionActive = false
FileLogger.d(TAG, "语音识别已停止: $message")
notifyStateChanged()
}
})
} catch (e: Exception) {
FileLogger.e(TAG, "停止语音识别异常: ${e.message}", e)
// 确保状态一致性
isRecognitionActive = false
notifyStateChanged()
}
}
/**
* 使用OpenAI处理语音识别结果
*/
private fun processWithOpenAI(text: String) {
// 保存当前用户输入,用于后续同步聊天记录
currentUserInput = text
Thread {
try {
val messages = JSONArray().apply {
put(openAIService.createUserMessage(text))
}
// 创建响应构建器
val responseBuilder = StringBuilder()
openAIService.sendMessageStream(
messages = messages,
callback = object : OpenAIService.StreamCallback {
override fun onToken(token: String) {
// 累加响应内容
responseBuilder.append(token)
}
override fun onComplete() {
// 处理完整响应
val response = responseBuilder.toString()
if (response.isNotEmpty()) {
// 播放AI回复
Log.d(TAG, "AI 回复: $response")
speakAIResponse(response)
// 同步聊天记录到Flutter端
sendChatHistoryUpdate("personal_assistant", text, response)
}
}
override fun onError(e: Exception) {
FileLogger.e(TAG, "AI流式处理出错: ${e.message}", e)
callback?.onError("AI处理出错")
}
override fun onFunctionCall(call: JSONObject) {
FileLogger.d(TAG, "收到函数调用请求: ${call.getString("name")}")
// 使用函数处理器处理函数调用
val handled = voiceFunctionHandler.handleFunctionCall(
functionCall = call,
messages = messages,
callback = object : VoiceFunctionHandler.FunctionCallCallback {
override fun onTokenReceived(token: String) {
responseBuilder.append(token)
}
override fun onComplete() {
val response = responseBuilder.toString()
if (response.isNotEmpty()) {
// 播放AI回复
Log.d(TAG, "AI Function Call 回复: $response")
speakAIResponse(response)
// 同步聊天记录到Flutter端
sendChatHistoryUpdate("personal_assistant", text, response)
}
notifyStateChanged()
}
override fun onError(message: String) {
FileLogger.e(TAG, "函数处理出错: $message")
callback?.onError(message)
}
override fun onFunctionCall(nestedCall: JSONObject) {
FileLogger.d(TAG, "收到嵌套函数调用: ${nestedCall.getString("name")}")
// 不处理嵌套函数调用,直接返回错误提示
// speakAIResponse("抱歉,暂不支持嵌套函数调用")
}
override fun onExitWithMessage(farewell: String) {
// 播放退出消息
speakAIResponse(farewell)
// 同步聊天记录
sendChatHistoryUpdate("personal_assistant", text, farewell)
// 停止语音识别
stopRecognition()
}
}
)
if (!handled) {
// 如果函数没有被处理,作为普通文本处理
FileLogger.d(TAG, "函数未处理,作为普通文本处理")
speakAIResponse("我无法处理这个请求")
sendChatHistoryUpdate("personal_assistant", text, "我无法处理这个请求")
}
}
}
)
} catch (e: Exception) {
FileLogger.e(TAG, "AI处理出错: ${e.message}", e)
callback?.onError("AI处理出错")
}
}.start()
}
/**
* 播放TTS
*/
fun playTts(text: String, callback: TtsCallback? = null) {
isTtsSpeaking = true
notifyStateChanged()
volcanoTtsHelper?.speak(text, object : VolcanoTtsHelper.TTSCallback {
override fun onStart(reqId: String) {
// TTS开始播放事件
}
override fun onProgress(reqId: String, progress: Double) {
// TTS播放进度事件
}
override fun onComplete(reqId: String) {
isTtsSpeaking = false
notifyStateChanged()
callback?.onComplete()
}
override fun onError(reqId: String, errorCode: Int, errorMsg: String) {
isTtsSpeaking = false
notifyStateChanged()
callback?.onError(errorMsg)
}
})
}
/**
* 停止TTS播放
*/
fun stopTts() {
if (isTtsSpeaking) {
volcanoTtsHelper?.stop()
isTtsSpeaking = false
notifyStateChanged()
}
}
/**
* 播放AI回复
*/
private fun speakAIResponse(text: String) {
isTtsSpeaking = true
notifyStateChanged()
volcanoTtsHelper?.speak(text, object : VolcanoTtsHelper.TTSCallback {
override fun onStart(reqId: String) {
// TTS开始播放事件
}
override fun onProgress(reqId: String, progress: Double) {
// TTS播放进度事件
}
override fun onComplete(reqId: String) {
isTtsSpeaking = false
notifyStateChanged()
}
override fun onError(reqId: String, errorCode: Int, errorMsg: String) {
isTtsSpeaking = false
notifyStateChanged()
}
})
}
/**
* 释放资源
*/
fun dispose() {
// 停止语音识别
stopRecognition()
// 停止TTS播放
stopTts()
// 释放Azure资源
azureAsrHelper?.let {
FileLogger.d(TAG, "关闭Azure ASR服务")
it.dispose()
}
volcanoTtsHelper?.let {
FileLogger.d(TAG, "关闭Volcano TTS服务")
it.release()
}
FileLogger.d(TAG, "语音交互处理器资源已释放")
}
/**
* 检查录音权限
*/
private fun checkRecordAudioPermission(): Boolean {
val permission = android.Manifest.permission.RECORD_AUDIO
val result = context.checkCallingOrSelfPermission(permission)
return result == android.content.pm.PackageManager.PERMISSION_GRANTED
}
/**
* 通知状态变化
*/
private fun notifyStateChanged() {
callback?.onStateChanged(isRecognitionActive, isTtsSpeaking, hasSpeechDetected)
}
/**
* 发送聊天历史更新
*/
private fun sendChatHistoryUpdate(agentId: String, userMessage: String, assistantMessage: String) {
val intent = android.content.Intent(VoiceInteractionService.ACTION_CHAT_HISTORY_UPDATED).apply {
putExtra("agentId", agentId)
putExtra("userMessage", userMessage)
putExtra("assistantMessage", assistantMessage)
putExtra("timestamp", System.currentTimeMillis())
}
// 发送广播
context.sendBroadcast(intent)
}
/**
* 交互回调接口
*/
interface InteractionCallback {
fun onStateChanged(isRecognitionActive: Boolean, isTtsSpeaking: Boolean, hasSpeechDetected: Boolean)
fun onError(message: String)
fun onPromptRequest(message: String)
}
/**
* TTS回调接口
*/
interface TtsCallback {
fun onComplete()
fun onError(error: String)
}
}

1
android/build.gradle.kts

@ -1,5 +1,6 @@
allprojects {
repositories {
mavenLocal()
google()
mavenCentral()
maven { url = uri("https://storage.googleapis.com/download.flutter.io") }

4
android/settings.gradle.kts

@ -16,6 +16,7 @@ pluginManagement {
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
mavenLocal()
google()
mavenCentral()
gradlePluginPortal()
@ -25,6 +26,7 @@ pluginManagement {
plugins {
id("dev.flutter.flutter-plugin-loader")
id("com.android.application") version "8.7.0" apply false
id("com.android.library") version "8.7.0" apply false
id("org.jetbrains.kotlin.android") version "2.1.10" apply false
}
@ -32,8 +34,10 @@ include(":app")
include(":azure_speech")
include(":open_ai_service")
include(":volcano_speech")
include(":chat_storage_sqlite")
// 设置azure_speech项目的路径
project(":azure_speech").projectDir = file("../local_plugins/azure_speech/android")
project(":open_ai_service").projectDir = file("../local_plugins/open_ai_service/android")
project(":volcano_speech").projectDir = file("../local_plugins/volcano_speech/android")
project(":chat_storage_sqlite").projectDir = file("../local_plugins/chat_storage_sqlite/android")

5
lib/core/bindings/initial_binding.dart

@ -9,7 +9,6 @@ import '../../data/services/bluetooth_media_button_service.dart';
import '../../core/utils/logger.dart';
import '../../data/services/voice_interaction_service.dart';
import '../../data/services/open_ai_service_adapter.dart';
/// 初始绑定,用于管理全局依赖
class InitialBinding extends Bindings {
@override
@ -48,11 +47,15 @@ class InitialBinding extends Bindings {
Get.lazyPut<BluetoothMediaButtonService>(
() => BluetoothMediaButtonService(),
fenix: true);
// 语音交互服务
Get.lazyPut<VoiceInteractionService>(() => VoiceInteractionService(),
fenix: true);
// 杰理蓝牙服务
// // 语言管理器(需要最先初始化)
// Get.put<LoginService>(LoginService(), permanent: true);
// await LoginService().FirebaseInit();

89
lib/core/utils/jieli_constants.dart

@ -0,0 +1,89 @@
/// 杰里(Jieli)设备相关常量定义
/// 用于标准化系统属性类型和功能码
/// 系统属性类型常量
class JieliAttrType {
// 公共属性
static const int VOLUME = 1; // 音量
static const int PLAYING_TIME = 2; // 播放时间
static const int BATTERY_LEVEL = 3; // 电池电量
static const int WORK_MODE = 4; // 工作模式
static const int PLAYBACK_MODE = 5; // 播放模式(循环模式)
static const int EQ_MODE = 6; // EQ模式
static const int LANGUAGE = 7; // 语言设置
static const int CONTRAST = 8; // 对比度
static const int LED_ONOFF = 9; // LED开关
static const int POWER_OFF_TIME = 10; // 关机时间
static const int LIGHT_ONOFF = 11; // 背光开关
static const int BLETYPE_SETTING = 12; // BLE类型设置
static const int TIME_OUT = 13; // 超时设置
static const int DEVICE_NAME = 14; // 设备名称
static const int ALARM_ENABLE = 15; // 闹钟开关
static const int DEV_MAC = 16; // 设备MAC地址
// 音乐相关属性
static const int MUSIC_STATUS = 20; // 音乐状态
static const int MUSIC_CURR_DEVICE = 21; // 当前音乐设备
static const int MUSIC_TOTAL_TIME = 22; // 音乐总时长
static const int MUSIC_CURR_PLAY_TIME = 23; // 当前播放时间
static const int MUSIC_NUM = 24; // 音乐数量
static const int MUSIC_DEV_STATUS = 25; // 设备存储状态
static const int MUSIC_BREAK_POINT = 26; // 断点信息
static const int MUSIC_CURR_FILENUM = 27; // 当前文件编号
static const int MUSIC_TOTAL_FILENUM = 28; // 总文件数
static const int MEDIA_TIMESTAMP = 29; // 媒体时间戳
// 模式相关
static const int CUR_MODE = 30; // 当前模式
// TWS相关
static const int TWS_CONN_STATUS = 60; // TWS连接状态
static const int LEFT_CHN_BAT = 61; // 左耳电量
static const int RIGHT_CHN_BAT = 62; // 右耳电量
static const int TWS_STATUS = 63; // TWS状态
}
/// 功能码
class JieliFunctionCode {
static const int COMMON = 0; // 公共功能
static const int MUSIC = 1; // 音乐功能
static const int RADIO = 2; // 收音机功能
static const int LINEIN = 3; // 线路输入功能
static const int RTC = 4; // 时钟功能
static const int BT = 5; // 蓝牙功能
static const int PC = 6; // PC功能
static const int RECORD = 7; // 录音功能
static const int UDISK = 8; // U盘功能
static const int TALK = 9; // 通话功能
static const int PHOTO = 10; // 拍照功能
static const int VIDEO = 11; // 视频功能
}
/// 音乐播放状态
class JieliMusicStatus {
static const int STOP = 0; // 停止状态
static const int PLAY = 1; // 播放状态
static const int PAUSE = 2; // 暂停状态
static const int FAST_FORWARD = 3; // 快进状态
static const int FAST_BACKWARD = 4; // 快退状态
}
/// 循环播放模式
class JieliPlaybackMode {
static const int ALL = 0; // 全部循环
static const int FOLDER = 1; // 文件夹循环
static const int ONE = 2; // 单曲循环
static const int RANDOM = 3; // 随机播放
static const int BROWSE = 4; // 浏览播放
}
/// EQ模式
class JieliEQMode {
static const int NORMAL = 0; // 正常
static const int ROCK = 1; // 摇滚
static const int POP = 2; // 流行
static const int CLASSIC = 3; // 经典
static const int JAZZ = 4; // 爵士
static const int COUNTRY = 5; // 乡村
static const int CUSTOM = 6; // 自定义
}

97
lib/data/services/function_service.dart

@ -1,97 +0,0 @@
import 'dart:convert';
/// 函数服务
/// 仅实现各种功能函数,不包含AI交互逻辑
class FunctionService {
/// 天气查询工具定义
/// 返回一个可直接用于tools参数的天气查询工具定义
static Map<String, dynamic> getWeatherToolDefinition() {
return {
'type': 'function',
'function': {
'name': 'get_current_weather',
'description': '获取指定地点的当前天气信息',
'parameters': {
'type': 'object',
'properties': {
'location': {
'type': 'string',
'description': '地理位置,例如:北京市',
},
'unit': {
'type': 'string',
'description': '温度单位',
'enum': ['celsius', 'fahrenheit'],
},
},
'required': ['location'],
},
},
};
}
/// 模拟天气API实现
/// 返回一个模拟的天气信息,实际应用中应该调用真实的天气API
static Future<Map<String, dynamic>> getWeatherInfo(Map<String, dynamic> args) async {
final location = args['location'] ?? '未知地点';
final unit = args['unit'] ?? 'celsius';
// 在实际应用中,这里应该调用真实的天气API
return {
'location': location,
'temperature': unit == 'celsius' ? '25°C' : '77°F',
'conditions': '晴转多云',
'humidity': '55%',
'wind': '西南风 3级',
'date': DateTime.now().toString().split(' ')[0],
};
}
/// 解析函数调用参数
/// 从AI响应中提取函数调用参数
static Map<String, dynamic> parseFunctionCallArguments(Map<String, dynamic> functionCallResponse) {
if (functionCallResponse['tool_calls'] == null ||
functionCallResponse['tool_calls'].isEmpty) {
throw FunctionServiceException('No tool calls found in response');
}
final toolCall = functionCallResponse['tool_calls'][0];
if (toolCall['function'] == null || toolCall['function']['arguments'] == null) {
throw FunctionServiceException('Invalid function call format');
}
try {
return json.decode(toolCall['function']['arguments']);
} catch (e) {
throw FunctionServiceException('Failed to parse function arguments: $e');
}
}
/// 获取函数调用ID和名称
/// 用于构建工具响应消息
static Map<String, String> getFunctionCallInfo(Map<String, dynamic> functionCallResponse) {
if (functionCallResponse['tool_calls'] == null ||
functionCallResponse['tool_calls'].isEmpty) {
throw FunctionServiceException('No tool calls found in response');
}
final toolCall = functionCallResponse['tool_calls'][0];
if (toolCall['id'] == null || toolCall['function'] == null || toolCall['function']['name'] == null) {
throw FunctionServiceException('Invalid function call format');
}
return {
'id': toolCall['id'],
'name': toolCall['function']['name'],
};
}
}
/// 函数调用服务异常类
class FunctionServiceException implements Exception {
final String message;
FunctionServiceException(this.message);
@override
String toString() => message;
}

21
lib/data/services/open_ai_service_adapter.dart

@ -41,25 +41,14 @@ class OpenAIServiceAdapter implements AiService {
_isProcessingStream = false;
break;
case OpenAIEventType.error:
if (event.content is String) {
_tokenStreamController.addError(event.content as String);
} else {
_tokenStreamController.addError('未知错误: ${event.content}');
}
_tokenStreamController.addError('未知错误: ${event.content}');
_isProcessingStream = false;
break;
case OpenAIEventType.functionCall:
try {
if (event.content is Map) {
_tokenStreamController.addError('收到函数调用,该流仅支持文本响应');
} else {
_tokenStreamController.addError('收到未知格式的函数调用');
printError(info: '函数调用格式错误: ${event.content}');
}
} catch (e) {
printError(info: '处理函数调用事件出错: $e');
_tokenStreamController.addError('处理函数调用失败: $e');
}
_tokenStreamController.addError('收到函数调用,该流仅支持文本响应');
_isProcessingStream = false;
break;
}

14
lib/data/services/voice_interaction_service.dart

@ -42,6 +42,8 @@ class VoiceInteractionService extends GetxService {
late String _openaiApiKey;
late String _openaiBaseUrl;
late String _openaiModel;
late String _volcanoSpeechAppId;
late String _volcanoSpeechAppToken;
// 聊天历史服务
late final ChatHistoryService _chatHistoryService;
@ -62,6 +64,8 @@ class VoiceInteractionService extends GetxService {
_openaiApiKey = dotenv.env['OPENAI_API_KEY'] ?? '';
_openaiBaseUrl = dotenv.env['OPENAI_BASE_URL'] ?? '';
_openaiModel = dotenv.env['OPENAI_MODEL'] ?? '';
_volcanoSpeechAppId = dotenv.env['VOLCANO_SPEECH_APP_ID'] ?? '';
_volcanoSpeechAppToken = dotenv.env['VOLCANO_SPEECH_APP_TOKEN'] ?? '';
if (_azureSpeechKey.isEmpty || _azureSpeechRegion.isEmpty) {
Logger.warning('未找到 Azure 语音服务配置。请在 .env 文件中设置 AZURE_SPEECH_KEY 和 AZURE_SPEECH_REGION');
@ -111,11 +115,7 @@ class VoiceInteractionService extends GetxService {
Logger.info('收到进入翻译模式事件,正在导航到翻译界面');
_navigateToTranslation();
final translationModeEvent = GenericVoiceInteractionEvent(
type: 'enter_translation_mode',
timestamp: DateTime.now().millisecondsSinceEpoch,
);
_eventStreamController.add(translationModeEvent);
break;
}
}
@ -124,7 +124,7 @@ class VoiceInteractionService extends GetxService {
void _navigateToTranslation() {
try {
// 使用GetX导航到翻译页面
Get.toNamed(Routes.translation);
Get.offNamedUntil(Routes.translation, (route) => route.settings.name == Routes.home);
Logger.info('已导航到翻译界面');
} catch (e) {
Logger.error('导航到翻译界面失败: $e');
@ -222,6 +222,8 @@ class VoiceInteractionService extends GetxService {
'openai_api_key': _openaiApiKey,
'openai_base_url': _openaiBaseUrl,
'openai_model': _openaiModel,
'volcano_speech_app_id': _volcanoSpeechAppId,
'volcano_speech_app_token': _volcanoSpeechAppToken,
}) ?? false;
if (result) {

11
lib/modules/jieli_test/bindings/jieli_test_binding.dart

@ -0,0 +1,11 @@
import 'package:get/get.dart';
import '../controllers/jieli_test_controller.dart';
class JieliTestBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<JieliTestController>(
() => JieliTestController(),
);
}
}

298
lib/modules/jieli_test/controllers/jieli_test_controller.dart

@ -0,0 +1,298 @@
import 'dart:async';
import 'dart:typed_data';
import 'package:get/get.dart';
import 'package:logger/logger.dart';
import 'package:jieli/jieli.dart';
/// 杰理测试控制器
class JieliTestController extends GetxController {
static final Logger logger = Logger();
// Jieli插件实例
late Jieli jieli;
// 状态变量
var isScanning = false.obs;
var deviceInfo = '未获取'.obs;
var connectStatus = '未连接'.obs;
var currentDevice = Rx<BleDevice?>(null);
var scannedDevices = RxList<BleDevice>([]);
var wakeupReceived = false.obs;
var audioDataSize = 0.obs;
var receivedFrames = 0.obs;
var audioDataReceiving = false.obs;
// 事件订阅
StreamSubscription? _scanSubscription;
StreamSubscription? _connectionSubscription;
StreamSubscription? _wakeupSubscription;
StreamSubscription? _audioSubscription;
@override
void onInit() {
super.onInit();
logger.d('JieliTestController 初始化');
_initializeJieli();
}
@override
void onClose() {
logger.d('JieliTestController 关闭');
_cancelAllSubscriptions();
_disposeJieli();
super.onClose();
}
/// 取消所有订阅
void _cancelAllSubscriptions() {
_scanSubscription?.cancel();
_connectionSubscription?.cancel();
_wakeupSubscription?.cancel();
_audioSubscription?.cancel();
}
/// 初始化杰理SDK
void _initializeJieli() {
try {
logger.d('初始化Jieli插件');
jieli = Jieli();
_setupEventListeners();
} catch (e) {
logger.e('初始化Jieli插件错误', error: e);
}
}
/// 设置事件监听器
void _setupEventListeners() {
// 监听扫描结果
_scanSubscription = jieli.scanResults.listen((devices) {
// logger.d('扫描到设备: ${devices.length}个');
scannedDevices.assignAll(devices);
});
// 监听连接状态
_connectionSubscription = jieli.connectionState.listen((state) {
logger.d('连接状态变化: $state');
switch (state) {
case ConnectionState.connected:
connectStatus.value = '已连接';
break;
case ConnectionState.disconnected:
connectStatus.value = '已断开';
currentDevice.value = null;
break;
case ConnectionState.connecting:
connectStatus.value = '连接中...';
break;
case ConnectionState.disconnecting:
connectStatus.value = '断开中...';
break;
case ConnectionState.error:
connectStatus.value = '连接错误';
break;
}
});
// 监听唤醒信号
_wakeupSubscription = jieli.wakeupStream.listen((wakeup) {
logger.d('收到唤醒信号');
wakeupReceived.value = true;
Get.snackbar('收到唤醒信号', '耳机发送了唤醒信号');
});
// 监听音频数据
_audioSubscription = jieli.audioDataStream.listen((data) {
// 更新音频数据统计
audioDataSize.value += data.length;
receivedFrames.value++;
audioDataReceiving.value = true;
// 每100帧更新一次日志,避免日志过多
if (receivedFrames.value % 100 == 0) {
logger.d('已接收音频数据: ${audioDataSize.value} 字节, ${receivedFrames.value} 帧');
}
});
}
/// 检查蓝牙状态
Future<bool> checkBluetoothStatus() async {
try {
final isEnabled = await jieli.isBluetoothEnabled();
if (!isEnabled) {
logger.d('蓝牙未启用,请求启用蓝牙');
return await jieli.requestBluetoothEnable();
}
return true;
} catch (e) {
logger.e('检查蓝牙状态错误', error: e);
return false;
}
}
/// 开始扫描设备
Future<void> startScan() async {
if (isScanning.value) return;
final bluetoothReady = await checkBluetoothStatus();
if (!bluetoothReady) {
Get.snackbar('蓝牙未启用', '请启用蓝牙后再试');
return;
}
try {
logger.d('开始扫描设备');
isScanning.value = true;
// 清空之前的设备列表
scannedDevices.clear();
// 开始扫描,10秒后自动停止
final success = await jieli.startScan(scanTimeout: 10000);
if (!success) {
logger.e('启动扫描失败');
isScanning.value = false;
Get.snackbar('扫描失败', '启动扫描设备失败,请检查蓝牙权限');
}
// 10秒后自动更新UI状态
Future.delayed(Duration(milliseconds: 10000), () {
isScanning.value = false;
});
} catch (e) {
logger.e('扫描设备错误', error: e);
isScanning.value = false;
Get.snackbar('扫描错误', e.toString());
}
}
/// 停止扫描
Future<void> stopScan() async {
if (!isScanning.value) return;
try {
logger.d('停止扫描');
await jieli.stopScan();
isScanning.value = false;
} catch (e) {
logger.e('停止扫描错误', error: e);
}
}
/// 连接到指定设备
Future<void> connectToDevice(BleDevice device) async {
try {
// 先断开当前连接
if (currentDevice.value != null) {
await disconnectDevice();
}
// 更新UI状态
connectStatus.value = '连接中...';
logger.d('连接设备: ${device.name} (${device.id})');
// 重置音频和唤醒相关的状态
wakeupReceived.value = false;
audioDataSize.value = 0;
receivedFrames.value = 0;
audioDataReceiving.value = false;
// 连接设备
final success = await jieli.connect(device.id);
if (success) {
// 连接成功,更新当前设备
currentDevice.value = device;
deviceInfo.value = '设备名称: ${device.name}\n设备ID: ${device.id}\nRSSI: ${device.rssi} dBm';
logger.d('设备连接成功');
} else {
deviceInfo.value = '连接失败';
logger.d('设备连接失败');
}
} catch (e) {
deviceInfo.value = '连接错误: ${e.toString()}';
logger.e('连接设备错误', error: e);
}
}
/// 断开设备连接
Future<void> disconnectDevice() async {
if (currentDevice.value == null) return;
try {
connectStatus.value = '断开中...';
logger.d('断开设备连接');
final success = await jieli.disconnect();
if (success) {
deviceInfo.value = '未连接设备';
logger.d('设备断开连接成功');
} else {
logger.d('设备断开连接失败');
}
} catch (e) {
logger.e('断开设备错误', error: e);
}
}
/// 发送开始录音命令
Future<void> sendStartRecordingCommand() async {
if (currentDevice.value == null) {
Get.snackbar('操作失败', '未连接设备');
return;
}
try {
logger.d('发送开始录音命令');
final success = await jieli.sendStartRecordingCommand();
if (success) {
Get.snackbar('命令已发送', '已发送开始录音命令');
logger.d('开始录音命令发送成功');
} else {
Get.snackbar('命令发送失败', '发送开始录音命令失败');
logger.d('开始录音命令发送失败');
}
} catch (e) {
logger.e('发送开始录音命令错误', error: e);
Get.snackbar('命令错误', e.toString());
}
}
/// 发送停止录音命令
Future<void> sendStopRecordingCommand() async {
if (currentDevice.value == null) {
Get.snackbar('操作失败', '未连接设备');
return;
}
try {
logger.d('发送停止录音命令');
final success = await jieli.sendStopRecordingCommand();
if (success) {
Get.snackbar('命令已发送', '已发送停止录音命令');
logger.d('停止录音命令发送成功');
} else {
Get.snackbar('命令发送失败', '发送停止录音命令失败');
logger.d('停止录音命令发送失败');
}
} catch (e) {
logger.e('发送停止录音命令错误', error: e);
Get.snackbar('命令错误', e.toString());
}
}
/// 清理资源
void _disposeJieli() {
try {
logger.d('释放Jieli插件资源');
jieli.dispose();
} catch (e) {
logger.e('释放Jieli插件资源错误', error: e);
}
}
}

494
lib/modules/jieli_test/views/jieli_test_view.dart

@ -0,0 +1,494 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../controllers/jieli_test_controller.dart';
import 'package:jieli/jieli.dart';
class JieliTestView extends GetView<JieliTestController> {
const JieliTestView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
return Scaffold(
backgroundColor: isDarkMode ? Colors.black : Colors.grey[50],
appBar: AppBar(
backgroundColor: isDarkMode ? Colors.black : Colors.white,
elevation: 0.5,
title: Text(
'杰理蓝牙测试',
style: TextStyle(
fontSize: 17.sp,
fontWeight: FontWeight.w600,
color: isDarkMode ? Colors.white : Colors.black,
),
),
centerTitle: true,
leading: IconButton(
icon: Icon(
Icons.arrow_back_ios,
color: isDarkMode ? Colors.white : Colors.black,
size: 20.sp,
),
onPressed: () => Get.back(),
),
),
body: Padding(
padding: EdgeInsets.all(16.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_buildStatusCard(isDarkMode),
SizedBox(height: 16.h),
_buildActionButtons(isDarkMode),
SizedBox(height: 16.h),
_buildDeviceInfo(isDarkMode),
SizedBox(height: 16.h),
_buildDevicesList(isDarkMode),
],
),
),
);
}
Widget _buildStatusCard(bool isDarkMode) {
return Container(
padding: EdgeInsets.all(16.w),
decoration: BoxDecoration(
color: isDarkMode ? Colors.grey[900] : Colors.white,
borderRadius: BorderRadius.circular(12.r),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
'连接状态: ',
style: TextStyle(
fontSize: 14.sp,
color: isDarkMode ? Colors.white : Colors.black87,
),
),
Obx(() => Text(
controller.connectStatus.value,
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.bold,
color: _getStatusColor(controller.connectStatus.value),
),
)),
],
),
SizedBox(height: 8.h),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Obx(() => _buildStatusChip(
label: '唤醒信号',
isActive: controller.wakeupReceived.value,
activeColor: Colors.orange,
isDarkMode: isDarkMode,
)),
Obx(() => _buildStatusChip(
label: '音频数据',
isActive: controller.audioDataReceiving.value,
activeColor: Colors.green,
isDarkMode: isDarkMode,
)),
],
),
SizedBox(height: 8.h),
Obx(() => controller.audioDataReceiving.value
? Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'音频数据统计:',
style: TextStyle(
fontSize: 12.sp,
color: isDarkMode ? Colors.white70 : Colors.black54,
),
),
SizedBox(height: 4.h),
Text(
'已接收 ${(controller.audioDataSize.value / 1024).toStringAsFixed(2)} KB',
style: TextStyle(
fontSize: 12.sp,
color: isDarkMode ? Colors.white70 : Colors.black54,
),
),
Text(
'共 ${controller.receivedFrames.value} 帧',
style: TextStyle(
fontSize: 12.sp,
color: isDarkMode ? Colors.white70 : Colors.black54,
),
),
],
)
: SizedBox.shrink()
),
],
),
);
}
Widget _buildStatusChip({
required String label,
required bool isActive,
required Color activeColor,
required bool isDarkMode,
}) {
return Container(
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 6.h),
decoration: BoxDecoration(
color: isActive
? activeColor
: (isDarkMode ? Colors.grey[800] : Colors.grey[200]),
borderRadius: BorderRadius.circular(16.r),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 8.w,
height: 8.w,
decoration: BoxDecoration(
color: isActive ? Colors.white : Colors.grey,
shape: BoxShape.circle,
),
),
SizedBox(width: 6.w),
Text(
label,
style: TextStyle(
fontSize: 12.sp,
color: isActive
? Colors.white
: (isDarkMode ? Colors.white70 : Colors.black54),
fontWeight: isActive ? FontWeight.bold : FontWeight.normal,
),
),
],
),
);
}
Widget _buildActionButtons(bool isDarkMode) {
return Column(
children: [
Row(
children: [
Expanded(
child: Obx(() => ElevatedButton.icon(
onPressed: controller.isScanning.value
? controller.stopScan
: controller.startScan,
icon: Icon(
controller.isScanning.value ? Icons.stop : Icons.bluetooth_searching,
size: 18.sp,
),
label: Text(
controller.isScanning.value ? '停止扫描' : '扫描设备',
style: TextStyle(fontSize: 14.sp),
),
style: ElevatedButton.styleFrom(
backgroundColor: controller.isScanning.value
? Colors.red[600]
: Colors.blue[600],
padding: EdgeInsets.symmetric(vertical: 12.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.r),
),
),
)),
),
],
),
SizedBox(height: 8.h),
Row(
children: [
Expanded(
child: ElevatedButton.icon(
onPressed: controller.sendStartRecordingCommand,
icon: Icon(
Icons.mic,
size: 18.sp,
),
label: Text(
'开始录音',
style: TextStyle(fontSize: 14.sp),
),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green[600],
padding: EdgeInsets.symmetric(vertical: 12.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.r),
),
),
),
),
SizedBox(width: 8.w),
Expanded(
child: ElevatedButton.icon(
onPressed: controller.sendStopRecordingCommand,
icon: Icon(
Icons.stop_circle,
size: 18.sp,
),
label: Text(
'停止录音',
style: TextStyle(fontSize: 14.sp),
),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red[600],
padding: EdgeInsets.symmetric(vertical: 12.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.r),
),
),
),
),
],
),
],
);
}
Widget _buildDeviceInfo(bool isDarkMode) {
return Container(
padding: EdgeInsets.all(16.w),
decoration: BoxDecoration(
color: isDarkMode ? Colors.grey[900] : Colors.white,
borderRadius: BorderRadius.circular(12.r),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'设备信息:',
style: TextStyle(
fontSize: 14.sp,
color: isDarkMode ? Colors.white : Colors.black87,
),
),
SizedBox(height: 8.h),
Container(
padding: EdgeInsets.all(8.w),
decoration: BoxDecoration(
color: isDarkMode ? Colors.black : Colors.grey[100],
borderRadius: BorderRadius.circular(8.r),
),
child: Obx(() => Text(
controller.deviceInfo.value,
style: TextStyle(
fontSize: 13.sp,
color: isDarkMode ? Colors.white70 : Colors.black54,
height: 1.4,
),
)),
),
SizedBox(height: 16.h),
Obx(() => controller.currentDevice.value != null
? ElevatedButton.icon(
onPressed: controller.disconnectDevice,
icon: Icon(
Icons.bluetooth_disabled,
size: 16.sp,
),
label: Text(
'断开连接',
style: TextStyle(fontSize: 13.sp),
),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red[600],
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 10.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.r),
),
),
)
: SizedBox.shrink()
),
],
),
);
}
Widget _buildDevicesList(bool isDarkMode) {
return Expanded(
child: Container(
decoration: BoxDecoration(
color: isDarkMode ? Colors.grey[900] : Colors.white,
borderRadius: BorderRadius.circular(12.r),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: EdgeInsets.all(16.w),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'扫描到的设备',
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
color: isDarkMode ? Colors.white : Colors.black87,
),
),
Obx(() => Text(
'${controller.scannedDevices.length}个设备',
style: TextStyle(
fontSize: 12.sp,
color: isDarkMode ? Colors.white70 : Colors.black54,
),
)),
],
),
),
Divider(height: 1, thickness: 1, color: isDarkMode ? Colors.grey[800] : Colors.grey[200]),
Expanded(
child: Obx(() => controller.scannedDevices.isEmpty
? Center(
child: controller.isScanning.value
? Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(),
SizedBox(height: 16.h),
Text(
'正在扫描...',
style: TextStyle(
fontSize: 14.sp,
color: isDarkMode ? Colors.white70 : Colors.black54,
),
),
],
)
: Text(
'未扫描到设备',
style: TextStyle(
fontSize: 14.sp,
color: isDarkMode ? Colors.white70 : Colors.black54,
),
),
)
: ListView.separated(
padding: EdgeInsets.symmetric(vertical: 8.h),
itemCount: controller.scannedDevices.length,
separatorBuilder: (context, index) => Divider(
height: 1,
thickness: 1,
color: isDarkMode ? Colors.grey[800] : Colors.grey[200],
),
itemBuilder: (context, index) {
final device = controller.scannedDevices[index];
return ListTile(
dense: true,
title: Text(
device.name.isEmpty ? '未知设备' : device.name,
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.w500,
color: isDarkMode ? Colors.white : Colors.black87,
),
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'设备ID: ${device.id}',
style: TextStyle(
fontSize: 11.sp,
color: isDarkMode ? Colors.white70 : Colors.black54,
),
),
Text(
'RSSI: ${device.rssi} dBm',
style: TextStyle(
fontSize: 11.sp,
color: isDarkMode ? Colors.white70 : Colors.black54,
),
),
],
),
trailing: Obx(() => controller.currentDevice.value?.id == device.id
? Chip(
label: Text(
'已连接',
style: TextStyle(
fontSize: 12.sp,
color: Colors.white,
),
),
backgroundColor: Colors.green,
padding: EdgeInsets.symmetric(horizontal: 8.w),
)
: ElevatedButton(
onPressed: () => controller.connectToDevice(device),
child: Text(
'连接',
style: TextStyle(fontSize: 12.sp),
),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green[600],
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 6.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(6.r),
),
),
),
),
contentPadding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h),
isThreeLine: true,
);
},
),
),
),
],
),
),
);
}
Color _getStatusColor(String status) {
switch (status) {
case '连接中...':
return Colors.orange;
case '已连接':
return Colors.green;
case '连接失败':
return Colors.red;
case '已断开':
return Colors.grey;
default:
return Colors.grey;
}
}
}

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

@ -6,11 +6,12 @@ import '../../../data/services/classic_bluetooth_service.dart';
import '../../../core/utils/logger.dart';
import 'dart:io';
import '../../../data/services/voice_interaction_service.dart';
/// 简化的配对控制器,仅用于显示已连接经典蓝牙设备和处理基本配对流程
class PairingController extends GetxController {
static PairingController get to => Get.find();
// 经典蓝牙服务
final ClassicBluetoothService _bluetoothService =
Get.find<ClassicBluetoothService>();
@ -109,6 +110,7 @@ class PairingController extends GetxController {
/// 初始化流程
Future<void> _initialize() async {
try {
// 初始化语音交互服务
_voiceInteractionService.initialize();

22
lib/modules/settings/views/settings_view.dart

@ -480,6 +480,28 @@ class SettingsView extends GetView<SettingsController> {
},
isDarkMode: isDarkMode,
),
Divider(
height: 1,
color: isDarkMode
? Colors.white.withOpacity(0.1)
: Colors.grey[200]),
// 杰理测试
_buildSimpleNavigationSetting(
title: '杰理蓝牙测试',
subtitle: '测试杰理蓝牙设备扫描、连接和功能',
icon: Icons.bluetooth,
iconBgColor: isDarkMode
? Colors.blue[900]!.withOpacity(0.3)
: Colors.blue[100]!,
iconColor:
isDarkMode ? Colors.blue[300]! : Colors.blue[600]!,
titleColor: isDarkMode ? Colors.white : null,
subtitleColor: isDarkMode ? Colors.white70 : null,
onTap: () {
Get.toNamed(Routes.jieliTest);
},
isDarkMode: isDarkMode,
),
Divider(
height: 1,
color: isDarkMode

7
lib/routes/app_pages.dart

@ -31,6 +31,8 @@ import '../modules/pairing/bindings/pairing_binding.dart';
import './app_routes.dart';
import '../modules/settings/views/settings_view.dart';
import '../modules/settings/bindings/settings_binding.dart';
import '../modules/jieli_test/views/jieli_test_view.dart';
import '../modules/jieli_test/bindings/jieli_test_binding.dart';
abstract class AppPages {
static final pages = [
@ -119,5 +121,10 @@ abstract class AppPages {
page: () => const SettingsView(),
binding: SettingsBinding(),
),
GetPage(
name: Routes.jieliTest,
page: () => const JieliTestView(),
binding: JieliTestBinding(),
),
];
}

1
lib/routes/app_routes.dart

@ -24,4 +24,5 @@ abstract class Routes {
static const FLUTTER_TTS_TEST = '/flutter_tts_test';
static const FLUTTER_ASR_TEST = '/flutter_asr_test';
static const editProfile = '/edit_profile';
static const jieliTest = '/jieli_test';
}

42
local_plugins/azure_speech/android/build.gradle.kts

@ -1,32 +1,11 @@
import com.android.build.gradle.LibraryExtension
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath("com.android.tools.build:gradle:7.3.0")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.7.10")
}
}
allprojects {
repositories {
google()
mavenCentral()
}
}
plugins {
id("com.android.library")
kotlin("android")
id("org.jetbrains.kotlin.android")
}
// 配置android扩展
configure<LibraryExtension> {
android {
namespace = "com.yunqiinnovation.azure_speech"
compileSdkVersion(33)
compileSdk = 33
defaultConfig {
minSdk = 21
@ -48,6 +27,20 @@ configure<LibraryExtension> {
lintOptions {
isCheckReleaseBuilds = false
}
// 添加packaging配置,排除冲突文件
packaging {
resources {
excludes.add("META-INF/DEPENDENCIES")
excludes.add("META-INF/LICENSE")
excludes.add("META-INF/LICENSE.txt")
excludes.add("META-INF/license.txt")
excludes.add("META-INF/NOTICE")
excludes.add("META-INF/NOTICE.txt")
excludes.add("META-INF/notice.txt")
excludes.add("META-INF/*.kotlin_module")
}
}
}
// 显式设置Kotlin JVM目标版本
@ -58,7 +51,6 @@ tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
}
dependencies {
// 添加Microsoft语音SDK
implementation("com.microsoft.cognitiveservices.speech:client-sdk:1.30.0")
}

80
local_plugins/chat_storage_sqlite/README.md

@ -0,0 +1,80 @@
# chat_storage_sqlite
SQLite存储插件,用于存储聊天记录并支持分页查询。
## 功能特点
- 使用SQLite数据库存储聊天记录
- 提供分页查询功能,可指定获取对应页码的记录
- 支持按会话ID组织和查询聊天记录
- 提供添加、查询、删除等基本操作
## 使用方法
### 保存聊天记录
```dart
import 'package:chat_storage_sqlite/chat_storage_sqlite.dart';
// 保存一条聊天记录
final result = await ChatStorageSqlite.saveMessage(
sessionId: 'session_123',
message: '你好,这是一条测试消息',
sender: 'user',
metadata: '{"timestamp": 1630000000000}' // 可选的元数据,支持JSON字符串
);
if (result['success']) {
print('消息保存成功,ID: ${result['id']}');
} else {
print('消息保存失败: ${result['error']}');
}
```
### 获取聊天记录(支持分页)
```dart
// 获取指定会话的聊天记录,支持分页
final pagedResult = await ChatStorageSqlite.getMessages(
sessionId: 'session_123',
page: 1, // 当前页码,从1开始
pageSize: 20, // 每页记录数
);
// 使用结果
print('总记录数: ${pagedResult.totalCount}');
print('总页数: ${pagedResult.totalPages}');
print('当前页: ${pagedResult.page}');
// 遍历消息列表
for (var message in pagedResult.messages) {
print('ID: ${message.id}');
print('发送者: ${message.sender}');
print('内容: ${message.message}');
print('时间: ${message.timestamp}');
print('---');
}
```
### 删除聊天记录
```dart
// 方式1:删除指定ID的消息
final deleteResult1 = await ChatStorageSqlite.deleteMessages(
messageIds: [1, 2, 3],
);
// 方式2:删除整个会话的所有消息
final deleteResult2 = await ChatStorageSqlite.deleteMessages(
sessionId: 'session_123',
);
// 方式3:清空整个数据库
final clearResult = await ChatStorageSqlite.clearDatabase();
```
## 注意事项
- 插件会自动创建和管理SQLite数据库,无需手动初始化
- 所有操作都是异步的,请使用async/await处理返回结果
- 插件内部已处理异常情况,但建议在使用时添加适当的错误处理

60
local_plugins/chat_storage_sqlite/android/build.gradle.kts

@ -0,0 +1,60 @@
plugins {
// Android Library 插件
id("com.android.library")
// Kotlin Android 插件
id("org.jetbrains.kotlin.android")
id("com.github.johnrengelman.shadow") version "8.1.1"
}
android {
// 命名空间,对应你插件的包名(需与代码内包名保持一致)
namespace = "com.yunqiinnovation.chat_storage_sqlite"
// 目标 SDK 版本
compileSdk = 33
defaultConfig {
// 最低 SDK 版本
minSdk = 21
targetSdk = 33
}
// Java 语言级别兼容配置
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
// Kotlin 语言级别
kotlinOptions {
jvmTarget = "11"
}
// 添加packaging配置,排除冲突文件
packaging {
resources {
excludes.add("META-INF/DEPENDENCIES")
excludes.add("META-INF/LICENSE")
excludes.add("META-INF/LICENSE.txt")
excludes.add("META-INF/license.txt")
excludes.add("META-INF/NOTICE")
excludes.add("META-INF/NOTICE.txt")
excludes.add("META-INF/notice.txt")
excludes.add("META-INF/*.kotlin_module")
}
}
}
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4")
implementation("com.fasterxml.jackson.core:jackson-databind:2.14.2")
implementation("com.squareup.okhttp3:okhttp:4.9.3")
implementation("com.anthropic:anthropic-java:0.8.0")
implementation("io.modelcontextprotocol:kotlin-sdk:0.4.0")
}
dependencies {
// SQLite相关依赖
implementation("androidx.sqlite:sqlite:2.3.1")
}

1
local_plugins/chat_storage_sqlite/android/settings.gradle.kts

@ -0,0 +1 @@
rootProject.name = "chat_storage_sqlite"

8
local_plugins/chat_storage_sqlite/android/src/main/AndroidManifest.xml

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.yunqiinnovation.chat_storage_sqlite">
<!-- 添加sqlite相关权限 -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
</manifest>

236
local_plugins/chat_storage_sqlite/android/src/main/kotlin/com/yunqiinnovation/chat_storage_sqlite/ChatStorageSqlitePlugin.kt

@ -0,0 +1,236 @@
package com.yunqiinnovation.chat_storage_sqlite
import android.content.Context
import android.database.Cursor
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper
import android.util.Log
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result
import org.json.JSONArray
import org.json.JSONObject
import java.text.SimpleDateFormat
import java.util.*
// 定义数据库帮助类
private class ChatDatabaseHelper(context: Context) :
SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) {
companion object {
private const val DATABASE_NAME = "chat_messages.db"
private const val DATABASE_VERSION = 1
// 定义消息表
private const val TABLE_MESSAGES = "messages"
private const val COLUMN_ID = "id"
private const val COLUMN_SESSION_ID = "session_id"
private const val COLUMN_MESSAGE = "message"
private const val COLUMN_SENDER = "sender"
private const val COLUMN_TIMESTAMP = "timestamp"
private const val COLUMN_METADATA = "metadata"
}
override fun onCreate(db: SQLiteDatabase) {
// 创建消息表
val createMessagesTable = """
CREATE TABLE $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
)
""".trimIndent()
db.execSQL(createMessagesTable)
// 创建索引以加快查询速度
db.execSQL("CREATE INDEX idx_session_id ON $TABLE_MESSAGES ($COLUMN_SESSION_ID)")
db.execSQL("CREATE INDEX idx_timestamp ON $TABLE_MESSAGES ($COLUMN_TIMESTAMP)")
}
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
// 处理数据库升级逻辑
if (oldVersion < newVersion) {
// 简单的升级策略 - 删除旧表并创建新表
// 在实际应用中,应该实现数据迁移而非删除
db.execSQL("DROP TABLE IF EXISTS $TABLE_MESSAGES")
onCreate(db)
}
}
}
/** ChatStorageSqlitePlugin */
class ChatStorageSqlitePlugin: FlutterPlugin, MethodCallHandler {
private lateinit var channel : MethodChannel
private lateinit var context: Context
private lateinit var dbHelper: ChatDatabaseHelper
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
context = flutterPluginBinding.applicationContext
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "chat_storage_sqlite")
channel.setMethodCallHandler(this)
dbHelper = ChatDatabaseHelper(context)
}
override fun onMethodCall(call: MethodCall, result: Result) {
when (call.method) {
"saveMessage" -> {
try {
val sessionId = call.argument<String>("sessionId") ?: ""
val message = call.argument<String>("message") ?: ""
val sender = call.argument<String>("sender") ?: ""
val metadata = call.argument<String>("metadata")
val db = dbHelper.writableDatabase
// 获取当前时间作为时间戳
val timestamp = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()).format(Date())
// 执行插入操作
val values = android.content.ContentValues().apply {
put("session_id", sessionId)
put("message", message)
put("sender", sender)
put("timestamp", timestamp)
put("metadata", metadata)
}
val id = db.insert("messages", null, values)
if (id != -1L) {
result.success(mapOf("success" to true, "id" to id))
} else {
result.error("DB_ERROR", "插入消息失败", null)
}
} catch (e: Exception) {
result.error("EXCEPTION", "保存消息时发生错误: ${e.message}", null)
}
}
"getMessages" -> {
try {
val sessionId = call.argument<String>("sessionId") ?: ""
val page = call.argument<Int>("page") ?: 1
val pageSize = call.argument<Int>("pageSize") ?: 20
val db = dbHelper.readableDatabase
val offset = (page - 1) * pageSize
// 构建查询语句
val query = """
SELECT * FROM messages
WHERE session_id = ?
ORDER BY timestamp DESC
LIMIT ? OFFSET ?
""".trimIndent()
val selectionArgs = arrayOf(sessionId, pageSize.toString(), offset.toString())
val cursor = db.rawQuery(query, selectionArgs)
val messages = cursorToJsonArray(cursor)
// 获取总记录数
val countQuery = "SELECT COUNT(*) FROM messages WHERE session_id = ?"
val countCursor = db.rawQuery(countQuery, arrayOf(sessionId))
var totalCount = 0
if (countCursor.moveToFirst()) {
totalCount = countCursor.getInt(0)
}
countCursor.close()
// 计算总页数
val totalPages = if (totalCount % pageSize == 0) {
totalCount / pageSize
} else {
(totalCount / pageSize) + 1
}
val response = JSONObject().apply {
put("messages", messages)
put("page", page)
put("pageSize", pageSize)
put("totalCount", totalCount)
put("totalPages", totalPages)
}
result.success(response.toString())
} catch (e: Exception) {
result.error("EXCEPTION", "获取消息时发生错误: ${e.message}", null)
}
}
"deleteMessages" -> {
try {
val sessionId = call.argument<String>("sessionId") ?: ""
val messageIds = call.argument<List<Int>>("messageIds")
val db = dbHelper.writableDatabase
if (messageIds != null && messageIds.isNotEmpty()) {
// 删除指定ID的消息
val idsStr = messageIds.joinToString(",")
val deleteQuery = "DELETE FROM messages WHERE id IN ($idsStr)"
db.execSQL(deleteQuery)
result.success(mapOf("success" to true, "count" to messageIds.size))
} else if (sessionId.isNotEmpty()) {
// 删除会话中的所有消息
val deleteQuery = "DELETE FROM messages WHERE session_id = ?"
db.execSQL(deleteQuery, arrayOf(sessionId))
result.success(mapOf("success" to true))
} else {
result.error("INVALID_ARGS", "必须提供sessionId或messageIds", null)
}
} catch (e: Exception) {
result.error("EXCEPTION", "删除消息时发生错误: ${e.message}", null)
}
}
"clearDatabase" -> {
try {
val db = dbHelper.writableDatabase
db.execSQL("DELETE FROM messages")
result.success(mapOf("success" to true))
} catch (e: Exception) {
result.error("EXCEPTION", "清空数据库时发生错误: ${e.message}", null)
}
}
else -> {
result.notImplemented()
}
}
}
private fun cursorToJsonArray(cursor: Cursor): JSONArray {
val jsonArray = JSONArray()
val idIndex = cursor.getColumnIndex("id")
val sessionIdIndex = cursor.getColumnIndex("session_id")
val messageIndex = cursor.getColumnIndex("message")
val senderIndex = cursor.getColumnIndex("sender")
val timestampIndex = cursor.getColumnIndex("timestamp")
val metadataIndex = cursor.getColumnIndex("metadata")
while (cursor.moveToNext()) {
val jsonObject = JSONObject().apply {
put("id", cursor.getInt(idIndex))
put("sessionId", cursor.getString(sessionIdIndex))
put("message", cursor.getString(messageIndex))
put("sender", cursor.getString(senderIndex))
put("timestamp", cursor.getString(timestampIndex))
if (metadataIndex != -1 && !cursor.isNull(metadataIndex)) {
put("metadata", cursor.getString(metadataIndex))
}
}
jsonArray.put(jsonObject)
}
cursor.close()
return jsonArray
}
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
channel.setMethodCallHandler(null)
}
}

155
local_plugins/chat_storage_sqlite/lib/chat_storage_sqlite.dart

@ -0,0 +1,155 @@
import 'dart:async';
import 'dart:convert';
import 'package:flutter/services.dart';
/// 聊天记录数据模型
class ChatMessage {
final int? id;
final String sessionId;
final String message;
final String sender;
final String timestamp;
final String? metadata;
ChatMessage({
this.id,
required this.sessionId,
required this.message,
required this.sender,
required this.timestamp,
this.metadata,
});
Map<String, dynamic> toMap() {
return {
'id': id,
'sessionId': sessionId,
'message': message,
'sender': sender,
'timestamp': timestamp,
'metadata': metadata,
};
}
factory ChatMessage.fromMap(Map<String, dynamic> map) {
return ChatMessage(
id: map['id'],
sessionId: map['sessionId'],
message: map['message'],
sender: map['sender'],
timestamp: map['timestamp'],
metadata: map['metadata'],
);
}
}
/// 分页结果数据模型
class PagedResult {
final List<ChatMessage> messages;
final int page;
final int pageSize;
final int totalCount;
final int totalPages;
PagedResult({
required this.messages,
required this.page,
required this.pageSize,
required this.totalCount,
required this.totalPages,
});
}
/// SQLite聊天记录存储插件
class ChatStorageSqlite {
static const MethodChannel _channel = MethodChannel('chat_storage_sqlite');
/// 保存一条聊天记录
static Future<Map<String, dynamic>> saveMessage({
required String sessionId,
required String message,
required String sender,
String? metadata,
}) async {
try {
final result = await _channel.invokeMethod('saveMessage', {
'sessionId': sessionId,
'message': message,
'sender': sender,
'metadata': metadata,
});
return Map<String, dynamic>.from(result);
} catch (e) {
return {'success': false, 'error': e.toString()};
}
}
/// 获取指定会话的聊天记录(支持分页)
static Future<PagedResult> getMessages({
required String sessionId,
int page = 1,
int pageSize = 20,
}) async {
try {
final String result = await _channel.invokeMethod('getMessages', {
'sessionId': sessionId,
'page': page,
'pageSize': pageSize,
});
final Map<String, dynamic> data = jsonDecode(result);
final List<dynamic> messagesList = data['messages'];
List<ChatMessage> messages = messagesList.map((item) {
return ChatMessage.fromMap(Map<String, dynamic>.from(item));
}).toList();
return PagedResult(
messages: messages,
page: data['page'],
pageSize: data['pageSize'],
totalCount: data['totalCount'],
totalPages: data['totalPages'],
);
} catch (e) {
return PagedResult(
messages: [],
page: page,
pageSize: pageSize,
totalCount: 0,
totalPages: 0,
);
}
}
/// 删除聊天记录,可按会话ID或消息ID列表删除
static Future<Map<String, dynamic>> deleteMessages({
String? sessionId,
List<int>? messageIds,
}) async {
try {
if (sessionId == null && (messageIds == null || messageIds.isEmpty)) {
return {'success': false, 'error': '必须提供sessionId或messageIds'};
}
final result = await _channel.invokeMethod('deleteMessages', {
'sessionId': sessionId,
'messageIds': messageIds,
});
return Map<String, dynamic>.from(result);
} catch (e) {
return {'success': false, 'error': e.toString()};
}
}
/// 清空整个数据库
static Future<Map<String, dynamic>> clearDatabase() async {
try {
final result = await _channel.invokeMethod('clearDatabase');
return Map<String, dynamic>.from(result);
} catch (e) {
return {'success': false, 'error': e.toString()};
}
}
}

30
local_plugins/chat_storage_sqlite/pubspec.yaml

@ -0,0 +1,30 @@
name: chat_storage_sqlite
description: SQLite存储插件,用于存储聊天记录并支持分页查询
version: 0.0.1
homepage:
environment:
sdk: ">=2.17.0 <3.0.0"
flutter: ">=2.5.0"
dependencies:
flutter:
sdk: flutter
sqflite: ^2.3.0
path: ^1.8.3
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^2.0.0
# The following section is specific to Flutter packages.
flutter:
# This section identifies this Flutter project as a plugin project.
plugin:
platforms:
android:
package: com.yunqiinnovation.chat_storage_sqlite
pluginClass: ChatStorageSqlitePlugin
ios:
pluginClass: ChatStorageSqlitePlugin

144
local_plugins/jieli/README.md

@ -0,0 +1,144 @@
# Jieli 插件
一个用于连接杰理蓝牙耳机的Flutter插件,支持Android 12+的BLE通信。
## 功能
- 扫描支持特定BLE服务的耳机设备
- 连接设备并发现服务
- 接收耳机的唤醒通知
- 向耳机发送控制命令启动录音
- 接收音频数据流
## 技术规范
此插件实现了以下BLE特性:
- **服务UUID**: 0000A100-0000-1000-8000-00805F9B34FB
- **音频数据特征UUID**: 0000A101-0000-1000-8000-00805F9B34FB
- **控制特征UUID**: 0000A102-0000-1000-8000-00805F9B34FB
## 使用方法
### 初始化插件
```dart
final jieli = Jieli();
```
### 检查蓝牙状态
```dart
bool isEnabled = await jieli.isBluetoothEnabled();
if (!isEnabled) {
await jieli.requestBluetoothEnable();
}
```
### 扫描设备
```dart
// 监听扫描结果
jieli.scanResults.listen((devices) {
// 处理扫描到的设备列表
for (var device in devices) {
print('发现设备: ${device.name} (${device.id}), RSSI: ${device.rssi}');
}
});
// 开始扫描,10秒后自动停止
await jieli.startScan(scanTimeout: 10000);
// 手动停止扫描
await jieli.stopScan();
```
### 连接设备
```dart
// 监听连接状态
jieli.connectionState.listen((state) {
switch (state) {
case ConnectionState.connected:
print('设备已连接');
break;
case ConnectionState.disconnected:
print('设备已断开');
break;
case ConnectionState.connecting:
print('正在连接...');
break;
case ConnectionState.disconnecting:
print('正在断开...');
break;
case ConnectionState.error:
print('连接错误');
break;
}
});
// 连接到设备
await jieli.connect(deviceId);
// 断开连接
await jieli.disconnect();
```
### 接收唤醒信号
```dart
// 监听唤醒信号
jieli.wakeupStream.listen((wakeup) {
print('收到唤醒信号');
// 通常在收到唤醒后自动发送开始录音命令
});
```
### 发送录音命令
```dart
// 发送开始录音命令
await jieli.sendStartRecordingCommand();
// 发送停止录音命令
await jieli.sendStopRecordingCommand();
```
### 接收音频数据
```dart
// 监听音频数据流
jieli.audioDataStream.listen((data) {
print('收到音频数据: ${data.length} 字节');
// 处理音频数据
});
```
### 资源清理
```dart
// 在不再需要插件时释放资源
await jieli.dispose();
```
## 权限
使用此插件需要以下Android权限:
- Android 12+ (API 31+): `BLUETOOTH_SCAN`, `BLUETOOTH_CONNECT`
- Android 11及以下: `BLUETOOTH`, `BLUETOOTH_ADMIN`
插件会在需要时自动请求权限,但建议在应用清单文件中声明这些权限:
```xml
<!-- 针对 Android 11 及以下的传统蓝牙权限 -->
<uses-permission android:name="android.permission.BLUETOOTH"
android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"
android:maxSdkVersion="30" />
<!-- Android 12+ 新的 BLE 扫描和连接权限 -->
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"
android:usesPermissionFlags="neverForLocation" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
```

55
local_plugins/jieli/android/build.gradle.kts

@ -0,0 +1,55 @@
plugins {
// Android Library 插件
id("com.android.library")
// Kotlin Android 插件
id("org.jetbrains.kotlin.android")
id("com.github.johnrengelman.shadow") version "8.1.1"
}
android {
// 命名空间,对应你插件的包名(需与代码内包名保持一致)
namespace = "com.yunqiinnovation.jieli"
// 目标 SDK 版本
compileSdk = 33
defaultConfig {
// 最低 SDK 版本
minSdk = 21
targetSdk = 33
}
// Java 语言级别兼容配置
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
// Kotlin 语言级别
kotlinOptions {
jvmTarget = "11"
}
// 添加packaging配置,排除冲突文件
packaging {
resources {
excludes.add("META-INF/DEPENDENCIES")
excludes.add("META-INF/LICENSE")
excludes.add("META-INF/LICENSE.txt")
excludes.add("META-INF/license.txt")
excludes.add("META-INF/NOTICE")
excludes.add("META-INF/NOTICE.txt")
excludes.add("META-INF/notice.txt")
excludes.add("META-INF/*.kotlin_module")
}
}
}
dependencies {
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.6.10")
implementation("androidx.core:core-ktx:1.7.0")
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0")
implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.4.0")
implementation("androidx.lifecycle:lifecycle-service:2.4.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2")
}

1
local_plugins/jieli/android/settings.gradle.kts

@ -0,0 +1 @@
rootProject.name = "jieli"

22
local_plugins/jieli/android/src/main/AndroidManifest.xml

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.yunqiinnovation.jieli">
<!-- 声明需要蓝牙功能 -->
<uses-feature android:name="android.hardware.bluetooth_le" android:required="true" />
<!-- 针对 Android 11 及以下的传统蓝牙权限 -->
<uses-permission android:name="android.permission.BLUETOOTH"
android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"
android:maxSdkVersion="30" />
<!-- Android 12+ 新的 BLE 扫描和连接权限 -->
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"
android:usesPermissionFlags="neverForLocation" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<!-- 前台服务权限 -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
</manifest>

460
local_plugins/jieli/android/src/main/kotlin/com/yunqiinnovation/jieli/BleService.kt

@ -0,0 +1,460 @@
package com.yunqiinnovation.jieli
import android.bluetooth.*
import android.bluetooth.le.*
import android.content.Context
import android.os.Handler
import android.os.Looper
import android.os.ParcelUuid
import android.util.Log
import androidx.lifecycle.LifecycleService
import androidx.lifecycle.MutableLiveData
import kotlinx.coroutines.*
import java.util.*
/**
* BLE回调接口,统一处理蓝牙事件
*/
interface BleCallback {
/**
* 扫描结果回调
* @param devices 扫描到的设备列表
*/
fun onScanResult(devices: List<Map<String, Any>>)
/**
* 连接状态变化回调
* @param state 连接状态
*/
fun onConnectionStateChanged(state: Int)
/**
* 音频数据回调
* @param data 音频数据字节数组
*/
fun onAudioDataReceived(data: ByteArray)
/**
* 唤醒信号回调
*/
fun onWakeupSignalReceived()
}
/**
* BLE服务类,提供核心蓝牙功能
*/
class BleService(private val context: Context) {
companion object {
private const val TAG = "JieliBleCoreService"
// 服务和特征UUID
private const val AUDIO_SERVICE_UUID = "0000A100-0000-1000-8000-00805F9B34FB"
private const val AUDIO_DATA_CHAR_UUID = "0000A101-0000-1000-8000-00805F9B34FB"
private const val CONTROL_CHAR_UUID = "0000A102-0000-1000-8000-00805F9B34FB"
private const val CLIENT_CHAR_CONFIG_UUID = "00002902-0000-1000-8000-00805f9b34fb"
// 连接状态常量
const val STATE_DISCONNECTED = 0
const val STATE_CONNECTING = 1
const val STATE_CONNECTED = 2
const val STATE_DISCONNECTING = 3
const val STATE_ERROR = 4
// 扫描结果更新间隔(毫秒)
private const val SCAN_RESULT_UPDATE_INTERVAL = 500L
}
// 蓝牙管理器和适配器
private val bluetoothManager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
private val bluetoothAdapter: BluetoothAdapter? = bluetoothManager.adapter
private val bluetoothLeScanner: BluetoothLeScanner? = bluetoothAdapter?.bluetoothLeScanner
// GATT连接和特征
private var bluetoothGatt: BluetoothGatt? = null
private var audioDataChar: BluetoothGattCharacteristic? = null
private var controlChar: BluetoothGattCharacteristic? = null
// 扫描相关
private val scanHandler = Handler(Looper.getMainLooper())
private val scanResults = ArrayList<ScanResult>()
private var isScanning = false
private var isPendingScanResultUpdate = false
private val scanResultUpdateRunnable = Runnable {
notifyScanResults()
isPendingScanResultUpdate = false
}
// 连接状态LiveData
val connectionState = MutableLiveData<Int>(STATE_DISCONNECTED)
// 统一回调接口
var callback: BleCallback? = null
/**
* 开始扫描BLE设备
*/
fun startScan(scanTimeout: Long = 5000): Boolean {
if (bluetoothLeScanner == null || isScanning) {
return false
}
Log.i(TAG, "开始扫描BLE设备...")
scanResults.clear()
val serviceUuid = UUID.fromString(AUDIO_SERVICE_UUID)
val scanFilter = ScanFilter.Builder()
// .setServiceUuid(ParcelUuid(serviceUuid))
.build()
val scanFilters = listOf(scanFilter)
val scanSettings = ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
.build()
bluetoothLeScanner.startScan(scanFilters, scanSettings, scanCallback)
isScanning = true
// 设置超时停止扫描
scanHandler.postDelayed({
Log.d(TAG, "定时器触发,准备停止扫描 [主线程:${Looper.myLooper() == Looper.getMainLooper()}]")
stopScan()
}, scanTimeout)
return true
}
/**
* 停止扫描
*/
fun stopScan(): Boolean {
Log.i(TAG, "停止扫描BLE设备")
if (bluetoothLeScanner == null || !isScanning) {
return false
}
bluetoothLeScanner.stopScan(scanCallback)
scanHandler.removeCallbacksAndMessages(null)
isScanning = false
// 最后一次发送扫描结果
notifyScanResults()
return true
}
/**
* 扫描回调
*/
private val scanCallback = object : ScanCallback() {
override fun onScanResult(callbackType: Int, result: ScanResult) {
val device = result.device
val deviceName = device.name ?: "未知设备"
val deviceAddress = device.address
val rssi = result.rssi
// Log.d(TAG, "发现设备: $deviceName [$deviceAddress], RSSI: $rssi")
// 避免重复添加
val existingIndex = scanResults.indexOfFirst { it.device.address == deviceAddress }
if (existingIndex >= 0) {
scanResults[existingIndex] = result
} else {
scanResults.add(result)
}
// 如果没有待处理的更新,安排一个
if (!isPendingScanResultUpdate) {
isPendingScanResultUpdate = true
scanHandler.postDelayed(scanResultUpdateRunnable, SCAN_RESULT_UPDATE_INTERVAL)
}
}
override fun onScanFailed(errorCode: Int) {
Log.e(TAG, "扫描失败,错误码: $errorCode")
isScanning = false
}
}
/**
* 将扫描结果通知回调
*/
private fun notifyScanResults() {
val devicesJson = scanResults.map { result ->
mapOf(
"id" to result.device.address,
"name" to (result.device.name ?: "未知设备"),
"rssi" to result.rssi
)
}
callback?.onScanResult(devicesJson)
}
/**
* 连接到指定设备
*/
fun connect(deviceAddress: String) {
if (bluetoothAdapter == null) {
Log.e(TAG, "蓝牙适配器不可用")
updateConnectionState(STATE_ERROR)
return
}
// 先断开现有连接
bluetoothGatt?.close()
bluetoothGatt = null
try {
val device = bluetoothAdapter.getRemoteDevice(deviceAddress)
updateConnectionState(STATE_CONNECTING)
// 连接GATT服务器
Log.i(TAG, "正在连接设备: ${device.name ?: "未知设备"} [$deviceAddress]")
bluetoothGatt = device.connectGatt(context, false, gattCallback)
} catch (e: Exception) {
Log.e(TAG, "连接设备时发生错误: ${e.message}")
updateConnectionState(STATE_ERROR)
}
}
/**
* 断开连接
*/
fun disconnect() {
bluetoothGatt?.disconnect()
updateConnectionState(STATE_DISCONNECTING)
}
/**
* 更新连接状态并通知回调
*/
private fun updateConnectionState(state: Int) {
connectionState.postValue(state)
callback?.onConnectionStateChanged(state)
}
/**
* GATT回调
*/
private val gattCallback = object : BluetoothGattCallback() {
override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
if (newState == BluetoothProfile.STATE_CONNECTED && status == BluetoothGatt.GATT_SUCCESS) {
Log.i(TAG, "设备已连接,开始发现服务...")
updateConnectionState(STATE_CONNECTED)
// 开始搜索服务
gatt.discoverServices()
} else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
Log.i(TAG, "设备已断开连接")
updateConnectionState(STATE_DISCONNECTED)
// 清理资源
gatt.close()
bluetoothGatt = null
audioDataChar = null
controlChar = null
} else if (status != BluetoothGatt.GATT_SUCCESS) {
Log.e(TAG, "连接失败,status=$status")
updateConnectionState(STATE_ERROR)
gatt.close()
bluetoothGatt = null
}
}
override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
if (status == BluetoothGatt.GATT_SUCCESS) {
Log.i(TAG, "服务发现成功")
// 查找目标服务
val audioService = gatt.getService(UUID.fromString(AUDIO_SERVICE_UUID))
if (audioService != null) {
Log.i(TAG, "找到音频服务")
// 获取音频数据和控制特征
audioDataChar = audioService.getCharacteristic(UUID.fromString(AUDIO_DATA_CHAR_UUID))
controlChar = audioService.getCharacteristic(UUID.fromString(CONTROL_CHAR_UUID))
if (audioDataChar != null && controlChar != null) {
Log.i(TAG, "找到所需特征")
// 注册特征通知
setupNotifications(gatt)
} else {
Log.e(TAG, "未找到所需特征")
updateConnectionState(STATE_ERROR)
}
} else {
Log.e(TAG, "未找到音频服务")
updateConnectionState(STATE_ERROR)
}
} else {
Log.e(TAG, "服务发现失败,status=$status")
updateConnectionState(STATE_ERROR)
}
}
override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) {
val characteristicUuid = characteristic.uuid.toString()
when (characteristicUuid) {
AUDIO_DATA_CHAR_UUID -> {
// 收到音频数据
val audioData = characteristic.value
Log.d(TAG, "收到音频数据帧,大小: ${audioData.size} 字节")
callback?.onAudioDataReceived(audioData)
}
CONTROL_CHAR_UUID -> {
// 收到唤醒信号
val value = characteristic.value
Log.i(TAG, "收到控制通知: ${value.contentToString()}")
// 假设0x01表示唤醒信号
if (value.isNotEmpty() && value[0].toInt() == 0x01) {
Log.i(TAG, "收到唤醒信号")
callback?.onWakeupSignalReceived()
// 收到唤醒后自动发送开始录音命令
sendStartRecordingCommand()
}
}
}
}
override fun onCharacteristicWrite(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, status: Int) {
if (characteristic.uuid.toString() == CONTROL_CHAR_UUID) {
if (status == BluetoothGatt.GATT_SUCCESS) {
Log.i(TAG, "命令发送成功: ${characteristic.value.contentToString()}")
} else {
Log.e(TAG, "命令发送失败,status=$status")
}
}
}
override fun onDescriptorWrite(gatt: BluetoothGatt, descriptor: BluetoothGattDescriptor, status: Int) {
if (status == BluetoothGatt.GATT_SUCCESS) {
Log.i(TAG, "描述符写入成功: ${descriptor.uuid}")
// 如果是音频数据特征的描述符,再订阅控制特征
if (descriptor.characteristic.uuid.toString() == AUDIO_DATA_CHAR_UUID) {
setupControlNotification(gatt)
}
} else {
Log.e(TAG, "描述符写入失败,status=$status")
}
}
}
/**
* 设置特征通知
*/
private fun setupNotifications(gatt: BluetoothGatt) {
// 先订阅音频数据通知
if (audioDataChar?.properties?.and(BluetoothGattCharacteristic.PROPERTY_NOTIFY) != 0) {
gatt.setCharacteristicNotification(audioDataChar, true)
val descriptor = audioDataChar?.getDescriptor(UUID.fromString(CLIENT_CHAR_CONFIG_UUID))
descriptor?.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
if (descriptor != null) {
gatt.writeDescriptor(descriptor)
Log.i(TAG, "设置音频数据特征通知")
}
} else {
Log.e(TAG, "音频数据特征不支持通知")
setupControlNotification(gatt)
}
}
/**
* 设置控制特征通知
*/
private fun setupControlNotification(gatt: BluetoothGatt) {
if (controlChar?.properties?.and(BluetoothGattCharacteristic.PROPERTY_NOTIFY) != 0) {
gatt.setCharacteristicNotification(controlChar, true)
val descriptor = controlChar?.getDescriptor(UUID.fromString(CLIENT_CHAR_CONFIG_UUID))
descriptor?.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
if (descriptor != null) {
gatt.writeDescriptor(descriptor)
Log.i(TAG, "设置控制特征通知")
}
} else {
Log.e(TAG, "控制特征不支持通知")
}
}
/**
* 发送开始录音命令(向控制特征写入0x01)
*/
fun sendStartRecordingCommand(): Boolean {
if (bluetoothGatt == null || controlChar == null || connectionState.value != STATE_CONNECTED) {
Log.e(TAG, "无法发送开始录音命令,设备未连接")
return false
}
try {
val commandData = byteArrayOf(0x01)
controlChar?.value = commandData
controlChar?.writeType = BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT
val success = bluetoothGatt?.writeCharacteristic(controlChar) ?: false
if (success) {
Log.i(TAG, "开始录音命令已发送")
} else {
Log.e(TAG, "开始录音命令发送失败")
}
return success
} catch (e: Exception) {
Log.e(TAG, "发送开始录音命令时发生异常: ${e.message}")
return false
}
}
/**
* 发送停止录音命令(向控制特征写入0x02)
*/
fun sendStopRecordingCommand(): Boolean {
if (bluetoothGatt == null || controlChar == null || connectionState.value != STATE_CONNECTED) {
Log.e(TAG, "无法发送停止录音命令,设备未连接")
return false
}
try {
val commandData = byteArrayOf(0x02)
controlChar?.value = commandData
controlChar?.writeType = BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT
val success = bluetoothGatt?.writeCharacteristic(controlChar) ?: false
if (success) {
Log.i(TAG, "停止录音命令已发送")
} else {
Log.e(TAG, "停止录音命令发送失败")
}
return success
} catch (e: Exception) {
Log.e(TAG, "发送停止录音命令时发生异常: ${e.message}")
return false
}
}
/**
* 清理资源
*/
fun cleanup() {
stopScan()
scanHandler.removeCallbacks(scanResultUpdateRunnable)
bluetoothGatt?.close()
bluetoothGatt = null
audioDataChar = null
controlChar = null
}
}

269
local_plugins/jieli/android/src/main/kotlin/com/yunqiinnovation/jieli/JieliPlugin.kt

@ -0,0 +1,269 @@
package com.yunqiinnovation.jieli
import android.app.Activity
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothManager
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import androidx.annotation.NonNull
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.embedding.engine.plugins.activity.ActivityAware
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result
import kotlinx.coroutines.*
/** JieliPlugin */
class JieliPlugin: FlutterPlugin, MethodCallHandler, ActivityAware, EventChannel.StreamHandler {
/// 方法通道
private lateinit var methodChannel: MethodChannel
/// 统一事件通道
private lateinit var eventChannel: EventChannel
private lateinit var context: Context
private var activity: Activity? = null
/// BLE服务实例
private lateinit var bleService: BleService
// 协程作用域
private val pluginScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
// 事件接收器
private var eventSink: EventChannel.EventSink? = null
// 事件类型常量
companion object {
private const val EVENT_TYPE_AUDIO_DATA = "audio_data"
private const val EVENT_TYPE_CONNECTION_STATE = "connection_state"
private const val EVENT_TYPE_SCAN_RESULT = "scan_result"
private const val EVENT_TYPE_WAKEUP_SIGNAL = "wakeup_signal"
}
// 蓝牙相关请求码
private val REQUEST_ENABLE_BT = 1
private val REQUEST_PERMISSION_BLE = 2
override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
val binaryMessenger = flutterPluginBinding.binaryMessenger
// 设置方法通道
methodChannel = MethodChannel(binaryMessenger, "com.yunqiinnovation.jieli")
methodChannel.setMethodCallHandler(this)
// 设置统一事件通道
eventChannel = EventChannel(binaryMessenger, "com.yunqiinnovation.jieli/events")
eventChannel.setStreamHandler(this)
context = flutterPluginBinding.applicationContext
// 初始化BLE服务
bleService = BleService(context)
// 设置回调
setupBleCallbacks()
}
/**
* 设置BLE服务回调
*/
private fun setupBleCallbacks() {
// 设置统一回调接口
bleService.callback = object : BleCallback {
override fun onScanResult(devices: List<Map<String, Any>>) {
pluginScope.launch(Dispatchers.Main) {
sendEvent(EVENT_TYPE_SCAN_RESULT, devices)
}
}
override fun onConnectionStateChanged(state: Int) {
pluginScope.launch(Dispatchers.Main) {
sendEvent(EVENT_TYPE_CONNECTION_STATE, state)
}
}
override fun onAudioDataReceived(data: ByteArray) {
pluginScope.launch(Dispatchers.Main) {
sendEvent(EVENT_TYPE_AUDIO_DATA, data)
}
}
override fun onWakeupSignalReceived() {
pluginScope.launch(Dispatchers.Main) {
sendEvent(EVENT_TYPE_WAKEUP_SIGNAL, true)
}
}
}
}
/**
* 发送事件到Flutter端
*/
private fun sendEvent(eventType: String, data: Any?) {
eventSink?.success(mapOf(
"type" to eventType,
"data" to data
))
}
override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
when (call.method) {
"startScan" -> {
val scanTimeout = call.argument<Int>("scanTimeout") ?: 10000
if (!hasRequiredPermissions()) {
requestBlePermissions()
result.error("PERMISSION_DENIED", "需要蓝牙权限才能扫描设备", null)
return
}
val success = bleService.startScan(scanTimeout.toLong())
result.success(success)
}
"stopScan" -> {
val success = bleService.stopScan()
result.success(success)
}
"connect" -> {
val deviceId = call.argument<String>("deviceId")
if (deviceId == null) {
result.error("INVALID_ARGUMENT", "设备ID不能为空", null)
return
}
if (!hasRequiredPermissions()) {
requestBlePermissions()
result.error("PERMISSION_DENIED", "需要蓝牙权限才能连接设备", null)
return
}
bleService.connect(deviceId)
result.success(true)
}
"disconnect" -> {
bleService.disconnect()
result.success(true)
}
"sendStartRecordingCommand" -> {
val success = bleService.sendStartRecordingCommand()
result.success(success)
}
"sendStopRecordingCommand" -> {
val success = bleService.sendStopRecordingCommand()
result.success(success)
}
"isBluetoothEnabled" -> {
val bluetoothManager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
val bluetoothAdapter = bluetoothManager.adapter
result.success(bluetoothAdapter?.isEnabled == true)
}
"requestBluetoothEnable" -> {
if (activity == null) {
result.error("ACTIVITY_NOT_AVAILABLE", "Activity不可用,无法请求打开蓝牙", null)
return
}
val bluetoothManager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
val bluetoothAdapter = bluetoothManager.adapter
if (bluetoothAdapter == null) {
result.error("BLUETOOTH_UNAVAILABLE", "设备不支持蓝牙", null)
return
}
if (bluetoothAdapter.isEnabled) {
result.success(true)
return
}
// 请求打开蓝牙
val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
activity?.startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT)
result.success(true)
}
else -> {
result.notImplemented()
}
}
}
override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
methodChannel.setMethodCallHandler(null)
eventChannel.setStreamHandler(null)
bleService.cleanup()
pluginScope.cancel()
}
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
activity = binding.activity
}
override fun onDetachedFromActivityForConfigChanges() {
activity = null
}
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
activity = binding.activity
}
override fun onDetachedFromActivity() {
activity = null
}
/// EventChannel.StreamHandler接口实现
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
eventSink = events
}
override fun onCancel(arguments: Any?) {
eventSink = null
}
/// 检查是否拥有所需权限
private fun hasRequiredPermissions(): Boolean {
return if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) {
hasPermission(android.Manifest.permission.BLUETOOTH_SCAN) &&
hasPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
} else {
hasPermission(android.Manifest.permission.BLUETOOTH) &&
hasPermission(android.Manifest.permission.BLUETOOTH_ADMIN)
}
}
/// 检查单个权限
private fun hasPermission(permission: String): Boolean {
return ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED
}
/// 请求BLE权限
private fun requestBlePermissions() {
activity?.let {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) {
ActivityCompat.requestPermissions(
it,
arrayOf(
android.Manifest.permission.BLUETOOTH_SCAN,
android.Manifest.permission.BLUETOOTH_CONNECT
),
REQUEST_PERMISSION_BLE
)
} else {
ActivityCompat.requestPermissions(
it,
arrayOf(
android.Manifest.permission.BLUETOOTH,
android.Manifest.permission.BLUETOOTH_ADMIN
),
REQUEST_PERMISSION_BLE
)
}
}
}
}

188
local_plugins/jieli/lib/jieli.dart

@ -0,0 +1,188 @@
import 'dart:async';
import 'dart:typed_data';
import 'package:flutter/services.dart';
/// 设备类,代表一个BLE设备
class BleDevice {
final String id;
final String name;
final int rssi;
BleDevice({required this.id, required this.name, required this.rssi});
factory BleDevice.fromJson(Map<String, dynamic> json) {
return BleDevice(
id: json['id'] as String,
name: json['name'] as String? ?? 'Unknown Device',
rssi: json['rssi'] as int? ?? 0,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'rssi': rssi,
};
}
@override
String toString() => 'BleDevice(id: $id, name: $name, rssi: $rssi)';
}
/// 连接状态
enum ConnectionState {
disconnected,
connecting,
connected,
disconnecting,
error
}
/// 事件类型
class EventType {
static const String audioData = 'audio_data';
static const String connectionState = 'connection_state';
static const String scanResult = 'scan_result';
static const String wakeupSignal = 'wakeup_signal';
}
/// 杰理BLE插件主类
class Jieli {
static const MethodChannel _methodChannel = MethodChannel('com.yunqiinnovation.jieli');
static const EventChannel _eventChannel = EventChannel('com.yunqiinnovation.jieli/events');
/// 扫描结果流
static final StreamController<List<BleDevice>> _scanController =
StreamController<List<BleDevice>>.broadcast();
Stream<List<BleDevice>> get scanResults => _scanController.stream;
/// 连接状态变化流
static final StreamController<ConnectionState> _connectionController =
StreamController<ConnectionState>.broadcast();
Stream<ConnectionState> get connectionState => _connectionController.stream;
/// 音频数据流
static final StreamController<Uint8List> _audioController =
StreamController<Uint8List>.broadcast();
Stream<Uint8List> get audioDataStream => _audioController.stream;
/// 唤醒信号流
static final StreamController<bool> _wakeupController =
StreamController<bool>.broadcast();
Stream<bool> get wakeupStream => _wakeupController.stream;
/// 事件流订阅
StreamSubscription? _eventSubscription;
/// 构造函数,初始化平台通道和事件监听
Jieli() {
_setupEventListener();
}
/// 设置事件监听器
void _setupEventListener() {
_eventSubscription = _eventChannel.receiveBroadcastStream().listen(_handleEvent);
}
/// 处理来自原生平台的事件
void _handleEvent(dynamic event) {
if (event is! Map<dynamic, dynamic>) return;
final eventData = Map<String, dynamic>.from(event);
final String eventType = eventData['type'] as String;
final dynamic data = eventData['data'];
switch (eventType) {
case EventType.scanResult:
if (data is List<dynamic>) {
final deviceList = data
.map((item) => BleDevice.fromJson(Map<String, dynamic>.from(item)))
.toList();
_scanController.add(deviceList);
}
break;
case EventType.connectionState:
if (data is int && data >= 0 && data < ConnectionState.values.length) {
_connectionController.add(ConnectionState.values[data]);
}
break;
case EventType.audioData:
if (data is Uint8List) {
_audioController.add(data);
}
break;
case EventType.wakeupSignal:
if (data is bool) {
_wakeupController.add(data);
}
break;
}
}
/// 开始扫描BLE设备
Future<bool> startScan({int scanTimeout = 10000}) async {
final result = await _methodChannel.invokeMethod<bool>(
'startScan',
{'scanTimeout': scanTimeout},
);
return result ?? false;
}
/// 停止扫描BLE设备
Future<bool> stopScan() async {
final result = await _methodChannel.invokeMethod<bool>('stopScan');
return result ?? false;
}
/// 连接到指定设备
Future<bool> connect(String deviceId) async {
final result = await _methodChannel.invokeMethod<bool>(
'connect',
{'deviceId': deviceId},
);
return result ?? false;
}
/// 断开连接
Future<bool> disconnect() async {
final result = await _methodChannel.invokeMethod<bool>('disconnect');
return result ?? false;
}
/// 发送开始录音命令
Future<bool> sendStartRecordingCommand() async {
final result = await _methodChannel.invokeMethod<bool>('sendStartRecordingCommand');
return result ?? false;
}
/// 发送停止录音命令
Future<bool> sendStopRecordingCommand() async {
final result = await _methodChannel.invokeMethod<bool>('sendStopRecordingCommand');
return result ?? false;
}
/// 关闭插件,释放资源
Future<void> dispose() async {
await stopScan();
await disconnect();
_eventSubscription?.cancel();
_scanController.close();
_connectionController.close();
_audioController.close();
_wakeupController.close();
}
/// 检查蓝牙是否已启用
Future<bool> isBluetoothEnabled() async {
final result = await _methodChannel.invokeMethod<bool>('isBluetoothEnabled');
return result ?? false;
}
/// 请求打开蓝牙
Future<bool> requestBluetoothEnable() async {
final result = await _methodChannel.invokeMethod<bool>('requestBluetoothEnable');
return result ?? false;
}
}

23
local_plugins/jieli/pubspec.yaml

@ -0,0 +1,23 @@
name: jieli
description: A Flutter plugin for connecting with Jieli BLE earphones
version: 0.0.1
homepage:
environment:
sdk: ">=2.12.0 <3.0.0"
flutter: ">=2.0.0"
dependencies:
flutter:
sdk: flutter
dev_dependencies:
flutter_test:
sdk: flutter
flutter:
plugin:
platforms:
android:
package: com.yunqiinnovation.jieli
pluginClass: JieliPlugin

12
local_plugins/open_ai_service/android/AndroidManifest.xml

@ -0,0 +1,12 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.yunqiinnovation.open_ai_service">
<!-- 拨打电话权限 -->
<uses-permission android:name="android.permission.CALL_PHONE" />
<!-- 发送短信权限 -->
<uses-permission android:name="android.permission.SEND_SMS" />
<!-- 读取联系人权限 -->
<uses-permission android:name="android.permission.READ_CONTACTS" />
</manifest>

24
local_plugins/open_ai_service/android/build.gradle.kts

@ -3,6 +3,7 @@ plugins {
id("com.android.library")
// Kotlin Android 插件
id("org.jetbrains.kotlin.android")
id("com.github.johnrengelman.shadow") version "8.1.1"
}
android {
@ -28,10 +29,27 @@ android {
kotlinOptions {
jvmTarget = "11"
}
// 添加packaging配置,排除冲突文件
packaging {
resources {
excludes.add("META-INF/DEPENDENCIES")
excludes.add("META-INF/LICENSE")
excludes.add("META-INF/LICENSE.txt")
excludes.add("META-INF/license.txt")
excludes.add("META-INF/NOTICE")
excludes.add("META-INF/NOTICE.txt")
excludes.add("META-INF/notice.txt")
excludes.add("META-INF/*.kotlin_module")
}
}
}
dependencies {
implementation("com.squareup.okhttp3:okhttp:4.10.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4")
implementation("com.fasterxml.jackson.core:jackson-databind:2.14.2")
implementation("com.squareup.okhttp3:okhttp:4.9.3")
implementation("com.anthropic:anthropic-java:0.8.0")
implementation("io.modelcontextprotocol:kotlin-sdk:0.4.0")
}

496
local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/MCPClient.kt

@ -0,0 +1,496 @@
package com.yunqiinnovation.open_ai_service
import android.content.Context
import android.util.Log
import com.anthropic.core.JsonValue
import com.anthropic.models.messages.Tool
import com.anthropic.models.messages.ToolUnion
import com.fasterxml.jackson.core.type.TypeReference
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.ObjectMapper
import io.ktor.client.HttpClient
import io.modelcontextprotocol.kotlin.sdk.Implementation
import io.modelcontextprotocol.kotlin.sdk.TextContent
import io.modelcontextprotocol.kotlin.sdk.client.Client
import io.modelcontextprotocol.kotlin.sdk.client.WebSocketClientTransport
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.json.JsonObject
import org.json.JSONObject
import kotlin.jvm.optionals.getOrNull
/**
* 工具类型枚举
*/
enum class ToolType {
LOCAL_FUNCTION, // 本地函数
MCP_TOOL // MCP工具
}
/**
* 函数处理器接口
*/
interface FunctionHandler {
/**
* 处理函数调用
* @param arguments 函数参数,Map格式
* @return 函数执行结果,字符串
*/
suspend fun handle(arguments: Map<String, Any>): String
}
/**
* MCP客户端,用于连接Model Context Protocol服务器和管理本地函数
*/
class MCPClient(private val context: Context? = null) : AutoCloseable {
private val TAG = "MCPClient"
// 初始化MCP客户端
private val mcp: Client = Client(
clientInfo = Implementation(
name = "deep-voice-mcp-client",
version = "1.0.0"
)
)
// 服务器提供的工具列表
private var tools: List<ToolUnion> = emptyList()
// 工具Map列表,用于OpenAI请求
private val toolMaps = mutableListOf<Map<String, Any>>()
// 本地函数Map,函数名 -> 处理器
private val localFunctions = mutableMapOf<String, FunctionHandler>()
// 本地函数定义Map,函数名 -> 定义
private val localFunctionDefs = mutableMapOf<String, String>()
// 是否已连接
private var isConnected = false
// 系统功能处理器
private val systemFunctionHandler = SystemFunctionHandler(context!!)
init {
// 如果提供了Context,自动注册语音功能处理器
context?.let { initializeVoiceFunctions(it) }
}
/**
* 初始化语音功能处理器
*/
private fun initializeVoiceFunctions(context: Context) {
try {
// 使用SystemFunctionHandler注册所有功能
systemFunctionHandler.registerAllFunctions(this)
Log.d(TAG, "语音功能初始化完成")
} catch (e: Exception) {
Log.e(TAG, "语音功能初始化失败: ${e.message}", e)
}
}
/**
* 将JsonObject转换为JsonValue
*/
private fun JsonObject.toJsonValue(): JsonValue {
val mapper = ObjectMapper()
val node = mapper.readTree(this.toString())
return JsonValue.fromJsonNode(node)
}
/**
* 连接到MCP服务器(通过WebSocket)
*/
suspend fun connectToWebSocket(serverUrl: String): Boolean {
try {
// 创建WebSocket传输层
val transport = WebSocketClientTransport(
client = HttpClient(),
urlString = serverUrl
)
// 连接MCP客户端到服务器
mcp.connect(transport)
// 获取服务器提供的工具列表
val toolsResult = mcp.listTools()
Log.d(TAG, "获取到工具列表: $toolsResult")
// 处理工具列表
processToolsList(toolsResult)
isConnected = true
Log.d(TAG, "已连接到WebSocket服务器,可用工具: ${tools.map { it.tool().get().name() }.joinToString(", ")}")
return true
} catch (e: Exception) {
Log.e(TAG, "连接WebSocket服务器失败: ${e.message}", e)
return false
}
}
/**
* 处理工具列表
*/
private fun processToolsList(toolsResult: io.modelcontextprotocol.kotlin.sdk.ListToolsResult?) {
tools = toolsResult?.tools?.map { tool ->
// 确保输入模式有效
val inputType = tool.inputSchema?.type ?: "object"
val inputProperties = tool.inputSchema?.properties ?: emptyMap()
val inputRequired = tool.inputSchema?.required ?: emptyList<String>()
ToolUnion.ofTool(
Tool.builder()
.name(tool.name)
.description(tool.description ?: "")
.inputSchema(
Tool.InputSchema.builder()
.type(JsonValue.from(inputType))
.properties(if (inputProperties is JsonObject) inputProperties.toJsonValue() else JsonValue.from(emptyMap<String, String>()))
.putAdditionalProperty("required", JsonValue.from(inputRequired))
.build()
)
.build()
)
} ?: emptyList()
// 更新用于OpenAI的工具映射
updateToolMaps()
}
/**
* 注册本地函数
* @param name 函数名称
* @param description 函数描述
* @param parameters 函数参数定义(Map或JSONObject)
* @param handler 函数处理器
* @return 是否注册成功
*/
fun registerLocalFunction(
name: String,
description: String,
parameters: Any,
handler: FunctionHandler
): Boolean {
try {
// 将参数统一转换为Map格式
val parametersMap: Map<String, Any> = when (parameters) {
is Map<*, *> -> {
@Suppress("UNCHECKED_CAST")
parameters as Map<String, Any>
}
is JSONObject -> {
val mapper = ObjectMapper()
mapper.readValue(
parameters.toString(),
object : TypeReference<Map<String, Any>>() {}
)
}
else -> {
Log.e(TAG, "参数类型不支持: ${parameters.javaClass.name}")
return false
}
}
// 检查参数是否包含必要字段
if (!parametersMap.containsKey("type") || (parametersMap["type"] != "object")) {
Log.e(TAG, "参数必须是object类型")
return false
}
// 注册函数处理器
localFunctions[name] = handler
// 创建函数定义
val functionDef = mapOf(
"name" to name,
"description" to description,
"parameters" to parametersMap
)
// 保存函数定义
localFunctionDefs[name] = ObjectMapper().writeValueAsString(functionDef)
// 更新工具映射
updateToolMaps()
Log.d(TAG, "注册本地函数成功: $name")
return true
} catch (e: Exception) {
Log.e(TAG, "注册本地函数失败: ${e.message}", e)
return false
}
}
/**
* 取消注册本地函数
* @param name 函数名称
* @return 是否取消成功
*/
fun unregisterLocalFunction(name: String): Boolean {
val removed = localFunctions.remove(name) != null
if (removed) {
localFunctionDefs.remove(name)
updateToolMaps()
Log.d(TAG, "取消注册本地函数成功: $name")
}
return removed
}
/**
* 更新工具Map列表,用于OpenAI请求
*/
private fun updateToolMaps() {
toolMaps.clear()
// 首先添加本地函数
localFunctionDefs.forEach { (name, functionDefJson) ->
try {
val mapper = ObjectMapper()
val functionDef = mapper.readValue(functionDefJson, object : TypeReference<Map<String, Any>>() {})
val toolMap = mapOf(
"type" to "function",
"function" to functionDef
)
toolMaps.add(toolMap)
} catch (e: Exception) {
Log.e(TAG, "解析本地函数定义失败: $name, ${e.message}", e)
}
}
// 然后添加MCP工具
tools.forEach { toolUnion ->
try {
val tool = toolUnion.tool().get()
val parametersMap = mutableMapOf<String, Any>()
// 提取工具类型及参数
try {
val inputSchema = tool.inputSchema()
// 解析JsonValue
val mapper = ObjectMapper()
val inputSchemaStr = inputSchema.toString()
val inputSchemaJson = mapper.readTree(inputSchemaStr)
val typeValue = inputSchemaJson.path("type")
parametersMap["type"] = if (typeValue.isTextual) typeValue.asText() else "object"
// 提取properties字段
val propertiesMap = mutableMapOf<String, Any>()
val propertiesNode = inputSchemaJson.path("properties")
if (propertiesNode.isObject) {
propertiesNode.fieldNames().forEach { fieldName ->
val fieldNode = propertiesNode.get(fieldName)
propertiesMap[fieldName] = convertJsonNodeToMap(fieldNode)
}
}
parametersMap["properties"] = propertiesMap
// 提取required字段
val requiredList = mutableListOf<String>()
val requiredNode = inputSchemaJson.path("required")
if (requiredNode.isArray) {
for (i in 0 until requiredNode.size()) {
if (requiredNode.get(i).isTextual) {
requiredList.add(requiredNode.get(i).asText())
}
}
}
parametersMap["required"] = requiredList
} catch (e: Exception) {
Log.e(TAG, "解析工具参数失败: ${e.message}", e)
// 设置默认参数
parametersMap["type"] = "object"
parametersMap["properties"] = emptyMap<String, Any>()
parametersMap["required"] = emptyList<String>()
}
// 创建工具Map
val toolMap = mapOf(
"type" to "function",
"function" to mapOf(
"name" to tool.name(),
"description" to (tool.description() ?: ""),
"parameters" to parametersMap
)
)
toolMaps.add(toolMap)
} catch (e: Exception) {
Log.e(TAG, "转换工具到Map失败: ${e.message}", e)
}
}
}
/**
* 将JsonNode转换为Map
*/
private fun convertJsonNodeToMap(node: JsonNode): Any {
return when {
node.isTextual() -> node.asText()
node.isNumber() -> node.asDouble()
node.isBoolean() -> node.asBoolean()
node.isObject() -> {
val result = mutableMapOf<String, Any>()
node.fieldNames().forEach { fieldName ->
result[fieldName] = convertJsonNodeToMap(node.get(fieldName))
}
result
}
node.isArray() -> {
val result = mutableListOf<Any>()
for (i in 0 until node.size()) {
result.add(convertJsonNodeToMap(node.get(i)))
}
result
}
node.isNull() -> "null"
else -> node.toString()
}
}
/**
* 获取工具Map列表,用于OpenAI请求
*/
fun getToolMaps(): List<Map<String, Any>> {
return toolMaps
}
/**
* 获取工具类型
* @param name 工具名称
* @return 工具类型,如果不存在则返回null
*/
fun getToolType(name: String): ToolType? {
if (localFunctions.containsKey(name)) {
return ToolType.LOCAL_FUNCTION
}
if (tools.any { it.tool().get().name() == name }) {
return ToolType.MCP_TOOL
}
return null
}
/**
* 调用工具(统一入口)
* @param name 工具名称
* @param arguments 参数
* @return 调用结果
*/
suspend fun callTool(name: String, arguments: Map<String, Any>): String? {
val toolType = getToolType(name)
return when (toolType) {
ToolType.LOCAL_FUNCTION -> {
callLocalFunction(name, arguments)
}
ToolType.MCP_TOOL -> {
callMcpTool(name, arguments)
}
null -> {
Log.e(TAG, "未找到工具: $name")
null
}
}
}
/**
* 调用本地函数
*/
private suspend fun callLocalFunction(name: String, arguments: Map<String, Any>): String? {
return try {
val handler = localFunctions[name]
if (handler != null) {
handler.handle(arguments)
} else {
Log.e(TAG, "未找到本地函数: $name")
null
}
} catch (e: Exception) {
Log.e(TAG, "调用本地函数失败: ${e.message}", e)
"调用本地函数失败: ${e.message}"
}
}
/**
* 调用MCP工具
*/
private suspend fun callMcpTool(name: String, arguments: Map<String, Any>): String? {
if (!isConnected) {
Log.e(TAG, "未连接到MCP服务器", null)
return "未连接到MCP服务器"
}
return try {
val result = mcp.callTool(
name = name,
arguments = arguments
)
// 处理工具调用结果
result?.content?.joinToString("\n") { content ->
when (content) {
is TextContent -> content.text ?: ""
else -> ""
}
}
} catch (e: Exception) {
Log.e(TAG, "调用MCP工具失败: ${e.message}", e)
"调用MCP工具失败: ${e.message}"
}
}
/**
* 获取可用的工具列表
*/
fun getTools(): List<ToolUnion> {
return tools
}
/**
* 检查工具是否存在
*/
fun hasToolWithName(name: String): Boolean {
return localFunctions.containsKey(name) || toolMaps.any {
val functionMap = it["function"] as? Map<String, Any>
functionMap?.get("name") == name
}
}
/**
* 从字符串解析JSON参数
*/
fun parseJsonArguments(json: String): Map<String, Any> {
return try {
ObjectMapper().readValue(json, object : TypeReference<Map<String, Any>>() {})
} catch (e: Exception) {
Log.e(TAG, "解析JSON参数失败: ${e.message}", e)
emptyMap()
}
}
/**
* 检查是否已连接到MCP服务器
*/
fun isConnected(): Boolean {
return isConnected
}
/**
* 关闭连接
*/
override fun close() {
runBlocking {
try {
mcp.close()
isConnected = false
tools = emptyList()
toolMaps.clear()
Log.d(TAG, "已关闭MCP服务器连接", null)
} catch (e: Exception) {
Log.e(TAG, "关闭MCP服务器连接失败: ${e.message}", e)
}
}
}
}

228
local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/OpenAIService.kt

@ -1,6 +1,13 @@
package com.yunqiinnovation.open_ai_service
import android.content.Context
import android.util.Log
import com.fasterxml.jackson.core.type.TypeReference
import com.fasterxml.jackson.databind.ObjectMapper
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import okhttp3.*
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.RequestBody.Companion.toRequestBody
@ -9,11 +16,13 @@ import org.json.JSONObject
import java.io.IOException
import java.util.concurrent.TimeUnit
/**
* OpenAI服务的原生实现
*/
class OpenAIService() {
class OpenAIService(private val context: Context? = null) {
private val TAG = "OpenAIService"
private var baseUrl = ""
private val client = OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
@ -25,8 +34,12 @@ class OpenAIService() {
private var isInitialized = false
private var model: String = "" // 默认模型
// 用于存储注册的函数
private val registeredFunctions = mutableListOf<JSONObject>()
// MCP客户端
private var mcpClient: MCPClient? = null
private var isMcpInitialized = false
// 是否自动处理MCP工具调用
private var autoHandleMcpTools = true
/**
* 构建curl命令用于测试
@ -88,40 +101,151 @@ class OpenAIService() {
if (model.isNotEmpty()) {
this.model = model
}
// 初始化MCPClient
if (mcpClient == null) {
mcpClient = MCPClient(context)
}
isInitialized = apiKey.isNotEmpty()
return isInitialized
}
/**
* MCP客户端是否已初始化
*/
fun isMcpInitialized(): Boolean {
return isMcpInitialized && mcpClient?.isConnected() == true
}
/**
* 关闭MCP客户端
*/
fun closeMcpClient() {
mcpClient?.close()
mcpClient = null
isMcpInitialized = false
}
/**
* 初始化MCP客户端(WebSocket方式)
*/
fun initializeMcpClient(serverUrl: String): Boolean {
if (mcpClient != null) {
mcpClient?.close()
}
mcpClient = MCPClient(context)
// 在后台线程中初始化MCP客户端
val scope = CoroutineScope(Dispatchers.IO)
var result = false
scope.launch {
try {
result = mcpClient?.connectToWebSocket(serverUrl) ?: false
isMcpInitialized = result
Log.d(TAG, "MCP客户端初始化${if (result) "成功" else "失败"}")
} catch (e: Exception) {
Log.e(TAG, "MCP客户端初始化失败: ${e.message}", e)
isMcpInitialized = false
}
}
return true // 立即返回,实际连接在后台进行
}
/**
* 处理MCP工具调用
*
* @param functionCall 函数调用JSON对象
* @return 处理结果,如果处理失败则返回null
*/
suspend fun handleMcpToolCall(functionCall: JSONObject): String? {
if (mcpClient == null || !isMcpInitialized) {
return "MCP客户端未初始化"
}
try {
// 获取函数名称
val name = functionCall.getString("name")
// 获取参数
val argumentsJson = functionCall.getString("arguments")
val arguments = mcpClient?.parseJsonArguments(argumentsJson) ?: mapOf()
// 调用工具
return mcpClient?.callTool(name, arguments)
} catch (e: Exception) {
Log.e(TAG, "处理MCP工具调用失败: ${e.message}", e)
return "处理MCP工具调用失败: ${e.message}"
}
}
/**
* 注册函数
*/
fun registerFunction(name: String, description: String, parameters: JSONObject): Boolean {
try {
val function = JSONObject().apply {
put("name", name)
put("description", description)
put("parameters", parameters)
}
// 检查是否已存在相同名称的函数
val existingIndex = registeredFunctions.indexOfFirst {
it.getString("name") == name
// 确保MCPClient已初始化
if (mcpClient == null) {
mcpClient = MCPClient()
}
if (existingIndex >= 0) {
// 如果已存在,则替换
registeredFunctions[existingIndex] = function
} else {
// 如果不存在,则添加
registeredFunctions.add(function)
// 创建函数处理器
val handler = object : FunctionHandler {
override suspend fun handle(arguments: Map<String, Any>): String {
// 由于本地函数的实际处理是在Flutter端完成的
// 这里只需返回一个标记,表示该函数是本地函数
return "LOCAL_FUNCTION:$name"
}
}
return true
// 直接委托给MCPClient处理
return mcpClient?.registerLocalFunction(name, description, parameters, handler) ?: false
} catch (e: Exception) {
Log.e(TAG, "注册函数失败: ${e.message}", e)
return false
}
}
/**
* 自动处理MCP工具调用
*/
private suspend fun autoHandleMcpToolCall(functionCall: JSONObject, messages: JSONArray, callback: StreamCallback) {
try {
// 获取函数名称
val name = functionCall.getString("name")
// 获取参数
val argumentsJson = functionCall.getString("arguments")
val arguments = mcpClient?.parseJsonArguments(argumentsJson) ?: mapOf()
// 调用工具(统一通过MCPClient处理)
val result = mcpClient?.callTool(name, arguments)
// 不论是本地函数还是MCP工具,都直接将结果发送回OpenAI
sendFunctionCallResult(
messages = messages,
functionCall = functionCall,
functionResult = result ?: "工具调用失败",
callback = callback
)
} catch (e: Exception) {
Log.e(TAG, "自动处理工具调用失败: ${e.message}", e)
// 失败时返回错误给回调函数
val errorMessage = "工具调用失败: ${e.message}"
sendFunctionCallResult(
messages = messages,
functionCall = functionCall,
functionResult = errorMessage,
callback = callback
)
}
}
/**
* 发送消息(非流式输出)
*/
@ -138,16 +262,21 @@ class OpenAIService() {
put("max_tokens", 2000)
put("stream", false)
// 如果有注册的函数,则添加到请求中
if (registeredFunctions.isNotEmpty()) {
val tools = JSONArray()
for (function in registeredFunctions) {
val tool = JSONObject().apply {
put("type", "function")
put("function", function)
}
// 添加工具列表
val tools = JSONArray()
// 使用MCPClient提供的所有工具
mcpClient?.getToolMaps()?.forEach { toolMap ->
try {
val tool = JSONObject(toolMap)
tools.put(tool)
} catch (e: Exception) {
Log.e(TAG, "转换工具失败: ${e.message}", e)
}
}
// 如果有工具,则添加到请求中
if (tools.length() > 0) {
put("tools", tools)
}
}
@ -231,16 +360,21 @@ class OpenAIService() {
put("max_tokens", 2000)
put("stream", true)
// 如果有注册的函数,则添加到请求中
if (registeredFunctions.isNotEmpty()) {
val tools = JSONArray()
for (function in registeredFunctions) {
val tool = JSONObject().apply {
put("type", "function")
put("function", function)
}
// 添加工具列表
val tools = JSONArray()
// 使用MCPClient提供的所有工具
mcpClient?.getToolMaps()?.forEach { toolMap ->
try {
val tool = JSONObject(toolMap)
tools.put(tool)
} catch (e: Exception) {
Log.e(TAG, "转换工具失败: ${e.message}", e)
}
}
// 如果有工具,则添加到请求中
if (tools.length() > 0) {
put("tools", tools)
}
}
@ -289,7 +423,7 @@ class OpenAIService() {
// 处理[DONE]消息
if (data == "[DONE]" || data == "[\"DONE\"]") {
processToolCalls(finalToolCalls, callback)
processToolCalls(finalToolCalls, callback, messages)
callback.onComplete()
break
}
@ -403,12 +537,6 @@ class OpenAIService() {
put("tool_call_id", functionCall.optString("id", "call_${System.currentTimeMillis()}"))
})
// 添加一个带有content的assistant消息,确保API请求不会因为缺少content而失败
fullMessages.put(JSONObject().apply {
put("role", "assistant")
put("content", "") // 空内容,让模型生成新的回复
})
// 发送完整对话
sendMessageStream(fullMessages, callback)
@ -418,9 +546,9 @@ class OpenAIService() {
}
/**
* 处理工具调用(处理完整的函数调用并回调)
* 处理工具调用结果
*/
private fun processToolCalls(toolCalls: Map<Int, ToolCallInfo>, callback: StreamCallback) {
private fun processToolCalls(toolCalls: Map<Int, ToolCallInfo>, callback: StreamCallback, messages: JSONArray? = null) {
if (toolCalls.isEmpty()) return
// 只处理第一个工具调用
@ -434,8 +562,16 @@ class OpenAIService() {
put("id", firstToolCall.id)
}
// 回调
callback.onFunctionCall(functionCall)
// 在协程中处理工具调用
if (messages != null) {
kotlinx.coroutines.GlobalScope.launch {
try {
autoHandleMcpToolCall(functionCall, messages, callback)
} catch (e: Exception) {
Log.e(TAG, "工具调用处理失败: ${e.message}", e)
}
}
}
}
}

65
local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/OpenAIServicePlugin.kt

@ -15,6 +15,10 @@ import org.json.JSONArray
import org.json.JSONObject
import java.util.concurrent.CountDownLatch
import java.util.concurrent.Executors
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
/** OpenAIServicePlugin */
class OpenAIServicePlugin : FlutterPlugin, MethodCallHandler, StreamHandler {
@ -34,7 +38,7 @@ class OpenAIServicePlugin : FlutterPlugin, MethodCallHandler, StreamHandler {
private lateinit var context: Context
/// OpenAI服务实例
private val openAIService = OpenAIService()
private lateinit var openAIService: OpenAIService
/// 事件接收器(用于流式输出)
private var eventSink: EventSink? = null
@ -46,6 +50,9 @@ class OpenAIServicePlugin : FlutterPlugin, MethodCallHandler, StreamHandler {
// 保存上下文
context = flutterPluginBinding.applicationContext
// 初始化OpenAI服务
openAIService = OpenAIService(context)
// 初始化方法通道
methodChannel = MethodChannel(flutterPluginBinding.binaryMessenger, methodChannelName)
methodChannel.setMethodCallHandler(this)
@ -66,6 +73,7 @@ class OpenAIServicePlugin : FlutterPlugin, MethodCallHandler, StreamHandler {
result.success(initialized)
}
"registerFunction" -> {
val name = call.argument<String>("name") ?: ""
val description = call.argument<String>("description") ?: ""
@ -256,6 +264,11 @@ class OpenAIServicePlugin : FlutterPlugin, MethodCallHandler, StreamHandler {
result.success(message.toMap())
}
"initializeMcpClient" -> initializeMcpClient(call, result)
"isMcpInitialized" -> isMcpInitialized(call, result)
"closeMcpClient" -> closeMcpClient(call, result)
"handleMcpToolCall" -> handleMcpToolCall(call, result)
else -> {
result.notImplemented()
}
@ -314,4 +327,54 @@ class OpenAIServicePlugin : FlutterPlugin, MethodCallHandler, StreamHandler {
}
return list
}
private fun initializeMcpClient(call: MethodCall, result: Result) {
val serverUrl = call.argument<String>("serverUrl")
if (serverUrl == null) {
result.error("INVALID_ARGUMENT", "Missing serverUrl", null)
return
}
val success = openAIService.initializeMcpClient(serverUrl)
result.success(success)
}
private fun isMcpInitialized(call: MethodCall, result: Result) {
result.success(openAIService.isMcpInitialized())
}
private fun closeMcpClient(call: MethodCall, result: Result) {
openAIService.closeMcpClient()
result.success(true)
}
private fun handleMcpToolCall(call: MethodCall, result: Result) {
val functionCall = call.argument<String>("functionCall")
if (functionCall == null) {
result.error("INVALID_ARGUMENT", "Missing functionCall", null)
return
}
try {
val jsonFunctionCall = JSONObject(functionCall)
// 使用协程处理异步操作
CoroutineScope(Dispatchers.IO).launch {
try {
val toolResult = openAIService.handleMcpToolCall(jsonFunctionCall)
// 切换到主线程返回结果
withContext(Dispatchers.Main) {
result.success(toolResult ?: "无法处理MCP工具调用")
}
} catch (e: Exception) {
withContext(Dispatchers.Main) {
result.error("MCP_TOOL_ERROR", "处理MCP工具调用失败: ${e.message}", null)
}
}
}
} catch (e: Exception) {
result.error("INVALID_ARGUMENT", "解析functionCall失败: ${e.message}", null)
}
}
}

422
local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/SystemFunctionHandler.kt

@ -0,0 +1,422 @@
package com.yunqiinnovation.open_ai_service
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.provider.ContactsContract
import android.telephony.SmsManager
import android.util.Log
import org.json.JSONObject
/**
* 系统功能处理器 - 负责调用系统API执行各种功能以及注册系统功能
*/
class SystemFunctionHandler(private val context: Context) {
companion object {
private const val TAG = "SystemFunctionHandler"
const val ACTION_EXIT_INTERACTION = "com.yunqiinnovation.deepsound.action.EXIT_INTERACTION"
}
/**
* 在MCPClient中注册所有系统功能
* @param client MCPClient实例
*/
fun registerAllFunctions(client: MCPClient) {
try {
// 注册退出交互函数
registerExitInteractionFunction(client)
// 注册翻译模式函数
registerTranslationModeFunction(client)
// 注册发送短信函数
registerSendTextMessageFunction(client)
// 注册拨打电话函数
registerMakePhoneCallFunction(client)
// 注册打开应用函数
registerOpenAppFunction(client)
Log.d(TAG, "所有系统功能注册完成")
} catch (e: Exception) {
Log.e(TAG, "注册系统功能失败: ${e.message}", e)
}
}
/**
* 注册退出交互函数
*/
private fun registerExitInteractionFunction(client: MCPClient) {
client.registerLocalFunction(
"exit_interaction",
"退出当前语音交互",
JSONObject("""
{
"type": "object",
"properties": {},
"required": []
}
"""),
object : FunctionHandler {
override suspend fun handle(arguments: Map<String, Any>): String {
// 调用发送广播的函数
sendExitInteractionBroadcast()
return "{\"result\": \"已退出语音交互\"}"
}
}
)
Log.d(TAG, "已注册退出交互功能")
}
/**
* 注册翻译模式函数
*/
private fun registerTranslationModeFunction(client: MCPClient) {
client.registerLocalFunction(
"enter_translation_mode",
"进入实时翻译模式",
JSONObject("""
{
"type": "object",
"properties": {},
"required": []
}
"""),
object : FunctionHandler {
override suspend fun handle(arguments: Map<String, Any>): String {
val result = enterTranslationMode()
return "{\"result\": \"${result.message}\"}"
}
}
)
Log.d(TAG, "已注册翻译模式功能")
}
/**
* 注册发送短信函数
*/
private fun registerSendTextMessageFunction(client: MCPClient) {
client.registerLocalFunction(
"send_text_message",
"发送短信",
JSONObject("""
{
"type": "object",
"properties": {
"contact": {
"type": "string",
"description": "联系人姓名或电话号码"
},
"message": {
"type": "string",
"description": "短信内容"
}
},
"required": ["contact", "message"]
}
"""),
object : FunctionHandler {
override suspend fun handle(arguments: Map<String, Any>): String {
val contact = arguments["contact"] as? String ?: ""
val message = arguments["message"] as? String ?: ""
val result = sendTextMessage(contact, message)
return "{\"result\": \"${result.message}\"}"
}
}
)
Log.d(TAG, "已注册发送短信功能")
}
/**
* 注册拨打电话函数
*/
private fun registerMakePhoneCallFunction(client: MCPClient) {
client.registerLocalFunction(
"make_phone_call",
"拨打电话",
JSONObject("""
{
"type": "object",
"properties": {
"contact": {
"type": "string",
"description": "联系人姓名或电话号码"
}
},
"required": ["contact"]
}
"""),
object : FunctionHandler {
override suspend fun handle(arguments: Map<String, Any>): String {
val contact = arguments["contact"] as? String ?: ""
val result = makePhoneCall(contact)
return "{\"result\": \"${result.message}\"}"
}
}
)
Log.d(TAG, "已注册拨打电话功能")
}
/**
* 注册打开应用函数
*/
private fun registerOpenAppFunction(client: MCPClient) {
client.registerLocalFunction(
"open_app",
"打开应用",
JSONObject("""
{
"type": "object",
"properties": {
"app_name": {
"type": "string",
"description": "应用名称"
}
},
"required": ["app_name"]
}
"""),
object : FunctionHandler {
override suspend fun handle(arguments: Map<String, Any>): String {
val appName = arguments["app_name"] as? String ?: ""
val result = openApp(appName)
return "{\"result\": \"${result.message}\"}"
}
}
)
Log.d(TAG, "已注册打开应用功能")
}
/**
* 发送退出交互广播
*/
private fun sendExitInteractionBroadcast() {
// 发送广播通知VoiceInteractionHandler停止语音识别
val intent = Intent(ACTION_EXIT_INTERACTION).apply {
// 明确指定接收广播的包名
setPackage(context.packageName)
putExtra("timestamp", System.currentTimeMillis())
// 添加FLAG_RECEIVER_FOREGROUND确保及时处理
addFlags(Intent.FLAG_RECEIVER_FOREGROUND)
}
Log.d(TAG, "准备发送停止交互广播,包名: ${context.packageName}")
context.sendBroadcast(intent)
Log.d(TAG, "已发送停止交互广播")
}
/**
* 启动翻译模式
* @return 操作结果
*/
fun enterTranslationMode(): Result {
Log.d(TAG, "进入翻译模式")
// 此处应添加实际的翻译模式实现
// 可能需要启动一个专门的翻译服务或活动
// 广播通知APP的其他部分进入翻译模式
val intent = Intent("com.yunqiinnovation.deepsound.ACTION_ENTER_TRANSLATION_MODE").apply {
setPackage(context.packageName)
putExtra("timestamp", System.currentTimeMillis())
// 添加FLAG_RECEIVER_FOREGROUND确保及时处理
addFlags(Intent.FLAG_RECEIVER_FOREGROUND)
}
// 发送广播
context.sendBroadcast(intent)
Log.d(TAG, "已发送广播: ENTER_TRANSLATION_MODE")
return Result(
success = true,
message = "已进入实时翻译模式"
)
}
/**
* 发送短信
* @param contact 联系人(名称或电话号码)
* @param message 短信内容
* @return 操作结果
*/
fun sendTextMessage(contact: String, message: String): Result {
Log.d(TAG, "发送短信给 $contact: $message")
try {
val phoneNumber = getPhoneNumberForContact(contact)
if (phoneNumber.isEmpty()) {
return Result(
success = false,
message = "未找到联系人信息: $contact"
)
}
// 使用SmsManager发送短信
val smsManager = SmsManager.getDefault()
smsManager.sendTextMessage(phoneNumber, null, message, null, null)
return Result(
success = true,
message = "已向${contact}发送短信: $message"
)
} catch (e: Exception) {
Log.e(TAG, "发送短信失败: ${e.message}", e)
return Result(
success = false,
message = "发送短信失败: ${e.message}"
)
}
}
/**
* 拨打电话
* @param contact 联系人(名称或电话号码)
* @return 操作结果
*/
fun makePhoneCall(contact: String): Result {
Log.d(TAG, "拨打电话给 $contact")
try {
val phoneNumber = getPhoneNumberForContact(contact)
if (phoneNumber.isEmpty()) {
return Result(
success = false,
message = "未找到联系人信息: $contact"
)
}
// 创建拨号意图
val intent = Intent(Intent.ACTION_CALL)
intent.data = Uri.parse("tel:$phoneNumber")
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
context.startActivity(intent)
return Result(
success = true,
message = "正在拨打${contact}的电话"
)
} catch (e: Exception) {
Log.e(TAG, "拨打电话失败: ${e.message}", e)
return Result(
success = false,
message = "拨打电话失败: ${e.message}"
)
}
}
/**
* 打开应用程序
* @param appName 应用名称
* @return 操作结果
*/
fun openApp(appName: String): Result {
Log.d(TAG, "打开应用: $appName")
try {
// 查找应用的包名
val packageManager = context.packageManager
val intent = Intent(Intent.ACTION_MAIN, null)
intent.addCategory(Intent.CATEGORY_LAUNCHER)
val apps = packageManager.queryIntentActivities(intent, 0)
for (resolveInfo in apps) {
val appLabel = resolveInfo.loadLabel(packageManager).toString()
if (appLabel.equals(appName, ignoreCase = true) ||
appLabel.contains(appName, ignoreCase = true)) {
// 找到匹配的应用
val packageName = resolveInfo.activityInfo.packageName
val launchIntent = packageManager.getLaunchIntentForPackage(packageName)
if (launchIntent != null) {
launchIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
context.startActivity(launchIntent)
return Result(
success = true,
message = "正在打开${appName}应用"
)
}
}
}
return Result(
success = false,
message = "未找到应用: $appName"
)
} catch (e: Exception) {
Log.e(TAG, "打开应用失败: ${e.message}", e)
return Result(
success = false,
message = "打开应用失败: ${e.message}"
)
}
}
/**
* 根据联系人名称获取电话号码
* @param contact 联系人名称或电话号码
* @return 电话号码
*/
private fun getPhoneNumberForContact(contact: String): String {
// 如果输入的是电话号码(简单判断,包含数字和特殊符号)
if (contact.matches(Regex("^[+\\d\\s\\-()]*\$")) && contact.any { it.isDigit() }) {
// 移除非数字字符
return contact.replace(Regex("[^+\\d]"), "")
}
// 从联系人数据库查询
try {
val contentResolver = context.contentResolver
val uri = Uri.withAppendedPath(
ContactsContract.Contacts.CONTENT_FILTER_URI,
Uri.encode(contact)
)
val projection = arrayOf(
ContactsContract.Contacts._ID,
ContactsContract.Contacts.DISPLAY_NAME,
ContactsContract.Contacts.HAS_PHONE_NUMBER
)
contentResolver.query(uri, projection, null, null, null)?.use { cursor ->
if (cursor.moveToFirst()) {
val hasPhoneNumber = cursor.getInt(cursor.getColumnIndexOrThrow(ContactsContract.Contacts.HAS_PHONE_NUMBER))
if (hasPhoneNumber > 0) {
val contactId = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.Contacts._ID))
val phoneUri = ContactsContract.CommonDataKinds.Phone.CONTENT_URI
val phoneProjection = arrayOf(ContactsContract.CommonDataKinds.Phone.NUMBER)
val phoneSelection = "${ContactsContract.CommonDataKinds.Phone.CONTACT_ID} = ?"
val phoneSelectionArgs = arrayOf(contactId)
contentResolver.query(phoneUri, phoneProjection, phoneSelection, phoneSelectionArgs, null)?.use { phoneCursor ->
if (phoneCursor.moveToFirst()) {
return phoneCursor.getString(phoneCursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.NUMBER))
}
}
}
}
}
} catch (e: Exception) {
Log.e(TAG, "查询联系人信息失败: ${e.message}", e)
}
return ""
}
/**
* 操作结果数据类
*/
data class Result(
val success: Boolean,
val message: String
)
}

101
local_plugins/open_ai_service/lib/open_ai_service.dart

@ -264,4 +264,105 @@ class OpenAIService {
throw OpenAIException('解析函数调用失败: $e');
}
}
/// 初始化MCP客户端
///
/// [serverUrl] MCP服务器WebSocket地址
///
/// 返回一个布尔值,表示请求是否已开始初始化(实际初始化过程是异步的)
Future<bool> initializeMcpClient(String serverUrl) async {
try {
final result = await _channel.invokeMethod<bool>(
'initializeMcpClient',
{'serverUrl': serverUrl},
);
return result ?? false;
} catch (e) {
throw OpenAIException('初始化MCP客户端失败: $e');
}
}
/// 检查MCP客户端是否已初始化
Future<bool> isMcpInitialized() async {
try {
final result = await _channel.invokeMethod<bool>('isMcpInitialized');
return result ?? false;
} catch (e) {
throw OpenAIException('检查MCP客户端状态失败: $e');
}
}
/// 关闭MCP客户端
Future<bool> closeMcpClient() async {
try {
final result = await _channel.invokeMethod<bool>('closeMcpClient');
return result ?? false;
} catch (e) {
throw OpenAIException('关闭MCP客户端失败: $e');
}
}
/// 处理MCP工具调用
///
/// [functionCall] 函数调用信息,必须包含name和arguments字段
///
/// 返回工具调用结果
Future<String> handleMcpToolCall(Map<String, dynamic> functionCall) async {
try {
final result = await _channel.invokeMethod<String>(
'handleMcpToolCall',
{'functionCall': jsonEncode(functionCall)},
);
return result ?? "处理MCP工具调用失败: 结果为空";
} catch (e) {
throw OpenAIException('处理MCP工具调用失败: $e');
}
}
/// 处理函数调用(自动处理本地函数和MCP工具)
///
/// 这是一个辅助方法,用于简化函数调用处理流程:
/// 1. MCP工具会被自动处理,不会通过onFunctionCall回调返回
/// 2. 只有本地函数才需要手动处理
/// 3. 提供统一的处理接口
///
/// 参数:
/// [functionCall] 函数调用信息
/// [messages] 消息上下文
/// [localFunctionHandler] 本地函数处理器,接收函数名和参数,返回处理结果
///
/// 返回值:
/// 布尔值表示是否成功处理(已将结果发送回OpenAI)
Future<bool> handleFunctionCall({
required Map<String, dynamic> functionCall,
required List<Map<String, dynamic>> messages,
required Future<String> Function(String name, Map<String, dynamic> arguments) localFunctionHandler,
}) async {
final name = functionCall['name'] as String;
final argumentsJson = functionCall['arguments'] as String;
// 解析参数
Map<String, dynamic> arguments;
try {
arguments = json.decode(argumentsJson) as Map<String, dynamic>;
} catch (e) {
arguments = <String, dynamic>{};
}
try {
// 调用本地函数处理器
final result = await localFunctionHandler(name, arguments);
// 发送结果回OpenAI
return sendFunctionCallResult(
messages: messages,
functionCall: functionCall,
functionResult: result,
);
} catch (e) {
throw OpenAIException('处理函数调用失败: $e');
}
}
}

4
local_plugins/volcano_speech/android/build.gradle.kts

@ -7,7 +7,7 @@ buildscript {
}
dependencies {
classpath("com.android.tools.build:gradle:7.3.0")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.7.10")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:2.1.0")
}
}
@ -21,7 +21,7 @@ allprojects {
plugins {
id("com.android.library")
kotlin("android")
id("org.jetbrains.kotlin.android")
}
// 配置android扩展

39
local_plugins/volcano_speech/android/src/main/kotlin/com/yunqiinnovation/volcano_speech/VolcanoAsrHelper.kt

@ -58,7 +58,7 @@ class VolcanoAsrHelper(private val context: Context) {
/**
* 初始化语音识别引擎
*/
fun initialize(appId: String, token: String, resourceId: String): Boolean {
fun initialize(appId: String, token: String, resourceId: String = "volc.bigasr.sauc.duration"): Boolean {
if (isInitialized) {
FileLogger.i(TAG, "引擎已经初始化")
return true
@ -66,7 +66,12 @@ class VolcanoAsrHelper(private val context: Context) {
try {
// 准备环境
SpeechEngineGenerator.PrepareEnvironment(context, null)
try {
SpeechEngineGenerator.PrepareEnvironment(context, null)
} catch (e: Exception) {
FileLogger.w(TAG, "准备环境时发生异常: ${e.message},尝试继续初始化")
// 即使PrepareEnvironment失败仍继续执行,因为某些环境下可能找不到特定类
}
// 创建引擎
engine = SpeechEngineGenerator.getInstance()
@ -134,7 +139,17 @@ class VolcanoAsrHelper(private val context: Context) {
return true
} else {
FileLogger.e(TAG, "引擎初始化失败: $result")
// 错误码说明:
// -1:通用错误
// -2:无效参数
// -3:无效状态
// -4:无效引擎
// -5:操作失败
// -6:缺少配置
// -7:参数无效
// -8:超时
// -9:资源不足
FileLogger.e(TAG, "引擎初始化失败: $result, 可能原因: ${getErrorDescription(result)}")
return false
}
} catch (e: Exception) {
@ -526,4 +541,22 @@ class VolcanoAsrHelper(private val context: Context) {
}
}
}
/**
* 获取错误描述
*/
private fun getErrorDescription(errorCode: Int?): String {
return when (errorCode) {
-1 -> "通用错误"
-2 -> "无效参数,检查appId、token、resourceId等参数是否正确"
-3 -> "无效状态,引擎状态异常"
-4 -> "无效引擎句柄"
-5 -> "操作失败,可能网络连接问题"
-6 -> "缺少配置参数"
-7 -> "参数设置无效,请检查appId、token等参数是否有效"
-8 -> "操作超时"
-9 -> "资源不足"
else -> "未知错误: $errorCode"
}
}
}

57
local_plugins/volcano_speech/android/src/main/kotlin/com/yunqiinnovation/volcano_speech/VolcanoTtsHelper.kt

@ -29,7 +29,7 @@ class VolcanoTtsHelper(private val context: Context) {
private var currentReqId = ""
// 配置参数
private var voice = "zh_female_yuxi"
private var voice = "zh_male_M392_conversation_wvae_bigtts"
private var ttsText = ""
private var isContinuous = false
@ -51,10 +51,15 @@ class VolcanoTtsHelper(private val context: Context) {
FileLogger.i(TAG, "引擎已经初始化")
return true
}
FileLogger.d(TAG, "初始化语音合成引擎: $appId, $token, $resourceId")
try {
// 准备环境
SpeechEngineGenerator.PrepareEnvironment(context, null)
try {
SpeechEngineGenerator.PrepareEnvironment(context, null)
} catch (e: Exception) {
FileLogger.w(TAG, "准备环境时发生异常: ${e.message},尝试继续初始化")
// 即使PrepareEnvironment失败仍继续执行,因为某些环境下可能找不到特定类
}
// 创建引擎
engine = SpeechEngineGenerator.getInstance()
@ -72,15 +77,15 @@ class VolcanoTtsHelper(private val context: Context) {
engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_ENGINE_NAME_STRING, SpeechEngineDefines.TTS_ENGINE)
// 设置日志级别
engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_LOG_LEVEL_STRING, SpeechEngineDefines.LOG_LEVEL_WARN)
engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_LOG_LEVEL_STRING, SpeechEngineDefines.LOG_LEVEL_DEBUG)
// 设置用户ID和设备ID (使用静态值,实际项目中应替换为真实值)
engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_UID_STRING, "user_id")
engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_DEVICE_ID_STRING, "device_id")
// 设置授权信息 - 大模型版本不需要Bearer前缀
FileLogger.d(TAG, "设置授权信息: $appId, $token, $resourceId")
engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_APP_ID_STRING, appId)
engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_APP_TOKEN_STRING, token)
engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_APP_TOKEN_STRING, "Bearer;$token")
// 设置资源ID - 大模型必需
engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_RESOURCE_ID_STRING, resourceId)
@ -93,14 +98,19 @@ class VolcanoTtsHelper(private val context: Context) {
// 设置在线请求资源配置
engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_TTS_ADDRESS_STRING, "wss://openspeech.bytedance.com")
engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_TTS_URI_STRING, "/api/v3/sauc/bigmodel")
engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_TTS_URI_STRING, "/api/v1/tts/ws_binary")
//【必需配置】语音合成服务所用集群
engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_TTS_CLUSTER_STRING, "volcano_tts")
// 设置发音人
engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_TTS_VOICE_ONLINE_STRING, voice)
// 设置播放进度回调
engine?.setOptionInt(engineHandler, SpeechEngineDefines.PARAMS_KEY_TTS_WITH_FRONTEND_INT, 1)
engine?.setOptionInt(engineHandler, SpeechEngineDefines.PARAMS_KEY_AUDIO_STREAM_TYPE_INT, SpeechEngineDefines.AUDIO_STREAM_TYPE_MEDIA);
engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_TTS_SCENARIO_STRING, SpeechEngineDefines.TTS_SCENARIO_TYPE_NORMAL)
// 初始化引擎
val result = engine?.initEngine(engineHandler)
isInitialized = result == SpeechEngineDefines.ERR_NO_ERROR
@ -118,7 +128,17 @@ class VolcanoTtsHelper(private val context: Context) {
return true
} else {
FileLogger.e(TAG, "引擎初始化失败: $result")
// 错误码说明:
// -1:通用错误
// -2:无效参数
// -3:无效状态
// -4:无效引擎
// -5:操作失败
// -6:缺少配置
// -7:参数无效
// -8:超时
// -9:资源不足
FileLogger.e(TAG, "引擎初始化失败: $result, 可能原因: ${getErrorDescription(result)}")
return false
}
} catch (e: Exception) {
@ -174,6 +194,7 @@ class VolcanoTtsHelper(private val context: Context) {
* 开始合成并播放
*/
fun speak(text: String, callback: TTSCallback? = null): Boolean {
FileLogger.d(TAG, "开始合成并播放: $text")
if (!isInitialized) {
FileLogger.e(TAG, "引擎未初始化")
return false
@ -396,4 +417,22 @@ class VolcanoTtsHelper(private val context: Context) {
}
}
}
/**
* 获取错误描述
*/
private fun getErrorDescription(errorCode: Int?): String {
return when (errorCode) {
-1 -> "通用错误"
-2 -> "无效参数,检查appId、token、resourceId等参数是否正确"
-3 -> "无效状态,引擎状态异常"
-4 -> "无效引擎句柄"
-5 -> "操作失败,可能网络连接问题"
-6 -> "缺少配置参数"
-7 -> "参数设置无效,请检查appId、token等参数是否有效"
-8 -> "操作超时"
-9 -> "资源不足"
else -> "未知错误: $errorCode"
}
}
}

5
pubspec.yaml

@ -66,6 +66,11 @@ dependencies:
path: local_plugins/open_ai_service
volcano_speech:
path: local_plugins/volcano_speech
jieli:
path: local_plugins/jieli
chat_storage_sqlite:
path: local_plugins/chat_storage_sqlite
dev_dependencies:
flutter_test:

58
test.json

@ -1 +1,57 @@
curl -v -X POST -H 'Content-Type: application/json' -H 'Authorization: Bearer 168deb3d-fd0c-4912-b9f1-aaee5c6743e6' -H 'Accept: text/event-stream' -d '{"model":"bot-20250405211523-l7c9r","messages":[{"role":"system","content":" 你是一个智能语音助手,能够简洁明了地回答用户的问题。\n时刻关心用户的情绪和需求,主动提供鼓励和温暖。\n\n语言风格活泼、亲切,能够幽默地互动,陪伴用户,缓解压力,增添生活乐趣。\n\n请始终以用户为中心,保持回应的高效性、准确性和温暖体贴,成为用户真正的灵魂伴侣。\n \n 当用户说\"退出\"、\"再见\"、\"结束对话\"等类似意图时,你应该使用exit_interaction函数来结束对话,\n 并在结束前说一句友好的告别语,例如\"再见,有需要随时找我\"。"},{"role":"system","content":" 你是一个智能语音助手,能够简洁明了地回答用户的问题。\n时刻关心用户的情绪和需求,主动提供鼓励和温暖。\n\n语言风格活泼、亲切,能够幽默地互动,陪伴用户,缓解压力,增添生活乐趣。\n\n请始终以用户为中心,保持回应的高效性、准确性和温暖体贴,成为用户真正的灵魂伴侣。\n \n 当用户说\"退出\"、\"再见\"、\"结束对话\"等类似意图时,你应该使用exit_interaction函数来结束对话,\n 并在结束前说一句友好的告别语,例如\"再见,有需要随时找我\"。"},{"role":"user","content":"退下吧。"},{"role":"assistant","content":"","tool_calls":[{"id":"call_8k680azmfc4thqrrnwpwqxah","type":"function","function":{"name":"exit_interaction","arguments":" {}"}}]},{"role":"tool","content":"{\"result\": \"已退出语音交互\"}","tool_call_id":"call_8k680azmfc4thqrrnwpwqxah"}],"temperature":0.7,"max_tokens":2000,"stream":true,"tools":[{"type":"function","function":{"name":"exit_interaction","description":"退出当前语音交互","parameters":{"type":"object","properties":{},"required":[]}}}]}' 'https://ark.cn-beijing.volces.com/api/v3/bots/chat/completions'
curl -v -X POST -H 'Content-Type: application/json' -H 'Authorization: Bearer 168deb3d-fd0c-4912-b9f1-aaee5c6743e6' -H 'Accept: text/event-stream' -d '{
"model": "bot-20250405211523-l7c9r",
"messages": [
{
"role": "system",
"content": " 你是一个智能语音助手,能够简洁明了地回答用户的问题。\n时刻关心用户的情绪和需求,主动提供鼓励和温暖。\n\n语言风格活泼、亲切,能够幽默地互动,陪伴用户,缓解压力,增添生活乐趣。\n\n请始终以用户为中心,保持回应的高效性、准确性和温暖体贴,成为用户真正的灵魂伴侣。\n \n 当用户说\"退出\"、\"再见\"、\"结束对话\"等类似意图时,你应该使用exit_interaction函数来结束对话,\n 并在结束前说一句友好的告别语,例如\"再见,有需要随时找我\"。"
},
{
"role": "system",
"content": " 你是一个智能语音助手,能够简洁明了地回答用户的问题。\n时刻关心用户的情绪和需求,主动提供鼓励和温暖。\n\n语言风格活泼、亲切,能够幽默地互动,陪伴用户,缓解压力,增添生活乐趣。\n\n请始终以用户为中心,保持回应的高效性、准确性和温暖体贴,成为用户真正的灵魂伴侣。\n \n 当用户说\"退出\"、\"再见\"、\"结束对话\"等类似意图时,你应该使用exit_interaction函数来结束对话,\n 并在结束前说一句友好的告别语,例如\"再见,有需要随时找我\"。"
},
{
"role": "user",
"content": "退下吧。"
},
{
"role": "assistant",
"content": "",
"tool_calls": [
{
"id": "call_8k680azmfc4thqrrnwpwqxah",
"type": "function",
"function": {
"name": "exit_interaction",
"arguments": " {}"
}
}
]
},
{
"role": "tool",
"content": "{\"result\": \"已退出语音交互\"}",
"tool_call_id": "call_8k680azmfc4thqrrnwpwqxah"
}
],
"temperature": 0.7,
"max_tokens": 2000,
"stream": true,
"tools": [
{
"type": "function",
"function": {
"name": "exit_interaction",
"description": "退出当前语音交互",
"parameters": {
"type": "object",
"properties": {},
"required": []
}
}
}
]
}' 'https: //ark.cn-beijing.volces.com/api/v3/bots/chat/completions'
{
"code": 0,
"message": "Error during WebSocket handshake: Unexpected response code: 400",
"request_log":"{\"ab_test\":{},\"base\":{\"has_upload\":false,\"is_pending\":true,\"load_flags\":18,\"load_state\":0,\"method\":\"GET\",\"net_error\":-320,\"network_isolation_key\":\"wss://openspeech.bytedance.com wss://openspeech.bytedance.com\",\"origin_url\":\"wss://openspeech.bytedance.com/api/v3/sauc/bigmodel\",\"redirect_times\":0,\"status\":\"FAILED\",\"traffic_annotation\":77012883},\"dns\":{\"address_list\":[\"163.181.50.229:443\",\"163.181.50.223:443\",\"163.181.50.221:443\",\"163.181.50.224:443\",\"163.181.50.226:443\",\"163.181.50.222:443\",\"163.181.50.228:443\",\"163.181.50.227:443\"],\"async_batch_httpdns\":0,\"cache_source\":6,\"cache_stale_reason\":0,\"from_stale_cache\":0,\"hijacked_error\":0,\"is_auth\":false,\"nameservers\":[],\"net_type\":0,\"preload_batch_httpdns\":0,\"queried_with_cip\":\"\",\"race_dns_stale_cache\":0,\"skip_prefer_ip_error\":0,\"source\":2},\"event\":{\"engine_startup\":1744019237.421243,\"network_change\":0.0},\"header\":{\"X-Tt-Logid\":\"2025040717474815E3294F926C737834FA\",\"server-timing\":\"inner; dur=5, cdn-cache;desc=MISS,edge;dur=653,origin;dur=14\",\"x-tt-trace-host\":\"012d6b6cae97bb056dd2540a62f7f6e52ccfb433febe931be5ddf1ac674ca77c57b7dee570790477696b7b5c17bbdd77bf841100de6d411ef8ccc26681b9799e4aee97897e0681b3a8b304853d1848fcbd9fec458fdd375cddc4a7873cdbcef3a0\",\"x-tt-trace-id\":\"00-0fa6216304269466036ad7b0703104d2-0fa6216304269466-01\",\"x-tt-trace-tag\":\"id=03;cdn-cache=miss;type=dyn\"},\"ifconfig\":{\"ipv4_reachable\":true,\"ipv6_reachable\":false},\"multinetwork\":{},\"nqe\":{\"bs_sys_state\":false,\"downlink_throughput\":2658,\"effective_net_type\":5,\"group_rtt_estimate\":[],\"http_rtt\":116,\"pending_requests\":\"1\",\"real_net_type\":2,\"tcp_rtt\":46,\"total_requests\":\"6\"},\"other\":{\"core_ver\":\"87.0.4273.1\",\"gateway\":\"0.0.0.0\",\"is_background\":false,\"is_internal_retry\":false,\"is_main_process\":true,\"libcore\":\"cronet\",\"net_thread_priority\":0,\"quic_commit\":\"6ea2111b/2023-03-16\",\"resume_time\":\"-1\",\"retry_attempts\":0,\"ttnet_build_timestamp\":\"1689596445\",\"ttnet_version\":\"4.1.127.2-tob\",\"version\":\"534436af/2023-07-17\"},\"response\":{\"cached\":false,\"code\":400,\"connection_info\":1,\"network_accessed\":true,\"received_bytes\":0,\"sent_bytes\":0,\"via_proxy\":false},\"session\":{},\"socket\":{\"address_family\":1,\"cip\":\"\",\"connection_attemps\":[],\"happy_eyeballs_result\":2,\"remote\":\"163.181.50.229:443\",\"socket_reused\":false},\"socketpool\":{\"assign_type\":0,\"consume_time\":0,\"is_backup_job\":false,\"pending_reason\":0,\"pending_time\":0,\"reuse_type\":0,\"socket_idle_time\":0},\"ssl\":{\"cert_status\":0,\"connection_status\":6296321,\"handshake_type\":1,\"is_issued_by_known_root\":true,\"server_cert_common_name\":\"*.bytedance.com\"},\"store_idc\":{\"init\":1},\"timing\":{\"detailed_duration\":{\"before_stream\":0,\"body_recv\":-1,\"cdn-cache\":\"miss\",\"connect_delay\":0,\"create_stream\":98,\"dispatch\":0,\"dns\":0,\"edge\":653,\"header_recv\":-1,\"inner\":5,\"origin\":9,\"proxy\":0,\"rtt\":43,\"security_callback\":0,\"send\":0,\"ssl\":48,\"tcp\":49,\"ttfb\":710},\"request\":{\"create_stream_finish\":1744019267.633028,\"create_stream_start\":1744019267.534112,\"duration\":812,\"http_transaction_start\":1744019267.534081,\"request_sent_time\":1744019267.633497,\"response_recv_time\":1744019268.344466,\"start_time\":1744019267.533685}},\"tnc\":{\"data\":{\"canary\":\"\",\"cold_start\":1,\"config\":\"\",\"config_updated\":0,\"local_probe_version\":\"\",\"rules\":\"\",\"source\":0,\"update_time\":\"-1\"},\"probe\":{\"cmd\":\"0\",\"get_time\":\"-1\",\"send_time\":\"-1\",\"state\":1,\"version\":\"0\"},\"request\":{\"probe_version\":\"0\",\"source\":0,\"start_time\":\"-1\",\"state\":1}},\"t
Loading…
Cancel
Save