Browse Source

Merge commit '372a5e60dd32a9d4f9eabe0c3a37caf064197750' into lxm

newdev_shunjiawei
lxm 1 year ago
parent
commit
f6e14741e7
  1. 9
      lib/modules/agent/controllers/agent_controller.dart
  2. 201
      lib/modules/agent/views/message_bubble.dart
  3. 8
      lib/modules/ota_upgrade/controllers/ota_upgrade_controller.dart
  4. 34
      lib/modules/pairing/controllers/pairing_controller.dart
  5. 225
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt
  6. 4
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleAgent.kt
  7. BIN
      local_plugins/agent_service/android/src/main/res/raw/calling.mp3
  8. BIN
      local_plugins/agent_service/android/src/main/res/raw/stop.mp3
  9. BIN
      local_plugins/agent_service/android/src/main/res/raw/stop3.mp3
  10. 1
      local_plugins/azure_speech/android/build.gradle.kts
  11. 58
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt
  12. 1
      local_plugins/ble_service/android/build.gradle.kts
  13. 65
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleCompanionManager.kt
  14. 8
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt
  15. 4
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleServicePlugin.kt
  16. 135
      local_plugins/open_ai/README.md
  17. 71
      local_plugins/open_ai/android/build.gradle.kts
  18. 258
      local_plugins/open_ai/android/src/main/kotlin/com/yunqiinnovation/open_ai/MCPClient.kt
  19. 988
      local_plugins/open_ai/android/src/main/kotlin/com/yunqiinnovation/open_ai/OpenAIService.kt
  20. 149
      local_plugins/open_ai/android/src/main/kotlin/com/yunqiinnovation/open_ai/OpenAiPlugin.kt
  21. 184
      local_plugins/open_ai/lib/open_ai.dart
  22. 27
      local_plugins/open_ai/pubspec.yaml
  23. 88
      local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/MCPClient.kt
  24. 489
      local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/OpenAIService.kt
  25. BIN
      local_plugins/opus/.gradle/8.10/fileHashes/fileHashes.lock
  26. BIN
      local_plugins/opus/.gradle/buildOutputCleanup/buildOutputCleanup.lock
  27. 2
      local_plugins/opus/.gradle/buildOutputCleanup/cache.properties
  28. 163
      macos/Podfile.lock
  29. 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:
