Browse Source

上传ai代码优化逻辑

weicu
liwei1dao 1 year ago
parent
commit
6ffb3333cb
  1. 23
      lib/data/models/message_model.dart
  2. 197
      lib/modules/agent/controllers/agent_controller.dart
  3. 8
      lib/modules/agent/views/agent_view.dart
  4. 236
      lib/modules/agent/views/message_bubble.dart
  5. 84
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt
  6. 12
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt
  7. 4
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleAgent.kt
  8. 45
      local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift
  9. 6
      local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServicePlugin.swift
  10. 7
      local_plugins/agent_service/lib/agent_service.dart
  11. 41
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt
  12. 28
      local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift
  13. 12
      local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureSpeechPlugin.swift
  14. 253
      local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiService.kt
  15. 16
      local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/CustomSseClientTransport.kt
  16. 52
      local_plugins/chat_storage/android/src/main/kotlin/com/yunqiinnovation/chat_storage/ChatStorageHelper.kt
  17. 14
      local_plugins/chat_storage/android/src/main/kotlin/com/yunqiinnovation/chat_storage/ChatStoragePlugin.kt
  18. 33
      local_plugins/chat_storage/ios/chat_storage/Sources/chat_storage/ChatStorageHelper.swift
  19. 12
      local_plugins/chat_storage/ios/chat_storage/Sources/chat_storage/SwiftChatStoragePlugin.swift
  20. 24
      local_plugins/chat_storage/lib/chat_storage.dart

23
lib/data/models/message_model.dart

