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';
class Message {
final String sessionid; // 会话ID
final bool isUser; //标识信息是否来自用户还是AI
final String text; //信息文本内容
final DateTime timestamp; // 消息时间戳
@ -10,9 +11,12 @@ class Message {
final bool hasImage; // 是否包含图片
final String? imagePath; // 图片路径
final Map? meta; //卡片数据
final List<String>? functionCalls; // 函数调用
bool isToolCallsExpanded; // 工具调用是否展开
Message({
// 构造函数
required this.sessionid, // 会话ID
required this.isUser,
required this.text,
DateTime? timestamp,
@ -20,11 +24,14 @@ class Message {
this.hasImage = false,
this.imagePath,
this.meta,
this.functionCalls,
this.isToolCallsExpanded = false, // 默认不展开
}) : this.timestamp = timestamp ?? DateTime.now();
// 从JSON构造函数
factory Message.fromJson(Map<String, dynamic> json) {
return Message(
sessionid: json['sessionid'] as String, // 会话ID
isUser: json['isUser'] as bool,
text: json['text'] as String,
timestamp: DateTime.parse(json['timestamp'] as String),
@ -32,6 +39,8 @@ class Message {
hasImage: json['hasImage'] as bool? ?? false,
imagePath: json['imagePath'] as String?,
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,
'imagePath': imagePath,
'meta': meta,
'functionCalls': functionCalls,
'isToolCallsExpanded': isToolCallsExpanded,
};
}
// 创建一个加载中的消息
factory Message.loading() {
return Message(
sessionid: '', // 会话ID
isUser: false,
text: '',
timestamp: DateTime.now(),
@ -60,6 +72,7 @@ class Message {
// 复制并修改
Message copyWith({
required String sessionid, // 会话ID
bool? isUser,
String? text,
DateTime? timestamp,
@ -67,8 +80,11 @@ class Message {
bool? hasImage,
String? imagePath,
Map? meta,
List<String>? functionCalls,
bool? isToolCallsExpanded,
}) {
return Message(
sessionid: sessionid, // 会话ID
isUser: isUser ?? this.isUser,
text: text ?? this.text,
timestamp: timestamp ?? this.timestamp,
@ -76,6 +92,13 @@ class Message {
hasImage: hasImage ?? this.hasImage,
imagePath: imagePath ?? this.imagePath,
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:convert';
import 'dart:ffi';
import 'dart:io';
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:url_launcher/url_launcher.dart';
import 'package:uuid/uuid.dart';
import 'package:voitrans/data/models/appconfig.dart';
import 'package:voitrans/data/models/song_model.dart';
import 'package:voitrans/data/services/ble_manager.dart';
import 'package:voitrans/data/services/bluetooth_manager.dart';
import 'package:voitrans/data/services/qq_music.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 '/core/utils/upload_oss.dart';
import '/data/models/user_Info.dart';
import '/data/services/music_manager.dart';
import '/data/services/navigation_manager.dart';
import '/data/services/network/api.dart';
import 'package:ble_service/ble_service.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
@ -46,6 +42,7 @@ class AgentController extends GetxController with WidgetsBindingObserver {
// 聊天消息列表
final messages = <Message>[].obs;
final currsessionid = ''.obs;
// UI控制器
final TextEditingController textController = TextEditingController();
@ -138,7 +135,7 @@ class AgentController extends GetxController with WidgetsBindingObserver {
final TAG = 'AgentController';
var AppDir = "".obs;
// 会话ID
final String sessionId = 'default_agent';
final String agentId = 'default_agent';
// 分页加载相关状态
final isLoadingMore = false.obs; // 是否正在加载更多消息
@ -312,7 +309,7 @@ class AgentController extends GetxController with WidgetsBindingObserver {
Logger.i(TAG, '正在加载聊天历史...');
//通过本地插件ChatStorage获取聊天记录
final result = await ChatStorage.getMessages(
sessionId: sessionId, //会话ID
agentId: agentId, //会话ID
page: 1, //从第一页开始
pageSize: pageSize, //每页20条记录
);
@ -367,7 +364,7 @@ class AgentController extends GetxController with WidgetsBindingObserver {
Logger.i(TAG, '正在加载第 $nextPage 页历史消息...');
final result = await ChatStorage.getMessages(
sessionId: sessionId,
agentId: agentId,
page: nextPage,
pageSize: pageSize,
);
@ -421,6 +418,7 @@ class AgentController extends GetxController with WidgetsBindingObserver {
: null;
return Message(
sessionid: msg.sessionId, // 会话ID
isUser: msg.sender == 'user',
text: msg.message,
meta: meta,
@ -488,6 +486,7 @@ class AgentController extends GetxController with WidgetsBindingObserver {
// 添加初始欢迎消息
void _addWelcomeMessage() {
messages.add(Message(
sessionid: "", // 会话ID
isUser: false,
text: 'welcomeMessage'.tr, // 你好!我是**小言**,我能帮你:
));
@ -504,6 +503,7 @@ class AgentController extends GetxController with WidgetsBindingObserver {
// isTextInputMode.refresh();
// 只标记识别开始,不添加消息
Logger.i(TAG, '识别开始');
break;
case AgentServiceEventType.recognitionStopped:
isListening.value = false;
@ -527,25 +527,29 @@ class AgentController extends GetxController with WidgetsBindingObserver {
break;
case AgentServiceEventType.recognizing:
final sessionid = event.data['sessionid'] ?? '';
final text = event.data['text'] ?? '';
currentText.value = text; // 保留当前文本,以便其他地方使用
Logger.i(TAG, '识别中间结果: $text');
if (text.isNotEmpty) {
// 查找是否有正在识别中的消息
final index = messages
.lastIndexWhere((msg) => msg.isRecognizing && msg.isUser);
final index = messages.lastIndexWhere(
(msg) => msg.sessionid == sessionid && msg.isUser);
if (index >= 0) {
// 更新已有的识别中消息
messages[index] = Message(
sessionid: sessionid, // 会话ID
isUser: true,
text: text,
isRecognizing: true,
timestamp: messages[index].timestamp,
);
} else {
currsessionid.value = event.data['sessionid'] ?? '';
// 没有识别中的消息,创建新消息
messages.add(Message(
sessionid: sessionid, // 会话ID
isUser: true,
text: text,
isRecognizing: true,
@ -556,26 +560,33 @@ class AgentController extends GetxController with WidgetsBindingObserver {
break;
case AgentServiceEventType.recognitionResult:
final sessionid = event.data['sessionid'] ?? '';
final text = event.data['text'] ?? '';
Logger.i(TAG, '识别最终结果: $text');
// 查找是否有正在识别中的消息
final index =
messages.lastIndexWhere((msg) => msg.isRecognizing && msg.isUser);
final index = messages.lastIndexWhere(
(msg) => msg.sessionid == sessionid && msg.isUser);
if (text.isNotEmpty) {
if (index >= 0) {
// 将识别中的消息转换为最终消息
messages[index] = Message(
sessionid: sessionid,
isUser: true,
text: text,
isRecognizing: false,
timestamp: messages[index].timestamp,
functionCalls: messages[index].functionCalls,
isToolCallsExpanded: messages[index].isToolCallsExpanded,
);
// messages.refresh();
} else {
// 如果没有找到识别中的消息,添加新消息
messages
.add(Message(isUser: true, text: text, isRecognizing: false));
messages.add(Message(
sessionid: sessionid,
isUser: true,
text: text,
isRecognizing: false));
}
isProcessing.value = true;
@ -590,24 +601,31 @@ class AgentController extends GetxController with WidgetsBindingObserver {
break;
//识别取消(网络波动会触发)
case AgentServiceEventType.recognitionCanceled:
final sessionid = event.data['sessionid'] ?? '';
// 查找是否有正在识别中的消息
final index =
messages.lastIndexWhere((msg) => msg.isRecognizing && msg.isUser);
final index = messages.lastIndexWhere(
(msg) => msg.sessionid == sessionid && msg.isUser);
if (currentText.value.isNotEmpty) {
if (index >= 0) {
// 将识别中的消息转换为最终消息
messages[index] = Message(
sessionid: sessionid, // 会话ID
isUser: true,
text: currentText.value,
isRecognizing: false,
timestamp: messages[index].timestamp,
functionCalls: messages[index].functionCalls,
isToolCallsExpanded: messages[index].isToolCallsExpanded,
);
// messages.refresh();
} else {
// 如果没有找到识别中的消息,添加新消息
messages.add(Message(
isUser: true, text: currentText.value, isRecognizing: false));
sessionid: sessionid,
isUser: true,
text: currentText.value,
isRecognizing: false));
}
isProcessing.value = true;
@ -654,29 +672,27 @@ class AgentController extends GetxController with WidgetsBindingObserver {
case AgentServiceEventType.assistantToken:
if (!isProcessing.value) isProcessing.value = true;
final sessionid = event.data['sessionid'] ?? '';
final token = event.data['token'] ?? '';
final responseId = event.data['responseId'] ?? '';
// Logger.i(TAG, 'AI回复Token: $token, responseId: $responseId');
if (token.isNotEmpty) {
// 如果是新的回复或者响应ID改变,创建新消息
if (_isNewAssistantResponse ||
(_currentResponseId != responseId && responseId.isNotEmpty)) {
messages.add(Message(isUser: false, text: token));
final index = messages.lastIndexWhere(
(msg) => msg.sessionid == sessionid && !msg.isUser);
if (index == -1) {
messages.add(
Message(sessionid: sessionid, isUser: false, text: token));
_isNewAssistantResponse = false;
_currentResponseId = responseId;
} else {
// 否则更新最后一条消息
final lastIndex = messages.length - 1;
if (lastIndex >= 0 && !messages[lastIndex].isUser) {
messages[lastIndex] = Message(
isUser: false,
text: messages[lastIndex].text + token,
meta: messages[lastIndex].meta,
timestamp: messages[lastIndex].timestamp,
);
}
messages[index] = Message(
sessionid: sessionid,
isUser: false,
text: messages[index].text + token,
meta: messages[index].meta,
timestamp: messages[index].timestamp,
functionCalls: messages[index].functionCalls,
isToolCallsExpanded: messages[index].isToolCallsExpanded,
);
}
// 使用reverse:true时通常不需要手动滚动
}
@ -713,10 +729,10 @@ class AgentController extends GetxController with WidgetsBindingObserver {
case AgentServiceEventType.autoStop:
isListening.value = false;
final sessionid = event.data['sessionid'] ?? '';
// 移除临时的识别消息
final index =
messages.lastIndexWhere((msg) => msg.isRecognizing && msg.isUser);
final index = messages.lastIndexWhere(
(msg) => msg.sessionid == sessionid && msg.isUser);
if (index >= 0) {
// 只有当消息为空或文本太短时才移除
if (messages[index].text.trim().length < 2) {
@ -725,21 +741,25 @@ class AgentController extends GetxController with WidgetsBindingObserver {
} else {
// 否则转换为正式消息
messages[index] = Message(
sessionid: sessionid,
isUser: true,
text: messages[index].text,
timestamp: messages[index].timestamp,
functionCalls: messages[index].functionCalls,
isToolCallsExpanded: messages[index].isToolCallsExpanded,
);
// messages.refresh();
// 发送此消息到AI处理
isProcessing.value = true;
_isNewAssistantResponse = true;
_handleAutoStop(index);
_handleAutoStop(sessionid, index);
}
}
break;
case AgentServiceEventType.functionCall:
final sessionid = event.data['sessionid'] ?? '';
final functionName = event.data['name'] ?? '';
final arguments = event.data['arguments'] ?? '';
// debugPrint('Fultter 开始调用 mcp: $functionName, $arguments');
@ -748,15 +768,39 @@ class AgentController extends GetxController with WidgetsBindingObserver {
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;
case AgentServiceEventType.functionCallResult:
final responseId = event.data['responseId'] ?? '';
final sessionid = event.data['sessionid'] ?? '';
// final result = event.data['result'];
final metaStr = event.data['meta'] ?? '';
final functionCall = event.data['function_call'] ?? '';
Logger.i(TAG, 'Flutter 结束调用 mcp: $metaStr');
Logger.i(TAG, 'Flutter 结束调用 $sessionid mcp: $metaStr');
if (metaStr.isNotEmpty) {
Map<String, dynamic> metaResult = {};
metaResult = jsonDecode(metaStr) as Map<String, dynamic>;
@ -771,25 +815,31 @@ class AgentController extends GetxController with WidgetsBindingObserver {
//不回复直接返回
break;
}
final index = messages.lastIndexWhere(
(msg) => msg.sessionid == sessionid && !msg.isUser);
Logger.i(TAG, 'Flutter 结束调用 卡片数据 $index $metaResult');
// 判断是否为新的回复或响应ID是否改变
if (_isNewAssistantResponse || _currentResponseId != responseId) {
if (index == -1) {
// 添加新消息
messages
.add(Message(isUser: false, text: "", meta: metaResult));
messages.add(Message(
sessionid: sessionid,
isUser: false,
text: "",
meta: metaResult));
_isNewAssistantResponse = false;
_currentResponseId = responseId;
} else {
// 更新最后一条消息
final lastIndex = messages.length - 1;
if (lastIndex >= 0 && !messages[lastIndex].isUser) {
messages[lastIndex] = Message(
isUser: false,
text: messages[lastIndex].text,
meta: metaResult,
timestamp: messages[lastIndex].timestamp,
);
}
messages[index] = Message(
sessionid: sessionid,
isUser: false,
text: messages[index].text,
meta: metaResult,
timestamp: messages[index].timestamp,
functionCalls: messages[index].functionCalls,
isToolCallsExpanded: messages[index].isToolCallsExpanded,
);
}
messages.refresh();
if (metaResult.containsKey("maps_route_navigation")) {
final 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")) {
@ -893,6 +946,7 @@ class AgentController extends GetxController with WidgetsBindingObserver {
// ✅ 创建新的 Message 替换原来的
messages[index] = Message(
sessionid: result.sessionid,
isUser: result.isUser,
text: result.text,
meta: newMeta,
@ -1002,10 +1056,13 @@ class AgentController extends GetxController with WidgetsBindingObserver {
// 标记为新的AI回复
_isNewAssistantResponse = true;
newAIReplyCompleted.value = false;
currsessionid.value = const Uuid().v4();
final sessionid = currsessionid.value;
// 根据TTS状态决定是否朗读
await AgentService.processTextInput(text,
AgentService.processTextInput(sessionid, text,
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);
textController.clear();
} catch (e) {
@ -1029,12 +1086,17 @@ class AgentController extends GetxController with WidgetsBindingObserver {
var ImageUrl = await UploadOss.upload(
filepath: permanentImagePath,
rootDir: 'User/${User.instance.uid}/ChatImages');
Logger.i(TAG, '图片上传: $permanentImagePath URL: $ImageUrl');
// 显示的文本,如果没有提供则使用默认值
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(
sessionid: sessionid,
isUser: true,
text: displayText,
hasImage: true,
@ -1055,9 +1117,11 @@ class AgentController extends GetxController with WidgetsBindingObserver {
// 保存消息到持久化存储
//await _saveMessageToStorage(message);
currsessionid.value = const Uuid().v4();
final sessionid = currsessionid.value;
// 调用Agent Service处理图片,考虑TTS状态
await AgentService.processImageInput(
sessionid,
relativeImagePath,
ImageUrl,
text: text ?? '',
@ -1855,11 +1919,13 @@ class AgentController extends GetxController with WidgetsBindingObserver {
}
// 修改自动停止处理,遵循TTS设置
void _handleAutoStop(int index) {
void _handleAutoStop(String sessionid, int index) {
// 发送此消息到AI处理
isProcessing.value = true;
// 标记为新的AI回复
_isNewAssistantResponse = true;
AgentService.processTextInput(messages[index].text,
currsessionid.value = sessionid;
AgentService.processTextInput(sessionid, messages[index].text,
speakResponse: isTtsEnabled.value);
}
@ -2612,4 +2678,15 @@ class AgentController extends GetxController with WidgetsBindingObserver {
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);
},
child: MessageBubble(
sessionid: message.sessionid,
isUser: message.isUser,
message: message.text,
timestamp: message.timestamp,
@ -294,6 +295,9 @@ class _AgentViewState extends State<AgentView> with WidgetsBindingObserver {
hasImage: message.hasImage,
imagePath: message.imagePath,
meta: message.meta,
functionCalls: message.functionCalls,
isToolCallsExpanded:
message.isToolCallsExpanded,
),
),
),
@ -307,6 +311,7 @@ class _AgentViewState extends State<AgentView> with WidgetsBindingObserver {
context, isDarkMode, details, message);
},
child: MessageBubble(
sessionid: message.sessionid,
isUser: message.isUser,
message: message.text,
timestamp: message.timestamp,
@ -314,6 +319,9 @@ class _AgentViewState extends State<AgentView> with WidgetsBindingObserver {
hasImage: message.hasImage,
imagePath: message.imagePath,
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> {
final String sessionid;
final bool isUser;
final String message;
final DateTime timestamp;
@ -25,9 +26,11 @@ class MessageBubble extends GetView<AgentController> {
final bool hasImage;
final String? imagePath;
final Map? meta;
final List<String>? functionCalls;
final bool isToolCallsExpanded;
const MessageBubble({
Key? key,
required this.sessionid,
required this.isUser,
required this.message,
required this.timestamp,
@ -35,6 +38,8 @@ class MessageBubble extends GetView<AgentController> {
this.hasImage = false,
this.imagePath,
this.meta,
this.functionCalls,
this.isToolCallsExpanded = false,
}) : super(key: key);
@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),
// 识别中的消息不显示时间
@ -89,7 +111,7 @@ class MessageBubble extends GetView<AgentController> {
Text(
_formatTimestamp(timestamp),
style: TextStyle(
fontSize: 10,
fontSize: 14,
color: Theme.of(context).brightness == Brightness.dark
? Colors.grey[400]
: 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();
}
// 如果只有工具调用而没有文本内容,显示一个占位符确保气泡有足够宽度
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) {
// 如果消息是默认的[图片]标记,只显示图片
@ -369,9 +402,12 @@ class MessageBubble extends GetView<AgentController> {
// }
}
// 创建消息内容Widget
Widget messageContent;
// 使用Markdown渲染AI消息,用户消息仍然使用普通文本
if (!isUser) {
return MarkdownBody(
messageContent = MarkdownBody(
data: message,
styleSheet: MarkdownStyleSheet(
p: TextStyle(
@ -442,21 +478,34 @@ class MessageBubble extends GetView<AgentController> {
),
//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(
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,
),
);
// 如果是AI消息且有工具调用,确保最小宽度以防止工具调用列表越界
if (!isUser && functionCalls != null && functionCalls!.isNotEmpty) {
return Container(
constraints: const BoxConstraints(
minWidth: 200, // 确保最小宽度,防止工具调用列表越界
),
child: messageContent,
);
}
return messageContent;
}
/// 构建输入中的指示器
@ -550,6 +599,155 @@ class MessageBubble extends GetView<AgentController> {
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.TtsEventListener
import com.deep_voice.speech.tts.TtsEventType
import java.util.UUID
import okhttp3.*
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.toRequestBody
@ -88,7 +87,7 @@ object AgentService : CoroutineScope {
private lateinit var chatStorageHelper: ChatStorageHelper
// 会话ID,用于区分不同聊天上下文
private val sessionId = "default_agent"
private val agentId = "default_agent"
// 服务端连接
private var serverurl = ""
private var usertoken = ""
@ -602,7 +601,7 @@ object AgentService : CoroutineScope {
Log.d(TAG, "recognizeCallback")
val success = azureAsrHelper?.setupEventListeners(object :
AzureAsrHelper.ContinuousRecognizeCallback {
override fun onRecognizing(recognizing: String, detectedLanguage: String) {
override fun onRecognizing(sessiondid:String, recognizing: String, detectedLanguage: String) {
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) {//加入是否结束要输出最后的结果判读
sendEvent(
"recognition_result", mapOf(
"sessiondid" to sessiondid,
"text" to result,
"language" to detectedLanguage
)
)
processWithChatApi(result)
processWithChatApi(sessiondid,result)
}
@ -665,8 +665,8 @@ object AgentService : CoroutineScope {
}
override fun onSessionStarted() {
sendEvent("recognition_started", mapOf("status" to "started"))
override fun onSessionStarted(sessiondid:String) {
sendEvent("recognition_started", mapOf("sessiondid" to sessiondid, "status" to "started"))
// 使用模式感知的空闲检测启动方法
audioPlayer?.playAudio(R.raw.start)
@ -674,33 +674,35 @@ object AgentService : CoroutineScope {
override fun onSessionStopped() {
sendEvent("recognition_stopped", mapOf("status" to "stopped"))
override fun onSessionStopped(sessiondid:String) {
sendEvent("recognition_stopped", mapOf("sessiondid" to sessiondid, "status" to "stopped"))
_isRecognitionActive.set(false)
stopIdleCheck()
audioPlayer?.playAudio(R.raw.stop)
}
override fun onCanceled(reason: String, errorDetails: String) {
override fun onCanceled(sessiondid: String,reason: String, errorDetails: String) {
_isRecognitionActive.set(false)
stopIdleCheck()
Log.d(TAG, "recognition_canceled:")
BleService.closeCodec()
sendEvent(
"recognition_canceled", mapOf(
"sessiondid" to sessiondid,
"reason" to reason,
"details" to errorDetails
)
)
}
override fun onError(code: Int, error: String) {
override fun onError(sessiondid:String, code: Int, error: String) {
_isRecognitionActive.set(false)
stopIdleCheck()
BleService.closeCodec()
Log.d(TAG, "语音识别错误: $error")
sendEvent(
"error", mapOf(
"sessiondid" to sessiondid,
"code" to code,
"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) {
Log.e(TAG, "服务未初始化")
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 "文本输入不能为空"))
return false
}
// 使用ChatAPI处理文本
processWithChatApi(text, speakResponse)
processWithChatApi(sessionid,text, speakResponse)
return true
}
/**
* 使用ChatAPI处理语音识别结果
*/
private fun processWithChatApi(text: String) {
processWithChatApi(text, true)
private fun processWithChatApi(sessionid:String,text: String) {
processWithChatApi(sessionid,text, true)
}
/**
@ -834,6 +835,7 @@ object AgentService : CoroutineScope {
* @param speakResponse 是否使用TTS朗读回复
*/
private fun processWithChatApi(
sessionid: String,
text: String,
speakResponse: Boolean = true
) {
@ -841,7 +843,7 @@ object AgentService : CoroutineScope {
// 创建用户文本消息并处理
val userMessage = createUserMessage(text)
processWithChatApiInternal(userMessage, text, speakResponse)
processWithChatApiInternal(sessionid,userMessage, text, speakResponse)
}
/**
@ -852,6 +854,7 @@ object AgentService : CoroutineScope {
* @param speakResponse 是否朗读回复
*/
private fun processImageWithChatApi(
sessionid: String,
imageBase64: String,
text: String = "",
speakResponse: Boolean = false
@ -861,7 +864,7 @@ object AgentService : CoroutineScope {
Log.d(TAG, "liwei--------- 图像Mesage: ${userMessage}")
// 图片描述用于存储
val displayText = text.ifEmpty { "[图片]" }
processWithChatApiInternal(userMessage, displayText, speakResponse, true)
processWithChatApiInternal(sessionid,userMessage, displayText, speakResponse, true)
}
/**
@ -874,21 +877,21 @@ object AgentService : CoroutineScope {
* @param imagePath 图片路径(如果有)
*/
private fun processWithChatApiInternal(
sessionid: String,
userMessage: JSONObject,
displayText: String,
speakResponse: Boolean = true,
hasImage: Boolean = false,
imagePath: String? = null
) {
Log.d(TAG, "内部方法:通用的ChatAPI处理逻辑")
// 如果有正在进行的AI流式输出,先停止它
audioPlayer?.stopAudio()
stopAiStream()
stopTts()
currsessionId = sessionid
// 记录新的AI任务
currentAiJob = launch {
try {
currsessionId = UUID.randomUUID().toString()
// 设置状态为正在流式输出
_isAiStreaming.set(true)
Log.d(TAG, "processWithChatApiInternal")
@ -958,7 +961,7 @@ object AgentService : CoroutineScope {
}
// Log.e("ChatApiService", "liwei---------sendMessageStream messages:${messagesWithSystemPrompt}")
chatApiService.sendMessageStream(
currsessionId,
sessionid,
messagesWithSystemPrompt,
object : StreamCallback {
@ -984,15 +987,17 @@ object AgentService : CoroutineScope {
override fun onToken(sessionid: String, token: String) {
try {
if (sessionid != currsessionId) {
// Log.d(TAG, "liwei--------------- AI Call onToken 会话过期 $sessionid-------$currsessionId")
return
}
responseBuilder.append(token)
// Log.d(TAG, "liwei--------------- AI Call onToken:$token speakResponse:$speakResponse reply:$reply broadcast:$broadcast")
if (speakResponse && reply && broadcast && token.isNotEmpty()) {
ttsService?.speakStream(token)
}
if (reply && token.isNotEmpty()) {
// 发送流式回复token
sendEvent("assistant_token", mapOf("token" to token))
sendEvent("assistant_token", mapOf("sessionid" to sessionid,"token" to token))
}
} catch (e: Exception) {
Log.e(TAG, "liwei--------------- AI Call onToken 异常", e)
@ -1011,6 +1016,7 @@ object AgentService : CoroutineScope {
val response = responseBuilder.toString()
// 发送完整回复,包含是否有图片的标记
val responseData = mutableMapOf<String, Any>(
"sessionid" to sessionid,
"text" to response,
"userInput" to displayText
)
@ -1027,6 +1033,7 @@ object AgentService : CoroutineScope {
}
// 保存聊天记录
saveChatMessage(
sessionid,
displayText,
response,
aiMetadata,
@ -1045,6 +1052,7 @@ object AgentService : CoroutineScope {
Log.e(TAG, "AI处理出错", e)
sendEvent(
"error", mapOf(
"sessionid" to sessionid,
"code" to "AI_ERROR",
"message" to e.message.toString()
)
@ -1064,6 +1072,7 @@ object AgentService : CoroutineScope {
val name = call.getString("name")
sendEvent(
"function_call", mapOf(
"sessionid" to sessionid,
"name" to name,
"arguments" to call.toString()
)
@ -1072,6 +1081,10 @@ object AgentService : CoroutineScope {
BleService.closeCodec()
stopRecognition()
}
//播报工具调用
ttsService?.speakStream("正在调用工具:${name}")
} catch (e: Exception) {
Log.e(TAG, "liwei--------------- AI Call onFunctionCall 异常", e)
}
@ -1083,6 +1096,11 @@ object AgentService : CoroutineScope {
functionCallResult: JSONObject
) {
try {
Log.d(
TAG,
"liwei--------------- AI Call onFunctionCallResult ${sessionid}:${functionCallResult.toString()}",
)
audioPlayer?.stopAudio()
val name = functionCall.get("name") as String;
val resultmap = autoHandleFcunCallResult(name, functionCallResult);
@ -1093,6 +1111,7 @@ object AgentService : CoroutineScope {
aiMetadata = resultmap["meta"] as String;
sendEvent(
"function_call_result", mapOf(
"sessionid" to sessionid,
"function_call" to functionCall.toString(),
"result" to functionCallResult.toString(),
"meta" to aiMetadata,
@ -1101,16 +1120,12 @@ object AgentService : CoroutineScope {
}else{
sendEvent(
"function_call_result", mapOf(
"sessionid" to sessionid,
"function_call" to functionCall.toString(),
"result" to functionCallResult.toString(),
)
)
}
Log.e(
TAG,
"liwei--------------- AI Call onFunctionCallResult broadcast:$broadcast reply:$reply iscard:$iscard"
)
} catch (e: Exception) {
Log.e(
TAG,
@ -1127,6 +1142,7 @@ object AgentService : CoroutineScope {
Log.e(TAG, "liwei--------------- AI处理出错", e)
sendEvent(
"error", mapOf(
"sessionid" to sessionid,
"code" to errorType,
"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()) {
return
}
@ -1389,6 +1405,7 @@ object AgentService : CoroutineScope {
* 保存聊天消息
*/
private fun saveChatMessage(
sessionid: String,
userMessage: String,
assistantMessage: String,
aiMetadata: String,
@ -1399,7 +1416,8 @@ object AgentService : CoroutineScope {
try {
// 保存用户消息
val userMessageId = chatStorageHelper.saveMessage(
sessionId = sessionId,
agentId = agentId,
sessionId = sessionid,
message = userMessage,
sender = "user",
metadata = userMetadata
@ -1409,7 +1427,8 @@ object AgentService : CoroutineScope {
if (assistantMessage.isNotEmpty() || aiMetadata.isNotEmpty()) {
// 保存AI回复
val assistantMessageId = chatStorageHelper.saveMessage(
sessionId = sessionId,
agentId = agentId,
sessionId = sessionid,
message = assistantMessage,
sender = "assistant",
metadata = aiMetadata
@ -1435,7 +1454,7 @@ object AgentService : CoroutineScope {
fun clearChatHistory(callback: (Boolean) -> Unit) {
launch {
try {
val success = chatStorageHelper.deleteMessages(sessionId)
val success = chatStorageHelper.deleteMessages(agentId)
if (success) {
synchronized(historyMessages) {
@ -1542,6 +1561,7 @@ object AgentService : CoroutineScope {
* 处理图片输入
*/
fun processImageInput(
sessionid: String,
imagePath: String,
imageUrl: String,
text: String = "",
@ -1593,7 +1613,7 @@ object AgentService : CoroutineScope {
)
Log.d(TAG, "liwei--------- 图像Mesage: ${userMessage}")
// 处理消息并传入存储路径
processWithChatApiInternal(userMessage, displayText, speakResponse, true, imagePath)
processWithChatApiInternal(sessionid,userMessage, displayText, speakResponse, true, imagePath)
} catch (e: Exception) {
Log.e(TAG, "处理图片失败: ${e.message}")
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" -> {
val sessionid = call.argument<String>("sessionid")
val text = call.argument<String>("text")
if (sessionid.isNullOrEmpty()) {
result.error("INVALID_ARGUMENTS", "回话id不能为空", null)
return
}
if (text.isNullOrEmpty()) {
result.error("INVALID_ARGUMENTS", "文本不能为空", null)
return
@ -154,6 +159,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream
try {
val isProcessing = AgentService.processTextInput(
sessionid = sessionid,
text = text,
speakResponse = speakResponse
)
@ -166,6 +172,11 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream
}
"processImageInput" -> {
val sessionid = call.argument<String>("sessionid")
if (sessionid.isNullOrEmpty()) {
result.error("INVALID_ARGUMENTS", "回话id不能为空", null)
return
}
val imagePath = call.argument<String>("imagePath")
if (imagePath.isNullOrEmpty()) {
result.error("INVALID_ARGUMENTS", "图片路径不能为空", null)
@ -181,6 +192,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream
try {
val isProcessing = AgentService.processImageInput(
sessionid=sessionid,
imagePath = imagePath,
imageUrl = imageUrl,
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 {
return AgentService.processTextInput(text, speakResponse)
fun processTextInput(sessionId: String,text: String, speakResponse: Boolean = false): Boolean {
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,用于区分不同聊天上下文
private let sessionId = "default_agent"
private let agentId = "default_agent"
private var isInitialized: Bool = false
private var isRecognizing: Bool = false
@ -565,7 +565,7 @@ audioStream.saveAudioDataTo(data: audioData)
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 ? "是" : "否")
if !isInitialized {
@ -581,21 +581,21 @@ audioStream.saveAudioDataTo(data: audioData)
}
os_log("开始处理文本输入", log: logger, type: .info)
processWithChatApiService(text: text, speakResponse: speakResponse)
processWithChatApiService(sessionid: sessionid, text: text, speakResponse: speakResponse)
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 {
sendError("ChatAPI服务未初始化", code: "CHATAPI_NOT_INITIALIZED")
return
}
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}@",
log: logger, type: .info, displayText, speakResponse ? "是" : "否", hasImage ? "是" : "否")
@ -650,7 +650,7 @@ audioStream.saveAudioDataTo(data: audioData)
hasImage: hasImage,
imagePath: imagePath
)
currsessionId = UUID().uuidString
currsessionId = sessionid
azureTtsHelper?.startspeak(sessionid: currsessionId)
os_log("设置回调并调用sendMessageStream", log: logger, type: .info)
chatApiService.setStreamCallback(currsessionId,callback)
@ -659,7 +659,7 @@ audioStream.saveAudioDataTo(data: audioData)
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 {
sendError("服务未初始化", code: "NOT_INITIALIZED")
return false
@ -703,6 +703,7 @@ audioStream.saveAudioDataTo(data: audioData)
let displayText = text.isEmpty ? "[图片]" : text
self.processWithChatApiServiceInternal(
sessionid: sessionid,
userMessage: userMessage,
displayText: displayText,
speakResponse: speakResponse,
@ -731,7 +732,7 @@ audioStream.saveAudioDataTo(data: audioData)
chatHistory.removeAll()
// 获取最近10条消息
let recentMessages = chatStorageHelper.getRecentMessages(sessionId: sessionId, limit: 10)
let recentMessages = chatStorageHelper.getRecentMessages(agentId: agentId, limit: 10)
if recentMessages.isEmpty {
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 {
// 保存用户消息
let userMessageId = self.chatStorageHelper.saveMessage(
sessionId: self.sessionId,
agentId: self.agentId,
sessionId: sessionId,
message: userMessage,
sender: "user",
metadata: userMetadata
@ -777,7 +779,8 @@ audioStream.saveAudioDataTo(data: audioData)
if !assistantMessage.isEmpty || !metadata.isEmpty {
// 保存AI回复
let assistantMessageId = self.chatStorageHelper.saveMessage(
sessionId: self.sessionId,
agentId: self.agentId,
sessionId: sessionId,
message: assistantMessage,
sender: "assistant",
metadata: metadata.isEmpty ? nil : metadata
@ -1348,7 +1351,7 @@ private func jsonToString(_ json: [String: Any]) -> String? {
chatHistory.removeAll()
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 {
os_log("清除聊天历史失败", log: self.logger, type: .error)
}
@ -1681,7 +1684,7 @@ class ChatApiStreamCallback: StreamCallback {
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
os_log("设置AI流式状态为false", log: agentService.logger, type: .info)
}catch{
@ -2061,7 +2064,7 @@ class AudioPlayer {
}
extension AgentServiceImpl: AzureAsrHelper.ContinuousRecognizeCallback {
func onResult(_ text: String, _ detectedLanguage: String) {
func onResult(sessionid:String ,_ text: String, _ detectedLanguage: String) {
if !text.isEmpty && isKeepResult {
var data: [String: Any] = ["text": text]
if !detectedLanguage.isEmpty {
@ -2069,7 +2072,7 @@ extension AgentServiceImpl: AzureAsrHelper.ContinuousRecognizeCallback {
}
sendEvent(name: "recognition_result", data: data)
processTextInput(text, speakResponse: true)
processTextInput(sessionid: sessionid,text, speakResponse: true)
}
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 {
let previousHasSpeech = hasSpeechDetected
hasSpeechDetected = true
@ -2102,7 +2105,7 @@ extension AgentServiceImpl: AzureAsrHelper.ContinuousRecognizeCallback {
}
}
func onSessionStarted() {
func onSessionStarted(sessionid:String) {
sendEvent(name: "recognition_started", data: ["status": "started"])
isRecognizing = true
hasSpeechDetected = false
@ -2123,7 +2126,7 @@ extension AgentServiceImpl: AzureAsrHelper.ContinuousRecognizeCallback {
}
}
func onSessionStopped() {
func onSessionStopped(sessionid:String) {
sendEvent(name: "recognition_stopped", data: ["status": "stopped"])
isRecognizing = 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] = [:]
if !reason.isEmpty {
data["reason"] = reason
@ -2150,7 +2153,7 @@ extension AgentServiceImpl: AzureAsrHelper.ContinuousRecognizeCallback {
stopIdleCheck()
}
func onError(_ errorCode: Int, _ error: String) {
func onError(sessionid:String ,_ errorCode: Int, _ error: String) {
let data: [String: Any] = ["message": error.isEmpty ? "未知错误" : error]
sendEvent(name: "error", data: data)
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
case "processTextInput":
guard let arguments = call.arguments as? [String: Any],
let sessionid = arguments["sessionid"] as? String,
let text = arguments["text"] as? String else {
result(FlutterError(code: "INVALID_ARGUMENTS", message: "文本不能为空", details: nil))
return
}
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)
case "processImageInput":
guard let arguments = call.arguments as? [String: Any],
let sessionid = arguments["sessionid"] as? String,
let imageUrl = arguments["imageUrl"] as? String,
let imagePath = arguments["imagePath"] as? String else {
result(FlutterError(code: "INVALID_ARGUMENTS", message: "图片路径不能为空", details: nil))
@ -123,7 +125,7 @@ public class AgentServicePlugin: NSObject, FlutterPlugin {
let text = arguments["text"] as? String ?? ""
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)
case "speakText":

7
local_plugins/agent_service/lib/agent_service.dart

@ -359,10 +359,11 @@ class AgentService {
/// [text] 文本内容
/// [speakResponse] 是否朗读响应
/// 返回是否成功处理文本
static Future<bool> processTextInput(String text,
static Future<bool> processTextInput(String sessionid, String text,
{bool speakResponse = false}) async {
try {
final bool result = await _channel.invokeMethod('processTextInput', {
'sessionid': sessionid,
'text': text,
'speakResponse': speakResponse,
});
@ -440,10 +441,12 @@ class AgentService {
/// [imageUrl] 图片url
/// [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 {
try {
final bool result = await _channel.invokeMethod('processImageInput', {
'sessionid': sessionid,
'imagePath': imagePath,
'imageUrl': imageUrl,
'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.microsoft.cognitiveservices.speech.audio.AudioConfig
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 val networkLostDebounceInterval = 3000L // 2秒防抖间隔
private var currsessionid = "" //识别回话id 关联到整个聊天过程中
/**
@ -242,7 +243,7 @@ class AzureAsrHelper(private val context: Context) {
// 检查网络状态
if (!checkNetworkStatus()) {
Log.e(tag, "网络不可用,无法开始语音识别")
continuousCallback?.onError(1000,"网络连接不可用,请检查网络设置")
continuousCallback?.onError(currsessionid,1000,"网络连接不可用,请检查网络设置")
return false
}
@ -258,7 +259,7 @@ class AzureAsrHelper(private val context: Context) {
Log.d(tag, "前台服务已启动")
} catch (e: Exception) {
Log.e(tag, "启动前台服务失败: ${e.message}")
continuousCallback?.onError(1003, "启动前台服务失败: ${e.message}")
continuousCallback?.onError(currsessionid,1003, "启动前台服务失败: ${e.message}")
return false
}
}
@ -268,7 +269,7 @@ class AzureAsrHelper(private val context: Context) {
val callback = continuousCallback
if (callback == null) {
Log.e(tag, "无法重新启动连续识别:回调为空")
continuousCallback?.onError(1001,"回调为空")
continuousCallback?.onError(currsessionid,1001,"回调为空")
return false
}
setupEventListeners(callback)
@ -308,7 +309,7 @@ class AzureAsrHelper(private val context: Context) {
fun setupEventListeners(callback: ContinuousRecognizeCallback): Boolean {
Log.d(tag, "设置ssssss监听器:${speechConfig ?: "null"} ")
if (speechConfig == null) {
callback.onError(1002,"语音服务未初始化")
callback.onError(currsessionid,1002,"语音服务未初始化")
return false
}
@ -330,7 +331,7 @@ class AzureAsrHelper(private val context: Context) {
} else {
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 {
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> { _, _ ->
// 直接在当前线程调用回调
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, "会话结束事件")
if (audioSourceType == AudioSourceType.EXTERNAL) {
callback.onSessionStopped()
callback.onSessionStopped(currsessionid)
audioStream?.isContinuousRecognitionActive = false
}
}
@ -378,11 +380,11 @@ class AzureAsrHelper(private val context: Context) {
val reason = event.reason.toString()
Log.d(tag, "识别被取消: reason=$reason, details=$errorDetails")
callback.onCanceled(reason,errorDetails)
callback.onCanceled(currsessionid,reason,errorDetails)
}
)
} catch (e: Exception) {
callback.onError(1001,"启动连续识别失败: ${e.message}")
callback.onError(currsessionid,1001,"启动连续识别失败: ${e.message}")
return false
}
return true
@ -495,7 +497,7 @@ class AzureAsrHelper(private val context: Context) {
// 确保状态被重置
audioStream.isContinuousRecognitionActive = false
currsessionid = ""
} catch (e: Exception) {
// 确保状态被重置
audioStream.isContinuousRecognitionActive = false
@ -503,6 +505,7 @@ class AzureAsrHelper(private val context: Context) {
recognizer = null
speechConfig = null
isForegroundServiceRunning = false
currsessionid = ""
}
}
@ -689,7 +692,7 @@ class AzureAsrHelper(private val context: Context) {
* @param text 识别的文本
* @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 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 errorDetails 错误详情
*/
fun onCanceled(reason: String, errorDetails: String)
fun onCanceled(sessiond:String,reason: String, errorDetails: String)
/**
* 识别出错时调用
*
* @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
isNetworkRecovering = true
} else {
continuousCallback?.onError(1000,"网络连接不可用,请检查网络设置")
continuousCallback?.onError(currsessionid,1000,"网络连接不可用,请检查网络设置")
Log.d(tag,"网络仍未恢复,继续检测")
networkLostCount = 0
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 pendingStopRequest = false
private var currsessionId = ""
// 配置参数
@ -197,7 +198,7 @@ public class AzureAsrHelper: NSObject {
self.lastNetworkLostTime = 0
self.isNetworkRecovering = true
} else {
self.continuousCallback?.onError(1000, "网络连接不可用,请检查网络设置")
self.continuousCallback?.onError(sessionid: currsessionId,1000, "网络连接不可用,请检查网络设置")
os_log("网络仍未恢复,继续检测", log: self.log, type: .debug)
self.networkLostCount = 0
self.stopContinuousRecognition()
@ -780,7 +781,7 @@ public class AzureAsrHelper: NSObject {
print("正在识别事件=检测到语言: \(detectedLanguage), 识别中: \(text)")
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 {
callback.onResult(result.text ?? "", detectedLanguage)
callback.onResult(sessionid: self.currsessionId,result.text ?? "", detectedLanguage)
}
}
// 会话开始事件
recognizer.addSessionStartedEventHandler { (sender, event) in
// 直接在当前线程调用回调
callback.onSessionStarted()
self.currsessionId = UUID().uuidString
callback.onSessionStarted(sessionid: self.currsessionId)
print("会话开始事件:")
}
@ -815,7 +817,7 @@ public class AzureAsrHelper: NSObject {
guard let self = self else { return }
print("会话结束事件:")
// 直接在当前线程调用回调
callback.onSessionStopped()
callback.onSessionStopped(sessionid: self.currsessionId)
self.audioStream?.isContinuousRecognitionActive = false
//self.stopAudioProcessing()
}
@ -830,7 +832,7 @@ public class AzureAsrHelper: NSObject {
os_log("识别取消: %{public}@", log: self.log, type: .error, errorDetails)
callback.onCanceled(reason, errorDetails)
callback.onCanceled(sessionid: self.currsessionId,reason, errorDetails)
}
return true
@ -994,7 +996,7 @@ public class AzureAsrHelper: NSObject {
* @param text 识别的文本
* @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 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 errorDetails 错误详情
*/
func onCanceled(_ reason: String, _ errorDetails: String)
func onCanceled(sessionid:String ,_ reason: String, _ errorDetails: String)
/**
* 识别出错时调用
*
* @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 {
os_log("网络不可用,无法启动语音识别", log: log, type: .error)
continuousCallback?.onError(1000,"网络连接不可用,请检查网络设置")
continuousCallback?.onError(sessionid: self.currsessionId,1000,"网络连接不可用,请检查网络设置")
}
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 detectedLanguage 检测到的语言
*/
func onResult(_ text: String, _ detectedLanguage: String) {
func onResult(sessionid:String,_ text: String, _ detectedLanguage: String) {
plugin?.sendAsrEvent([
"type": "result",
"text": text,
@ -1221,7 +1221,7 @@ private class AsrCallbackWrapper: AzureAsrHelper.ContinuousRecognizeCallback {
* @param text 正在识别的文本
* @param detectedLanguage 检测到的语言
*/
func onRecognizing(_ text: String, _ detectedLanguage: String) {
func onRecognizing(sessionid:String,_ text: String, _ detectedLanguage: String) {
plugin?.sendAsrEvent([
"type": "recognizing",
"text": text,
@ -1232,7 +1232,7 @@ private class AsrCallbackWrapper: AzureAsrHelper.ContinuousRecognizeCallback {
/**
* 会话开始回调
*/
func onSessionStarted() {
func onSessionStarted(sessionid:String) {
plugin?.sendAsrEvent([
"type": "sessionStarted"
])
@ -1241,7 +1241,7 @@ private class AsrCallbackWrapper: AzureAsrHelper.ContinuousRecognizeCallback {
/**
* 会话结束回调
*/
func onSessionStopped() {
func onSessionStopped(sessionid:String) {
plugin?.sendAsrEvent([
"type": "sessionStopped"
])
@ -1252,7 +1252,7 @@ private class AsrCallbackWrapper: AzureAsrHelper.ContinuousRecognizeCallback {
* @param reason 取消原因
* @param errorDetails 错误详情
*/
func onCanceled(_ reason: String, _ errorDetails: String) {
func onCanceled(sessionid:String,_ reason: String, _ errorDetails: String) {
plugin?.sendAsrEvent([
"type": "canceled",
"reason": reason,
@ -1265,7 +1265,7 @@ private class AsrCallbackWrapper: AzureAsrHelper.ContinuousRecognizeCallback {
* @param errorCode 错误码
* @param error 错误信息
*/
func onError(_ errorCode: Int, _ error: String) {
func onError(sessionid:String,_ errorCode: Int, _ error: String) {
plugin?.sendAsrEvent([
"type": "error",
"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
// 1. 取消当前的协程任务
currentStreamJob?.cancel()
currentStreamJob?.cancel(CancellationException("New session started"))
currentStreamJob = null
// 2. 通知旧会话被中止
getSessionCallback(oldSessionId)?.onComplete(currSessionId) // 直接完成当前会话
getSessionCallback(oldSessionId)?.onComplete(oldSessionId) // 直接完成当前会话
// 3. 清理状态
currSessionId = ""
sessionCallbacks.remove(oldSessionId)
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 chatMessages = convertToChatMessages(messages)
@ -352,10 +354,10 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
if (!isInitialized || apiKey.isEmpty() || openAI == null) {
Log.e("ChatApiService", "ChatAPI服务未初始化,无法发送消息")
try {
getSessionCallback(sessionid)?.onError(sessionid,ChatApiException("ChatAPI服务未初始化"))
} catch (e: Exception) {
Log.e(TAG, "onError回调异常: ${e.message}", e)
}
getSessionCallback(sessionid)?.onError(sessionid,ChatApiException("ChatAPI服务未初始化"))
} catch (e: Exception) {
Log.e(TAG, "onError回调异常: ${e.message}", e)
}
return
}
@ -369,6 +371,8 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
try {
// 转换消息格式
val chatMessages = try {
// ✨ 第一步:上下文精简
// val reducedMessages = reduce(messages)
convertToChatMessages(messages)
} catch (e: Exception) {
Log.e("ChatApiService", "转换消息格式失败: ${e.message}", e)
@ -425,12 +429,10 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
}
var replyContext = "";
// 为每个会话新增一个状态标记:是否为中间工具调用(默认false)
var isIntermediateToolCall = false
// Log.d(TAG, "liwei-------------------------开始AI 对话 7")
chatsFlow.collect { result ->
// val usage = result.usage
// if (usage != null){ //发现流式不返回这歌数据
// getSessionCallback(sessionid)?.onUsage(sessionid,usage.promptTokens,usage.completionTokens,usage.totalTokens)
// }
// 每次处理前检查是否仍是当前会话
if (sessionid != currSessionId) {
// 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")
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 ->
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")
// 收集工具调用信息
delta.toolCalls?.forEach { toolCall ->
@ -577,113 +601,131 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
"arguments" to firstToolCall.arguments,
"id" to firstToolCall.id
)
// 通知上层工具调用事件
try {
getSessionCallback(sessionid)?.onFunctionCall(sessionid,convertMapToJsonObject(functionCall))
} catch (e: Exception) {
Log.e(TAG, "onFunctionCall回调异常: ${e.message}", e)
}
// 在当前协程作用域内处理工具调用,使用async确保生命周期管理
val toolCallDeferred = async {
try {
if (sessionid == currSessionId) {
// 通过MCP客户端处理工具调用
val functionName = firstToolCall.name
val argumentsJson = firstToolCall.arguments
var isError = false
var errorText = ""
val result = if (_mcpClient?.hasToolWithName(functionName) == true) {
// 解析参数,如果参数为空JSON对象,使用空Map
val arguments = if (argumentsJson == "{}") {
emptyMap()
} else {
_mcpClient?.parseJsonArguments(argumentsJson) ?: emptyMap()
}
// 调用MCP工具
val toolResult = try {
withTimeout(60000) { // 60秒超时
_mcpClient?.callTool(functionName, arguments)
// 检查会话是否仍然有效
if (sessionid != currSessionId) {
Log.d(TAG, "[Session: $sessionid] 会话已被替换,跳过工具调用")
return@async
}
// 通过MCP客户端处理工具调用
val functionName = firstToolCall.name
val argumentsJson = firstToolCall.arguments
var isError = false
var errorText = ""
val result = if (_mcpClient?.hasToolWithName(functionName) == true) {
// 解析参数,如果参数为空JSON对象,使用空Map
val arguments = if (argumentsJson == "{}") {
emptyMap()
} else {
_mcpClient?.parseJsonArguments(argumentsJson) ?: emptyMap()
}
// 调用MCP工具
val toolResult = try {
withTimeout(60000) { // 60秒超时
// 再次检查会话状态
if (sessionid != currSessionId) {
throw CancellationException("Session cancelled")
}
} catch (e: TimeoutCancellationException) {
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
)
_mcpClient?.callTool(functionName, arguments)
}
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 {
// MCP工具结果
val content = toolResult["content"] as? List<*>
val firstContent = content?.firstOrNull() as? Map<*, *>
val text = firstContent?.get("text") as? String ?: ""
mapOf("context" to text)
}
} catch (e: TimeoutCancellationException) {
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: CancellationException) {
Log.d("ChatApiService", "[Session: $sessionid] MCP工具调用被取消: $functionName")
// 不抛出异常,直接返回取消结果
mapOf(
"content" to listOf(mapOf(
"type" to "text",
"text" to "Tool call cancelled"
)),
"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")
// 处理结果
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 {
Log.w("ChatApiService", "[Session: $sessionid] MCP工具调用返回null")
mapOf("context" to "Tool call failed")
// MCP工具结果
val content = toolResult["content"] as? List<*>
val firstContent = content?.firstOrNull() as? Map<*, *>
val text = firstContent?.get("text") as? String ?: ""
mapOf("context" to text)
}
} else {
// 工具不存在
mapOf("context" to "Tool not found: $functionName")
Log.w("ChatApiService", "[Session: $sessionid] MCP工具调用返回null")
mapOf("context" to "Tool call failed")
}
if (sessionid == currSessionId) {
// if (functionName != "set_user_profile_field"){
// 处理结果
try {
if(!isError){
getSessionCallback(sessionid)?.onFunctionCallResult(
sessionid,
convertMapToJsonObject(functionCall),
convertMapToJsonObject(result)
)
}else{
getSessionCallback(sessionid)?.onError(sessionid, ChatApiException(errorText))
}
} catch (e: Exception) {
Log.e(TAG, "onFunctionCallResult回调异常: ${e.message}", e)
}
// 将结果发送回OpenAI继续对话
sendFunctionCallResultInternal(
sessionid= sessionid,
messages = currentMessages,
functionCall = functionCall,
functionResult = gson.toJson(result)
} else {
// 工具不存在
mapOf("context" to "Tool not found: $functionName")
}
// 最后检查会话状态
if (sessionid == currSessionId) {
try {
if(!isError){
getSessionCallback(sessionid)?.onFunctionCallResult(
sessionid,
convertMapToJsonObject(functionCall),
convertMapToJsonObject(result)
)
// }else{
// try {
// getSessionCallback(sessionid)?.onComplete(sessionid)
// } catch (ex: Exception) {
// Log.e(TAG, "onComplete回调异常: ${ex.message}", ex)
// }
// clearCurrentSession()
// }
}else{
getSessionCallback(sessionid)?.onError(sessionid, ChatApiException(errorText))
}
} catch (e: Exception) {
Log.e(TAG, "onFunctionCallResult回调异常: ${e.message}", e)
}
// 将结果发送回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) {
Log.e("ChatApiService", "工具调用处理过程中出错: ${e.message}", e)
if (sessionid == currSessionId) {
@ -702,9 +744,10 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
try {
toolCallDeferred.await()
} catch (e: CancellationException) {
Log.d(TAG, "[Session: $sessionid] 工具调用被取消")
// 协程被取消,确保子任务也被取消
toolCallDeferred.cancel()
throw e
// 不重新抛出异常,让流程正常结束
}
return true
@ -1045,7 +1088,7 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
}
return false
}
/**
* 转换为OpenAI ChatMessage格式
*/

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

@ -30,9 +30,10 @@ class CustomSseClientTransport(
) : AbstractTransport() {
private val TAG = "CustomSseClientTransport"
private val scope by lazy {
CoroutineScope(session.coroutineContext + SupervisorJob())
}
private val transportScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
// ✅ 为每个会话创建独立的作用域
private var sessionScope: CoroutineScope? = null
private val initialized = AtomicBoolean(false)
private var session: ClientSSESession by Delegates.notNull()
@ -98,7 +99,7 @@ class CustomSseClientTransport(
* 收集SSE事件
*/
private suspend fun collectEvents() {
job = scope.launch(CoroutineName("CustomSseMcpClientTransport.collect#${hashCode()}")) {
job = sessionScope?.launch(CoroutineName("CustomSseMcpClientTransport.collect#${hashCode()}")) {
try {
session.incoming.collect { event ->
Log.e(TAG, "$serviceidString event:${event.event}-${event.data}")
@ -238,7 +239,7 @@ class CustomSseClientTransport(
* 启动连接监控,定期检查连接状态
*/
private fun startConnectionMonitor() {
connectionMonitorJob = scope.launch {
connectionMonitorJob = transportScope.launch {
while (isActive && isConnected.get()) {
try {
delay(10000) // 每10秒检查一次
@ -278,6 +279,8 @@ class CustomSseClientTransport(
queryParams = urlInfo.third
}
sessionScope = CoroutineScope(transportScope.coroutineContext + SupervisorJob())
// 创建SSE会话 - 直接使用原始URL
session = urlString?.let {
val sseConnectUrl = if (queryParams.isNotEmpty()) {
@ -381,7 +384,8 @@ class CustomSseClientTransport(
_onClose()
job?.cancelAndJoin()
connectionMonitorJob?.cancelAndJoin()
sessionScope?.cancel()
transportScope.cancel()
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 sender 发送者("user" 或 "assistant")
* @param metadata 元数据(可选)
* @return 成功返回消息ID,失败返回-1
*/
fun saveMessage(
agentId: String,
sessionId: String,
message: String,
sender: String,
@ -42,6 +43,7 @@ class ChatStorageHelper(context: Context) {
// 执行插入操作
val values = android.content.ContentValues().apply {
put(ChatDatabaseHelper.COLUMN_AGENT_ID, agentId)
put(ChatDatabaseHelper.COLUMN_SESSION_ID, sessionId)
put(ChatDatabaseHelper.COLUMN_MESSAGE, message)
put(ChatDatabaseHelper.COLUMN_SENDER, sender)
@ -59,12 +61,12 @@ class ChatStorageHelper(context: Context) {
/**
* 获取指定会话的消息
*
* @param sessionId 会话ID
* @param agentId 会话ID
* @param page 页码,从1开始
* @param pageSize 每页大小
* @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 {
val db = dbHelper.readableDatabase
val offset = (page - 1) * pageSize
@ -72,19 +74,19 @@ class ChatStorageHelper(context: Context) {
// 构建查询语句
val query = """
SELECT * FROM ${ChatDatabaseHelper.TABLE_MESSAGES}
WHERE ${ChatDatabaseHelper.COLUMN_SESSION_ID} = ?
WHERE ${ChatDatabaseHelper.COLUMN_AGENT_ID} = ?
ORDER BY ${ChatDatabaseHelper.COLUMN_TIMESTAMP} DESC
LIMIT ? OFFSET ?
""".trimIndent()
val selectionArgs = arrayOf(sessionId, pageSize.toString(), offset.toString())
val selectionArgs = arrayOf(agentId, pageSize.toString(), offset.toString())
val cursor = db.rawQuery(query, selectionArgs)
val messages = cursorToJsonArray(cursor)
// 获取总记录数
val countQuery = "SELECT COUNT(*) FROM ${ChatDatabaseHelper.TABLE_MESSAGES} WHERE ${ChatDatabaseHelper.COLUMN_SESSION_ID} = ?"
val countCursor = db.rawQuery(countQuery, arrayOf(sessionId))
val countQuery = "SELECT COUNT(*) FROM ${ChatDatabaseHelper.TABLE_MESSAGES} WHERE ${ChatDatabaseHelper.COLUMN_AGENT_ID} = ?"
val countCursor = db.rawQuery(countQuery, arrayOf(agentId))
var totalCount = 0
if (countCursor.moveToFirst()) {
totalCount = countCursor.getInt(0)
@ -125,24 +127,26 @@ class ChatStorageHelper(context: Context) {
// 查询不同的会话ID及其最新消息
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_SENDER},
s1.${ChatDatabaseHelper.COLUMN_TIMESTAMP}
FROM ${ChatDatabaseHelper.TABLE_MESSAGES} s1
INNER JOIN (
SELECT ${ChatDatabaseHelper.COLUMN_SESSION_ID},
SELECT ${ChatDatabaseHelper.COLUMN_AGENT_ID},
MAX(${ChatDatabaseHelper.COLUMN_TIMESTAMP}) as max_timestamp
FROM ${ChatDatabaseHelper.TABLE_MESSAGES}
GROUP BY ${ChatDatabaseHelper.COLUMN_SESSION_ID}
) s2 ON s1.${ChatDatabaseHelper.COLUMN_SESSION_ID} = s2.${ChatDatabaseHelper.COLUMN_SESSION_ID}
GROUP BY ${ChatDatabaseHelper.COLUMN_AGENT_ID}
) s2 ON s1.${ChatDatabaseHelper.COLUMN_AGENT_ID} = s2.${ChatDatabaseHelper.COLUMN_AGENT_ID}
AND s1.${ChatDatabaseHelper.COLUMN_TIMESTAMP} = s2.max_timestamp
ORDER BY s1.${ChatDatabaseHelper.COLUMN_TIMESTAMP} DESC
""".trimIndent()
val cursor = db.rawQuery(query, null)
val sessions = JSONArray()
val agentIdIndex = cursor.getColumnIndex(ChatDatabaseHelper.COLUMN_AGENT_ID)
val sessionIdIndex = cursor.getColumnIndex(ChatDatabaseHelper.COLUMN_SESSION_ID)
val messageIndex = cursor.getColumnIndex(ChatDatabaseHelper.COLUMN_MESSAGE)
val senderIndex = cursor.getColumnIndex(ChatDatabaseHelper.COLUMN_SENDER)
@ -150,13 +154,14 @@ class ChatStorageHelper(context: Context) {
while (cursor.moveToNext()) {
val session = JSONObject().apply {
put("agentId", cursor.getString(agentIdIndex))
put("sessionId", cursor.getString(sessionIdIndex))
put("lastMessage", cursor.getString(messageIndex))
put("lastSender", cursor.getString(senderIndex))
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)))
if (countCursor.moveToFirst()) {
put("messageCount", countCursor.getInt(0))
@ -181,7 +186,7 @@ class ChatStorageHelper(context: Context) {
* @param messageIds 消息ID列表(可选,如果提供则删除指定消息)
* @return 成功返回true,失败返回false
*/
fun deleteMessages(sessionId: String? = null, messageIds: List<Int>? = null): Boolean {
fun deleteMessages(agentId: String? = null, messageIds: List<Int>? = null): Boolean {
try {
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)"
db.execSQL(deleteQuery)
return true
} else if (!sessionId.isNullOrEmpty()) {
} else if (!agentId.isNullOrEmpty()) {
// 删除会话中的所有消息
val deleteQuery = "DELETE FROM ${ChatDatabaseHelper.TABLE_MESSAGES} WHERE ${ChatDatabaseHelper.COLUMN_SESSION_ID} = ?"
db.execSQL(deleteQuery, arrayOf(sessionId))
val deleteQuery = "DELETE FROM ${ChatDatabaseHelper.TABLE_MESSAGES} WHERE ${ChatDatabaseHelper.COLUMN_AGENT_ID} = ?"
db.execSQL(deleteQuery, arrayOf(agentId))
return true
}
return false
@ -223,11 +228,11 @@ class ChatStorageHelper(context: Context) {
/**
* 查询指定会话的最近N条消息
*
* @param sessionId 会话ID
* @param agentId 会话ID
* @param limit 限制数量
* @return 消息列表,按时间正序排列
*/
fun getRecentMessages(sessionId: String, limit: Int): List<Map<String, Any>> {
fun getRecentMessages(agentId: String, limit: Int): List<Map<String, Any>> {
try {
val db = dbHelper.readableDatabase
@ -235,13 +240,13 @@ class ChatStorageHelper(context: Context) {
val query = """
SELECT * FROM (
SELECT * FROM ${ChatDatabaseHelper.TABLE_MESSAGES}
WHERE ${ChatDatabaseHelper.COLUMN_SESSION_ID} = ?
WHERE ${ChatDatabaseHelper.COLUMN_AGENT_ID} = ?
ORDER BY ${ChatDatabaseHelper.COLUMN_TIMESTAMP} DESC
LIMIT ?
) tmp ORDER BY ${ChatDatabaseHelper.COLUMN_TIMESTAMP} ASC
""".trimIndent()
val selectionArgs = arrayOf(sessionId, limit.toString())
val selectionArgs = arrayOf(agentId, limit.toString())
val cursor = db.rawQuery(query, selectionArgs)
val messages = ArrayList<Map<String, Any>>()
@ -282,6 +287,7 @@ class ChatStorageHelper(context: Context) {
val jsonArray = JSONArray()
val idIndex = cursor.getColumnIndex(ChatDatabaseHelper.COLUMN_ID)
val agentIdIndex = cursor.getColumnIndex(ChatDatabaseHelper.COLUMN_AGENT_ID)
val sessionIdIndex = cursor.getColumnIndex(ChatDatabaseHelper.COLUMN_SESSION_ID)
val messageIndex = cursor.getColumnIndex(ChatDatabaseHelper.COLUMN_MESSAGE)
val senderIndex = cursor.getColumnIndex(ChatDatabaseHelper.COLUMN_SENDER)
@ -291,6 +297,7 @@ class ChatStorageHelper(context: Context) {
while (cursor.moveToNext()) {
val jsonObject = JSONObject().apply {
put("id", cursor.getInt(idIndex))
put("agentId", cursor.getString(agentIdIndex))
put("sessionId", cursor.getString(sessionIdIndex))
put("message", cursor.getString(messageIndex))
put("sender", cursor.getString(senderIndex))
@ -328,6 +335,7 @@ class ChatDatabaseHelper(context: Context) :
// 定义消息表
const val TABLE_MESSAGES = "messages"
const val COLUMN_ID = "id"
const val COLUMN_AGENT_ID = "agent_id"
const val COLUMN_SESSION_ID = "session_id"
const val COLUMN_MESSAGE = "message"
const val COLUMN_SENDER = "sender"
@ -340,6 +348,7 @@ class ChatDatabaseHelper(context: Context) :
val createMessagesTable = """
CREATE TABLE $TABLE_MESSAGES (
$COLUMN_ID INTEGER PRIMARY KEY AUTOINCREMENT,
$COLUMN_AGENT_ID TEXT NOT NULL,
$COLUMN_SESSION_ID TEXT NOT NULL,
$COLUMN_MESSAGE TEXT NOT NULL,
$COLUMN_SENDER TEXT NOT NULL,
@ -351,6 +360,7 @@ class ChatDatabaseHelper(context: Context) :
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_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) {
"saveMessage" -> {
try {
val agentId = call.argument<String>("agentId") ?: ""
val sessionId = call.argument<String>("sessionId") ?: ""
val message = call.argument<String>("message") ?: ""
val sender = call.argument<String>("sender") ?: ""
val metadata = call.argument<String>("metadata")
val id = chatStorageHelper.saveMessage(
agentId = agentId,
sessionId = sessionId,
message = message,
sender = sender,
@ -55,12 +57,12 @@ class ChatStoragePlugin: FlutterPlugin, MethodCallHandler {
}
"getMessages" -> {
try {
val sessionId = call.argument<String>("sessionId") ?: ""
val agentId = call.argument<String>("agentId") ?: ""
val page = call.argument<Int>("page") ?: 1
val pageSize = call.argument<Int>("pageSize") ?: 20
val messagesJson = chatStorageHelper.getMessages(
sessionId = sessionId,
agentId = agentId,
page = page,
pageSize = pageSize
)
@ -82,11 +84,11 @@ class ChatStoragePlugin: FlutterPlugin, MethodCallHandler {
}
"deleteMessages" -> {
try {
val sessionId = call.argument<String>("sessionId")
val agentId = call.argument<String>("agentId")
val messageIds = call.argument<List<Int>>("messageIds")
val success = chatStorageHelper.deleteMessages(
sessionId = sessionId,
agentId = agentId,
messageIds = messageIds
)
@ -111,11 +113,11 @@ class ChatStoragePlugin: FlutterPlugin, MethodCallHandler {
}
"getRecentMessages" -> {
try {
val sessionId = call.argument<String>("sessionId") ?: ""
val agentId = call.argument<String>("agentId") ?: ""
val limit = call.argument<Int>("limit") ?: 10
val messages = chatStorageHelper.getRecentMessages(
sessionId = sessionId,
agentId = agentId,
limit = limit
)

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

@ -34,12 +34,14 @@ public class ChatStorageHelper {
let createTableString = """
CREATE TABLE IF NOT EXISTS messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
agent_id TEXT NOT NULL,
session_id TEXT NOT NULL,
message TEXT NOT NULL,
sender TEXT NOT NULL,
metadata TEXT,
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_created_at ON messages (created_at);
"""
@ -64,11 +66,12 @@ public class ChatStorageHelper {
* @param metadata 可选的元数据
* @return 插入的消息ID,失败则返回-1
*/
public func saveMessage(sessionId: String, message: String, sender: String, metadata: String?) -> Int64 {
let insertStatementString = "INSERT INTO messages (session_id, message, sender, metadata, created_at) VALUES (?, ?, ?, ?, ?);"
public func saveMessage(agentId:String,sessionId: String, message: String, sender: String, metadata: String?) -> Int64 {
let insertStatementString = "INSERT INTO messages (agent_id,session_id, message, sender, metadata, created_at) VALUES (?, ?, ?, ?, ?, ?);"
var insertStatement: OpaquePointer?
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, 2, (message 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 每页消息数量
* @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
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 totalCount = 0
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 {
totalCount = Int(sqlite3_column_int(countStatement, 0))
}
@ -130,7 +133,7 @@ public class ChatStorageHelper {
let queryString = """
SELECT id, message, sender, metadata, created_at
FROM messages
WHERE session_id = ?
WHERE agent_id = ?
ORDER BY created_at DESC
LIMIT ? OFFSET ?
"""
@ -138,7 +141,7 @@ public class ChatStorageHelper {
var queryStatement: OpaquePointer?
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, 3, Int32(offset))
@ -163,7 +166,7 @@ public class ChatStorageHelper {
var messageDict: [String: Any] = [
"id": id,
"sessionId": sessionId, // 添加sessionId字段
"agentId": agentId, // 添加agentId字段
"message": message,
"sender": sender,
"timestamp": timestamp // 使用timestamp而不是created_at
@ -227,13 +230,13 @@ public class ChatStorageHelper {
* @param messageIds 可选的消息ID列表,指定则删除特定消息
* @return 是否删除成功
*/
public func deleteMessages(sessionId: String?, messageIds: [Int]?) -> Bool {
if let sessionId = sessionId {
let deleteString = "DELETE FROM messages WHERE session_id = ?;"
public func deleteMessages(agentId: String?, messageIds: [Int]?) -> Bool {
if let agentId = agentId {
let deleteString = "DELETE FROM messages WHERE agent_id = ?;"
var deleteStatement: OpaquePointer?
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 {
sqlite3_finalize(deleteStatement)
@ -302,7 +305,7 @@ public class ChatStorageHelper {
* @param limit 限制数量
* @return 消息列表,按时间正序排列
*/
public func getRecentMessages(sessionId: String, limit: Int) -> [[String: Any]] {
public func getRecentMessages(agentId: String, limit: Int) -> [[String: Any]] {
var messages: [[String: Any]] = []
// 首先检查数据库中是否有该会话的消息
@ -310,7 +313,7 @@ public class ChatStorageHelper {
var countStatement: OpaquePointer?
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 {
_ = Int(sqlite3_column_int(countStatement, 0))
}
@ -331,7 +334,7 @@ public class ChatStorageHelper {
var queryStatement: OpaquePointer?
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))
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":
do {
guard let args = call.arguments as? [String: Any],
let agentId = args["agentId"] as? String,
let sessionId = args["sessionId"] as? String,
let message = args["message"] as? String,
let sender = args["sender"] as? String else {
@ -30,6 +31,7 @@ public class SwiftChatStoragePlugin: NSObject, FlutterPlugin {
let metadata = args["metadata"] as? String
let id = ChatStorageHelper.shared.saveMessage(
agentId:agentId,
sessionId: sessionId,
message: message,
sender: sender,
@ -49,7 +51,7 @@ public class SwiftChatStoragePlugin: NSObject, FlutterPlugin {
case "getMessages":
do {
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))
return
}
@ -58,7 +60,7 @@ public class SwiftChatStoragePlugin: NSObject, FlutterPlugin {
let pageSize = args["pageSize"] as? Int ?? 20
let messagesJson = ChatStorageHelper.shared.getMessages(
sessionId: sessionId,
agentId: agentId,
page: page,
pageSize: pageSize
)
@ -77,11 +79,11 @@ public class SwiftChatStoragePlugin: NSObject, FlutterPlugin {
return
}
let sessionId = args["sessionId"] as? String
let agentId = args["agentId"] as? String
let messageIds = args["messageIds"] as? [Int]
let success = ChatStorageHelper.shared.deleteMessages(
sessionId: sessionId,
agentId: agentId,
messageIds: messageIds
)
@ -109,4 +111,4 @@ public class SwiftChatStoragePlugin: NSObject, FlutterPlugin {
result(FlutterMethodNotImplemented)
}
}
}
}

24
local_plugins/chat_storage/lib/chat_storage.dart

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

Loading…
Cancel
Save