Browse Source

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

weicu
tanlongsheng 1 year ago
parent
commit
97b6d7f69f
  1. 3
      lib/data/services/ble_manager.dart
  2. 43
      lib/data/services/chat_history_service.dart
  3. 7
      lib/modules/agent/controllers/agent_controller.dart
  4. 95
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt
  5. 8
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt
  6. 3
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleMediaService.kt
  7. 3
      local_plugins/agent_service/lib/agent_service.dart
  8. 8
      local_plugins/amap_service/android/src/main/kotlin/com/yunqiinnovation/amap_service/AmapServicePlugin.kt
  9. 37
      local_plugins/amap_service/android/src/main/kotlin/com/yunqiinnovation/amap_service/NavigationActivity.kt
  10. 40
      local_plugins/amap_service/android/src/main/kotlin/com/yunqiinnovation/amap_service/NavigationService.kt
  11. 2
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureTtsHelper.kt
  12. 67
      local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiPlugin.kt
  13. 165
      local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiService.kt
  14. 53
      local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/SystemFunctionHandler.kt
  15. 6
      local_plugins/chat_api/ios/chat_api/Sources/chat_api/ChatApiService.swift
  16. 16
      local_plugins/location_service/android/src/main/kotlin/com/yunqiinnovation/location_service/LocationService.kt
  17. 5
      local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt
  18. 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 =