@ -3,6 +3,7 @@ import 'dart:io';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
class Message { class Message {
final String sessionid; // 会话ID
final bool isUser; //标识信息是否来自用户还是AI final bool isUser; //标识信息是否来自用户还是AI
final String text; //信息文本内容 final String text; //信息文本内容
final DateTime timestamp; // 消息时间戳 final DateTime timestamp; // 消息时间戳
@ -10,9 +11,12 @@ class Message {
final bool hasImage; // 是否包含图片 final bool hasImage; // 是否包含图片
final String? imagePath; // 图片路径 final String? imagePath; // 图片路径
final Map? meta; //卡片数据 final Map? meta; //卡片数据
final List<String>? functionCalls; // 函数调用
bool isToolCallsExpanded; // 工具调用是否展开
Message({ Message({
// 构造函数 // 构造函数
required this.sessionid, // 会话ID
required this.isUser, required this.isUser,
required this.text, required this.text,
DateTime? timestamp, DateTime? timestamp,
@ -20,11 +24,14 @@ class Message {
this.hasImage = false, this.hasImage = false,
this.imagePath, this.imagePath,
this.meta, this.meta,
this.functionCalls,
this.isToolCallsExpanded = false, // 默认不展开
}) : this.timestamp = timestamp ?? DateTime.now(); }) : this.timestamp = timestamp ?? DateTime.now();
// 从JSON构造函数 // 从JSON构造函数
factory Message.fromJson(Map<String, dynamic> json) { factory Message.fromJson(Map<String, dynamic> json) {
return Message( return Message(
sessionid: json['sessionid'] as String, // 会话ID
isUser: json['isUser'] as bool, isUser: json['isUser'] as bool,
text: json['text'] as String, text: json['text'] as String,
timestamp: DateTime.parse(json['timestamp'] as String), timestamp: DateTime.parse(json['timestamp'] as String),
@ -32,6 +39,8 @@ class Message {
hasImage: json['hasImage'] as bool? ?? false, hasImage: json['hasImage'] as bool? ?? false,
imagePath: json['imagePath'] as String?, imagePath: json['imagePath'] as String?,
meta: json['meta'] as Map?, meta: json['meta'] as Map?,
functionCalls: json['functionCalls'] as List<String>?,
isToolCallsExpanded: json['isToolCallsExpanded'] as bool? ?? false,
); );
} }
@ -45,12 +54,15 @@ class Message {
'hasImage': hasImage, 'hasImage': hasImage,
'imagePath': imagePath, 'imagePath': imagePath,
'meta': meta, 'meta': meta,
'functionCalls': functionCalls,
'isToolCallsExpanded': isToolCallsExpanded,
}; };
} }
// 创建一个加载中的消息 // 创建一个加载中的消息
factory Message.loading() { factory Message.loading() {
return Message( return Message(
sessionid: '', // 会话ID
isUser: false, isUser: false,
text: '', text: '',
timestamp: DateTime.now(), timestamp: DateTime.now(),
@ -60,6 +72,7 @@ class Message {
// 复制并修改 // 复制并修改
Message copyWith({ Message copyWith({
required String sessionid, // 会话ID
bool? isUser, bool? isUser,
String? text, String? text,
DateTime? timestamp, DateTime? timestamp,
@ -67,8 +80,11 @@ class Message {
bool? hasImage, bool? hasImage,
String? imagePath, String? imagePath,
Map? meta, Map? meta,
List<String>? functionCalls,
bool? isToolCallsExpanded,
}) { }) {
return Message( return Message(
sessionid: sessionid, // 会话ID
isUser: isUser ?? this.isUser, isUser: isUser ?? this.isUser,
text: text ?? this.text, text: text ?? this.text,
timestamp: timestamp ?? this.timestamp, timestamp: timestamp ?? this.timestamp,
@ -76,6 +92,13 @@ class Message {
hasImage: hasImage ?? this.hasImage, hasImage: hasImage ?? this.hasImage,
imagePath: imagePath ?? this.imagePath, imagePath: imagePath ?? this.imagePath,
meta: meta ?? this.meta, meta: meta ?? this.meta,
functionCalls: functionCalls ?? this.functionCalls,
isToolCallsExpanded: isToolCallsExpanded ?? this.isToolCallsExpanded,
); );
} }
// 切换工具调用展开状态
void toggleToolCallsExpanded() {
isToolCallsExpanded = !isToolCallsExpanded;
}
} }

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

@ -1,25 +1,21 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:ffi';
import 'dart:io'; import 'dart:io';
import 'package:amap_service/amap_service.dart'; import 'package:amap_service/amap_service.dart';
import 'package:http/http.dart' as http;
import 'package:package_info_plus/package_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import 'package:uuid/uuid.dart';
import 'package:voitrans/data/models/appconfig.dart'; import 'package:voitrans/data/models/appconfig.dart';
import 'package:voitrans/data/models/song_model.dart'; import 'package:voitrans/data/models/song_model.dart';
import 'package:voitrans/data/services/ble_manager.dart'; import 'package:voitrans/data/services/ble_manager.dart';
import 'package:voitrans/data/services/bluetooth_manager.dart'; import 'package:voitrans/data/services/bluetooth_manager.dart';
import 'package:voitrans/data/services/qq_music.dart'; import 'package:voitrans/data/services/qq_music.dart';
import 'package:flutter_intro/flutter_intro.dart'; import 'package:flutter_intro/flutter_intro.dart';
import '../../../data/services/ble_manager.dart';
import '../../../data/services/bluetooth_manager.dart';
import '../../../data/services/novice_guide_service.dart'; import '../../../data/services/novice_guide_service.dart';
import '/core/utils/upload_oss.dart'; import '/core/utils/upload_oss.dart';
import '/data/models/user_Info.dart'; import '/data/models/user_Info.dart';
import '/data/services/music_manager.dart'; import '/data/services/music_manager.dart';
import '/data/services/navigation_manager.dart'; import '/data/services/navigation_manager.dart';
import '/data/services/network/api.dart';
import 'package:ble_service/ble_service.dart'; import 'package:ble_service/ble_service.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
@ -46,6 +42,7 @@ class AgentController extends GetxController with WidgetsBindingObserver {
// 聊天消息列表 // 聊天消息列表
final messages = <Message>[].obs; final messages = <Message>[].obs;
final currsessionid = ''.obs;
// UI控制器 // UI控制器
final TextEditingController textController = TextEditingController(); final TextEditingController textController = TextEditingController();
@ -138,7 +135,7 @@ class AgentController extends GetxController with WidgetsBindingObserver {
final TAG = 'AgentController'; final TAG = 'AgentController';
var AppDir = "".obs; var AppDir = "".obs;
// 会话ID // 会话ID
final String sessionId = 'default_agent'; final String agentId = 'default_agent';
// 分页加载相关状态 // 分页加载相关状态
final isLoadingMore = false.obs; // 是否正在加载更多消息 final isLoadingMore = false.obs; // 是否正在加载更多消息
@ -312,7 +309,7 @@ class AgentController extends GetxController with WidgetsBindingObserver {
Logger.i(TAG, '正在加载聊天历史...'); Logger.i(TAG, '正在加载聊天历史...');
//通过本地插件ChatStorage获取聊天记录 //通过本地插件ChatStorage获取聊天记录
final result = await ChatStorage.getMessages( final result = await ChatStorage.getMessages(
sessionId: sessionId, //会话ID agentId: agentId, //会话ID
page: 1, //从第一页开始 page: 1, //从第一页开始
pageSize: pageSize, //每页20条记录 pageSize: pageSize, //每页20条记录
); );
@ -367,7 +364,7 @@ class AgentController extends GetxController with WidgetsBindingObserver {
Logger.i(TAG, '正在加载第 $nextPage 页历史消息...'); Logger.i(TAG, '正在加载第 $nextPage 页历史消息...');
final result = await ChatStorage.getMessages( final result = await ChatStorage.getMessages(
sessionId: sessionId, agentId: agentId,
page: nextPage, page: nextPage,
pageSize: pageSize, pageSize: pageSize,
); );
@ -421,6 +418,7 @@ class AgentController extends GetxController with WidgetsBindingObserver {
: null; : null;
return Message( return Message(
sessionid: msg.sessionId, // 会话ID
isUser: msg.sender == 'user', isUser: msg.sender == 'user',
text: msg.message, text: msg.message,
meta: meta, meta: meta,
@ -488,6 +486,7 @@ class AgentController extends GetxController with WidgetsBindingObserver {
// 添加初始欢迎消息 // 添加初始欢迎消息
void _addWelcomeMessage() { void _addWelcomeMessage() {
messages.add(Message( messages.add(Message(
sessionid: "", // 会话ID
isUser: false, isUser: false,
text: 'welcomeMessage'.tr, // 你好!我是**小言**,我能帮你: text: 'welcomeMessage'.tr, // 你好!我是**小言**,我能帮你:
)); ));
@ -504,6 +503,7 @@ class AgentController extends GetxController with WidgetsBindingObserver {
// isTextInputMode.refresh(); // isTextInputMode.refresh();
// 只标记识别开始,不添加消息 // 只标记识别开始,不添加消息
Logger.i(TAG, '识别开始'); Logger.i(TAG, '识别开始');
break; break;
case AgentServiceEventType.recognitionStopped: case AgentServiceEventType.recognitionStopped:
isListening.value = false; isListening.value = false;
@ -527,25 +527,29 @@ class AgentController extends GetxController with WidgetsBindingObserver {
break; break;
case AgentServiceEventType.recognizing: case AgentServiceEventType.recognizing:
final sessionid = event.data['sessionid'] ?? '';
final text = event.data['text'] ?? ''; final text = event.data['text'] ?? '';
currentText.value = text; // 保留当前文本,以便其他地方使用 currentText.value = text; // 保留当前文本,以便其他地方使用
Logger.i(TAG, '识别中间结果: $text'); Logger.i(TAG, '识别中间结果: $text');
if (text.isNotEmpty) { if (text.isNotEmpty) {
// 查找是否有正在识别中的消息 // 查找是否有正在识别中的消息
final index = messages final index = messages.lastIndexWhere(
.lastIndexWhere((msg) => msg.isRecognizing && msg.isUser); (msg) => msg.sessionid == sessionid && msg.isUser);
if (index >= 0) { if (index >= 0) {
// 更新已有的识别中消息 // 更新已有的识别中消息
messages[index] = Message( messages[index] = Message(
sessionid: sessionid, // 会话ID
isUser: true, isUser: true,
text: text, text: text,
isRecognizing: true, isRecognizing: true,
timestamp: messages[index].timestamp, timestamp: messages[index].timestamp,
); );
} else { } else {
currsessionid.value = event.data['sessionid'] ?? '';
// 没有识别中的消息,创建新消息 // 没有识别中的消息,创建新消息
messages.add(Message( messages.add(Message(
sessionid: sessionid, // 会话ID
isUser: true, isUser: true,
text: text, text: text,
isRecognizing: true, isRecognizing: true,
@ -556,26 +560,33 @@ class AgentController extends GetxController with WidgetsBindingObserver {
break; break;
case AgentServiceEventType.recognitionResult: case AgentServiceEventType.recognitionResult:
final sessionid = event.data['sessionid'] ?? '';
final text = event.data['text'] ?? ''; final text = event.data['text'] ?? '';
Logger.i(TAG, '识别最终结果: $text'); Logger.i(TAG, '识别最终结果: $text');
// 查找是否有正在识别中的消息 // 查找是否有正在识别中的消息
final index = final index = messages.lastIndexWhere(
messages.lastIndexWhere((msg) => msg.isRecognizing && msg.isUser); (msg) => msg.sessionid == sessionid && msg.isUser);
if (text.isNotEmpty) { if (text.isNotEmpty) {
if (index >= 0) { if (index >= 0) {
// 将识别中的消息转换为最终消息 // 将识别中的消息转换为最终消息
messages[index] = Message( messages[index] = Message(
sessionid: sessionid,
isUser: true, isUser: true,
text: text, text: text,
isRecognizing: false, isRecognizing: false,
timestamp: messages[index].timestamp, timestamp: messages[index].timestamp,
functionCalls: messages[index].functionCalls,
isToolCallsExpanded: messages[index].isToolCallsExpanded,
); );
// messages.refresh(); // messages.refresh();
} else { } else {
// 如果没有找到识别中的消息,添加新消息 // 如果没有找到识别中的消息,添加新消息
messages messages.add(Message(
.add(Message(isUser: true, text: text, isRecognizing: false)); sessionid: sessionid,
isUser: true,
text: text,
isRecognizing: false));
} }
isProcessing.value = true; isProcessing.value = true;
@ -590,24 +601,31 @@ class AgentController extends GetxController with WidgetsBindingObserver {
break; break;
//识别取消(网络波动会触发) //识别取消(网络波动会触发)
case AgentServiceEventType.recognitionCanceled: case AgentServiceEventType.recognitionCanceled:
final sessionid = event.data['sessionid'] ?? '';
// 查找是否有正在识别中的消息 // 查找是否有正在识别中的消息
final index = final index = messages.lastIndexWhere(
messages.lastIndexWhere((msg) => msg.isRecognizing && msg.isUser); (msg) => msg.sessionid == sessionid && msg.isUser);
if (currentText.value.isNotEmpty) { if (currentText.value.isNotEmpty) {
if (index >= 0) { if (index >= 0) {
// 将识别中的消息转换为最终消息 // 将识别中的消息转换为最终消息
messages[index] = Message( messages[index] = Message(
sessionid: sessionid, // 会话ID
isUser: true, isUser: true,
text: currentText.value, text: currentText.value,
isRecognizing: false, isRecognizing: false,
timestamp: messages[index].timestamp, timestamp: messages[index].timestamp,
functionCalls: messages[index].functionCalls,
isToolCallsExpanded: messages[index].isToolCallsExpanded,
); );
// messages.refresh(); // messages.refresh();
} else { } else {
// 如果没有找到识别中的消息,添加新消息 // 如果没有找到识别中的消息,添加新消息
messages.add(Message( messages.add(Message(
isUser: true, text: currentText.value, isRecognizing: false)); sessionid: sessionid,
isUser: true,
text: currentText.value,
isRecognizing: false));
} }
isProcessing.value = true; isProcessing.value = true;
@ -654,29 +672,27 @@ class AgentController extends GetxController with WidgetsBindingObserver {
case AgentServiceEventType.assistantToken: case AgentServiceEventType.assistantToken:
if (!isProcessing.value) isProcessing.value = true; if (!isProcessing.value) isProcessing.value = true;
final sessionid = event.data['sessionid'] ?? '';
final token = event.data['token'] ?? ''; final token = event.data['token'] ?? '';
final responseId = event.data['responseId'] ?? '';
// Logger.i(TAG, 'AI回复Token: $token, responseId: $responseId'); // Logger.i(TAG, 'AI回复Token: $token, responseId: $responseId');
if (token.isNotEmpty) { if (token.isNotEmpty) {
// 如果是新的回复或者响应ID改变,创建新消息 // 如果是新的回复或者响应ID改变,创建新消息
if (_isNewAssistantResponse || final index = messages.lastIndexWhere(
(_currentResponseId != responseId && responseId.isNotEmpty)) { (msg) => msg.sessionid == sessionid && !msg.isUser);
messages.add(Message(isUser: false, text: token)); if (index == -1) {
messages.add(
Message(sessionid: sessionid, isUser: false, text: token));
_isNewAssistantResponse = false; _isNewAssistantResponse = false;
_currentResponseId = responseId;
} else { } else {
// 否则更新最后一条消息 messages[index] = Message(
final lastIndex = messages.length - 1; sessionid: sessionid,
isUser: false,
if (lastIndex >= 0 && !messages[lastIndex].isUser) { text: messages[index].text + token,
messages[lastIndex] = Message( meta: messages[index].meta,
isUser: false, timestamp: messages[index].timestamp,
text: messages[lastIndex].text + token, functionCalls: messages[index].functionCalls,
meta: messages[lastIndex].meta, isToolCallsExpanded: messages[index].isToolCallsExpanded,
timestamp: messages[lastIndex].timestamp, );
);
}
} }
// 使用reverse:true时通常不需要手动滚动 // 使用reverse:true时通常不需要手动滚动
} }
@ -713,10 +729,10 @@ class AgentController extends GetxController with WidgetsBindingObserver {
case AgentServiceEventType.autoStop: case AgentServiceEventType.autoStop:
isListening.value = false; isListening.value = false;
final sessionid = event.data['sessionid'] ?? '';
// 移除临时的识别消息 // 移除临时的识别消息
final index = final index = messages.lastIndexWhere(
messages.lastIndexWhere((msg) => msg.isRecognizing && msg.isUser); (msg) => msg.sessionid == sessionid && msg.isUser);
if (index >= 0) { if (index >= 0) {
// 只有当消息为空或文本太短时才移除 // 只有当消息为空或文本太短时才移除
if (messages[index].text.trim().length < 2) { if (messages[index].text.trim().length < 2) {
@ -725,21 +741,25 @@ class AgentController extends GetxController with WidgetsBindingObserver {
} else { } else {
// 否则转换为正式消息 // 否则转换为正式消息
messages[index] = Message( messages[index] = Message(
sessionid: sessionid,
isUser: true, isUser: true,
text: messages[index].text, text: messages[index].text,
timestamp: messages[index].timestamp, timestamp: messages[index].timestamp,
functionCalls: messages[index].functionCalls,
isToolCallsExpanded: messages[index].isToolCallsExpanded,
); );
// messages.refresh(); // messages.refresh();
// 发送此消息到AI处理 // 发送此消息到AI处理
isProcessing.value = true; isProcessing.value = true;
_isNewAssistantResponse = true; _isNewAssistantResponse = true;
_handleAutoStop(index); _handleAutoStop(sessionid, index);
} }
} }
break; break;
case AgentServiceEventType.functionCall: case AgentServiceEventType.functionCall:
final sessionid = event.data['sessionid'] ?? '';
final functionName = event.data['name'] ?? ''; final functionName = event.data['name'] ?? '';
final arguments = event.data['arguments'] ?? ''; final arguments = event.data['arguments'] ?? '';
// debugPrint('Fultter 开始调用 mcp: $functionName, $arguments'); // debugPrint('Fultter 开始调用 mcp: $functionName, $arguments');
@ -748,15 +768,39 @@ class AgentController extends GetxController with WidgetsBindingObserver {
endPhoneCall(); //退出语音交互,同时退出打电话模式 endPhoneCall(); //退出语音交互,同时退出打电话模式
} }
} }
final index = messages.lastIndexWhere(
(msg) => msg.sessionid == sessionid && !msg.isUser);
if (index == -1) {
messages.add(Message(
sessionid: sessionid,
isUser: false,
text: '',
functionCalls: [functionName]));
_isNewAssistantResponse = false;
} else {
// 否则更新最后一条消息
final lastIndex = messages.length - 1;
if (lastIndex >= 0 && !messages[lastIndex].isUser) {
messages[lastIndex] = Message(
sessionid: sessionid,
isUser: false,
text: messages[lastIndex].text,
meta: messages[lastIndex].meta,
timestamp: messages[lastIndex].timestamp,
functionCalls: messages[lastIndex].functionCalls ?? []
..add(functionName),
);
}
}
break; break;
case AgentServiceEventType.functionCallResult: case AgentServiceEventType.functionCallResult:
final responseId = event.data['responseId'] ?? ''; final sessionid = event.data['sessionid'] ?? '';
// final result = event.data['result']; // final result = event.data['result'];
final metaStr = event.data['meta'] ?? ''; final metaStr = event.data['meta'] ?? '';
final functionCall = event.data['function_call'] ?? ''; final functionCall = event.data['function_call'] ?? '';
Logger.i(TAG, 'Flutter 结束调用 mcp: $metaStr'); Logger.i(TAG, 'Flutter 结束调用 $sessionid mcp: $metaStr');
if (metaStr.isNotEmpty) { if (metaStr.isNotEmpty) {
Map<String, dynamic> metaResult = {}; Map<String, dynamic> metaResult = {};
metaResult = jsonDecode(metaStr) as Map<String, dynamic>; metaResult = jsonDecode(metaStr) as Map<String, dynamic>;
@ -771,25 +815,31 @@ class AgentController extends GetxController with WidgetsBindingObserver {
//不回复直接返回 //不回复直接返回
break; break;
} }
final index = messages.lastIndexWhere(
(msg) => msg.sessionid == sessionid && !msg.isUser);
Logger.i(TAG, 'Flutter 结束调用 卡片数据 $index $metaResult');
// 判断是否为新的回复或响应ID是否改变 // 判断是否为新的回复或响应ID是否改变
if (_isNewAssistantResponse || _currentResponseId != responseId) { if (index == -1) {
// 添加新消息 // 添加新消息
messages messages.add(Message(
.add(Message(isUser: false, text: "", meta: metaResult)); sessionid: sessionid,
isUser: false,
text: "",
meta: metaResult));
_isNewAssistantResponse = false; _isNewAssistantResponse = false;
_currentResponseId = responseId;
} else { } else {
// 更新最后一条消息 // 更新最后一条消息
final lastIndex = messages.length - 1; messages[index] = Message(
if (lastIndex >= 0 && !messages[lastIndex].isUser) { sessionid: sessionid,
messages[lastIndex] = Message( isUser: false,
isUser: false, text: messages[index].text,
text: messages[lastIndex].text, meta: metaResult,
meta: metaResult, timestamp: messages[index].timestamp,
timestamp: messages[lastIndex].timestamp, functionCalls: messages[index].functionCalls,
); isToolCallsExpanded: messages[index].isToolCallsExpanded,
} );
} }
messages.refresh();
if (metaResult.containsKey("maps_route_navigation")) { if (metaResult.containsKey("maps_route_navigation")) {
final maps_route_navigation = final maps_route_navigation =
metaResult["maps_route_navigation"]; metaResult["maps_route_navigation"];
@ -847,6 +897,9 @@ class AgentController extends GetxController with WidgetsBindingObserver {
} }
} }
} }
} else {
// 其他函数调用
Logger.i(TAG, 'Flutter 结束调用 解析失败 $metaStr');
} }
} }
if (functionCall.contains("music")) { if (functionCall.contains("music")) {
@ -893,6 +946,7 @@ class AgentController extends GetxController with WidgetsBindingObserver {
// ✅ 创建新的 Message 替换原来的 // ✅ 创建新的 Message 替换原来的
messages[index] = Message( messages[index] = Message(
sessionid: result.sessionid,
isUser: result.isUser, isUser: result.isUser,
text: result.text, text: result.text,
meta: newMeta, meta: newMeta,
@ -1002,10 +1056,13 @@ class AgentController extends GetxController with WidgetsBindingObserver {
// 标记为新的AI回复 // 标记为新的AI回复
_isNewAssistantResponse = true; _isNewAssistantResponse = true;
newAIReplyCompleted.value = false; newAIReplyCompleted.value = false;
currsessionid.value = const Uuid().v4();
final sessionid = currsessionid.value;
// 根据TTS状态决定是否朗读 // 根据TTS状态决定是否朗读
await AgentService.processTextInput(text, AgentService.processTextInput(sessionid, text,
speakResponse: isTtsEnabled.value); speakResponse: isTtsEnabled.value);
final message = Message(isUser: true, text: text); final message = Message(sessionid: sessionid, isUser: true, text: text);
Logger.i(TAG, '发送AI消息:$sessionid $text');
messages.add(message); messages.add(message);
textController.clear(); textController.clear();
} catch (e) { } catch (e) {
@ -1029,12 +1086,17 @@ class AgentController extends GetxController with WidgetsBindingObserver {
var ImageUrl = await UploadOss.upload( var ImageUrl = await UploadOss.upload(
filepath: permanentImagePath, filepath: permanentImagePath,
rootDir: 'User/${User.instance.uid}/ChatImages'); rootDir: 'User/${User.instance.uid}/ChatImages');
Logger.i(TAG, '图片上传: $permanentImagePath URL: $ImageUrl');
// 显示的文本,如果没有提供则使用默认值 // 显示的文本,如果没有提供则使用默认值
final displayText = text?.isNotEmpty == true ? text! : '[图片]'; final displayText = text?.isNotEmpty == true ? text! : '[图片]';
currsessionid.value = const Uuid().v4();
final sessionid = currsessionid.value;
Logger.i(TAG,
'发送AI图片消息:$sessionid $displayText $permanentImagePath URL: $ImageUrl');
// 添加用户消息 // 添加用户消息
final message = Message( final message = Message(
sessionid: sessionid,
isUser: true, isUser: true,
text: displayText, text: displayText,
hasImage: true, hasImage: true,
@ -1055,9 +1117,11 @@ class AgentController extends GetxController with WidgetsBindingObserver {
// 保存消息到持久化存储 // 保存消息到持久化存储
//await _saveMessageToStorage(message); //await _saveMessageToStorage(message);
currsessionid.value = const Uuid().v4();
final sessionid = currsessionid.value;
// 调用Agent Service处理图片,考虑TTS状态 // 调用Agent Service处理图片,考虑TTS状态
await AgentService.processImageInput( await AgentService.processImageInput(
sessionid,
relativeImagePath, relativeImagePath,
ImageUrl, ImageUrl,
text: text ?? '', text: text ?? '',
@ -1855,11 +1919,13 @@ class AgentController extends GetxController with WidgetsBindingObserver {
} }
// 修改自动停止处理,遵循TTS设置 // 修改自动停止处理,遵循TTS设置
void _handleAutoStop(int index) { void _handleAutoStop(String sessionid, int index) {
// 发送此消息到AI处理 // 发送此消息到AI处理
isProcessing.value = true; isProcessing.value = true;
// 标记为新的AI回复
_isNewAssistantResponse = true; _isNewAssistantResponse = true;
AgentService.processTextInput(messages[index].text, currsessionid.value = sessionid;
AgentService.processTextInput(sessionid, messages[index].text,
speakResponse: isTtsEnabled.value); speakResponse: isTtsEnabled.value);
} }
@ -2612,4 +2678,15 @@ class AgentController extends GetxController with WidgetsBindingObserver {
Get.snackbar('错误', '打开苹果地图时发生错误'); Get.snackbar('错误', '打开苹果地图时发生错误');
} }
} }
Future<void> updateToolCallsExpanded(
String sessionid,
bool isExpanded,
) async {
final message = messages.firstWhere(
(msg) => msg.sessionid == sessionid && msg.isUser == false,
);
message.isToolCallsExpanded = isExpanded;
messages.refresh();
}
} }

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

@ -287,6 +287,7 @@ class _AgentViewState extends State<AgentView> with WidgetsBindingObserver {
isDarkMode, details, message); isDarkMode, details, message);
}, },
child: MessageBubble( child: MessageBubble(
sessionid: message.sessionid,
isUser: message.isUser, isUser: message.isUser,
message: message.text, message: message.text,
timestamp: message.timestamp, timestamp: message.timestamp,
@ -294,6 +295,9 @@ class _AgentViewState extends State<AgentView> with WidgetsBindingObserver {
hasImage: message.hasImage, hasImage: message.hasImage,
imagePath: message.imagePath, imagePath: message.imagePath,
meta: message.meta, meta: message.meta,
functionCalls: message.functionCalls,
isToolCallsExpanded:
message.isToolCallsExpanded,
), ),
), ),
), ),
@ -307,6 +311,7 @@ class _AgentViewState extends State<AgentView> with WidgetsBindingObserver {
context, isDarkMode, details, message); context, isDarkMode, details, message);
}, },
child: MessageBubble( child: MessageBubble(
sessionid: message.sessionid,
isUser: message.isUser, isUser: message.isUser,
message: message.text, message: message.text,
timestamp: message.timestamp, timestamp: message.timestamp,
@ -314,6 +319,9 @@ class _AgentViewState extends State<AgentView> with WidgetsBindingObserver {
hasImage: message.hasImage, hasImage: message.hasImage,
imagePath: message.imagePath, imagePath: message.imagePath,
meta: message.meta, meta: message.meta,
functionCalls: message.functionCalls,
isToolCallsExpanded:
message.isToolCallsExpanded,
), ),
), ),
); );

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

@ -18,6 +18,7 @@ import '../controllers/agent_controller.dart';
/// 消息气泡组件 /// 消息气泡组件
class MessageBubble extends GetView<AgentController> { class MessageBubble extends GetView<AgentController> {
final String sessionid;
final bool isUser; final bool isUser;
final String message; final String message;
final DateTime timestamp; final DateTime timestamp;
@ -25,9 +26,11 @@ class MessageBubble extends GetView<AgentController> {
final bool hasImage; final bool hasImage;
final String? imagePath; final String? imagePath;
final Map? meta; final Map? meta;
final List<String>? functionCalls;
final bool isToolCallsExpanded;
const MessageBubble({ const MessageBubble({
Key? key, Key? key,
required this.sessionid,
required this.isUser, required this.isUser,
required this.message, required this.message,
required this.timestamp, required this.timestamp,
@ -35,6 +38,8 @@ class MessageBubble extends GetView<AgentController> {
this.hasImage = false, this.hasImage = false,
this.imagePath, this.imagePath,
this.meta, this.meta,
this.functionCalls,
this.isToolCallsExpanded = false,
}) : super(key: key); }) : super(key: key);
@override @override
@ -75,7 +80,24 @@ class MessageBubble extends GetView<AgentController> {
), ),
], ],
), ),
child: _buildMessageContent(context), child: Stack(
children: [
// 主要消息内容
Padding(
padding: (!isUser &&
functionCalls != null &&
functionCalls!.isNotEmpty)
? const EdgeInsets.only(top: 20)
: EdgeInsets.zero,
child: _buildMessageContent(context),
),
//工具调用流程显示(仅在AI消息且有functionCalls时显示)
if (!isUser &&
functionCalls != null &&
functionCalls!.isNotEmpty)
_buildFunctionCallsWidget(context),
],
),
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
// 识别中的消息不显示时间 // 识别中的消息不显示时间
@ -89,7 +111,7 @@ class MessageBubble extends GetView<AgentController> {
Text( Text(
_formatTimestamp(timestamp), _formatTimestamp(timestamp),
style: TextStyle( style: TextStyle(
fontSize: 10, fontSize: 14,
color: Theme.of(context).brightness == Brightness.dark color: Theme.of(context).brightness == Brightness.dark
? Colors.grey[400] ? Colors.grey[400]
: Colors.grey[500], : Colors.grey[500],
@ -160,10 +182,21 @@ class MessageBubble extends GetView<AgentController> {
); );
} }
if (!isUser && message.isEmpty && meta == null) { if (!isUser && message.isEmpty && functionCalls == null && meta == null) {
return _buildTypingIndicator(); return _buildTypingIndicator();
} }
// 如果只有工具调用而没有文本内容,显示一个占位符确保气泡有足够宽度
if (!isUser && message.isEmpty && functionCalls != null && meta == null) {
return Container(
constraints: const BoxConstraints(
minWidth: 200, // 确保最小宽度
minHeight: 20, // 确保最小高度
),
child: const SizedBox.shrink(), // 透明占位符
);
}
// 如果是图片消息,显示图片 // 如果是图片消息,显示图片
if (hasImage && imagePath != null) { if (hasImage && imagePath != null) {
// 如果消息是默认的[图片]标记,只显示图片 // 如果消息是默认的[图片]标记,只显示图片
@ -369,9 +402,12 @@ class MessageBubble extends GetView<AgentController> {
// } // }
} }
// 创建消息内容Widget
Widget messageContent;
// 使用Markdown渲染AI消息,用户消息仍然使用普通文本 // 使用Markdown渲染AI消息,用户消息仍然使用普通文本
if (!isUser) { if (!isUser) {
return MarkdownBody( messageContent = MarkdownBody(
data: message, data: message,
styleSheet: MarkdownStyleSheet( styleSheet: MarkdownStyleSheet(
p: TextStyle( p: TextStyle(
@ -442,21 +478,34 @@ class MessageBubble extends GetView<AgentController> {
), ),
//selectable: true, //selectable: true,
); );
} else {
// 用户消息仍然使用普通文本
messageContent = Text(
message,
style: TextStyle(
color: isUser
? Colors.white
: (Theme.of(context).brightness == Brightness.dark
? Colors.white.withOpacity(0.9)
: Colors.black87),
fontSize: 14,
height: 1.4,
fontWeight: FontWeight.w400,
),
);
} }
// 用户消息仍然使用普通文本
return Text( // 如果是AI消息且有工具调用,确保最小宽度以防止工具调用列表越界
message, if (!isUser && functionCalls != null && functionCalls!.isNotEmpty) {
style: TextStyle( return Container(
color: isUser constraints: const BoxConstraints(
? Colors.white minWidth: 200, // 确保最小宽度,防止工具调用列表越界
: (Theme.of(context).brightness == Brightness.dark ),
? Colors.white.withOpacity(0.9) child: messageContent,
: Colors.black87), );
fontSize: 14, }
height: 1.4,
fontWeight: FontWeight.w400, return messageContent;
),
);
} }
/// 构建输入中的指示器 /// 构建输入中的指示器
@ -550,6 +599,155 @@ class MessageBubble extends GetView<AgentController> {
return ''; return '';
} }
} }
/// 构建工具调用流程组件
Widget _buildFunctionCallsWidget(BuildContext context) {
return Positioned(
top: 0,
left: 0,
right: 0,
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 0, vertical: 0), // 减少垂直padding
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 可点击的标题行
GestureDetector(
onTap: () async {
await controller.updateToolCallsExpanded(
sessionid,
!isToolCallsExpanded,
);
},
child: Container(
height: 20, // 增加固定高度,给字体更多空间
decoration: BoxDecoration(
color: Theme.of(context).brightness == Brightness.dark
? Colors.grey[800]?.withOpacity(0.8)
: Colors.grey[100]?.withOpacity(0.9),
borderRadius: BorderRadius.circular(6),
border: Border.all(
color: Theme.of(context).brightness == Brightness.dark
? Colors.white.withOpacity(0.1)
: Colors.black.withOpacity(0.1),
width: 0.5,
),
),
child: Row(
children: [
// 显示最后一个工具名称
Expanded(
child: Text(
functionCalls != null && functionCalls!.isNotEmpty
? functionCalls!.last
: '工具调用',
style: TextStyle(
fontSize: 12, // 适当增大字体
color:
Theme.of(context).brightness == Brightness.dark
? Colors.white.withOpacity(0.7)
: Colors.black54,
fontWeight: FontWeight.w500,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(width: 4),
// 箭头放在右侧
Icon(
isToolCallsExpanded
? Icons.keyboard_arrow_up // 展开时显示向上箭头
: Icons.keyboard_arrow_down, // 收起时显示向下箭头
size: 14, // 减小图标大小
color: Theme.of(context).brightness == Brightness.dark
? Colors.white.withOpacity(0.7)
: Colors.black54,
),
],
),
),
),
// 展开的工具调用列表
if (isToolCallsExpanded && functionCalls != null)
Container(
margin: const EdgeInsets.only(top: 2), // 减少顶部间距
padding: const EdgeInsets.all(6), // 减少内边距
constraints: const BoxConstraints(
maxHeight: 120, // 限制最大高度,避免超界
),
decoration: BoxDecoration(
// 添加更强的背景色来遮挡下层UI
color: Theme.of(context).brightness == Brightness.dark
? Colors.grey[900]?.withOpacity(0.95) // 深色模式下使用深灰色背景
: Colors.white.withOpacity(0.95), // 浅色模式下使用白色背景
borderRadius: BorderRadius.circular(4), // 减小圆角
border: Border.all(
color: Theme.of(context).brightness == Brightness.dark
? Colors.white.withOpacity(0.2)
: Colors.black.withOpacity(0.2),
width: 0.5,
),
// 添加阴影增强遮挡效果
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: SingleChildScrollView(
// 添加滚动,防止超界
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: functionCalls!.asMap().entries.map((entry) {
int index = entry.key;
String call = entry.value;
return Padding(
padding: EdgeInsets.only(
bottom: index < functionCalls!.length - 1
? 2
: 0), // 减少间距
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${index + 1}. ',
style: TextStyle(
fontSize: 12, // 修改为更小的字体
color: Theme.of(context).brightness ==
Brightness.dark
? Colors.white.withOpacity(0.6)
: Colors.black45,
fontWeight: FontWeight.w500,
),
),
Expanded(
child: Text(
call,
style: TextStyle(
fontSize: 12, // 修改为更小的字体
color: Theme.of(context).brightness ==
Brightness.dark
? Colors.white.withOpacity(0.8)
: Colors.black87,
height: 1.1, // 减小行高
),
),
),
],
),
);
}).toList(),
),
),
),
],
),
));
}
} }
/// 动画点组件 /// 动画点组件

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

@ -27,7 +27,6 @@ import kotlinx.coroutines.sync.Mutex
import com.deep_voice.speech.tts.TtsEvent import com.deep_voice.speech.tts.TtsEvent
import com.deep_voice.speech.tts.TtsEventListener import com.deep_voice.speech.tts.TtsEventListener
import com.deep_voice.speech.tts.TtsEventType import com.deep_voice.speech.tts.TtsEventType
import java.util.UUID
import okhttp3.* import okhttp3.*
import okhttp3.MediaType.Companion.toMediaType import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.RequestBody.Companion.toRequestBody
@ -88,7 +87,7 @@ object AgentService : CoroutineScope {
private lateinit var chatStorageHelper: ChatStorageHelper private lateinit var chatStorageHelper: ChatStorageHelper
// 会话ID,用于区分不同聊天上下文 // 会话ID,用于区分不同聊天上下文
private val sessionId = "default_agent" private val agentId = "default_agent"
// 服务端连接 // 服务端连接
private var serverurl = "" private var serverurl = ""
private var usertoken = "" private var usertoken = ""
@ -602,7 +601,7 @@ object AgentService : CoroutineScope {
Log.d(TAG, "recognizeCallback") Log.d(TAG, "recognizeCallback")
val success = azureAsrHelper?.setupEventListeners(object : val success = azureAsrHelper?.setupEventListeners(object :
AzureAsrHelper.ContinuousRecognizeCallback { AzureAsrHelper.ContinuousRecognizeCallback {
override fun onRecognizing(recognizing: String, detectedLanguage: String) { override fun onRecognizing(sessiondid:String, recognizing: String, detectedLanguage: String) {
if (recognizing.isNotEmpty()&&isKeepResult==true) { if (recognizing.isNotEmpty()&&isKeepResult==true) {
// 检测到语音,更新状态 // 检测到语音,更新状态
@ -646,16 +645,17 @@ object AgentService : CoroutineScope {
} }
} }
override fun onResult(result: String, detectedLanguage: String) { override fun onResult(sessiondid:String,result: String, detectedLanguage: String) {
if (result.isNotEmpty()&&isKeepResult==true) {//加入是否结束要输出最后的结果判读 if (result.isNotEmpty()&&isKeepResult==true) {//加入是否结束要输出最后的结果判读
sendEvent( sendEvent(
"recognition_result", mapOf( "recognition_result", mapOf(
"sessiondid" to sessiondid,
"text" to result, "text" to result,
"language" to detectedLanguage "language" to detectedLanguage
) )
) )
processWithChatApi(result) processWithChatApi(sessiondid,result)
} }
@ -665,8 +665,8 @@ object AgentService : CoroutineScope {
} }
override fun onSessionStarted() { override fun onSessionStarted(sessiondid:String) {
sendEvent("recognition_started", mapOf("status" to "started")) sendEvent("recognition_started", mapOf("sessiondid" to sessiondid, "status" to "started"))
// 使用模式感知的空闲检测启动方法 // 使用模式感知的空闲检测启动方法
audioPlayer?.playAudio(R.raw.start) audioPlayer?.playAudio(R.raw.start)
@ -674,33 +674,35 @@ object AgentService : CoroutineScope {
override fun onSessionStopped() { override fun onSessionStopped(sessiondid:String) {
sendEvent("recognition_stopped", mapOf("status" to "stopped")) sendEvent("recognition_stopped", mapOf("sessiondid" to sessiondid, "status" to "stopped"))
_isRecognitionActive.set(false) _isRecognitionActive.set(false)
stopIdleCheck() stopIdleCheck()
audioPlayer?.playAudio(R.raw.stop) audioPlayer?.playAudio(R.raw.stop)
} }
override fun onCanceled(reason: String, errorDetails: String) { override fun onCanceled(sessiondid: String,reason: String, errorDetails: String) {
_isRecognitionActive.set(false) _isRecognitionActive.set(false)
stopIdleCheck() stopIdleCheck()
Log.d(TAG, "recognition_canceled:") Log.d(TAG, "recognition_canceled:")
BleService.closeCodec() BleService.closeCodec()
sendEvent( sendEvent(
"recognition_canceled", mapOf( "recognition_canceled", mapOf(
"sessiondid" to sessiondid,
"reason" to reason, "reason" to reason,
"details" to errorDetails "details" to errorDetails
) )
) )
} }
override fun onError(code: Int, error: String) { override fun onError(sessiondid:String, code: Int, error: String) {
_isRecognitionActive.set(false) _isRecognitionActive.set(false)
stopIdleCheck() stopIdleCheck()
BleService.closeCodec() BleService.closeCodec()
Log.d(TAG, "语音识别错误: $error") Log.d(TAG, "语音识别错误: $error")
sendEvent( sendEvent(
"error", mapOf( "error", mapOf(
"sessiondid" to sessiondid,
"code" to code, "code" to code,
"message" to error "message" to error
) )
@ -802,7 +804,7 @@ object AgentService : CoroutineScope {
/** /**
* 处理文本输入 * 处理文本输入
*/ */
fun processTextInput(text: String, speakResponse: Boolean = false): Boolean { fun processTextInput(sessionid: 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 "服务未初始化"))
@ -814,17 +816,16 @@ object AgentService : CoroutineScope {
sendEvent("error", mapOf("code" to "EMPTY_TEXT", "message" to "文本输入不能为空")) sendEvent("error", mapOf("code" to "EMPTY_TEXT", "message" to "文本输入不能为空"))
return false return false
} }
// 使用ChatAPI处理文本 // 使用ChatAPI处理文本
processWithChatApi(text, speakResponse) processWithChatApi(sessionid,text, speakResponse)
return true return true
} }
/** /**
* 使用ChatAPI处理语音识别结果 * 使用ChatAPI处理语音识别结果
*/ */
private fun processWithChatApi(text: String) { private fun processWithChatApi(sessionid:String,text: String) {
processWithChatApi(text, true) processWithChatApi(sessionid,text, true)
} }
/** /**
@ -834,6 +835,7 @@ object AgentService : CoroutineScope {
* @param speakResponse 是否使用TTS朗读回复 * @param speakResponse 是否使用TTS朗读回复
*/ */
private fun processWithChatApi( private fun processWithChatApi(
sessionid: String,
text: String, text: String,
speakResponse: Boolean = true speakResponse: Boolean = true
) { ) {
@ -841,7 +843,7 @@ object AgentService : CoroutineScope {
// 创建用户文本消息并处理 // 创建用户文本消息并处理
val userMessage = createUserMessage(text) val userMessage = createUserMessage(text)
processWithChatApiInternal(userMessage, text, speakResponse) processWithChatApiInternal(sessionid,userMessage, text, speakResponse)
} }
/** /**
@ -852,6 +854,7 @@ object AgentService : CoroutineScope {
* @param speakResponse 是否朗读回复 * @param speakResponse 是否朗读回复
*/ */
private fun processImageWithChatApi( private fun processImageWithChatApi(
sessionid: String,
imageBase64: String, imageBase64: String,
text: String = "", text: String = "",
speakResponse: Boolean = false speakResponse: Boolean = false
@ -861,7 +864,7 @@ object AgentService : CoroutineScope {
Log.d(TAG, "liwei--------- 图像Mesage: ${userMessage}") Log.d(TAG, "liwei--------- 图像Mesage: ${userMessage}")
// 图片描述用于存储 // 图片描述用于存储
val displayText = text.ifEmpty { "[图片]" } val displayText = text.ifEmpty { "[图片]" }
processWithChatApiInternal(userMessage, displayText, speakResponse, true) processWithChatApiInternal(sessionid,userMessage, displayText, speakResponse, true)
} }
/** /**
@ -874,21 +877,21 @@ object AgentService : CoroutineScope {
* @param imagePath 图片路径(如果有) * @param imagePath 图片路径(如果有)
*/ */
private fun processWithChatApiInternal( private fun processWithChatApiInternal(
sessionid: String,
userMessage: JSONObject, userMessage: JSONObject,
displayText: String, displayText: String,
speakResponse: Boolean = true, speakResponse: Boolean = true,
hasImage: Boolean = false, hasImage: Boolean = false,
imagePath: String? = null imagePath: String? = null
) { ) {
Log.d(TAG, "内部方法:通用的ChatAPI处理逻辑")
// 如果有正在进行的AI流式输出,先停止它 // 如果有正在进行的AI流式输出,先停止它
audioPlayer?.stopAudio() audioPlayer?.stopAudio()
stopAiStream() stopAiStream()
stopTts() stopTts()
currsessionId = sessionid
// 记录新的AI任务 // 记录新的AI任务
currentAiJob = launch { currentAiJob = launch {
try { try {
currsessionId = UUID.randomUUID().toString()
// 设置状态为正在流式输出 // 设置状态为正在流式输出
_isAiStreaming.set(true) _isAiStreaming.set(true)
Log.d(TAG, "processWithChatApiInternal") Log.d(TAG, "processWithChatApiInternal")
@ -958,7 +961,7 @@ object AgentService : CoroutineScope {
} }
// Log.e("ChatApiService", "liwei---------sendMessageStream messages:${messagesWithSystemPrompt}") // Log.e("ChatApiService", "liwei---------sendMessageStream messages:${messagesWithSystemPrompt}")
chatApiService.sendMessageStream( chatApiService.sendMessageStream(
currsessionId, sessionid,
messagesWithSystemPrompt, messagesWithSystemPrompt,
object : StreamCallback { object : StreamCallback {
@ -984,15 +987,17 @@ object AgentService : CoroutineScope {
override fun onToken(sessionid: String, token: String) { override fun onToken(sessionid: String, token: String) {
try { try {
if (sessionid != currsessionId) { if (sessionid != currsessionId) {
// Log.d(TAG, "liwei--------------- AI Call onToken 会话过期 $sessionid-------$currsessionId")
return return
} }
responseBuilder.append(token) responseBuilder.append(token)
// Log.d(TAG, "liwei--------------- AI Call onToken:$token speakResponse:$speakResponse reply:$reply broadcast:$broadcast")
if (speakResponse && reply && broadcast && token.isNotEmpty()) { if (speakResponse && reply && broadcast && token.isNotEmpty()) {
ttsService?.speakStream(token) ttsService?.speakStream(token)
} }
if (reply && token.isNotEmpty()) { if (reply && token.isNotEmpty()) {
// 发送流式回复token // 发送流式回复token
sendEvent("assistant_token", mapOf("token" to token)) sendEvent("assistant_token", mapOf("sessionid" to sessionid,"token" to token))
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "liwei--------------- AI Call onToken 异常", e) Log.e(TAG, "liwei--------------- AI Call onToken 异常", e)
@ -1011,6 +1016,7 @@ object AgentService : CoroutineScope {
val response = responseBuilder.toString() val response = responseBuilder.toString()
// 发送完整回复,包含是否有图片的标记 // 发送完整回复,包含是否有图片的标记
val responseData = mutableMapOf<String, Any>( val responseData = mutableMapOf<String, Any>(
"sessionid" to sessionid,
"text" to response, "text" to response,
"userInput" to displayText "userInput" to displayText
) )
@ -1027,6 +1033,7 @@ object AgentService : CoroutineScope {
} }
// 保存聊天记录 // 保存聊天记录
saveChatMessage( saveChatMessage(
sessionid,
displayText, displayText,
response, response,
aiMetadata, aiMetadata,
@ -1045,6 +1052,7 @@ object AgentService : CoroutineScope {
Log.e(TAG, "AI处理出错", e) Log.e(TAG, "AI处理出错", e)
sendEvent( sendEvent(
"error", mapOf( "error", mapOf(
"sessionid" to sessionid,
"code" to "AI_ERROR", "code" to "AI_ERROR",
"message" to e.message.toString() "message" to e.message.toString()
) )
@ -1064,6 +1072,7 @@ object AgentService : CoroutineScope {
val name = call.getString("name") val name = call.getString("name")
sendEvent( sendEvent(
"function_call", mapOf( "function_call", mapOf(
"sessionid" to sessionid,
"name" to name, "name" to name,
"arguments" to call.toString() "arguments" to call.toString()
) )
@ -1072,6 +1081,10 @@ object AgentService : CoroutineScope {
BleService.closeCodec() BleService.closeCodec()
stopRecognition() stopRecognition()
} }
//播报工具调用
ttsService?.speakStream("正在调用工具:${name}")
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "liwei--------------- AI Call onFunctionCall 异常", e) Log.e(TAG, "liwei--------------- AI Call onFunctionCall 异常", e)
} }
@ -1083,6 +1096,11 @@ object AgentService : CoroutineScope {
functionCallResult: JSONObject functionCallResult: JSONObject
) { ) {
try { try {
Log.d(
TAG,
"liwei--------------- AI Call onFunctionCallResult ${sessionid}:${functionCallResult.toString()}",
)
audioPlayer?.stopAudio() audioPlayer?.stopAudio()
val name = functionCall.get("name") as String; val name = functionCall.get("name") as String;
val resultmap = autoHandleFcunCallResult(name, functionCallResult); val resultmap = autoHandleFcunCallResult(name, functionCallResult);
@ -1093,6 +1111,7 @@ object AgentService : CoroutineScope {
aiMetadata = resultmap["meta"] as String; aiMetadata = resultmap["meta"] as String;
sendEvent( sendEvent(
"function_call_result", mapOf( "function_call_result", mapOf(
"sessionid" to sessionid,
"function_call" to functionCall.toString(), "function_call" to functionCall.toString(),
"result" to functionCallResult.toString(), "result" to functionCallResult.toString(),
"meta" to aiMetadata, "meta" to aiMetadata,
@ -1101,16 +1120,12 @@ object AgentService : CoroutineScope {
}else{ }else{
sendEvent( sendEvent(
"function_call_result", mapOf( "function_call_result", mapOf(
"sessionid" to sessionid,
"function_call" to functionCall.toString(), "function_call" to functionCall.toString(),
"result" to functionCallResult.toString(), "result" to functionCallResult.toString(),
) )
) )
} }
Log.e(
TAG,
"liwei--------------- AI Call onFunctionCallResult broadcast:$broadcast reply:$reply iscard:$iscard"
)
} catch (e: Exception) { } catch (e: Exception) {
Log.e( Log.e(
TAG, TAG,
@ -1127,6 +1142,7 @@ object AgentService : CoroutineScope {
Log.e(TAG, "liwei--------------- AI处理出错", e) Log.e(TAG, "liwei--------------- AI处理出错", e)
sendEvent( sendEvent(
"error", mapOf( "error", mapOf(
"sessionid" to sessionid,
"code" to errorType, "code" to errorType,
"message" to e.message.toString() "message" to e.message.toString()
) )
@ -1152,7 +1168,7 @@ object AgentService : CoroutineScope {
} }
// 获取最近消息 // 获取最近消息
val recentMessages = chatStorageHelper.getRecentMessages(sessionId, 10) val recentMessages = chatStorageHelper.getRecentMessages(agentId, 10)
if (recentMessages.isEmpty()) { if (recentMessages.isEmpty()) {
return return
} }
@ -1389,6 +1405,7 @@ object AgentService : CoroutineScope {
* 保存聊天消息 * 保存聊天消息
*/ */
private fun saveChatMessage( private fun saveChatMessage(
sessionid: String,
userMessage: String, userMessage: String,
assistantMessage: String, assistantMessage: String,
aiMetadata: String, aiMetadata: String,
@ -1399,7 +1416,8 @@ object AgentService : CoroutineScope {
try { try {
// 保存用户消息 // 保存用户消息
val userMessageId = chatStorageHelper.saveMessage( val userMessageId = chatStorageHelper.saveMessage(
sessionId = sessionId, agentId = agentId,
sessionId = sessionid,
message = userMessage, message = userMessage,
sender = "user", sender = "user",
metadata = userMetadata metadata = userMetadata
@ -1409,7 +1427,8 @@ object AgentService : CoroutineScope {
if (assistantMessage.isNotEmpty() || aiMetadata.isNotEmpty()) { if (assistantMessage.isNotEmpty() || aiMetadata.isNotEmpty()) {
// 保存AI回复 // 保存AI回复
val assistantMessageId = chatStorageHelper.saveMessage( val assistantMessageId = chatStorageHelper.saveMessage(
sessionId = sessionId, agentId = agentId,
sessionId = sessionid,
message = assistantMessage, message = assistantMessage,
sender = "assistant", sender = "assistant",
metadata = aiMetadata metadata = aiMetadata
@ -1435,7 +1454,7 @@ object AgentService : CoroutineScope {
fun clearChatHistory(callback: (Boolean) -> Unit) { fun clearChatHistory(callback: (Boolean) -> Unit) {
launch { launch {
try { try {
val success = chatStorageHelper.deleteMessages(sessionId) val success = chatStorageHelper.deleteMessages(agentId)
if (success) { if (success) {
synchronized(historyMessages) { synchronized(historyMessages) {
@ -1542,6 +1561,7 @@ object AgentService : CoroutineScope {
* 处理图片输入 * 处理图片输入
*/ */
fun processImageInput( fun processImageInput(
sessionid: String,
imagePath: String, imagePath: String,
imageUrl: String, imageUrl: String,
text: String = "", text: String = "",
@ -1593,7 +1613,7 @@ object AgentService : CoroutineScope {
) )
Log.d(TAG, "liwei--------- 图像Mesage: ${userMessage}") Log.d(TAG, "liwei--------- 图像Mesage: ${userMessage}")
// 处理消息并传入存储路径 // 处理消息并传入存储路径
processWithChatApiInternal(userMessage, displayText, speakResponse, true, imagePath) processWithChatApiInternal(sessionid,userMessage, displayText, speakResponse, true, imagePath)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "处理图片失败: ${e.message}") Log.e(TAG, "处理图片失败: ${e.message}")
sendEvent( sendEvent(

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

@ -144,7 +144,12 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream
} }
"processTextInput" -> { "processTextInput" -> {
val sessionid = call.argument<String>("sessionid")
val text = call.argument<String>("text") val text = call.argument<String>("text")
if (sessionid.isNullOrEmpty()) {
result.error("INVALID_ARGUMENTS", "回话id不能为空", null)
return
}
if (text.isNullOrEmpty()) { if (text.isNullOrEmpty()) {
result.error("INVALID_ARGUMENTS", "文本不能为空", null) result.error("INVALID_ARGUMENTS", "文本不能为空", null)
return return
@ -154,6 +159,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream
try { try {
val isProcessing = AgentService.processTextInput( val isProcessing = AgentService.processTextInput(
sessionid = sessionid,
text = text, text = text,
speakResponse = speakResponse speakResponse = speakResponse
) )
@ -166,6 +172,11 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream
} }
"processImageInput" -> { "processImageInput" -> {
val sessionid = call.argument<String>("sessionid")
if (sessionid.isNullOrEmpty()) {
result.error("INVALID_ARGUMENTS", "回话id不能为空", null)
return
}
val imagePath = call.argument<String>("imagePath") val imagePath = call.argument<String>("imagePath")
if (imagePath.isNullOrEmpty()) { if (imagePath.isNullOrEmpty()) {
result.error("INVALID_ARGUMENTS", "图片路径不能为空", null) result.error("INVALID_ARGUMENTS", "图片路径不能为空", null)
@ -181,6 +192,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream
try { try {
val isProcessing = AgentService.processImageInput( val isProcessing = AgentService.processImageInput(
sessionid=sessionid,
imagePath = imagePath, imagePath = imagePath,
imageUrl = imageUrl, imageUrl = imageUrl,
text = text, text = text,

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

@ -85,8 +85,8 @@ Log.d(TAG, "手动启动语音识别: ")
/** /**
* 处理文本输入 * 处理文本输入
*/ */
fun processTextInput(text: String, speakResponse: Boolean = false): Boolean { fun processTextInput(sessionId: String,text: String, speakResponse: Boolean = false): Boolean {
return AgentService.processTextInput(text, speakResponse) return AgentService.processTextInput(sessionId,text, speakResponse)
} }
/** /**

45
local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift

@ -59,7 +59,7 @@ class AgentServiceImpl: NSObject {
} }
// 会话ID,用于区分不同聊天上下文 // 会话ID,用于区分不同聊天上下文
private let sessionId = "default_agent" private let agentId = "default_agent"
private var isInitialized: Bool = false private var isInitialized: Bool = false
private var isRecognizing: Bool = false private var isRecognizing: Bool = false
@ -565,7 +565,7 @@ audioStream.saveAudioDataTo(data: audioData)
return interrupted return interrupted
} }
func processTextInput(_ text: String, speakResponse: Bool) -> Bool { func processTextInput(sessionid:String, _ text: String, speakResponse: Bool) -> Bool {
os_log("收到文本输入: text=%{public}@, speakResponse=%{public}@", log: logger, type: .info, text, speakResponse ? "是" : "否") os_log("收到文本输入: text=%{public}@, speakResponse=%{public}@", log: logger, type: .info, text, speakResponse ? "是" : "否")
if !isInitialized { if !isInitialized {
@ -581,21 +581,21 @@ audioStream.saveAudioDataTo(data: audioData)
} }
os_log("开始处理文本输入", log: logger, type: .info) os_log("开始处理文本输入", log: logger, type: .info)
processWithChatApiService(text: text, speakResponse: speakResponse) processWithChatApiService(sessionid: sessionid, text: text, speakResponse: speakResponse)
return true return true
} }
private func processWithChatApiService(text: String, speakResponse: Bool = true) { private func processWithChatApiService(sessionid:String,text: String, speakResponse: Bool = true) {
guard let chatApiService = chatApiService else { guard let chatApiService = chatApiService else {
sendError("ChatAPI服务未初始化", code: "CHATAPI_NOT_INITIALIZED") sendError("ChatAPI服务未初始化", code: "CHATAPI_NOT_INITIALIZED")
return return
} }
let userMessage = chatApiService.createUserMessage(content: text) let userMessage = chatApiService.createUserMessage(content: text)
processWithChatApiServiceInternal(userMessage: userMessage, displayText: text, speakResponse: speakResponse) processWithChatApiServiceInternal(sessionid: sessionid,userMessage: userMessage, displayText: text, speakResponse: speakResponse)
} }
private func processWithChatApiServiceInternal(userMessage: [String: Any], displayText: String, speakResponse: Bool = true, hasImage: Bool = false,imagePath:String = "") { private func processWithChatApiServiceInternal(sessionid:String,userMessage: [String: Any], displayText: String, speakResponse: Bool = true, hasImage: Bool = false,imagePath:String = "") {
os_log("开始处理ChatAPI请求: displayText=%{public}@, speakResponse=%{public}@, hasImage=%{public}@", os_log("开始处理ChatAPI请求: displayText=%{public}@, speakResponse=%{public}@, hasImage=%{public}@",
log: logger, type: .info, displayText, speakResponse ? "是" : "否", hasImage ? "是" : "否") log: logger, type: .info, displayText, speakResponse ? "是" : "否", hasImage ? "是" : "否")
@ -650,7 +650,7 @@ audioStream.saveAudioDataTo(data: audioData)
hasImage: hasImage, hasImage: hasImage,
imagePath: imagePath imagePath: imagePath
) )
currsessionId = UUID().uuidString currsessionId = sessionid
azureTtsHelper?.startspeak(sessionid: currsessionId) azureTtsHelper?.startspeak(sessionid: currsessionId)
os_log("设置回调并调用sendMessageStream", log: logger, type: .info) os_log("设置回调并调用sendMessageStream", log: logger, type: .info)
chatApiService.setStreamCallback(currsessionId,callback) chatApiService.setStreamCallback(currsessionId,callback)
@ -659,7 +659,7 @@ audioStream.saveAudioDataTo(data: audioData)
os_log("sendMessageStream调用完成", log: logger, type: .info) os_log("sendMessageStream调用完成", log: logger, type: .info)
} }
func processImageInput(imagePath: String,imageUrl: String, text: String, speakResponse: Bool) -> Bool { func processImageInput(sessionid: String,imagePath: String,imageUrl: String, text: String, speakResponse: Bool) -> Bool {
if !isInitialized { if !isInitialized {
sendError("服务未初始化", code: "NOT_INITIALIZED") sendError("服务未初始化", code: "NOT_INITIALIZED")
return false return false
@ -703,6 +703,7 @@ audioStream.saveAudioDataTo(data: audioData)
let displayText = text.isEmpty ? "[图片]" : text let displayText = text.isEmpty ? "[图片]" : text
self.processWithChatApiServiceInternal( self.processWithChatApiServiceInternal(
sessionid: sessionid,
userMessage: userMessage, userMessage: userMessage,
displayText: displayText, displayText: displayText,
speakResponse: speakResponse, speakResponse: speakResponse,
@ -731,7 +732,7 @@ audioStream.saveAudioDataTo(data: audioData)
chatHistory.removeAll() chatHistory.removeAll()
// 获取最近10条消息 // 获取最近10条消息
let recentMessages = chatStorageHelper.getRecentMessages(sessionId: sessionId, limit: 10) let recentMessages = chatStorageHelper.getRecentMessages(agentId: agentId, limit: 10)
if recentMessages.isEmpty { if recentMessages.isEmpty {
return return
@ -763,11 +764,12 @@ audioStream.saveAudioDataTo(data: audioData)
/** /**
* 保存聊天消息 * 保存聊天消息
*/ */
internal func saveChatMessage(userMessage: String, assistantMessage: String, metadata: String = "",userMetadata: String) { internal func saveChatMessage(sessionId:String,userMessage: String, assistantMessage: String, metadata: String = "",userMetadata: String) {
DispatchQueue.global(qos: .utility).async { DispatchQueue.global(qos: .utility).async {
// 保存用户消息 // 保存用户消息
let userMessageId = self.chatStorageHelper.saveMessage( let userMessageId = self.chatStorageHelper.saveMessage(
sessionId: self.sessionId, agentId: self.agentId,
sessionId: sessionId,
message: userMessage, message: userMessage,
sender: "user", sender: "user",
metadata: userMetadata metadata: userMetadata
@ -777,7 +779,8 @@ audioStream.saveAudioDataTo(data: audioData)
if !assistantMessage.isEmpty || !metadata.isEmpty { if !assistantMessage.isEmpty || !metadata.isEmpty {
// 保存AI回复 // 保存AI回复
let assistantMessageId = self.chatStorageHelper.saveMessage( let assistantMessageId = self.chatStorageHelper.saveMessage(
sessionId: self.sessionId, agentId: self.agentId,
sessionId: sessionId,
message: assistantMessage, message: assistantMessage,
sender: "assistant", sender: "assistant",
metadata: metadata.isEmpty ? nil : metadata metadata: metadata.isEmpty ? nil : metadata
@ -1348,7 +1351,7 @@ private func jsonToString(_ json: [String: Any]) -> String? {
chatHistory.removeAll() chatHistory.removeAll()
DispatchQueue.global(qos: .utility).async { DispatchQueue.global(qos: .utility).async {
let success = self.chatStorageHelper.deleteMessages(sessionId: self.sessionId, messageIds: nil as [Int]?) let success = self.chatStorageHelper.deleteMessages(agentId:self.agentId, messageIds: nil as [Int]?)
if !success { if !success {
os_log("清除聊天历史失败", log: self.logger, type: .error) os_log("清除聊天历史失败", log: self.logger, type: .error)
} }
@ -1681,7 +1684,7 @@ class ChatApiStreamCallback: StreamCallback {
try agentService.addToHistoryMessages(assistantMessage) try agentService.addToHistoryMessages(assistantMessage)
} }
// 保存聊天记录 // 保存聊天记录
try agentService.saveChatMessage(userMessage: displayText, assistantMessage: response, metadata: metadata,userMetadata:toJSONString(userMetadata)) try agentService.saveChatMessage(sessionId: sessionId,userMessage: displayText, assistantMessage: response, metadata: metadata,userMetadata:toJSONString(userMetadata))
agentService.isAiStreaming = false agentService.isAiStreaming = false
os_log("设置AI流式状态为false", log: agentService.logger, type: .info) os_log("设置AI流式状态为false", log: agentService.logger, type: .info)
}catch{ }catch{
@ -2061,7 +2064,7 @@ class AudioPlayer {
} }
extension AgentServiceImpl: AzureAsrHelper.ContinuousRecognizeCallback { extension AgentServiceImpl: AzureAsrHelper.ContinuousRecognizeCallback {
func onResult(_ text: String, _ detectedLanguage: String) { func onResult(sessionid:String ,_ text: String, _ detectedLanguage: String) {
if !text.isEmpty && isKeepResult { if !text.isEmpty && isKeepResult {
var data: [String: Any] = ["text": text] var data: [String: Any] = ["text": text]
if !detectedLanguage.isEmpty { if !detectedLanguage.isEmpty {
@ -2069,7 +2072,7 @@ extension AgentServiceImpl: AzureAsrHelper.ContinuousRecognizeCallback {
} }
sendEvent(name: "recognition_result", data: data) sendEvent(name: "recognition_result", data: data)
processTextInput(text, speakResponse: true) processTextInput(sessionid: sessionid,text, speakResponse: true)
} }
let previousHasSpeech = hasSpeechDetected let previousHasSpeech = hasSpeechDetected
@ -2080,7 +2083,7 @@ extension AgentServiceImpl: AzureAsrHelper.ContinuousRecognizeCallback {
} }
} }
func onRecognizing(_ recognizing: String, _ detectedLanguage: String) { func onRecognizing(sessionid:String ,_ recognizing: String, _ detectedLanguage: String) {
if !recognizing.isEmpty && isKeepResult { if !recognizing.isEmpty && isKeepResult {
let previousHasSpeech = hasSpeechDetected let previousHasSpeech = hasSpeechDetected
hasSpeechDetected = true hasSpeechDetected = true
@ -2102,7 +2105,7 @@ extension AgentServiceImpl: AzureAsrHelper.ContinuousRecognizeCallback {
} }
} }
func onSessionStarted() { func onSessionStarted(sessionid:String) {
sendEvent(name: "recognition_started", data: ["status": "started"]) sendEvent(name: "recognition_started", data: ["status": "started"])
isRecognizing = true isRecognizing = true
hasSpeechDetected = false hasSpeechDetected = false
@ -2123,7 +2126,7 @@ extension AgentServiceImpl: AzureAsrHelper.ContinuousRecognizeCallback {
} }
} }
func onSessionStopped() { func onSessionStopped(sessionid:String) {
sendEvent(name: "recognition_stopped", data: ["status": "stopped"]) sendEvent(name: "recognition_stopped", data: ["status": "stopped"])
isRecognizing = false isRecognizing = false
hasSpeechDetected = false hasSpeechDetected = false
@ -2133,7 +2136,7 @@ extension AgentServiceImpl: AzureAsrHelper.ContinuousRecognizeCallback {
} }
} }
func onCanceled(_ reason: String, _ errorDetails: String) { func onCanceled(sessionid:String ,_ reason: String, _ errorDetails: String) {
var data: [String: Any] = [:] var data: [String: Any] = [:]
if !reason.isEmpty { if !reason.isEmpty {
data["reason"] = reason data["reason"] = reason
@ -2150,7 +2153,7 @@ extension AgentServiceImpl: AzureAsrHelper.ContinuousRecognizeCallback {
stopIdleCheck() stopIdleCheck()
} }
func onError(_ errorCode: Int, _ error: String) { func onError(sessionid:String ,_ errorCode: Int, _ error: String) {
let data: [String: Any] = ["message": error.isEmpty ? "未知错误" : error] let data: [String: Any] = ["message": error.isEmpty ? "未知错误" : error]
sendEvent(name: "error", data: data) sendEvent(name: "error", data: data)
isRecognizing = false isRecognizing = false

6
local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServicePlugin.swift

@ -104,17 +104,19 @@ public class AgentServicePlugin: NSObject, FlutterPlugin {
return return
case "processTextInput": case "processTextInput":
guard let arguments = call.arguments as? [String: Any], guard let arguments = call.arguments as? [String: Any],
let sessionid = arguments["sessionid"] as? String,
let text = arguments["text"] as? String else { let text = arguments["text"] as? String else {
result(FlutterError(code: "INVALID_ARGUMENTS", message: "文本不能为空", details: nil)) result(FlutterError(code: "INVALID_ARGUMENTS", message: "文本不能为空", details: nil))
return return
} }
let speakResponse = arguments["speakResponse"] as? Bool ?? false let speakResponse = arguments["speakResponse"] as? Bool ?? false
let success = impl.processTextInput(text, speakResponse: speakResponse) let success = impl.processTextInput(sessionid: sessionid,text, speakResponse: speakResponse)
result(success) result(success)
case "processImageInput": case "processImageInput":
guard let arguments = call.arguments as? [String: Any], guard let arguments = call.arguments as? [String: Any],
let sessionid = arguments["sessionid"] as? String,
let imageUrl = arguments["imageUrl"] as? String, let imageUrl = arguments["imageUrl"] as? String,
let imagePath = arguments["imagePath"] as? String else { let imagePath = arguments["imagePath"] as? String else {
result(FlutterError(code: "INVALID_ARGUMENTS", message: "图片路径不能为空", details: nil)) result(FlutterError(code: "INVALID_ARGUMENTS", message: "图片路径不能为空", details: nil))
@ -123,7 +125,7 @@ public class AgentServicePlugin: NSObject, FlutterPlugin {
let text = arguments["text"] as? String ?? "" let text = arguments["text"] as? String ?? ""
let speakResponse = arguments["speakResponse"] as? Bool ?? false let speakResponse = arguments["speakResponse"] as? Bool ?? false
let success = impl.processImageInput(imagePath: imagePath,imageUrl: imageUrl, text: text, speakResponse: speakResponse) let success = impl.processImageInput(sessionid: sessionid,imagePath: imagePath,imageUrl: imageUrl, text: text, speakResponse: speakResponse)
result(success) result(success)
case "speakText": case "speakText":

7
local_plugins/agent_service/lib/agent_service.dart

@ -359,10 +359,11 @@ class AgentService {
/// [text] 文本内容 /// [text] 文本内容
/// [speakResponse] 是否朗读响应 /// [speakResponse] 是否朗读响应
/// 返回是否成功处理文本 /// 返回是否成功处理文本
static Future<bool> processTextInput(String text, static Future<bool> processTextInput(String sessionid, String text,
{bool speakResponse = false}) async { {bool speakResponse = false}) async {
try { try {
final bool result = await _channel.invokeMethod('processTextInput', { final bool result = await _channel.invokeMethod('processTextInput', {
'sessionid': sessionid,
'text': text, 'text': text,
'speakResponse': speakResponse, 'speakResponse': speakResponse,
}); });
@ -440,10 +441,12 @@ class AgentService {
/// [imageUrl] 图片url /// [imageUrl] 图片url
/// [speakResponse] 是否朗读响应 /// [speakResponse] 是否朗读响应
/// 返回是否成功处理图片 /// 返回是否成功处理图片
static Future<bool> processImageInput(String imagePath, String imageUrl, static Future<bool> processImageInput(
String sessionid, 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', {
'sessionid': sessionid,
'imagePath': imagePath, 'imagePath': imagePath,
'imageUrl': imageUrl, 'imageUrl': imageUrl,
'text': text, 'text': text,

41
local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt

@ -13,6 +13,7 @@ import java.util.concurrent.TimeUnit
import com.yunqiinnovation.azure_speech.tools.RecordFile import com.yunqiinnovation.azure_speech.tools.RecordFile
import com.microsoft.cognitiveservices.speech.audio.AudioConfig import com.microsoft.cognitiveservices.speech.audio.AudioConfig
import com.yunqiinnovation.azure_speech.tools.AudioRecordingForegroundService import com.yunqiinnovation.azure_speech.tools.AudioRecordingForegroundService
import java.util.UUID
/** /**
@ -58,7 +59,7 @@ class AzureAsrHelper(private val context: Context) {
// 防抖机制相关变量 // 防抖机制相关变量
private var lastNetworkLostTime = 0L private var lastNetworkLostTime = 0L
private val networkLostDebounceInterval = 3000L // 2秒防抖间隔 private val networkLostDebounceInterval = 3000L // 2秒防抖间隔
private var currsessionid = "" //识别回话id 关联到整个聊天过程中
/** /**
@ -242,7 +243,7 @@ class AzureAsrHelper(private val context: Context) {
// 检查网络状态 // 检查网络状态
if (!checkNetworkStatus()) { if (!checkNetworkStatus()) {
Log.e(tag, "网络不可用,无法开始语音识别") Log.e(tag, "网络不可用,无法开始语音识别")
continuousCallback?.onError(1000,"网络连接不可用,请检查网络设置") continuousCallback?.onError(currsessionid,1000,"网络连接不可用,请检查网络设置")
return false return false
} }
@ -258,7 +259,7 @@ class AzureAsrHelper(private val context: Context) {
Log.d(tag, "前台服务已启动") Log.d(tag, "前台服务已启动")
} catch (e: Exception) { } catch (e: Exception) {
Log.e(tag, "启动前台服务失败: ${e.message}") Log.e(tag, "启动前台服务失败: ${e.message}")
continuousCallback?.onError(1003, "启动前台服务失败: ${e.message}") continuousCallback?.onError(currsessionid,1003, "启动前台服务失败: ${e.message}")
return false return false
} }
} }
@ -268,7 +269,7 @@ class AzureAsrHelper(private val context: Context) {
val callback = continuousCallback val callback = continuousCallback
if (callback == null) { if (callback == null) {
Log.e(tag, "无法重新启动连续识别:回调为空") Log.e(tag, "无法重新启动连续识别:回调为空")
continuousCallback?.onError(1001,"回调为空") continuousCallback?.onError(currsessionid,1001,"回调为空")
return false return false
} }
setupEventListeners(callback) setupEventListeners(callback)
@ -308,7 +309,7 @@ class AzureAsrHelper(private val context: Context) {
fun setupEventListeners(callback: ContinuousRecognizeCallback): Boolean { fun setupEventListeners(callback: ContinuousRecognizeCallback): Boolean {
Log.d(tag, "设置ssssss监听器:${speechConfig ?: "null"} ") Log.d(tag, "设置ssssss监听器:${speechConfig ?: "null"} ")
if (speechConfig == null) { if (speechConfig == null) {
callback.onError(1002,"语音服务未初始化") callback.onError(currsessionid,1002,"语音服务未初始化")
return false return false
} }
@ -330,7 +331,7 @@ class AzureAsrHelper(private val context: Context) {
} else { } else {
currentLanguage currentLanguage
} }
callback.onRecognizing(event.result.text, detectedLanguage) callback.onRecognizing(currsessionid,event.result.text, detectedLanguage)
} }
} }
) )
@ -345,7 +346,7 @@ class AzureAsrHelper(private val context: Context) {
} else { } else {
currentLanguage currentLanguage
} }
callback.onResult(event.result.text, detectedLanguage) callback.onResult(currsessionid,event.result.text, detectedLanguage)
} }
} }
) )
@ -355,7 +356,8 @@ class AzureAsrHelper(private val context: Context) {
EventHandler<SessionEventArgs> { _, _ -> EventHandler<SessionEventArgs> { _, _ ->
// 直接在当前线程调用回调 // 直接在当前线程调用回调
Log.d(tag, "会话开始事件") Log.d(tag, "会话开始事件")
callback.onSessionStarted() currsessionid = UUID.randomUUID().toString()
callback.onSessionStarted(currsessionid)
} }
) )
@ -365,7 +367,7 @@ class AzureAsrHelper(private val context: Context) {
// 直接在当前线程调用回调 // 直接在当前线程调用回调
Log.d(tag, "会话结束事件") Log.d(tag, "会话结束事件")
if (audioSourceType == AudioSourceType.EXTERNAL) { if (audioSourceType == AudioSourceType.EXTERNAL) {
callback.onSessionStopped() callback.onSessionStopped(currsessionid)
audioStream?.isContinuousRecognitionActive = false audioStream?.isContinuousRecognitionActive = false
} }
} }
@ -378,11 +380,11 @@ class AzureAsrHelper(private val context: Context) {
val reason = event.reason.toString() val reason = event.reason.toString()
Log.d(tag, "识别被取消: reason=$reason, details=$errorDetails") Log.d(tag, "识别被取消: reason=$reason, details=$errorDetails")
callback.onCanceled(reason,errorDetails) callback.onCanceled(currsessionid,reason,errorDetails)
} }
) )
} catch (e: Exception) { } catch (e: Exception) {
callback.onError(1001,"启动连续识别失败: ${e.message}") callback.onError(currsessionid,1001,"启动连续识别失败: ${e.message}")
return false return false
} }
return true return true
@ -495,7 +497,7 @@ class AzureAsrHelper(private val context: Context) {
// 确保状态被重置 // 确保状态被重置
audioStream.isContinuousRecognitionActive = false audioStream.isContinuousRecognitionActive = false
currsessionid = ""
} catch (e: Exception) { } catch (e: Exception) {
// 确保状态被重置 // 确保状态被重置
audioStream.isContinuousRecognitionActive = false audioStream.isContinuousRecognitionActive = false
@ -503,6 +505,7 @@ class AzureAsrHelper(private val context: Context) {
recognizer = null recognizer = null
speechConfig = null speechConfig = null
isForegroundServiceRunning = false isForegroundServiceRunning = false
currsessionid = ""
} }
} }
@ -689,7 +692,7 @@ class AzureAsrHelper(private val context: Context) {
* @param text 识别的文本 * @param text 识别的文本
* @param detectedLanguage 检测到的语言 * @param detectedLanguage 检测到的语言
*/ */
fun onResult(text: String, detectedLanguage: String) fun onResult(sessiond:String, text: String, detectedLanguage: String)
/** /**
* 识别进行中调用 * 识别进行中调用
@ -697,17 +700,17 @@ class AzureAsrHelper(private val context: Context) {
* @param recognizing 正在识别的文本 * @param recognizing 正在识别的文本
* @param detectedLanguage 检测到的语言 * @param detectedLanguage 检测到的语言
*/ */
fun onRecognizing(recognizing: String, detectedLanguage: String) fun onRecognizing(sessiond:String, recognizing: String, detectedLanguage: String)
/** /**
* 会话开始时调用 * 会话开始时调用
*/ */
fun onSessionStarted() fun onSessionStarted(sessiond:String)
/** /**
* 会话结束时调用 * 会话结束时调用
*/ */
fun onSessionStopped() fun onSessionStopped(sessiond:String)
/** /**
@ -716,14 +719,14 @@ class AzureAsrHelper(private val context: Context) {
* @param reason 取消原因 * @param reason 取消原因
* @param errorDetails 错误详情 * @param errorDetails 错误详情
*/ */
fun onCanceled(reason: String, errorDetails: String) fun onCanceled(sessiond:String,reason: String, errorDetails: String)
/** /**
* 识别出错时调用 * 识别出错时调用
* *
* @param error 错误信息 * @param error 错误信息
*/ */
fun onError(code: Int,error: String) fun onError(sessiond:String, code: Int,error: String)
} }
@ -781,7 +784,7 @@ class AzureAsrHelper(private val context: Context) {
lastNetworkLostTime = 0L lastNetworkLostTime = 0L
isNetworkRecovering = true isNetworkRecovering = true
} else { } else {
continuousCallback?.onError(1000,"网络连接不可用,请检查网络设置") continuousCallback?.onError(currsessionid,1000,"网络连接不可用,请检查网络设置")
Log.d(tag,"网络仍未恢复,继续检测") Log.d(tag,"网络仍未恢复,继续检测")
networkLostCount = 0 networkLostCount = 0
stopAudioProcessingImmediately() stopAudioProcessingImmediately()

28
local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift

@ -47,6 +47,7 @@ public class AzureAsrHelper: NSObject {
private var isAudioStarted = false private var isAudioStarted = false
private var pendingStopRequest = false private var pendingStopRequest = false
private var currsessionId = ""
// 配置参数 // 配置参数
@ -197,7 +198,7 @@ public class AzureAsrHelper: NSObject {
self.lastNetworkLostTime = 0 self.lastNetworkLostTime = 0
self.isNetworkRecovering = true self.isNetworkRecovering = true
} else { } else {
self.continuousCallback?.onError(1000, "网络连接不可用,请检查网络设置") self.continuousCallback?.onError(sessionid: currsessionId,1000, "网络连接不可用,请检查网络设置")
os_log("网络仍未恢复,继续检测", log: self.log, type: .debug) os_log("网络仍未恢复,继续检测", log: self.log, type: .debug)
self.networkLostCount = 0 self.networkLostCount = 0
self.stopContinuousRecognition() self.stopContinuousRecognition()
@ -780,7 +781,7 @@ public class AzureAsrHelper: NSObject {
print("正在识别事件=检测到语言: \(detectedLanguage), 识别中: \(text)") print("正在识别事件=检测到语言: \(detectedLanguage), 识别中: \(text)")
DispatchQueue.main.async { DispatchQueue.main.async {
callback.onRecognizing(result.text ?? "", detectedLanguage) callback.onRecognizing(sessionid: self.currsessionId,result.text ?? "", detectedLanguage)
} }
} }
@ -799,14 +800,15 @@ public class AzureAsrHelper: NSObject {
// 在主线程回调结果 // 在主线程回调结果
DispatchQueue.main.async { DispatchQueue.main.async {
callback.onResult(result.text ?? "", detectedLanguage) callback.onResult(sessionid: self.currsessionId,result.text ?? "", detectedLanguage)
} }
} }
// 会话开始事件 // 会话开始事件
recognizer.addSessionStartedEventHandler { (sender, event) in recognizer.addSessionStartedEventHandler { (sender, event) in
// 直接在当前线程调用回调 // 直接在当前线程调用回调
callback.onSessionStarted() self.currsessionId = UUID().uuidString
callback.onSessionStarted(sessionid: self.currsessionId)
print("会话开始事件:") print("会话开始事件:")
} }
@ -815,7 +817,7 @@ public class AzureAsrHelper: NSObject {
guard let self = self else { return } guard let self = self else { return }
print("会话结束事件:") print("会话结束事件:")
// 直接在当前线程调用回调 // 直接在当前线程调用回调
callback.onSessionStopped() callback.onSessionStopped(sessionid: self.currsessionId)
self.audioStream?.isContinuousRecognitionActive = false self.audioStream?.isContinuousRecognitionActive = false
//self.stopAudioProcessing() //self.stopAudioProcessing()
} }
@ -830,7 +832,7 @@ public class AzureAsrHelper: NSObject {
os_log("识别取消: %{public}@", log: self.log, type: .error, errorDetails) os_log("识别取消: %{public}@", log: self.log, type: .error, errorDetails)
callback.onCanceled(reason, errorDetails) callback.onCanceled(sessionid: self.currsessionId,reason, errorDetails)
} }
return true return true
@ -994,7 +996,7 @@ public class AzureAsrHelper: NSObject {
* @param text 识别的文本 * @param text 识别的文本
* @param detectedLanguage 检测到的语言 * @param detectedLanguage 检测到的语言
*/ */
func onResult(_ text: String, _ detectedLanguage: String) func onResult(sessionid:String,_ text: String, _ detectedLanguage: String)
/** /**
* 识别进行中调用 * 识别进行中调用
@ -1002,17 +1004,17 @@ public class AzureAsrHelper: NSObject {
* @param recognizing 正在识别的文本 * @param recognizing 正在识别的文本
* @param detectedLanguage 检测到的语言 * @param detectedLanguage 检测到的语言
*/ */
func onRecognizing(_ recognizing: String, _ detectedLanguage: String) func onRecognizing(sessionid:String ,_ recognizing: String, _ detectedLanguage: String)
/** /**
* 会话开始时调用 * 会话开始时调用
*/ */
func onSessionStarted() func onSessionStarted(sessionid:String)
/** /**
* 会话结束时调用 * 会话结束时调用
*/ */
func onSessionStopped() func onSessionStopped(sessionid:String)
/** /**
* 识别取消时调用 * 识别取消时调用
@ -1020,14 +1022,14 @@ public class AzureAsrHelper: NSObject {
* @param reason 取消原因 * @param reason 取消原因
* @param errorDetails 错误详情 * @param errorDetails 错误详情
*/ */
func onCanceled(_ reason: String, _ errorDetails: String) func onCanceled(sessionid:String ,_ reason: String, _ errorDetails: String)
/** /**
* 识别出错时调用 * 识别出错时调用
* *
* @param error 错误信息 * @param error 错误信息
*/ */
func onError(_ code: Int,_ error: String) func onError(sessionid:String ,_ code: Int,_ error: String)
} }
/** /**
* 检查网络状态 * 检查网络状态
@ -1039,7 +1041,7 @@ private func checkNetworkStatus() -> Bool {
if !isAvailable { if !isAvailable {
os_log("网络不可用,无法启动语音识别", log: log, type: .error) os_log("网络不可用,无法启动语音识别", log: log, type: .error)
continuousCallback?.onError(1000,"网络连接不可用,请检查网络设置") continuousCallback?.onError(sessionid: self.currsessionId,1000,"网络连接不可用,请检查网络设置")
} }
return isAvailable return isAvailable

12
local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureSpeechPlugin.swift

@ -1208,7 +1208,7 @@ private class AsrCallbackWrapper: AzureAsrHelper.ContinuousRecognizeCallback {
* @param text 识别的文本 * @param text 识别的文本
* @param detectedLanguage 检测到的语言 * @param detectedLanguage 检测到的语言
*/ */
func onResult(_ text: String, _ detectedLanguage: String) { func onResult(sessionid:String,_ text: String, _ detectedLanguage: String) {
plugin?.sendAsrEvent([ plugin?.sendAsrEvent([
"type": "result", "type": "result",
"text": text, "text": text,
@ -1221,7 +1221,7 @@ private class AsrCallbackWrapper: AzureAsrHelper.ContinuousRecognizeCallback {
* @param text 正在识别的文本 * @param text 正在识别的文本
* @param detectedLanguage 检测到的语言 * @param detectedLanguage 检测到的语言
*/ */
func onRecognizing(_ text: String, _ detectedLanguage: String) { func onRecognizing(sessionid:String,_ text: String, _ detectedLanguage: String) {
plugin?.sendAsrEvent([ plugin?.sendAsrEvent([
"type": "recognizing", "type": "recognizing",
"text": text, "text": text,
@ -1232,7 +1232,7 @@ private class AsrCallbackWrapper: AzureAsrHelper.ContinuousRecognizeCallback {
/** /**
* 会话开始回调 * 会话开始回调
*/ */
func onSessionStarted() { func onSessionStarted(sessionid:String) {
plugin?.sendAsrEvent([ plugin?.sendAsrEvent([
"type": "sessionStarted" "type": "sessionStarted"
]) ])
@ -1241,7 +1241,7 @@ private class AsrCallbackWrapper: AzureAsrHelper.ContinuousRecognizeCallback {
/** /**
* 会话结束回调 * 会话结束回调
*/ */
func onSessionStopped() { func onSessionStopped(sessionid:String) {
plugin?.sendAsrEvent([ plugin?.sendAsrEvent([
"type": "sessionStopped" "type": "sessionStopped"
]) ])
@ -1252,7 +1252,7 @@ private class AsrCallbackWrapper: AzureAsrHelper.ContinuousRecognizeCallback {
* @param reason 取消原因 * @param reason 取消原因
* @param errorDetails 错误详情 * @param errorDetails 错误详情
*/ */
func onCanceled(_ reason: String, _ errorDetails: String) { func onCanceled(sessionid:String,_ reason: String, _ errorDetails: String) {
plugin?.sendAsrEvent([ plugin?.sendAsrEvent([
"type": "canceled", "type": "canceled",
"reason": reason, "reason": reason,
@ -1265,7 +1265,7 @@ private class AsrCallbackWrapper: AzureAsrHelper.ContinuousRecognizeCallback {
* @param errorCode 错误码 * @param errorCode 错误码
* @param error 错误信息 * @param error 错误信息
*/ */
func onError(_ errorCode: Int, _ error: String) { func onError(sessionid:String,_ errorCode: Int, _ error: String) {
plugin?.sendAsrEvent([ plugin?.sendAsrEvent([
"type": "error", "type": "error",
"code": errorCode, "code": errorCode,

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

@ -164,16 +164,18 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
val oldSessionId = currSessionId val oldSessionId = currSessionId
// 1. 取消当前的协程任务 // 1. 取消当前的协程任务
currentStreamJob?.cancel() currentStreamJob?.cancel(CancellationException("New session started"))
currentStreamJob = null currentStreamJob = null
// 2. 通知旧会话被中止 // 2. 通知旧会话被中止
getSessionCallback(oldSessionId)?.onComplete(currSessionId) // 直接完成当前会话 getSessionCallback(oldSessionId)?.onComplete(oldSessionId) // 直接完成当前会话
// 3. 清理状态 // 3. 清理状态
currSessionId = "" currSessionId = ""
sessionCallbacks.remove(oldSessionId) sessionCallbacks.remove(oldSessionId)
toolCalls.clear() toolCalls.clear()
Log.d(TAG, "会话 $oldSessionId 已被中止")
} }
} }
@ -288,7 +290,7 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
// 检查是否包含图片,决定使用哪个模型 // 检查是否包含图片,决定使用哪个模型
val currentModel = if (containsImage(messages)) visionModel else model val currentModel = if (containsImage(messages)) visionModel else model
// 转换消息格式 // 转换消息格式
val chatMessages = convertToChatMessages(messages) val chatMessages = convertToChatMessages(messages)
@ -352,10 +354,10 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
if (!isInitialized || apiKey.isEmpty() || openAI == null) { if (!isInitialized || apiKey.isEmpty() || openAI == null) {
Log.e("ChatApiService", "ChatAPI服务未初始化,无法发送消息") Log.e("ChatApiService", "ChatAPI服务未初始化,无法发送消息")
try { try {
getSessionCallback(sessionid)?.onError(sessionid,ChatApiException("ChatAPI服务未初始化")) getSessionCallback(sessionid)?.onError(sessionid,ChatApiException("ChatAPI服务未初始化"))
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "onError回调异常: ${e.message}", e) Log.e(TAG, "onError回调异常: ${e.message}", e)
} }
return return
} }
@ -369,6 +371,8 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
try { try {
// 转换消息格式 // 转换消息格式
val chatMessages = try { val chatMessages = try {
// ✨ 第一步:上下文精简
// val reducedMessages = reduce(messages)
convertToChatMessages(messages) convertToChatMessages(messages)
} catch (e: Exception) { } catch (e: Exception) {
Log.e("ChatApiService", "转换消息格式失败: ${e.message}", e) Log.e("ChatApiService", "转换消息格式失败: ${e.message}", e)
@ -425,12 +429,10 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
} }
var replyContext = ""; var replyContext = "";
// 为每个会话新增一个状态标记:是否为中间工具调用(默认false)
var isIntermediateToolCall = false
// Log.d(TAG, "liwei-------------------------开始AI 对话 7") // Log.d(TAG, "liwei-------------------------开始AI 对话 7")
chatsFlow.collect { result -> chatsFlow.collect { result ->
// val usage = result.usage
// if (usage != null){ //发现流式不返回这歌数据
// getSessionCallback(sessionid)?.onUsage(sessionid,usage.promptTokens,usage.completionTokens,usage.totalTokens)
// }
// 每次处理前检查是否仍是当前会话 // 每次处理前检查是否仍是当前会话
if (sessionid != currSessionId) { if (sessionid != currSessionId) {
// throw CancellationException("Session cancelled") // throw CancellationException("Session cancelled")
@ -448,8 +450,22 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
Log.w("ChatApiService", "[Session: $sessionid] delta为null") Log.w("ChatApiService", "[Session: $sessionid] delta为null")
return@collect return@collect
} }
// Log.d(TAG, "liwei-------------------------开始AI 对话 7-3")
if (delta.toolCalls == null){
// --------------------------
// 核心修改:第一帧判断是否为工具调用
// --------------------------
if (!isIntermediateToolCall) {
// 第一帧(或未标记时):检查是否有工具调用字段
val hasToolCalls = delta.toolCalls?.isNotEmpty() == true
val hasFunctionCall = delta.functionCall != null
if (hasToolCalls || hasFunctionCall) {
// 标记为中间工具调用,后续所有帧都拦截
isIntermediateToolCall = true
}
}
//中间过程不输出
if (!isIntermediateToolCall){
// 处理普通文本内容 // 处理普通文本内容
delta.content?.let { content -> delta.content?.let { content ->
try { try {
@ -460,7 +476,15 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
// 不重新抛出异常,继续处理后续内容 // 不重新抛出异常,继续处理后续内容
} }
} }
} }
//else{
// delta.content?.let { content ->
// Log.d("ChatApiService", "liwei------------ [Session: $sessionid] 中间过程不输出 $content")
// }
// }
// Log.d(TAG, "liwei-------------------------开始AI 对话 7-4") // Log.d(TAG, "liwei-------------------------开始AI 对话 7-4")
// 收集工具调用信息 // 收集工具调用信息
delta.toolCalls?.forEach { toolCall -> delta.toolCalls?.forEach { toolCall ->
@ -577,113 +601,131 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
"arguments" to firstToolCall.arguments, "arguments" to firstToolCall.arguments,
"id" to firstToolCall.id "id" to firstToolCall.id
) )
// 通知上层工具调用事件 // 通知上层工具调用事件
try { try {
getSessionCallback(sessionid)?.onFunctionCall(sessionid,convertMapToJsonObject(functionCall)) getSessionCallback(sessionid)?.onFunctionCall(sessionid,convertMapToJsonObject(functionCall))
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "onFunctionCall回调异常: ${e.message}", e) Log.e(TAG, "onFunctionCall回调异常: ${e.message}", e)
} }
// 在当前协程作用域内处理工具调用,使用async确保生命周期管理 // 在当前协程作用域内处理工具调用,使用async确保生命周期管理
val toolCallDeferred = async { val toolCallDeferred = async {
try { try {
if (sessionid == currSessionId) { // 检查会话是否仍然有效
// 通过MCP客户端处理工具调用 if (sessionid != currSessionId) {
val functionName = firstToolCall.name Log.d(TAG, "[Session: $sessionid] 会话已被替换,跳过工具调用")
val argumentsJson = firstToolCall.arguments return@async
var isError = false }
var errorText = ""
val result = if (_mcpClient?.hasToolWithName(functionName) == true) { // 通过MCP客户端处理工具调用
// 解析参数,如果参数为空JSON对象,使用空Map val functionName = firstToolCall.name
val arguments = if (argumentsJson == "{}") { val argumentsJson = firstToolCall.arguments
emptyMap() var isError = false
} else { var errorText = ""
_mcpClient?.parseJsonArguments(argumentsJson) ?: emptyMap() val result = if (_mcpClient?.hasToolWithName(functionName) == true) {
} // 解析参数,如果参数为空JSON对象,使用空Map
// 调用MCP工具 val arguments = if (argumentsJson == "{}") {
val toolResult = try { emptyMap()
withTimeout(60000) { // 60秒超时 } else {
_mcpClient?.callTool(functionName, arguments) _mcpClient?.parseJsonArguments(argumentsJson) ?: emptyMap()
}
// 调用MCP工具
val toolResult = try {
withTimeout(60000) { // 60秒超时
// 再次检查会话状态
if (sessionid != currSessionId) {
throw CancellationException("Session cancelled")
} }
} catch (e: TimeoutCancellationException) { _mcpClient?.callTool(functionName, arguments)
Log.w("ChatApiService", "[Session: $sessionid] MCP工具调用超时: $functionName")
mapOf(
"content" to listOf(mapOf(
"type" to "text",
"text" to "Tool call timeout: $functionName"
)),
"isError" to true
)
} catch (e: Exception) {
Log.e("ChatApiService", "[Session: $sessionid] MCP工具调用异常: $functionName", e)
mapOf(
"content" to listOf(mapOf(
"type" to "text",
"text" to "Tool call failed: ${e.message}"
)),
"isError" to true
)
} }
Log.d("ChatApiService", "[Session: $sessionid] AI调用工具结果: $functionName, 参数: $arguments, 结果: $toolResult") } catch (e: TimeoutCancellationException) {
// 处理结果 Log.w("ChatApiService", "[Session: $sessionid] MCP工具调用超时: $functionName")
if (toolResult != null) { mapOf(
if (toolResult["isError"] == true) { "content" to listOf(mapOf(
// 处理错误情况 "type" to "text",
isError = true "text" to "Tool call timeout: $functionName"
val content = toolResult["content"] as? List<*> )),
val firstContent = content?.firstOrNull() as? Map<*, *> "isError" to true
errorText = firstContent?.get("text") as? String ?: "Tool execution failed" )
mapOf("context" to errorText) } catch (e: CancellationException) {
} else if (toolResult.containsKey("context")) { Log.d("ChatApiService", "[Session: $sessionid] MCP工具调用被取消: $functionName")
// 本地函数结果 // 不抛出异常,直接返回取消结果
toolResult mapOf(
} else { "content" to listOf(mapOf(
// MCP工具结果 "type" to "text",
val content = toolResult["content"] as? List<*> "text" to "Tool call cancelled"
val firstContent = content?.firstOrNull() as? Map<*, *> )),
val text = firstContent?.get("text") as? String ?: "" "isError" to true
mapOf("context" to text) )
} } catch (e: Exception) {
Log.e("ChatApiService", "[Session: $sessionid] MCP工具调用异常: $functionName", e)
mapOf(
"content" to listOf(mapOf(
"type" to "text",
"text" to "Tool call failed: ${e.message}"
)),
"isError" to true
)
}
Log.d("ChatApiService", "[Session: $sessionid] AI调用工具结果: $functionName, 参数: $arguments, 结果: $toolResult")
// 处理结果
if (toolResult != null) {
if (toolResult["isError"] == true) {
// 处理错误情况
isError = true
val content = toolResult["content"] as? List<*>
val firstContent = content?.firstOrNull() as? Map<*, *>
errorText = firstContent?.get("text") as? String ?: "Tool execution failed"
mapOf("context" to errorText)
} else if (toolResult.containsKey("context")) {
// 本地函数结果
toolResult
} else { } else {
Log.w("ChatApiService", "[Session: $sessionid] MCP工具调用返回null") // MCP工具结果
mapOf("context" to "Tool call failed") val content = toolResult["content"] as? List<*>
val firstContent = content?.firstOrNull() as? Map<*, *>
val text = firstContent?.get("text") as? String ?: ""
mapOf("context" to text)
} }
} else { } else {
// 工具不存在 Log.w("ChatApiService", "[Session: $sessionid] MCP工具调用返回null")
mapOf("context" to "Tool not found: $functionName") mapOf("context" to "Tool call failed")
} }
if (sessionid == currSessionId) { } else {
// if (functionName != "set_user_profile_field"){ // 工具不存在
// 处理结果 mapOf("context" to "Tool not found: $functionName")
try { }
if(!isError){
getSessionCallback(sessionid)?.onFunctionCallResult( // 最后检查会话状态
sessionid, if (sessionid == currSessionId) {
convertMapToJsonObject(functionCall), try {
convertMapToJsonObject(result) if(!isError){
) getSessionCallback(sessionid)?.onFunctionCallResult(
}else{ sessionid,
getSessionCallback(sessionid)?.onError(sessionid, ChatApiException(errorText)) convertMapToJsonObject(functionCall),
} convertMapToJsonObject(result)
} catch (e: Exception) {
Log.e(TAG, "onFunctionCallResult回调异常: ${e.message}", e)
}
// 将结果发送回OpenAI继续对话
sendFunctionCallResultInternal(
sessionid= sessionid,
messages = currentMessages,
functionCall = functionCall,
functionResult = gson.toJson(result)
) )
// }else{ }else{
// try { getSessionCallback(sessionid)?.onError(sessionid, ChatApiException(errorText))
// getSessionCallback(sessionid)?.onComplete(sessionid) }
// } catch (ex: Exception) { } catch (e: Exception) {
// Log.e(TAG, "onComplete回调异常: ${ex.message}", ex) Log.e(TAG, "onFunctionCallResult回调异常: ${e.message}", e)
// }
// clearCurrentSession()
// }
} }
// 将结果发送回OpenAI继续对话
sendFunctionCallResultInternal(
sessionid= sessionid,
messages = currentMessages,
functionCall = functionCall,
functionResult = gson.toJson(result)
)
} else {
Log.d(TAG, "[Session: $sessionid] 工具调用完成时会话已被替换,跳过结果处理")
} }
} catch (e: CancellationException) {
Log.d("ChatApiService", "[Session: $sessionid] 工具调用协程被取消")
// 不重新抛出CancellationException,避免传播
} catch (e: Exception) { } catch (e: Exception) {
Log.e("ChatApiService", "工具调用处理过程中出错: ${e.message}", e) Log.e("ChatApiService", "工具调用处理过程中出错: ${e.message}", e)
if (sessionid == currSessionId) { if (sessionid == currSessionId) {
@ -702,9 +744,10 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
try { try {
toolCallDeferred.await() toolCallDeferred.await()
} catch (e: CancellationException) { } catch (e: CancellationException) {
Log.d(TAG, "[Session: $sessionid] 工具调用被取消")
// 协程被取消,确保子任务也被取消 // 协程被取消,确保子任务也被取消
toolCallDeferred.cancel() toolCallDeferred.cancel()
throw e // 不重新抛出异常,让流程正常结束
} }
return true return true
@ -1045,7 +1088,7 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
} }
return false return false
} }
/** /**
* 转换为OpenAI ChatMessage格式 * 转换为OpenAI ChatMessage格式
*/ */

16
local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/CustomSseClientTransport.kt

@ -30,9 +30,10 @@ class CustomSseClientTransport(
) : AbstractTransport() { ) : AbstractTransport() {
private val TAG = "CustomSseClientTransport" private val TAG = "CustomSseClientTransport"
private val scope by lazy { private val transportScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
CoroutineScope(session.coroutineContext + SupervisorJob())
} // ✅ 为每个会话创建独立的作用域
private var sessionScope: CoroutineScope? = null
private val initialized = AtomicBoolean(false) private val initialized = AtomicBoolean(false)
private var session: ClientSSESession by Delegates.notNull() private var session: ClientSSESession by Delegates.notNull()
@ -98,7 +99,7 @@ class CustomSseClientTransport(
* 收集SSE事件 * 收集SSE事件
*/ */
private suspend fun collectEvents() { private suspend fun collectEvents() {
job = scope.launch(CoroutineName("CustomSseMcpClientTransport.collect#${hashCode()}")) { job = sessionScope?.launch(CoroutineName("CustomSseMcpClientTransport.collect#${hashCode()}")) {
try { try {
session.incoming.collect { event -> session.incoming.collect { event ->
Log.e(TAG, "$serviceidString event:${event.event}-${event.data}") Log.e(TAG, "$serviceidString event:${event.event}-${event.data}")
@ -238,7 +239,7 @@ class CustomSseClientTransport(
* 启动连接监控,定期检查连接状态 * 启动连接监控,定期检查连接状态
*/ */
private fun startConnectionMonitor() { private fun startConnectionMonitor() {
connectionMonitorJob = scope.launch { connectionMonitorJob = transportScope.launch {
while (isActive && isConnected.get()) { while (isActive && isConnected.get()) {
try { try {
delay(10000) // 每10秒检查一次 delay(10000) // 每10秒检查一次
@ -278,6 +279,8 @@ class CustomSseClientTransport(
queryParams = urlInfo.third queryParams = urlInfo.third
} }
sessionScope = CoroutineScope(transportScope.coroutineContext + SupervisorJob())
// 创建SSE会话 - 直接使用原始URL // 创建SSE会话 - 直接使用原始URL
session = urlString?.let { session = urlString?.let {
val sseConnectUrl = if (queryParams.isNotEmpty()) { val sseConnectUrl = if (queryParams.isNotEmpty()) {
@ -381,7 +384,8 @@ class CustomSseClientTransport(
_onClose() _onClose()
job?.cancelAndJoin() job?.cancelAndJoin()
connectionMonitorJob?.cancelAndJoin() connectionMonitorJob?.cancelAndJoin()
sessionScope?.cancel()
transportScope.cancel()
Log.d(TAG, "$serviceidString CustomSseClientTransport已关闭") Log.d(TAG, "$serviceidString CustomSseClientTransport已关闭")
} }
} }

52
local_plugins/chat_storage/android/src/main/kotlin/com/yunqiinnovation/chat_storage/ChatStorageHelper.kt

@ -22,13 +22,14 @@ class ChatStorageHelper(context: Context) {
/** /**
* 保存消息 * 保存消息
* *
* @param sessionId 会话ID * @param agentId 会话ID
* @param message 消息内容 * @param message 消息内容
* @param sender 发送者("user" 或 "assistant") * @param sender 发送者("user" 或 "assistant")
* @param metadata 元数据(可选) * @param metadata 元数据(可选)
* @return 成功返回消息ID,失败返回-1 * @return 成功返回消息ID,失败返回-1
*/ */
fun saveMessage( fun saveMessage(
agentId: String,
sessionId: String, sessionId: String,
message: String, message: String,
sender: String, sender: String,
@ -42,6 +43,7 @@ class ChatStorageHelper(context: Context) {
// 执行插入操作 // 执行插入操作
val values = android.content.ContentValues().apply { val values = android.content.ContentValues().apply {
put(ChatDatabaseHelper.COLUMN_AGENT_ID, agentId)
put(ChatDatabaseHelper.COLUMN_SESSION_ID, sessionId) put(ChatDatabaseHelper.COLUMN_SESSION_ID, sessionId)
put(ChatDatabaseHelper.COLUMN_MESSAGE, message) put(ChatDatabaseHelper.COLUMN_MESSAGE, message)
put(ChatDatabaseHelper.COLUMN_SENDER, sender) put(ChatDatabaseHelper.COLUMN_SENDER, sender)
@ -59,12 +61,12 @@ class ChatStorageHelper(context: Context) {
/** /**
* 获取指定会话的消息 * 获取指定会话的消息
* *
* @param sessionId 会话ID * @param agentId 会话ID
* @param page 页码,从1开始 * @param page 页码,从1开始
* @param pageSize 每页大小 * @param pageSize 每页大小
* @return 包含消息列表和分页信息的JSON对象 * @return 包含消息列表和分页信息的JSON对象
*/ */
fun getMessages(sessionId: String, page: Int = 1, pageSize: Int = 20): JSONObject { fun getMessages(agentId: String, page: Int = 1, pageSize: Int = 20): JSONObject {
try { try {
val db = dbHelper.readableDatabase val db = dbHelper.readableDatabase
val offset = (page - 1) * pageSize val offset = (page - 1) * pageSize
@ -72,19 +74,19 @@ class ChatStorageHelper(context: Context) {
// 构建查询语句 // 构建查询语句
val query = """ val query = """
SELECT * FROM ${ChatDatabaseHelper.TABLE_MESSAGES} SELECT * FROM ${ChatDatabaseHelper.TABLE_MESSAGES}
WHERE ${ChatDatabaseHelper.COLUMN_SESSION_ID} = ? WHERE ${ChatDatabaseHelper.COLUMN_AGENT_ID} = ?
ORDER BY ${ChatDatabaseHelper.COLUMN_TIMESTAMP} DESC ORDER BY ${ChatDatabaseHelper.COLUMN_TIMESTAMP} DESC
LIMIT ? OFFSET ? LIMIT ? OFFSET ?
""".trimIndent() """.trimIndent()
val selectionArgs = arrayOf(sessionId, pageSize.toString(), offset.toString()) val selectionArgs = arrayOf(agentId, pageSize.toString(), offset.toString())
val cursor = db.rawQuery(query, selectionArgs) val cursor = db.rawQuery(query, selectionArgs)
val messages = cursorToJsonArray(cursor) val messages = cursorToJsonArray(cursor)
// 获取总记录数 // 获取总记录数
val countQuery = "SELECT COUNT(*) FROM ${ChatDatabaseHelper.TABLE_MESSAGES} WHERE ${ChatDatabaseHelper.COLUMN_SESSION_ID} = ?" val countQuery = "SELECT COUNT(*) FROM ${ChatDatabaseHelper.TABLE_MESSAGES} WHERE ${ChatDatabaseHelper.COLUMN_AGENT_ID} = ?"
val countCursor = db.rawQuery(countQuery, arrayOf(sessionId)) val countCursor = db.rawQuery(countQuery, arrayOf(agentId))
var totalCount = 0 var totalCount = 0
if (countCursor.moveToFirst()) { if (countCursor.moveToFirst()) {
totalCount = countCursor.getInt(0) totalCount = countCursor.getInt(0)
@ -125,24 +127,26 @@ class ChatStorageHelper(context: Context) {
// 查询不同的会话ID及其最新消息 // 查询不同的会话ID及其最新消息
val query = """ val query = """
SELECT s1.${ChatDatabaseHelper.COLUMN_SESSION_ID}, SELECT s1.${ChatDatabaseHelper.COLUMN_AGENT_ID},
s1.${ChatDatabaseHelper.COLUMN_SESSION_ID},
s1.${ChatDatabaseHelper.COLUMN_MESSAGE}, s1.${ChatDatabaseHelper.COLUMN_MESSAGE},
s1.${ChatDatabaseHelper.COLUMN_SENDER}, s1.${ChatDatabaseHelper.COLUMN_SENDER},
s1.${ChatDatabaseHelper.COLUMN_TIMESTAMP} s1.${ChatDatabaseHelper.COLUMN_TIMESTAMP}
FROM ${ChatDatabaseHelper.TABLE_MESSAGES} s1 FROM ${ChatDatabaseHelper.TABLE_MESSAGES} s1
INNER JOIN ( INNER JOIN (
SELECT ${ChatDatabaseHelper.COLUMN_SESSION_ID}, SELECT ${ChatDatabaseHelper.COLUMN_AGENT_ID},
MAX(${ChatDatabaseHelper.COLUMN_TIMESTAMP}) as max_timestamp MAX(${ChatDatabaseHelper.COLUMN_TIMESTAMP}) as max_timestamp
FROM ${ChatDatabaseHelper.TABLE_MESSAGES} FROM ${ChatDatabaseHelper.TABLE_MESSAGES}
GROUP BY ${ChatDatabaseHelper.COLUMN_SESSION_ID} GROUP BY ${ChatDatabaseHelper.COLUMN_AGENT_ID}
) s2 ON s1.${ChatDatabaseHelper.COLUMN_SESSION_ID} = s2.${ChatDatabaseHelper.COLUMN_SESSION_ID} ) s2 ON s1.${ChatDatabaseHelper.COLUMN_AGENT_ID} = s2.${ChatDatabaseHelper.COLUMN_AGENT_ID}
AND s1.${ChatDatabaseHelper.COLUMN_TIMESTAMP} = s2.max_timestamp AND s1.${ChatDatabaseHelper.COLUMN_TIMESTAMP} = s2.max_timestamp
ORDER BY s1.${ChatDatabaseHelper.COLUMN_TIMESTAMP} DESC ORDER BY s1.${ChatDatabaseHelper.COLUMN_TIMESTAMP} DESC
""".trimIndent() """.trimIndent()
val cursor = db.rawQuery(query, null) val cursor = db.rawQuery(query, null)
val sessions = JSONArray() val sessions = JSONArray()
val agentIdIndex = cursor.getColumnIndex(ChatDatabaseHelper.COLUMN_AGENT_ID)
val sessionIdIndex = cursor.getColumnIndex(ChatDatabaseHelper.COLUMN_SESSION_ID) val sessionIdIndex = cursor.getColumnIndex(ChatDatabaseHelper.COLUMN_SESSION_ID)
val messageIndex = cursor.getColumnIndex(ChatDatabaseHelper.COLUMN_MESSAGE) val messageIndex = cursor.getColumnIndex(ChatDatabaseHelper.COLUMN_MESSAGE)
val senderIndex = cursor.getColumnIndex(ChatDatabaseHelper.COLUMN_SENDER) val senderIndex = cursor.getColumnIndex(ChatDatabaseHelper.COLUMN_SENDER)
@ -150,13 +154,14 @@ class ChatStorageHelper(context: Context) {
while (cursor.moveToNext()) { while (cursor.moveToNext()) {
val session = JSONObject().apply { val session = JSONObject().apply {
put("agentId", cursor.getString(agentIdIndex))
put("sessionId", cursor.getString(sessionIdIndex)) put("sessionId", cursor.getString(sessionIdIndex))
put("lastMessage", cursor.getString(messageIndex)) put("lastMessage", cursor.getString(messageIndex))
put("lastSender", cursor.getString(senderIndex)) put("lastSender", cursor.getString(senderIndex))
put("timestamp", cursor.getString(timestampIndex)) put("timestamp", cursor.getString(timestampIndex))
// 获取会话中的消息数量 // 获取会话中的消息数量
val countQuery = "SELECT COUNT(*) FROM ${ChatDatabaseHelper.TABLE_MESSAGES} WHERE ${ChatDatabaseHelper.COLUMN_SESSION_ID} = ?" val countQuery = "SELECT COUNT(*) FROM ${ChatDatabaseHelper.TABLE_MESSAGES} WHERE ${ChatDatabaseHelper.COLUMN_AGENT_ID} = ?"
val countCursor = db.rawQuery(countQuery, arrayOf(cursor.getString(sessionIdIndex))) val countCursor = db.rawQuery(countQuery, arrayOf(cursor.getString(sessionIdIndex)))
if (countCursor.moveToFirst()) { if (countCursor.moveToFirst()) {
put("messageCount", countCursor.getInt(0)) put("messageCount", countCursor.getInt(0))
@ -181,7 +186,7 @@ class ChatStorageHelper(context: Context) {
* @param messageIds 消息ID列表(可选,如果提供则删除指定消息) * @param messageIds 消息ID列表(可选,如果提供则删除指定消息)
* @return 成功返回true,失败返回false * @return 成功返回true,失败返回false
*/ */
fun deleteMessages(sessionId: String? = null, messageIds: List<Int>? = null): Boolean { fun deleteMessages(agentId: String? = null, messageIds: List<Int>? = null): Boolean {
try { try {
val db = dbHelper.writableDatabase val db = dbHelper.writableDatabase
@ -191,10 +196,10 @@ class ChatStorageHelper(context: Context) {
val deleteQuery = "DELETE FROM ${ChatDatabaseHelper.TABLE_MESSAGES} WHERE ${ChatDatabaseHelper.COLUMN_ID} IN ($idsStr)" val deleteQuery = "DELETE FROM ${ChatDatabaseHelper.TABLE_MESSAGES} WHERE ${ChatDatabaseHelper.COLUMN_ID} IN ($idsStr)"
db.execSQL(deleteQuery) db.execSQL(deleteQuery)
return true return true
} else if (!sessionId.isNullOrEmpty()) { } else if (!agentId.isNullOrEmpty()) {
// 删除会话中的所有消息 // 删除会话中的所有消息
val deleteQuery = "DELETE FROM ${ChatDatabaseHelper.TABLE_MESSAGES} WHERE ${ChatDatabaseHelper.COLUMN_SESSION_ID} = ?" val deleteQuery = "DELETE FROM ${ChatDatabaseHelper.TABLE_MESSAGES} WHERE ${ChatDatabaseHelper.COLUMN_AGENT_ID} = ?"
db.execSQL(deleteQuery, arrayOf(sessionId)) db.execSQL(deleteQuery, arrayOf(agentId))
return true return true
} }
return false return false
@ -223,11 +228,11 @@ class ChatStorageHelper(context: Context) {
/** /**
* 查询指定会话的最近N条消息 * 查询指定会话的最近N条消息
* *
* @param sessionId 会话ID * @param agentId 会话ID
* @param limit 限制数量 * @param limit 限制数量
* @return 消息列表,按时间正序排列 * @return 消息列表,按时间正序排列
*/ */
fun getRecentMessages(sessionId: String, limit: Int): List<Map<String, Any>> { fun getRecentMessages(agentId: String, limit: Int): List<Map<String, Any>> {
try { try {
val db = dbHelper.readableDatabase val db = dbHelper.readableDatabase
@ -235,13 +240,13 @@ class ChatStorageHelper(context: Context) {
val query = """ val query = """
SELECT * FROM ( SELECT * FROM (
SELECT * FROM ${ChatDatabaseHelper.TABLE_MESSAGES} SELECT * FROM ${ChatDatabaseHelper.TABLE_MESSAGES}
WHERE ${ChatDatabaseHelper.COLUMN_SESSION_ID} = ? WHERE ${ChatDatabaseHelper.COLUMN_AGENT_ID} = ?
ORDER BY ${ChatDatabaseHelper.COLUMN_TIMESTAMP} DESC ORDER BY ${ChatDatabaseHelper.COLUMN_TIMESTAMP} DESC
LIMIT ? LIMIT ?
) tmp ORDER BY ${ChatDatabaseHelper.COLUMN_TIMESTAMP} ASC ) tmp ORDER BY ${ChatDatabaseHelper.COLUMN_TIMESTAMP} ASC
""".trimIndent() """.trimIndent()
val selectionArgs = arrayOf(sessionId, limit.toString()) val selectionArgs = arrayOf(agentId, limit.toString())
val cursor = db.rawQuery(query, selectionArgs) val cursor = db.rawQuery(query, selectionArgs)
val messages = ArrayList<Map<String, Any>>() val messages = ArrayList<Map<String, Any>>()
@ -282,6 +287,7 @@ class ChatStorageHelper(context: Context) {
val jsonArray = JSONArray() val jsonArray = JSONArray()
val idIndex = cursor.getColumnIndex(ChatDatabaseHelper.COLUMN_ID) val idIndex = cursor.getColumnIndex(ChatDatabaseHelper.COLUMN_ID)
val agentIdIndex = cursor.getColumnIndex(ChatDatabaseHelper.COLUMN_AGENT_ID)
val sessionIdIndex = cursor.getColumnIndex(ChatDatabaseHelper.COLUMN_SESSION_ID) val sessionIdIndex = cursor.getColumnIndex(ChatDatabaseHelper.COLUMN_SESSION_ID)
val messageIndex = cursor.getColumnIndex(ChatDatabaseHelper.COLUMN_MESSAGE) val messageIndex = cursor.getColumnIndex(ChatDatabaseHelper.COLUMN_MESSAGE)
val senderIndex = cursor.getColumnIndex(ChatDatabaseHelper.COLUMN_SENDER) val senderIndex = cursor.getColumnIndex(ChatDatabaseHelper.COLUMN_SENDER)
@ -291,6 +297,7 @@ class ChatStorageHelper(context: Context) {
while (cursor.moveToNext()) { while (cursor.moveToNext()) {
val jsonObject = JSONObject().apply { val jsonObject = JSONObject().apply {
put("id", cursor.getInt(idIndex)) put("id", cursor.getInt(idIndex))
put("agentId", cursor.getString(agentIdIndex))
put("sessionId", cursor.getString(sessionIdIndex)) put("sessionId", cursor.getString(sessionIdIndex))
put("message", cursor.getString(messageIndex)) put("message", cursor.getString(messageIndex))
put("sender", cursor.getString(senderIndex)) put("sender", cursor.getString(senderIndex))
@ -328,6 +335,7 @@ class ChatDatabaseHelper(context: Context) :
// 定义消息表 // 定义消息表
const val TABLE_MESSAGES = "messages" const val TABLE_MESSAGES = "messages"
const val COLUMN_ID = "id" const val COLUMN_ID = "id"
const val COLUMN_AGENT_ID = "agent_id"
const val COLUMN_SESSION_ID = "session_id" const val COLUMN_SESSION_ID = "session_id"
const val COLUMN_MESSAGE = "message" const val COLUMN_MESSAGE = "message"
const val COLUMN_SENDER = "sender" const val COLUMN_SENDER = "sender"
@ -340,6 +348,7 @@ class ChatDatabaseHelper(context: Context) :
val createMessagesTable = """ val createMessagesTable = """
CREATE TABLE $TABLE_MESSAGES ( CREATE TABLE $TABLE_MESSAGES (
$COLUMN_ID INTEGER PRIMARY KEY AUTOINCREMENT, $COLUMN_ID INTEGER PRIMARY KEY AUTOINCREMENT,
$COLUMN_AGENT_ID TEXT NOT NULL,
$COLUMN_SESSION_ID TEXT NOT NULL, $COLUMN_SESSION_ID TEXT NOT NULL,
$COLUMN_MESSAGE TEXT NOT NULL, $COLUMN_MESSAGE TEXT NOT NULL,
$COLUMN_SENDER TEXT NOT NULL, $COLUMN_SENDER TEXT NOT NULL,
@ -351,6 +360,7 @@ class ChatDatabaseHelper(context: Context) :
db.execSQL(createMessagesTable) db.execSQL(createMessagesTable)
// 创建索引以加快查询速度 // 创建索引以加快查询速度
db.execSQL("CREATE INDEX idx_agent_id ON $TABLE_MESSAGES ($COLUMN_AGENT_ID)")
db.execSQL("CREATE INDEX idx_session_id ON $TABLE_MESSAGES ($COLUMN_SESSION_ID)") db.execSQL("CREATE INDEX idx_session_id ON $TABLE_MESSAGES ($COLUMN_SESSION_ID)")
db.execSQL("CREATE INDEX idx_timestamp ON $TABLE_MESSAGES ($COLUMN_TIMESTAMP)") db.execSQL("CREATE INDEX idx_timestamp ON $TABLE_MESSAGES ($COLUMN_TIMESTAMP)")
} }

14
local_plugins/chat_storage/android/src/main/kotlin/com/yunqiinnovation/chat_storage/ChatStoragePlugin.kt

@ -31,12 +31,14 @@ class ChatStoragePlugin: FlutterPlugin, MethodCallHandler {
when (call.method) { when (call.method) {
"saveMessage" -> { "saveMessage" -> {
try { try {
val agentId = call.argument<String>("agentId") ?: ""
val sessionId = call.argument<String>("sessionId") ?: "" val sessionId = call.argument<String>("sessionId") ?: ""
val message = call.argument<String>("message") ?: "" val message = call.argument<String>("message") ?: ""
val sender = call.argument<String>("sender") ?: "" val sender = call.argument<String>("sender") ?: ""
val metadata = call.argument<String>("metadata") val metadata = call.argument<String>("metadata")
val id = chatStorageHelper.saveMessage( val id = chatStorageHelper.saveMessage(
agentId = agentId,
sessionId = sessionId, sessionId = sessionId,
message = message, message = message,
sender = sender, sender = sender,
@ -55,12 +57,12 @@ class ChatStoragePlugin: FlutterPlugin, MethodCallHandler {
} }
"getMessages" -> { "getMessages" -> {
try { try {
val sessionId = call.argument<String>("sessionId") ?: "" val agentId = call.argument<String>("agentId") ?: ""
val page = call.argument<Int>("page") ?: 1 val page = call.argument<Int>("page") ?: 1
val pageSize = call.argument<Int>("pageSize") ?: 20 val pageSize = call.argument<Int>("pageSize") ?: 20
val messagesJson = chatStorageHelper.getMessages( val messagesJson = chatStorageHelper.getMessages(
sessionId = sessionId, agentId = agentId,
page = page, page = page,
pageSize = pageSize pageSize = pageSize
) )
@ -82,11 +84,11 @@ class ChatStoragePlugin: FlutterPlugin, MethodCallHandler {
} }
"deleteMessages" -> { "deleteMessages" -> {
try { try {
val sessionId = call.argument<String>("sessionId") val agentId = call.argument<String>("agentId")
val messageIds = call.argument<List<Int>>("messageIds") val messageIds = call.argument<List<Int>>("messageIds")
val success = chatStorageHelper.deleteMessages( val success = chatStorageHelper.deleteMessages(
sessionId = sessionId, agentId = agentId,
messageIds = messageIds messageIds = messageIds
) )
@ -111,11 +113,11 @@ class ChatStoragePlugin: FlutterPlugin, MethodCallHandler {
} }
"getRecentMessages" -> { "getRecentMessages" -> {
try { try {
val sessionId = call.argument<String>("sessionId") ?: "" val agentId = call.argument<String>("agentId") ?: ""
val limit = call.argument<Int>("limit") ?: 10 val limit = call.argument<Int>("limit") ?: 10
val messages = chatStorageHelper.getRecentMessages( val messages = chatStorageHelper.getRecentMessages(
sessionId = sessionId, agentId = agentId,
limit = limit limit = limit
) )

33
local_plugins/chat_storage/ios/chat_storage/Sources/chat_storage/ChatStorageHelper.swift

@ -34,12 +34,14 @@ public class ChatStorageHelper {
let createTableString = """ let createTableString = """
CREATE TABLE IF NOT EXISTS messages ( CREATE TABLE IF NOT EXISTS messages (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
agent_id TEXT NOT NULL,
session_id TEXT NOT NULL, session_id TEXT NOT NULL,
message TEXT NOT NULL, message TEXT NOT NULL,
sender TEXT NOT NULL, sender TEXT NOT NULL,
metadata TEXT, metadata TEXT,
created_at INTEGER NOT NULL created_at INTEGER NOT NULL
); );
CREATE INDEX IF NOT EXISTS idx_agent_id ON messages (session_id);
CREATE INDEX IF NOT EXISTS idx_session_id ON messages (session_id); CREATE INDEX IF NOT EXISTS idx_session_id ON messages (session_id);
CREATE INDEX IF NOT EXISTS idx_created_at ON messages (created_at); CREATE INDEX IF NOT EXISTS idx_created_at ON messages (created_at);
""" """
@ -64,11 +66,12 @@ public class ChatStorageHelper {
* @param metadata 可选的元数据 * @param metadata 可选的元数据
* @return 插入的消息ID,失败则返回-1 * @return 插入的消息ID,失败则返回-1
*/ */
public func saveMessage(sessionId: String, message: String, sender: String, metadata: String?) -> Int64 { public func saveMessage(agentId:String,sessionId: String, message: String, sender: String, metadata: String?) -> Int64 {
let insertStatementString = "INSERT INTO messages (session_id, message, sender, metadata, created_at) VALUES (?, ?, ?, ?, ?);" let insertStatementString = "INSERT INTO messages (agent_id,session_id, message, sender, metadata, created_at) VALUES (?, ?, ?, ?, ?, ?);"
var insertStatement: OpaquePointer? var insertStatement: OpaquePointer?
if sqlite3_prepare_v2(db, insertStatementString, -1, &insertStatement, nil) == SQLITE_OK { if sqlite3_prepare_v2(db, insertStatementString, -1, &insertStatement, nil) == SQLITE_OK {
sqlite3_bind_text(insertStatement, 1, (agentId as NSString).utf8String, -1, nil)
sqlite3_bind_text(insertStatement, 1, (sessionId as NSString).utf8String, -1, nil) sqlite3_bind_text(insertStatement, 1, (sessionId as NSString).utf8String, -1, nil)
sqlite3_bind_text(insertStatement, 2, (message as NSString).utf8String, -1, nil) sqlite3_bind_text(insertStatement, 2, (message as NSString).utf8String, -1, nil)
sqlite3_bind_text(insertStatement, 3, (sender as NSString).utf8String, -1, nil) sqlite3_bind_text(insertStatement, 3, (sender as NSString).utf8String, -1, nil)
@ -107,17 +110,17 @@ public class ChatStorageHelper {
* @param pageSize 每页消息数量 * @param pageSize 每页消息数量
* @return 消息列表的JSON字符串 * @return 消息列表的JSON字符串
*/ */
public func getMessages(sessionId: String, page: Int, pageSize: Int) -> String { public func getMessages(agentId: String, page: Int, pageSize: Int) -> String {
let offset = (page - 1) * pageSize let offset = (page - 1) * pageSize
var messagesArray: [[String: Any]] = [] var messagesArray: [[String: Any]] = []
// 首先获取总记录数 // 首先获取总记录数
let countQuery = "SELECT COUNT(*) FROM messages WHERE session_id = ?" let countQuery = "SELECT COUNT(*) FROM messages WHERE agent_id = ?"
var countStatement: OpaquePointer? var countStatement: OpaquePointer?
var totalCount = 0 var totalCount = 0
if sqlite3_prepare_v2(db, countQuery, -1, &countStatement, nil) == SQLITE_OK { if sqlite3_prepare_v2(db, countQuery, -1, &countStatement, nil) == SQLITE_OK {
sqlite3_bind_text(countStatement, 1, (sessionId as NSString).utf8String, -1, nil) sqlite3_bind_text(countStatement, 1, (agentId as NSString).utf8String, -1, nil)
if sqlite3_step(countStatement) == SQLITE_ROW { if sqlite3_step(countStatement) == SQLITE_ROW {
totalCount = Int(sqlite3_column_int(countStatement, 0)) totalCount = Int(sqlite3_column_int(countStatement, 0))
} }
@ -130,7 +133,7 @@ public class ChatStorageHelper {
let queryString = """ let queryString = """
SELECT id, message, sender, metadata, created_at SELECT id, message, sender, metadata, created_at
FROM messages FROM messages
WHERE session_id = ? WHERE agent_id = ?
ORDER BY created_at DESC ORDER BY created_at DESC
LIMIT ? OFFSET ? LIMIT ? OFFSET ?
""" """
@ -138,7 +141,7 @@ public class ChatStorageHelper {
var queryStatement: OpaquePointer? var queryStatement: OpaquePointer?
if sqlite3_prepare_v2(db, queryString, -1, &queryStatement, nil) == SQLITE_OK { if sqlite3_prepare_v2(db, queryString, -1, &queryStatement, nil) == SQLITE_OK {
sqlite3_bind_text(queryStatement, 1, (sessionId as NSString).utf8String, -1, nil) sqlite3_bind_text(queryStatement, 1, (agentId as NSString).utf8String, -1, nil)
sqlite3_bind_int(queryStatement, 2, Int32(pageSize)) sqlite3_bind_int(queryStatement, 2, Int32(pageSize))
sqlite3_bind_int(queryStatement, 3, Int32(offset)) sqlite3_bind_int(queryStatement, 3, Int32(offset))
@ -163,7 +166,7 @@ public class ChatStorageHelper {
var messageDict: [String: Any] = [ var messageDict: [String: Any] = [
"id": id, "id": id,
"sessionId": sessionId, // 添加sessionId字段 "agentId": agentId, // 添加agentId字段
"message": message, "message": message,
"sender": sender, "sender": sender,
"timestamp": timestamp // 使用timestamp而不是created_at "timestamp": timestamp // 使用timestamp而不是created_at
@ -227,13 +230,13 @@ public class ChatStorageHelper {
* @param messageIds 可选的消息ID列表,指定则删除特定消息 * @param messageIds 可选的消息ID列表,指定则删除特定消息
* @return 是否删除成功 * @return 是否删除成功
*/ */
public func deleteMessages(sessionId: String?, messageIds: [Int]?) -> Bool { public func deleteMessages(agentId: String?, messageIds: [Int]?) -> Bool {
if let sessionId = sessionId { if let agentId = agentId {
let deleteString = "DELETE FROM messages WHERE session_id = ?;" let deleteString = "DELETE FROM messages WHERE agent_id = ?;"
var deleteStatement: OpaquePointer? var deleteStatement: OpaquePointer?
if sqlite3_prepare_v2(db, deleteString, -1, &deleteStatement, nil) == SQLITE_OK { if sqlite3_prepare_v2(db, deleteString, -1, &deleteStatement, nil) == SQLITE_OK {
sqlite3_bind_text(deleteStatement, 1, (sessionId as NSString).utf8String, -1, nil) sqlite3_bind_text(deleteStatement, 1, (agentId as NSString).utf8String, -1, nil)
if sqlite3_step(deleteStatement) == SQLITE_DONE { if sqlite3_step(deleteStatement) == SQLITE_DONE {
sqlite3_finalize(deleteStatement) sqlite3_finalize(deleteStatement)
@ -302,7 +305,7 @@ public class ChatStorageHelper {
* @param limit 限制数量 * @param limit 限制数量
* @return 消息列表,按时间正序排列 * @return 消息列表,按时间正序排列
*/ */
public func getRecentMessages(sessionId: String, limit: Int) -> [[String: Any]] { public func getRecentMessages(agentId: String, limit: Int) -> [[String: Any]] {
var messages: [[String: Any]] = [] var messages: [[String: Any]] = []
// 首先检查数据库中是否有该会话的消息 // 首先检查数据库中是否有该会话的消息
@ -310,7 +313,7 @@ public class ChatStorageHelper {
var countStatement: OpaquePointer? var countStatement: OpaquePointer?
if sqlite3_prepare_v2(db, countQuery, -1, &countStatement, nil) == SQLITE_OK { if sqlite3_prepare_v2(db, countQuery, -1, &countStatement, nil) == SQLITE_OK {
sqlite3_bind_text(countStatement, 1, (sessionId as NSString).utf8String, -1, nil) sqlite3_bind_text(countStatement, 1, (agentId as NSString).utf8String, -1, nil)
if sqlite3_step(countStatement) == SQLITE_ROW { if sqlite3_step(countStatement) == SQLITE_ROW {
_ = Int(sqlite3_column_int(countStatement, 0)) _ = Int(sqlite3_column_int(countStatement, 0))
} }
@ -331,7 +334,7 @@ public class ChatStorageHelper {
var queryStatement: OpaquePointer? var queryStatement: OpaquePointer?
if sqlite3_prepare_v2(db, queryString, -1, &queryStatement, nil) == SQLITE_OK { if sqlite3_prepare_v2(db, queryString, -1, &queryStatement, nil) == SQLITE_OK {
sqlite3_bind_text(queryStatement, 1, (sessionId as NSString).utf8String, -1, nil) sqlite3_bind_text(queryStatement, 1, (agentId as NSString).utf8String, -1, nil)
sqlite3_bind_int(queryStatement, 2, Int32(limit)) sqlite3_bind_int(queryStatement, 2, Int32(limit))
while sqlite3_step(queryStatement) == SQLITE_ROW { while sqlite3_step(queryStatement) == SQLITE_ROW {

12
local_plugins/chat_storage/ios/chat_storage/Sources/chat_storage/SwiftChatStoragePlugin.swift

@ -20,6 +20,7 @@ public class SwiftChatStoragePlugin: NSObject, FlutterPlugin {
case "saveMessage": case "saveMessage":
do { do {
guard let args = call.arguments as? [String: Any], guard let args = call.arguments as? [String: Any],
let agentId = args["agentId"] as? String,
let sessionId = args["sessionId"] as? String, let sessionId = args["sessionId"] as? String,
let message = args["message"] as? String, let message = args["message"] as? String,
let sender = args["sender"] as? String else { let sender = args["sender"] as? String else {
@ -30,6 +31,7 @@ public class SwiftChatStoragePlugin: NSObject, FlutterPlugin {
let metadata = args["metadata"] as? String let metadata = args["metadata"] as? String
let id = ChatStorageHelper.shared.saveMessage( let id = ChatStorageHelper.shared.saveMessage(
agentId:agentId,
sessionId: sessionId, sessionId: sessionId,
message: message, message: message,
sender: sender, sender: sender,
@ -49,7 +51,7 @@ public class SwiftChatStoragePlugin: NSObject, FlutterPlugin {
case "getMessages": case "getMessages":
do { do {
guard let args = call.arguments as? [String: Any], guard let args = call.arguments as? [String: Any],
let sessionId = args["sessionId"] as? String else { let agentId = args["agentId"] as? String else {
result(FlutterError(code: "INVALID_ARGS", message: "参数无效", details: nil)) result(FlutterError(code: "INVALID_ARGS", message: "参数无效", details: nil))
return return
} }
@ -58,7 +60,7 @@ public class SwiftChatStoragePlugin: NSObject, FlutterPlugin {
let pageSize = args["pageSize"] as? Int ?? 20 let pageSize = args["pageSize"] as? Int ?? 20
let messagesJson = ChatStorageHelper.shared.getMessages( let messagesJson = ChatStorageHelper.shared.getMessages(
sessionId: sessionId, agentId: agentId,
page: page, page: page,
pageSize: pageSize pageSize: pageSize
) )
@ -77,11 +79,11 @@ public class SwiftChatStoragePlugin: NSObject, FlutterPlugin {
return return
} }
let sessionId = args["sessionId"] as? String let agentId = args["agentId"] as? String
let messageIds = args["messageIds"] as? [Int] let messageIds = args["messageIds"] as? [Int]
let success = ChatStorageHelper.shared.deleteMessages( let success = ChatStorageHelper.shared.deleteMessages(
sessionId: sessionId, agentId: agentId,
messageIds: messageIds messageIds: messageIds
) )
@ -109,4 +111,4 @@ public class SwiftChatStoragePlugin: NSObject, FlutterPlugin {
result(FlutterMethodNotImplemented) result(FlutterMethodNotImplemented)
} }
} }
} }

24
local_plugins/chat_storage/lib/chat_storage.dart

@ -6,7 +6,8 @@ import 'package:flutter/services.dart';
/// 聊天记录数据模型 /// 聊天记录数据模型
class ChatMessage { class ChatMessage {
final int? id; final int? id;
final String sessionId; final String agentId; // 智能体ID
final String sessionId; // 会话ID
final String message; final String message;
final String sender; final String sender;
final String timestamp; final String timestamp;
@ -14,6 +15,7 @@ class ChatMessage {
ChatMessage({ ChatMessage({
this.id, this.id,
required this.agentId,
required this.sessionId, required this.sessionId,
required this.message, required this.message,
required this.sender, required this.sender,
@ -24,6 +26,7 @@ class ChatMessage {
Map<String, dynamic> toMap() { Map<String, dynamic> toMap() {
return { return {
'id': id, 'id': id,
'agentid': agentId,
'sessionId': sessionId, 'sessionId': sessionId,
'message': message, 'message': message,
'sender': sender, 'sender': sender,
@ -35,6 +38,7 @@ class ChatMessage {
factory ChatMessage.fromMap(Map<String, dynamic> map) { factory ChatMessage.fromMap(Map<String, dynamic> map) {
return ChatMessage( return ChatMessage(
id: map['id'], id: map['id'],
agentId: map['agentId'],
sessionId: map['sessionId'], sessionId: map['sessionId'],
message: map['message'], message: map['message'],
sender: map['sender'], sender: map['sender'],
@ -44,8 +48,6 @@ class ChatMessage {
} }
} }
/// 分页结果数据模型 /// 分页结果数据模型
class PagedResult { class PagedResult {
final List<ChatMessage> messages; final List<ChatMessage> messages;
@ -69,6 +71,7 @@ class ChatStorage {
/// 保存一条聊天记录 /// 保存一条聊天记录
static Future<Map<String, dynamic>> saveMessage({ static Future<Map<String, dynamic>> saveMessage({
required String agentId,
required String sessionId, required String sessionId,
required String message, required String message,
required String sender, required String sender,
@ -76,6 +79,7 @@ class ChatStorage {
}) async { }) async {
try { try {
final result = await _channel.invokeMethod('saveMessage', { final result = await _channel.invokeMethod('saveMessage', {
'agentId': agentId,
'sessionId': sessionId, 'sessionId': sessionId,
'message': message, 'message': message,
'sender': sender, 'sender': sender,
@ -89,13 +93,13 @@ class ChatStorage {
/// 获取指定会话的聊天记录(支持分页) /// 获取指定会话的聊天记录(支持分页)
static Future<PagedResult> getMessages({ static Future<PagedResult> getMessages({
required String sessionId, required String agentId,
int page = 1, int page = 1,
int pageSize = 20, int pageSize = 20,
}) async { }) async {
try { try {
final String result = await _channel.invokeMethod('getMessages', { final String result = await _channel.invokeMethod('getMessages', {
'sessionId': sessionId, 'agentId': agentId,
'page': page, 'page': page,
'pageSize': pageSize, 'pageSize': pageSize,
}); });
@ -125,20 +129,18 @@ class ChatStorage {
} }
} }
/// 删除聊天记录,可按会话ID或消息ID列表删除 /// 删除聊天记录,可按会话ID或消息ID列表删除
static Future<Map<String, dynamic>> deleteMessages({ static Future<Map<String, dynamic>> deleteMessages({
String? sessionId, String? agentId,
List<int>? messageIds, List<int>? messageIds,
}) async { }) async {
try { try {
if (sessionId == null && (messageIds == null || messageIds.isEmpty)) { if (agentId == null && (messageIds == null || messageIds.isEmpty)) {
return {'success': false, 'error': '必须提供sessionId或messageIds'}; return {'success': false, 'error': '必须提供sessionId或messageIds'};
} }
final result = await _channel.invokeMethod('deleteMessages', { final result = await _channel.invokeMethod('deleteMessages', {
'sessionId': sessionId, 'agentId': agentId,
'messageIds': messageIds, 'messageIds': messageIds,
}); });
return Map<String, dynamic>.from(result); return Map<String, dynamic>.from(result);
@ -156,6 +158,4 @@ class ChatStorage {
return {'success': false, 'error': e.toString()}; return {'success': false, 'error': e.toString()};
} }
} }
} }

Loading…
Cancel
Save