final functionName = event.data['name'] ?? '';
final arguments = event.data['arguments'] ?? '';
logger.i('Fultter 开始调用 mcp: $functionName, $arguments');
// logger.i('Fultter 开始调用 mcp: $functionName, $arguments');
break;
case AgentServiceEventType.functionCallResult:
@ -352,10 +352,10 @@ class AgentController extends GetxController {
if (result.isNotEmpty) {
final parsedResult = jsonDecode(result);
final meta = parsedResult["meta"] ?? "";
final metaResult = jsonDecode(meta);
final meta = parsedResult["meta"] ?? {};
final metaResult = meta is String && meta.isNotEmpty ? jsonDecode(meta) : meta;
logger.i('Flutter 结束调用 mcp metaResult: $metaResult');
// logger.i('Flutter 结束调用 mcp metaResult: $metaResult');
// 判断是否为新的回复或响应ID是否改变
if (_isNewAssistantResponse || _currentResponseId != responseId) {
// 添加新消息
@ -377,6 +377,7 @@ class AgentController extends GetxController {
// 滚动到底部
_scrollToBottom();
}
logger.i('Flutter 结束调用 mcp end');
break;
default:
break;

201
lib/modules/agent/views/message_bubble.dart

@ -217,7 +217,7 @@ class MessageBubble extends GetView<AgentController> {
final rawList = mate!['card_weather']['dayweather'] as List;
final daylist =
rawList.map((e) => Map<String, dynamic>.from(e)).toList();
final now = daylist[0];
final now = mate!['card_weather']['day'] as Map<String, dynamic>;
//天气
return _buildWeatherContent(
context,
@ -677,7 +677,7 @@ class MessageBubble extends GetView<AgentController> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'当前天气情况:',
'查询天气情况:',
style: TextStyle(
color: Colors.black87,
fontSize: 14,
@ -714,7 +714,7 @@ class MessageBubble extends GetView<AgentController> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
city,
"${city} ${now["date"]}",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
@ -764,7 +764,7 @@ class MessageBubble extends GetView<AgentController> {
),
],
),
Icon(
const Icon(
Icons.wb_sunny,
size: 48,
color: Colors.white,
@ -777,7 +777,7 @@ class MessageBubble extends GetView<AgentController> {
color: Colors.white.withOpacity(0.2),
),
SizedBox(height: 16),
Text(
const Text(
'未来预报',
style: TextStyle(
fontSize: 14,
@ -803,11 +803,15 @@ class MessageBubble extends GetView<AgentController> {
(index) {
String dateStr = days[index]['date'];
DateTime? date = DateUtil.getDateTime(dateStr);
String weekday = DateUtil.getWeekday(date);
IconData weatherIcon =
getWeatherIcon(days[index]['weatherCondition']);
String weekday = DateUtil.getWeekday(date,
languageCode: "zh", short: true);
String temp = "${days[index]['temperature']}°";
return _buildForecastDay(
weekday,
Icons.wb_sunny,
days[index]['temperature'],
weatherIcon,
temp,
);
},
),
@ -842,46 +846,46 @@ class MessageBubble extends GetView<AgentController> {
],
),
),
const SizedBox(height: 10),
Row(
mainAxisSize: MainAxisSize.min,
children: [
Expanded(
child: OutlinedButton.icon(
onPressed: () {},
icon: Icon(Icons.location_on, size: 16),
label: Text('更多地区'),
style: OutlinedButton.styleFrom(
foregroundColor: Theme.of(context).primaryColor,
side: BorderSide(
color: Theme.of(context).primaryColor.withOpacity(0.5)),
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 4),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
textStyle: TextStyle(fontSize: 12),
),
),
),
SizedBox(width: 8),
Expanded(
child: OutlinedButton.icon(
onPressed: () {},
icon: Icon(Icons.calendar_today, size: 16),
label: Text('15天预报'),
style: OutlinedButton.styleFrom(
foregroundColor: Colors.grey[700],
side: BorderSide(color: Colors.grey[400]!),
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 4),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
textStyle: TextStyle(fontSize: 12),
),
),
),
],
),
// const SizedBox(height: 10),
// Row(
// mainAxisSize: MainAxisSize.min,
// children: [
// Expanded(
// child: OutlinedButton.icon(
// onPressed: () {},
// icon: Icon(Icons.location_on, size: 16),
// label: Text('更多地区'),
// style: OutlinedButton.styleFrom(
// foregroundColor: Theme.of(context).primaryColor,
// side: BorderSide(
// color: Theme.of(context).primaryColor.withOpacity(0.5)),
// padding: EdgeInsets.symmetric(horizontal: 12, vertical: 4),
// shape: RoundedRectangleBorder(
// borderRadius: BorderRadius.circular(16),
// ),
// textStyle: TextStyle(fontSize: 12),
// ),
// ),
// ),
// SizedBox(width: 8),
// Expanded(
// child: OutlinedButton.icon(
// onPressed: () {},
// icon: Icon(Icons.calendar_today, size: 16),
// label: Text('15天预报'),
// style: OutlinedButton.styleFrom(
// foregroundColor: Colors.grey[700],
// side: BorderSide(color: Colors.grey[400]!),
// padding: EdgeInsets.symmetric(horizontal: 12, vertical: 4),
// shape: RoundedRectangleBorder(
// borderRadius: BorderRadius.circular(16),
// ),
// textStyle: TextStyle(fontSize: 12),
// ),
// ),
// ),
// ],
// ),
],
);
}
@ -915,6 +919,107 @@ class MessageBubble extends GetView<AgentController> {
],
);
}
/// 获取天气图标
IconData getWeatherIcon(String description) {
description = description.trim();
if (["晴", "少云", "晴间多云"].contains(description)) {
return Icons.wb_sunny;
}
if (["多云", "阴"].contains(description)) {
return Icons.cloud;
}
if (description.contains("雾") ||
["雾", "浓雾", "强浓雾", "轻雾", "大雾", "特强浓雾", "霾", "中度霾", "重度霾", "严重霾"]
.contains(description)) {
return Icons.blur_on;
}
if (description.contains("风") ||
[
"有风",
"平静",
"微风",
"和风",
"清风",
"强风/劲风",
"疾风",
"大风",
"烈风",
"风暴",
"狂爆风",
"飓风",
"热带风暴"
].contains(description)) {
return Icons.air;
}
if (description.contains("雨") ||
[
"阵雨",
"雷阵雨",
"雷阵雨并伴有冰雹",
"小雨",
"中雨",
"大雨",
"暴雨",
"大暴雨",
"特大暴雨",
"强阵雨",
"强雷阵雨",
"极端降雨",
"毛毛雨/细雨",
"雨",
"小雨-中雨",
"中雨-大雨",
"大雨-暴雨",
"暴雨-大暴雨",
"大暴雨-特大暴雨"
].contains(description)) {
return Icons.grain; // 或使用 community icon 包:weather_icons 中的 rain
}
if (description.contains("雪") ||
[
"雨雪天气",
"雨夹雪",
"阵雨夹雪",
"冻雨",
"雪",
"阵雪",
"小雪",
"中雪",
"大雪",
"暴雪",
"小雪-中雪",
"中雪-大雪",
"大雪-暴雪"
].contains(description)) {
return Icons.ac_unit;
}
if (["浮尘", "扬沙", "沙尘暴", "强沙尘暴"].contains(description)) {
return Icons.filter_drama;
}
if (["龙卷风"].contains(description)) {
return Icons.waves;
}
if (["热"].contains(description)) {
return Icons.local_fire_department;
}
if (["冷"].contains(description)) {
return Icons.ac_unit;
}
// 默认或未知天气
return Icons.help_outline;
}
}
/// 动画点组件

