Browse Source

feat(agent): 支持图片消息持久化并优化聊天记录加载

- 在AgentService中新增图片路径参数,用于处理带图片的聊天消息
- 优化聊天记录加载逻辑,支持从元数据中提取图片信息
- 将图片复制到应用永久存储目录,确保图片持久化
- 更新欢迎消息中的助手名称从Emma改为小言
newdev_shunjiawei
lxm 1 year ago
parent
commit
a89ef59a13
  1. 111
      lib/modules/agent/controllers/agent_controller.dart
  2. 2
      lib/modules/agent/views/agent_view.dart
  3. 36
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt
  4. 17
      local_plugins/agent_service/lib/agent_service.dart

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

@ -11,6 +11,7 @@ import 'package:get_storage/get_storage.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
import 'package:app_settings/app_settings.dart'; import 'package:app_settings/app_settings.dart';
import 'package:deep_voice/core/utils/logger.dart'; import 'package:deep_voice/core/utils/logger.dart';
import 'package:path_provider/path_provider.dart';
class Message { class Message {
final bool isUser; //标识信息是否来自用户还是AI final bool isUser; //标识信息是否来自用户还是AI
@ -136,10 +137,11 @@ class AgentController extends GetxController {
Future<void> _loadChatHistory() async { Future<void> _loadChatHistory() async {
try { try {
Logger.i(TAG, '正在加载聊天历史...'); Logger.i(TAG, '正在加载聊天历史...');
//通过本地插件ChatStorage获取聊天记录
final result = await ChatStorage.getMessages( final result = await ChatStorage.getMessages(
sessionId: sessionId, sessionId: sessionId, //会话ID
page: 1, page: 1, //从第一页开始
pageSize: 20, pageSize: 20, //每页20条记录
); );
if (result.messages.isNotEmpty) { if (result.messages.isNotEmpty) {
@ -148,11 +150,22 @@ class AgentController extends GetxController {
final meta = msg.metadata != null && msg.metadata != "" final meta = msg.metadata != null && msg.metadata != ""
? jsonDecode(msg.metadata!) ? jsonDecode(msg.metadata!)
: null; // 如果为空,使用空对象 : null; // 如果为空,使用空对象
// 从元数据中提取图片信息
final hasImage = meta != null && meta.containsKey('hasImage')
? meta['hasImage'] as bool
: false;
final imagePath = meta != null && meta.containsKey('imagePath')
? meta['imagePath'] as String?
: null;
return Message( return Message(
isUser: msg.sender == 'user', isUser: msg.sender == 'user',
text: msg.message, text: msg.message,
meta: meta, meta: meta,
timestamp: DateTime.parse(msg.timestamp), timestamp: DateTime.parse(msg.timestamp),
hasImage: hasImage,
imagePath: imagePath,
); );
}).toList(); }).toList();
@ -161,6 +174,22 @@ class AgentController extends GetxController {
messages.assignAll(historyMessages); messages.assignAll(historyMessages);
Logger.i(TAG, '成功加载 ${historyMessages.length} 条聊天记录'); Logger.i(TAG, '成功加载 ${historyMessages.length} 条聊天记录');
// 详细打印每条消息的内容和属性,便于调试
Logger.d(TAG, '===== 历史消息详情开始 =====');
for (int i = 0; i < historyMessages.length; i++) {
final msg = historyMessages[i];
Logger.d(
TAG,
'消息[$i]: {'
'isUser: ${msg.isUser}, '
'text: "${msg.text.length > 50 ? msg.text.substring(0, 50) + "..." : msg.text}", '
'time: ${msg.timestamp}, '
'hasImage: ${msg.hasImage}, '
'imagePath: ${msg.imagePath ?? "无"}, '
'meta: ${msg.meta != null ? "有" : "无"}'
'}');
}
Logger.d(TAG, '===== 历史消息详情结束 =====');
} else { } else {
// 没有历史记录时添加欢迎消息 // 没有历史记录时添加欢迎消息
_addWelcomeMessage(); _addWelcomeMessage();
@ -180,7 +209,7 @@ class AgentController extends GetxController {
void _addWelcomeMessage() { void _addWelcomeMessage() {
messages.add(Message( messages.add(Message(
isUser: false, isUser: false,
text: '你好!我是**Emma**,我能帮你:\n\n' text: '你好!我是**小言**,我能帮你:\n\n'
'• 答问题和提供信息\n\n' '• 答问题和提供信息\n\n'
'• 实时语音翻译\n\n' '• 实时语音翻译\n\n'
'• 推荐音乐和播客\n\n' '• 推荐音乐和播客\n\n'
@ -424,7 +453,7 @@ class AgentController extends GetxController {
}); });
} }
// 发送文本消息 // 发送消息(包括所有支持的消息类型)
Future<void> sendTextMessage() async { Future<void> sendTextMessage() async {
// 如果图片输入是活跃的,使用选中的图片和文本框的内容 // 如果图片输入是活跃的,使用选中的图片和文本框的内容
if (isImageInputActive.value && selectedImagePath.value != null) { if (isImageInputActive.value && selectedImagePath.value != null) {
@ -466,6 +495,9 @@ class AgentController extends GetxController {
return; return;
} }
// 复制图片到应用永久存储目录
final permanentImagePath = await _copyImageToPermanentStorage(imagePath);
// 显示的文本,如果没有提供则使用默认值 // 显示的文本,如果没有提供则使用默认值
final displayText = text?.isNotEmpty == true ? text! : '[图片]'; final displayText = text?.isNotEmpty == true ? text! : '[图片]';
@ -474,7 +506,7 @@ class AgentController extends GetxController {
isUser: true, isUser: true,
text: displayText, text: displayText,
hasImage: true, hasImage: true,
imagePath: imagePath, imagePath: permanentImagePath,
); );
messages.add(message); messages.add(message);
@ -489,9 +521,12 @@ class AgentController extends GetxController {
// 标记为新的AI回复 // 标记为新的AI回复
_isNewAssistantResponse = true; _isNewAssistantResponse = true;
// 保存消息到持久化存储
//await _saveMessageToStorage(message);
// 调用Agent Service处理图片,考虑TTS状态 // 调用Agent Service处理图片,考虑TTS状态
await AgentService.processImageInput( await AgentService.processImageInput(
imagePath, permanentImagePath,
text: text ?? '', text: text ?? '',
speakResponse: isTtsEnabled.value, speakResponse: isTtsEnabled.value,
); );
@ -502,6 +537,68 @@ class AgentController extends GetxController {
} }
} }
// 将图片复制到应用的永久存储目录
Future<String> _copyImageToPermanentStorage(String sourcePath) async {
try {
final File sourceFile = File(sourcePath);
if (!await sourceFile.exists()) {
Logger.e(TAG, '源图片文件不存在: $sourcePath');
return sourcePath; // 如果源文件不存在,返回原路径
}
// 创建应用文档目录下的chatImages文件夹
final appDir = await getApplicationDocumentsDirectory();
final imagesDir = Directory('${appDir.path}/chatImages');
if (!await imagesDir.exists()) {
await imagesDir.create(recursive: true);
}
// 生成唯一文件名
final fileName =
'${DateTime.now().millisecondsSinceEpoch}_${sourcePath.split('/').last}';
final targetPath = '${imagesDir.path}/$fileName';
// 复制文件
await sourceFile.copy(targetPath);
Logger.i(TAG, '图片已复制到永久存储: $targetPath');
return targetPath;
} catch (e) {
Logger.e(TAG, '复制图片到永久存储失败: $e');
return sourcePath; // 出错时返回原路径
}
}
// 保存消息到持久化存储
// Future<void> _saveMessageToStorage(Message message) async {
// try {
// // 准备元数据,包含图片信息
// Map<String, dynamic> metadata = {};
// if (message.meta != null) {
// metadata = Map<String, dynamic>.from(message.meta!);
// }
// // 添加图片相关信息到元数据
// if (message.hasImage) {
// metadata['hasImage'] = true;
// metadata['imagePath'] = message.imagePath;
// }
// // 保存消息
// await ChatStorage.saveMessage(
// sessionId: sessionId,
// sender: message.isUser ? 'user' : 'assistant',
// message: message.text,
// metadata: metadata.isNotEmpty ? jsonEncode(metadata) : null,
// //timestamp: message.timestamp.toIso8601String(),
// );
// Logger.i(TAG, '消息已保存到持久化存储');
// } catch (e) {
// Logger.e(TAG, '保存消息到持久化存储失败: $e');
// }
// }
// 切换输入模式 // 切换输入模式
void toggleInputMode() async { void toggleInputMode() async {
// 如果当前在图片输入模式,先清除 // 如果当前在图片输入模式,先清除

2
lib/modules/agent/views/agent_view.dart

@ -52,7 +52,7 @@ class AgentView extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
'Emma', '小言'.tr,
style: TextStyle( style: TextStyle(
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,

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

@ -91,7 +91,6 @@ object AgentService : CoroutineScope {
// 空闲检测相关 // 空闲检测相关
private var idleCheckJob: Job? = null private var idleCheckJob: Job? = null
private val maxIdleSeconds = 10 // 最大空闲秒数 private val maxIdleSeconds = 10 // 最大空闲秒数
/** /**
* 初始化系统提示词 * 初始化系统提示词
*/ */
@ -644,12 +643,14 @@ object AgentService : CoroutineScope {
* @param displayText 用于显示和存储的文本 * @param displayText 用于显示和存储的文本
* @param speakResponse 是否朗读回复 * @param speakResponse 是否朗读回复
* @param hasImage 是否包含图片 * @param hasImage 是否包含图片
* @param imagePath 图片路径(如果有)
*/ */
private fun processWithOpenAIInternal( private fun processWithOpenAIInternal(
userMessage: JSONObject, userMessage: JSONObject,
displayText: String, displayText: String,
speakResponse: Boolean = true, speakResponse: Boolean = true,
hasImage: Boolean = false hasImage: Boolean = false,
imagePath: String? = null
) { ) {
// 如果有正在进行的AI流式输出,先停止它 // 如果有正在进行的AI流式输出,先停止它
stopAiStream() stopAiStream()
@ -662,7 +663,14 @@ object AgentService : CoroutineScope {
// 使用历史记录作为上下文发送到OpenAI // 使用历史记录作为上下文发送到OpenAI
val responseBuilder = StringBuilder() val responseBuilder = StringBuilder()
var metadata:String = "" var aiMetadata:String = ""
var userMetadata = JSONObject()
// 如果有图片,添加图片信息到元数据
if (hasImage) {
userMetadata.put("hasImage", true)
userMetadata.put("imagePath", imagePath)
}
// 添加系统提示词到历史消息的副本中 // 添加系统提示词到历史消息的副本中
val messagesWithSystemPrompt = JSONArray() val messagesWithSystemPrompt = JSONArray()
@ -741,7 +749,9 @@ object AgentService : CoroutineScope {
addToHistoryMessages(openAIService.createAssistantMessage(response)) addToHistoryMessages(openAIService.createAssistantMessage(response))
// 保存聊天记录 // 保存聊天记录
saveChatMessage(displayText, response,metadata) saveChatMessage(displayText, response,aiMetadata,userMetadata.toString())
} }
// 标记AI流式输出已完成 // 标记AI流式输出已完成
@ -784,7 +794,7 @@ object AgentService : CoroutineScope {
"result" to functionCallResult.toString(), "result" to functionCallResult.toString(),
)) ))
Log.d(TAG, "mcp调用结果: $functionCallResult") Log.d(TAG, "mcp调用结果: $functionCallResult")
metadata = autoHandleFcunCallResult(functionCallResult); aiMetadata = autoHandleFcunCallResult(functionCallResult);
} }
} }
) )
@ -887,7 +897,7 @@ object AgentService : CoroutineScope {
/** /**
* 保存聊天消息 * 保存聊天消息
*/ */
private fun saveChatMessage(userMessage: String, assistantMessage: String,metadata : String) { private fun saveChatMessage(userMessage: String, assistantMessage: String,aiMetadata : String,userMetadata : String) {
// 显式指定IO调度器,数据库操作应在后台线程执行,即使父协程在Main上下文中 // 显式指定IO调度器,数据库操作应在后台线程执行,即使父协程在Main上下文中
launch(Dispatchers.IO) { launch(Dispatchers.IO) {
try { try {
@ -895,7 +905,8 @@ object AgentService : CoroutineScope {
val userMessageId = chatStorageHelper.saveMessage( val userMessageId = chatStorageHelper.saveMessage(
sessionId = sessionId, sessionId = sessionId,
message = userMessage, message = userMessage,
sender = "user" sender = "user",
metadata = userMetadata
) )
if (userMessageId != -1L) { if (userMessageId != -1L) {
@ -904,7 +915,7 @@ object AgentService : CoroutineScope {
sessionId = sessionId, sessionId = sessionId,
message = assistantMessage, message = assistantMessage,
sender = "assistant", sender = "assistant",
metadata = metadata metadata = aiMetadata
) )
// if (assistantMessageId != -1L) { // if (assistantMessageId != -1L) {
@ -1010,14 +1021,19 @@ object AgentService : CoroutineScope {
return@launch return@launch
} }
// 创建带图片的用户消息并处理
val userMessage = openAIService.createUserMessageWithImage(text, imageBase64)
// 图片描述用于存储
val displayText = text.ifEmpty { "[图片]" }
// 通知图片准备完成 // 通知图片准备完成
sendEvent("image_ready", mapOf( sendEvent("image_ready", mapOf(
"status" to "ready", "status" to "ready",
"imagePath" to imagePath "imagePath" to imagePath
)) ))
// 处理包含图片的消息 // 处理消息并传入存储路径
processImageWithOpenAI(imageBase64, text, speakResponse) processWithOpenAIInternal(userMessage, displayText, speakResponse, true, imagePath)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "处理图片失败: ${e.message}") Log.e(TAG, "处理图片失败: ${e.message}")
sendEvent("error", mapOf( sendEvent("error", mapOf(

17
local_plugins/agent_service/lib/agent_service.dart

@ -94,8 +94,10 @@ class AgentServiceException implements Exception {
/// 代理服务插件 /// 代理服务插件
class AgentService { class AgentService {
static const MethodChannel _channel = MethodChannel('com.yunqiinnovation.agent_service'); static const MethodChannel _channel =
static const EventChannel _eventChannel = EventChannel('com.yunqiinnovation.agent_service/events'); MethodChannel('com.yunqiinnovation.agent_service');
static const EventChannel _eventChannel =
EventChannel('com.yunqiinnovation.agent_service/events');
static Stream<AgentServiceEvent>? _eventStream; static Stream<AgentServiceEvent>? _eventStream;
@ -113,9 +115,8 @@ class AgentService {
return AgentServiceEvent(type: type, data: eventData); return AgentServiceEvent(type: type, data: eventData);
} catch (e) { } catch (e) {
return AgentServiceEvent( return AgentServiceEvent(
type: AgentServiceEventType.unknown, type: AgentServiceEventType.unknown,
data: {'error': e.toString(), 'rawEvent': event} data: {'error': e.toString(), 'rawEvent': event});
);
} }
}); });
@ -256,7 +257,8 @@ class AgentService {
/// [text] 文本内容 /// [text] 文本内容
/// [speakResponse] 是否朗读响应 /// [speakResponse] 是否朗读响应
/// 返回是否成功处理文本 /// 返回是否成功处理文本
static Future<bool> processTextInput(String text, {bool speakResponse = false}) async { static Future<bool> processTextInput(String text,
{bool speakResponse = false}) async {
try { try {
final bool result = await _channel.invokeMethod('processTextInput', { final bool result = await _channel.invokeMethod('processTextInput', {
'text': text, 'text': text,
@ -325,7 +327,8 @@ class AgentService {
/// [text] 可选的文本描述或问题 /// [text] 可选的文本描述或问题
/// [speakResponse] 是否朗读响应 /// [speakResponse] 是否朗读响应
/// 返回是否成功处理图片 /// 返回是否成功处理图片
static Future<bool> processImageInput(String imagePath, {String text = "", bool speakResponse = false}) async { static Future<bool> processImageInput(String imagePath,
{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,

Loading…
Cancel
Save