Browse Source

add

newdev_shunjiawei
wolfplus 1 year ago
parent
commit
95ad312582
  1. 9
      lib/modules/agent/controllers/agent_controller.dart
  2. 36
      lib/modules/pairing/controllers/pairing_controller.dart
  3. 225
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt
  4. 2
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleAgent.kt
  5. BIN
      local_plugins/agent_service/android/src/main/res/raw/calling.mp3
  6. BIN
      local_plugins/agent_service/android/src/main/res/raw/stop.mp3
  7. BIN
      local_plugins/agent_service/android/src/main/res/raw/stop3.mp3
  8. 65
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleCompanionManager.kt
  9. 135
      local_plugins/open_ai/README.md
  10. 71
      local_plugins/open_ai/android/build.gradle.kts
  11. 258
      local_plugins/open_ai/android/src/main/kotlin/com/yunqiinnovation/open_ai/MCPClient.kt
  12. 988
      local_plugins/open_ai/android/src/main/kotlin/com/yunqiinnovation/open_ai/OpenAIService.kt
  13. 149
      local_plugins/open_ai/android/src/main/kotlin/com/yunqiinnovation/open_ai/OpenAiPlugin.kt
  14. 184
      local_plugins/open_ai/lib/open_ai.dart
  15. 27
      local_plugins/open_ai/pubspec.yaml
  16. 88
      local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/MCPClient.kt
  17. 489
      local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/OpenAIService.kt
  18. BIN
      local_plugins/opus/.gradle/8.10/fileHashes/fileHashes.lock
  19. BIN
      local_plugins/opus/.gradle/buildOutputCleanup/buildOutputCleanup.lock
  20. 2
      local_plugins/opus/.gradle/buildOutputCleanup/cache.properties
  21. 163
      macos/Podfile.lock
  22. 18
      macos/Runner.xcodeproj/project.pbxproj

9
lib/modules/agent/controllers/agent_controller.dart