AppConfig.env('OPENAI_BASE_URL') ?? 'https://api.openai.com/v1';
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 =
AppConfig.env('OPENAI_AGENT_ID') ?? 'personal_assistant';
final systemPrompt = AppConfig.agentsystemPrompt(openaiagentid);
@ -515,6 +517,7 @@ class BleManager extends GetxService {
'openaiApiKey': openaiApiKey,
'openaiBaseUrl': openaiBaseUrl,
'openaiModel': openaiModel,
'openaivisionModel': openaiModelMedia,
'systemPrompt': systemPrompt,
'mcpServer': jsonEncode(jsonData),
'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 {
// 最大历史记录数量
static const int _maxHistoryCount = 200; // 每个对话最多保存的消息数
@override
void onInit() {
super.onInit();
Logger.info('ChatHistoryService 已初始化');
}
/// 保存聊天记录
///
///
/// [agentId] 代理ID
/// [messages] 消息列表
Future<void> saveHistory(String agentId, List<Message> messages) async {
try {
if (messages.isEmpty) return;
// 获取最新的消息
final latestMessage = messages.last;
// 如果是加载中的消息则跳过
if (latestMessage.isIntermediate) return;
// 直接插入最新的消息到数据库
await ChatStorage.saveMessage(
sessionId: agentId,
message: latestMessage.content,
sender: latestMessage.role,
);
// 检查并清理过多的历史记录
await _cleanupOldMessages(agentId);
} catch (e) {
Logger.error('保存聊天记录失败', e.toString());
}
}
/// 清理旧消息,保持每个会话的消息数量不超过最大限制
Future<void> _cleanupOldMessages(String agentId) async {
try {
final pagedResult = await ChatStorage.getMessages(
sessionId: agentId,
page: 1,
pageSize: 1, // 只获取总数信息
pageSize: 1, // 只获取总数信息
);
// 如果消息总数超过最大限制,删除最早的消息
if (pagedResult.totalCount > _maxHistoryCount) {
final messagesToDelete = pagedResult.totalCount - _maxHistoryCount;
@ -61,13 +61,13 @@ class ChatHistoryService extends GetxService {
page: 1,
pageSize: messagesToDelete,
);
// 构建要删除的消息ID列表
final List<int> messageIds = oldestMessages.messages
.where((m) => m.id != null)
.map((m) => m.id!)
.toList();
// 删除这些消息
if (messageIds.isNotEmpty) {
await ChatStorage.deleteMessages(messageIds: messageIds);
@ -77,9 +77,9 @@ class ChatHistoryService extends GetxService {
Logger.error('清理旧消息失败', e);
}
}
/// 加载聊天记录
///
///
/// [agentId] 代理ID
/// 返回消息列表,如果没有找到或解析错误则返回空列表
Future<List<Message>> loadHistory(String agentId) async {
@ -89,29 +89,30 @@ class ChatHistoryService extends GetxService {
page: 1,
pageSize: _maxHistoryCount,
);
if (pagedResult.messages.isEmpty) {
return [];
}
// 将ChatStorage.ChatMessage转换为Message
final messages = pagedResult.messages.map((chatMessage) {
return Message(
role: chatMessage.sender,
content: chatMessage.message,
timestamp: DateTime.parse(chatMessage.timestamp), // 直接使用chatMessage的timestamp
timestamp: DateTime.parse(
chatMessage.timestamp), // 直接使用chatMessage的timestamp
// isLoading 默认false
// isLoading: metadata['isLoading'] ?? false,
);
}).toList();
return messages;
} catch (e) {
Logger.error('加载聊天记录失败', e);
return [];
}
}
/// 清除特定代理的聊天记录
Future<void> clearHistory(String agentId) async {
try {
@ -121,7 +122,7 @@ class ChatHistoryService extends GetxService {
Logger.error('清除聊天记录失败', e);
}
}
/// 清除所有聊天记录
Future<void> clearAllHistory() async {
try {
@ -131,4 +132,4 @@ class ChatHistoryService extends GetxService {
Logger.error('清除所有聊天记录失败', e);
}
}
}
}

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

@ -2,6 +2,8 @@ import 'dart:async';
import 'dart:convert';
import 'dart:io';
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/music_manager.dart';
import 'package:deep_voice/data/services/navigation_manager.dart';
@ -548,7 +550,9 @@ class AgentController extends GetxController {
// 复制图片到应用永久存储目录
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! : '[图片]';
@ -578,6 +582,7 @@ class AgentController extends GetxController {
// 调用Agent Service处理图片,考虑TTS状态
await AgentService.processImageInput(
permanentImagePath,
ImageUrl,
text: text ?? '',
speakResponse: isTtsEnabled.value,
);

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["openaiBaseUrl"]?.toString() ?: "",
config["openaiModel"]?.toString() ?: "",
config["openaivisionModel"]?.toString() ?: "",
config["mcpServer"]?.toString() ?: ""
)
@ -576,6 +577,7 @@ object AgentService : CoroutineScope {
) {
// 创建带图片的用户消息并处理
val userMessage = createUserMessageWithImage(text, imageBase64)
Log.d(TAG, "liwei--------- 图像Mesage: ${userMessage}")
// 图片描述用于存储
val displayText = text.ifEmpty { "[图片]" }
processWithChatApiInternal(userMessage, displayText, speakResponse, true)
@ -599,7 +601,7 @@ object AgentService : CoroutineScope {
) {
// 如果有正在进行的AI流式输出,先停止它
stopAiStream()
stopTts()
// 记录新的AI任务
currentAiJob = launch {
try {
@ -621,16 +623,16 @@ object AgentService : CoroutineScope {
val messagesWithSystemPrompt = JSONArray()
// 先添加系统提示词
if (systemPrompt.isNotEmpty()) {
val location = LocationService.getCachedLocation()?: mapOf(
"user_location" to "定位未开启",
"user_lat" to "定位未开启",
"user_lng" to "定位未开启",
)
val _systemPrompt = Utils.fillTemplate(systemPrompt,location)
messagesWithSystemPrompt.put(createSystemMessage(_systemPrompt))
Log.d(TAG, "liwei--------- 系统提示词: ${_systemPrompt}")
}
if (systemPrompt.isNotEmpty()) {
val location = LocationService.getCachedLocation()?: mapOf(
"user_location" to "定位未开启",
"user_lat" to "定位未开启",
"user_lng" to "定位未开启",
)
val _systemPrompt = Utils.fillTemplate(systemPrompt,location)
messagesWithSystemPrompt.put(createSystemMessage(_systemPrompt))
Log.d(TAG, "liwei--------- 系统提示词: ${_systemPrompt}")
}
// 再添加历史消息
for (i in 0 until historyMessages.length()) {
@ -665,7 +667,7 @@ object AgentService : CoroutineScope {
// 兜底处理,直接添加原消息
addToHistoryMessages(userMessage)
}
// Log.e("ChatApiService", "liwei---------sendMessageStream messages:${messagesWithSystemPrompt}")
chatApiService.sendMessageStream(
messagesWithSystemPrompt,
object : StreamCallback {
@ -684,25 +686,21 @@ object AgentService : CoroutineScope {
ttsService?.flushStream()
}
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()) {
// 发送完整回复,包含是否有图片的标记
val responseData = mutableMapOf<String, Any>(
"text" to response,
"userInput" to displayText
)
if (hasImage) {
responseData["hasImage"] = true
}
sendEvent("assistant_response", responseData)
// 添加AI回复到历史记录
addToHistoryMessages(createAssistantMessage(response))
// 保存聊天记录
saveChatMessage(displayText, response,aiMetadata,userMetadata.toString())
}
// 保存聊天记录
saveChatMessage(displayText, response,aiMetadata,userMetadata.toString())
// 标记AI流式输出已完成
isAiStreaming = false
currentAiJob = null
@ -861,14 +859,15 @@ object AgentService : CoroutineScope {
)
if (userMessageId != -1L) {
// 保存AI回复
val assistantMessageId = chatStorageHelper.saveMessage(
sessionId = sessionId,
message = assistantMessage,
sender = "assistant",
metadata = aiMetadata
)
if (assistantMessage.isNotEmpty() ) {
// 保存AI回复
val assistantMessageId = chatStorageHelper.saveMessage(
sessionId = sessionId,
message = assistantMessage,
sender = "assistant",
metadata = aiMetadata
)
}
// if (assistantMessageId != -1L) {
// Log.d(TAG, "聊天记录已保存:用户消息ID=$userMessageId, 助手消息ID=$assistantMessageId")
// } 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 {
if (text.isNotEmpty()) {
put(JSONObject().apply {
@ -981,8 +980,8 @@ object AgentService : CoroutineScope {
put(JSONObject().apply {
put("type", "image_url")
put("image_url", JSONObject().apply {
put("url", "data:image/jpeg;base64,$imageBase64")
put("detail", "auto")
// put("url", "data:image/jpeg;base64,$imageBase64")
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) {
Log.e(TAG, "服务未初始化")
sendEvent("error", mapOf("code" to "NOT_INITIALIZED", "message" to "服务未初始化"))
@ -1019,16 +1018,16 @@ object AgentService : CoroutineScope {
launch {
try {
// 将图片转换为Base64格式
val imageBase64 = chatApiService.fileToBase64(imagePath) ?: run {
sendEvent("error", mapOf(
"code" to "IMAGE_CONVERSION_FAILED",
"message" to "图片转换失败"
))
return@launch
}
// val imageBase64 = chatApiService.fileToBase64(imagePath) ?: run {
// sendEvent("error", mapOf(
// "code" to "IMAGE_CONVERSION_FAILED",
// "message" to "图片转换失败"
// ))
// return@launch
// }
// 创建带图片的用户消息并处理
val userMessage = createUserMessageWithImage(text, imageBase64)
val userMessage = createUserMessageWithImage(text, imageUrl)
// 图片描述用于存储
val displayText = text.ifEmpty { "[图片]" }
@ -1037,7 +1036,7 @@ object AgentService : CoroutineScope {
"status" to "ready",
"imagePath" to imagePath
))
Log.d(TAG, "liwei--------- 图像Mesage: ${userMessage}")
// 处理消息并传入存储路径
processWithChatApiInternal(userMessage, displayText, speakResponse, true, imagePath)
} 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["openaiBaseUrl"]?.toString()?.let { intent.putExtra("openaiBaseUrl", 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["mcpServer"]?.toString()?.let { intent.putExtra("mcpServer", it) }
config["volcanoAppId"]?.toString()?.let { intent.putExtra("volcanoAppId", it) }
@ -130,13 +131,18 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream
result.error("INVALID_ARGUMENTS", "图片路径不能为空", null)
return
}
val imageUrl = call.argument<String>("imageUrl")
if (imageUrl.isNullOrEmpty()) {
result.error("INVALID_ARGUMENTS", "图片Url不能为空", null)
return
}
val text = call.argument<String>("text") ?: ""
val speakResponse = call.argument<Boolean>("speakResponse") ?: false
try {
val isProcessing = AgentService.processImageInput(
imagePath = imagePath,
imageUrl = imageUrl,
text = text,
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_BASE_URL = "openaiBaseUrl"
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_MCP_SERVER = "mcpServer"
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_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_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_MCP_SERVER)?.let { value -> config[KEY_MCP_SERVER] = 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_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_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_MCP_SERVER to getConfigString(prefs, KEY_MCP_SERVER, ""),
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] 可选的文本描述或问题
/// [speakResponse] 是否朗读响应
/// 返回是否成功处理图片
static Future<bool> processImageInput(String imagePath,
static Future<bool> processImageInput(String imagePath, String imageUrl,
{String text = "", bool speakResponse = false}) async {
try {
final bool result = await _channel.invokeMethod('processImageInput', {
'imagePath': imagePath,
'imageUrl': imageUrl,
'text': text,
'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
import android.app.Activity
import android.content.BroadcastReceiver
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 io.flutter.embedding.engine.plugins.FlutterPlugin
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
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Build
import android.os.Bundle
import android.util.Log
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.NaviInfo
import com.amap.api.navi.model.NaviLatLng
import com.yunqiinnovation.amap_service.NavigationService.Companion.sharedNavi
class NavigationActivity : AppCompatActivity(), AMapNaviListener, AMapNaviViewListener {
private lateinit var naviView: AMapNaviView
@ -32,6 +36,7 @@ class NavigationActivity : AppCompatActivity(), AMapNaviListener, AMapNaviViewLi
private var startpos:String = "";
private var endpos:String = "";
private var ntype:String = "";
private var isReceiverRegistered = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_navigation)
@ -60,7 +65,31 @@ class NavigationActivity : AppCompatActivity(), AMapNaviListener, AMapNaviViewLi
}
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() {
@ -80,6 +109,10 @@ class NavigationActivity : AppCompatActivity(), AMapNaviListener, AMapNaviViewLi
NavigationServiceHelper.startNavigation(context,"stop")
naviView.onDestroy()
super.onDestroy()
if (isReceiverRegistered) {
unregisterReceiver(controlReceiver)
isReceiverRegistered = false
}
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.NotificationManager
import android.app.Service
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.graphics.Bitmap
import android.os.Build
import android.os.IBinder
@ -52,11 +54,24 @@ class NavigationService: Service(),AMapNaviListener {
fun isServiceRunning() = isRunning.get()
}
private var isReceiverRegistered = false
override fun onCreate() {
super.onCreate()
instance = this
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 {
@ -84,7 +99,17 @@ class NavigationService: Service(),AMapNaviListener {
}
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() {
try {
sharedNavi = AMapNavi.getInstance(applicationContext).apply {
@ -399,4 +424,15 @@ class NavigationService: Service(),AMapNaviListener {
Log.e(TAG, "onGpsSignalWeak! $p0")
// 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("<", "&lt;")
.replace(">", "&gt;")
.replace("#", "&gt;")
.replace("*", "&gt;")
.replace(Regex("[😀-🟿]+"), "") // 简化表情符号移除
.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?.setStreamCallback(object : StreamCallback {
override fun onToken(token: String) {
channel.invokeMethod("onToken", token)
}
override fun onComplete() {
channel.invokeMethod("onComplete", null)
}
override fun onError(error: Exception) {
channel.invokeMethod("onError", error.message)
}
override fun onFunctionCall(functionCall: org.json.JSONObject) {
channel.invokeMethod("onFunctionCall", functionCall.toString())
}
override fun onFunctionCallResult(functionCall: org.json.JSONObject, functionCallResult: org.json.JSONObject) {
channel.invokeMethod("onFunctionCallResult", mapOf(
"functionCall" to functionCall.toString(),
"functionCallResult" to functionCallResult.toString()
))
}
})
// chatApiService?.setStreamCallback(object : StreamCallback {
// override fun onToken(token: String) {
// channel.invokeMethod("onToken", token)
// }
//
// override fun onComplete() {
// channel.invokeMethod("onComplete", null)
// }
//
// override fun onError(error: Exception) {
// channel.invokeMethod("onError", error.message)
// }
//
// override fun onFunctionCall(functionCall: org.json.JSONObject) {
// channel.invokeMethod("onFunctionCall", functionCall.toString())
// }
//
// override fun onFunctionCallResult(functionCall: org.json.JSONObject, functionCallResult: org.json.JSONObject) {
// channel.invokeMethod("onFunctionCallResult", mapOf(
// "functionCall" to functionCall.toString(),
// "functionCallResult" to functionCallResult.toString()
// ))
// }
// })
}
override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
@ -80,18 +80,19 @@ class ChatApiPlugin : FlutterPlugin, MethodCallHandler, EventChannel.StreamHandl
val apiKey = call.argument<String>("apiKey") ?: ""
val baseUrl = call.argument<String>("baseUrl") ?: ""
val model = call.argument<String>("model") ?: ""
val modelmedia = call.argument<String>("modelmedia") ?: ""
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)
}
"chatCompletionStream" -> {
val messages = call.argument<List<Map<String, Any>>>("messages") ?: emptyList()
val tool = call.argument<Boolean>("tool") ?: false
pluginScope.launch {
chatApiService?.chatCompletionStream(messages, tool)
}
result.success(true)
}
// "chatCompletionStream" -> {
// val messages = call.argument<List<Map<String, Any>>>("messages") ?: emptyList()
// val tool = call.argument<Boolean>("tool") ?: false
// pluginScope.launch {
// chatApiService?.chatCompletionStream(messages, tool)
// }
// result.success(true)
// }
"cancelChatStream" -> {
chatApiService?.cancelChatStream()
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 android.os.Handler
import android.os.Looper
import java.util.UUID
/**
* ChatAPI服务异常
@ -109,10 +110,12 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
// 流式请求相关
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 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) {
streamCallback = callback
}
// fun setStreamCallback(callback: StreamCallback) {
// streamCallback = callback
// }
/**
* 初始化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
if (baseUrl.isNotEmpty()) {
this.baseUrl = baseUrl
@ -139,7 +142,9 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
if (model.isNotEmpty()) {
this.model = model
}
if (visionModel.isNotEmpty()) {
this.visionModel = visionModel
}
// 处理 baseUrl:移除末尾的 /chat/completions(如果存在)
// 因为 openai-kotlin 会自动拼接 /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) {
// 设置回调
setStreamCallback(callback)
// setStreamCallback(callback)
// 转换JSONArray为List<Map<String, Any>>
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) {
Log.e("ChatApiService", "ChatAPI服务未初始化,无法发送消息")
streamCallback?.onError(ChatApiException("ChatAPI服务未初始化"))
sessions[sessionid]?.onError(ChatApiException("ChatAPI服务未初始化"))
return
}
// 重置状态
isCanceled = false
// isCanceled = false
currentMessages = messages
toolCalls.clear()
// 检查是否包含图片,决定使用哪个模型
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")
throw IllegalStateException("OpenAI客户端未初始化")
}
// Log.e("ChatApiService", "liwei---------,发送AI请求 model:${currentModel} messages:${chatMessages}")
val flow = openAI!!.chatCompletions(chatCompletionRequest)
flow
} catch (e: Exception) {
@ -332,7 +337,9 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
}
chatsFlow.collect { result ->
if (isCanceled) return@collect
if (currSessionId != sessionid) {
return@collect
}
val choice = result.choices.firstOrNull()
if (choice == null) {
@ -348,7 +355,7 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
// 处理普通文本内容
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) {
streamCallback?.onComplete()
sessions[sessionid]?.onComplete()
}
}else{
sessions[sessionid]?.onComplete()
}
} catch (e: Exception) {
if (!isCanceled) {
streamCallback?.onError(ChatApiException("流式请求失败: ${e.message}"))
}
// if (currSessionId == sessionid) {
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 {
toolCalls.values.firstOrNull { it.isValid() }
} catch (e: Exception) {
@ -430,11 +439,11 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
return false
}
// 通知上层工具调用事件
streamCallback?.onFunctionCall(convertMapToJsonObject(functionCall))
sessions[sessionid]?.onFunctionCall(convertMapToJsonObject(functionCall))
// 在后台队列处理工具调用
launch {
try {
if (!isCanceled) {
if (currSessionId == sessionid) {
// 通过MCP客户端处理工具调用
val functionName = firstToolCall.name
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")
}
if (!isCanceled) {
if (currSessionId == sessionid) {
// 处理结果
streamCallback?.onFunctionCallResult(
sessions[sessionid]?.onFunctionCallResult(
convertMapToJsonObject(functionCall),
convertMapToJsonObject(result)
)
// 将结果发送回OpenAI继续对话
sendFunctionCallResultInternal(
sessionid= sessionid,
messages = currentMessages,
functionCall = functionCall,
functionResult = gson.toJson(result)
)
}else{
sessions[sessionid]?.onComplete()
}
}
} catch (e: Exception) {
Log.e("ChatApiService", "工具调用处理过程中出错: ${e.message}", e)
if (!isCanceled) {
if (currSessionId == sessionid) {
val errorMessage = "工具调用处理失败: ${e.message}"
sendFunctionCallResultInternal(
sessionid= sessionid,
messages = currentMessages,
functionCall = functionCall,
functionResult = errorMessage
)
}else{
sessions[sessionid]?.onComplete()
}
}
}
@ -505,11 +520,12 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
* 发送函数调用结果(内部方法)
*/
private suspend fun sendFunctionCallResultInternal(
sessionid: String,
messages: List<Map<String, Any>>,
functionCall: Map<String, Any>,
functionResult: String
) {
if (isCanceled) return
if (currSessionId != sessionid) return
val fullMessages = messages.toMutableList()
@ -542,14 +558,16 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
)
// 重新发起对话,让AI基于工具调用结果生成回复
sendMessageStream(fullMessages)
sendMessageStream(sessionid,fullMessages)
}
/**
* 取消当前流式请求
*/
fun cancelCurrentStream(): Boolean {
isCanceled = true
sessions[currSessionId]?.onComplete()
sessions.remove(currSessionId)
currSessionId = ""
currentStreamJob?.cancel()
currentStreamJob = null
return true
@ -778,9 +796,9 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
* 聊天完成流式接口
* 与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(messages)
sendMessageStream(sessionid,messages)
}
// MARK: - 工具方法
@ -893,27 +911,70 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
}
is List<*> -> {
// 对于多模态内容,提取文本部分(简化处理)
var textContent = ""
val contentParts = mutableListOf<ContentPart>()
content.forEach { item ->
if (item is Map<*, *>) {
val type = item["type"] as? String
if (type == "text") {
val text = item["text"] as? String ?: ""
textContent += text
when (type) {
"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
val chatMessage = when (chatRole) {
Role.System -> ChatMessage.System(finalContent)
Role.User -> ChatMessage.User(finalContent)
Role.Assistant -> ChatMessage.Assistant(finalContent)
else -> ChatMessage.User(finalContent)
// 创建消息对象
if (contentParts.isNotEmpty()) {
val chatMessage = when (chatRole) {
Role.System -> {
// 系统消息简化显示,将图片替换为[图片]
val displayText = contentParts.joinToString { part ->
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>> {
val result = mutableListOf<Map<String, Any>>()
// Log.e("ChatApiService", "convertJsonArrayToList 1:${jsonArray}")
for (i in 0 until jsonArray.length()) {
val jsonObject = jsonArray.optJSONObject(i) ?: continue
val map = convertJsonObjectToMap(jsonObject)
result.add(map)
}
// Log.e("ChatApiService", "convertJsonArrayToList 2:${result}")
return result
}
@ -1123,9 +1184,9 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
}
// 通知Flutter端处理函数调用
mainHandler.post {
streamCallback?.onFunctionCall(functionCall)
}
// mainHandler.post {
// streamCallback?.onFunctionCall(functionCall)
// }
// 返回一个标记,表示函数已被调用
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)
)
// 注册媒体暂停功能
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(
name = "media_previous",
@ -199,6 +209,32 @@ class SystemFunctionHandler(private val context: Context? = null) {
),
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\": \"已暂停媒体播放\"}"
}
}
/**
* 媒体暂停处理器
*/
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
if containsImage(in: messages) {
currentModel = visionModel
}
// if containsImage(in: messages) {
// currentModel = visionModel
// }
// 转换消息格式
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) {
if (initialized) return
Log.e(TAG, "liwei--------- 初始化高德定位服务 1")
// Log.e(TAG, "liwei--------- 初始化高德定位服务 1")
try {
AMapLocationClient.updatePrivacyShow(context, true, true)
AMapLocationClient.updatePrivacyAgree(context, true)
Log.e(TAG, "liwei--------- 初始化高德定位服务 2")
// Log.e(TAG, "liwei--------- 初始化高德定位服务 2")
locationClient = AMapLocationClient(context.applicationContext)
val option = AMapLocationClientOption().apply {
locationMode = AMapLocationClientOption.AMapLocationMode.Hight_Accuracy
isOnceLocation = false
interval = 5000L
interval = 30000L
}
Log.e(TAG, "liwei--------- 初始化高德定位服务 3")
// Log.e(TAG, "liwei--------- 初始化高德定位服务 3")
locationClient?.setLocationOption(option)
Log.e(TAG, "liwei--------- 初始化高德定位服务 4")
// Log.e(TAG, "liwei--------- 初始化高德定位服务 4")
locationClient?.setLocationListener { location ->
if (location != null && location.errorCode == 0) {
currentLocation = location
Log.d(TAG, "liwei---------定位更新: ${location.latitude}, ${location.longitude}")
// Log.d(TAG, "liwei---------定位更新: ${location.latitude}, ${location.longitude}")
} else {
Log.w(TAG, "liwei---------定位失败: ${location?.errorCode} - ${location?.errorInfo}")
}
}
Log.e(TAG, "liwei--------- 初始化高德定位服务 5")
// Log.e(TAG, "liwei--------- 初始化高德定位服务 5")
locationClient?.startLocation()
Log.e(TAG, "liwei--------- 初始化高德定位服务 6")
// Log.e(TAG, "liwei--------- 初始化高德定位服务 6")
initialized = true
} catch (e: Exception) {
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 {
addAction("com.yunqiinnovation.music_service.MEDIA_PLAY")
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_NEXT")
}
@ -132,6 +133,10 @@ class MusicService : MediaBrowserServiceCompat() {
Log.i(TAG, "收到 MEDIA_PLAY 广播")
resume()
}
"com.yunqiinnovation.music_service.MEDIA_CLOSE" -> {
Log.i(TAG, "收到 MEDIA_CLOSE 广播")
stopSelf()
}
"com.yunqiinnovation.music_service.MEDIA_PREVIOUS" -> {
Log.i(TAG, "收到 MEDIA_PREVIOUS 广播")
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)
false
}
Log.d(" MusicServiceStarter", "liwei------------------- Service start success: $success")
Log.d(" MusicServiceStarter", "Service start success: $success")
}
//启动播放列表

Loading…
Cancel
Save