8
lib/modules/ota_upgrade/controllers/ota_upgrade_controller.dart

@ -73,10 +73,8 @@ class OtaUpgradeController extends GetxController {
}
// 检查固件信息有效性
if (deviceInfo.firmwareVersion.isEmpty
//||
// deviceInfo.firmwareProductId.isEmpty
) {
if (deviceInfo.firmwareVersion.isEmpty ||
deviceInfo.firmwareProductId.isEmpty) {
throw const FormatException('设备固件信息不完整');
}
@ -85,7 +83,7 @@ class OtaUpgradeController extends GetxController {
// API请求
final response = await Api.getequipmentinfo({
"id": "a2",
"id": deviceInfo.firmwareProductId,
});
// 处理API响应

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

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

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

@ -128,7 +128,7 @@ object BleAgent : BleService.Callback, AgentServiceListener {
}
override fun onAudioDataReceived(data: ByteArray) {
// FileLogger.d(TAG, "onAudioDataReceived, data: ${data.size}")
FileLogger.d(TAG, "onAudioDataReceived, data: ${data.size}")
AgentService.pushAudioData(data)
// 可选:处理音频数据
}
@ -142,7 +142,7 @@ object BleAgent : BleService.Callback, AgentServiceListener {
// 停止当前TTS,避免冲突
AgentService.interruptCurrentResponse()
// AgentService.speakText("我在!")
AgentService.speakText("我在!")
// 启动语音识别
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.

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

@ -53,4 +53,5 @@ tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
dependencies {
// 添加Microsoft语音SDK
implementation("com.microsoft.cognitiveservices.speech:client-sdk:1.30.0")
implementation(project(":ble_service"))
}

58
local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt

@ -14,9 +14,10 @@ import io.flutter.plugin.common.EventChannel
import kotlinx.coroutines.*
import kotlin.coroutines.CoroutineContext
import com.microsoft.cognitiveservices.speech.audio.*
import com.yunqiinnovation.ble_service.BleService
import com.yunqiinnovation.ble_service.BleConst
/** AzureSpeechPlugin */
class AzureSpeechPlugin: FlutterPlugin, CoroutineScope {
class AzureSpeechPlugin: FlutterPlugin,BleService.Callback, CoroutineScope {
private val tag = "AzureSpeechPlugin"
private lateinit var context: Context
private val mainHandler = Handler(Looper.getMainLooper())
@ -37,7 +38,10 @@ class AzureSpeechPlugin: FlutterPlugin, CoroutineScope {
private lateinit var ttsEventChannel: EventChannel
private var ttsEventSink: EventChannel.EventSink? = null
private lateinit var azureTtsHelper: AzureTtsHelper
// 是否已注册BLE回调
private var isRegisteredToBle = false
// ASR 事件发送方法
private fun sendAsrEvent(event: Map<String, Any>) {
if (asrEventSink == null) {
@ -110,6 +114,19 @@ class AzureSpeechPlugin: FlutterPlugin, CoroutineScope {
// 初始化Azure语音服务
azureTtsHelper = AzureTtsHelper(context)
azureAsrHelper = AzureAsrHelper(context)
// 2. 初始化BleService并注册回调
if (BleService.initialize(context)) {
if (BleService.addCallback(this)) {
isRegisteredToBle = true
FileLogger.d(tag, "已注册BLE回调")
} else {
FileLogger.w(tag, "BLE回调注册失败")
}
} else {
FileLogger.e(tag, "BLE服务初始化失败")
}
}
// ASR方法处理器
@ -359,7 +376,12 @@ class AzureSpeechPlugin: FlutterPlugin, CoroutineScope {
try {
// 取消所有协程
job.cancel()
// 取消BLE回调注册
if (isRegisteredToBle) {
BleService.removeCallback(this)
isRegisteredToBle = false
}
// 释放资源
azureTtsHelper.dispose()
azureAsrHelper.dispose()
@ -367,4 +389,34 @@ class AzureSpeechPlugin: FlutterPlugin, CoroutineScope {
FileLogger.e(tag, "释放资源失败: ${e.message}")
}
}
override fun onScanResult(devices: List<Map<String, Any>>) {
}
override fun onConnectionStateChanged(state: Int) {
// 可选:处理连接状态变化
if (state == BleConst.STATE_CONNECTED) {
FileLogger.d(tag, "Ble设备已连接,准备好语音交互")
} else if (state == BleConst.STATE_DISCONNECTED) {
FileLogger.d(tag, "Ble设备已断开,停止语音服务")
// 设备断开连接,停止语音服务
// stopListening()
}
}
override fun onAudioDataReceived(data: ByteArray) {
FileLogger.d(tag, "AzureSpeechPlugin onAudioDataReceived, data: ${data.size}")
azureAsrHelper.pushAudioData(data)
}
override fun onWakeupSignalReceived() {
}
override fun onDeviceInfoReceived(infoType: Int, infoData: Map<String, Any>) {
}
}

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

@ -31,5 +31,4 @@ dependencies {
// 使用从本地Maven仓库引用的AAR依赖
implementation("com.jieli.audio:jl_audio_decode:2.0.0")
implementation(project(":deepvoice_utils"))
implementation(project(":azure_speech"))
}

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

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

@ -661,10 +661,10 @@ object BleService {
"rawData" to responseData
)
if (responseData.isNotEmpty() && responseData.size >= 2) {
val productId = "0x${(responseData[0].toInt() and 0xFF).toString(16)}${
(responseData[1].toInt() and 0xFF).toString(16)
}"
if (responseData.isNotEmpty()) {
// 转换为字符串前,确认是否是有效的ASCII字符
val productId =
responseData.joinToString(".") { (it.toInt() and 0xFF).toString() }
resultMap["productId"] = productId
FileLogger.i(TAG, "接收到产品ID: $productId")
}

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

@ -185,6 +185,10 @@ class BleServicePlugin : FlutterPlugin, MethodCallHandler, ActivityAware,
"getDeviceVersion" -> {
val success = BleService.getDeviceVersion()
result.success(success)
}
"getProductId" -> {
val success = BleService.getProductId()
result.success(success)
}
"getBatteryInfo" -> {
val success = BleService.getBatteryInfo()

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 {
try {
Log.d(TAG, "开始连接到服务器: $serverUrl")
Log.d(TAG, "开始连接服务器: $serverUrl")
// 使用提供的客户端或创建带SSL配置的客户端
val client = customClient ?: createSslTrustAllClient()
// 使用自定义传输层,直接传递完整URL
// 使用自定义传输层
sseTransport = CustomSseClientTransport(
client = client,
urlString = serverUrl,
requestBuilder = {
// 添加基本请求头
// headers {
// append(HttpHeaders.Accept, "text/event-stream")
// append(HttpHeaders.CacheControl, "no-cache")
// }
}
urlString = serverUrl
)
Log.d(TAG, "创建自定义SSE传输层成功")
// 连接MCP客户端到服务器
mcp.connect(sseTransport!!)
// 获取服务器提供的工具列表
// 获取工具列表并处理
val toolsResult = mcp.listTools()
Log.d(TAG, "获取到工具列表: $toolsResult")
// 处理工具列表
processToolsList(toolsResult)
isConnected = true
Log.d(TAG, "成功连接到MCP服务器,可用工具: ${tools.map { it.tool().get().name() }.joinToString(", ")}")
Log.d(TAG, "连接成功,可用工具: ${tools.size}个")
return true
} catch (e: Exception) {
Log.e(TAG, "连接服务器失败: ${e.message}", e)
// 记录详细的错误堆栈
Log.e(TAG, "详细错误: ${e.stackTraceToString()}")
Log.e(TAG, "连接失败: ${e.message}")
return false
}
}
@ -383,7 +369,7 @@ class MCPSubClient(
*/
suspend fun callTool(name: String, arguments: Map<String, Any>): JSONObject? {
if (!isConnected) {
Log.e(TAG, "未连接到MCP服务器", null)
Log.e(TAG, "未连接到服务器")
return JSONObject().apply { put("context","未连接到MCP服务器") }
}
@ -393,13 +379,7 @@ class MCPSubClient(
}
return try {
// 直接使用原始参数
Log.d(TAG, "调用工具 $name 使用参数: $arguments")
val result = mcp.callTool(
name = name,
arguments = arguments
)
val result = mcp.callTool(name = name, arguments = arguments)
// 处理工具调用结果
val context = result?.content?.joinToString("\n") { content ->
@ -408,13 +388,14 @@ class MCPSubClient(
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) {
Log.e(TAG, "调用MCP工具失败: ${e.message}", e)
"调用MCP工具失败: ${e.message}"
JSONObject().apply { put("context", "调用MCP工具失败: ${e.message}")}
Log.e(TAG, "调用工具失败: ${e.message}")
JSONObject().apply { put("context", "调用工具失败: ${e.message}") }
}
}
@ -736,20 +717,25 @@ class MCPClient(private val context: Context? = null) : AutoCloseable {
* @return 调用结果
*/
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)
return when (toolType) {
val result = when (toolType) {
ToolType.LOCAL_FUNCTION -> {
callLocalFunction(name, arguments)
}
ToolType.MCP_TOOL -> {
callMcpTool(name, arguments)
callMcpTool(name, arguments, requestId)
}
null -> {
Log.e(TAG, "未找到工具: $name")
JSONObject().apply { put("context","未找到工具: $name") }
}
}
return result
}
/**
@ -760,13 +746,13 @@ class MCPClient(private val context: Context? = null) : AutoCloseable {
val handler = localFunctions[name]
if (handler != null) {
val context = handler.handle(arguments)
JSONObject().apply { put("context", context) }
JSONObject().apply { put("context", context) }
} else {
Log.e(TAG, "未找到本地函数: $name")
JSONObject().apply { put("context","未找到本地函数: $name") }
}
} catch (e: Exception) {
Log.e(TAG, "调用本地函数失败: ${e.message}", e)
Log.e(TAG, "调用本地函数失败: ${e.message}")
JSONObject().apply { put("context","调用本地函数失败: ${e.message}") }
}
}
@ -774,24 +760,32 @@ class MCPClient(private val context: Context? = null) : AutoCloseable {
/**
* 调用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) {
Log.e(TAG, "未连接到MCP服务器", null)
Log.e(TAG, "未连接到服务器")
return JSONObject().apply { put("context","未连接到MCP服务器") }
}
// 查找包含此工具的子客户端
val clientEntry = subClients.entries.find { it.value.containsTool(name) }
if (clientEntry != null) {
return if (clientEntry != null) {
val (serverId, client) = clientEntry
Log.d(TAG, "通过子客户端 [$serverId] 调用工具: $name")
return client.callTool(name, arguments)
try {
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.graphics.Bitmap
import android.graphics.BitmapFactory
import android.os.Handler
import android.os.Looper
import android.util.Base64
import android.util.Log
import com.fasterxml.jackson.core.type.TypeReference
import com.fasterxml.jackson.databind.ObjectMapper
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.coroutines.*
import okhttp3.*
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.RequestBody.Companion.toRequestBody
@ -21,14 +20,38 @@ import java.io.ByteArrayOutputStream
import java.io.File
import java.io.IOException
import java.util.concurrent.TimeUnit
import kotlin.coroutines.CoroutineContext
import kotlin.math.min
import kotlin.math.sqrt
/**
* OpenAI服务的原生实现
*/
class OpenAIService(private val context: Context? = null) {
class OpenAIService(private val context: Context? = null) : CoroutineScope {
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 val client = OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
@ -50,7 +73,7 @@ class OpenAIService(private val context: Context? = null) {
// 添加成员变量以跟踪当前请求
private var currentStreamCall: Call? = null
private var isCanceled = false
@Volatile private var isCanceled = false
/**
* 构建curl命令用于测试
@ -92,57 +115,42 @@ class OpenAIService(private val context: Context? = null) {
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
// 读取文件
var bitmap = BitmapFactory.decodeFile(filePath) ?: return null
// 检查图片尺寸,限制最大为1024*1024
val maxDimension = 1024
if (originalBitmap.width > maxDimension || originalBitmap.height > maxDimension) {
Log.d(TAG, "图片尺寸超过限制,进行缩放: ${originalBitmap.width}x${originalBitmap.height} -> ${maxDimension}x${maxDimension}")
if (bitmap.width > maxDimension || bitmap.height > maxDimension) {
// 计算缩放比例,保持纵横比
val widthRatio = maxDimension.toFloat() / originalBitmap.width
val heightRatio = maxDimension.toFloat() / originalBitmap.height
val ratio = Math.min(widthRatio, heightRatio)
val ratio = min(maxDimension.toFloat() / bitmap.width, maxDimension.toFloat() / bitmap.height)
val newWidth = (originalBitmap.width * ratio).toInt()
val newHeight = (originalBitmap.height * ratio).toInt()
val newWidth = (bitmap.width * ratio).toInt()
val newHeight = (bitmap.height * ratio).toInt()
compressedBitmap = Bitmap.createScaledBitmap(originalBitmap, newWidth, newHeight, true)
Log.d(TAG, "缩放后图片尺寸: ${newWidth}x${newHeight}")
bitmap = Bitmap.createScaledBitmap(bitmap, newWidth, newHeight, true)
}
// 压缩图片
val outputStream = ByteArrayOutputStream()
var quality = 100
bitmap.compress(Bitmap.CompressFormat.JPEG, quality, outputStream)
// 如果原始图片太大,继续优化文件大小
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)
val scale = sqrt(maxSizeKB.toDouble() / fileSize)
val newWidth = (bitmap.width * scale).toInt()
val newHeight = (bitmap.height * scale).toInt()
bitmap = Bitmap.createScaledBitmap(bitmap, newWidth, newHeight, true)
outputStream.reset()
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")
bitmap.compress(Bitmap.CompressFormat.JPEG, quality, outputStream)
}
// 转为Base64
return Base64.encodeToString(imageBytes, Base64.NO_WRAP)
return Base64.encodeToString(outputStream.toByteArray(), Base64.NO_WRAP)
} catch (e: Exception) {
Log.e(TAG, "转换文件到Base64失败: ${e.message}", e)
Log.e(TAG, "转换Base64失败: ${e.message}")
return null
}
}
@ -151,29 +159,26 @@ class OpenAIService(private val context: Context? = null) {
* 创建带图片的用户消息
*/
fun createUserMessageWithImage(text: String, imageBase64: String): JSONObject {
// 创建包含文本和图片的内容数组
val contentArray = JSONArray().apply {
// 添加文本部分
if (text.isNotEmpty()) {
return JSONObject().apply {
put("role", "user")
put("content", JSONArray().apply {
// 添加文本部分
if (text.isNotEmpty()) {
put(JSONObject().apply {
put("type", "text")
put("text", text)
})
}
// 添加图片部分
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")
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)
// 在后台线程中初始化MCP客户端
val scope = CoroutineScope(Dispatchers.IO)
var result = false
scope.launch {
// 直接使用类的CoroutineScope启动协程
launch {
try {
result = mcpClient?.connectToSSE(mcpServer) ?: false
val result = mcpClient?.connectToSSE(mcpServer) ?: false
isMcpInitialized = result
Log.d(TAG, "MCP客户端初始化${if (result) "成功" else "失败"}")
} catch (e: Exception) {
@ -271,8 +273,6 @@ class OpenAIService(private val context: Context? = null) {
return true // 立即返回,实际连接在后台进行
}
/**
* 处理MCP工具调用
*
@ -343,7 +343,7 @@ class OpenAIService(private val context: Context? = null) {
// 调用工具(统一通过MCPClient处理)
val result = mcpClient?.callTool(name, arguments)
Log.d(TAG, "自动处理MCP工具调用结果: ${result?.toString()}")
// 检查是否已取消
if (isCanceled) {
Log.d(TAG, "工具调用已被取消,不处理结果")
@ -352,14 +352,10 @@ class OpenAIService(private val context: Context? = null) {
// 回调结果
val resultObj = result ?: JSONObject().apply { put("context", "工具调用失败") }
callback.onFunctionCallResult(functionCall, resultObj)
callback.dispatch { onFunctionCallResult(functionCall, resultObj) }
// 发送结果到OpenAI继续对话
val resultContent = if (resultObj.has("context") && resultObj.getString("context").isNotEmpty()) {
resultObj.getString("context")
} else {
"工具调用失败"
}
val resultContent = resultObj.optString("context", "工具调用失败")
sendFunctionCallResult(
messages = messages,
functionCall = functionCall,
@ -484,7 +480,7 @@ class OpenAIService(private val context: Context? = null) {
*/
fun sendMessageStream(messages: JSONArray, callback: StreamCallback) {
if (!isInitialized || apiKey.isEmpty()) {
callback.onError(OpenAIException("OpenAI服务未初始化"))
callback.dispatch { onError(OpenAIException("OpenAI服务未初始化")) }
return
}
@ -496,10 +492,13 @@ class OpenAIService(private val context: Context? = null) {
if (messages.length() > 0) {
val lastMessage = messages.getJSONObject(messages.length() - 1)
if (lastMessage.has("content")) {
val content = lastMessage.getString("content")
currentModel = visionModel
val content = lastMessage.optString("content", "")
if (content.contains("image_url")) {
currentModel = visionModel
}
}
}
val requestBody = JSONObject().apply {
put("model", currentModel)
put("messages", messages)
@ -535,10 +534,6 @@ class OpenAIService(private val context: Context? = null) {
.post(requestBody.toString().toRequestBody(mediaType))
.build()
// 输出用于测试的curl命令
// val curlCommand = buildCurlCommand(request, requestBody.toString())
// Log.d(TAG, "curl command: $curlCommand")
val call = client.newCall(request)
// 保存当前Call对象以便后续取消
currentStreamCall = call
@ -549,7 +544,7 @@ class OpenAIService(private val context: Context? = null) {
if (isCanceled) {
return
}
callback.onError(OpenAIException(e.message ?: "请求失败"))
callback.dispatch { onError(OpenAIException(e.message ?: "请求失败")) }
}
override fun onResponse(call: Call, response: Response) {
@ -560,7 +555,7 @@ class OpenAIService(private val context: Context? = null) {
}
if (!response.isSuccessful) {
callback.onError(OpenAIException("API调用失败: ${response.code}"))
callback.dispatch { onError(OpenAIException("API调用失败: ${response.code}")) }
return
}
@ -570,7 +565,7 @@ class OpenAIService(private val context: Context? = null) {
try {
// 用于存储函数调用的各个部分
val finalToolCalls = mutableMapOf<Int, ToolCallInfo>()
val toolCalls = mutableMapOf<Int, ToolCallInfo>()
// 逐块读取数据,而不是一次性预取
val BUFFER_SIZE = 8192L // 8KB的块大小
@ -590,84 +585,16 @@ class OpenAIService(private val context: Context? = null) {
// 处理[DONE]消息
if (data == "[DONE]" || data == "[\"DONE\"]") {
val hasToolCalls = processToolCalls(finalToolCalls, callback, messages)
val hasToolCalls = processToolCalls(toolCalls, callback, messages)
// 如果没有工具调用,这是对话的真正结束
if (!hasToolCalls) {
callback.onComplete()
callback.dispatch { onComplete() }
}
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) {
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
}
}
}
}
handleStreamJson(JSONObject(data), toolCalls, callback, messages)
} catch (e: Exception) {
// 忽略解析错误,但记录日志
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) {
callback.onComplete()
callback.dispatch { onComplete() }
}
} catch (e: Exception) {
// 如果是由于取消导致的异常,不报告错误
if (isCanceled) {
return
}
callback.onError(OpenAIException("处理响应流时出错: ${e.message}"))
callback.dispatch { onError(OpenAIException("处理响应流时出错: ${e.message}")) }
} finally {
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) {
if (!isCanceled) {
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 {
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")
// 通知上层工具调用事件
callback.onFunctionCall(functionCall)
fun cancelCurrentStream(): Boolean {
try {
// 标记已取消
isCanceled = true
// 在协程中处理工具调用
if (messages != null) {
kotlinx.coroutines.GlobalScope.launch {
try {
if (!isCanceled) {
autoHandleMcpToolCall(functionCall, messages, callback)
} 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)
}
}
}
}
// 取消当前Call对象
val call = currentStreamCall
if (call != null && !call.isCanceled()) {
call.cancel()
Log.d(TAG, "已取消当前流式请求")
return true
}
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 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

163
macos/Podfile.lock

@ -1,21 +1,110 @@
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):
- FlutterMacOS
- device_info_plus (0.0.1):
- FlutterMacOS
- facebook_auth_desktop (0.0.3):
- FlutterMacOS
- file_picker (0.0.1):
- FlutterMacOS
- file_selector_macos (0.0.1):
- 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
- FlutterMacOS
- flutter_image_compress_macos (1.0.0):
- FlutterMacOS
- flutter_local_notifications (0.0.1):
- FlutterMacOS
- flutter_secure_storage_macos (6.1.3):
- FlutterMacOS
- flutter_tts (0.0.1):
- FlutterMacOS
- 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):
- Flutter
- FlutterMacOS
- package_info_plus (0.0.1):
- FlutterMacOS
- path_provider_foundation (0.0.1):
- Flutter
- FlutterMacOS
@ -27,46 +116,93 @@ PODS:
- Flutter
- FlutterMacOS
- Try
- sqflite_darwin (0.0.4):
- Flutter
- FlutterMacOS
- Try (2.1.1)
- webview_flutter_wkwebview (0.0.1):
- Flutter
- FlutterMacOS
- window_size (0.0.2):
- FlutterMacOS
DEPENDENCIES:
- app_settings (from `Flutter/ephemeral/.symlinks/plugins/app_settings/macos`)
- audio_session (from `Flutter/ephemeral/.symlinks/plugins/audio_session/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`)
- 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_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_secure_storage_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos`)
- flutter_tts (from `Flutter/ephemeral/.symlinks/plugins/flutter_tts/macos`)
- 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`)
- package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`)
- path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`)
- record_darwin (from `Flutter/ephemeral/.symlinks/plugins/record_darwin/macos`)
- share_plus (from `Flutter/ephemeral/.symlinks/plugins/share_plus/macos`)
- 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`)
SPEC REPOS:
trunk:
- AppAuth
- Firebase
- FirebaseAppCheckInterop
- FirebaseAuth
- FirebaseAuthInterop
- FirebaseCore
- FirebaseCoreExtension
- FirebaseCoreInternal
- GoogleSignIn
- GoogleUtilities
- GTMAppAuth
- GTMSessionFetcher
- Try
EXTERNAL SOURCES:
app_settings:
:path: Flutter/ephemeral/.symlinks/plugins/app_settings/macos
audio_session:
:path: Flutter/ephemeral/.symlinks/plugins/audio_session/macos
device_info_plus:
: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:
: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:
: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:
: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:
:path: Flutter/ephemeral/.symlinks/plugins/flutter_tts/macos
FlutterMacOS:
:path: Flutter/ephemeral
google_sign_in_ios:
:path: Flutter/ephemeral/.symlinks/plugins/google_sign_in_ios/darwin
just_audio:
:path: Flutter/ephemeral/.symlinks/plugins/just_audio/darwin
package_info_plus:
:path: Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos
path_provider_foundation:
:path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin
record_darwin:
@ -75,23 +211,50 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/share_plus/macos
speech_to_text:
: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:
:path: Flutter/ephemeral/.symlinks/plugins/window_size/macos
SPEC CHECKSUMS:
app_settings: cd21e176b56f8172043640ade81322a98896bff4
AppAuth: d4f13a8fe0baf391b2108511793e4b479691fb73
audio_session: eaca2512cf2b39212d724f35d11f46180ad3a33e
device_info_plus: a56e6e74dbbd2bb92f2da12c64ddd4f67a749041
facebook_auth_desktop: bf89ccf460c903de45e86fa0c7878b8b62db65fb
file_picker: 7584aae6fa07a041af2b36a2655122d42f578c1a
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_image_compress_macos: e68daf54bb4bf2144c580fd4d151c949cbf492f0
flutter_local_notifications: 13862b132e32eb858dea558a86d45d08daeacfe7
flutter_secure_storage_macos: 7f45e30f838cf2659862a4e4e3ee1c347c2b3b54
flutter_tts: ae915565cc6948444b513acc8ee021993281e027
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
google_sign_in_ios: 19297361f2c51d7d8ac0201b866ef1fa5d1f94a8
GoogleSignIn: d4281ab6cf21542b1cfaff85c191f230b399d2db
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
GTMAppAuth: f69bd07d68cd3b766125f7e072c45d7340dea0de
GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6
just_audio: 4e391f57b79cad2b0674030a00453ca5ce817eed
package_info_plus: f0052d280d17aa382b932f399edf32507174e870
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
record_darwin: 30509266ae213af8afdb09a8ae7467cb64c1377e
share_plus: 510bf0af1a42cd602274b4629920c9649c52f4cc
speech_to_text: 9dc43a5df3cbc2813f8c7cc9bd0fbf94268ed7ac
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
Try: 5ef669ae832617b3cee58cb2c6f99fb767a4ff96
webview_flutter_wkwebview: 1821ceac936eba6f7984d89a9f3bcb4dea99ebb2
window_size: 4bd15034e6e3d0720fd77928a7c42e5492cfece9
PODFILE CHECKSUM: 8c4442624fc136b0afb378a2502958e01c05d073

18
macos/Runner.xcodeproj/project.pbxproj

@ -241,6 +241,7 @@
33CC110E2044A8840003C045 /* Bundle Framework */,
3399D490228B24CF009A79C7 /* ShellScript */,
EEBEF79BCB6B649D2C3D134C /* [CP] Embed Pods Frameworks */,
5D85B8FB5391E947F8A1E18F /* [CP] Copy Pods Resources */,
);
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";
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 */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;

Loading…
Cancel
Save