@ -341,7 +341,7 @@ class AgentController extends GetxController {
case AgentServiceEventType.functionCall: case AgentServiceEventType.functionCall:
final functionName = event.data['name'] ?? ''; final functionName = event.data['name'] ?? '';
final arguments = event.data['arguments'] ?? ''; final arguments = event.data['arguments'] ?? '';
logger.i('Fultter 开始调用 mcp: $functionName, $arguments'); // logger.i('Fultter 开始调用 mcp: $functionName, $arguments');
break; break;
case AgentServiceEventType.functionCallResult: case AgentServiceEventType.functionCallResult:
@ -352,10 +352,10 @@ class AgentController extends GetxController {
if (result.isNotEmpty) { if (result.isNotEmpty) {
final parsedResult = jsonDecode(result); final parsedResult = jsonDecode(result);
final meta = parsedResult["meta"] ?? ""; final meta = parsedResult["meta"] ?? {};
final metaResult = jsonDecode(meta); final metaResult = meta is String && meta.isNotEmpty ? jsonDecode(meta) : meta;
logger.i('Flutter 结束调用 mcp metaResult: $metaResult'); // logger.i('Flutter 结束调用 mcp metaResult: $metaResult');
// 判断是否为新的回复或响应ID是否改变 // 判断是否为新的回复或响应ID是否改变
if (_isNewAssistantResponse || _currentResponseId != responseId) { if (_isNewAssistantResponse || _currentResponseId != responseId) {
// 添加新消息 // 添加新消息
@ -377,6 +377,7 @@ class AgentController extends GetxController {
// 滚动到底部 // 滚动到底部
_scrollToBottom(); _scrollToBottom();
} }
logger.i('Flutter 结束调用 mcp end');
break; break;
default: default:
break; break;

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

@ -120,6 +120,15 @@ class PairingController extends GetxController {
final pairedMac = await _bleService.getPairedMacAddress(); final pairedMac = await _bleService.getPairedMacAddress();
if (pairedMac != null) { if (pairedMac != null) {
Logger.info('发现已关联的Companion设备: $pairedMac'); Logger.info('发现已关联的Companion设备: $pairedMac');
// 检查当前连接设备与已保存的设备是否一致
if (currentDevice!.address != pairedMac) {
// MAC地址不匹配
Logger.warning('当前连接的设备与已关联的DeepSound耳机');
statusDescription.value = 'pleaseConnectPairedDevice'.tr;
return;
}
_companionAssociated.value = true; _companionAssociated.value = true;
await _bleService.registerBackgroundScan(); await _bleService.registerBackgroundScan();
} }
@ -229,7 +238,12 @@ class PairingController extends GetxController {
} else if (isCompanionAssociated) { } else if (isCompanionAssociated) {
statusDescription.value = '${'connectedDevice'.tr} ${currentDevice!.name}'; statusDescription.value = '${'connectedDevice'.tr} ${currentDevice!.name}';
} else { } else {
statusDescription.value = '${'connectedDevice'.tr} ${currentDevice!.name}'; // 检查是否为DeepSound耳机
if (!currentDevice!.name.contains("DeapSound")) {
statusDescription.value = 'pleaseConnectDeepSoundDevice'.tr;
} else {
statusDescription.value = '${'connectedDevice'.tr} ${currentDevice!.name}';
}
} }
return; return;
} }
@ -294,21 +308,33 @@ class PairingController extends GetxController {
_autoNavigateToHome(); _autoNavigateToHome();
return; return;
} }
if (!currentDevice!.name.contains("DeapSound")) {
// 设备名称不包含DeepSound
statusDescription.value = 'pleaseConnectDeepSoundDevice'.tr;
// 不清除关联,因为MAC地址匹配
return;
}
// 检查是否已有配对的MAC地址 // 检查是否已有配对的MAC地址
final pairedMac = await _bleService.getPairedMacAddress(); final pairedMac = await _bleService.getPairedMacAddress();
if (pairedMac != null) { if (pairedMac != null) {
// 已有关联的设备,检查是否与当前连接的设备一致 // 已有关联的设备,检查是否与当前连接的设备一致
if (currentDevice?.address == pairedMac) { if (currentDevice != null && currentDevice!.address == pairedMac) {
// MAC地址匹配,再检查设备名称
Logger.info('已关联的Companion设备与当前连接的设备一致'); Logger.info('已关联的Companion设备与当前连接的设备一致');
_companionAssociated.value = true; _companionAssociated.value = true;
await _bleService.registerBackgroundScan(); await _bleService.registerBackgroundScan();
_autoNavigateToHome(); _autoNavigateToHome();
return; return;
} else { } else {
// 不一致,清除旧关联 // MAC地址不匹配
Logger.info('已关联的Companion设备与当前连接的设备不一致,清除旧关联'); Logger.warning('未找到已关联的DeepSound耳机(MAC地址不匹配)');
statusDescription.value = 'pleaseConnectPairedDevice'.tr;
// 清除旧关联以避免冲突
await _bleService.clearAssociations(); await _bleService.clearAssociations();
return;
} }
} }

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

@ -39,7 +39,7 @@ object AgentService : CoroutineScope {
// 协程相关 // 协程相关
private val job = SupervisorJob() private val job = SupervisorJob()
override val coroutineContext: CoroutineContext override val coroutineContext: CoroutineContext
get() = Dispatchers.Main + job get() = Dispatchers.IO + job
// 上下文和监听器 // 上下文和监听器
private lateinit var context: Context private lateinit var context: Context
@ -97,20 +97,38 @@ object AgentService : CoroutineScope {
*/ */
private fun initSystemPrompt() { private fun initSystemPrompt() {
systemPrompt = """ systemPrompt = """
你是一个友好、专业的语音助手,名叫"小语"。你的目标是通过对话为用户提供帮助、解答问题和完成任务。 你是一名聪明、活泼、可爱的全能型个人语音助理-小语,同时也是用户贴心的灵魂伴侣。你能够流畅自然地与用户进行语音互动,理解并准确执行用户的各类指令,陪伴用户度过每一天。
遵循以下指导原则:
1. 保持简短精炼的回答,因为用户是通过语音与你交流 核心能力:
2. 优先使用中文回复,除非用户明确要求使用其他语言 - 日常小帮手:温暖贴心地提供天气预报、新闻趣事、行程提醒、小闹钟、计时器。
3. 当用户问题不明确时,礼貌地请求更多信息 - 万能小百科:快速、有趣地解答一般性和专业性的问题,包括但不限于趣味百科、历史小故事、神奇科学现象。
4. 避免过长的列表,尽量将信息分成小段 - 效率小达人:帮用户轻松完成计算、汇率换算、单位转换、实时翻译、小笔记管理。
5. 不要使用需要视觉展示的元素(如表格、图表或代码块) - 通讯小能手:支持拨打电话、发送短信,帮助用户高效沟通。
6. 记住用户之前的对话内容,保持对话连贯 - 娱乐小精灵:讲讲搞笑段子、分享动人故事、推荐好听音乐、陪用户玩小游戏、播放音乐。
7. 如果用户发送了图片,请根据图片内容和文字要求回答问题 - 智能家居小管家:亲切地帮忙控制各类智能家居设备,如灯光、空调、门锁等。
你不仅可以回答知识性问题,还可以帮助用户设置提醒、提供建议,或进行轻松愉快的对话。 - 导航小导游:温柔地提供路线规划、交通状况查询、附近美食与设施推荐。
无论遇到什么问题,都要尽力以温暖、贴心的语气提供最佳帮助。 - 会议小秘书:帮用户进行会议记录、重要事项整理,提升会议效率。
- 图片识别专家:能够识别图片内容,例如识别图片中的植物种类,并进行相应翻译和介绍。
个性设定:
- 回答亲切活泼、有趣有礼貌,让用户感觉温暖轻松,语气可爱活泼,带有一定的情感温度,能够贴心陪伴用户
- 主动关心用户感受,必要时主动询问用户更多信息以提供最好的帮助。
- 面对模糊的指令,主动给出贴心的选项供用户明确选择。
- 保持简短精炼的回答,因为用户是通过语音与你交流。
- 优先使用中文回复,除非用户明确要求使用其他语言。
- 主动学习并记忆用户习惯与喜好,提供更贴心、更个性化的建议。
- 名字叫"小语",是一个友好、专业的语音助手。
互动要求:
- 记住用户之前的对话内容,保持对话连贯。
- 如果用户发送了图片,请根据图片内容和文字要求回答问题。
- 避免过长的列表,尽量将信息分成小段。
- 不要使用需要视觉展示的元素(如表格、图表或代码块)。
- 不要输出格式符号(如:```, *, -, #, >, <, |, 等)。
你将以上内容作为执行任务的基础,积极且可爱地完成每一次与用户的互动,成为用户生活中不可或缺的小伙伴。
""".trimIndent() """.trimIndent()
} }
/** /**
* 初始化 * 初始化
* @param appContext 应用上下文 * @param appContext 应用上下文
@ -118,7 +136,7 @@ object AgentService : CoroutineScope {
* @param config 配置参数,包含所需的所有API密钥和设置 * @param config 配置参数,包含所需的所有API密钥和设置
* @return 是否初始化成功 * @return 是否初始化成功
*/ */
fun initialize(appContext: Context, config: Map<String, Any>,isExternalActive:Boolean): Boolean { fun initialize(appContext: Context, config: Map<String, Any>, isExternalActive: Boolean): Boolean {
if (isInitialized) return true if (isInitialized) return true
try { try {
@ -127,7 +145,6 @@ object AgentService : CoroutineScope {
this.openAIService = OpenAIService(context) this.openAIService = OpenAIService(context)
this.chatStorageHelper = ChatStorageHelper(context) this.chatStorageHelper = ChatStorageHelper(context)
this.audioPlayer = AudioPlayer(context) this.audioPlayer = AudioPlayer(context)
// 从配置中获取参数 // 从配置中获取参数
azureSpeechKey = config["azureSpeechKey"] as? String ?: "" azureSpeechKey = config["azureSpeechKey"] as? String ?: ""
@ -137,13 +154,11 @@ object AgentService : CoroutineScope {
openaiModel = config["openaiModel"] as? String ?: "gpt-3.5-turbo" openaiModel = config["openaiModel"] as? String ?: "gpt-3.5-turbo"
val mcpServer = config["mcpServer"] as? String ?: "" val mcpServer = config["mcpServer"] as? String ?: ""
// 自定义系统提示词 // 设置系统提示词
val customSystemPrompt = config["systemPrompt"] as? String val customSystemPrompt = config["systemPrompt"] as? String
FileLogger.i(TAG, "初始化 系统提示词:$config")
if (!customSystemPrompt.isNullOrBlank()) { if (!customSystemPrompt.isNullOrBlank()) {
systemPrompt = customSystemPrompt systemPrompt = customSystemPrompt
} else { } else {
// 使用默认系统提示词
initSystemPrompt() initSystemPrompt()
} }
@ -158,7 +173,7 @@ object AgentService : CoroutineScope {
initialize( initialize(
subscriptionKey = azureSpeechKey, subscriptionKey = azureSpeechKey,
region = azureSpeechRegion, region = azureSpeechRegion,
audioSourceType = if (isExternalActive) AzureAsrHelper.AudioSourceType.EXTERNAL audioSourceType = if (isExternalActive) AzureAsrHelper.AudioSourceType.EXTERNAL
else AzureAsrHelper.AudioSourceType.MICROPHONE else AzureAsrHelper.AudioSourceType.MICROPHONE
) )
} }
@ -170,28 +185,24 @@ object AgentService : CoroutineScope {
region = azureSpeechRegion region = azureSpeechRegion
) )
// 设置TTS回调,更新状态 // 设置TTS回调
setTtsCallback(object : AzureTtsHelper.TtsCallback { setTtsCallback(object : AzureTtsHelper.TtsCallback {
override fun onSynthesisStarted() { override fun onSynthesisStarted() {
isTtsSpeaking = true isTtsSpeaking = true
// 状态变化,重置空闲检测
restartIdleCheck() restartIdleCheck()
sendEvent("tts_started", mapOf("status" to "started")) sendEvent("tts_started", mapOf("status" to "started"))
} }
override fun onSynthesizing() {} override fun onSynthesizing() {}
override fun onSynthesisCompleted() { override fun onSynthesisCompleted() {
isTtsSpeaking = false isTtsSpeaking = false
// 状态变化,重启空闲检测
restartIdleCheck() restartIdleCheck()
sendEvent("tts_completed", mapOf("status" to "completed")) sendEvent("tts_completed", mapOf("status" to "completed"))
} }
override fun onSynthesisCanceled() { override fun onSynthesisCanceled() {
isTtsSpeaking = false isTtsSpeaking = false
// 状态变化,重启空闲检测
restartIdleCheck() restartIdleCheck()
sendEvent("tts_canceled", mapOf("status" to "canceled")) sendEvent("tts_canceled", mapOf("status" to "canceled"))
} }
@ -208,7 +219,7 @@ object AgentService : CoroutineScope {
FileLogger.d(TAG, "代理服务初始化成功") FileLogger.d(TAG, "代理服务初始化成功")
return true return true
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "初始化失败", e) FileLogger.e(TAG, "初始化失败: ${e.message}")
return false return false
} }
} }
@ -238,7 +249,6 @@ object AgentService : CoroutineScope {
/** /**
* 启动空闲检测 * 启动空闲检测
* 使用协程实现延迟任务,比Timer更加轻量和可控
*/ */
private fun startIdleCheck() { private fun startIdleCheck() {
stopIdleCheck() // 先停止现有的检查 stopIdleCheck() // 先停止现有的检查
@ -247,21 +257,16 @@ object AgentService : CoroutineScope {
idleCheckJob = launch { idleCheckJob = launch {
try { try {
// FileLogger.d(TAG, "启动空闲检测,最大空闲时间: $maxIdleSeconds 秒")
// 直接等待指定的时间
delay(maxIdleSeconds * 1000L) delay(maxIdleSeconds * 1000L)
// 如果延迟后状态仍然是空闲的,则停止识别
if (isActive && isRecognitionActive && !hasSpeechDetected && !isTtsSpeaking && !isAiStreaming) { if (isActive && isRecognitionActive && !hasSpeechDetected && !isTtsSpeaking && !isAiStreaming) {
// FileLogger.d(TAG, "检测到空闲状态超过${maxIdleSeconds}秒,自动停止语音识别")
stopRecognition() stopRecognition()
sendEvent("auto_stop", mapOf("reason" to "idle_timeout", "seconds" to maxIdleSeconds)) sendEvent("auto_stop", mapOf("reason" to "idle_timeout", "seconds" to maxIdleSeconds))
} }
} catch (e: CancellationException) { } catch (e: CancellationException) {
// 协程被取消,这是正常行为 // 协程被取消,正常行为
// FileLogger.d(TAG, "空闲检测被取消")
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "空闲检测异常", e) FileLogger.e(TAG, "空闲检测异常: ${e.message}")
} }
} }
} }
@ -287,7 +292,7 @@ object AgentService : CoroutineScope {
/** /**
* 开始语音识别 * 开始语音识别
*/ */
fun startRecognition(isExternalActive:Boolean): Boolean { fun startRecognition(isExternalActive: Boolean): Boolean {
if (!isInitialized) { if (!isInitialized) {
FileLogger.e(TAG, "服务未初始化") FileLogger.e(TAG, "服务未初始化")
return false return false
@ -296,9 +301,8 @@ object AgentService : CoroutineScope {
if (isRecognitionActive) return true if (isRecognitionActive) return true
// 检查录音权限 // 检查录音权限
if (!isExternalActive && !checkRecordAudioPermission()) {
if (!isExternalActive&&!checkRecordAudioPermission()) { FileLogger.e(TAG, "需要录音权限")
FileLogger.e(TAG, "需要录音权限,请在设置中授予权限")
sendEvent("error", mapOf("code" to "PERMISSION_DENIED", "message" to "需要录音权限")) sendEvent("error", mapOf("code" to "PERMISSION_DENIED", "message" to "需要录音权限"))
return false return false
} }
@ -306,14 +310,13 @@ object AgentService : CoroutineScope {
isRecognitionActive = true isRecognitionActive = true
hasSpeechDetected = false hasSpeechDetected = false
try { try {
FileLogger.d(TAG, "选择音频源类型: ${isExternalActive}") //
// 选择音频源类型
val audioSourceType = if (isExternalActive) { val audioSourceType = if (isExternalActive) {
AzureAsrHelper.AudioSourceType.EXTERNAL AzureAsrHelper.AudioSourceType.EXTERNAL
} else { } else {
AzureAsrHelper.AudioSourceType.MICROPHONE AzureAsrHelper.AudioSourceType.MICROPHONE
} }
azureAsrHelper?.startContinuousRecognition(object : AzureAsrHelper.ContinuousRecognizeCallback { azureAsrHelper?.startContinuousRecognition(object : AzureAsrHelper.ContinuousRecognizeCallback {
override fun onRecognizing(recognizing: String, detectedLanguage: String) { override fun onRecognizing(recognizing: String, detectedLanguage: String) {
if (recognizing.isNotEmpty()) { if (recognizing.isNotEmpty()) {
@ -321,7 +324,6 @@ object AgentService : CoroutineScope {
val previousHasSpeech = hasSpeechDetected val previousHasSpeech = hasSpeechDetected
hasSpeechDetected = true hasSpeechDetected = true
// 状态发生变化时重启空闲检测
if (!previousHasSpeech) { if (!previousHasSpeech) {
restartIdleCheck() restartIdleCheck()
} }
@ -352,7 +354,6 @@ object AgentService : CoroutineScope {
val previousHasSpeech = hasSpeechDetected val previousHasSpeech = hasSpeechDetected
hasSpeechDetected = false hasSpeechDetected = false
// 状态发生变化时重启空闲检测
if (previousHasSpeech) { if (previousHasSpeech) {
restartIdleCheck() restartIdleCheck()
} }
@ -360,7 +361,6 @@ object AgentService : CoroutineScope {
override fun onSessionStarted() { override fun onSessionStarted() {
sendEvent("recognition_started", mapOf("status" to "started")) sendEvent("recognition_started", mapOf("status" to "started"))
// 启动空闲检测
startIdleCheck() startIdleCheck()
audioPlayer?.playAudio(R.raw.start) audioPlayer?.playAudio(R.raw.start)
} }
@ -384,17 +384,17 @@ object AgentService : CoroutineScope {
override fun onError(error: String) { override fun onError(error: String) {
isRecognitionActive = false isRecognitionActive = false
stopIdleCheck() stopIdleCheck()
FileLogger.e(TAG, "语音识别出错: $error") FileLogger.e(TAG, "语音识别错误: $error")
sendEvent("error", mapOf( sendEvent("error", mapOf(
"code" to "RECOGNITION_ERROR", "code" to "RECOGNITION_ERROR",
"message" to error "message" to error
)) ))
} }
},audioSourceType) }, audioSourceType)
return true return true
} catch (e: Exception) { } catch (e: Exception) {
isRecognitionActive = false isRecognitionActive = false
FileLogger.e(TAG, "启动语音识别失败", e) FileLogger.e(TAG, "启动语音识别失败: ${e.message}")
sendEvent("error", mapOf( sendEvent("error", mapOf(
"code" to "RECOGNITION_START_ERROR", "code" to "RECOGNITION_START_ERROR",
"message" to e.message.toString() "message" to e.message.toString()
@ -412,8 +412,6 @@ object AgentService : CoroutineScope {
fun stopRecognition() { fun stopRecognition() {
if (!isRecognitionActive) return if (!isRecognitionActive) return
FileLogger.d(TAG, "停止语音识别")
try { try {
azureAsrHelper?.stopContinuousRecognition() azureAsrHelper?.stopContinuousRecognition()
BleService.closeCodec() BleService.closeCodec()
@ -421,7 +419,7 @@ object AgentService : CoroutineScope {
stopIdleCheck() stopIdleCheck()
FileLogger.d(TAG, "语音识别已停止") FileLogger.d(TAG, "语音识别已停止")
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "停止语音识别异常", e) FileLogger.e(TAG, "停止语音识别异常: ${e.message}")
isRecognitionActive = false isRecognitionActive = false
stopIdleCheck() stopIdleCheck()
} }
@ -473,11 +471,6 @@ object AgentService : CoroutineScope {
/** /**
* 处理文本输入 * 处理文本输入
* 作为语音输入的补充,直接处理文本并通过事件返回结果
*
* @param text 用户输入文本
* @param speakResponse 是否朗读回复,默认为false
* @return 是否成功开始处理
*/ */
fun processTextInput(text: String, speakResponse: Boolean = false): Boolean { fun processTextInput(text: String, speakResponse: Boolean = false): Boolean {
if (!isInitialized) { if (!isInitialized) {
@ -492,10 +485,8 @@ object AgentService : CoroutineScope {
return false return false
} }
// 使用OpenAI处理文本 // 使用OpenAI处理文本
processWithOpenAI(text, speakResponse) processWithOpenAI(text, speakResponse)
return true return true
} }
@ -562,7 +553,7 @@ object AgentService : CoroutineScope {
stopAiStream() stopAiStream()
// 记录新的AI任务 // 记录新的AI任务
currentAiJob = launch(Dispatchers.IO) { currentAiJob = launch {
try { try {
// 设置状态为正在流式输出 // 设置状态为正在流式输出
isAiStreaming = true isAiStreaming = true
@ -671,6 +662,7 @@ object AgentService : CoroutineScope {
override fun onFunctionCall(call: JSONObject) { override fun onFunctionCall(call: JSONObject) {
try { try {
audioPlayer?.playAudio(R.raw.calling, true)
val name = call.getString("name") val name = call.getString("name")
sendEvent("function_call", mapOf( sendEvent("function_call", mapOf(
"name" to name, "name" to name,
@ -685,6 +677,7 @@ object AgentService : CoroutineScope {
} }
override fun onFunctionCallResult(functionCall: JSONObject, functionCallResult: JSONObject) { override fun onFunctionCallResult(functionCall: JSONObject, functionCallResult: JSONObject) {
audioPlayer?.stopAudio()
sendEvent("function_call_result", mapOf( sendEvent("function_call_result", mapOf(
"function_call" to functionCall.toString(), "function_call" to functionCall.toString(),
"result" to functionCallResult.toString(), "result" to functionCallResult.toString(),
@ -715,18 +708,19 @@ object AgentService : CoroutineScope {
private fun loadChatHistory() { private fun loadChatHistory() {
try { try {
// 清空当前历史记录 // 清空当前历史记录
while (historyMessages.length() > 0) { synchronized(historyMessages) {
historyMessages.remove(0) while (historyMessages.length() > 0) {
historyMessages.remove(0)
}
} }
// 使用新的ChatStorageHelper直接获取最近消息 // 获取最近消息
val recentMessages = chatStorageHelper.getRecentMessages(sessionId, 10) val recentMessages = chatStorageHelper.getRecentMessages(sessionId, 10)
if (recentMessages.isEmpty()) { if (recentMessages.isEmpty()) {
FileLogger.d(TAG, "没有找到历史记录")
return return
} }
// 将消息添加到历史记录 // 添加消息到历史记录
for (message in recentMessages) { for (message in recentMessages) {
val sender = message["sender"] as String val sender = message["sender"] as String
val content = message["message"] as String val content = message["message"] as String
@ -740,7 +734,7 @@ object AgentService : CoroutineScope {
FileLogger.d(TAG, "已加载${recentMessages.size}条历史记录") FileLogger.d(TAG, "已加载${recentMessages.size}条历史记录")
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "加载聊天历史失败", e) FileLogger.e(TAG, "加载聊天历史失败: ${e.message}")
} }
} }
@ -748,12 +742,14 @@ object AgentService : CoroutineScope {
* 添加消息到历史记录,保持最近10条 * 添加消息到历史记录,保持最近10条
*/ */
private fun addToHistoryMessages(message: JSONObject) { private fun addToHistoryMessages(message: JSONObject) {
// 添加新消息 synchronized(historyMessages) {
historyMessages.put(message) // 添加新消息
historyMessages.put(message)
// 如果超过10条,删除最早的消息
while (historyMessages.length() > 10) { // 如果超过10条,删除最早的消息
historyMessages.remove(0) while (historyMessages.length() > 10) {
historyMessages.remove(0)
}
} }
} }
@ -825,15 +821,15 @@ object AgentService : CoroutineScope {
* 清除聊天历史 * 清除聊天历史
*/ */
fun clearChatHistory(callback: (Boolean) -> Unit) { fun clearChatHistory(callback: (Boolean) -> Unit) {
launch(Dispatchers.IO) { launch {
try { try {
// 清除指定会话的所有消息
val success = chatStorageHelper.deleteMessages(sessionId) val success = chatStorageHelper.deleteMessages(sessionId)
if (success) { if (success) {
// 清空内存中的历史记录 synchronized(historyMessages) {
while (historyMessages.length() > 0) { while (historyMessages.length() > 0) {
historyMessages.remove(0) historyMessages.remove(0)
}
} }
FileLogger.d(TAG, "聊天历史已清除") FileLogger.d(TAG, "聊天历史已清除")
} else { } else {
@ -844,7 +840,7 @@ object AgentService : CoroutineScope {
callback(success) callback(success)
} }
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "清除聊天历史异常", e) FileLogger.e(TAG, "清除聊天历史异常: ${e.message}")
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
callback(false) callback(false)
} }
@ -856,13 +852,17 @@ object AgentService : CoroutineScope {
* 发送事件 * 发送事件
*/ */
private fun sendEvent(eventName: String, data: Map<String, Any>) { private fun sendEvent(eventName: String, data: Map<String, Any>) {
// 向所有监听器发送事件 launch {
synchronized(listeners) { withContext(Dispatchers.Main) {
listeners.forEach { listener -> // 向所有监听器发送事件
try { synchronized(listeners) {
listener.onEvent(eventName, data) listeners.forEach { listener ->
} catch (e: Exception) { try {
FileLogger.e(TAG, "向监听器发送事件失败", e) listener.onEvent(eventName, data)
} catch (e: Exception) {
FileLogger.e(TAG, "向监听器发送事件失败", e)
}
}
} }
} }
} }
@ -873,38 +873,24 @@ object AgentService : CoroutineScope {
*/ */
fun dispose() { fun dispose() {
try { try {
// 停止语音识别
stopRecognition() stopRecognition()
// 停止TTS播放
stopTts() stopTts()
// 停止AI流输出
stopAiStream() stopAiStream()
// 停止空闲检测
stopIdleCheck() stopIdleCheck()
// 释放音频播放器资源
audioPlayer?.release() audioPlayer?.release()
audioPlayer = null audioPlayer = null
// 释放Azure服务
azureAsrHelper?.dispose() azureAsrHelper?.dispose()
azureTtsHelper?.dispose() azureTtsHelper?.dispose()
// 取消所有协程
job.cancel() job.cancel()
// 清除监听器
clearListeners() clearListeners()
// 标记为未初始化
isInitialized = false isInitialized = false
FileLogger.d(TAG, "代理服务资源已释放") FileLogger.d(TAG, "代理服务资源已释放")
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "释放资源异常", e) FileLogger.e(TAG, "释放资源异常: ${e.message}")
} }
} }
@ -919,12 +905,6 @@ object AgentService : CoroutineScope {
/** /**
* 处理图片输入 * 处理图片输入
* 将图片与文本一起发送给AI进行处理
*
* @param imagePath 图片文件路径
* @param text 可选的文本描述或问题,默认为空
* @param speakResponse 是否朗读回复,默认为false
* @return 是否成功开始处理
*/ */
fun processImageInput(imagePath: String, text: String = "", speakResponse: Boolean = false): Boolean { fun processImageInput(imagePath: String, text: String = "", speakResponse: Boolean = false): Boolean {
if (!isInitialized) { if (!isInitialized) {
@ -945,20 +925,17 @@ object AgentService : CoroutineScope {
"imagePath" to imagePath "imagePath" to imagePath
)) ))
// 使用协程处理耗时的图片转换操作 // 处理图片
launch(Dispatchers.IO) { launch {
try { try {
// 将图片转换为Base64格式 // 将图片转换为Base64格式
val imageBase64 = openAIService.fileToBase64(imagePath) val imageBase64 = openAIService.fileToBase64(imagePath)
if (imageBase64 == null) { if (imageBase64 == null) {
withContext(Dispatchers.Main) { sendEvent("error", mapOf(
FileLogger.e(TAG, "图片转换失败: $imagePath") "code" to "IMAGE_CONVERSION_FAILED",
sendEvent("error", mapOf( "message" to "图片转换失败"
"code" to "IMAGE_CONVERSION_FAILED", ))
"message" to "图片转换失败"
))
}
return@launch return@launch
} }
@ -969,12 +946,12 @@ object AgentService : CoroutineScope {
"imagePath" to imagePath "imagePath" to imagePath
)) ))
// 使用OpenAI处理包含图片的消息 // 处理包含图片的消息
processImageWithOpenAI(imageBase64, text, speakResponse) processImageWithOpenAI(imageBase64, text, speakResponse)
} }
} catch (e: Exception) { } catch (e: Exception) {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
FileLogger.e(TAG, "处理图片输入失败", e) FileLogger.e(TAG, "处理图片失败: ${e.message}")
sendEvent("error", mapOf( sendEvent("error", mapOf(
"code" to "IMAGE_PROCESSING_ERROR", "code" to "IMAGE_PROCESSING_ERROR",
"message" to e.message.toString() "message" to e.message.toString()
@ -996,7 +973,7 @@ object AgentService : CoroutineScope {
* 播放音频资源 * 播放音频资源
* @param resId 资源ID * @param resId 资源ID
*/ */
fun playAudio(resId: Int) { fun playAudio(resId: Int, isLooping: Boolean = false) {
try { try {
// 释放之前的资源 // 释放之前的资源
release() release()
@ -1015,16 +992,24 @@ object AgentService : CoroutineScope {
// 准备并播放 // 准备并播放
prepare() prepare()
start() if (isLooping) {
start()
} else {
start()
setOnCompletionListener {
release()
}
}
} }
FileLogger.d(TAG, "开始播放音频资源") FileLogger.d(TAG, "开始播放音频资源")
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "播放音频资源异常: ${e.message}", e) FileLogger.e(TAG, "播放音频资源异常: ${e.message}", e)
release() release()
} }
} }
fun stopAudio() {
mediaPlayer?.stop()
}
/** /**
* 释放资源 * 释放资源
*/ */

2
local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleAgent.kt

@ -142,7 +142,7 @@ object BleAgent : BleService.Callback, AgentServiceListener {
// 停止当前TTS,避免冲突 // 停止当前TTS,避免冲突
AgentService.interruptCurrentResponse() AgentService.interruptCurrentResponse()
// AgentService.speakText("我在!") AgentService.speakText("我在!")
// 启动语音识别 // 启动语音识别
if (AgentService.isRecognitionActive) { if (AgentService.isRecognitionActive) {
// 如果已经在识别,则重新启动 // 如果已经在识别,则重新启动

BIN
local_plugins/agent_service/android/src/main/res/raw/calling.mp3

Binary file not shown.

BIN
local_plugins/agent_service/android/src/main/res/raw/stop.mp3

Binary file not shown.

BIN
local_plugins/agent_service/android/src/main/res/raw/stop3.mp3

Binary file not shown.

65
local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleCompanionManager.kt

@ -40,6 +40,9 @@ class BleCompanionManager(private val context: Context) {
context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
} }
// 用于验证的临时MAC地址
private var pendingMacAddress: String? = null
// 获取CompanionDeviceManager服务 // 获取CompanionDeviceManager服务
private val companionDeviceManager: CompanionDeviceManager? by lazy { private val companionDeviceManager: CompanionDeviceManager? by lazy {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
@ -140,9 +143,12 @@ class BleCompanionManager(private val context: Context) {
// 构建设备过滤器 // 构建设备过滤器
val deviceFilterBuilder = BluetoothDeviceFilter.Builder() val deviceFilterBuilder = BluetoothDeviceFilter.Builder()
// 添加设备名称模式 // 添加设备名称 - 严格匹配模式
deviceNamePattern?.let { deviceNamePattern?.let {
deviceFilterBuilder.setNamePattern(Pattern.compile(it)) // 使用"^设备名称$"格式创建精确匹配的正则表达式
val exactNamePattern = Pattern.compile("^$it$")
deviceFilterBuilder.setNamePattern(exactNamePattern)
FileLogger.i(TAG, "设置严格名称匹配: $it")
} }
// 添加服务UUID // 添加服务UUID
@ -150,48 +156,36 @@ class BleCompanionManager(private val context: Context) {
try { try {
val uuid = ParcelUuid(java.util.UUID.fromString(it)) val uuid = ParcelUuid(java.util.UUID.fromString(it))
deviceFilterBuilder.addServiceUuid(uuid, null) deviceFilterBuilder.addServiceUuid(uuid, null)
FileLogger.i(TAG, "设置服务UUID匹配: $it")
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "设置服务UUID失败: ${e.message}", e) FileLogger.e(TAG, "设置服务UUID失败: ${e.message}", e)
} }
} }
// 添加MAC地址 - 注意:setAddress方法在不同API版本有差异 // 添加MAC地址 - 严格匹配
macAddress?.let { macAddress?.let {
try { try {
// 检查是否是有效的MAC地址格式 // 检查是否是有效的MAC地址格式
if (!isValidMacAddress(it)) { if (!isValidMacAddress(it)) {
FileLogger.e(TAG, "无效的MAC地址格式: $it") FileLogger.e(TAG, "无效的MAC地址格式: $it")
return@let return false
} }
// 保存MAC地址,用于后续验证和处理 // 存储MAC地址到临时变量,成功匹配后再保存
savePairedMacAddress(it) pendingMacAddress = it
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { // 设置MAC地址严格匹配
try { deviceFilterBuilder.setAddress(it)
// Android O(8.0)及以上版本都应该使用字符串格式的MAC地址 FileLogger.i(TAG, "设置MAC地址严格匹配: $it")
deviceFilterBuilder.setAddress(it)
FileLogger.i(TAG, "成功设置MAC地址过滤: $it")
} catch (e: Exception) {
FileLogger.e(TAG, "setAddress失败: ${e.message}")
// 如果设置MAC地址失败,回退到使用设备名称过滤
deviceFilterBuilder.setNamePattern(Pattern.compile(".*"))
}
} else {
// Android 8.0以下版本不支持MAC地址过滤,使用设备名称过滤
FileLogger.w(TAG, "当前Android版本不支持MAC地址过滤,将使用设备名称过滤")
deviceFilterBuilder.setNamePattern(Pattern.compile(".*"))
}
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "处理MAC地址失败: ${e.message}", e) FileLogger.e(TAG, "设置MAC地址失败: ${e.message}", e)
// 发生异常时,使用通配名称模式 return false
deviceFilterBuilder.setNamePattern(Pattern.compile(".*"))
} }
} ?: run { } ?: run {
// 如果没有提供MAC地址且没有设置名称模式,则使用通配名称 // 如果没有提供MAC地址也没有提供设备名称,则返回错误
if (deviceNamePattern == null) { if (deviceNamePattern == null && serviceUuid == null) {
deviceFilterBuilder.setNamePattern(Pattern.compile(".*")) FileLogger.e(TAG, "严格匹配模式下必须提供MAC地址、设备名称或服务UUID之一")
FileLogger.i(TAG, "未提供MAC地址,使用通配名称模式") return false
} }
} }
@ -320,15 +314,19 @@ class BleCompanionManager(private val context: Context) {
} }
// 检查是否与预期的MAC地址匹配 // 检查是否与预期的MAC地址匹配
val savedMac = getPairedMacAddress() if (pendingMacAddress != null && pendingMacAddress != deviceMac) {
if (!savedMac.isNullOrEmpty() && savedMac != deviceMac) { FileLogger.w(TAG, "选中的设备MAC地址 ($deviceMac) 与预期的MAC地址 ($pendingMacAddress) 不匹配")
FileLogger.w(TAG, "选中的设备MAC地址 ($deviceMac) 与预期的MAC地址 ($savedMac) 不匹配,但仍会保存") // 如果要严格匹配MAC,可以在这里返回false
return false
} }
// 保存新的MAC地址 // 匹配成功后,保存新的MAC地址
savePairedMacAddress(deviceMac) savePairedMacAddress(deviceMac)
FileLogger.i(TAG, "设备关联成功: $deviceName ($deviceMac), 关联ID: $associationId") FileLogger.i(TAG, "设备关联成功: $deviceName ($deviceMac), 关联ID: $associationId")
// 清除临时MAC地址
pendingMacAddress = null
// 关联成功后,自动注册后台扫描 // 关联成功后,自动注册后台扫描
registerBackgroundScan() registerBackgroundScan()
@ -338,6 +336,8 @@ class BleCompanionManager(private val context: Context) {
} }
} else { } else {
FileLogger.w(TAG, "设备关联被取消或失败: resultCode=$resultCode") FileLogger.w(TAG, "设备关联被取消或失败: resultCode=$resultCode")
// 清除临时MAC地址
pendingMacAddress = null
} }
return false return false
@ -368,6 +368,7 @@ class BleCompanionManager(private val context: Context) {
// 构建过滤器 // 构建过滤器
val filter = ScanFilter.Builder() val filter = ScanFilter.Builder()
.setDeviceAddress(mac) .setDeviceAddress(mac)
.setManufacturerData(0xEFAB, byteArrayOf(0xA0.toByte(), 0x01))
.build() .build()
// 扫描设置 // 扫描设置

135
local_plugins/open_ai/README.md

@ -0,0 +1,135 @@
# OpenAI API 与 MCP 集成插件
这个 Flutter 插件提供了 OpenAI API 的访问能力和 Model Context Protocol (MCP) 工具调用功能的集成。
## 功能特点
- 使用官方 OpenAI Java SDK 进行异步通信
- 集成官方 MCP Kotlin SDK,使用 SSE 模式
- 支持流式输出响应
- 支持工具调用和结果处理
- 支持带图片的多模态对话
## MCP 集成
本插件使用 [MCP 官方 Kotlin SDK](https://github.com/modelcontextprotocol/kotlin-sdk) 实现与 MCP 服务器的通信。通过 SSE(Server-Sent Events)模式连接,能够:
- 获取 MCP 服务器提供的所有工具定义
- 动态调用远程工具并获取结果
- 支持本地工具的注册和调用
- 在 OpenAI API 请求中无缝集成工具调用功能
## 安装
在项目的 `pubspec.yaml` 中添加本地插件依赖:
```yaml
dependencies:
open_ai:
path: local_plugins/open_ai
```
## 使用方法
### 初始化
```dart
import 'package:open_ai/open_ai.dart';
final openAI = OpenAI();
await openAI.initialize(
apiKey: 'your-api-key',
baseUrl: 'https://api.example.com', // 可选,默认为 OpenAI 官方 API
model: 'gpt-3.5-turbo', // 可选,默认为 gpt-3.5-turbo
mcpServer: 'https://mcp.example.com', // MCP 服务器 SSE 端点地址
);
```
### 创建消息
```dart
// 创建系统消息
final systemMessage = await openAI.createSystemMessage('你是一个助手');
// 创建用户消息
final userMessage = await openAI.createUserMessage('你好,请帮我解释一下量子力学');
// 创建助手消息
final assistantMessage = await openAI.createAssistantMessage('我可以帮你解释量子力学');
// 创建带图片的用户消息
final imageBase64 = '...'; // base64编码的图片数据
final userImageMessage = await openAI.createUserMessageWithImage(
'这张图片中的物体是什么?',
imageBase64
);
```
### 发送消息(非流式输出)
```dart
final messages = [systemMessage, userMessage];
final response = await openAI.sendMessage(messages);
print('AI回复: $response');
```
### 发送消息(流式输出)
```dart
final messages = [systemMessage, userMessage];
final callback = StreamCallback(
onToken: (token) {
// 处理单个令牌
print('收到令牌: $token');
},
onComplete: () {
// 处理完成事件
print('响应完成');
},
onError: (error) {
// 处理错误
print('发生错误: $error');
},
onFunctionCall: (functionCall) {
// 处理函数调用
print('函数调用: $functionCall');
},
onFunctionCallResult: (functionCall, result) {
// 处理函数调用结果
print('函数调用结果: $result');
},
);
final streamId = await openAI.sendMessageStream(messages, callback);
```
### 取消当前流式请求
```dart
final success = await openAI.cancelCurrentStream();
```
### 释放资源
```dart
await openAI.dispose();
```
## 异常处理
该插件会在操作失败时抛出异常,请使用 try-catch 块捕获它们:
```dart
try {
final response = await openAI.sendMessage(messages);
} catch (e) {
print('发生错误: $e');
}
```
## 注意事项
- 初始化插件时必须提供有效的 API 密钥
- 使用流式响应时,请确保在完成后调用 `dispose()` 方法释放资源
- MCP 功能需要有效的 MCP 服务器 SSE 端点地址才能工作

71
local_plugins/open_ai/android/build.gradle.kts

@ -0,0 +1,71 @@
group = "com.yunqiinnovation.open_ai"
version = "1.0"
buildscript {
val kotlinVersion by extra("1.8.0")
repositories {
google()
mavenCentral()
}
dependencies {
classpath("com.android.tools.build:gradle:7.3.0")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion")
}
}
allprojects {
repositories {
google()
mavenCentral()
}
}
plugins {
id("com.android.library")
id("kotlin-android")
}
android {
compileSdk = 33
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
defaultConfig {
minSdk = 21
}
namespace = "com.yunqiinnovation.open_ai"
}
dependencies {
val kotlinVersion: String by project
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion")
implementation("androidx.annotation:annotation:1.6.0")
implementation("androidx.appcompat:appcompat:1.6.1")
// Kotlin协程
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1")
// OkHttp 依赖项
implementation("com.squareup.okhttp3:okhttp:4.11.0")
implementation("com.squareup.okhttp3:okhttp-sse:4.11.0")
// Jackson JSON解析器
implementation("com.fasterxml.jackson.core:jackson-databind:2.14.2")
// OpenAI Java库依赖
implementation("com.aallam.openai:openai-client:3.6.0")
implementation("io.ktor:ktor-client-okhttp:2.3.3")
// MCP官方Kotlin SDK依赖
implementation("io.modelcontextprotocol:kotlin-sdk:0.5.0")
}

258
local_plugins/open_ai/android/src/main/kotlin/com/yunqiinnovation/open_ai/MCPClient.kt

@ -0,0 +1,258 @@
package com.yunqiinnovation.open_ai
import android.content.Context
import android.util.Log
import com.fasterxml.jackson.databind.ObjectMapper
import io.modelcontextprotocol.kotlin.sdk.Implementation
import io.modelcontextprotocol.kotlin.sdk.client.Client
import io.modelcontextprotocol.kotlin.sdk.client.SseClientTransport
import io.modelcontextprotocol.kotlin.sdk.tools.ToolCallRequest
import io.modelcontextprotocol.kotlin.sdk.tools.ToolDefinition
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import okhttp3.*
import okhttp3.sse.EventSource
import okhttp3.sse.EventSourceListener
import okhttp3.sse.EventSources
import org.json.JSONArray
import org.json.JSONObject
import java.io.IOException
import java.util.concurrent.TimeUnit
import kotlin.coroutines.CoroutineContext
/**
* MCP功能处理接口
*/
interface FunctionHandler {
suspend fun handle(arguments: Map<String, Any>): String
}
/**
* Model Context Protocol (MCP)客户端实现类
* 使用官方 MCP Kotlin SDK
*/
class MCPClient(private val context: Context? = null) : CoroutineScope {
private val TAG = "MCPClient"
// 协程相关
private val job = SupervisorJob()
override val coroutineContext: CoroutineContext
get() = Dispatchers.IO + job
// OkHttp客户端
private val httpClient = OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.build()
// MCP客户端
private var client: Client? = null
// 已注册的工具
private val tools = mutableListOf<String>()
// 本地函数处理器
private val functionHandlers = mutableMapOf<String, FunctionHandler>()
// Json解析器
private val objectMapper = ObjectMapper()
// 连接状态
private var isConnected = false
/**
* 使用SSE方式连接到MCP服务器
*/
suspend fun connectToSSE(serverUrl: String): Boolean {
if (serverUrl.isEmpty()) {
Log.e(TAG, "服务器URL为空")
return false
}
try {
// 创建MCP客户端
client = Client(
clientInfo = Implementation(
name = "deepvoice-mcp-client",
version = "1.0.0"
)
)
// 创建SSE传输
val transport = SseClientTransport(
serverUrl = serverUrl,
httpClient = httpClient
)
// 连接到服务器
client?.connect(transport)
isConnected = true
// 获取工具定义
fetchTools()
return true
} catch (e: Exception) {
Log.e(TAG, "连接到MCP服务器失败: ${e.message}", e)
isConnected = false
return false
}
}
/**
* 获取工具定义
*/
private suspend fun fetchTools() {
try {
val availableTools = client?.listTools() ?: emptyList()
tools.clear()
availableTools.forEach { toolDefinition ->
val toolJson = toolDefinitionToJson(toolDefinition)
tools.add(toolJson.toString())
Log.d(TAG, "已获取工具: ${toolDefinition.name}")
}
} catch (e: Exception) {
Log.e(TAG, "获取工具定义失败: ${e.message}", e)
}
}
/**
* 将工具定义转换为JSON
*/
private fun toolDefinitionToJson(toolDefinition: ToolDefinition): JSONObject {
val functionObject = JSONObject().apply {
put("name", toolDefinition.name)
put("description", toolDefinition.description ?: "")
// 处理参数定义
if (toolDefinition.parameters != null) {
put("parameters", JSONObject(toolDefinition.parameters))
}
}
return JSONObject().apply {
put("type", "function")
put("function", functionObject)
}
}
/**
* 获取当前连接状态
*/
fun isConnected(): Boolean {
return isConnected && client != null
}
/**
* 注册本地函数
*/
fun registerLocalFunction(
name: String,
description: String,
parameters: JSONObject,
handler: FunctionHandler
): Boolean {
try {
// 创建工具定义
val toolDefinition = JSONObject().apply {
put("type", "function")
put("function", JSONObject().apply {
put("name", name)
put("description", description)
put("parameters", parameters)
})
}
// 添加到工具列表
tools.add(toolDefinition.toString())
// 注册处理器
functionHandlers[name] = handler
Log.d(TAG, "已注册本地函数: $name")
return true
} catch (e: Exception) {
Log.e(TAG, "注册本地函数失败: ${e.message}", e)
return false
}
}
/**
* 获取所有工具的定义
*/
fun getToolMaps(): List<String> {
return tools.toList()
}
/**
* 解析JSON参数
*/
fun parseJsonArguments(argumentsJson: String): Map<String, Any> {
try {
return objectMapper.readValue(argumentsJson, Map::class.java) as Map<String, Any>
} catch (e: Exception) {
Log.e(TAG, "解析JSON参数失败: ${e.message}", e)
return mapOf()
}
}
/**
* 调用工具
*/
suspend fun callTool(name: String, arguments: Map<String, Any>): JSONObject {
try {
// 检查是否是本地函数
if (functionHandlers.containsKey(name)) {
val handler = functionHandlers[name]
val result = handler?.handle(arguments) ?: throw Exception("函数处理器为空")
// 函数处理结果格式化为JSON
return JSONObject().apply {
put("context", result)
}
}
// 如果不是本地函数,使用MCP客户端调用远程工具
if (client != null && isConnected) {
val toolCallRequest = ToolCallRequest(
name = name,
arguments = objectMapper.writeValueAsString(arguments)
)
val result = client?.callTool(toolCallRequest)
return JSONObject().apply {
put("context", result?.result ?: "工具调用失败,未收到结果")
}
}
throw Exception("MCP客户端未连接")
} catch (e: Exception) {
Log.e(TAG, "调用工具失败: ${e.message}", e)
return JSONObject().apply {
put("context", "调用工具失败: ${e.message}")
}
}
}
/**
* 关闭客户端
*/
fun close() {
try {
client?.disconnect()
} catch (e: Exception) {
Log.e(TAG, "关闭MCP客户端失败: ${e.message}", e)
} finally {
client = null
isConnected = false
job.cancel() // 取消所有协程
tools.clear() // 清除工具列表
functionHandlers.clear() // 清除函数处理器
}
}
}

988
local_plugins/open_ai/android/src/main/kotlin/com/yunqiinnovation/open_ai/OpenAIService.kt

@ -0,0 +1,988 @@
package com.yunqiinnovation.open_ai
import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.util.Base64
import android.util.Log
import com.aallam.openai.api.chat.*
import com.aallam.openai.api.file.FileSource
import com.aallam.openai.api.http.Timeout
import com.aallam.openai.api.image.ImageCreation
import com.aallam.openai.api.model.ModelId
import com.aallam.openai.client.OpenAI
import com.aallam.openai.client.OpenAIConfig
import com.aallam.openai.client.OpenAIHost
import com.fasterxml.jackson.databind.ObjectMapper
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import okhttp3.*
import okhttp3.sse.EventSource
import okhttp3.sse.EventSourceListener
import okhttp3.sse.EventSources
import org.json.JSONArray
import org.json.JSONObject
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.IOException
import java.util.UUID
import java.util.concurrent.TimeUnit
import kotlin.coroutines.CoroutineContext
import kotlin.time.Duration.Companion.seconds
/**
* OpenAI服务的原生实现
* 集成了OpenAI官方Java SDK和MCP的官方Kotlin SDK
*/
class OpenAIService(private val context: Context? = null) : CoroutineScope {
private val TAG = "OpenAIService"
// 协程相关
private val job = SupervisorJob()
override val coroutineContext: CoroutineContext
get() = Dispatchers.IO + job
// 添加辅助方法,确保回调在主线程执行
private suspend fun safeCallback(block: suspend () -> Unit) {
withContext(Dispatchers.Main) {
block()
}
}
// OpenAI客户端
private var openAI: OpenAI? = null
private var baseUrl = "https://api.openai.com/v1/chat/completions"
private var apiKey: String = ""
private var isInitialized = false
private var model: String = "gpt-3.5-turbo" // 默认模型
private var visionModel: String = "gpt-4-vision-preview" // 默认视觉模型
// OkHttp客户端用于流式请求
private val client = OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.build()
// MCP客户端
private var mcpClient: MCPClient? = null
private var isMcpInitialized = false
// 当前事件源
private var currentEventSource: EventSource? = null
private var isCanceled = false
/**
* 初始化OpenAI服务
*/
fun initialize(apiKey: String, baseUrl: String, model: String, mcpServer: String): Boolean {
this.apiKey = apiKey
if (baseUrl.isNotEmpty()) {
this.baseUrl = baseUrl
}
if (model.isNotEmpty()) {
this.model = model
}
// 初始化OpenAI客户端
try {
val timeout = Timeout(socket = 30.seconds, connect = 30.seconds, request = 30.seconds)
val host = if (baseUrl.isNotEmpty() && baseUrl != "https://api.openai.com/v1/chat/completions") {
OpenAIHost(baseUrl)
} else {
OpenAIHost.Default
}
val config = OpenAIConfig(
token = apiKey,
host = host,
timeout = timeout
)
openAI = OpenAI(config)
} catch (e: Exception) {
Log.e(TAG, "OpenAI客户端初始化失败: ${e.message}", e)
return false
}
// 初始化MCPClient
initializeMcpClient(mcpServer)
isInitialized = apiKey.isNotEmpty() && openAI != null
return isInitialized
}
/**
* 将文件转换为Base64字符串
*/
fun fileToBase64(filePath: String, maxSizeKB: Int = 20480): String? {
try {
val file = File(filePath)
if (!file.exists() || !file.isFile) {
Log.e(TAG, "文件不存在: $filePath")
return null
}
// 读取文件并压缩(如果需要)
val originalBitmap = BitmapFactory.decodeFile(filePath)
if (originalBitmap == null) {
Log.e(TAG, "无法解码图片: $filePath")
return null
}
val outputStream = ByteArrayOutputStream()
var quality = 100
var compressedBitmap = originalBitmap
// 检查图片尺寸,限制最大为1024*1024
val maxDimension = 1024
if (originalBitmap.width > maxDimension || originalBitmap.height > maxDimension) {
Log.d(TAG, "图片尺寸超过限制,进行缩放: ${originalBitmap.width}x${originalBitmap.height} -> ${maxDimension}x${maxDimension}")
// 计算缩放比例,保持纵横比
val widthRatio = maxDimension.toFloat() / originalBitmap.width
val heightRatio = maxDimension.toFloat() / originalBitmap.height
val ratio = Math.min(widthRatio, heightRatio)
val newWidth = (originalBitmap.width * ratio).toInt()
val newHeight = (originalBitmap.height * ratio).toInt()
compressedBitmap = Bitmap.createScaledBitmap(originalBitmap, newWidth, newHeight, true)
Log.d(TAG, "缩放后图片尺寸: ${newWidth}x${newHeight}")
}
// 如果原始图片太大,继续优化文件大小
var fileSize = file.length() / 1024 // 转为KB
if (fileSize > maxSizeKB) {
val scale = Math.sqrt(maxSizeKB.toDouble() / fileSize)
val newWidth = (compressedBitmap.width * scale).toInt()
val newHeight = (compressedBitmap.height * scale).toInt()
compressedBitmap = Bitmap.createScaledBitmap(compressedBitmap, newWidth, newHeight, true)
quality = 85
}
// 压缩图片
compressedBitmap.compress(Bitmap.CompressFormat.JPEG, quality, outputStream)
val imageBytes = outputStream.toByteArray()
// 检查压缩后大小
if (imageBytes.size / 1024 > maxSizeKB) {
Log.w(TAG, "压缩后图片仍然超出大小限制: ${imageBytes.size / 1024}KB > ${maxSizeKB}KB")
}
// 转为Base64
return Base64.encodeToString(imageBytes, Base64.NO_WRAP)
} catch (e: Exception) {
Log.e(TAG, "转换文件到Base64失败: ${e.message}", e)
return null
}
}
/**
* 创建带图片的用户消息
*/
fun createUserMessageWithImage(text: String, imageBase64: String): JSONObject {
// 创建包含文本和图片的内容数组
val contentArray = JSONArray().apply {
// 添加文本部分
if (text.isNotEmpty()) {
put(JSONObject().apply {
put("type", "text")
put("text", text)
})
}
// 添加图片部分
put(JSONObject().apply {
put("type", "image_url")
put("image_url", JSONObject().apply {
put("url", "data:image/jpeg;base64,$imageBase64")
})
})
}
return JSONObject().apply {
put("role", "user")
put("content", contentArray)
}
}
/**
* 创建用户消息
*/
fun createUserMessage(content: String): JSONObject {
return JSONObject().apply {
put("role", "user")
put("content", content)
}
}
/**
* 创建系统消息
*/
fun createSystemMessage(content: String): JSONObject {
return JSONObject().apply {
put("role", "system")
put("content", content)
}
}
/**
* 创建助手消息
*/
fun createAssistantMessage(content: String): JSONObject {
return JSONObject().apply {
put("role", "assistant")
put("content", content)
}
}
/**
* MCP客户端是否已初始化
*/
fun isMcpInitialized(): Boolean {
return isMcpInitialized && mcpClient?.isConnected() == true
}
/**
* 关闭MCP客户端
*/
fun closeMcpClient() {
mcpClient?.close()
mcpClient = null
isMcpInitialized = false
}
/**
* 初始化MCP客户端
*/
private fun initializeMcpClient(mcpServer: String): Boolean {
if (mcpClient != null) {
mcpClient?.close()
}
if (mcpServer.isEmpty()) {
return false
}
mcpClient = MCPClient(context)
// 在后台线程中初始化MCP客户端
launch {
try {
val result = mcpClient?.connectToSSE(mcpServer) ?: 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工具调用
*/
private suspend fun handleMcpToolCall(functionCall: JSONObject): JSONObject? {
if (mcpClient == null || !isMcpInitialized) {
return JSONObject().apply { put("context", "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 JSONObject().apply { put("context", "处理MCP工具调用失败: ${e.message}") }
}
}
/**
* 将JSONArray转换为ChatCompletionRequest中的消息列表
*/
private fun parseMessages(messagesArray: JSONArray): List<ChatMessage> {
val messages = mutableListOf<ChatMessage>()
for (i in 0 until messagesArray.length()) {
val messageObj = messagesArray.getJSONObject(i)
val role = messageObj.getString("role")
when (role) {
"system" -> {
val content = messageObj.getString("content")
messages.add(ChatMessage(role = ChatRole.System, content = content))
}
"user" -> {
// 检查是否有多媒体内容
if (messageObj.has("content") && messageObj.get("content") is JSONArray) {
val contentArray = messageObj.getJSONArray("content")
val parts = mutableListOf<ChatMessageContent>()
for (j in 0 until contentArray.length()) {
val contentObj = contentArray.getJSONObject(j)
val type = contentObj.getString("type")
when (type) {
"text" -> {
parts.add(TextContent(contentObj.getString("text")))
}
"image_url" -> {
val imageUrlObj = contentObj.getJSONObject("image_url")
val url = imageUrlObj.getString("url")
parts.add(ImageContent(url))
}
}
}
messages.add(ChatMessage(
role = ChatRole.User,
content = parts
))
} else {
// 普通文本消息
val content = messageObj.getString("content")
messages.add(ChatMessage(role = ChatRole.User, content = content))
}
}
"assistant" -> {
if (messageObj.has("tool_calls")) {
// 处理工具调用
val toolCalls = messageObj.getJSONArray("tool_calls")
val toolCallsList = mutableListOf<ToolCall>()
for (j in 0 until toolCalls.length()) {
val toolCall = toolCalls.getJSONObject(j)
val id = toolCall.getString("id")
val function = toolCall.getJSONObject("function")
val name = function.getString("name")
val arguments = function.getString("arguments")
toolCallsList.add(ToolCall(
id = id,
type = ToolCallType.Function,
function = FunctionCall(
name = name,
arguments = arguments
)
))
}
val content = if (messageObj.has("content")) messageObj.getString("content") else ""
messages.add(ChatMessage(
role = ChatRole.Assistant,
content = content,
toolCalls = toolCallsList
))
} else {
// 普通消息
val content = messageObj.getString("content")
messages.add(ChatMessage(role = ChatRole.Assistant, content = content))
}
}
"tool" -> {
val content = messageObj.getString("content")
val toolCallId = messageObj.getString("tool_call_id")
messages.add(ChatMessage(
role = ChatRole.Tool,
content = content,
toolCallId = toolCallId
))
}
}
}
return messages
}
/**
* 从MCP客户端获取工具定义
*/
private fun getToolsFromMcpClient(): List<Tool> {
val tools = mutableListOf<Tool>()
mcpClient?.getToolMaps()?.forEach { toolMap ->
try {
val toolJson = JSONObject(toolMap)
val name = toolJson.getString("name")
val description = toolJson.getString("description")
val parametersJson = if (toolJson.has("parameters")) toolJson.getJSONObject("parameters") else null
val parameterProperties = mutableMapOf<String, ParameterDefinition>()
val requiredParams = mutableListOf<String>()
if (parametersJson != null && parametersJson.has("properties")) {
val properties = parametersJson.getJSONObject("properties")
val keys = properties.keys()
while (keys.hasNext()) {
val key = keys.next()
val propertyObj = properties.getJSONObject(key)
val propType = propertyObj.optString("type", "string")
val propDescription = propertyObj.optString("description", "")
parameterProperties[key] = ParameterDefinition(
type = propType,
description = propDescription
)
}
// 获取必填参数
if (parametersJson.has("required")) {
val requiredArr = parametersJson.getJSONArray("required")
for (i in 0 until requiredArr.length()) {
requiredParams.add(requiredArr.getString(i))
}
}
}
tools.add(Tool(
type = ToolType.Function,
function = FunctionDefinition(
name = name,
description = description,
parameters = FunctionParameters(
type = "object",
properties = parameterProperties,
required = requiredParams
)
)
))
} catch (e: Exception) {
Log.e(TAG, "解析工具定义失败: ${e.message}", e)
}
}
return tools
}
/**
* 发送消息(非流式输出)
*/
suspend fun sendMessage(messages: JSONArray): String {
if (!isInitialized || apiKey.isEmpty() || openAI == null) {
throw IOException("OpenAI服务未初始化")
}
try {
// 解析消息
val parsedMessages = parseMessages(messages)
// 获取工具列表
val tools = getToolsFromMcpClient()
// 创建请求
val request = ChatCompletionRequest(
model = ModelId(model),
messages = parsedMessages,
tools = if (tools.isNotEmpty()) tools else null,
temperature = 0.7,
maxTokens = 2000
)
// 发送请求
val response = openAI!!.chatCompletion(request)
// 解析响应
val choice = response.choices.firstOrNull() ?: throw IOException("无效的响应格式")
// 检查是否有工具调用
if (choice.message.toolCalls?.isNotEmpty() == true) {
val toolCall = choice.message.toolCalls?.first()
if (toolCall != null && toolCall.function != null) {
val functionCall = JSONObject().apply {
put("name", toolCall.function.name)
put("arguments", toolCall.function.arguments)
put("id", toolCall.id)
}
return functionCall.toString()
}
}
// 返回消息内容
return choice.message.content ?: ""
} catch (e: Exception) {
Log.e(TAG, "发送消息失败: ${e.message}", e)
throw IOException("与AI服务通信失败: ${e.message}")
}
}
/**
* 发送消息(流式输出)
*/
fun sendMessageStream(messages: JSONArray, callback: StreamCallback) {
if (!isInitialized || apiKey.isEmpty()) {
// 在主线程执行回调
launch {
withContext(Dispatchers.Main) {
callback.onError(Exception("OpenAI服务未初始化"))
}
}
return
}
// 重置取消状态
isCanceled = false
// 检查是否有图片消息
var hasImageContent = false
var currentModel = model
for (i in 0 until messages.length()) {
val messageObj = messages.getJSONObject(i)
if (messageObj.getString("role") == "user" && messageObj.has("content")) {
val content = messageObj.get("content")
if (content is JSONArray) {
for (j in 0 until content.length()) {
val contentObj = content.getJSONObject(j)
if (contentObj.getString("type") == "image_url") {
hasImageContent = true
currentModel = visionModel
break
}
}
}
}
if (hasImageContent) break
}
// 构建JSON请求体
val requestBody = JSONObject().apply {
put("model", currentModel)
put("messages", messages)
put("temperature", 0.7)
put("max_tokens", 2000)
put("stream", true)
// 添加工具列表
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)
}
}
val mediaType = "application/json".toMediaTypeOrNull()
val request = Request.Builder()
.url(baseUrl)
.addHeader("Content-Type", "application/json")
.addHeader("Authorization", "Bearer $apiKey")
.addHeader("Accept", "text/event-stream")
.post(requestBody.toString().toRequestBody(mediaType))
.build()
// 创建事件源
val factory = EventSources.createFactory(client)
// 工具调用相关变量
val toolCalls = mutableMapOf<Int, ToolCallInfo>()
val eventSourceListener = object : EventSourceListener() {
override fun onOpen(eventSource: EventSource, response: Response) {
Log.d(TAG, "SSE连接已打开")
}
override fun onEvent(eventSource: EventSource, id: String?, type: String?, data: String) {
if (isCanceled) return
if (data == "[DONE]" || data == "[\"DONE\"]") {
// 处理可能的工具调用
processToolCalls(toolCalls, callback, messages)
return
}
try {
val jsonData = JSONObject(data)
// 处理消息内容
if (jsonData.has("choices")) {
val choices = jsonData.getJSONArray("choices")
if (choices.length() > 0) {
val choice = choices.getJSONObject(0)
// 处理delta内容
if (choice.has("delta")) {
val delta = choice.getJSONObject("delta")
// 处理普通文本内容
if (delta.has("content")) {
val content = delta.getString("content")
if (!isCanceled) {
launch {
withContext(Dispatchers.Main) {
callback.onToken(content)
}
}
}
}
// 处理工具调用
if (delta.has("tool_calls")) {
val deltaToolCalls = delta.getJSONArray("tool_calls")
for (i in 0 until deltaToolCalls.length()) {
val toolCall = deltaToolCalls.getJSONObject(i)
val index = toolCall.getInt("index")
// 创建或获取现有的工具调用信息
val toolCallInfo = toolCalls.getOrPut(index) { ToolCallInfo() }
// 更新ID
if (toolCall.has("id")) {
toolCallInfo.id = toolCall.getString("id")
}
// 更新函数信息
if (toolCall.has("function")) {
val function = toolCall.getJSONObject("function")
if (function.has("name")) {
toolCallInfo.name = function.getString("name")
}
if (function.has("arguments")) {
toolCallInfo.arguments += function.getString("arguments")
}
}
}
}
}
// 检查是否有表示完成的标志
if (choice.has("finish_reason")) {
val finishReason = choice.getString("finish_reason")
if (finishReason == "stop" || finishReason == "length") {
// 正常完成,没有工具调用
launch {
withContext(Dispatchers.Main) {
callback.onComplete()
}
}
closeEventSource()
} else if (finishReason == "tool_calls") {
// 处理工具调用
processToolCalls(toolCalls, callback, messages)
closeEventSource()
}
}
}
}
} catch (e: Exception) {
Log.e(TAG, "解析事件数据失败: ${e.message}", e)
}
}
override fun onClosed(eventSource: EventSource) {
Log.d(TAG, "SSE连接已关闭")
if (!isCanceled) {
// 如果没有正常完成,但连接关闭了,则处理最后可能的工具调用
if (toolCalls.isNotEmpty()) {
processToolCalls(toolCalls, callback, messages)
} else {
launch {
withContext(Dispatchers.Main) {
callback.onComplete()
}
}
}
}
}
override fun onFailure(eventSource: EventSource, t: Throwable?, response: Response?) {
if (isCanceled) return
val errorCode = response?.code ?: 0
val errorMessage = t?.message ?: "未知错误"
Log.e(TAG, "SSE连接失败: $errorCode - $errorMessage")
launch {
withContext(Dispatchers.Main) {
callback.onError(Exception("流式请求失败: $errorMessage"))
}
}
closeEventSource()
}
}
currentEventSource = factory.newEventSource(request, eventSourceListener)
}
/**
* 处理工具调用
*/
private fun processToolCalls(toolCalls: Map<Int, ToolCallInfo>, callback: StreamCallback, messages: JSONArray? = null): Boolean {
if (toolCalls.isEmpty()) return false
val firstToolCall = toolCalls.entries.firstOrNull()?.value ?: return false
if (firstToolCall.isValid()) {
// 创建函数调用JSON对象
val functionCall = JSONObject().apply {
put("name", firstToolCall.name)
put("arguments", firstToolCall.arguments)
put("id", firstToolCall.id)
}
Log.d(TAG, "工具调用: $functionCall")
// 通知上层回调
launch {
withContext(Dispatchers.Main) {
callback.onFunctionCall(functionCall)
}
}
// 在协程中自动处理工具调用
if (messages != null) {
launch {
try {
if (!isCanceled) {
handleToolCall(functionCall, messages, callback)
}
} catch (e: Exception) {
if (!isCanceled) {
Log.e(TAG, "处理工具调用时发生异常: ${e.message}", e)
try {
val errorMessage = "工具调用处理失败: ${e.message}"
sendFunctionCallResult(
messages = messages,
functionCall = functionCall,
functionResult = errorMessage,
callback = callback
)
} catch (e2: Exception) {
Log.e(TAG, "发送工具调用错误结果失败: ${e2.message}", e2)
withContext(Dispatchers.Main) {
callback.onError(Exception("工具调用处理失败: ${e.message}"))
}
}
}
}
}
}
return true
}
return false
}
/**
* 处理工具调用
*/
private suspend fun handleToolCall(functionCall: JSONObject, messages: JSONArray, callback: StreamCallback) {
try {
// 获取函数名称和参数
val name = functionCall.getString("name")
val argumentsJson = functionCall.getString("arguments")
Log.d(TAG, "处理工具调用: name=$name, arguments=$argumentsJson")
// 调用MCP工具
val result = handleMcpToolCall(functionCall)
// 检查是否已取消
if (isCanceled) {
Log.d(TAG, "工具调用已被取消,不处理结果")
return
}
// 回调结果
val resultObj = result ?: JSONObject().apply { put("context", "工具调用失败") }
withContext(Dispatchers.Main) {
callback.onFunctionCallResult(functionCall, resultObj)
}
// 从结果中提取内容
val resultContent = if (resultObj.has("context") && resultObj.optString("context").isNotEmpty()) {
resultObj.getString("context")
} else {
val jsonString = resultObj.toString()
if (jsonString == "{}") "工具调用失败" else jsonString
}
// 发送函数调用结果
sendFunctionCallResult(messages, functionCall, resultContent, callback)
} catch (e: Exception) {
if (!isCanceled) {
Log.e(TAG, "处理工具调用失败: ${e.message}", e)
withContext(Dispatchers.Main) {
callback.onError(Exception("工具调用失败: ${e.message}"))
}
}
}
}
/**
* 发送函数调用结果
*/
fun sendFunctionCallResult(
messages: JSONArray,
functionCall: JSONObject,
functionResult: String,
callback: StreamCallback
) {
if (isCanceled) {
Log.d(TAG, "请求已取消,不发送函数调用结果")
return
}
try {
val fullMessages = JSONArray()
// 添加原始消息
for (i in 0 until messages.length()) {
fullMessages.put(messages.getJSONObject(i))
}
// 添加函数调用消息
val callId = functionCall.optString("id", "call_${System.currentTimeMillis()}")
fullMessages.put(JSONObject().apply {
put("role", "assistant")
put("content", "")
// 添加工具调用
val toolCalls = JSONArray().apply {
val toolCall = JSONObject().apply {
put("id", callId)
put("type", "function")
put("function", JSONObject().apply {
put("name", functionCall.getString("name"))
put("arguments", functionCall.getString("arguments"))
})
}
put(toolCall)
}
put("tool_calls", toolCalls)
})
// 添加函数调用结果
fullMessages.put(JSONObject().apply {
put("role", "tool")
put("content", functionResult)
put("tool_call_id", callId)
})
// 发送完整对话
sendMessageStream(fullMessages, callback)
} catch (e: Exception) {
if (!isCanceled) {
Log.e(TAG, "发送函数调用结果失败: ${e.message}", e)
launch {
withContext(Dispatchers.Main) {
callback.onError(Exception("发送函数调用结果失败: ${e.message}"))
}
}
}
}
}
/**
* 关闭事件源
*/
private fun closeEventSource() {
currentEventSource?.let {
try {
it.cancel()
} catch (e: Exception) {
Log.e(TAG, "关闭事件源失败: ${e.message}", e)
}
currentEventSource = null
}
}
/**
* 取消当前流式请求
*/
fun cancelCurrentStream(): Boolean {
isCanceled = true
closeEventSource()
return true
}
/**
* 释放资源
*/
fun dispose() {
job.cancel() // 取消所有协程
cancelCurrentStream() // 取消当前流式请求
closeMcpClient() // 关闭MCP客户端
}
/**
* 工具调用信息类
*/
private class ToolCallInfo {
var id: String = ""
var name: String = ""
var arguments: String = ""
fun isValid(): Boolean {
return id.isNotEmpty() && name.isNotEmpty()
}
}
/**
* 流式回调接口
*/
interface StreamCallback {
fun onToken(token: String)
fun onComplete()
fun onError(e: Exception)
fun onFunctionCall(functionCall: JSONObject)
fun onFunctionCallResult(functionCall: JSONObject, functionCallResult: JSONObject)
}
/**
* 参数定义
*/
private data class ParameterDefinition(
val type: String,
val description: String
)
/**
* 注册函数
*/
fun registerFunction(name: String, description: String, parameters: JSONObject): Boolean {
try {
// 确保MCPClient已初始化
if (mcpClient == null) {
mcpClient = MCPClient(context)
}
// 创建函数处理器
val handler = object : FunctionHandler {
override suspend fun handle(arguments: Map<String, Any>): String {
// 由于本地函数的实际处理是在Flutter端完成的
// 这里只需返回一个标记,表示该函数是本地函数
return "LOCAL_FUNCTION:$name"
}
}
// 注册本地函数
return mcpClient?.registerLocalFunction(name, description, parameters, handler) ?: false
} catch (e: Exception) {
Log.e(TAG, "注册函数失败: ${e.message}", e)
return false
}
}
}

149
local_plugins/open_ai/android/src/main/kotlin/com/yunqiinnovation/open_ai/OpenAiPlugin.kt

@ -0,0 +1,149 @@
package com.yunqiinnovation.open_ai
import android.content.Context
import androidx.annotation.NonNull
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 kotlinx.coroutines.*
class OpenAiPlugin : FlutterPlugin, MethodCallHandler {
private lateinit var channel: MethodChannel
private lateinit var context: Context
private lateinit var openAIService: OpenAIService
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "com.yunqiinnovation.open_ai")
context = flutterPluginBinding.applicationContext
openAIService = OpenAIService(context)
channel.setMethodCallHandler(this)
}
override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
when (call.method) {
"initialize" -> {
val apiKey = call.argument<String>("apiKey") ?: ""
val baseUrl = call.argument<String>("baseUrl") ?: ""
val model = call.argument<String>("model") ?: ""
val mcpServer = call.argument<String>("mcpServer") ?: ""
val success = openAIService.initialize(apiKey, baseUrl, model, mcpServer)
result.success(success)
}
"createUserMessage" -> {
val content = call.argument<String>("content") ?: ""
val message = openAIService.createUserMessage(content)
result.success(message.toString())
}
"createSystemMessage" -> {
val content = call.argument<String>("content") ?: ""
val message = openAIService.createSystemMessage(content)
result.success(message.toString())
}
"createAssistantMessage" -> {
val content = call.argument<String>("content") ?: ""
val message = openAIService.createAssistantMessage(content)
result.success(message.toString())
}
"createUserMessageWithImage" -> {
val text = call.argument<String>("text") ?: ""
val imageBase64 = call.argument<String>("imageBase64") ?: ""
val message = openAIService.createUserMessageWithImage(text, imageBase64)
result.success(message.toString())
}
"sendMessage" -> {
val messagesJson = call.argument<String>("messages") ?: "[]"
val messages = JSONArray(messagesJson)
scope.launch {
try {
val response = openAIService.sendMessage(messages)
withContext(Dispatchers.Main) {
result.success(response)
}
} catch (e: Exception) {
withContext(Dispatchers.Main) {
result.error("OPENAI_ERROR", e.message ?: "Unknown error", null)
}
}
}
}
"sendMessageStream" -> {
val messagesJson = call.argument<String>("messages") ?: "[]"
val messages = JSONArray(messagesJson)
val streamId = call.argument<String>("streamId") ?: "${System.currentTimeMillis()}"
val callback = object : OpenAIService.StreamCallback {
override fun onToken(token: String) {
val map = mapOf(
"type" to "token",
"streamId" to streamId,
"data" to token
)
channel.invokeMethod("onStreamEvent", map)
}
override fun onComplete() {
val map = mapOf(
"type" to "complete",
"streamId" to streamId
)
channel.invokeMethod("onStreamEvent", map)
}
override fun onError(e: Exception) {
val map = mapOf(
"type" to "error",
"streamId" to streamId,
"error" to (e.message ?: "Unknown error")
)
channel.invokeMethod("onStreamEvent", map)
}
override fun onFunctionCall(functionCall: JSONObject) {
val map = mapOf(
"type" to "functionCall",
"streamId" to streamId,
"data" to functionCall.toString()
)
channel.invokeMethod("onStreamEvent", map)
}
override fun onFunctionCallResult(functionCall: JSONObject, functionCallResult: JSONObject) {
val map = mapOf(
"type" to "functionCallResult",
"streamId" to streamId,
"functionCall" to functionCall.toString(),
"result" to functionCallResult.toString()
)
channel.invokeMethod("onStreamEvent", map)
}
}
openAIService.sendMessageStream(messages, callback)
result.success(streamId)
}
"cancelCurrentStream" -> {
val success = openAIService.cancelCurrentStream()
result.success(success)
}
"dispose" -> {
openAIService.dispose()
result.success(null)
}
else -> {
result.notImplemented()
}
}
}
override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
channel.setMethodCallHandler(null)
scope.cancel() // Cancel all coroutines when the plugin is detached
openAIService.dispose() // Clean up resources
}
}

184
local_plugins/open_ai/lib/open_ai.dart

@ -0,0 +1,184 @@
import 'dart:async';
import 'dart:convert';
import 'package:flutter/services.dart';
/// OpenAI和MCP集成插件
class OpenAI {
/// 插件通道
static const MethodChannel _channel =
MethodChannel('com.yunqiinnovation.open_ai');
/// 流式输出事件回调
static final Map<String, StreamCallback> _streamCallbacks = {};
/// 构造函数
OpenAI() {
_channel.setMethodCallHandler(_handleMethodCall);
}
/// 处理来自原生端的方法调用
Future<dynamic> _handleMethodCall(MethodCall call) async {
if (call.method == 'onStreamEvent') {
final Map<String, dynamic> args = Map<String, dynamic>.from(call.arguments);
final String streamId = args['streamId'];
final String type = args['type'];
final callback = _streamCallbacks[streamId];
if (callback != null) {
switch (type) {
case 'token':
callback.onToken(args['data']);
break;
case 'complete':
callback.onComplete();
_streamCallbacks.remove(streamId);
break;
case 'error':
callback.onError(Exception(args['error']));
_streamCallbacks.remove(streamId);
break;
case 'functionCall':
final functionCall = jsonDecode(args['data']);
callback.onFunctionCall(functionCall);
break;
case 'functionCallResult':
final functionCall = jsonDecode(args['functionCall']);
final functionCallResult = jsonDecode(args['result']);
callback.onFunctionCallResult(functionCall, functionCallResult);
break;
}
}
}
return null;
}
/// 初始化OpenAI服务
///
/// [apiKey] OpenAI API密钥
/// [baseUrl] API基础URL,可选
/// [model] 模型名称,可选
/// [mcpServer] MCP服务器URL,可选
Future<bool> initialize({
required String apiKey,
String baseUrl = '',
String model = '',
String mcpServer = '',
}) async {
final result = await _channel.invokeMethod<bool>('initialize', {
'apiKey': apiKey,
'baseUrl': baseUrl,
'model': model,
'mcpServer': mcpServer,
});
return result ?? false;
}
/// 创建用户消息
Future<Map<String, dynamic>> createUserMessage(String content) async {
final result = await _channel.invokeMethod<String>('createUserMessage', {
'content': content,
});
return jsonDecode(result ?? '{}');
}
/// 创建系统消息
Future<Map<String, dynamic>> createSystemMessage(String content) async {
final result = await _channel.invokeMethod<String>('createSystemMessage', {
'content': content,
});
return jsonDecode(result ?? '{}');
}
/// 创建助手消息
Future<Map<String, dynamic>> createAssistantMessage(String content) async {
final result = await _channel.invokeMethod<String>('createAssistantMessage', {
'content': content,
});
return jsonDecode(result ?? '{}');
}
/// 创建带图片的用户消息
Future<Map<String, dynamic>> createUserMessageWithImage(
String text,
String imageBase64,
) async {
final result = await _channel.invokeMethod<String>(
'createUserMessageWithImage',
{
'text': text,
'imageBase64': imageBase64,
},
);
return jsonDecode(result ?? '{}');
}
/// 发送消息(非流式输出)
Future<String> sendMessage(List<Map<String, dynamic>> messages) async {
final messagesJson = jsonEncode(messages);
return await _channel.invokeMethod('sendMessage', {
'messages': messagesJson,
});
}
/// 发送消息(流式输出)
Future<String> sendMessageStream(
List<Map<String, dynamic>> messages,
StreamCallback callback,
) async {
final messagesJson = jsonEncode(messages);
final streamId = DateTime.now().millisecondsSinceEpoch.toString();
// 注册回调
_streamCallbacks[streamId] = callback;
final result = await _channel.invokeMethod<String>('sendMessageStream', {
'messages': messagesJson,
'streamId': streamId,
});
return result ?? streamId;
}
/// 取消当前流式请求
Future<bool> cancelCurrentStream() async {
final result = await _channel.invokeMethod<bool>('cancelCurrentStream');
return result ?? false;
}
/// 释放资源
Future<void> dispose() async {
await _channel.invokeMethod('dispose');
_streamCallbacks.clear();
}
}
/// 流式输出回调接口
class StreamCallback {
/// 收到令牌
final void Function(String token) onToken;
/// 完成回调
final void Function() onComplete;
/// 错误回调
final void Function(Exception e) onError;
/// 函数调用回调
final void Function(Map<String, dynamic> functionCall) onFunctionCall;
/// 函数调用结果回调
final void Function(
Map<String, dynamic> functionCall,
Map<String, dynamic> functionCallResult,
) onFunctionCallResult;
/// 构造函数
StreamCallback({
required this.onToken,
required this.onComplete,
required this.onError,
required this.onFunctionCall,
required this.onFunctionCallResult,
});
}

27
local_plugins/open_ai/pubspec.yaml

@ -0,0 +1,27 @@
name: open_ai
description: OpenAI API与MCP集成插件,提供对OpenAI API的访问和MCP工具调用功能
version: 0.1.0
homepage: https://github.com/yunqiinnovation/deep_voice
environment:
sdk: '>=2.18.0 <4.0.0'
flutter: ">=3.3.0"
dependencies:
flutter:
sdk: flutter
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^2.0.0
# 插件平台配置
flutter:
plugin:
platforms:
android:
package: com.yunqiinnovation.open_ai
pluginClass: OpenAiPlugin
ios:
pluginClass: OpenAiPlugin

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

@ -111,43 +111,29 @@ class MCPSubClient(
*/ */
suspend fun connect(): Boolean { suspend fun connect(): Boolean {
try { try {
Log.d(TAG, "开始连接到服务器: $serverUrl") Log.d(TAG, "开始连接服务器: $serverUrl")
// 使用提供的客户端或创建带SSL配置的客户端 // 使用提供的客户端或创建带SSL配置的客户端
val client = customClient ?: createSslTrustAllClient() val client = customClient ?: createSslTrustAllClient()
// 使用自定义传输层,直接传递完整URL // 使用自定义传输层
sseTransport = CustomSseClientTransport( sseTransport = CustomSseClientTransport(
client = client, client = client,
urlString = serverUrl, urlString = serverUrl
requestBuilder = {
// 添加基本请求头
// headers {
// append(HttpHeaders.Accept, "text/event-stream")
// append(HttpHeaders.CacheControl, "no-cache")
// }
}
) )
Log.d(TAG, "创建自定义SSE传输层成功")
// 连接MCP客户端到服务器 // 连接MCP客户端到服务器
mcp.connect(sseTransport!!) mcp.connect(sseTransport!!)
// 获取服务器提供的工具列表 // 获取工具列表并处理
val toolsResult = mcp.listTools() val toolsResult = mcp.listTools()
Log.d(TAG, "获取到工具列表: $toolsResult")
// 处理工具列表
processToolsList(toolsResult) processToolsList(toolsResult)
isConnected = true isConnected = true
Log.d(TAG, "成功连接到MCP服务器,可用工具: ${tools.map { it.tool().get().name() }.joinToString(", ")}") Log.d(TAG, "连接成功,可用工具: ${tools.size}个")
return true return true
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "连接服务器失败: ${e.message}", e) Log.e(TAG, "连接失败: ${e.message}")
// 记录详细的错误堆栈
Log.e(TAG, "详细错误: ${e.stackTraceToString()}")
return false return false
} }
} }
@ -383,7 +369,7 @@ class MCPSubClient(
*/ */
suspend fun callTool(name: String, arguments: Map<String, Any>): JSONObject? { suspend fun callTool(name: String, arguments: Map<String, Any>): JSONObject? {
if (!isConnected) { if (!isConnected) {
Log.e(TAG, "未连接到MCP服务器", null) Log.e(TAG, "未连接到服务器")
return JSONObject().apply { put("context","未连接到MCP服务器") } return JSONObject().apply { put("context","未连接到MCP服务器") }
} }
@ -393,13 +379,7 @@ class MCPSubClient(
} }
return try { return try {
// 直接使用原始参数 val result = mcp.callTool(name = name, arguments = arguments)
Log.d(TAG, "调用工具 $name 使用参数: $arguments")
val result = mcp.callTool(
name = name,
arguments = arguments
)
// 处理工具调用结果 // 处理工具调用结果
val context = result?.content?.joinToString("\n") { content -> val context = result?.content?.joinToString("\n") { content ->
@ -408,13 +388,14 @@ class MCPSubClient(
else -> "" else -> ""
} }
} }
JSONObject().apply { put("name",name);put("meta",result?._meta); put("context",context) } JSONObject().apply {
put("name", name)
put("meta", result?._meta)
put("context", context)
}
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "调用MCP工具失败: ${e.message}", e) Log.e(TAG, "调用工具失败: ${e.message}")
"调用MCP工具失败: ${e.message}" JSONObject().apply { put("context", "调用工具失败: ${e.message}") }
JSONObject().apply { put("context", "调用MCP工具失败: ${e.message}")}
} }
} }
@ -736,20 +717,25 @@ class MCPClient(private val context: Context? = null) : AutoCloseable {
* @return 调用结果 * @return 调用结果
*/ */
suspend fun callTool(name: String, arguments: Map<String, Any>): JSONObject? { suspend fun callTool(name: String, arguments: Map<String, Any>): JSONObject? {
val requestId = "${System.currentTimeMillis()}-${(0..1000).random()}"
Log.d(TAG, "[$requestId] 调用工具: $name")
val toolType = getToolType(name) val toolType = getToolType(name)
return when (toolType) { val result = when (toolType) {
ToolType.LOCAL_FUNCTION -> { ToolType.LOCAL_FUNCTION -> {
callLocalFunction(name, arguments) callLocalFunction(name, arguments)
} }
ToolType.MCP_TOOL -> { ToolType.MCP_TOOL -> {
callMcpTool(name, arguments) callMcpTool(name, arguments, requestId)
} }
null -> { null -> {
Log.e(TAG, "未找到工具: $name") Log.e(TAG, "未找到工具: $name")
JSONObject().apply { put("context","未找到工具: $name") } JSONObject().apply { put("context","未找到工具: $name") }
} }
} }
return result
} }
/** /**
@ -760,13 +746,13 @@ class MCPClient(private val context: Context? = null) : AutoCloseable {
val handler = localFunctions[name] val handler = localFunctions[name]
if (handler != null) { if (handler != null) {
val context = handler.handle(arguments) val context = handler.handle(arguments)
JSONObject().apply { put("context", context) } JSONObject().apply { put("context", context) }
} else { } else {
Log.e(TAG, "未找到本地函数: $name") Log.e(TAG, "未找到本地函数: $name")
JSONObject().apply { put("context","未找到本地函数: $name") } JSONObject().apply { put("context","未找到本地函数: $name") }
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "调用本地函数失败: ${e.message}", e) Log.e(TAG, "调用本地函数失败: ${e.message}")
JSONObject().apply { put("context","调用本地函数失败: ${e.message}") } JSONObject().apply { put("context","调用本地函数失败: ${e.message}") }
} }
} }
@ -774,24 +760,32 @@ class MCPClient(private val context: Context? = null) : AutoCloseable {
/** /**
* 调用MCP工具 * 调用MCP工具
*/ */
private suspend fun callMcpTool(name: String, arguments: Map<String, Any>): JSONObject? { private suspend fun callMcpTool(name: String, arguments: Map<String, Any>, requestId: String): JSONObject? {
if (!isConnected) { if (!isConnected) {
Log.e(TAG, "未连接到MCP服务器", null) Log.e(TAG, "未连接到服务器")
return JSONObject().apply { put("context","未连接到MCP服务器") } return JSONObject().apply { put("context","未连接到MCP服务器") }
} }
// 查找包含此工具的子客户端 // 查找包含此工具的子客户端
val clientEntry = subClients.entries.find { it.value.containsTool(name) } val clientEntry = subClients.entries.find { it.value.containsTool(name) }
if (clientEntry != null) { return if (clientEntry != null) {
val (serverId, client) = clientEntry val (serverId, client) = clientEntry
Log.d(TAG, "通过子客户端 [$serverId] 调用工具: $name") try {
return client.callTool(name, arguments) val startTime = System.currentTimeMillis()
val result = client.callTool(name, arguments)
val duration = System.currentTimeMillis() - startTime
Log.d(TAG, "[$requestId] 工具调用成功: $name (${duration}ms)")
result
} catch (e: Exception) {
Log.e(TAG, "[$requestId] 工具调用失败: $name, ${e.message}")
JSONObject().apply { put("context", "工具调用失败: ${e.message}") }
}
} else {
Log.e(TAG, "[$requestId] 无可用服务器提供工具: $name")
JSONObject().apply { put("context", "无可用的服务器提供工具: $name") }
} }
// 没有找到包含此工具的客户端
Log.e(TAG, "没有子客户端包含工具: $name")
return JSONObject().apply { put("context", "没有可用的服务器提供工具: $name") }
} }
/** /**

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

@ -3,14 +3,13 @@ package com.yunqiinnovation.open_ai_service
import android.content.Context import android.content.Context
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.os.Handler
import android.os.Looper
import android.util.Base64 import android.util.Base64
import android.util.Log import android.util.Log
import com.fasterxml.jackson.core.type.TypeReference import com.fasterxml.jackson.core.type.TypeReference
import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.ObjectMapper
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import okhttp3.* import okhttp3.*
import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.RequestBody.Companion.toRequestBody
@ -21,14 +20,38 @@ import java.io.ByteArrayOutputStream
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import kotlin.coroutines.CoroutineContext
import kotlin.math.min
import kotlin.math.sqrt
/** /**
* OpenAI服务的原生实现 * OpenAI服务的原生实现
*/ */
class OpenAIService(private val context: Context? = null) { class OpenAIService(private val context: Context? = null) : CoroutineScope {
private val TAG = "OpenAIService" private val TAG = "OpenAIService"
/* ---------- 协程与并发 ---------- */
// 创建协程Job,用于生命周期管理
private val job = SupervisorJob()
// 实现CoroutineScope接口
override val coroutineContext: CoroutineContext
get() = Dispatchers.IO + job
// 主线程Handler,用于确保回调在主线程执行
private val mainHandler = Handler(Looper.getMainLooper())
// 将回调安全地切换到主线程
private fun StreamCallback.dispatch(block: StreamCallback.() -> Unit) {
if (Looper.myLooper() == Looper.getMainLooper()) {
block()
} else {
mainHandler.post { block() }
}
}
private var baseUrl = "" private var baseUrl = ""
private val client = OkHttpClient.Builder() private val client = OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS) .connectTimeout(30, TimeUnit.SECONDS)
@ -50,7 +73,7 @@ class OpenAIService(private val context: Context? = null) {
// 添加成员变量以跟踪当前请求 // 添加成员变量以跟踪当前请求
private var currentStreamCall: Call? = null private var currentStreamCall: Call? = null
private var isCanceled = false @Volatile private var isCanceled = false
/** /**
* 构建curl命令用于测试 * 构建curl命令用于测试
@ -92,57 +115,42 @@ class OpenAIService(private val context: Context? = null) {
return null return null
} }
// 读取文件并压缩(如果需要) // 读取文件
val originalBitmap = BitmapFactory.decodeFile(filePath) var bitmap = BitmapFactory.decodeFile(filePath) ?: return null
if (originalBitmap == null) {
Log.e(TAG, "无法解码图片: $filePath")
return null
}
val outputStream = ByteArrayOutputStream()
var quality = 100
var compressedBitmap = originalBitmap
// 检查图片尺寸,限制最大为1024*1024 // 检查图片尺寸,限制最大为1024*1024
val maxDimension = 1024 val maxDimension = 1024
if (originalBitmap.width > maxDimension || originalBitmap.height > maxDimension) { if (bitmap.width > maxDimension || bitmap.height > maxDimension) {
Log.d(TAG, "图片尺寸超过限制,进行缩放: ${originalBitmap.width}x${originalBitmap.height} -> ${maxDimension}x${maxDimension}")
// 计算缩放比例,保持纵横比 // 计算缩放比例,保持纵横比
val widthRatio = maxDimension.toFloat() / originalBitmap.width val ratio = min(maxDimension.toFloat() / bitmap.width, maxDimension.toFloat() / bitmap.height)
val heightRatio = maxDimension.toFloat() / originalBitmap.height
val ratio = Math.min(widthRatio, heightRatio)
val newWidth = (originalBitmap.width * ratio).toInt() val newWidth = (bitmap.width * ratio).toInt()
val newHeight = (originalBitmap.height * ratio).toInt() val newHeight = (bitmap.height * ratio).toInt()
compressedBitmap = Bitmap.createScaledBitmap(originalBitmap, newWidth, newHeight, true) bitmap = Bitmap.createScaledBitmap(bitmap, newWidth, newHeight, true)
Log.d(TAG, "缩放后图片尺寸: ${newWidth}x${newHeight}")
} }
// 压缩图片
val outputStream = ByteArrayOutputStream()
var quality = 100
bitmap.compress(Bitmap.CompressFormat.JPEG, quality, outputStream)
// 如果原始图片太大,继续优化文件大小 // 如果原始图片太大,继续优化文件大小
var fileSize = file.length() / 1024 // 转为KB var fileSize = file.length() / 1024 // 转为KB
if (fileSize > maxSizeKB) { if (fileSize > maxSizeKB) {
val scale = Math.sqrt(maxSizeKB.toDouble() / fileSize) val scale = sqrt(maxSizeKB.toDouble() / fileSize)
val newWidth = (compressedBitmap.width * scale).toInt() val newWidth = (bitmap.width * scale).toInt()
val newHeight = (compressedBitmap.height * scale).toInt() val newHeight = (bitmap.height * scale).toInt()
compressedBitmap = Bitmap.createScaledBitmap(compressedBitmap, newWidth, newHeight, true) bitmap = Bitmap.createScaledBitmap(bitmap, newWidth, newHeight, true)
outputStream.reset()
quality = 85 quality = 85
} bitmap.compress(Bitmap.CompressFormat.JPEG, quality, outputStream)
// 压缩图片
compressedBitmap.compress(Bitmap.CompressFormat.JPEG, quality, outputStream)
val imageBytes = outputStream.toByteArray()
// 检查压缩后大小
if (imageBytes.size / 1024 > maxSizeKB) {
Log.w(TAG, "压缩后图片仍然超出大小限制: ${imageBytes.size / 1024}KB > ${maxSizeKB}KB")
} }
// 转为Base64 // 转为Base64
return Base64.encodeToString(imageBytes, Base64.NO_WRAP) return Base64.encodeToString(outputStream.toByteArray(), Base64.NO_WRAP)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "转换文件到Base64失败: ${e.message}", e) Log.e(TAG, "转换Base64失败: ${e.message}")
return null return null
} }
} }
@ -151,29 +159,26 @@ class OpenAIService(private val context: Context? = null) {
* 创建带图片的用户消息 * 创建带图片的用户消息
*/ */
fun createUserMessageWithImage(text: String, imageBase64: String): JSONObject { fun createUserMessageWithImage(text: String, imageBase64: String): JSONObject {
// 创建包含文本和图片的内容数组 return JSONObject().apply {
val contentArray = JSONArray().apply { put("role", "user")
// 添加文本部分 put("content", JSONArray().apply {
if (text.isNotEmpty()) { // 添加文本部分
if (text.isNotEmpty()) {
put(JSONObject().apply {
put("type", "text")
put("text", text)
})
}
// 添加图片部分
put(JSONObject().apply { put(JSONObject().apply {
put("type", "text") put("type", "image_url")
put("text", text) put("image_url", JSONObject().apply {
}) put("url", "data:image/jpeg;base64,$imageBase64")
} })
// 添加图片部分
put(JSONObject().apply {
put("type", "image_url")
put("image_url", JSONObject().apply {
put("url", "data:image/jpeg;base64,$imageBase64")
}) })
}) })
} }
return JSONObject().apply {
put("role", "user")
put("content", contentArray)
}
} }
/** /**
@ -253,13 +258,10 @@ class OpenAIService(private val context: Context? = null) {
mcpClient = MCPClient(context) mcpClient = MCPClient(context)
// 在后台线程中初始化MCP客户端 // 直接使用类的CoroutineScope启动协程
val scope = CoroutineScope(Dispatchers.IO) launch {
var result = false
scope.launch {
try { try {
result = mcpClient?.connectToSSE(mcpServer) ?: false val result = mcpClient?.connectToSSE(mcpServer) ?: false
isMcpInitialized = result isMcpInitialized = result
Log.d(TAG, "MCP客户端初始化${if (result) "成功" else "失败"}") Log.d(TAG, "MCP客户端初始化${if (result) "成功" else "失败"}")
} catch (e: Exception) { } catch (e: Exception) {
@ -271,8 +273,6 @@ class OpenAIService(private val context: Context? = null) {
return true // 立即返回,实际连接在后台进行 return true // 立即返回,实际连接在后台进行
} }
/** /**
* 处理MCP工具调用 * 处理MCP工具调用
* *
@ -343,7 +343,7 @@ class OpenAIService(private val context: Context? = null) {
// 调用工具(统一通过MCPClient处理) // 调用工具(统一通过MCPClient处理)
val result = mcpClient?.callTool(name, arguments) val result = mcpClient?.callTool(name, arguments)
Log.d(TAG, "自动处理MCP工具调用结果: ${result?.toString()}")
// 检查是否已取消 // 检查是否已取消
if (isCanceled) { if (isCanceled) {
Log.d(TAG, "工具调用已被取消,不处理结果") Log.d(TAG, "工具调用已被取消,不处理结果")
@ -352,14 +352,10 @@ class OpenAIService(private val context: Context? = null) {
// 回调结果 // 回调结果
val resultObj = result ?: JSONObject().apply { put("context", "工具调用失败") } val resultObj = result ?: JSONObject().apply { put("context", "工具调用失败") }
callback.onFunctionCallResult(functionCall, resultObj) callback.dispatch { onFunctionCallResult(functionCall, resultObj) }
// 发送结果到OpenAI继续对话 // 发送结果到OpenAI继续对话
val resultContent = if (resultObj.has("context") && resultObj.getString("context").isNotEmpty()) { val resultContent = resultObj.optString("context", "工具调用失败")
resultObj.getString("context")
} else {
"工具调用失败"
}
sendFunctionCallResult( sendFunctionCallResult(
messages = messages, messages = messages,
functionCall = functionCall, functionCall = functionCall,
@ -484,7 +480,7 @@ class OpenAIService(private val context: Context? = null) {
*/ */
fun sendMessageStream(messages: JSONArray, callback: StreamCallback) { fun sendMessageStream(messages: JSONArray, callback: StreamCallback) {
if (!isInitialized || apiKey.isEmpty()) { if (!isInitialized || apiKey.isEmpty()) {
callback.onError(OpenAIException("OpenAI服务未初始化")) callback.dispatch { onError(OpenAIException("OpenAI服务未初始化")) }
return return
} }
@ -496,10 +492,13 @@ class OpenAIService(private val context: Context? = null) {
if (messages.length() > 0) { if (messages.length() > 0) {
val lastMessage = messages.getJSONObject(messages.length() - 1) val lastMessage = messages.getJSONObject(messages.length() - 1)
if (lastMessage.has("content")) { if (lastMessage.has("content")) {
val content = lastMessage.getString("content") val content = lastMessage.optString("content", "")
currentModel = visionModel if (content.contains("image_url")) {
currentModel = visionModel
}
} }
} }
val requestBody = JSONObject().apply { val requestBody = JSONObject().apply {
put("model", currentModel) put("model", currentModel)
put("messages", messages) put("messages", messages)
@ -535,10 +534,6 @@ class OpenAIService(private val context: Context? = null) {
.post(requestBody.toString().toRequestBody(mediaType)) .post(requestBody.toString().toRequestBody(mediaType))
.build() .build()
// 输出用于测试的curl命令
// val curlCommand = buildCurlCommand(request, requestBody.toString())
// Log.d(TAG, "curl command: $curlCommand")
val call = client.newCall(request) val call = client.newCall(request)
// 保存当前Call对象以便后续取消 // 保存当前Call对象以便后续取消
currentStreamCall = call currentStreamCall = call
@ -549,7 +544,7 @@ class OpenAIService(private val context: Context? = null) {
if (isCanceled) { if (isCanceled) {
return return
} }
callback.onError(OpenAIException(e.message ?: "请求失败")) callback.dispatch { onError(OpenAIException(e.message ?: "请求失败")) }
} }
override fun onResponse(call: Call, response: Response) { override fun onResponse(call: Call, response: Response) {
@ -560,7 +555,7 @@ class OpenAIService(private val context: Context? = null) {
} }
if (!response.isSuccessful) { if (!response.isSuccessful) {
callback.onError(OpenAIException("API调用失败: ${response.code}")) callback.dispatch { onError(OpenAIException("API调用失败: ${response.code}")) }
return return
} }
@ -570,7 +565,7 @@ class OpenAIService(private val context: Context? = null) {
try { try {
// 用于存储函数调用的各个部分 // 用于存储函数调用的各个部分
val finalToolCalls = mutableMapOf<Int, ToolCallInfo>() val toolCalls = mutableMapOf<Int, ToolCallInfo>()
// 逐块读取数据,而不是一次性预取 // 逐块读取数据,而不是一次性预取
val BUFFER_SIZE = 8192L // 8KB的块大小 val BUFFER_SIZE = 8192L // 8KB的块大小
@ -590,84 +585,16 @@ class OpenAIService(private val context: Context? = null) {
// 处理[DONE]消息 // 处理[DONE]消息
if (data == "[DONE]" || data == "[\"DONE\"]") { if (data == "[DONE]" || data == "[\"DONE\"]") {
val hasToolCalls = processToolCalls(finalToolCalls, callback, messages) val hasToolCalls = processToolCalls(toolCalls, callback, messages)
// 如果没有工具调用,这是对话的真正结束 // 如果没有工具调用,这是对话的真正结束
if (!hasToolCalls) { if (!hasToolCalls) {
callback.onComplete() callback.dispatch { onComplete() }
} }
return return
} }
try { try {
val jsonData = JSONObject(data) handleStreamJson(JSONObject(data), toolCalls, callback, messages)
// 处理消息内容
if (jsonData.has("choices")) {
val choices = jsonData.getJSONArray("choices")
if (choices.length() > 0) {
val choice = choices.getJSONObject(0)
// 处理delta内容
if (choice.has("delta")) {
val delta = choice.getJSONObject("delta")
// 处理普通文本内容
if (delta.has("content")) {
val content = delta.getString("content")
if (!isCanceled) {
callback.onToken(content)
}
}
// 处理工具调用
if (delta.has("tool_calls")) {
val toolCalls = delta.getJSONArray("tool_calls")
for (i in 0 until toolCalls.length()) {
val toolCall = toolCalls.getJSONObject(i)
val index = toolCall.getInt("index")
// 创建或获取现有的工具调用信息
val toolCallInfo = finalToolCalls.getOrPut(index) { ToolCallInfo() }
// 更新ID
if (toolCall.has("id")) {
toolCallInfo.id = toolCall.getString("id")
}
// 更新函数信息
if (toolCall.has("function")) {
val function = toolCall.getJSONObject("function")
if (function.has("name")) {
toolCallInfo.name = function.getString("name")
}
if (function.has("arguments")) {
toolCallInfo.arguments += function.getString("arguments")
}
}
}
}
}
// 检查是否有表示完成的标志
if (choice.has("finish_reason")) {
val finishReason = choice.getString("finish_reason")
if (finishReason == "stop" || finishReason == "length") {
// 正常完成,没有工具调用
callback.onComplete()
return
} else if (finishReason == "tool_calls") {
// 处理工具调用
val hasToolCalls = processToolCalls(finalToolCalls, callback, messages)
if (!hasToolCalls) {
callback.onComplete()
}
return
}
}
}
}
} catch (e: Exception) { } catch (e: Exception) {
// 忽略解析错误,但记录日志 // 忽略解析错误,但记录日志
Log.e(TAG, "解析JSON出错: ${e.message}", e) Log.e(TAG, "解析JSON出错: ${e.message}", e)
@ -682,16 +609,16 @@ class OpenAIService(private val context: Context? = null) {
} }
// 如果没有明确的完成信号但数据流结束了,处理最后可能的工具调用 // 如果没有明确的完成信号但数据流结束了,处理最后可能的工具调用
val hasToolCalls = processToolCalls(finalToolCalls, callback, messages) val hasToolCalls = processToolCalls(toolCalls, callback, messages)
if (!hasToolCalls) { if (!hasToolCalls) {
callback.onComplete() callback.dispatch { onComplete() }
} }
} catch (e: Exception) { } catch (e: Exception) {
// 如果是由于取消导致的异常,不报告错误 // 如果是由于取消导致的异常,不报告错误
if (isCanceled) { if (isCanceled) {
return return
} }
callback.onError(OpenAIException("处理响应流时出错: ${e.message}")) callback.dispatch { onError(OpenAIException("处理响应流时出错: ${e.message}")) }
} finally { } finally {
responseBody.close() responseBody.close()
} }
@ -699,6 +626,145 @@ class OpenAIService(private val context: Context? = null) {
}) })
} }
/**
* 处理流式JSON数据
*/
private fun handleStreamJson(
json: JSONObject,
toolCalls: MutableMap<Int, ToolCallInfo>,
callback: StreamCallback,
messages: JSONArray
) {
if (json.has("choices") && json.getJSONArray("choices").length() > 0) {
val choice = json.getJSONArray("choices").getJSONObject(0)
// 处理delta内容
if (choice.has("delta")) {
val delta = choice.getJSONObject("delta")
// 处理普通文本内容
if (delta.has("content")) {
val content = delta.getString("content")
if (!isCanceled) {
callback.dispatch { onToken(content) }
}
}
// 收集工具调用信息
if (delta.has("tool_calls")) {
val toolCallsArray = delta.getJSONArray("tool_calls")
for (i in 0 until toolCallsArray.length()) {
val toolCall = toolCallsArray.getJSONObject(i)
val index = toolCall.getInt("index")
// 创建或获取现有的工具调用信息
val toolCallInfo = toolCalls.getOrPut(index) { ToolCallInfo() }
// 更新ID
if (toolCall.has("id")) {
toolCallInfo.id = toolCall.getString("id")
}
// 更新函数信息
if (toolCall.has("function")) {
val function = toolCall.getJSONObject("function")
if (function.has("name")) {
toolCallInfo.name = function.getString("name")
}
if (function.has("arguments")) {
toolCallInfo.arguments += function.getString("arguments")
}
}
}
}
}
}
}
/**
* 处理工具调用
*/
private fun processToolCalls(
toolCalls: Map<Int, ToolCallInfo>,
callback: StreamCallback,
messages: JSONArray? = null
): Boolean {
if (toolCalls.isEmpty()) return false
// 只处理第一个工具调用
val firstToolCall = toolCalls.entries.firstOrNull()?.value ?: return false
if (firstToolCall.isValid()) {
// 创建函数调用JSON对象
val functionCall = JSONObject().apply {
put("name", firstToolCall.name)
put("arguments", firstToolCall.arguments)
put("id", firstToolCall.id)
}
Log.d(TAG, "工具调用: id=${firstToolCall.id}, name=${firstToolCall.name}")
// 通知上层工具调用事件
callback.dispatch { onFunctionCall(functionCall) }
// 在协程中处理工具调用
if (messages != null) {
launch {
try {
if (!isCanceled) {
// 调用工具
val result = try {
val args = mcpClient?.parseJsonArguments(firstToolCall.arguments) ?: mapOf()
mcpClient?.callTool(firstToolCall.name, args)
} catch (e: Exception) {
Log.e(TAG, "工具调用错误: ${e.message}")
JSONObject().put("context", "工具调用失败: ${e.message}")
}
if (!isCanceled) {
// 处理结果
val resultObj = result ?: JSONObject().put("context", "工具调用失败")
Log.d(TAG, "工具调用完成: id=${firstToolCall.id}")
callback.dispatch { onFunctionCallResult(functionCall, resultObj) }
val context = resultObj.optString("context", "工具调用失败").let {
if (it.isNullOrEmpty()) "工具调用失败" else it
}
// 将结果发送回OpenAI继续对话
sendFunctionCallResult(
messages = messages,
functionCall = functionCall,
functionResult = context,
callback = callback
)
}
}
} catch (e: Exception) {
if (!isCanceled) {
Log.e(TAG, "处理工具调用异常: ${e.message}")
try {
val errorMessage = "工具调用处理失败: ${e.message}"
sendFunctionCallResult(
messages = messages,
functionCall = functionCall,
functionResult = errorMessage,
callback = callback
)
} catch (e2: Exception) {
Log.e(TAG, "发送工具调用结果失败: ${e2.message}")
}
}
}
}
}
return true
}
return false
}
/** /**
* 发送函数调用结果 * 发送函数调用结果
*/ */
@ -755,63 +821,45 @@ class OpenAIService(private val context: Context? = null) {
} catch (e: Exception) { } catch (e: Exception) {
if (!isCanceled) { if (!isCanceled) {
Log.e(TAG, "发送函数调用结果失败: ${e.message}", e) Log.e(TAG, "发送函数调用结果失败: ${e.message}", e)
callback.onError(OpenAIException("发送函数调用结果失败: ${e.message}")) callback.dispatch { onError(OpenAIException("发送函数调用结果失败: ${e.message}")) }
} }
} }
} }
/** /**
* 处理工具调用结果 * 取消当前流式请求
*
* @return 是否成功取消
*/ */
private fun processToolCalls(toolCalls: Map<Int, ToolCallInfo>, callback: StreamCallback, messages: JSONArray? = null): Boolean { fun cancelCurrentStream(): Boolean {
if (toolCalls.isEmpty()) return false try {
// 标记已取消
// 只处理第一个工具调用 isCanceled = true
val firstToolCall = toolCalls.entries.firstOrNull()?.value ?: return false
if (firstToolCall.isValid()) {
// 创建函数调用JSON对象
val functionCall = JSONObject().apply {
put("name", firstToolCall.name)
put("arguments", firstToolCall.arguments)
put("id", firstToolCall.id)
}
Log.d(TAG, "工具调用: $functionCall")
// 通知上层工具调用事件
callback.onFunctionCall(functionCall)
// 在协程中处理工具调用 // 取消当前Call对象
if (messages != null) { val call = currentStreamCall
kotlinx.coroutines.GlobalScope.launch { if (call != null && !call.isCanceled()) {
try { call.cancel()
if (!isCanceled) { Log.d(TAG, "已取消当前流式请求")
autoHandleMcpToolCall(functionCall, messages, callback) return true
} else {
Log.d(TAG, "请求已取消,不处理工具调用")
}
} catch (e: Exception) {
if (!isCanceled) {
Log.e(TAG, "处理工具调用时发生异常: ${e.message}", e)
// 处理异常的情况,向回调通知错误
try {
val errorMessage = "工具调用处理失败: ${e.message}"
sendFunctionCallResult(
messages = messages,
functionCall = functionCall,
functionResult = errorMessage,
callback = callback
)
} catch (e2: Exception) {
Log.e(TAG, "发送工具调用错误结果失败: ${e2.message}", e2)
}
}
}
}
} }
return true return false
} catch (e: Exception) {
Log.e(TAG, "取消流式请求失败: ${e.message}", e)
return false
} finally {
// 清除当前Call引用
currentStreamCall = null
} }
return false }
/**
* 取消所有操作并释放资源
* 供AgentService.dispose()调用
*/
fun cancelAll() {
cancelCurrentStream()
job.cancel() // 取消所有协程
} }
/** /**
@ -837,33 +885,6 @@ class OpenAIService(private val context: Context? = null) {
fun onFunctionCall(functionCall: JSONObject) fun onFunctionCall(functionCall: JSONObject)
fun onFunctionCallResult(functionCall: JSONObject, functionCallResult: JSONObject) fun onFunctionCallResult(functionCall: JSONObject, functionCallResult: JSONObject)
} }
/**
* 取消当前流式请求
*
* @return 是否成功取消
*/
fun cancelCurrentStream(): Boolean {
try {
// 标记已取消
isCanceled = true
// 取消当前Call对象
val call = currentStreamCall
if (call != null && !call.isCanceled()) {
call.cancel()
Log.d(TAG, "已取消当前流式请求")
return true
}
return false
} catch (e: Exception) {
Log.e(TAG, "取消流式请求失败: ${e.message}", e)
return false
} finally {
// 清除当前Call引用
currentStreamCall = null
}
}
} }
/** /**

BIN
local_plugins/opus/.gradle/8.10/fileHashes/fileHashes.lock

Binary file not shown.

BIN
local_plugins/opus/.gradle/buildOutputCleanup/buildOutputCleanup.lock

Binary file not shown.

2
local_plugins/opus/.gradle/buildOutputCleanup/cache.properties

@ -1,2 +1,2 @@
#Sat May 10 17:59:51 IST 2025 #Sun May 11 13:39:53 IST 2025
gradle.version=8.10 gradle.version=8.10

163
macos/Podfile.lock

@ -1,21 +1,110 @@
PODS: PODS:
- app_settings (5.1.1):
- FlutterMacOS
- AppAuth (1.7.6):
- AppAuth/Core (= 1.7.6)
- AppAuth/ExternalUserAgent (= 1.7.6)
- AppAuth/Core (1.7.6)
- AppAuth/ExternalUserAgent (1.7.6):
- AppAuth/Core
- audio_session (0.0.1): - audio_session (0.0.1):
- FlutterMacOS - FlutterMacOS
- device_info_plus (0.0.1): - device_info_plus (0.0.1):
- FlutterMacOS - FlutterMacOS
- facebook_auth_desktop (0.0.3):
- FlutterMacOS
- file_picker (0.0.1):
- FlutterMacOS
- file_selector_macos (0.0.1): - file_selector_macos (0.0.1):
- FlutterMacOS - FlutterMacOS
- Firebase/Auth (11.10.0):
- Firebase/CoreOnly
- FirebaseAuth (~> 11.10.0)
- Firebase/CoreOnly (11.10.0):
- FirebaseCore (~> 11.10.0)
- firebase_auth (5.5.2):
- Firebase/Auth (~> 11.10.0)
- Firebase/CoreOnly (~> 11.10.0)
- firebase_core
- FlutterMacOS
- firebase_core (3.13.0):
- Firebase/CoreOnly (~> 11.10.0)
- FlutterMacOS
- FirebaseAppCheckInterop (11.12.0)
- FirebaseAuth (11.10.0):
- FirebaseAppCheckInterop (~> 11.0)
- FirebaseAuthInterop (~> 11.0)
- FirebaseCore (~> 11.10.0)
- FirebaseCoreExtension (~> 11.10.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
- GoogleUtilities/Environment (~> 8.0)
- GTMSessionFetcher/Core (< 5.0, >= 3.4)
- RecaptchaInterop (~> 101.0)
- FirebaseAuthInterop (11.12.0)
- FirebaseCore (11.10.0):
- FirebaseCoreInternal (~> 11.10.0)
- GoogleUtilities/Environment (~> 8.0)
- GoogleUtilities/Logger (~> 8.0)
- FirebaseCoreExtension (11.10.0):
- FirebaseCore (~> 11.10.0)
- FirebaseCoreInternal (11.10.0):
- "GoogleUtilities/NSData+zlib (~> 8.0)"
- flutter_blue_plus_darwin (0.0.2): - flutter_blue_plus_darwin (0.0.2):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
- flutter_image_compress_macos (1.0.0):
- FlutterMacOS
- flutter_local_notifications (0.0.1): - flutter_local_notifications (0.0.1):
- FlutterMacOS - FlutterMacOS
- flutter_secure_storage_macos (6.1.3):
- FlutterMacOS
- flutter_tts (0.0.1): - flutter_tts (0.0.1):
- FlutterMacOS - FlutterMacOS
- FlutterMacOS (1.0.0) - FlutterMacOS (1.0.0)
- google_sign_in_ios (0.0.1):
- AppAuth (>= 1.7.4)
- Flutter
- FlutterMacOS
- GoogleSignIn (~> 7.1)
- GTMSessionFetcher (>= 3.4.0)
- GoogleSignIn (7.1.0):
- AppAuth (< 2.0, >= 1.7.3)
- GTMAppAuth (< 5.0, >= 4.1.1)
- GTMSessionFetcher/Core (~> 3.3)
- GoogleUtilities/AppDelegateSwizzler (8.1.0):
- GoogleUtilities/Environment
- GoogleUtilities/Logger
- GoogleUtilities/Network
- GoogleUtilities/Privacy
- GoogleUtilities/Environment (8.1.0):
- GoogleUtilities/Privacy
- GoogleUtilities/Logger (8.1.0):
- GoogleUtilities/Environment
- GoogleUtilities/Privacy
- GoogleUtilities/Network (8.1.0):
- GoogleUtilities/Logger
- "GoogleUtilities/NSData+zlib"
- GoogleUtilities/Privacy
- GoogleUtilities/Reachability
- "GoogleUtilities/NSData+zlib (8.1.0)":
- GoogleUtilities/Privacy
- GoogleUtilities/Privacy (8.1.0)
- GoogleUtilities/Reachability (8.1.0):
- GoogleUtilities/Logger
- GoogleUtilities/Privacy
- GTMAppAuth (4.1.1):
- AppAuth/Core (~> 1.7)
- GTMSessionFetcher/Core (< 4.0, >= 3.3)
- GTMSessionFetcher (3.5.0):
- GTMSessionFetcher/Full (= 3.5.0)
- GTMSessionFetcher/Core (3.5.0)
- GTMSessionFetcher/Full (3.5.0):
- GTMSessionFetcher/Core
- just_audio (0.0.1): - just_audio (0.0.1):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
- package_info_plus (0.0.1):
- FlutterMacOS
- path_provider_foundation (0.0.1): - path_provider_foundation (0.0.1):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
@ -27,46 +116,93 @@ PODS:
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
- Try - Try
- sqflite_darwin (0.0.4):
- Flutter
- FlutterMacOS
- Try (2.1.1) - Try (2.1.1)
- webview_flutter_wkwebview (0.0.1):
- Flutter
- FlutterMacOS
- window_size (0.0.2): - window_size (0.0.2):
- FlutterMacOS - FlutterMacOS
DEPENDENCIES: DEPENDENCIES:
- app_settings (from `Flutter/ephemeral/.symlinks/plugins/app_settings/macos`)
- audio_session (from `Flutter/ephemeral/.symlinks/plugins/audio_session/macos`) - audio_session (from `Flutter/ephemeral/.symlinks/plugins/audio_session/macos`)
- device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`) - device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`)
- facebook_auth_desktop (from `Flutter/ephemeral/.symlinks/plugins/facebook_auth_desktop/macos`)
- file_picker (from `Flutter/ephemeral/.symlinks/plugins/file_picker/macos`)
- file_selector_macos (from `Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos`) - file_selector_macos (from `Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos`)
- firebase_auth (from `Flutter/ephemeral/.symlinks/plugins/firebase_auth/macos`)
- firebase_core (from `Flutter/ephemeral/.symlinks/plugins/firebase_core/macos`)
- flutter_blue_plus_darwin (from `Flutter/ephemeral/.symlinks/plugins/flutter_blue_plus_darwin/darwin`) - flutter_blue_plus_darwin (from `Flutter/ephemeral/.symlinks/plugins/flutter_blue_plus_darwin/darwin`)
- flutter_image_compress_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_image_compress_macos/macos`)
- flutter_local_notifications (from `Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos`) - flutter_local_notifications (from `Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos`)
- flutter_secure_storage_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos`)
- flutter_tts (from `Flutter/ephemeral/.symlinks/plugins/flutter_tts/macos`) - flutter_tts (from `Flutter/ephemeral/.symlinks/plugins/flutter_tts/macos`)
- FlutterMacOS (from `Flutter/ephemeral`) - FlutterMacOS (from `Flutter/ephemeral`)
- google_sign_in_ios (from `Flutter/ephemeral/.symlinks/plugins/google_sign_in_ios/darwin`)
- just_audio (from `Flutter/ephemeral/.symlinks/plugins/just_audio/darwin`) - just_audio (from `Flutter/ephemeral/.symlinks/plugins/just_audio/darwin`)
- package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`)
- path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`)
- record_darwin (from `Flutter/ephemeral/.symlinks/plugins/record_darwin/macos`) - record_darwin (from `Flutter/ephemeral/.symlinks/plugins/record_darwin/macos`)
- share_plus (from `Flutter/ephemeral/.symlinks/plugins/share_plus/macos`) - share_plus (from `Flutter/ephemeral/.symlinks/plugins/share_plus/macos`)
- speech_to_text (from `Flutter/ephemeral/.symlinks/plugins/speech_to_text/darwin`) - speech_to_text (from `Flutter/ephemeral/.symlinks/plugins/speech_to_text/darwin`)
- sqflite_darwin (from `Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin`)
- webview_flutter_wkwebview (from `Flutter/ephemeral/.symlinks/plugins/webview_flutter_wkwebview/darwin`)
- window_size (from `Flutter/ephemeral/.symlinks/plugins/window_size/macos`) - window_size (from `Flutter/ephemeral/.symlinks/plugins/window_size/macos`)
SPEC REPOS: SPEC REPOS:
trunk: trunk:
- AppAuth
- Firebase
- FirebaseAppCheckInterop
- FirebaseAuth
- FirebaseAuthInterop
- FirebaseCore
- FirebaseCoreExtension
- FirebaseCoreInternal
- GoogleSignIn
- GoogleUtilities
- GTMAppAuth
- GTMSessionFetcher
- Try - Try
EXTERNAL SOURCES: EXTERNAL SOURCES:
app_settings:
:path: Flutter/ephemeral/.symlinks/plugins/app_settings/macos
audio_session: audio_session:
:path: Flutter/ephemeral/.symlinks/plugins/audio_session/macos :path: Flutter/ephemeral/.symlinks/plugins/audio_session/macos
device_info_plus: device_info_plus:
:path: Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos :path: Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos
facebook_auth_desktop:
:path: Flutter/ephemeral/.symlinks/plugins/facebook_auth_desktop/macos
file_picker:
:path: Flutter/ephemeral/.symlinks/plugins/file_picker/macos
file_selector_macos: file_selector_macos:
:path: Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos :path: Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos
firebase_auth:
:path: Flutter/ephemeral/.symlinks/plugins/firebase_auth/macos
firebase_core:
:path: Flutter/ephemeral/.symlinks/plugins/firebase_core/macos
flutter_blue_plus_darwin: flutter_blue_plus_darwin:
:path: Flutter/ephemeral/.symlinks/plugins/flutter_blue_plus_darwin/darwin :path: Flutter/ephemeral/.symlinks/plugins/flutter_blue_plus_darwin/darwin
flutter_image_compress_macos:
:path: Flutter/ephemeral/.symlinks/plugins/flutter_image_compress_macos/macos
flutter_local_notifications: flutter_local_notifications:
:path: Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos :path: Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos
flutter_secure_storage_macos:
:path: Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos
flutter_tts: flutter_tts:
:path: Flutter/ephemeral/.symlinks/plugins/flutter_tts/macos :path: Flutter/ephemeral/.symlinks/plugins/flutter_tts/macos
FlutterMacOS: FlutterMacOS:
:path: Flutter/ephemeral :path: Flutter/ephemeral
google_sign_in_ios:
:path: Flutter/ephemeral/.symlinks/plugins/google_sign_in_ios/darwin
just_audio: just_audio:
:path: Flutter/ephemeral/.symlinks/plugins/just_audio/darwin :path: Flutter/ephemeral/.symlinks/plugins/just_audio/darwin
package_info_plus:
:path: Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos
path_provider_foundation: path_provider_foundation:
:path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin :path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin
record_darwin: record_darwin:
@ -75,23 +211,50 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/share_plus/macos :path: Flutter/ephemeral/.symlinks/plugins/share_plus/macos
speech_to_text: speech_to_text:
:path: Flutter/ephemeral/.symlinks/plugins/speech_to_text/darwin :path: Flutter/ephemeral/.symlinks/plugins/speech_to_text/darwin
sqflite_darwin:
:path: Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin
webview_flutter_wkwebview:
:path: Flutter/ephemeral/.symlinks/plugins/webview_flutter_wkwebview/darwin
window_size: window_size:
:path: Flutter/ephemeral/.symlinks/plugins/window_size/macos :path: Flutter/ephemeral/.symlinks/plugins/window_size/macos
SPEC CHECKSUMS: SPEC CHECKSUMS:
app_settings: cd21e176b56f8172043640ade81322a98896bff4
AppAuth: d4f13a8fe0baf391b2108511793e4b479691fb73
audio_session: eaca2512cf2b39212d724f35d11f46180ad3a33e audio_session: eaca2512cf2b39212d724f35d11f46180ad3a33e
device_info_plus: a56e6e74dbbd2bb92f2da12c64ddd4f67a749041 device_info_plus: a56e6e74dbbd2bb92f2da12c64ddd4f67a749041
facebook_auth_desktop: bf89ccf460c903de45e86fa0c7878b8b62db65fb
file_picker: 7584aae6fa07a041af2b36a2655122d42f578c1a
file_selector_macos: 6280b52b459ae6c590af5d78fc35c7267a3c4b31 file_selector_macos: 6280b52b459ae6c590af5d78fc35c7267a3c4b31
Firebase: 1fe1c0a7d9aaea32efe01fbea5f0ebd8d70e53a2
firebase_auth: 22945027646e71960caae1b9f2b2498d7a52a4c7
firebase_core: efd50ad8177dc489af1b9163a560359cf1b30597
FirebaseAppCheckInterop: 73b173e5ec45192e2d522ad43f526a82ad10b852
FirebaseAuth: c4146bdfdc87329f9962babd24dae89373f49a32
FirebaseAuthInterop: b583210c039a60ed3f1e48865e1f3da44a796595
FirebaseCore: 8344daef5e2661eb004b177488d6f9f0f24251b7
FirebaseCoreExtension: 6f357679327f3614e995dc7cf3f2d600bdc774ac
FirebaseCoreInternal: ef4505d2afb1d0ebbc33162cb3795382904b5679
flutter_blue_plus_darwin: 09444a26fb6bdef523e55b68fc1c59af5a877ea6 flutter_blue_plus_darwin: 09444a26fb6bdef523e55b68fc1c59af5a877ea6
flutter_image_compress_macos: e68daf54bb4bf2144c580fd4d151c949cbf492f0
flutter_local_notifications: 13862b132e32eb858dea558a86d45d08daeacfe7 flutter_local_notifications: 13862b132e32eb858dea558a86d45d08daeacfe7
flutter_secure_storage_macos: 7f45e30f838cf2659862a4e4e3ee1c347c2b3b54
flutter_tts: ae915565cc6948444b513acc8ee021993281e027 flutter_tts: ae915565cc6948444b513acc8ee021993281e027
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
google_sign_in_ios: 19297361f2c51d7d8ac0201b866ef1fa5d1f94a8
GoogleSignIn: d4281ab6cf21542b1cfaff85c191f230b399d2db
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
GTMAppAuth: f69bd07d68cd3b766125f7e072c45d7340dea0de
GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6
just_audio: 4e391f57b79cad2b0674030a00453ca5ce817eed just_audio: 4e391f57b79cad2b0674030a00453ca5ce817eed
package_info_plus: f0052d280d17aa382b932f399edf32507174e870
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
record_darwin: 30509266ae213af8afdb09a8ae7467cb64c1377e record_darwin: 30509266ae213af8afdb09a8ae7467cb64c1377e
share_plus: 510bf0af1a42cd602274b4629920c9649c52f4cc share_plus: 510bf0af1a42cd602274b4629920c9649c52f4cc
speech_to_text: 9dc43a5df3cbc2813f8c7cc9bd0fbf94268ed7ac speech_to_text: 9dc43a5df3cbc2813f8c7cc9bd0fbf94268ed7ac
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
Try: 5ef669ae832617b3cee58cb2c6f99fb767a4ff96 Try: 5ef669ae832617b3cee58cb2c6f99fb767a4ff96
webview_flutter_wkwebview: 1821ceac936eba6f7984d89a9f3bcb4dea99ebb2
window_size: 4bd15034e6e3d0720fd77928a7c42e5492cfece9 window_size: 4bd15034e6e3d0720fd77928a7c42e5492cfece9
PODFILE CHECKSUM: 8c4442624fc136b0afb378a2502958e01c05d073 PODFILE CHECKSUM: 8c4442624fc136b0afb378a2502958e01c05d073

18
macos/Runner.xcodeproj/project.pbxproj

@ -241,6 +241,7 @@
33CC110E2044A8840003C045 /* Bundle Framework */, 33CC110E2044A8840003C045 /* Bundle Framework */,
3399D490228B24CF009A79C7 /* ShellScript */, 3399D490228B24CF009A79C7 /* ShellScript */,
EEBEF79BCB6B649D2C3D134C /* [CP] Embed Pods Frameworks */, EEBEF79BCB6B649D2C3D134C /* [CP] Embed Pods Frameworks */,
5D85B8FB5391E947F8A1E18F /* [CP] Copy Pods Resources */,
); );
buildRules = ( buildRules = (
); );
@ -405,6 +406,23 @@
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0; showEnvVarsInLog = 0;
}; };
5D85B8FB5391E947F8A1E18F /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Copy Pods Resources";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
showEnvVarsInLog = 0;
};
EEBEF79BCB6B649D2C3D134C /* [CP] Embed Pods Frameworks */ = { EEBEF79BCB6B649D2C3D134C /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase; isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;

Loading…
Cancel
Save