From 78ea4618cf489e1cc5085d3af37156611bb1b564 Mon Sep 17 00:00:00 2001 From: wolfplus Date: Thu, 10 Apr 2025 19:13:53 +0100 Subject: [PATCH] add --- .cursorrules | 65 ++- android/app/build.gradle.kts | 1 + .../VoiceInteractionHandler copy.kt.bak | 456 ---------------- android/build.gradle.kts | 1 + android/settings.gradle.kts | 4 + lib/core/bindings/initial_binding.dart | 5 +- lib/core/utils/jieli_constants.dart | 89 ++++ lib/data/services/function_service.dart | 97 ---- .../services/open_ai_service_adapter.dart | 21 +- .../services/voice_interaction_service.dart | 14 +- .../bindings/jieli_test_binding.dart | 11 + .../controllers/jieli_test_controller.dart | 298 +++++++++++ .../jieli_test/views/jieli_test_view.dart | 494 +++++++++++++++++ .../controllers/pairing_controller.dart | 4 +- lib/modules/settings/views/settings_view.dart | 22 + lib/routes/app_pages.dart | 7 + lib/routes/app_routes.dart | 1 + .../azure_speech/android/build.gradle.kts | 42 +- local_plugins/chat_storage_sqlite/README.md | 80 +++ .../android/build.gradle.kts | 60 +++ .../android/settings.gradle.kts | 1 + .../android/src/main/AndroidManifest.xml | 8 + .../ChatStorageSqlitePlugin.kt | 236 +++++++++ .../lib/chat_storage_sqlite.dart | 155 ++++++ .../chat_storage_sqlite/pubspec.yaml | 30 ++ local_plugins/jieli/README.md | 144 +++++ local_plugins/jieli/android/build.gradle.kts | 55 ++ .../jieli/android/settings.gradle.kts | 1 + .../android/src/main/AndroidManifest.xml | 22 + .../com/yunqiinnovation/jieli/BleService.kt | 460 ++++++++++++++++ .../com/yunqiinnovation/jieli/JieliPlugin.kt | 269 ++++++++++ local_plugins/jieli/lib/jieli.dart | 188 +++++++ local_plugins/jieli/pubspec.yaml | 23 + .../android/AndroidManifest.xml | 12 + .../open_ai_service/android/build.gradle.kts | 24 +- .../open_ai_service/MCPClient.kt | 496 ++++++++++++++++++ .../open_ai_service/OpenAIService.kt | 228 ++++++-- .../open_ai_service/OpenAIServicePlugin.kt | 65 ++- .../open_ai_service/SystemFunctionHandler.kt | 422 +++++++++++++++ .../open_ai_service/lib/open_ai_service.dart | 101 ++++ .../volcano_speech/android/build.gradle.kts | 4 +- .../volcano_speech/VolcanoAsrHelper.kt | 39 +- .../volcano_speech/VolcanoTtsHelper.kt | 57 +- pubspec.yaml | 5 + test.json | 58 +- 45 files changed, 4207 insertions(+), 668 deletions(-) delete mode 100644 android/app/src/main/kotlin/com/yunqiinnovation/deepsound/VoiceInteractionHandler copy.kt.bak create mode 100644 lib/core/utils/jieli_constants.dart delete mode 100644 lib/data/services/function_service.dart create mode 100644 lib/modules/jieli_test/bindings/jieli_test_binding.dart create mode 100644 lib/modules/jieli_test/controllers/jieli_test_controller.dart create mode 100644 lib/modules/jieli_test/views/jieli_test_view.dart create mode 100644 local_plugins/chat_storage_sqlite/README.md create mode 100644 local_plugins/chat_storage_sqlite/android/build.gradle.kts create mode 100644 local_plugins/chat_storage_sqlite/android/settings.gradle.kts create mode 100644 local_plugins/chat_storage_sqlite/android/src/main/AndroidManifest.xml create mode 100644 local_plugins/chat_storage_sqlite/android/src/main/kotlin/com/yunqiinnovation/chat_storage_sqlite/ChatStorageSqlitePlugin.kt create mode 100644 local_plugins/chat_storage_sqlite/lib/chat_storage_sqlite.dart create mode 100644 local_plugins/chat_storage_sqlite/pubspec.yaml create mode 100644 local_plugins/jieli/README.md create mode 100644 local_plugins/jieli/android/build.gradle.kts create mode 100644 local_plugins/jieli/android/settings.gradle.kts create mode 100644 local_plugins/jieli/android/src/main/AndroidManifest.xml create mode 100644 local_plugins/jieli/android/src/main/kotlin/com/yunqiinnovation/jieli/BleService.kt create mode 100644 local_plugins/jieli/android/src/main/kotlin/com/yunqiinnovation/jieli/JieliPlugin.kt create mode 100644 local_plugins/jieli/lib/jieli.dart create mode 100644 local_plugins/jieli/pubspec.yaml create mode 100644 local_plugins/open_ai_service/android/AndroidManifest.xml create mode 100644 local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/MCPClient.kt create mode 100644 local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/SystemFunctionHandler.kt diff --git a/.cursorrules b/.cursorrules index 99d6aa58c..6e3d3fd0c 100644 --- a/.cursorrules +++ b/.cursorrules @@ -7,4 +7,67 @@ 7. 尽可能避免打印日志,仅在出错时记录日志 8. 请使用较小的代码量改动,最安全的方法进行代码编写和优化 9. 请尽可能使用中文回复问题 -10. 重要:不要过度设计!! \ No newline at end of file +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文件管理 +- 支持不同环境的配置切换 +- 敏感信息不应直接硬编码在代码中 \ No newline at end of file diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 8c371408e..009a8926c 100644 --- a/android/app/build.gradle.kts +++ b/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") diff --git a/android/app/src/main/kotlin/com/yunqiinnovation/deepsound/VoiceInteractionHandler copy.kt.bak b/android/app/src/main/kotlin/com/yunqiinnovation/deepsound/VoiceInteractionHandler copy.kt.bak deleted file mode 100644 index 1a9e2df13..000000000 --- a/android/app/src/main/kotlin/com/yunqiinnovation/deepsound/VoiceInteractionHandler copy.kt.bak +++ /dev/null @@ -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) - } -} \ No newline at end of file diff --git a/android/build.gradle.kts b/android/build.gradle.kts index 5421f73bd..ca0ce6220 100644 --- a/android/build.gradle.kts +++ b/android/build.gradle.kts @@ -1,5 +1,6 @@ allprojects { repositories { + mavenLocal() google() mavenCentral() maven { url = uri("https://storage.googleapis.com/download.flutter.io") } diff --git a/android/settings.gradle.kts b/android/settings.gradle.kts index e332da5f9..74e47be3f 100644 --- a/android/settings.gradle.kts +++ b/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") diff --git a/lib/core/bindings/initial_binding.dart b/lib/core/bindings/initial_binding.dart index 748589f63..9a7a3abec 100644 --- a/lib/core/bindings/initial_binding.dart +++ b/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(), fenix: true); + + // 语音交互服务 Get.lazyPut(() => VoiceInteractionService(), fenix: true); + // 杰理蓝牙服务 + // // 语言管理器(需要最先初始化) // Get.put(LoginService(), permanent: true); // await LoginService().FirebaseInit(); diff --git a/lib/core/utils/jieli_constants.dart b/lib/core/utils/jieli_constants.dart new file mode 100644 index 000000000..55f8cb802 --- /dev/null +++ b/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; // 自定义 +} \ No newline at end of file diff --git a/lib/data/services/function_service.dart b/lib/data/services/function_service.dart deleted file mode 100644 index b1e3a1e4d..000000000 --- a/lib/data/services/function_service.dart +++ /dev/null @@ -1,97 +0,0 @@ -import 'dart:convert'; - -/// 函数服务 -/// 仅实现各种功能函数,不包含AI交互逻辑 -class FunctionService { - /// 天气查询工具定义 - /// 返回一个可直接用于tools参数的天气查询工具定义 - static Map 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> getWeatherInfo(Map 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 parseFunctionCallArguments(Map 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 getFunctionCallInfo(Map 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; -} \ No newline at end of file diff --git a/lib/data/services/open_ai_service_adapter.dart b/lib/data/services/open_ai_service_adapter.dart index 2c0da53ac..7eb337605 100644 --- a/lib/data/services/open_ai_service_adapter.dart +++ b/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; } diff --git a/lib/data/services/voice_interaction_service.dart b/lib/data/services/voice_interaction_service.dart index cce506a58..57e8636c5 100644 --- a/lib/data/services/voice_interaction_service.dart +++ b/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) { diff --git a/lib/modules/jieli_test/bindings/jieli_test_binding.dart b/lib/modules/jieli_test/bindings/jieli_test_binding.dart new file mode 100644 index 000000000..9df61ef94 --- /dev/null +++ b/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(), + ); + } +} \ No newline at end of file diff --git a/lib/modules/jieli_test/controllers/jieli_test_controller.dart b/lib/modules/jieli_test/controllers/jieli_test_controller.dart new file mode 100644 index 000000000..8afa86cf4 --- /dev/null +++ b/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(null); + var scannedDevices = RxList([]); + 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 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 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 stopScan() async { + if (!isScanning.value) return; + + try { + logger.d('停止扫描'); + await jieli.stopScan(); + isScanning.value = false; + } catch (e) { + logger.e('停止扫描错误', error: e); + } + } + + /// 连接到指定设备 + Future 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 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 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 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); + } + } +} \ No newline at end of file diff --git a/lib/modules/jieli_test/views/jieli_test_view.dart b/lib/modules/jieli_test/views/jieli_test_view.dart new file mode 100644 index 000000000..aa6316366 --- /dev/null +++ b/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 { + 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; + } + } +} \ No newline at end of file diff --git a/lib/modules/pairing/controllers/pairing_controller.dart b/lib/modules/pairing/controllers/pairing_controller.dart index 2501e26bd..5c36698c3 100644 --- a/lib/modules/pairing/controllers/pairing_controller.dart +++ b/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(); @@ -109,6 +110,7 @@ class PairingController extends GetxController { /// 初始化流程 Future _initialize() async { try { + // 初始化语音交互服务 _voiceInteractionService.initialize(); diff --git a/lib/modules/settings/views/settings_view.dart b/lib/modules/settings/views/settings_view.dart index ce4f7866a..fac80cae2 100644 --- a/lib/modules/settings/views/settings_view.dart +++ b/lib/modules/settings/views/settings_view.dart @@ -480,6 +480,28 @@ class SettingsView extends GetView { }, 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 diff --git a/lib/routes/app_pages.dart b/lib/routes/app_pages.dart index 8a2466fda..07e5cc5fb 100644 --- a/lib/routes/app_pages.dart +++ b/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(), + ), ]; } diff --git a/lib/routes/app_routes.dart b/lib/routes/app_routes.dart index 4488f63db..7d661c0a3 100644 --- a/lib/routes/app_routes.dart +++ b/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'; } diff --git a/local_plugins/azure_speech/android/build.gradle.kts b/local_plugins/azure_speech/android/build.gradle.kts index a26b13967..9aabbb990 100644 --- a/local_plugins/azure_speech/android/build.gradle.kts +++ b/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 { +android { namespace = "com.yunqiinnovation.azure_speech" - compileSdkVersion(33) + compileSdk = 33 defaultConfig { minSdk = 21 @@ -48,6 +27,20 @@ configure { 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 { } dependencies { - // 添加Microsoft语音SDK implementation("com.microsoft.cognitiveservices.speech:client-sdk:1.30.0") } \ No newline at end of file diff --git a/local_plugins/chat_storage_sqlite/README.md b/local_plugins/chat_storage_sqlite/README.md new file mode 100644 index 000000000..8db279af0 --- /dev/null +++ b/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处理返回结果 +- 插件内部已处理异常情况,但建议在使用时添加适当的错误处理 \ No newline at end of file diff --git a/local_plugins/chat_storage_sqlite/android/build.gradle.kts b/local_plugins/chat_storage_sqlite/android/build.gradle.kts new file mode 100644 index 000000000..78e1b31ea --- /dev/null +++ b/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") +} \ No newline at end of file diff --git a/local_plugins/chat_storage_sqlite/android/settings.gradle.kts b/local_plugins/chat_storage_sqlite/android/settings.gradle.kts new file mode 100644 index 000000000..613be470e --- /dev/null +++ b/local_plugins/chat_storage_sqlite/android/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "chat_storage_sqlite" \ No newline at end of file diff --git a/local_plugins/chat_storage_sqlite/android/src/main/AndroidManifest.xml b/local_plugins/chat_storage_sqlite/android/src/main/AndroidManifest.xml new file mode 100644 index 000000000..b7fb5f78b --- /dev/null +++ b/local_plugins/chat_storage_sqlite/android/src/main/AndroidManifest.xml @@ -0,0 +1,8 @@ + + + + + + + \ No newline at end of file diff --git a/local_plugins/chat_storage_sqlite/android/src/main/kotlin/com/yunqiinnovation/chat_storage_sqlite/ChatStorageSqlitePlugin.kt b/local_plugins/chat_storage_sqlite/android/src/main/kotlin/com/yunqiinnovation/chat_storage_sqlite/ChatStorageSqlitePlugin.kt new file mode 100644 index 000000000..d302c4d7d --- /dev/null +++ b/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("sessionId") ?: "" + val message = call.argument("message") ?: "" + val sender = call.argument("sender") ?: "" + val metadata = call.argument("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("sessionId") ?: "" + val page = call.argument("page") ?: 1 + val pageSize = call.argument("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("sessionId") ?: "" + val messageIds = call.argument>("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) + } +} \ No newline at end of file diff --git a/local_plugins/chat_storage_sqlite/lib/chat_storage_sqlite.dart b/local_plugins/chat_storage_sqlite/lib/chat_storage_sqlite.dart new file mode 100644 index 000000000..f41f473a7 --- /dev/null +++ b/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 toMap() { + return { + 'id': id, + 'sessionId': sessionId, + 'message': message, + 'sender': sender, + 'timestamp': timestamp, + 'metadata': metadata, + }; + } + + factory ChatMessage.fromMap(Map 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 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> 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.from(result); + } catch (e) { + return {'success': false, 'error': e.toString()}; + } + } + + /// 获取指定会话的聊天记录(支持分页) + static Future 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 data = jsonDecode(result); + final List messagesList = data['messages']; + + List messages = messagesList.map((item) { + return ChatMessage.fromMap(Map.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> deleteMessages({ + String? sessionId, + List? 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.from(result); + } catch (e) { + return {'success': false, 'error': e.toString()}; + } + } + + /// 清空整个数据库 + static Future> clearDatabase() async { + try { + final result = await _channel.invokeMethod('clearDatabase'); + return Map.from(result); + } catch (e) { + return {'success': false, 'error': e.toString()}; + } + } +} \ No newline at end of file diff --git a/local_plugins/chat_storage_sqlite/pubspec.yaml b/local_plugins/chat_storage_sqlite/pubspec.yaml new file mode 100644 index 000000000..a4d6e1bf1 --- /dev/null +++ b/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 \ No newline at end of file diff --git a/local_plugins/jieli/README.md b/local_plugins/jieli/README.md new file mode 100644 index 000000000..d926109c8 --- /dev/null +++ b/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 + + + + + + + +``` \ No newline at end of file diff --git a/local_plugins/jieli/android/build.gradle.kts b/local_plugins/jieli/android/build.gradle.kts new file mode 100644 index 000000000..70b01c20e --- /dev/null +++ b/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") +} \ No newline at end of file diff --git a/local_plugins/jieli/android/settings.gradle.kts b/local_plugins/jieli/android/settings.gradle.kts new file mode 100644 index 000000000..c0323d3bc --- /dev/null +++ b/local_plugins/jieli/android/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "jieli" \ No newline at end of file diff --git a/local_plugins/jieli/android/src/main/AndroidManifest.xml b/local_plugins/jieli/android/src/main/AndroidManifest.xml new file mode 100644 index 000000000..3cf7a945e --- /dev/null +++ b/local_plugins/jieli/android/src/main/AndroidManifest.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/local_plugins/jieli/android/src/main/kotlin/com/yunqiinnovation/jieli/BleService.kt b/local_plugins/jieli/android/src/main/kotlin/com/yunqiinnovation/jieli/BleService.kt new file mode 100644 index 000000000..0ba59a6e6 --- /dev/null +++ b/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>) + + /** + * 连接状态变化回调 + * @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() + private var isScanning = false + private var isPendingScanResultUpdate = false + private val scanResultUpdateRunnable = Runnable { + notifyScanResults() + isPendingScanResultUpdate = false + } + + // 连接状态LiveData + val connectionState = MutableLiveData(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 + } +} \ No newline at end of file diff --git a/local_plugins/jieli/android/src/main/kotlin/com/yunqiinnovation/jieli/JieliPlugin.kt b/local_plugins/jieli/android/src/main/kotlin/com/yunqiinnovation/jieli/JieliPlugin.kt new file mode 100644 index 000000000..17cc5d21f --- /dev/null +++ b/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>) { + 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("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("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 + ) + } + } + } +} \ No newline at end of file diff --git a/local_plugins/jieli/lib/jieli.dart b/local_plugins/jieli/lib/jieli.dart new file mode 100644 index 000000000..78ec090bb --- /dev/null +++ b/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 json) { + return BleDevice( + id: json['id'] as String, + name: json['name'] as String? ?? 'Unknown Device', + rssi: json['rssi'] as int? ?? 0, + ); + } + + Map 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> _scanController = + StreamController>.broadcast(); + Stream> get scanResults => _scanController.stream; + + /// 连接状态变化流 + static final StreamController _connectionController = + StreamController.broadcast(); + Stream get connectionState => _connectionController.stream; + + /// 音频数据流 + static final StreamController _audioController = + StreamController.broadcast(); + Stream get audioDataStream => _audioController.stream; + + /// 唤醒信号流 + static final StreamController _wakeupController = + StreamController.broadcast(); + Stream get wakeupStream => _wakeupController.stream; + + /// 事件流订阅 + StreamSubscription? _eventSubscription; + + /// 构造函数,初始化平台通道和事件监听 + Jieli() { + _setupEventListener(); + } + + /// 设置事件监听器 + void _setupEventListener() { + _eventSubscription = _eventChannel.receiveBroadcastStream().listen(_handleEvent); + } + + /// 处理来自原生平台的事件 + void _handleEvent(dynamic event) { + if (event is! Map) return; + + final eventData = Map.from(event); + final String eventType = eventData['type'] as String; + final dynamic data = eventData['data']; + + switch (eventType) { + case EventType.scanResult: + if (data is List) { + final deviceList = data + .map((item) => BleDevice.fromJson(Map.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 startScan({int scanTimeout = 10000}) async { + final result = await _methodChannel.invokeMethod( + 'startScan', + {'scanTimeout': scanTimeout}, + ); + return result ?? false; + } + + /// 停止扫描BLE设备 + Future stopScan() async { + final result = await _methodChannel.invokeMethod('stopScan'); + return result ?? false; + } + + /// 连接到指定设备 + Future connect(String deviceId) async { + final result = await _methodChannel.invokeMethod( + 'connect', + {'deviceId': deviceId}, + ); + return result ?? false; + } + + /// 断开连接 + Future disconnect() async { + final result = await _methodChannel.invokeMethod('disconnect'); + return result ?? false; + } + + /// 发送开始录音命令 + Future sendStartRecordingCommand() async { + final result = await _methodChannel.invokeMethod('sendStartRecordingCommand'); + return result ?? false; + } + + /// 发送停止录音命令 + Future sendStopRecordingCommand() async { + final result = await _methodChannel.invokeMethod('sendStopRecordingCommand'); + return result ?? false; + } + + /// 关闭插件,释放资源 + Future dispose() async { + await stopScan(); + await disconnect(); + _eventSubscription?.cancel(); + _scanController.close(); + _connectionController.close(); + _audioController.close(); + _wakeupController.close(); + } + + /// 检查蓝牙是否已启用 + Future isBluetoothEnabled() async { + final result = await _methodChannel.invokeMethod('isBluetoothEnabled'); + return result ?? false; + } + + /// 请求打开蓝牙 + Future requestBluetoothEnable() async { + final result = await _methodChannel.invokeMethod('requestBluetoothEnable'); + return result ?? false; + } +} \ No newline at end of file diff --git a/local_plugins/jieli/pubspec.yaml b/local_plugins/jieli/pubspec.yaml new file mode 100644 index 000000000..ab230e685 --- /dev/null +++ b/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 \ No newline at end of file diff --git a/local_plugins/open_ai_service/android/AndroidManifest.xml b/local_plugins/open_ai_service/android/AndroidManifest.xml new file mode 100644 index 000000000..1d834e578 --- /dev/null +++ b/local_plugins/open_ai_service/android/AndroidManifest.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/local_plugins/open_ai_service/android/build.gradle.kts b/local_plugins/open_ai_service/android/build.gradle.kts index 4f7b2f643..57889671a 100644 --- a/local_plugins/open_ai_service/android/build.gradle.kts +++ b/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") } \ No newline at end of file diff --git a/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/MCPClient.kt b/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/MCPClient.kt new file mode 100644 index 000000000..fef91ab05 --- /dev/null +++ b/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 +} + +/** + * 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 = emptyList() + + // 工具Map列表,用于OpenAI请求 + private val toolMaps = mutableListOf>() + + // 本地函数Map,函数名 -> 处理器 + private val localFunctions = mutableMapOf() + + // 本地函数定义Map,函数名 -> 定义 + private val localFunctionDefs = mutableMapOf() + + // 是否已连接 + 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() + + 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())) + .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 = when (parameters) { + is Map<*, *> -> { + @Suppress("UNCHECKED_CAST") + parameters as Map + } + is JSONObject -> { + val mapper = ObjectMapper() + mapper.readValue( + parameters.toString(), + object : TypeReference>() {} + ) + } + 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>() {}) + 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() + + // 提取工具类型及参数 + 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() + 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() + 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() + parametersMap["required"] = emptyList() + } + + // 创建工具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() + node.fieldNames().forEach { fieldName -> + result[fieldName] = convertJsonNodeToMap(node.get(fieldName)) + } + result + } + node.isArray() -> { + val result = mutableListOf() + 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> { + 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? { + 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? { + 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? { + 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 { + return tools + } + + /** + * 检查工具是否存在 + */ + fun hasToolWithName(name: String): Boolean { + return localFunctions.containsKey(name) || toolMaps.any { + val functionMap = it["function"] as? Map + functionMap?.get("name") == name + } + } + + /** + * 从字符串解析JSON参数 + */ + fun parseJsonArguments(json: String): Map { + return try { + ObjectMapper().readValue(json, object : TypeReference>() {}) + } 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) + } + } + } +} \ No newline at end of file diff --git a/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/OpenAIService.kt b/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/OpenAIService.kt index 907be93a2..9e67d15f8 100644 --- a/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/OpenAIService.kt +++ b/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() + // 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 { + // 由于本地函数的实际处理是在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, callback: StreamCallback) { + private fun processToolCalls(toolCalls: Map, 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) + } + } + } } } diff --git a/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/OpenAIServicePlugin.kt b/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/OpenAIServicePlugin.kt index 70af36c13..1e1bb9446 100644 --- a/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/OpenAIServicePlugin.kt +++ b/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("name") ?: "" val description = call.argument("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("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("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) + } + } } \ No newline at end of file diff --git a/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/SystemFunctionHandler.kt b/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/SystemFunctionHandler.kt new file mode 100644 index 000000000..f2e7cbcdb --- /dev/null +++ b/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 { + // 调用发送广播的函数 + 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 { + 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 { + 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 { + 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 { + 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 + ) +} \ No newline at end of file diff --git a/local_plugins/open_ai_service/lib/open_ai_service.dart b/local_plugins/open_ai_service/lib/open_ai_service.dart index 7c6fe8024..8666f0100 100644 --- a/local_plugins/open_ai_service/lib/open_ai_service.dart +++ b/local_plugins/open_ai_service/lib/open_ai_service.dart @@ -264,4 +264,105 @@ class OpenAIService { throw OpenAIException('解析函数调用失败: $e'); } } + + /// 初始化MCP客户端 + /// + /// [serverUrl] MCP服务器WebSocket地址 + /// + /// 返回一个布尔值,表示请求是否已开始初始化(实际初始化过程是异步的) + Future initializeMcpClient(String serverUrl) async { + try { + final result = await _channel.invokeMethod( + 'initializeMcpClient', + {'serverUrl': serverUrl}, + ); + + return result ?? false; + } catch (e) { + throw OpenAIException('初始化MCP客户端失败: $e'); + } + } + + /// 检查MCP客户端是否已初始化 + Future isMcpInitialized() async { + try { + final result = await _channel.invokeMethod('isMcpInitialized'); + return result ?? false; + } catch (e) { + throw OpenAIException('检查MCP客户端状态失败: $e'); + } + } + + /// 关闭MCP客户端 + Future closeMcpClient() async { + try { + final result = await _channel.invokeMethod('closeMcpClient'); + return result ?? false; + } catch (e) { + throw OpenAIException('关闭MCP客户端失败: $e'); + } + } + + /// 处理MCP工具调用 + /// + /// [functionCall] 函数调用信息,必须包含name和arguments字段 + /// + /// 返回工具调用结果 + Future handleMcpToolCall(Map functionCall) async { + try { + final result = await _channel.invokeMethod( + 'handleMcpToolCall', + {'functionCall': jsonEncode(functionCall)}, + ); + + return result ?? "处理MCP工具调用失败: 结果为空"; + } catch (e) { + throw OpenAIException('处理MCP工具调用失败: $e'); + } + } + + /// 处理函数调用(自动处理本地函数和MCP工具) + /// + /// 这是一个辅助方法,用于简化函数调用处理流程: + /// 1. MCP工具会被自动处理,不会通过onFunctionCall回调返回 + /// 2. 只有本地函数才需要手动处理 + /// 3. 提供统一的处理接口 + /// + /// 参数: + /// [functionCall] 函数调用信息 + /// [messages] 消息上下文 + /// [localFunctionHandler] 本地函数处理器,接收函数名和参数,返回处理结果 + /// + /// 返回值: + /// 布尔值表示是否成功处理(已将结果发送回OpenAI) + Future handleFunctionCall({ + required Map functionCall, + required List> messages, + required Future Function(String name, Map arguments) localFunctionHandler, + }) async { + final name = functionCall['name'] as String; + final argumentsJson = functionCall['arguments'] as String; + + // 解析参数 + Map arguments; + try { + arguments = json.decode(argumentsJson) as Map; + } catch (e) { + arguments = {}; + } + + try { + // 调用本地函数处理器 + final result = await localFunctionHandler(name, arguments); + + // 发送结果回OpenAI + return sendFunctionCallResult( + messages: messages, + functionCall: functionCall, + functionResult: result, + ); + } catch (e) { + throw OpenAIException('处理函数调用失败: $e'); + } + } } \ No newline at end of file diff --git a/local_plugins/volcano_speech/android/build.gradle.kts b/local_plugins/volcano_speech/android/build.gradle.kts index 86b03fd0e..713a21a59 100644 --- a/local_plugins/volcano_speech/android/build.gradle.kts +++ b/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扩展 diff --git a/local_plugins/volcano_speech/android/src/main/kotlin/com/yunqiinnovation/volcano_speech/VolcanoAsrHelper.kt b/local_plugins/volcano_speech/android/src/main/kotlin/com/yunqiinnovation/volcano_speech/VolcanoAsrHelper.kt index 614474d1a..d9b63819a 100644 --- a/local_plugins/volcano_speech/android/src/main/kotlin/com/yunqiinnovation/volcano_speech/VolcanoAsrHelper.kt +++ b/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" + } + } } diff --git a/local_plugins/volcano_speech/android/src/main/kotlin/com/yunqiinnovation/volcano_speech/VolcanoTtsHelper.kt b/local_plugins/volcano_speech/android/src/main/kotlin/com/yunqiinnovation/volcano_speech/VolcanoTtsHelper.kt index e24a1ceb1..4642272d4 100644 --- a/local_plugins/volcano_speech/android/src/main/kotlin/com/yunqiinnovation/volcano_speech/VolcanoTtsHelper.kt +++ b/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" + } + } } diff --git a/pubspec.yaml b/pubspec.yaml index 41fa9edf0..b2962bec9 100644 --- a/pubspec.yaml +++ b/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: diff --git a/test.json b/test.json index 703948620..473daebb6 100644 --- a/test.json +++ b/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' \ No newline at end of file +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 \ No newline at end of file