Browse Source

Merge branch 'new_dev' of https://github.com/deepcloud2048/deep_voice into new_dev

newdev_shunjiawei
lxm 1 year ago
parent
commit
a591d90f70
  1. 3
      lib/data/services/ble_manager.dart
  2. 43
      lib/data/services/chat_history_service.dart
  3. 10
      lib/data/services/db/sqflite_api.dart
  4. 7
      lib/modules/agent/controllers/agent_controller.dart
  5. 2
      lib/modules/meeting/controllers/meeting_details_controller.dart
  6. 30
      lib/modules/meeting/views/meeting_details_view.dart
  7. 12
      lib/modules/meeting/views/share_bottom_sheet.dart
  8. 95
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt
  9. 8
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt
  10. 3
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleMediaService.kt
  11. 3
      local_plugins/agent_service/lib/agent_service.dart
  12. 8
      local_plugins/amap_service/android/src/main/kotlin/com/yunqiinnovation/amap_service/AmapServicePlugin.kt
  13. 37
      local_plugins/amap_service/android/src/main/kotlin/com/yunqiinnovation/amap_service/NavigationActivity.kt
  14. 40
      local_plugins/amap_service/android/src/main/kotlin/com/yunqiinnovation/amap_service/NavigationService.kt
  15. 2
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureTtsHelper.kt
  16. 67
      local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiPlugin.kt
  17. 165
      local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiService.kt
  18. 53
      local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/SystemFunctionHandler.kt
  19. 6
      local_plugins/chat_api/ios/chat_api/Sources/chat_api/ChatApiService.swift
  20. 16
      local_plugins/location_service/android/src/main/kotlin/com/yunqiinnovation/location_service/LocationService.kt
  21. 5
      local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt
  22. 2
      local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicServiceStarter.kt

3
lib/data/services/ble_manager.dart

@ -501,6 +501,8 @@ class BleManager extends GetxService {
final openaiBaseUrl = final openaiBaseUrl =
AppConfig.env('OPENAI_BASE_URL') ?? 'https://api.openai.com/v1'; AppConfig.env('OPENAI_BASE_URL') ?? 'https://api.openai.com/v1';
final openaiModel = AppConfig.env('OPENAI_MODEL') ?? 'gpt-3.5-turbo'; final openaiModel = AppConfig.env('OPENAI_MODEL') ?? 'gpt-3.5-turbo';
final openaiModelMedia = AppConfig.env('OPENAI_API_VISION_MODEL') ??
'doubao-1-5-vision-pro-32k-250115';
final openaiagentid = final openaiagentid =
AppConfig.env('OPENAI_AGENT_ID') ?? 'personal_assistant'; AppConfig.env('OPENAI_AGENT_ID') ?? 'personal_assistant';
final systemPrompt = AppConfig.agentsystemPrompt(openaiagentid); final systemPrompt = AppConfig.agentsystemPrompt(openaiagentid);
@ -515,6 +517,7 @@ class BleManager extends GetxService {
'openaiApiKey': openaiApiKey, 'openaiApiKey': openaiApiKey,
'openaiBaseUrl': openaiBaseUrl, 'openaiBaseUrl': openaiBaseUrl,
'openaiModel': openaiModel, 'openaiModel': openaiModel,
'openaivisionModel': openaiModelMedia,
'systemPrompt': systemPrompt, 'systemPrompt': systemPrompt,
'mcpServer': jsonEncode(jsonData), 'mcpServer': jsonEncode(jsonData),
'volcanoAppId': '2316081933', 'volcanoAppId': '2316081933',

43
lib/data/services/chat_history_service.dart

@ -8,50 +8,50 @@ import 'package:chat_storage/chat_storage.dart';
class ChatHistoryService extends GetxService { class ChatHistoryService extends GetxService {
// 最大历史记录数量 // 最大历史记录数量
static const int _maxHistoryCount = 200; // 每个对话最多保存的消息数 static const int _maxHistoryCount = 200; // 每个对话最多保存的消息数
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
Logger.info('ChatHistoryService 已初始化'); Logger.info('ChatHistoryService 已初始化');
} }
/// 保存聊天记录 /// 保存聊天记录
/// ///
/// [agentId] 代理ID /// [agentId] 代理ID
/// [messages] 消息列表 /// [messages] 消息列表
Future<void> saveHistory(String agentId, List<Message> messages) async { Future<void> saveHistory(String agentId, List<Message> messages) async {
try { try {
if (messages.isEmpty) return; if (messages.isEmpty) return;
// 获取最新的消息 // 获取最新的消息
final latestMessage = messages.last; final latestMessage = messages.last;
// 如果是加载中的消息则跳过 // 如果是加载中的消息则跳过
if (latestMessage.isIntermediate) return; if (latestMessage.isIntermediate) return;
// 直接插入最新的消息到数据库 // 直接插入最新的消息到数据库
await ChatStorage.saveMessage( await ChatStorage.saveMessage(
sessionId: agentId, sessionId: agentId,
message: latestMessage.content, message: latestMessage.content,
sender: latestMessage.role, sender: latestMessage.role,
); );
// 检查并清理过多的历史记录 // 检查并清理过多的历史记录
await _cleanupOldMessages(agentId); await _cleanupOldMessages(agentId);
} catch (e) { } catch (e) {
Logger.error('保存聊天记录失败', e.toString()); Logger.error('保存聊天记录失败', e.toString());
} }
} }
/// 清理旧消息,保持每个会话的消息数量不超过最大限制 /// 清理旧消息,保持每个会话的消息数量不超过最大限制
Future<void> _cleanupOldMessages(String agentId) async { Future<void> _cleanupOldMessages(String agentId) async {
try { try {
final pagedResult = await ChatStorage.getMessages( final pagedResult = await ChatStorage.getMessages(
sessionId: agentId, sessionId: agentId,
page: 1, page: 1,
pageSize: 1, // 只获取总数信息 pageSize: 1, // 只获取总数信息
); );
// 如果消息总数超过最大限制,删除最早的消息 // 如果消息总数超过最大限制,删除最早的消息
if (pagedResult.totalCount > _maxHistoryCount) { if (pagedResult.totalCount > _maxHistoryCount) {
final messagesToDelete = pagedResult.totalCount - _maxHistoryCount; final messagesToDelete = pagedResult.totalCount - _maxHistoryCount;
@ -61,13 +61,13 @@ class ChatHistoryService extends GetxService {
page: 1, page: 1,
pageSize: messagesToDelete, pageSize: messagesToDelete,
); );
// 构建要删除的消息ID列表 // 构建要删除的消息ID列表
final List<int> messageIds = oldestMessages.messages final List<int> messageIds = oldestMessages.messages
.where((m) => m.id != null) .where((m) => m.id != null)
.map((m) => m.id!) .map((m) => m.id!)
.toList(); .toList();
// 删除这些消息 // 删除这些消息
if (messageIds.isNotEmpty) { if (messageIds.isNotEmpty) {
await ChatStorage.deleteMessages(messageIds: messageIds); await ChatStorage.deleteMessages(messageIds: messageIds);
@ -77,9 +77,9 @@ class ChatHistoryService extends GetxService {
Logger.error('清理旧消息失败', e); Logger.error('清理旧消息失败', e);
} }
} }
/// 加载聊天记录 /// 加载聊天记录
/// ///
/// [agentId] 代理ID /// [agentId] 代理ID
/// 返回消息列表,如果没有找到或解析错误则返回空列表 /// 返回消息列表,如果没有找到或解析错误则返回空列表
Future<List<Message>> loadHistory(String agentId) async { Future<List<Message>> loadHistory(String agentId) async {
@ -89,29 +89,30 @@ class ChatHistoryService extends GetxService {
page: 1, page: 1,
pageSize: _maxHistoryCount, pageSize: _maxHistoryCount,
); );
if (pagedResult.messages.isEmpty) { if (pagedResult.messages.isEmpty) {
return []; return [];
} }
// 将ChatStorage.ChatMessage转换为Message // 将ChatStorage.ChatMessage转换为Message
final messages = pagedResult.messages.map((chatMessage) { final messages = pagedResult.messages.map((chatMessage) {
return Message( return Message(
role: chatMessage.sender, role: chatMessage.sender,
content: chatMessage.message, content: chatMessage.message,
timestamp: DateTime.parse(chatMessage.timestamp), // 直接使用chatMessage的timestamp timestamp: DateTime.parse(
chatMessage.timestamp), // 直接使用chatMessage的timestamp
// isLoading 默认false // isLoading 默认false
// isLoading: metadata['isLoading'] ?? false, // isLoading: metadata['isLoading'] ?? false,
); );
}).toList(); }).toList();
return messages; return messages;
} catch (e) { } catch (e) {
Logger.error('加载聊天记录失败', e); Logger.error('加载聊天记录失败', e);
return []; return [];
} }
} }
/// 清除特定代理的聊天记录 /// 清除特定代理的聊天记录
Future<void> clearHistory(String agentId) async { Future<void> clearHistory(String agentId) async {
try { try {
@ -121,7 +122,7 @@ class ChatHistoryService extends GetxService {
Logger.error('清除聊天记录失败', e); Logger.error('清除聊天记录失败', e);
} }
} }
/// 清除所有聊天记录 /// 清除所有聊天记录
Future<void> clearAllHistory() async { Future<void> clearAllHistory() async {
try { try {
@ -131,4 +132,4 @@ class ChatHistoryService extends GetxService {
Logger.error('清除所有聊天记录失败', e); Logger.error('清除所有聊天记录失败', e);
} }
} }
} }

10
lib/data/services/db/sqflite_api.dart

@ -7,18 +7,18 @@ import 'package:get_storage/get_storage.dart';
import 'database_helper.dart'; import 'database_helper.dart';
class SqfliteApi { class SqfliteApi {
static GetStorage storage = GetStorage();
static String userId = '';
// 保存操作记录 // 保存操作记录
static Future saveMeetingOperationRecord( static Future saveMeetingOperationRecord(
String method, String method,
dynamic params, dynamic params,
) async { ) async {
if (userId.isEmpty) { GetStorage storage = GetStorage();
Map userInfo = storage.read("user_info"); String userId = '';
Map? userInfo = storage.read("user_info");
if (userInfo != null) {
userId = userInfo['user']['uid']; userId = userInfo['user']['uid'];
} }
final db = await DatabaseHelper().database; final db = await DatabaseHelper().database;
String data = ''; String data = '';
if (method == 'records') { if (method == 'records') {

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

@ -2,6 +2,8 @@ import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:amap_service/amap_service.dart'; import 'package:amap_service/amap_service.dart';
import 'package:deep_voice/core/utils/upload_oss.dart';
import 'package:deep_voice/data/models/user_Info.dart';
import 'package:deep_voice/data/services/location_manager.dart'; import 'package:deep_voice/data/services/location_manager.dart';
import 'package:deep_voice/data/services/music_manager.dart'; import 'package:deep_voice/data/services/music_manager.dart';
import 'package:deep_voice/data/services/navigation_manager.dart'; import 'package:deep_voice/data/services/navigation_manager.dart';
@ -548,7 +550,9 @@ class AgentController extends GetxController {
// 复制图片到应用永久存储目录 // 复制图片到应用永久存储目录
final permanentImagePath = await _copyImageToPermanentStorage(imagePath); final permanentImagePath = await _copyImageToPermanentStorage(imagePath);
var ImageUrl = await UploadOss.upload(
filepath: imagePath, rootDir: 'User/${User.instance.uid}/ChatImages');
Logger.i(TAG, '图片上传: $imagePath URL: $ImageUrl');
// 显示的文本,如果没有提供则使用默认值 // 显示的文本,如果没有提供则使用默认值
final displayText = text?.isNotEmpty == true ? text! : '[图片]'; final displayText = text?.isNotEmpty == true ? text! : '[图片]';
@ -578,6 +582,7 @@ class AgentController extends GetxController {
// 调用Agent Service处理图片,考虑TTS状态 // 调用Agent Service处理图片,考虑TTS状态
await AgentService.processImageInput( await AgentService.processImageInput(
permanentImagePath, permanentImagePath,
ImageUrl,
text: text ?? '', text: text ?? '',
speakResponse: isTtsEnabled.value, speakResponse: isTtsEnabled.value,
); );

2
lib/modules/meeting/controllers/meeting_details_controller.dart

@ -145,7 +145,7 @@ class MeetingDetailsController extends GetxController
} }
await playerController.preparePlayer( await playerController.preparePlayer(
path: meetingData.value.filepath, path: meetingData.value.filepath,
noOfSamples: 1200, noOfSamples: 88,
); );
durationMax.value = await playerController.getDuration(DurationType.max); durationMax.value = await playerController.getDuration(DurationType.max);
playerController.setFinishMode(finishMode: FinishMode.pause); playerController.setFinishMode(finishMode: FinishMode.pause);

30
lib/modules/meeting/views/meeting_details_view.dart

@ -101,15 +101,24 @@ class MeetingDetailsView extends GetView<MeetingDetailsController> {
controller.dragOffset.value = 210.w; controller.dragOffset.value = 210.w;
controller.isTop.value = false; controller.isTop.value = false;
}, },
child: AudioFileWaveforms( child: SizedBox(
size: Size(130.w, 30.w), width: 130.w,
playerController: controller.playerController, child: FittedBox(
enableSeekGesture: false, fit: BoxFit.scaleDown,
playerWaveStyle: PlayerWaveStyle( child: AudioFileWaveforms(
fixedWaveColor: Colors.grey[400]!, size: Size(350.w, 30.w),
liveWaveColor: Colors.red, playerController: controller.playerController,
seekLineColor: Colors.red, waveformType: WaveformType.fitWidth,
scaleFactor: 100, enableSeekGesture: false,
playerWaveStyle: PlayerWaveStyle(
spacing: 4.w,
waveThickness: 2.w,
fixedWaveColor: Colors.grey[400]!,
liveWaveColor: Colors.red,
seekLineColor: Colors.red,
scaleFactor: 200,
),
),
), ),
), ),
), ),
@ -341,7 +350,10 @@ class MeetingDetailsView extends GetView<MeetingDetailsController> {
child: AudioFileWaveforms( child: AudioFileWaveforms(
size: Size(350.w, 120.w), size: Size(350.w, 120.w),
playerController: controller.playerController, playerController: controller.playerController,
waveformType: WaveformType.fitWidth,
playerWaveStyle: PlayerWaveStyle( playerWaveStyle: PlayerWaveStyle(
spacing: 4.w,
waveThickness: 2.w,
fixedWaveColor: Colors.grey[300]!, fixedWaveColor: Colors.grey[300]!,
liveWaveColor: Colors.red, liveWaveColor: Colors.red,
seekLineColor: Colors.red, seekLineColor: Colors.red,

12
lib/modules/meeting/views/share_bottom_sheet.dart

@ -149,14 +149,12 @@ class _ShareBottomSheetState extends State<ShareBottomSheet> {
EasyLoading.showToast('没有可分享的音频文件'); EasyLoading.showToast('没有可分享的音频文件');
return; return;
} }
try { final result = await SharePlus.instance.share(ShareParams(
await Share.shareXFiles( text: '分享会议音频',
[XFile(_controller.meetingData.value.filepath)], files: [XFile(_controller.meetingData.value.filepath)],
text: '分享会议音频', ));
); if (result.status == ShareResultStatus.success) {
EasyLoading.showToast('分享完成!'); EasyLoading.showToast('分享完成!');
} catch (e) {
EasyLoading.showToast('分享失败: ${e.toString()}');
} }
} }
} }

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

@ -153,6 +153,7 @@ object AgentService : CoroutineScope {
config["openaiApiKey"]?.toString() ?: "", config["openaiApiKey"]?.toString() ?: "",
config["openaiBaseUrl"]?.toString() ?: "", config["openaiBaseUrl"]?.toString() ?: "",
config["openaiModel"]?.toString() ?: "", config["openaiModel"]?.toString() ?: "",
config["openaivisionModel"]?.toString() ?: "",
config["mcpServer"]?.toString() ?: "" config["mcpServer"]?.toString() ?: ""
) )
@ -576,6 +577,7 @@ object AgentService : CoroutineScope {
) { ) {
// 创建带图片的用户消息并处理 // 创建带图片的用户消息并处理
val userMessage = createUserMessageWithImage(text, imageBase64) val userMessage = createUserMessageWithImage(text, imageBase64)
Log.d(TAG, "liwei--------- 图像Mesage: ${userMessage}")
// 图片描述用于存储 // 图片描述用于存储
val displayText = text.ifEmpty { "[图片]" } val displayText = text.ifEmpty { "[图片]" }
processWithChatApiInternal(userMessage, displayText, speakResponse, true) processWithChatApiInternal(userMessage, displayText, speakResponse, true)
@ -599,7 +601,7 @@ object AgentService : CoroutineScope {
) { ) {
// 如果有正在进行的AI流式输出,先停止它 // 如果有正在进行的AI流式输出,先停止它
stopAiStream() stopAiStream()
stopTts()
// 记录新的AI任务 // 记录新的AI任务
currentAiJob = launch { currentAiJob = launch {
try { try {
@ -621,16 +623,16 @@ object AgentService : CoroutineScope {
val messagesWithSystemPrompt = JSONArray() val messagesWithSystemPrompt = JSONArray()
// 先添加系统提示词 // 先添加系统提示词
if (systemPrompt.isNotEmpty()) { if (systemPrompt.isNotEmpty()) {
val location = LocationService.getCachedLocation()?: mapOf( val location = LocationService.getCachedLocation()?: mapOf(
"user_location" to "定位未开启", "user_location" to "定位未开启",
"user_lat" to "定位未开启", "user_lat" to "定位未开启",
"user_lng" to "定位未开启", "user_lng" to "定位未开启",
) )
val _systemPrompt = Utils.fillTemplate(systemPrompt,location) val _systemPrompt = Utils.fillTemplate(systemPrompt,location)
messagesWithSystemPrompt.put(createSystemMessage(_systemPrompt)) messagesWithSystemPrompt.put(createSystemMessage(_systemPrompt))
Log.d(TAG, "liwei--------- 系统提示词: ${_systemPrompt}") Log.d(TAG, "liwei--------- 系统提示词: ${_systemPrompt}")
} }
// 再添加历史消息 // 再添加历史消息
for (i in 0 until historyMessages.length()) { for (i in 0 until historyMessages.length()) {
@ -665,7 +667,7 @@ object AgentService : CoroutineScope {
// 兜底处理,直接添加原消息 // 兜底处理,直接添加原消息
addToHistoryMessages(userMessage) addToHistoryMessages(userMessage)
} }
// Log.e("ChatApiService", "liwei---------sendMessageStream messages:${messagesWithSystemPrompt}")
chatApiService.sendMessageStream( chatApiService.sendMessageStream(
messagesWithSystemPrompt, messagesWithSystemPrompt,
object : StreamCallback { object : StreamCallback {
@ -684,25 +686,21 @@ object AgentService : CoroutineScope {
ttsService?.flushStream() ttsService?.flushStream()
} }
val response = responseBuilder.toString() val response = responseBuilder.toString()
// 发送完整回复,包含是否有图片的标记
val responseData = mutableMapOf<String, Any>(
"text" to response,
"userInput" to displayText
)
if (hasImage) {
responseData["hasImage"] = true
}
sendEvent("assistant_response", responseData)
if (response.isNotEmpty()) { if (response.isNotEmpty()) {
// 发送完整回复,包含是否有图片的标记
val responseData = mutableMapOf<String, Any>(
"text" to response,
"userInput" to displayText
)
if (hasImage) {
responseData["hasImage"] = true
}
sendEvent("assistant_response", responseData)
// 添加AI回复到历史记录 // 添加AI回复到历史记录
addToHistoryMessages(createAssistantMessage(response)) addToHistoryMessages(createAssistantMessage(response))
// 保存聊天记录
saveChatMessage(displayText, response,aiMetadata,userMetadata.toString())
} }
// 保存聊天记录
saveChatMessage(displayText, response,aiMetadata,userMetadata.toString())
// 标记AI流式输出已完成 // 标记AI流式输出已完成
isAiStreaming = false isAiStreaming = false
currentAiJob = null currentAiJob = null
@ -861,14 +859,15 @@ object AgentService : CoroutineScope {
) )
if (userMessageId != -1L) { if (userMessageId != -1L) {
// 保存AI回复 if (assistantMessage.isNotEmpty() ) {
val assistantMessageId = chatStorageHelper.saveMessage( // 保存AI回复
sessionId = sessionId, val assistantMessageId = chatStorageHelper.saveMessage(
message = assistantMessage, sessionId = sessionId,
sender = "assistant", message = assistantMessage,
metadata = aiMetadata sender = "assistant",
) metadata = aiMetadata
)
}
// if (assistantMessageId != -1L) { // if (assistantMessageId != -1L) {
// Log.d(TAG, "聊天记录已保存:用户消息ID=$userMessageId, 助手消息ID=$assistantMessageId") // Log.d(TAG, "聊天记录已保存:用户消息ID=$userMessageId, 助手消息ID=$assistantMessageId")
// } else { // } else {
@ -970,7 +969,7 @@ object AgentService : CoroutineScope {
/** /**
* 创建带图片的用户消息 * 创建带图片的用户消息
*/ */
private fun createUserMessageWithImage(text: String, imageBase64: String): JSONObject { private fun createUserMessageWithImage(text: String, imageUrl: String): JSONObject {
val content = JSONArray().apply { val content = JSONArray().apply {
if (text.isNotEmpty()) { if (text.isNotEmpty()) {
put(JSONObject().apply { put(JSONObject().apply {
@ -981,8 +980,8 @@ object AgentService : CoroutineScope {
put(JSONObject().apply { put(JSONObject().apply {
put("type", "image_url") put("type", "image_url")
put("image_url", JSONObject().apply { put("image_url", JSONObject().apply {
put("url", "data:image/jpeg;base64,$imageBase64") // put("url", "data:image/jpeg;base64,$imageBase64")
put("detail", "auto") put("url", imageUrl)
}) })
}) })
} }
@ -996,7 +995,7 @@ object AgentService : CoroutineScope {
/** /**
* 处理图片输入 * 处理图片输入
*/ */
fun processImageInput(imagePath: String, text: String = "", speakResponse: Boolean = false): Boolean { fun processImageInput(imagePath: String, imageUrl: String,text: String = "", speakResponse: Boolean = false): Boolean {
if (!isInitialized) { if (!isInitialized) {
Log.e(TAG, "服务未初始化") Log.e(TAG, "服务未初始化")
sendEvent("error", mapOf("code" to "NOT_INITIALIZED", "message" to "服务未初始化")) sendEvent("error", mapOf("code" to "NOT_INITIALIZED", "message" to "服务未初始化"))
@ -1019,16 +1018,16 @@ object AgentService : CoroutineScope {
launch { launch {
try { try {
// 将图片转换为Base64格式 // 将图片转换为Base64格式
val imageBase64 = chatApiService.fileToBase64(imagePath) ?: run { // val imageBase64 = chatApiService.fileToBase64(imagePath) ?: run {
sendEvent("error", mapOf( // sendEvent("error", mapOf(
"code" to "IMAGE_CONVERSION_FAILED", // "code" to "IMAGE_CONVERSION_FAILED",
"message" to "图片转换失败" // "message" to "图片转换失败"
)) // ))
return@launch // return@launch
} // }
// 创建带图片的用户消息并处理 // 创建带图片的用户消息并处理
val userMessage = createUserMessageWithImage(text, imageBase64) val userMessage = createUserMessageWithImage(text, imageUrl)
// 图片描述用于存储 // 图片描述用于存储
val displayText = text.ifEmpty { "[图片]" } val displayText = text.ifEmpty { "[图片]" }
@ -1037,7 +1036,7 @@ object AgentService : CoroutineScope {
"status" to "ready", "status" to "ready",
"imagePath" to imagePath "imagePath" to imagePath
)) ))
Log.d(TAG, "liwei--------- 图像Mesage: ${userMessage}")
// 处理消息并传入存储路径 // 处理消息并传入存储路径
processWithChatApiInternal(userMessage, displayText, speakResponse, true, imagePath) processWithChatApiInternal(userMessage, displayText, speakResponse, true, imagePath)
} catch (e: Exception) { } catch (e: Exception) {

8
local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt

@ -57,6 +57,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream
config["openaiApiKey"]?.toString()?.let { intent.putExtra("openaiApiKey", it) } config["openaiApiKey"]?.toString()?.let { intent.putExtra("openaiApiKey", it) }
config["openaiBaseUrl"]?.toString()?.let { intent.putExtra("openaiBaseUrl", it) } config["openaiBaseUrl"]?.toString()?.let { intent.putExtra("openaiBaseUrl", it) }
config["openaiModel"]?.toString()?.let { intent.putExtra("openaiModel", it) } config["openaiModel"]?.toString()?.let { intent.putExtra("openaiModel", it) }
config["openaivisionModel"]?.toString()?.let { intent.putExtra("openaivisionModel", it) }
config["systemPrompt"]?.toString()?.let { intent.putExtra("systemPrompt", it) } config["systemPrompt"]?.toString()?.let { intent.putExtra("systemPrompt", it) }
config["mcpServer"]?.toString()?.let { intent.putExtra("mcpServer", it) } config["mcpServer"]?.toString()?.let { intent.putExtra("mcpServer", it) }
config["volcanoAppId"]?.toString()?.let { intent.putExtra("volcanoAppId", it) } config["volcanoAppId"]?.toString()?.let { intent.putExtra("volcanoAppId", it) }
@ -130,13 +131,18 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream
result.error("INVALID_ARGUMENTS", "图片路径不能为空", null) result.error("INVALID_ARGUMENTS", "图片路径不能为空", null)
return return
} }
val imageUrl = call.argument<String>("imageUrl")
if (imageUrl.isNullOrEmpty()) {
result.error("INVALID_ARGUMENTS", "图片Url不能为空", null)
return
}
val text = call.argument<String>("text") ?: "" val text = call.argument<String>("text") ?: ""
val speakResponse = call.argument<Boolean>("speakResponse") ?: false val speakResponse = call.argument<Boolean>("speakResponse") ?: false
try { try {
val isProcessing = AgentService.processImageInput( val isProcessing = AgentService.processImageInput(
imagePath = imagePath, imagePath = imagePath,
imageUrl = imageUrl,
text = text, text = text,
speakResponse = speakResponse speakResponse = speakResponse
) )

3
local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleMediaService.kt

@ -43,6 +43,7 @@ class BleMediaService : Service(), CoroutineScope {
private const val KEY_OPENAI_API_KEY = "openaiApiKey" private const val KEY_OPENAI_API_KEY = "openaiApiKey"
private const val KEY_OPENAI_BASE_URL = "openaiBaseUrl" private const val KEY_OPENAI_BASE_URL = "openaiBaseUrl"
private const val KEY_OPENAI_MODEL = "openaiModel" private const val KEY_OPENAI_MODEL = "openaiModel"
private const val KEY_OPENAI_VISION_MODEL = "openaivisionModel"
private const val KEY_SYSTEM_PROMPT = "systemPrompt" private const val KEY_SYSTEM_PROMPT = "systemPrompt"
private const val KEY_MCP_SERVER = "mcpServer" private const val KEY_MCP_SERVER = "mcpServer"
private const val KEY_VOLCANO_APP_ID = "volcanoAppId" private const val KEY_VOLCANO_APP_ID = "volcanoAppId"
@ -165,6 +166,7 @@ class BleMediaService : Service(), CoroutineScope {
it.getStringExtra(KEY_OPENAI_API_KEY)?.let { value -> config[KEY_OPENAI_API_KEY] = value } it.getStringExtra(KEY_OPENAI_API_KEY)?.let { value -> config[KEY_OPENAI_API_KEY] = value }
it.getStringExtra(KEY_OPENAI_BASE_URL)?.let { value -> config[KEY_OPENAI_BASE_URL] = value } it.getStringExtra(KEY_OPENAI_BASE_URL)?.let { value -> config[KEY_OPENAI_BASE_URL] = value }
it.getStringExtra(KEY_OPENAI_MODEL)?.let { value -> config[KEY_OPENAI_MODEL] = value } it.getStringExtra(KEY_OPENAI_MODEL)?.let { value -> config[KEY_OPENAI_MODEL] = value }
it.getStringExtra(KEY_OPENAI_VISION_MODEL)?.let { value -> config[KEY_OPENAI_VISION_MODEL] = value }
it.getStringExtra(KEY_SYSTEM_PROMPT)?.let { value -> config[KEY_SYSTEM_PROMPT] = value } it.getStringExtra(KEY_SYSTEM_PROMPT)?.let { value -> config[KEY_SYSTEM_PROMPT] = value }
it.getStringExtra(KEY_MCP_SERVER)?.let { value -> config[KEY_MCP_SERVER] = value } it.getStringExtra(KEY_MCP_SERVER)?.let { value -> config[KEY_MCP_SERVER] = value }
it.getStringExtra(KEY_VOLCANO_APP_ID)?.let { value -> config[KEY_VOLCANO_APP_ID] = value } it.getStringExtra(KEY_VOLCANO_APP_ID)?.let { value -> config[KEY_VOLCANO_APP_ID] = value }
@ -198,6 +200,7 @@ class BleMediaService : Service(), CoroutineScope {
KEY_OPENAI_API_KEY to getConfigString(prefs, KEY_OPENAI_API_KEY, ""), KEY_OPENAI_API_KEY to getConfigString(prefs, KEY_OPENAI_API_KEY, ""),
KEY_OPENAI_BASE_URL to getConfigString(prefs, KEY_OPENAI_BASE_URL, "https://api.openai.com/v1"), KEY_OPENAI_BASE_URL to getConfigString(prefs, KEY_OPENAI_BASE_URL, "https://api.openai.com/v1"),
KEY_OPENAI_MODEL to getConfigString(prefs, KEY_OPENAI_MODEL, "gpt-3.5-turbo"), KEY_OPENAI_MODEL to getConfigString(prefs, KEY_OPENAI_MODEL, "gpt-3.5-turbo"),
KEY_OPENAI_VISION_MODEL to getConfigString(prefs, KEY_OPENAI_VISION_MODEL, "gpt-4-vision-preview"),
KEY_SYSTEM_PROMPT to getConfigString(prefs, KEY_SYSTEM_PROMPT, "gpt-3.5-turbo"), KEY_SYSTEM_PROMPT to getConfigString(prefs, KEY_SYSTEM_PROMPT, "gpt-3.5-turbo"),
KEY_MCP_SERVER to getConfigString(prefs, KEY_MCP_SERVER, ""), KEY_MCP_SERVER to getConfigString(prefs, KEY_MCP_SERVER, ""),
KEY_VOLCANO_APP_ID to getConfigString(prefs, KEY_VOLCANO_APP_ID, ""), KEY_VOLCANO_APP_ID to getConfigString(prefs, KEY_VOLCANO_APP_ID, ""),

3
local_plugins/agent_service/lib/agent_service.dart

@ -326,11 +326,12 @@ class AgentService {
/// [text] 可选的文本描述或问题 /// [text] 可选的文本描述或问题
/// [speakResponse] 是否朗读响应 /// [speakResponse] 是否朗读响应
/// 返回是否成功处理图片 /// 返回是否成功处理图片
static Future<bool> processImageInput(String imagePath, static Future<bool> processImageInput(String imagePath, String imageUrl,
{String text = "", bool speakResponse = false}) async { {String text = "", bool speakResponse = false}) async {
try { try {
final bool result = await _channel.invokeMethod('processImageInput', { final bool result = await _channel.invokeMethod('processImageInput', {
'imagePath': imagePath, 'imagePath': imagePath,
'imageUrl': imageUrl,
'text': text, 'text': text,
'speakResponse': speakResponse, 'speakResponse': speakResponse,
}); });

8
local_plugins/amap_service/android/src/main/kotlin/com/yunqiinnovation/amap_service/AmapServicePlugin.kt

@ -1,7 +1,13 @@
package com.yunqiinnovation.amap_service package com.yunqiinnovation.amap_service
import android.app.Activity import android.app.Activity
import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Build
import android.util.Log
import androidx.core.content.ContextCompat.registerReceiver
import com.amap.api.navi.AMapNaviView import com.amap.api.navi.AMapNaviView
import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.EventChannel import io.flutter.plugin.common.EventChannel
@ -98,4 +104,6 @@ class AmapServicePlugin: FlutterPlugin, MethodCallHandler,EventChannel.StreamHan
} }
} }

37
local_plugins/amap_service/android/src/main/kotlin/com/yunqiinnovation/amap_service/NavigationActivity.kt

@ -1,6 +1,10 @@
package com.yunqiinnovation.amap_service package com.yunqiinnovation.amap_service
import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
@ -23,7 +27,7 @@ import com.amap.api.navi.model.AimLessModeCongestionInfo
import com.amap.api.navi.model.AimLessModeStat import com.amap.api.navi.model.AimLessModeStat
import com.amap.api.navi.model.NaviInfo import com.amap.api.navi.model.NaviInfo
import com.amap.api.navi.model.NaviLatLng import com.amap.api.navi.model.NaviLatLng
import com.yunqiinnovation.amap_service.NavigationService.Companion.sharedNavi
class NavigationActivity : AppCompatActivity(), AMapNaviListener, AMapNaviViewListener { class NavigationActivity : AppCompatActivity(), AMapNaviListener, AMapNaviViewListener {
private lateinit var naviView: AMapNaviView private lateinit var naviView: AMapNaviView
@ -32,6 +36,7 @@ class NavigationActivity : AppCompatActivity(), AMapNaviListener, AMapNaviViewLi
private var startpos:String = ""; private var startpos:String = "";
private var endpos:String = ""; private var endpos:String = "";
private var ntype:String = ""; private var ntype:String = "";
private var isReceiverRegistered = false
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_navigation) setContentView(R.layout.activity_navigation)
@ -60,7 +65,31 @@ class NavigationActivity : AppCompatActivity(), AMapNaviListener, AMapNaviViewLi
} }
navi?.addAMapNaviListener(this); navi?.addAMapNaviListener(this);
// 绑定当前 activity 到导航状态
// 注册广播监听器
val filter = IntentFilter().apply {
addAction("com.yunqiinnovation.amap_service.EXIT_NAVIGATION")
}
// Android 14+ 需要指定 flag
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
registerReceiver(controlReceiver, filter, Context.RECEIVER_NOT_EXPORTED)
} else {
registerReceiver(controlReceiver, filter)
}
isReceiverRegistered = true
}
private val controlReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
Log.i("NavigationActivity", "接受到通知事件 ${intent?.action}")
when (intent?.action) {
"com.yunqiinnovation.amap_service.EXIT_NAVIGATION" -> {
Log.d("NavigationService", "收到退出导航广播,准备停止服务")
finish()
}
}
}
} }
override fun onResume() { override fun onResume() {
@ -80,6 +109,10 @@ class NavigationActivity : AppCompatActivity(), AMapNaviListener, AMapNaviViewLi
NavigationServiceHelper.startNavigation(context,"stop") NavigationServiceHelper.startNavigation(context,"stop")
naviView.onDestroy() naviView.onDestroy()
super.onDestroy() super.onDestroy()
if (isReceiverRegistered) {
unregisterReceiver(controlReceiver)
isReceiverRegistered = false
}
Log.i("NavigationActivity", "liwei------------------- onDestroy!") Log.i("NavigationActivity", "liwei------------------- onDestroy!")
} }

40
local_plugins/amap_service/android/src/main/kotlin/com/yunqiinnovation/amap_service/NavigationService.kt

@ -3,8 +3,10 @@ package com.yunqiinnovation.amap_service
import android.app.NotificationChannel import android.app.NotificationChannel
import android.app.NotificationManager import android.app.NotificationManager
import android.app.Service import android.app.Service
import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentFilter
import android.graphics.Bitmap import android.graphics.Bitmap
import android.os.Build import android.os.Build
import android.os.IBinder import android.os.IBinder
@ -52,11 +54,24 @@ class NavigationService: Service(),AMapNaviListener {
fun isServiceRunning() = isRunning.get() fun isServiceRunning() = isRunning.get()
} }
private var isReceiverRegistered = false
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
instance = this instance = this
startForegroundNotification() startForegroundNotification()
// 注册广播监听器
val filter = IntentFilter().apply {
addAction("com.yunqiinnovation.amap_service.EXIT_NAVIGATION")
}
// Android 14+ 需要指定 flag
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
registerReceiver(controlReceiver, filter, Context.RECEIVER_NOT_EXPORTED)
} else {
registerReceiver(controlReceiver, filter)
}
isReceiverRegistered = false
} }
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
@ -84,7 +99,17 @@ class NavigationService: Service(),AMapNaviListener {
} }
return START_STICKY return START_STICKY
} }
private val controlReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
Log.i(TAG, "接受到通知事件 ${intent?.action}")
when (intent?.action) {
"com.yunqiinnovation.amap_service.EXIT_NAVIGATION" -> {
Log.d("NavigationService", "收到退出导航广播,准备停止服务")
stopNavigation();
}
}
}
}
private fun initializeAMapNavi() { private fun initializeAMapNavi() {
try { try {
sharedNavi = AMapNavi.getInstance(applicationContext).apply { sharedNavi = AMapNavi.getInstance(applicationContext).apply {
@ -399,4 +424,15 @@ class NavigationService: Service(),AMapNaviListener {
Log.e(TAG, "onGpsSignalWeak! $p0") Log.e(TAG, "onGpsSignalWeak! $p0")
// TODO("Not yet implemented") // TODO("Not yet implemented")
} }
override fun onDestroy() {
if (isReceiverRegistered) {
unregisterReceiver(controlReceiver)
isReceiverRegistered = false
}
stopForeground(true)
isRunning.set(false)
super.onDestroy()
}
} }

2
local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureTtsHelper.kt

@ -395,6 +395,8 @@ class AzureTtsHelper(private val context: Context) : ITtsService {
.replace("&", "&amp;") .replace("&", "&amp;")
.replace("<", "&lt;") .replace("<", "&lt;")
.replace(">", "&gt;") .replace(">", "&gt;")
.replace("#", "&gt;")
.replace("*", "&gt;")
.replace(Regex("[😀-🟿]+"), "") // 简化表情符号移除 .replace(Regex("[😀-🟿]+"), "") // 简化表情符号移除
.trim() .trim()

67
local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiPlugin.kt

@ -37,30 +37,30 @@ class ChatApiPlugin : FlutterPlugin, MethodCallHandler, EventChannel.StreamHandl
chatApiService = ChatApiService(flutterPluginBinding.applicationContext) chatApiService = ChatApiService(flutterPluginBinding.applicationContext)
// 设置流式回调 // 设置流式回调
chatApiService?.setStreamCallback(object : StreamCallback { // chatApiService?.setStreamCallback(object : StreamCallback {
override fun onToken(token: String) { // override fun onToken(token: String) {
channel.invokeMethod("onToken", token) // channel.invokeMethod("onToken", token)
} // }
//
override fun onComplete() { // override fun onComplete() {
channel.invokeMethod("onComplete", null) // channel.invokeMethod("onComplete", null)
} // }
//
override fun onError(error: Exception) { // override fun onError(error: Exception) {
channel.invokeMethod("onError", error.message) // channel.invokeMethod("onError", error.message)
} // }
//
override fun onFunctionCall(functionCall: org.json.JSONObject) { // override fun onFunctionCall(functionCall: org.json.JSONObject) {
channel.invokeMethod("onFunctionCall", functionCall.toString()) // channel.invokeMethod("onFunctionCall", functionCall.toString())
} // }
//
override fun onFunctionCallResult(functionCall: org.json.JSONObject, functionCallResult: org.json.JSONObject) { // override fun onFunctionCallResult(functionCall: org.json.JSONObject, functionCallResult: org.json.JSONObject) {
channel.invokeMethod("onFunctionCallResult", mapOf( // channel.invokeMethod("onFunctionCallResult", mapOf(
"functionCall" to functionCall.toString(), // "functionCall" to functionCall.toString(),
"functionCallResult" to functionCallResult.toString() // "functionCallResult" to functionCallResult.toString()
)) // ))
} // }
}) // })
} }
override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
@ -80,18 +80,19 @@ class ChatApiPlugin : FlutterPlugin, MethodCallHandler, EventChannel.StreamHandl
val apiKey = call.argument<String>("apiKey") ?: "" val apiKey = call.argument<String>("apiKey") ?: ""
val baseUrl = call.argument<String>("baseUrl") ?: "" val baseUrl = call.argument<String>("baseUrl") ?: ""
val model = call.argument<String>("model") ?: "" val model = call.argument<String>("model") ?: ""
val modelmedia = call.argument<String>("modelmedia") ?: ""
val mcpServer = call.argument<String>("mcpServer") ?: "" val mcpServer = call.argument<String>("mcpServer") ?: ""
val success = chatApiService?.initialize(apiKey, baseUrl, model, mcpServer) ?: false val success = chatApiService?.initialize(apiKey, baseUrl, model,modelmedia, mcpServer) ?: false
result.success(success) result.success(success)
} }
"chatCompletionStream" -> { // "chatCompletionStream" -> {
val messages = call.argument<List<Map<String, Any>>>("messages") ?: emptyList() // val messages = call.argument<List<Map<String, Any>>>("messages") ?: emptyList()
val tool = call.argument<Boolean>("tool") ?: false // val tool = call.argument<Boolean>("tool") ?: false
pluginScope.launch { // pluginScope.launch {
chatApiService?.chatCompletionStream(messages, tool) // chatApiService?.chatCompletionStream(messages, tool)
} // }
result.success(true) // result.success(true)
} // }
"cancelChatStream" -> { "cancelChatStream" -> {
chatApiService?.cancelChatStream() chatApiService?.cancelChatStream()
result.success(true) result.success(true)

165
local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiService.kt

@ -23,6 +23,7 @@ import android.util.Log
import org.json.JSONObject import org.json.JSONObject
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import java.util.UUID
/** /**
* ChatAPI服务异常 * ChatAPI服务异常
@ -109,10 +110,12 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
// 流式请求相关 // 流式请求相关
private var currentStreamJob: Job? = null private var currentStreamJob: Job? = null
private var streamCallback: StreamCallback? = null // private var streamCallback: StreamCallback? = null
private var currentMessages: List<Map<String, Any>> = emptyList() private var currentMessages: List<Map<String, Any>> = emptyList()
private var toolCalls: MutableMap<Int, ToolCallInfo> = mutableMapOf() private var toolCalls: MutableMap<Int, ToolCallInfo> = mutableMapOf()
private var isCanceled = false private var sessions: MutableMap<String, StreamCallback?> = mutableMapOf()
private var currSessionId = "" //
// private var isCanceled = false
@ -124,14 +127,14 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
/** /**
* 设置流式回调 * 设置流式回调
*/ */
fun setStreamCallback(callback: StreamCallback) { // fun setStreamCallback(callback: StreamCallback) {
streamCallback = callback // streamCallback = callback
} // }
/** /**
* 初始化ChatAPI服务 * 初始化ChatAPI服务
*/ */
fun initialize(apiKey: String, baseUrl: String, model: String, mcpServer: String): Boolean { fun initialize(apiKey: String, baseUrl: String, model: String, visionModel: String, mcpServer: String): Boolean {
this.apiKey = apiKey this.apiKey = apiKey
if (baseUrl.isNotEmpty()) { if (baseUrl.isNotEmpty()) {
this.baseUrl = baseUrl this.baseUrl = baseUrl
@ -139,7 +142,9 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
if (model.isNotEmpty()) { if (model.isNotEmpty()) {
this.model = model this.model = model
} }
if (visionModel.isNotEmpty()) {
this.visionModel = visionModel
}
// 处理 baseUrl:移除末尾的 /chat/completions(如果存在) // 处理 baseUrl:移除末尾的 /chat/completions(如果存在)
// 因为 openai-kotlin 会自动拼接 /chat/completions // 因为 openai-kotlin 会自动拼接 /chat/completions
var processedBaseUrl = if (this.baseUrl.endsWith("/chat/completions")) { var processedBaseUrl = if (this.baseUrl.endsWith("/chat/completions")) {
@ -258,30 +263,30 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
*/ */
fun sendMessageStream(messages: org.json.JSONArray, callback: StreamCallback) { fun sendMessageStream(messages: org.json.JSONArray, callback: StreamCallback) {
// 设置回调 // 设置回调
setStreamCallback(callback) // setStreamCallback(callback)
// 转换JSONArray为List<Map<String, Any>> // 转换JSONArray为List<Map<String, Any>>
val messageList = convertJsonArrayToList(messages) val messageList = convertJsonArrayToList(messages)
currSessionId = UUID.randomUUID().toString()
sessions[currSessionId] = callback
// 调用原始方法 // 调用原始方法
sendMessageStream(messageList) sendMessageStream(currSessionId,messageList)
} }
/** /**
* 发送消息(流式输出) * 发送消息(流式输出)
*/ */
fun sendMessageStream(messages: List<Map<String, Any>>) { fun sendMessageStream(sessionid:String,messages: List<Map<String, Any>>) {
if (!isInitialized || apiKey.isEmpty() || openAI == null) { if (!isInitialized || apiKey.isEmpty() || openAI == null) {
Log.e("ChatApiService", "ChatAPI服务未初始化,无法发送消息") Log.e("ChatApiService", "ChatAPI服务未初始化,无法发送消息")
streamCallback?.onError(ChatApiException("ChatAPI服务未初始化")) sessions[sessionid]?.onError(ChatApiException("ChatAPI服务未初始化"))
return return
} }
// 重置状态 // 重置状态
isCanceled = false // isCanceled = false
currentMessages = messages currentMessages = messages
toolCalls.clear() toolCalls.clear()
// 检查是否包含图片,决定使用哪个模型 // 检查是否包含图片,决定使用哪个模型
val currentModel = if (containsImage(messages)) visionModel else model val currentModel = if (containsImage(messages)) visionModel else model
@ -323,7 +328,7 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
Log.e("ChatApiService", "openAI对象为null") Log.e("ChatApiService", "openAI对象为null")
throw IllegalStateException("OpenAI客户端未初始化") throw IllegalStateException("OpenAI客户端未初始化")
} }
// Log.e("ChatApiService", "liwei---------,发送AI请求 model:${currentModel} messages:${chatMessages}")
val flow = openAI!!.chatCompletions(chatCompletionRequest) val flow = openAI!!.chatCompletions(chatCompletionRequest)
flow flow
} catch (e: Exception) { } catch (e: Exception) {
@ -332,7 +337,9 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
} }
chatsFlow.collect { result -> chatsFlow.collect { result ->
if (isCanceled) return@collect if (currSessionId != sessionid) {
return@collect
}
val choice = result.choices.firstOrNull() val choice = result.choices.firstOrNull()
if (choice == null) { if (choice == null) {
@ -348,7 +355,7 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
// 处理普通文本内容 // 处理普通文本内容
delta.content?.let { content -> delta.content?.let { content ->
streamCallback?.onToken(content) sessions[sessionid]?.onToken(content)
} }
// 收集工具调用信息 // 收集工具调用信息
@ -389,18 +396,20 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
} }
} }
if (!isCanceled) { if (currSessionId == sessionid) {
// 检查是否有工具调用需要处理 // 检查是否有工具调用需要处理
val hasToolCalls = processToolCalls() val hasToolCalls = processToolCalls(sessionid)
if (!hasToolCalls) { if (!hasToolCalls) {
streamCallback?.onComplete() sessions[sessionid]?.onComplete()
} }
}else{
sessions[sessionid]?.onComplete()
} }
} catch (e: Exception) { } catch (e: Exception) {
if (!isCanceled) { // if (currSessionId == sessionid) {
streamCallback?.onError(ChatApiException("流式请求失败: ${e.message}")) sessions[sessionid]?.onError(ChatApiException("流式请求失败: ${e.message}"))
} // }
} }
} }
} }
@ -408,7 +417,7 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
/** /**
* 处理工具调用 * 处理工具调用
*/ */
private suspend fun processToolCalls(): Boolean { private suspend fun processToolCalls(sessionid: String): Boolean {
val firstToolCall = try { val firstToolCall = try {
toolCalls.values.firstOrNull { it.isValid() } toolCalls.values.firstOrNull { it.isValid() }
} catch (e: Exception) { } catch (e: Exception) {
@ -430,11 +439,11 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
return false return false
} }
// 通知上层工具调用事件 // 通知上层工具调用事件
streamCallback?.onFunctionCall(convertMapToJsonObject(functionCall)) sessions[sessionid]?.onFunctionCall(convertMapToJsonObject(functionCall))
// 在后台队列处理工具调用 // 在后台队列处理工具调用
launch { launch {
try { try {
if (!isCanceled) { if (currSessionId == sessionid) {
// 通过MCP客户端处理工具调用 // 通过MCP客户端处理工具调用
val functionName = firstToolCall.name val functionName = firstToolCall.name
val argumentsJson = firstToolCall.arguments val argumentsJson = firstToolCall.arguments
@ -471,29 +480,35 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
// 工具不存在 // 工具不存在
mapOf("context" to "Tool not found: $functionName") mapOf("context" to "Tool not found: $functionName")
} }
if (!isCanceled) { if (currSessionId == sessionid) {
// 处理结果 // 处理结果
streamCallback?.onFunctionCallResult( sessions[sessionid]?.onFunctionCallResult(
convertMapToJsonObject(functionCall), convertMapToJsonObject(functionCall),
convertMapToJsonObject(result) convertMapToJsonObject(result)
) )
// 将结果发送回OpenAI继续对话 // 将结果发送回OpenAI继续对话
sendFunctionCallResultInternal( sendFunctionCallResultInternal(
sessionid= sessionid,
messages = currentMessages, messages = currentMessages,
functionCall = functionCall, functionCall = functionCall,
functionResult = gson.toJson(result) functionResult = gson.toJson(result)
) )
}else{
sessions[sessionid]?.onComplete()
} }
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e("ChatApiService", "工具调用处理过程中出错: ${e.message}", e) Log.e("ChatApiService", "工具调用处理过程中出错: ${e.message}", e)
if (!isCanceled) { if (currSessionId == sessionid) {
val errorMessage = "工具调用处理失败: ${e.message}" val errorMessage = "工具调用处理失败: ${e.message}"
sendFunctionCallResultInternal( sendFunctionCallResultInternal(
sessionid= sessionid,
messages = currentMessages, messages = currentMessages,
functionCall = functionCall, functionCall = functionCall,
functionResult = errorMessage functionResult = errorMessage
) )
}else{
sessions[sessionid]?.onComplete()
} }
} }
} }
@ -505,11 +520,12 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
* 发送函数调用结果(内部方法) * 发送函数调用结果(内部方法)
*/ */
private suspend fun sendFunctionCallResultInternal( private suspend fun sendFunctionCallResultInternal(
sessionid: String,
messages: List<Map<String, Any>>, messages: List<Map<String, Any>>,
functionCall: Map<String, Any>, functionCall: Map<String, Any>,
functionResult: String functionResult: String
) { ) {
if (isCanceled) return if (currSessionId != sessionid) return
val fullMessages = messages.toMutableList() val fullMessages = messages.toMutableList()
@ -542,14 +558,16 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
) )
// 重新发起对话,让AI基于工具调用结果生成回复 // 重新发起对话,让AI基于工具调用结果生成回复
sendMessageStream(fullMessages) sendMessageStream(sessionid,fullMessages)
} }
/** /**
* 取消当前流式请求 * 取消当前流式请求
*/ */
fun cancelCurrentStream(): Boolean { fun cancelCurrentStream(): Boolean {
isCanceled = true sessions[currSessionId]?.onComplete()
sessions.remove(currSessionId)
currSessionId = ""
currentStreamJob?.cancel() currentStreamJob?.cancel()
currentStreamJob = null currentStreamJob = null
return true return true
@ -778,9 +796,9 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
* 聊天完成流式接口 * 聊天完成流式接口
* 与iOS版本保持一致的接口 * 与iOS版本保持一致的接口
*/ */
fun chatCompletionStream(messages: List<Map<String, Any>>, tool: Boolean = false) { fun chatCompletionStream(sessionid: String,messages: List<Map<String, Any>>, tool: Boolean = false) {
// 直接调用sendMessageStream,因为该方法已经处理了工具调用 // 直接调用sendMessageStream,因为该方法已经处理了工具调用
sendMessageStream(messages) sendMessageStream(sessionid,messages)
} }
// MARK: - 工具方法 // MARK: - 工具方法
@ -893,27 +911,70 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
} }
is List<*> -> { is List<*> -> {
// 对于多模态内容,提取文本部分(简化处理) // 对于多模态内容,提取文本部分(简化处理)
var textContent = "" val contentParts = mutableListOf<ContentPart>()
content.forEach { item -> content.forEach { item ->
if (item is Map<*, *>) { if (item is Map<*, *>) {
val type = item["type"] as? String val type = item["type"] as? String
if (type == "text") { when (type) {
val text = item["text"] as? String ?: "" "text" -> {
textContent += text val text = item["text"] as? String ?: ""
contentParts.add(TextPart(text))
}
"image_url" -> {
val imageUrlMap = item["image_url"] as? Map<*, *>
val url = imageUrlMap?.get("url") as? String ?: ""
val detail = imageUrlMap?.get("detail") as? String ?: "auto"
if (url.isNotEmpty()) {
contentParts.add(ImagePart(url, detail))
}
} // 处理其他可能的内容类型
else -> {
// 记录未知类型,便于调试
Log.d("MessageParser", "Unknown content type: $type")
}
} }
} else {
// 非Map类型的内容,记录日志
Log.d("MessageParser", "Item is not a Map: $item")
} }
} }
val finalContent = if (textContent.isEmpty()) "多媒体内容" else textContent // 创建消息对象
if (contentParts.isNotEmpty()) {
val chatMessage = when (chatRole) { val chatMessage = when (chatRole) {
Role.System -> ChatMessage.System(finalContent) Role.System -> {
Role.User -> ChatMessage.User(finalContent) // 系统消息简化显示,将图片替换为[图片]
Role.Assistant -> ChatMessage.Assistant(finalContent) val displayText = contentParts.joinToString { part ->
else -> ChatMessage.User(finalContent) when (part) {
is TextPart -> part.text // 使用TextPart
is ImagePart -> "[图片]" // 使用ImagePart
}
}
ChatMessage.System(displayText)
}
Role.User -> ChatMessage.User(contentParts)
Role.Assistant -> {
// 将多模态内容转换为ListContent
val listContent = ListContent(contentParts)
// 提取文本内容作为Assistant消息的content
val textContent = contentParts
.filterIsInstance<TextPart>()
.joinToString(separator = "") { it.text }
// 调用Assistant函数,传递文本内容
ChatMessage.Assistant(
content = textContent.ifEmpty { null },
toolCalls = null // 如果有工具调用,需要在这里添加
)
}
else -> ChatMessage.User(contentParts)
}
chatMessages.add(chatMessage)
} else {
// 内容为空时的处理
Log.d("MessageParser", "No valid content parts found")
} }
chatMessages.add(chatMessage)
} }
} }
} }
@ -1035,13 +1096,13 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
*/ */
private fun convertJsonArrayToList(jsonArray: org.json.JSONArray): List<Map<String, Any>> { private fun convertJsonArrayToList(jsonArray: org.json.JSONArray): List<Map<String, Any>> {
val result = mutableListOf<Map<String, Any>>() val result = mutableListOf<Map<String, Any>>()
// Log.e("ChatApiService", "convertJsonArrayToList 1:${jsonArray}")
for (i in 0 until jsonArray.length()) { for (i in 0 until jsonArray.length()) {
val jsonObject = jsonArray.optJSONObject(i) ?: continue val jsonObject = jsonArray.optJSONObject(i) ?: continue
val map = convertJsonObjectToMap(jsonObject) val map = convertJsonObjectToMap(jsonObject)
result.add(map) result.add(map)
} }
// Log.e("ChatApiService", "convertJsonArrayToList 2:${result}")
return result return result
} }
@ -1123,9 +1184,9 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
} }
// 通知Flutter端处理函数调用 // 通知Flutter端处理函数调用
mainHandler.post { // mainHandler.post {
streamCallback?.onFunctionCall(functionCall) // streamCallback?.onFunctionCall(functionCall)
} // }
// 返回一个标记,表示函数已被调用 // 返回一个标记,表示函数已被调用
return "Function '$functionName' called with arguments: $arguments" return "Function '$functionName' called with arguments: $arguments"

53
local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/SystemFunctionHandler.kt

@ -163,7 +163,17 @@ class SystemFunctionHandler(private val context: Context? = null) {
), ),
handler = MediaPauseHandler(context) handler = MediaPauseHandler(context)
) )
// 注册媒体暂停功能
client.registerLocalFunction(
name = "media_close",
description = "关闭媒体播放",
parameters = mapOf(
"type" to "object",
"properties" to emptyMap<String, Any>(),
"required" to emptyList<String>()
),
handler = MediaCloseHandler(context)
)
// 注册媒体上一首功能 // 注册媒体上一首功能
client.registerLocalFunction( client.registerLocalFunction(
name = "media_previous", name = "media_previous",
@ -199,6 +209,32 @@ class SystemFunctionHandler(private val context: Context? = null) {
), ),
handler = OpenRecorderHandler(context) handler = OpenRecorderHandler(context)
) )
// 注册退出交互函数
client.registerLocalFunction(
name = "exit_navigation",
description = "结束当前导航服务",
parameters = mapOf(
"type" to "object",
"properties" to emptyMap<String, Any>(),
"required" to emptyList<String>()
),
handler = ExitNavigationHandler(context)
)
}
}
/**
* 退出导航服务
*/
private class ExitNavigationHandler(private val context: Context?) : FunctionHandler {
override suspend fun handle(arguments: Map<String, Any>): String {
// 发送广播通知退出交互
val intent = Intent("com.yunqiinnovation.amap_service.EXIT_NAVIGATION").apply {
setPackage(context?.packageName) // 设置包名使其成为显式 Intent
}
context?.sendBroadcast(intent)
return "{\"result\": \"已结束当前导航服务\"}"
} }
} }
@ -417,7 +453,20 @@ private class MediaPauseHandler(private val context: Context?) : FunctionHandler
return "{\"result\": \"已暂停媒体播放\"}" return "{\"result\": \"已暂停媒体播放\"}"
} }
} }
/**
* 媒体暂停处理器
*/
private class MediaCloseHandler(private val context: Context?) : FunctionHandler {
override suspend fun handle(arguments: Map<String, Any>): String {
Log.d("MediaPauseHandler", "📤 正在发送广播 context = $context")
// 发送媒体暂停广播
val intent = Intent("com.yunqiinnovation.music_service.MEDIA_CLOSE").apply {
setPackage(context?.packageName) // 设置包名使其成为显式 Intent
}
context?.sendBroadcast(intent)
return "{\"result\": \"已关闭媒体播放\"}"
}
}
/** /**
* 媒体上一首处理器 * 媒体上一首处理器
*/ */

6
local_plugins/chat_api/ios/chat_api/Sources/chat_api/ChatApiService.swift

@ -184,9 +184,9 @@ public class ChatApiService: NSObject {
// 检查是否包含图片,决定使用哪个模型 // 检查是否包含图片,决定使用哪个模型
var currentModel = model var currentModel = model
if containsImage(in: messages) { // if containsImage(in: messages) {
currentModel = visionModel // currentModel = visionModel
} // }
// 转换消息格式 // 转换消息格式
let chatMessages = try convertToChatMessages(messages) let chatMessages = try convertToChatMessages(messages)

16
local_plugins/location_service/android/src/main/kotlin/com/yunqiinnovation/location_service/LocationService.kt

@ -15,32 +15,32 @@ object LocationService {
fun initialize(context: Context) { fun initialize(context: Context) {
if (initialized) return if (initialized) return
Log.e(TAG, "liwei--------- 初始化高德定位服务 1") // Log.e(TAG, "liwei--------- 初始化高德定位服务 1")
try { try {
AMapLocationClient.updatePrivacyShow(context, true, true) AMapLocationClient.updatePrivacyShow(context, true, true)
AMapLocationClient.updatePrivacyAgree(context, true) AMapLocationClient.updatePrivacyAgree(context, true)
Log.e(TAG, "liwei--------- 初始化高德定位服务 2") // Log.e(TAG, "liwei--------- 初始化高德定位服务 2")
locationClient = AMapLocationClient(context.applicationContext) locationClient = AMapLocationClient(context.applicationContext)
val option = AMapLocationClientOption().apply { val option = AMapLocationClientOption().apply {
locationMode = AMapLocationClientOption.AMapLocationMode.Hight_Accuracy locationMode = AMapLocationClientOption.AMapLocationMode.Hight_Accuracy
isOnceLocation = false isOnceLocation = false
interval = 5000L interval = 30000L
} }
Log.e(TAG, "liwei--------- 初始化高德定位服务 3") // Log.e(TAG, "liwei--------- 初始化高德定位服务 3")
locationClient?.setLocationOption(option) locationClient?.setLocationOption(option)
Log.e(TAG, "liwei--------- 初始化高德定位服务 4") // Log.e(TAG, "liwei--------- 初始化高德定位服务 4")
locationClient?.setLocationListener { location -> locationClient?.setLocationListener { location ->
if (location != null && location.errorCode == 0) { if (location != null && location.errorCode == 0) {
currentLocation = location currentLocation = location
Log.d(TAG, "liwei---------定位更新: ${location.latitude}, ${location.longitude}") // Log.d(TAG, "liwei---------定位更新: ${location.latitude}, ${location.longitude}")
} else { } else {
Log.w(TAG, "liwei---------定位失败: ${location?.errorCode} - ${location?.errorInfo}") Log.w(TAG, "liwei---------定位失败: ${location?.errorCode} - ${location?.errorInfo}")
} }
} }
Log.e(TAG, "liwei--------- 初始化高德定位服务 5") // Log.e(TAG, "liwei--------- 初始化高德定位服务 5")
locationClient?.startLocation() locationClient?.startLocation()
Log.e(TAG, "liwei--------- 初始化高德定位服务 6") // Log.e(TAG, "liwei--------- 初始化高德定位服务 6")
initialized = true initialized = true
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "高德定位初始化失败: ${e.message}", e) Log.e(TAG, "高德定位初始化失败: ${e.message}", e)

5
local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt

@ -60,6 +60,7 @@ class MusicService : MediaBrowserServiceCompat() {
val filter = IntentFilter().apply { val filter = IntentFilter().apply {
addAction("com.yunqiinnovation.music_service.MEDIA_PLAY") addAction("com.yunqiinnovation.music_service.MEDIA_PLAY")
addAction("com.yunqiinnovation.music_service.MEDIA_PAUSE") addAction("com.yunqiinnovation.music_service.MEDIA_PAUSE")
addAction("com.yunqiinnovation.music_service.MEDIA_CLOSE")
addAction("com.yunqiinnovation.music_service.MEDIA_PREVIOUS") addAction("com.yunqiinnovation.music_service.MEDIA_PREVIOUS")
addAction("com.yunqiinnovation.music_service.MEDIA_NEXT") addAction("com.yunqiinnovation.music_service.MEDIA_NEXT")
} }
@ -132,6 +133,10 @@ class MusicService : MediaBrowserServiceCompat() {
Log.i(TAG, "收到 MEDIA_PLAY 广播") Log.i(TAG, "收到 MEDIA_PLAY 广播")
resume() resume()
} }
"com.yunqiinnovation.music_service.MEDIA_CLOSE" -> {
Log.i(TAG, "收到 MEDIA_CLOSE 广播")
stopSelf()
}
"com.yunqiinnovation.music_service.MEDIA_PREVIOUS" -> { "com.yunqiinnovation.music_service.MEDIA_PREVIOUS" -> {
Log.i(TAG, "收到 MEDIA_PREVIOUS 广播") Log.i(TAG, "收到 MEDIA_PREVIOUS 广播")
playPrevious() playPrevious()

2
local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicServiceStarter.kt

@ -34,7 +34,7 @@ object MusicServiceStarter {
Log.e("MusicServiceStarter", "liwei------------------- Failed to start service", e) Log.e("MusicServiceStarter", "liwei------------------- Failed to start service", e)
false false
} }
Log.d(" MusicServiceStarter", "liwei------------------- Service start success: $success") Log.d(" MusicServiceStarter", "Service start success: $success")
} }
//启动播放列表 //启动播放列表

Loading…
Cancel
Save