wolfplus 2 years ago
parent
commit
d23056bb94
  1. 14
      .cursorrules
  2. 3
      android/app/build.gradle.kts
  3. 6
      android/settings.gradle.kts
  4. 2
      lib/core/widgets/message_bubble.dart
  5. 105
      lib/data/services/chat_history_service.dart
  6. 99
      lib/data/services/open_ai_service_adapter.dart
  7. 15
      lib/data/services/voice_interaction_impl/android_voice_interaction_service.dart
  8. 8
      lib/data/services/voice_interaction_impl/ios_voice_interaction_service.dart
  9. 13
      lib/data/services/voice_interaction_service.dart
  10. 300
      lib/modules/chat/controllers/chat_controller.dart
  11. 20
      lib/modules/chat/models/message_model.dart
  12. 2
      lib/modules/chat/views/chat_view.dart
  13. 2
      lib/modules/splash/controllers/splash_controller.dart
  14. 0
      local_plugins/azure_speech/LICENSE
  15. 0
      local_plugins/azure_speech/ios/Classes/.gitkeep
  16. 24
      local_plugins/azure_speech/ios/azure_speech.podspec
  17. 0
      local_plugins/chat_storage/LICENSE
  18. 12
      local_plugins/chat_storage/README.md
  19. 2
      local_plugins/chat_storage/android/build.gradle.kts
  20. 1
      local_plugins/chat_storage/android/settings.gradle.kts
  21. 2
      local_plugins/chat_storage/android/src/main/AndroidManifest.xml
  22. 6
      local_plugins/chat_storage/android/src/main/kotlin/com/yunqiinnovation/chat_storage/ChatStoragePlugin.kt
  23. 0
      local_plugins/chat_storage/ios/Classes/.gitkeep
  24. 23
      local_plugins/chat_storage/ios/chat_storage.podspec
  25. 5
      local_plugins/chat_storage/lib/chat_storage.dart
  26. 12
      local_plugins/chat_storage/pubspec.yaml
  27. 1
      local_plugins/chat_storage_sqlite/android/settings.gradle.kts
  28. 2
      local_plugins/jieli/android/src/main/kotlin/com/yunqiinnovation/jieli/BleService.kt
  29. 0
      local_plugins/open_ai_service/LICENSE
  30. 141
      local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/OpenAIService.kt
  31. 169
      local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/OpenAIServicePlugin.kt
  32. 4
      local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/SystemFunctionHandler.kt
  33. 0
      local_plugins/open_ai_service/ios/Classes/.gitkeep
  34. 296
      local_plugins/open_ai_service/ios/Classes/OpenAIService.swift
  35. 23
      local_plugins/open_ai_service/ios/open_ai_service.podspec
  36. 0
      local_plugins/volcano_speech/LICENSE
  37. 0
      local_plugins/volcano_speech/ios/Classes/.gitkeep
  38. 23
      local_plugins/volcano_speech/ios/volcano_speech.podspec
  39. 4
      pubspec.yaml
  40. 204
      test.json

14
.cursorrules

@ -1,10 +1,10 @@
1. 不要主动运行程序, 如flutter run 1. 请严格遵循 Effective Dart 的风格指南来编写代码
2. 不要改动android, iOS项目的配置信息,包括SDK, NDK版本等 2. 不要主动运行程序, 如flutter run
3. 不要使用print, 而是使用logger 3. 不要改动android, iOS项目的配置信息,包括SDK, NDK, JAVA版本等
4. 尽可能使用GetX提供的各种功能, 而不是使用原生的StatefulWidget 4. 不要使用print, 而是使用logger, 尽可能避免打印日志,仅在出错时记录日志
5. 不要增加重试逻辑,出现失败直接返回错误 5. 尽可能使用GetX提供的各种功能, 而不是使用原生的StatefulWidget
6. 尽可能避免使用try catch 6. 不要增加重试逻辑,出现失败直接返回错误
7. 尽可能避免打印日志,仅在出错时记录日志 7. 尽可能避免使用try catch
8. 请使用较小的代码量改动,最安全的方法进行代码编写和优化 8. 请使用较小的代码量改动,最安全的方法进行代码编写和优化
9. 请尽可能使用中文回复问题 9. 请尽可能使用中文回复问题
10. 重要:不要过度设计!! 10. 重要:不要过度设计!!

3
android/app/build.gradle.kts

@ -108,7 +108,8 @@ dependencies {
implementation(project(":azure_speech")) implementation(project(":azure_speech"))
implementation(project(":open_ai_service")) implementation(project(":open_ai_service"))
implementation(project(":volcano_speech")) implementation(project(":volcano_speech"))
implementation(project(":chat_storage_sqlite")) // 添加新创建的SQLite聊天存储插件 implementation(project(":chat_storage")) // 添加新创建的SQLite聊天存储插件
implementation(project(":jieli")) // 添加新创建的SQLite聊天存储插件
// 添加OkHttp依赖 // 添加OkHttp依赖
implementation("com.squareup.okhttp3:okhttp:4.9.3") implementation("com.squareup.okhttp3:okhttp:4.9.3")

6
android/settings.gradle.kts

@ -34,10 +34,12 @@ include(":app")
include(":azure_speech") include(":azure_speech")
include(":open_ai_service") include(":open_ai_service")
include(":volcano_speech") include(":volcano_speech")
include(":chat_storage_sqlite") include(":chat_storage")
include(":jieli")
// 设置azure_speech项目的路径 // 设置azure_speech项目的路径
project(":azure_speech").projectDir = file("../local_plugins/azure_speech/android") project(":azure_speech").projectDir = file("../local_plugins/azure_speech/android")
project(":open_ai_service").projectDir = file("../local_plugins/open_ai_service/android") project(":open_ai_service").projectDir = file("../local_plugins/open_ai_service/android")
project(":volcano_speech").projectDir = file("../local_plugins/volcano_speech/android") project(":volcano_speech").projectDir = file("../local_plugins/volcano_speech/android")
project(":chat_storage_sqlite").projectDir = file("../local_plugins/chat_storage_sqlite/android") project(":chat_storage").projectDir = file("../local_plugins/chat_storage/android")
project(":jieli").projectDir = file("../local_plugins/jieli/android")

2
lib/core/widgets/message_bubble.dart

@ -52,7 +52,7 @@ class MessageBubble extends StatelessWidget {
), ),
// 加载指示器 // 加载指示器
if (message.isLoading) if (message.isIntermediate)
Padding( Padding(
padding: EdgeInsets.only(top: message.content.isNotEmpty ? 8.h : 0), padding: EdgeInsets.only(top: message.content.isNotEmpty ? 8.h : 0),
child: _buildLoadingIndicator(), child: _buildLoadingIndicator(),

105
lib/data/services/chat_history_service.dart

@ -1,16 +1,13 @@
import 'dart:convert'; import 'dart:convert';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import '../../modules/chat/models/message_model.dart'; import '../../modules/chat/models/message_model.dart';
import '../../core/utils/logger.dart'; import '../../core/utils/logger.dart';
import 'package:chat_storage/chat_storage.dart';
/// 聊天记录管理服务,负责保存和加载聊天记录 /// 聊天记录管理服务,负责保存和加载聊天记录
class ChatHistoryService extends GetxService { class ChatHistoryService extends GetxService {
final GetStorage _storage = GetStorage(); // 最大历史记录数量
static const int _maxHistoryCount = 200; // 每个对话最多保存的消息数
// 存储键名
static const String _historyKeyPrefix = 'chat_history_';
static const int _maxHistoryCount = 10; // 每个对话最多保存的消息数
@override @override
void onInit() { void onInit() {
@ -18,50 +15,95 @@ class ChatHistoryService extends GetxService {
Logger.info('ChatHistoryService 已初始化'); Logger.info('ChatHistoryService 已初始化');
} }
/// 生成历史记录存储键
String _getHistoryKey(String agentId) => '$_historyKeyPrefix$agentId';
/// 保存聊天记录 /// 保存聊天记录
/// ///
/// [agentId] 代理ID /// [agentId] 代理ID
/// [messages] 消息列表 /// [messages] 消息列表
Future<void> saveHistory(String agentId, List<Message> messages) async { Future<void> saveHistory(String agentId, List<Message> messages) async {
try { try {
// 过滤掉加载中的消息 if (messages.isEmpty) return;
final filteredMessages = messages.where((m) => !m.isLoading).toList();
// 如果消息超过最大数量,只保留最新的消息 // 获取最新的消息
final messagesToSave = filteredMessages.length > _maxHistoryCount final latestMessage = messages.last;
? filteredMessages.sublist(filteredMessages.length - _maxHistoryCount)
: filteredMessages;
// 转换为JSON // 如果是加载中的消息则跳过
final jsonList = messagesToSave.map((m) => m.toJson()).toList(); if (latestMessage.isIntermediate) return;
final jsonString = jsonEncode(jsonList);
// 保存到存储 // 直接插入最新的消息到数据库
await _storage.write(_getHistoryKey(agentId), jsonString); await ChatStorage.saveMessage(
// Logger.info('已保存聊天记录,代理: $agentId,消息数: ${messagesToSave.length}'); sessionId: agentId,
message: latestMessage.content,
sender: latestMessage.role,
);
// 检查并清理过多的历史记录
await _cleanupOldMessages(agentId);
} catch (e) { } catch (e) {
Logger.error('保存聊天记录失败', e); Logger.error('保存聊天记录失败', e);
} }
} }
/// 清理旧消息,保持每个会话的消息数量不超过最大限制
Future<void> _cleanupOldMessages(String agentId) async {
try {
final pagedResult = await ChatStorage.getMessages(
sessionId: agentId,
page: 1,
pageSize: 1, // 只获取总数信息
);
// 如果消息总数超过最大限制,删除最早的消息
if (pagedResult.totalCount > _maxHistoryCount) {
final messagesToDelete = pagedResult.totalCount - _maxHistoryCount;
// 获取最早的N条消息ID
final oldestMessages = await ChatStorage.getMessages(
sessionId: agentId,
page: 1,
pageSize: messagesToDelete,
);
// 构建要删除的消息ID列表
final List<int> messageIds = oldestMessages.messages
.where((m) => m.id != null)
.map((m) => m.id!)
.toList();
// 删除这些消息
if (messageIds.isNotEmpty) {
await ChatStorage.deleteMessages(messageIds: messageIds);
}
}
} catch (e) {
Logger.error('清理旧消息失败', e);
}
}
/// 加载聊天记录 /// 加载聊天记录
/// ///
/// [agentId] 代理ID /// [agentId] 代理ID
/// 返回消息列表,如果没有找到或解析错误则返回空列表 /// 返回消息列表,如果没有找到或解析错误则返回空列表
List<Message> loadHistory(String agentId) { Future<List<Message>> loadHistory(String agentId) async {
try { try {
final jsonString = _storage.read<String>(_getHistoryKey(agentId)); final pagedResult = await ChatStorage.getMessages(
sessionId: agentId,
page: 1,
pageSize: _maxHistoryCount,
);
if (jsonString == null) { if (pagedResult.messages.isEmpty) {
return []; return [];
} }
final jsonList = jsonDecode(jsonString) as List; // 将ChatStorage.ChatMessage转换为Message
final messages = jsonList.map((json) => Message.fromJson(json)).toList(); final messages = pagedResult.messages.map((chatMessage) {
return Message(
role: chatMessage.sender,
content: chatMessage.message,
timestamp: DateTime.parse(chatMessage.timestamp), // 直接使用chatMessage的timestamp
// isLoading 默认false
// isLoading: metadata['isLoading'] ?? false,
);
}).toList();
return messages; return messages;
} catch (e) { } catch (e) {
@ -73,7 +115,7 @@ class ChatHistoryService extends GetxService {
/// 清除特定代理的聊天记录 /// 清除特定代理的聊天记录
Future<void> clearHistory(String agentId) async { Future<void> clearHistory(String agentId) async {
try { try {
await _storage.remove(_getHistoryKey(agentId)); await ChatStorage.deleteMessages(sessionId: agentId);
Logger.info('已清除聊天记录,代理: $agentId'); Logger.info('已清除聊天记录,代理: $agentId');
} catch (e) { } catch (e) {
Logger.error('清除聊天记录失败', e); Logger.error('清除聊天记录失败', e);
@ -83,12 +125,7 @@ class ChatHistoryService extends GetxService {
/// 清除所有聊天记录 /// 清除所有聊天记录
Future<void> clearAllHistory() async { Future<void> clearAllHistory() async {
try { try {
final keys = _storage.getKeys(); await ChatStorage.clearDatabase();
for (final key in keys) {
if (key.startsWith(_historyKeyPrefix)) {
await _storage.remove(key);
}
}
Logger.info('已清除所有聊天记录'); Logger.info('已清除所有聊天记录');
} catch (e) { } catch (e) {
Logger.error('清除所有聊天记录失败', e); Logger.error('清除所有聊天记录失败', e);

99
lib/data/services/open_ai_service_adapter.dart

@ -4,12 +4,22 @@ import 'package:open_ai_service/open_ai_service.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'ai_service.dart'; import 'ai_service.dart';
/// 定义流事件类型,用于区分不同类型的事件
enum StreamEventType { token, complete, error }
/// 流事件包装类
class StreamEvent {
final StreamEventType type;
final String? content;
StreamEvent(this.type, {this.content});
}
/// OpenAI服务适配器 - 连接AiService接口与OpenAIService插件 /// OpenAI服务适配器 - 连接AiService接口与OpenAIService插件
class OpenAIServiceAdapter implements AiService { class OpenAIServiceAdapter implements AiService {
final OpenAIService _openAIService = OpenAIService(); final OpenAIService _openAIService = OpenAIService();
StreamSubscription<OpenAIEvent>? _eventSubscription; StreamSubscription<OpenAIEvent>? _eventSubscription;
final StreamController<String> _tokenStreamController = StreamController<String>.broadcast(); final StreamController<StreamEvent> _tokenStreamController = StreamController<StreamEvent>.broadcast();
bool _isProcessingStream = false;
/// 构造函数 /// 构造函数
OpenAIServiceAdapter() { OpenAIServiceAdapter() {
@ -26,46 +36,51 @@ class OpenAIServiceAdapter implements AiService {
// 设置事件处理 // 设置事件处理
_eventSubscription = _openAIService.eventStream.listen( _eventSubscription = _openAIService.eventStream.listen(
(event) { (event) {
if (!_isProcessingStream) return;
try { try {
switch (event.type) { switch (event.type) {
case OpenAIEventType.token: case OpenAIEventType.token:
if (event.content is String) { if (event.content is String) {
_tokenStreamController.add(event.content as String); _tokenStreamController.add(StreamEvent(
StreamEventType.token,
content: event.content as String
));
} else { } else {
printInfo(info: '收到非字符串类型的token: ${event.content}'); printInfo(info: '收到非字符串类型的token: ${event.content}');
} }
break; break;
case OpenAIEventType.complete: case OpenAIEventType.complete:
_isProcessingStream = false; _tokenStreamController.add(StreamEvent(StreamEventType.complete));
break; break;
case OpenAIEventType.error: case OpenAIEventType.error:
_tokenStreamController.addError('未知错误: ${event.content}'); _tokenStreamController.add(StreamEvent(
StreamEventType.error,
_isProcessingStream = false; content: '未知错误: ${event.content}'
));
break; break;
case OpenAIEventType.functionCall: case OpenAIEventType.functionCall:
_tokenStreamController.addError('收到函数调用,该流仅支持文本响应'); _tokenStreamController.add(StreamEvent(
StreamEventType.error,
content: '收到函数调用,该流仅支持文本响应'
_isProcessingStream = false; ));
break; break;
} }
} catch (e) { } catch (e) {
printError(info: '处理事件出错: $e'); printError(info: '处理事件出错: $e');
_tokenStreamController.addError('处理事件失败: $e'); _tokenStreamController.add(StreamEvent(
_isProcessingStream = false; StreamEventType.error,
content: '处理事件失败: $e'
));
} }
}, },
onError: (error) { onError: (error) {
printError(info: '事件流错误: $error'); printError(info: '事件流错误: $error');
_tokenStreamController.addError('事件流错误: $error'); _tokenStreamController.add(StreamEvent(
_isProcessingStream = false; StreamEventType.error,
content: '事件流错误: $error'
));
}, },
onDone: () { onDone: () {
printInfo(info: '事件流已关闭'); printInfo(info: '事件流已关闭');
_isProcessingStream = false;
}, },
); );
} catch (e) { } catch (e) {
@ -147,20 +162,28 @@ class OpenAIServiceAdapter implements AiService {
// 创建用于接收token的控制器 // 创建用于接收token的控制器
final localController = StreamController<String>(); final localController = StreamController<String>();
// 标记开始处理流
_isProcessingStream = true;
// 添加从广播流到本地流的订阅 // 添加从广播流到本地流的订阅
final subscription = _tokenStreamController.stream.listen( final subscription = _tokenStreamController.stream.listen(
(token) => localController.add(token), (streamEvent) {
onError: (error) { switch (streamEvent.type) {
printError(info: '令牌流错误: $error'); case StreamEventType.token:
localController.addError(error); if (streamEvent.content != null && !localController.isClosed) {
localController.close(); localController.add(streamEvent.content!);
}, }
onDone: () { break;
printInfo(info: '令牌流已完成'); case StreamEventType.complete:
localController.close(); if (!localController.isClosed) {
localController.close();
}
break;
case StreamEventType.error:
if (!localController.isClosed) {
printError(info: '令牌流错误: ${streamEvent.content}');
localController.addError(streamEvent.content ?? '未知错误');
localController.close();
}
break;
}
} }
); );
@ -177,17 +200,19 @@ class OpenAIServiceAdapter implements AiService {
); );
} catch (e) { } catch (e) {
printError(info: '启动消息流失败: $e'); printError(info: '启动消息流失败: $e');
localController.addError('启动消息流失败: $e'); if (!localController.isClosed) {
localController.close(); localController.addError('启动消息流失败: $e');
_isProcessingStream = false; localController.close();
}
throw '启动消息流失败: $e'; throw '启动消息流失败: $e';
} }
if (!started) { if (!started) {
printError(info: '无法启动消息流'); printError(info: '无法启动消息流');
localController.addError('无法启动消息流'); if (!localController.isClosed) {
localController.close(); localController.addError('无法启动消息流');
_isProcessingStream = false; localController.close();
}
throw '无法启动消息流'; throw '无法启动消息流';
} }
@ -224,7 +249,6 @@ class OpenAIServiceAdapter implements AiService {
/// 释放资源 /// 释放资源
void dispose() { void dispose() {
try { try {
_isProcessingStream = false;
_eventSubscription?.cancel(); _eventSubscription?.cancel();
_tokenStreamController.close(); _tokenStreamController.close();
printInfo(info: 'OpenAIServiceAdapter资源已释放'); printInfo(info: 'OpenAIServiceAdapter资源已释放');
@ -232,5 +256,4 @@ class OpenAIServiceAdapter implements AiService {
printError(info: '释放资源时出错: $e'); printError(info: '释放资源时出错: $e');
} }
} }
} }

15
lib/data/services/voice_interaction_impl/android_voice_interaction_service.dart

@ -267,12 +267,12 @@ class AndroidVoiceInteractionService extends GetxService {
} }
/// 保存聊天记录 /// 保存聊天记录
void _saveChatHistory(String agentId, String userMessage, String assistantMessage, int timestamp) { Future<void> _saveChatHistory(String agentId, String userMessage, String assistantMessage, int timestamp) async {
try { try {
// 获取ChatHistoryService实例 if (userMessage.isEmpty) {
final chatHistoryService = Get.find<ChatHistoryService>(); return;
}
// 创建用户消息和助手消息
final userMsg = Message( final userMsg = Message(
role: 'user', role: 'user',
content: userMessage, content: userMessage,
@ -285,14 +285,17 @@ class AndroidVoiceInteractionService extends GetxService {
timestamp: DateTime.fromMillisecondsSinceEpoch(timestamp + 1), // 确保助手消息时间戳晚于用户消息 timestamp: DateTime.fromMillisecondsSinceEpoch(timestamp + 1), // 确保助手消息时间戳晚于用户消息
); );
// 获取ChatHistoryService实例
final chatHistoryService = Get.find<ChatHistoryService>();
// 加载现有历史记录 // 加载现有历史记录
final existingMessages = chatHistoryService.loadHistory(agentId); final existingMessages = await chatHistoryService.loadHistory(agentId);
// 添加新消息 // 添加新消息
existingMessages.addAll([userMsg, assistantMsg]); existingMessages.addAll([userMsg, assistantMsg]);
// 保存更新后的历史记录 // 保存更新后的历史记录
chatHistoryService.saveHistory(agentId, existingMessages); await chatHistoryService.saveHistory(agentId, existingMessages);
} catch (e) { } catch (e) {
Logger.error('保存聊天记录失败: $e'); Logger.error('保存聊天记录失败: $e');
} }

8
lib/data/services/voice_interaction_impl/ios_voice_interaction_service.dart

@ -250,7 +250,7 @@ class IosVoiceInteractionService extends GetxService {
); );
// 保存聊天历史 // 保存聊天历史
_saveChatHistory(userMsg, assistantMsg); await _saveChatHistory(userMsg, assistantMsg);
// 发送聊天历史事件 // 发送聊天历史事件
_sendChatHistoryEvent(userMsg.content, assistantMsg.content); _sendChatHistoryEvent(userMsg.content, assistantMsg.content);
@ -424,16 +424,16 @@ class IosVoiceInteractionService extends GetxService {
} }
// 保存聊天历史 // 保存聊天历史
void _saveChatHistory(Message userMsg, Message assistantMsg) { Future<void> _saveChatHistory(Message userMsg, Message assistantMsg) async {
try { try {
// 加载现有历史记录 // 加载现有历史记录
final existingMessages = _chatHistoryService.loadHistory(_agentId); final existingMessages = await _chatHistoryService.loadHistory(_agentId);
// 添加新消息 // 添加新消息
existingMessages.addAll([userMsg, assistantMsg]); existingMessages.addAll([userMsg, assistantMsg]);
// 保存更新后的历史记录 // 保存更新后的历史记录
_chatHistoryService.saveHistory(_agentId, existingMessages); await _chatHistoryService.saveHistory(_agentId, existingMessages);
Logger.info('保存聊天历史记录成功'); Logger.info('保存聊天历史记录成功');
} catch (e) { } catch (e) {

13
lib/data/services/voice_interaction_service.dart

@ -107,7 +107,7 @@ class VoiceInteractionService extends GetxService {
_eventStreamController.add(chatHistoryEvent); _eventStreamController.add(chatHistoryEvent);
// 保存聊天历史到ChatHistoryService // 保存聊天历史到ChatHistoryService
_saveChatHistory(agentId, userMessage, assistantMessage, DateTime.now().millisecondsSinceEpoch); _saveChatHistory(agentId, userMessage, assistantMessage);
break; break;
case 'enter_translation_mode': case 'enter_translation_mode':
@ -132,12 +132,9 @@ class VoiceInteractionService extends GetxService {
} }
/// 保存聊天历史 /// 保存聊天历史
void _saveChatHistory(String agentId, String userMessage, String assistantMessage, int timestamp) { Future<void> _saveChatHistory(String agentId, String userMessage, String assistantMessage) async {
try { try {
// 检查参数有效性 final timestamp = DateTime.now().millisecondsSinceEpoch;
if (userMessage.isEmpty) {
return;
}
// 创建用户消息和助手消息 // 创建用户消息和助手消息
final userMsg = Message( final userMsg = Message(
@ -153,13 +150,13 @@ class VoiceInteractionService extends GetxService {
); );
// 加载现有历史记录 // 加载现有历史记录
final existingMessages = _chatHistoryService.loadHistory(agentId); final existingMessages = await _chatHistoryService.loadHistory(agentId);
// 添加新消息 // 添加新消息
existingMessages.addAll([userMsg, assistantMsg]); existingMessages.addAll([userMsg, assistantMsg]);
// 保存更新后的历史记录 // 保存更新后的历史记录
_chatHistoryService.saveHistory(agentId, existingMessages); await _chatHistoryService.saveHistory(agentId, existingMessages);
Logger.info('已保存聊天历史: agentId=$agentId'); Logger.info('已保存聊天历史: agentId=$agentId');
} catch (e) { } catch (e) {

300
lib/modules/chat/controllers/chat_controller.dart

@ -2,18 +2,17 @@ import 'dart:async';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:get_storage/get_storage.dart'; import 'package:get_storage/get_storage.dart';
import 'package:chat_storage/chat_storage.dart';
import '../../../data/services/ai_service.dart'; import '../../../data/services/ai_service.dart';
import '../../../data/services/deapsound_ai_service.dart'; import '../../../data/services/deapsound_ai_service.dart';
import '../../../data/services/kouzi_ai_service.dart'; import '../../../data/services/kouzi_ai_service.dart';
import '../../../data/services/speech_impl/voice_clone_tts_service.dart'; import '../../../data/services/speech_impl/voice_clone_tts_service.dart';
import '../models/message_model.dart'; import '../models/message_model.dart';
import '../../../core/utils/logger.dart'; import '../../../core/utils/logger.dart';
import '../../../data/services/volcano_ai_service.dart';
import '../../../data/services/tts_service.dart'; import '../../../data/services/tts_service.dart';
import '../../../data/providers/agent_provider.dart'; import '../../../data/providers/agent_provider.dart';
import '../../../data/models/agent.dart'; import '../../../data/models/agent.dart';
import '../../../data/services/asr_service.dart'; import '../../../data/services/asr_service.dart';
import '../../../data/services/chat_history_service.dart';
import '../../../data/models/events/voice_interaction_event.dart'; import '../../../data/models/events/voice_interaction_event.dart';
import '../../../data/services/voice_interaction_service.dart'; import '../../../data/services/voice_interaction_service.dart';
import '../../../data/services/open_ai_service_adapter.dart'; import '../../../data/services/open_ai_service_adapter.dart';
@ -23,7 +22,6 @@ class ChatController extends GetxController {
late AiService _aiService; late AiService _aiService;
late TtsService _ttsService; late TtsService _ttsService;
final AsrService _asrService = Get.find<AsrService>(); final AsrService _asrService = Get.find<AsrService>();
final ChatHistoryService _historyService = Get.find<ChatHistoryService>();
final VoiceInteractionService _voiceInteractionService = final VoiceInteractionService _voiceInteractionService =
Get.find<VoiceInteractionService>(); Get.find<VoiceInteractionService>();
@ -45,6 +43,13 @@ class ChatController extends GetxController {
final RxBool isTtsEnabled = true.obs; final RxBool isTtsEnabled = true.obs;
final RxBool isVoiceRecognitionActive = false.obs; final RxBool isVoiceRecognitionActive = false.obs;
// 分页加载状态
final RxInt _currentPage = 1.obs;
final RxInt _totalPages = 1.obs;
final RxBool _isLoadingMore = false.obs;
final RxBool _hasMoreHistory = true.obs;
static const int _pageSize = 20; // 每页加载的消息数量
// 语音交互模式 // 语音交互模式
bool _isVoiceInteractionMode = false; bool _isVoiceInteractionMode = false;
@ -53,9 +58,6 @@ class ChatController extends GetxController {
final RxBool isListening = false.obs; final RxBool isListening = false.obs;
final RxBool isSpeechDetected = false.obs; final RxBool isSpeechDetected = false.obs;
// 临时消息索引,用于更新临时消息
int _tempMessageIndex = -1;
// 防抖计时器 // 防抖计时器
Timer? _scrollDebounceTimer; Timer? _scrollDebounceTimer;
@ -107,14 +109,9 @@ class ChatController extends GetxController {
break; break;
default: default:
// 默认使用OpenAIServiceAdapter // 默认使用OpenAIServiceAdapter
try {
_aiService = Get.find<OpenAIServiceAdapter>(); _aiService = Get.find<OpenAIServiceAdapter>();
Logger.info('使用OpenAIServiceAdapter'); Logger.info('使用OpenAIServiceAdapter');
} catch (e) {
// 如果找不到OpenAIServiceAdapter,则回退到VolcanoAIService
Logger.info('未找到OpenAIServiceAdapter,回退使用VolcanoAIService: $e');
_aiService = Get.find<VolcanoAIService>();
}
} }
// 使用克隆音色语音合成 // 使用克隆音色语音合成
@ -136,8 +133,11 @@ class ChatController extends GetxController {
// 设置TTS服务 // 设置TTS服务
await _ttsService.setVoice(agent.voice); await _ttsService.setVoice(agent.voice);
// 加载聊天历史 // 添加滚动监听器,用于触发加载更多
_loadChatHistory(); scrollController.addListener(_scrollListener);
// 加载聊天历史 (加载第一页)
await _loadChatHistory();
// 如果没有历史记录,添加欢迎消息 // 如果没有历史记录,添加欢迎消息
if (messages.isEmpty && agent.welcomeMessage.isNotEmpty) { if (messages.isEmpty && agent.welcomeMessage.isNotEmpty) {
@ -213,7 +213,9 @@ class ChatController extends GetxController {
messages.add(userMsg); messages.add(userMsg);
messages.add(assistantMsg); messages.add(assistantMsg);
} }
_scrollToBottom(); WidgetsBinding.instance.addPostFrameCallback((_) {
_scrollToBottom();
});
} }
/// 停止当前交互 /// 停止当前交互
@ -239,6 +241,7 @@ class ChatController extends GetxController {
Logger.info('ChatController onClose'); Logger.info('ChatController onClose');
// 清理资源 // 清理资源
scrollController.removeListener(_scrollListener); // 移除监听器
scrollController.dispose(); scrollController.dispose();
_aiResponseSubscription?.cancel(); _aiResponseSubscription?.cancel();
_recognitionSubscription?.cancel(); _recognitionSubscription?.cancel();
@ -255,7 +258,7 @@ class ChatController extends GetxController {
// 保存当前聊天记录 // 保存当前聊天记录
if (agent.id != null) { if (agent.id != null) {
_historyService.saveHistory(agent.id!, messages.toList()); // _historyService.saveHistory(agent.id!, messages.toList());
} }
super.onClose(); super.onClose();
@ -289,11 +292,11 @@ class ChatController extends GetxController {
_aiResponseSubscription!.cancel(); _aiResponseSubscription!.cancel();
_aiResponseSubscription = null; _aiResponseSubscription = null;
// 将加载中的消息转换为正常消息,保留已输出的内容 // 将AI中间消息转换为正常消息,保留已输出的内容
final index = messages.indexWhere((m) => m.isLoading); final index = messages.indexWhere((m) => m.isIntermediate && m.role == 'assistant');
if (index != -1) { if (index != -1) {
messages[index] = messages[index].copyWith( messages[index] = messages[index].copyWith(
isLoading: false, isIntermediate: false,
); );
messages.refresh(); messages.refresh();
} }
@ -323,10 +326,10 @@ class ChatController extends GetxController {
// 清空输入框 // 清空输入框
inputText.value = ''; inputText.value = '';
// 添加加载中的消息 // 添加中间状态的AI消息
final loadingMessage = Message.loading(); final intermediateMessage = Message.intermediate('assistant');
messages.add(loadingMessage); messages.add(intermediateMessage);
messages.refresh();
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
_scrollToBottom(); _scrollToBottom();
}); });
@ -355,8 +358,8 @@ class ChatController extends GetxController {
fullResponse += chunk; fullResponse += chunk;
// 更新加载中的消息内容 // 更新中间状态的消息内容
_updateLoadingMessage(fullResponse); _updateIntermediateMessage(fullResponse);
// 如果TTS启用,使用流式处理发送到TTS // 如果TTS启用,使用流式处理发送到TTS
if (isTtsEnabled.value) { if (isTtsEnabled.value) {
@ -366,21 +369,18 @@ class ChatController extends GetxController {
onDone: () { onDone: () {
if (_shouldCancelAiResponse) return; if (_shouldCancelAiResponse) return;
// 完成响应,将isLoading设为false // 完成响应,将isIntermediate设为false
_finalizeResponse(fullResponse); _finalizeResponse(fullResponse);
// 如果TTS启用,处理缓冲区中的剩余文本 // 如果TTS启用,处理缓冲区中的剩余文本
if (isTtsEnabled.value) { if (isTtsEnabled.value) {
_ttsService.flushStream(); _ttsService.flushStream();
} }
// 保存聊天历史
_saveChatHistory();
}, },
onError: (error) { onError: (error) {
Logger.error('AI响应错误', error); Logger.error('AI响应错误', error);
// 更新加载中的消息为错误消息 // 更新中间状态的消息为错误消息
_handleResponseError(); _handleResponseError();
}, },
); );
@ -396,7 +396,7 @@ class ChatController extends GetxController {
final List<Map<String, String>> messageHistory = []; final List<Map<String, String>> messageHistory = [];
// 只取最近的10条消息作为上下文 // 只取最近的10条消息作为上下文
final recentMessages = messages.where((m) => !m.isLoading).toList(); final recentMessages = messages.where((m) => !m.isIntermediate).toList();
final startIndex = final startIndex =
recentMessages.length > 10 ? recentMessages.length - 10 : 0; recentMessages.length > 10 ? recentMessages.length - 10 : 0;
@ -411,42 +411,61 @@ class ChatController extends GetxController {
return messageHistory; return messageHistory;
} }
// 更新加载中的消息 // 更新中间状态的消息
void _updateLoadingMessage(String content) { void _updateIntermediateMessage(String content) {
final index = messages.indexWhere((m) => m.isLoading); final index = messages.indexWhere((m) => m.isIntermediate && m.role == 'assistant');
if (index != -1) { if (index != -1) {
messages[index] = messages[index].copyWith( messages[index] = messages[index].copyWith(
content: content, content: content,
isLoading: true,
); );
messages.refresh(); messages.refresh();
_scrollToBottom(); _scrollToBottom();
} }
} }
// 完成响应 // 更新或添加最终响应
void _finalizeResponse(String content) { void _finalizeResponse(String content) {
final index = messages.indexWhere((m) => m.isLoading); // 查找AI的中间状态消息
final index = messages.indexWhere((m) => m.isIntermediate && m.role == 'assistant');
if (index != -1) { if (index != -1) {
// 更新中间状态消息为最终状态
messages[index] = messages[index].copyWith( messages[index] = messages[index].copyWith(
content: content, content: content,
isLoading: false, isIntermediate: false,
timestamp: DateTime.now(), // 更新时间戳为最终时间
); );
messages.refresh(); // 保存最终的助手消息到数据库
_scrollToBottom(); _saveMessageToDb(messages[index]);
} else {
// 如果没有中间状态消息,直接添加新消息 (理论上不应发生)
final assistantMsg = Message(
role: 'assistant',
content: content,
timestamp: DateTime.now(),
);
messages.add(assistantMsg);
_saveMessageToDb(assistantMsg); // 保存到数据库
} }
isLoading.value = false;
isSending.value = false; isSending.value = false;
_aiResponseSubscription = null; _aiResponseSubscription = null; // 清理订阅
_shouldCancelAiResponse = false; // 重置取消标志
// 响应完成后,重新启动语音识别的自动停止计时器
if (_isVoiceInteractionMode) {
_startAutoStopTimer();
}
} }
// 处理响应错误 // 处理响应错误
void _handleResponseError() { void _handleResponseError() {
final index = messages.indexWhere((m) => m.isLoading); // 查找AI的中间状态消息
final index = messages.indexWhere((m) => m.isIntermediate && m.role == 'assistant');
if (index != -1) { if (index != -1) {
messages[index] = messages[index].copyWith( messages[index] = messages[index].copyWith(
content: '抱歉,我遇到了一些问题,无法回应您的请求。', content: '抱歉,我遇到了一些问题,无法回应您的请求。',
isLoading: false, isIntermediate: false,
); );
messages.refresh(); messages.refresh();
} }
@ -479,9 +498,6 @@ class ChatController extends GetxController {
isListening.value = true; isListening.value = true;
recognizedText.value = ''; recognizedText.value = '';
// 重置临时消息索引
_tempMessageIndex = -1;
// 启动自动停止计时器 // 启动自动停止计时器
_startAutoStopTimer(); _startAutoStopTimer();
@ -573,24 +589,23 @@ class ChatController extends GetxController {
_interruptCurrentResponse("检测到用户开始讲话,中断当前响应"); _interruptCurrentResponse("检测到用户开始讲话,中断当前响应");
} }
// 更新或创建临时消息 // 查找或创建用户的中间状态消息
if (_tempMessageIndex >= 0 && _tempMessageIndex < messages.length) { final intermediateIndex = messages.indexWhere((m) => m.isIntermediate && m.role == 'user');
// 更新已有的临时消息
messages[_tempMessageIndex] = messages[_tempMessageIndex].copyWith( if (intermediateIndex >= 0) {
// 更新已有的中间状态消息
messages[intermediateIndex] = messages[intermediateIndex].copyWith(
content: text, content: text,
); );
messages.refresh(); messages.refresh();
} else { } else {
// 添加新的临时消息 // 添加新的中间状态消息
final tempMessage = Message( var intermediateMessage = Message.intermediate('user');
role: 'user', intermediateMessage = intermediateMessage.copyWith(content: text);
content: text, messages.add(intermediateMessage);
timestamp: DateTime.now(),
);
messages.add(tempMessage);
_tempMessageIndex = messages.length - 1;
messages.refresh(); messages.refresh();
} }
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
_scrollToBottom(); _scrollToBottom();
}); });
@ -606,14 +621,12 @@ class ChatController extends GetxController {
// 更新活动时间 // 更新活动时间
_updateActivityTime(); _updateActivityTime();
// 查找并移除临时消息 // 查找并移除用户的中间状态消息
if (_tempMessageIndex >= 0 && _tempMessageIndex < messages.length) { final intermediateIndex = messages.indexWhere((m) => m.isIntermediate && m.role == 'user');
messages.removeAt(_tempMessageIndex); if (intermediateIndex >= 0) {
messages.removeAt(intermediateIndex);
} }
// 重置临时消息索引
_tempMessageIndex = -1;
// 发送识别到的文本 // 发送识别到的文本
sendMessage(text); sendMessage(text);
@ -640,16 +653,20 @@ class ChatController extends GetxController {
// 清理语音识别资源 // 清理语音识别资源
void _cleanupRecognition() { void _cleanupRecognition() {
// 查找并移除临时消息 // 查找并移除用户的中间状态消息
if (_tempMessageIndex >= 0 && _tempMessageIndex < messages.length) { final intermediateIndex = messages.indexWhere((m) => m.isIntermediate && m.role == 'user');
if (messages[_tempMessageIndex].content.trim().isEmpty) { if (intermediateIndex >= 0) {
messages.removeAt(_tempMessageIndex); if (messages[intermediateIndex].content.trim().isEmpty) {
messages.removeAt(intermediateIndex);
} else {
// 如果中间状态消息有内容,将其转换为正式消息
messages[intermediateIndex] = messages[intermediateIndex].copyWith(
isIntermediate: false,
);
} }
messages.refresh();
} }
// 重置临时消息索引
_tempMessageIndex = -1;
// 停止语音识别 // 停止语音识别
isVoiceRecognitionActive.value = false; isVoiceRecognitionActive.value = false;
isListening.value = false; isListening.value = false;
@ -671,15 +688,20 @@ class ChatController extends GetxController {
recognizedText.value = ''; recognizedText.value = '';
isSpeechDetected.value = false; isSpeechDetected.value = false;
// 查找并移除临时消息 // 查找并处理用户的中间状态消息
if (_tempMessageIndex >= 0 && _tempMessageIndex < messages.length) { final intermediateIndex = messages.indexWhere((m) => m.isIntermediate && m.role == 'user');
if (messages[_tempMessageIndex].content.trim().isEmpty) { if (intermediateIndex >= 0) {
messages.removeAt(_tempMessageIndex); if (messages[intermediateIndex].content.trim().isEmpty) {
// 如果没有内容则移除
messages.removeAt(intermediateIndex);
} else {
// 有内容则转为正式消息
messages[intermediateIndex] = messages[intermediateIndex].copyWith(
isIntermediate: false,
);
} }
messages.refresh();
} }
// 重置临时消息索引
_tempMessageIndex = -1;
} catch (e) { } catch (e) {
Logger.error('停止语音识别失败', e); Logger.error('停止语音识别失败', e);
} }
@ -688,7 +710,7 @@ class ChatController extends GetxController {
// 清空聊天记录 // 清空聊天记录
void clearChat() { void clearChat() {
messages.clear(); messages.clear();
_ttsService.stop();
// 添加欢迎消息 // 添加欢迎消息
if (agent.welcomeMessage.isNotEmpty) { if (agent.welcomeMessage.isNotEmpty) {
messages.add(Message( messages.add(Message(
@ -697,31 +719,119 @@ class ChatController extends GetxController {
timestamp: DateTime.now(), timestamp: DateTime.now(),
)); ));
} }
// 删除数据库中的历史记录
ChatStorage.deleteMessages(sessionId: agent.id).catchError((e) {
Logger.error('清空数据库聊天记录失败: $e');
});
}
// 滚动监听,用于触发加载更多历史记录
void _scrollListener() {
// 当滚动到顶部并且有更多历史记录且不在加载中时,加载更多
if (scrollController.position.pixels == scrollController.position.minScrollExtent &&
_hasMoreHistory.value &&
!_isLoadingMore.value) {
_loadMoreHistory();
}
}
// 加载聊天历史 (首次加载或刷新)
Future<void> _loadChatHistory() async {
isLoading.value = true;
_currentPage.value = 1; // 重置为第一页
_hasMoreHistory.value = true; // 假设有更多
try {
final pagedResult = await ChatStorage.getMessages(
sessionId: agent.id,
page: _currentPage.value,
pageSize: _pageSize,
);
_totalPages.value = pagedResult.totalPages;
_hasMoreHistory.value = _currentPage.value < _totalPages.value;
// 删除存储的历史记录 final historyMessages = pagedResult.messages.map((chatMsg) {
_historyService.clearHistory(agent.id); return Message(
role: chatMsg.sender,
content: chatMsg.message,
timestamp: DateTime.parse(chatMsg.timestamp),
);
}).toList();
messages.assignAll(historyMessages.reversed); // 最新的在底部,所以要反转
// 滚动到底部 (如果是首次加载)
WidgetsBinding.instance.addPostFrameCallback((_) {
if (scrollController.hasClients) {
scrollController.jumpTo(scrollController.position.maxScrollExtent);
}
});
} catch (e) {
Logger.error('加载聊天历史失败', e);
} finally {
isLoading.value = false;
}
} }
// 加载聊天历史 // 加载更多历史记录
void _loadChatHistory() { Future<void> _loadMoreHistory() async {
// 从历史服务加载 if (_isLoadingMore.value || !_hasMoreHistory.value) return;
final history = _historyService.loadHistory(agent.id);
if (history.isNotEmpty) { _isLoadingMore.value = true;
messages.clear(); _currentPage.value++;
messages.addAll(history);
try {
final pagedResult = await ChatStorage.getMessages(
sessionId: agent.id,
page: _currentPage.value,
pageSize: _pageSize,
);
_totalPages.value = pagedResult.totalPages;
_hasMoreHistory.value = _currentPage.value < _totalPages.value;
final olderMessages = pagedResult.messages.map((chatMsg) {
return Message(
role: chatMsg.sender,
content: chatMsg.message,
timestamp: DateTime.parse(chatMsg.timestamp),
);
}).toList();
// 将旧消息插入到列表顶部
messages.insertAll(0, olderMessages.reversed);
} catch (e) {
Logger.error('加载更多聊天历史失败', e);
_currentPage.value--; // 加载失败,恢复页码
} finally {
_isLoadingMore.value = false;
} }
} }
// 保存聊天历史 // 保存单条聊天消息到数据库
void _saveChatHistory() { Future<void> _saveMessageToDb(Message message) async {
_historyService.saveHistory(agent.id, messages); if (message.isIntermediate) return; // 不保存中间状态的消息
try {
await ChatStorage.saveMessage(
sessionId: agent.id,
message: message.content,
sender: message.role,
);
} catch (e) {
Logger.error('保存消息到数据库失败', e);
}
} }
// 添加消息,供外部调用 // 添加消息,供外部调用 (如用户发送消息、AI回复)
void addMessage(Message message) { void addMessage(Message message) {
messages.add(message); messages.add(message);
_saveChatHistory(); _saveMessageToDb(message); // 保存到数据库
_scrollToBottom(); WidgetsBinding.instance.addPostFrameCallback((_) {
_scrollToBottom();
});
} }
void _stopVoiceRecognitionAutomatically() { void _stopVoiceRecognitionAutomatically() {

20
lib/modules/chat/models/message_model.dart

@ -2,13 +2,13 @@ class Message {
final String role; // 'user' or 'assistant' final String role; // 'user' or 'assistant'
final String content; final String content;
final DateTime timestamp; final DateTime timestamp;
final bool isLoading; final bool isIntermediate; // 中间状态标记,替代isLoading
Message({ Message({
required this.role, required this.role,
required this.content, required this.content,
required this.timestamp, required this.timestamp,
this.isLoading = false, this.isIntermediate = false, // 默认不是中间状态
}); });
// 从JSON构造函数 // 从JSON构造函数
@ -17,7 +17,7 @@ class Message {
role: json['role'] as String, role: json['role'] as String,
content: json['content'] as String, content: json['content'] as String,
timestamp: DateTime.parse(json['timestamp'] as String), timestamp: DateTime.parse(json['timestamp'] as String),
isLoading: json['isLoading'] as bool? ?? false, isIntermediate: json['isIntermediate'] as bool? ?? false,
); );
} }
@ -27,17 +27,17 @@ class Message {
'role': role, 'role': role,
'content': content, 'content': content,
'timestamp': timestamp.toIso8601String(), 'timestamp': timestamp.toIso8601String(),
'isLoading': isLoading, 'isIntermediate': isIntermediate,
}; };
} }
// 创建一个加载中的消息 // 创建一个中间状态的消息
factory Message.loading() { factory Message.intermediate(String role) {
return Message( return Message(
role: 'assistant', role: role,
content: '', content: '',
timestamp: DateTime.now(), timestamp: DateTime.now(),
isLoading: true, isIntermediate: true,
); );
} }
@ -46,13 +46,13 @@ class Message {
String? role, String? role,
String? content, String? content,
DateTime? timestamp, DateTime? timestamp,
bool? isLoading, bool? isIntermediate,
}) { }) {
return Message( return Message(
role: role ?? this.role, role: role ?? this.role,
content: content ?? this.content, content: content ?? this.content,
timestamp: timestamp ?? this.timestamp, timestamp: timestamp ?? this.timestamp,
isLoading: isLoading ?? this.isLoading, isIntermediate: isIntermediate ?? this.isIntermediate,
); );
} }
} }

2
lib/modules/chat/views/chat_view.dart

@ -312,7 +312,7 @@ class ChatView extends GetView<ChatController> {
), ),
// 加载中动画 // 加载中动画
if (message.isLoading) if (message.isIntermediate)
Padding( Padding(
padding: EdgeInsets.only(top: 8.h), padding: EdgeInsets.only(top: 8.h),
child: Row( child: Row(

2
lib/modules/splash/controllers/splash_controller.dart

@ -34,7 +34,7 @@ class SplashController extends GetxController {
// 进度条动画(持续2秒) // 进度条动画(持续2秒)
void _startLoadingAnimation() { void _startLoadingAnimation() {
const totalDuration = 2000; // 动画总时长2秒 const totalDuration = 500; // 动画总时长2秒
const steps = totalDuration ~/ 50; // 40次更新(50ms间隔) const steps = totalDuration ~/ 50; // 40次更新(50ms间隔)
final stepValue = 1.0 / steps; final stepValue = 1.0 / steps;

0
local_plugins/azure_speech/LICENSE

0
local_plugins/azure_speech/ios/Classes/.gitkeep

24
local_plugins/azure_speech/ios/azure_speech.podspec

@ -0,0 +1,24 @@
#
# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html.
# Run `pod lib lint azure_speech.podspec` to validate before publishing.
#
Pod::Spec.new do |s|
s.name = 'azure_speech'
s.version = '0.0.1'
s.summary = 'Azure Speech plugin'
s.description = <<-DESC
A Flutter plugin for Azure Speech services.
DESC
s.homepage = 'http://example.com'
s.license = { :file => '../LICENSE' }
s.author = { 'Your Company' => 'email@example.com' }
s.source = { :path => '.' }
s.source_files = 'Classes/**/*'
s.dependency 'Flutter'
s.dependency 'MicrosoftCognitiveServicesSpeech-iOS'
s.platform = :ios, '12.0'
# Flutter.framework does not contain a i386 slice.
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' }
s.swift_version = '5.0'
end

0
local_plugins/chat_storage/LICENSE

12
local_plugins/chat_storage_sqlite/README.md → local_plugins/chat_storage/README.md

@ -14,10 +14,10 @@ SQLite存储插件,用于存储聊天记录并支持分页查询。
### 保存聊天记录 ### 保存聊天记录
```dart ```dart
import 'package:chat_storage_sqlite/chat_storage_sqlite.dart'; import 'package:chat_storage/chat_storage.dart';
// 保存一条聊天记录 // 保存一条聊天记录
final result = await ChatStorageSqlite.saveMessage( final result = await ChatStorage.saveMessage(
sessionId: 'session_123', sessionId: 'session_123',
message: '你好,这是一条测试消息', message: '你好,这是一条测试消息',
sender: 'user', sender: 'user',
@ -35,7 +35,7 @@ if (result['success']) {
```dart ```dart
// 获取指定会话的聊天记录,支持分页 // 获取指定会话的聊天记录,支持分页
final pagedResult = await ChatStorageSqlite.getMessages( final pagedResult = await ChatStorage.getMessages(
sessionId: 'session_123', sessionId: 'session_123',
page: 1, // 当前页码,从1开始 page: 1, // 当前页码,从1开始
pageSize: 20, // 每页记录数 pageSize: 20, // 每页记录数
@ -60,17 +60,17 @@ for (var message in pagedResult.messages) {
```dart ```dart
// 方式1:删除指定ID的消息 // 方式1:删除指定ID的消息
final deleteResult1 = await ChatStorageSqlite.deleteMessages( final deleteResult1 = await ChatStorage.deleteMessages(
messageIds: [1, 2, 3], messageIds: [1, 2, 3],
); );
// 方式2:删除整个会话的所有消息 // 方式2:删除整个会话的所有消息
final deleteResult2 = await ChatStorageSqlite.deleteMessages( final deleteResult2 = await ChatStorage.deleteMessages(
sessionId: 'session_123', sessionId: 'session_123',
); );
// 方式3:清空整个数据库 // 方式3:清空整个数据库
final clearResult = await ChatStorageSqlite.clearDatabase(); final clearResult = await ChatStorage.clearDatabase();
``` ```
## 注意事项 ## 注意事项

2
local_plugins/chat_storage_sqlite/android/build.gradle.kts → local_plugins/chat_storage/android/build.gradle.kts

@ -8,7 +8,7 @@ plugins {
android { android {
// 命名空间,对应你插件的包名(需与代码内包名保持一致) // 命名空间,对应你插件的包名(需与代码内包名保持一致)
namespace = "com.yunqiinnovation.chat_storage_sqlite" namespace = "com.yunqiinnovation.chat_storage"
// 目标 SDK 版本 // 目标 SDK 版本
compileSdk = 33 compileSdk = 33

1
local_plugins/chat_storage/android/settings.gradle.kts

@ -0,0 +1 @@
rootProject.name = "chat_storage"

2
local_plugins/chat_storage_sqlite/android/src/main/AndroidManifest.xml → local_plugins/chat_storage/android/src/main/AndroidManifest.xml

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.yunqiinnovation.chat_storage_sqlite"> package="com.yunqiinnovation.chat_storage">
<!-- 添加sqlite相关权限 --> <!-- 添加sqlite相关权限 -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

6
local_plugins/chat_storage_sqlite/android/src/main/kotlin/com/yunqiinnovation/chat_storage_sqlite/ChatStorageSqlitePlugin.kt → local_plugins/chat_storage/android/src/main/kotlin/com/yunqiinnovation/chat_storage/ChatStoragePlugin.kt

@ -1,4 +1,4 @@
package com.yunqiinnovation.chat_storage_sqlite package com.yunqiinnovation.chat_storage
import android.content.Context import android.content.Context
import android.database.Cursor import android.database.Cursor
@ -65,14 +65,14 @@ private class ChatDatabaseHelper(context: Context) :
} }
/** ChatStorageSqlitePlugin */ /** ChatStorageSqlitePlugin */
class ChatStorageSqlitePlugin: FlutterPlugin, MethodCallHandler { class ChatStoragePlugin: FlutterPlugin, MethodCallHandler {
private lateinit var channel : MethodChannel private lateinit var channel : MethodChannel
private lateinit var context: Context private lateinit var context: Context
private lateinit var dbHelper: ChatDatabaseHelper private lateinit var dbHelper: ChatDatabaseHelper
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
context = flutterPluginBinding.applicationContext context = flutterPluginBinding.applicationContext
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "chat_storage_sqlite") channel = MethodChannel(flutterPluginBinding.binaryMessenger, "chat_storage")
channel.setMethodCallHandler(this) channel.setMethodCallHandler(this)
dbHelper = ChatDatabaseHelper(context) dbHelper = ChatDatabaseHelper(context)
} }

0
local_plugins/chat_storage/ios/Classes/.gitkeep

23
local_plugins/chat_storage/ios/chat_storage.podspec

@ -0,0 +1,23 @@
#
# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html.
# Run `pod lib lint chat_storage.podspec` to validate before publishing.
#
Pod::Spec.new do |s|
s.name = 'chat_storage'
s.version = '0.0.1'
s.summary = 'Chat Storage plugin'
s.description = <<-DESC
A Flutter plugin for chat storage services.
DESC
s.homepage = 'http://example.com'
s.license = { :file => '../LICENSE' }
s.author = { 'Your Company' => 'email@example.com' }
s.source = { :path => '.' }
s.source_files = 'Classes/**/*'
s.dependency 'Flutter'
s.platform = :ios, '12.0'
# Flutter.framework does not contain a i386 slice.
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' }
s.swift_version = '5.0'
end

5
local_plugins/chat_storage_sqlite/lib/chat_storage_sqlite.dart → local_plugins/chat_storage/lib/chat_storage.dart

@ -62,8 +62,8 @@ class PagedResult {
} }
/// SQLite聊天记录存储插件 /// SQLite聊天记录存储插件
class ChatStorageSqlite { class ChatStorage {
static const MethodChannel _channel = MethodChannel('chat_storage_sqlite'); static const MethodChannel _channel = MethodChannel('chat_storage');
/// 保存一条聊天记录 /// 保存一条聊天记录
static Future<Map<String, dynamic>> saveMessage({ static Future<Map<String, dynamic>> saveMessage({
@ -77,7 +77,6 @@ class ChatStorageSqlite {
'sessionId': sessionId, 'sessionId': sessionId,
'message': message, 'message': message,
'sender': sender, 'sender': sender,
'metadata': metadata,
}); });
return Map<String, dynamic>.from(result); return Map<String, dynamic>.from(result);
} catch (e) { } catch (e) {

12
local_plugins/chat_storage_sqlite/pubspec.yaml → local_plugins/chat_storage/pubspec.yaml

@ -1,7 +1,7 @@
name: chat_storage_sqlite name: chat_storage
description: SQLite存储插件,用于存储聊天记录并支持分页查询 description: A Flutter plugin for storing chat messages using SQLite.
version: 0.0.1 version: 0.0.1
homepage: # homepage:
environment: environment:
sdk: ">=2.17.0 <3.0.0" sdk: ">=2.17.0 <3.0.0"
@ -24,7 +24,7 @@ flutter:
plugin: plugin:
platforms: platforms:
android: android:
package: com.yunqiinnovation.chat_storage_sqlite package: com.yunqiinnovation.chat_storage
pluginClass: ChatStorageSqlitePlugin pluginClass: ChatStoragePlugin
ios: ios:
pluginClass: ChatStorageSqlitePlugin pluginClass: ChatStoragePlugin

1
local_plugins/chat_storage_sqlite/android/settings.gradle.kts

@ -1 +0,0 @@
rootProject.name = "chat_storage_sqlite"

2
local_plugins/jieli/android/src/main/kotlin/com/yunqiinnovation/jieli/BleService.kt

@ -45,7 +45,7 @@ interface BleCallback {
*/ */
class BleService(private val context: Context) { class BleService(private val context: Context) {
companion object { companion object {
private const val TAG = "JieliBleCoreService" private const val TAG = "JieliBleService"
// 服务和特征UUID // 服务和特征UUID
private const val AUDIO_SERVICE_UUID = "0000A100-0000-1000-8000-00805F9B34FB" private const val AUDIO_SERVICE_UUID = "0000A100-0000-1000-8000-00805F9B34FB"

0
local_plugins/open_ai_service/LICENSE

141
local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/OpenAIService.kt

@ -11,6 +11,7 @@ import kotlinx.coroutines.withContext
import okhttp3.* import okhttp3.*
import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.RequestBody.Companion.toRequestBody
import okio.Buffer
import org.json.JSONArray import org.json.JSONArray
import org.json.JSONObject import org.json.JSONObject
import java.io.IOException import java.io.IOException
@ -389,8 +390,8 @@ class OpenAIService(private val context: Context? = null) {
.build() .build()
// 输出用于测试的curl命令 // 输出用于测试的curl命令
// val curlCommand = buildCurlCommand(request, requestBody.toString()) val curlCommand = buildCurlCommand(request, requestBody.toString())
// Log.d(TAG, "curl command: $curlCommand") Log.d(TAG, "curl command: $curlCommand")
client.newCall(request).enqueue(object : Callback { client.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) { override fun onFailure(call: Call, e: IOException) {
@ -406,81 +407,95 @@ class OpenAIService(private val context: Context? = null) {
val responseBody = response.body ?: return val responseBody = response.body ?: return
val source = responseBody.source() val source = responseBody.source()
val buffer = Buffer()
try { try {
// 预取数据到缓冲区
source.request(Long.MAX_VALUE)
val bufferedSource = source.buffer
// 用于存储函数调用的各个部分 // 用于存储函数调用的各个部分
val finalToolCalls = mutableMapOf<Int, ToolCallInfo>() val finalToolCalls = mutableMapOf<Int, ToolCallInfo>()
val lineBuffer = StringBuilder()
while (!bufferedSource.exhausted()) { // 逐块读取数据,而不是一次性预取
val line = bufferedSource.readUtf8Line()?.trim() ?: continue val BUFFER_SIZE = 8192L // 8KB的块大小
if (line.isEmpty()) continue
if (line.startsWith("data:")) { while (!Thread.currentThread().isInterrupted) {
val data = line.substring(5).trim() // 读取一小块数据
val bytesRead = source.read(buffer, BUFFER_SIZE)
// 处理[DONE]消息 if (bytesRead == -1L) break // 流结束
if (data == "[DONE]" || data == "[\"DONE\"]") {
processToolCalls(finalToolCalls, callback, messages) // 处理接收到的数据
callback.onComplete() while (true) {
break val line = buffer.readUtf8Line() ?: break
} if (line.trim().isEmpty()) continue
try { if (line.startsWith("data:")) {
val jsonData = JSONObject(data) val data = line.substring(5).trim()
Log.d("OpenAIService", "$data")
// 处理消息内容 // 处理[DONE]消息
if (jsonData.has("choices")) { if (data == "[DONE]" || data == "[\"DONE\"]") {
val choices = jsonData.getJSONArray("choices") Log.d(TAG, "收到[DONE]消息")
if (choices.length() > 0) { val hasToolCalls = processToolCalls(finalToolCalls, callback, messages)
val choice = choices.getJSONObject(0) // 只有在没有工具调用时才认为对话真正完成
if (!hasToolCalls) {
if (choice.has("delta")) { callback.onComplete()
val delta = choice.getJSONObject("delta") }
return
// 处理普通文本内容 }
if (delta.has("content")) {
val content = delta.getString("content") try {
callback.onToken(content) val jsonData = JSONObject(data)
}
// 处理消息内容
if (jsonData.has("choices")) {
val choices = jsonData.getJSONArray("choices")
if (choices.length() > 0) {
val choice = choices.getJSONObject(0)
// 处理工具调用(函数调用) if (choice.has("delta")) {
if (delta.has("tool_calls")) { val delta = choice.getJSONObject("delta")
val toolCalls = delta.getJSONArray("tool_calls")
for (i in 0 until toolCalls.length()) { // 处理普通文本内容
val toolCall = toolCalls.getJSONObject(i) if (delta.has("content")) {
val index = toolCall.getInt("index") val content = delta.getString("content")
callback.onToken(content)
// 创建或获取现有的工具调用信息 }
val toolCallInfo = finalToolCalls.getOrPut(index) { ToolCallInfo() }
// 处理工具调用(函数调用)
// 更新ID if (delta.has("tool_calls")) {
if (toolCall.has("id")) { val toolCalls = delta.getJSONArray("tool_calls")
toolCallInfo.id = toolCall.getString("id") for (i in 0 until toolCalls.length()) {
} val toolCall = toolCalls.getJSONObject(i)
val index = toolCall.getInt("index")
// 更新函数信息
if (toolCall.has("function")) { // 创建或获取现有的工具调用信息
val function = toolCall.getJSONObject("function") val toolCallInfo = finalToolCalls.getOrPut(index) { ToolCallInfo() }
if (function.has("name")) { // 更新ID
toolCallInfo.name = function.getString("name") if (toolCall.has("id")) {
toolCallInfo.id = toolCall.getString("id")
} }
if (function.has("arguments")) { // 更新函数信息
toolCallInfo.arguments += function.getString("arguments") if (toolCall.has("function")) {
val function = toolCall.getJSONObject("function")
if (function.has("name")) {
toolCallInfo.name = function.getString("name")
}
if (function.has("arguments")) {
toolCallInfo.arguments += function.getString("arguments")
}
} }
} }
} }
} }
} }
} }
} catch (e: Exception) {
// 忽略解析错误
Log.e(TAG, "解析JSON出错: ${e.message}")
} }
} catch (e: Exception) {
// 忽略解析错误
Log.e(TAG, "解析JSON出错: ${e.message}")
} }
} }
} }
@ -548,11 +563,11 @@ class OpenAIService(private val context: Context? = null) {
/** /**
* 处理工具调用结果 * 处理工具调用结果
*/ */
private fun processToolCalls(toolCalls: Map<Int, ToolCallInfo>, callback: StreamCallback, messages: JSONArray? = null) { private fun processToolCalls(toolCalls: Map<Int, ToolCallInfo>, callback: StreamCallback, messages: JSONArray? = null): Boolean {
if (toolCalls.isEmpty()) return if (toolCalls.isEmpty()) return false
// 只处理第一个工具调用 // 只处理第一个工具调用
val firstToolCall = toolCalls.entries.firstOrNull()?.value ?: return val firstToolCall = toolCalls.entries.firstOrNull()?.value ?: return false
if (firstToolCall.isValid()) { if (firstToolCall.isValid()) {
// 创建函数调用JSON对象 // 创建函数调用JSON对象
@ -572,7 +587,9 @@ class OpenAIService(private val context: Context? = null) {
} }
} }
} }
return true
} }
return false
} }
/** /**

169
local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/OpenAIServicePlugin.kt

@ -1,6 +1,8 @@
package com.yunqiinnovation.open_ai_service package com.yunqiinnovation.open_ai_service
import android.content.Context import android.content.Context
import android.os.Handler
import android.os.Looper
import android.util.Log import android.util.Log
import androidx.annotation.NonNull import androidx.annotation.NonNull
import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.embedding.engine.plugins.FlutterPlugin
@ -43,8 +45,11 @@ class OpenAIServicePlugin : FlutterPlugin, MethodCallHandler, StreamHandler {
/// 事件接收器(用于流式输出) /// 事件接收器(用于流式输出)
private var eventSink: EventSink? = null private var eventSink: EventSink? = null
/// 执行器(用于后台线程) /// 执行器(用于真正需要在后台线程处理的任务)
private val executor = Executors.newSingleThreadExecutor() private val executor = Executors.newSingleThreadExecutor()
/// 主线程Handler
private val mainHandler = Handler(Looper.getMainLooper())
override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
// 保存上下文 // 保存上下文
@ -98,17 +103,17 @@ class OpenAIServicePlugin : FlutterPlugin, MethodCallHandler, StreamHandler {
messages.put(JSONObject(message)) messages.put(JSONObject(message))
} }
// 在后台线程执行请求 // 对于需要在后台执行的同步阻塞操作,仍然使用executor
executor.execute { executor.execute {
try { try {
val response = openAIService.sendMessage(messages) val response = openAIService.sendMessage(messages)
// 在主线程返回结果 // 在主线程返回结果
android.os.Handler(android.os.Looper.getMainLooper()).post { mainHandler.post {
result.success(response) result.success(response)
} }
} catch (e: Exception) { } catch (e: Exception) {
// 在主线程返回错误 // 在主线程返回错误
android.os.Handler(android.os.Looper.getMainLooper()).post { mainHandler.post {
result.error("OPENAI_ERROR", e.message, null) result.error("OPENAI_ERROR", e.message, null)
} }
} }
@ -130,53 +135,47 @@ class OpenAIServicePlugin : FlutterPlugin, MethodCallHandler, StreamHandler {
messages.put(JSONObject(message)) messages.put(JSONObject(message))
} }
// 在后台线程执行请求 // 不再在后台线程包装OkHttp的异步调用
executor.execute { try {
try { openAIService.sendMessageStream(
openAIService.sendMessageStream( messages = messages,
messages = messages, callback = object : OpenAIService.StreamCallback {
callback = object : OpenAIService.StreamCallback { override fun onToken(token: String) {
override fun onToken(token: String) { // 发送token事件
// 发送token事件 mainHandler.post {
android.os.Handler(android.os.Looper.getMainLooper()).post { eventSink?.success(mapOf("type" to "token", "content" to token))
eventSink?.success(mapOf("type" to "token", "content" to token))
}
} }
}
override fun onComplete() {
// 发送完成事件 override fun onComplete() {
android.os.Handler(android.os.Looper.getMainLooper()).post { // 发送完成事件
eventSink?.success(mapOf("type" to "complete")) mainHandler.post {
} eventSink?.success(mapOf("type" to "complete"))
} }
}
override fun onError(e: Exception) {
// 发送错误事件 override fun onError(e: Exception) {
android.os.Handler(android.os.Looper.getMainLooper()).post { // 发送错误事件
eventSink?.success(mapOf("type" to "error", "content" to e.message)) mainHandler.post {
} eventSink?.success(mapOf("type" to "error", "content" to e.message))
} }
}
override fun onFunctionCall(functionCall: JSONObject) {
// 发送函数调用事件 override fun onFunctionCall(functionCall: JSONObject) {
android.os.Handler(android.os.Looper.getMainLooper()).post { // 发送函数调用事件
val functionCallMap = functionCall.toMap() mainHandler.post {
eventSink?.success(mapOf("type" to "functionCall", "content" to functionCallMap)) val functionCallMap = functionCall.toMap()
} eventSink?.success(mapOf("type" to "functionCall", "content" to functionCallMap))
} }
} }
)
// 请求已开始
android.os.Handler(android.os.Looper.getMainLooper()).post {
result.success(true)
} }
} catch (e: Exception) { )
// 在主线程返回错误
android.os.Handler(android.os.Looper.getMainLooper()).post { // 请求已开始,直接返回结果
result.error("OPENAI_ERROR", e.message, null) result.success(true)
} } catch (e: Exception) {
} // 直接在当前线程处理错误
result.error("OPENAI_ERROR", e.message, null)
} }
} }
@ -200,55 +199,49 @@ class OpenAIServicePlugin : FlutterPlugin, MethodCallHandler, StreamHandler {
// 转换函数调用 // 转换函数调用
val functionCall = JSONObject(functionCallRaw) val functionCall = JSONObject(functionCallRaw)
// 在后台线程执行请求 // 不再在后台线程包装OkHttp的异步调用
executor.execute { try {
try { openAIService.sendFunctionCallResult(
openAIService.sendFunctionCallResult( messages = messages,
messages = messages, functionCall = functionCall,
functionCall = functionCall, functionResult = functionResult,
functionResult = functionResult, callback = object : OpenAIService.StreamCallback {
callback = object : OpenAIService.StreamCallback { override fun onToken(token: String) {
override fun onToken(token: String) { // 发送token事件
// 发送token事件 mainHandler.post {
android.os.Handler(android.os.Looper.getMainLooper()).post { eventSink?.success(mapOf("type" to "token", "content" to token))
eventSink?.success(mapOf("type" to "token", "content" to token))
}
} }
}
override fun onComplete() {
// 发送完成事件 override fun onComplete() {
android.os.Handler(android.os.Looper.getMainLooper()).post { // 发送完成事件
eventSink?.success(mapOf("type" to "complete")) mainHandler.post {
} eventSink?.success(mapOf("type" to "complete"))
} }
}
override fun onError(e: Exception) {
// 发送错误事件 override fun onError(e: Exception) {
android.os.Handler(android.os.Looper.getMainLooper()).post { // 发送错误事件
eventSink?.success(mapOf("type" to "error", "content" to e.message)) mainHandler.post {
} eventSink?.success(mapOf("type" to "error", "content" to e.message))
} }
}
override fun onFunctionCall(nestedFunctionCall: JSONObject) {
// 发送函数调用事件 override fun onFunctionCall(nestedFunctionCall: JSONObject) {
android.os.Handler(android.os.Looper.getMainLooper()).post { // 发送函数调用事件
val functionCallMap = nestedFunctionCall.toMap() mainHandler.post {
eventSink?.success(mapOf("type" to "functionCall", "content" to functionCallMap)) val functionCallMap = nestedFunctionCall.toMap()
} eventSink?.success(mapOf("type" to "functionCall", "content" to functionCallMap))
} }
} }
)
// 请求已开始
android.os.Handler(android.os.Looper.getMainLooper()).post {
result.success(true)
} }
} catch (e: Exception) { )
// 在主线程返回错误
android.os.Handler(android.os.Looper.getMainLooper()).post { // 请求已开始,直接返回结果
result.error("OPENAI_ERROR", e.message, null) result.success(true)
} } catch (e: Exception) {
} // 直接在当前线程处理错误
result.error("OPENAI_ERROR", e.message, null)
} }
} }

4
local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/SystemFunctionHandler.kt

@ -78,7 +78,7 @@ class SystemFunctionHandler(private val context: Context) {
private fun registerTranslationModeFunction(client: MCPClient) { private fun registerTranslationModeFunction(client: MCPClient) {
client.registerLocalFunction( client.registerLocalFunction(
"enter_translation_mode", "enter_translation_mode",
"进入实时翻译模式", "用户请求进入实时翻译模式时,启动实时翻译功能",
JSONObject(""" JSONObject("""
{ {
"type": "object", "type": "object",
@ -167,7 +167,7 @@ class SystemFunctionHandler(private val context: Context) {
private fun registerOpenAppFunction(client: MCPClient) { private fun registerOpenAppFunction(client: MCPClient) {
client.registerLocalFunction( client.registerLocalFunction(
"open_app", "open_app",
"打开应用", "当用户请求打开特定应用时,启动相应应用",
JSONObject(""" JSONObject("""
{ {
"type": "object", "type": "object",

0
local_plugins/open_ai_service/ios/Classes/.gitkeep

296
local_plugins/open_ai_service/ios/Classes/OpenAIService.swift

@ -237,30 +237,47 @@ public class OpenAIService {
} }
/// 发送消息(流式输出) /// 发送消息(流式输出)
public func sendMessageStream(messages: [[String: Any]], systemPrompt: String, callback: @escaping StreamCallback) { public func sendMessageStream(
messages: [[String: Any]],
systemPrompt: String,
callback: @escaping StreamCallback
) {
guard isInitialized, !apiKey.isEmpty else { guard isInitialized, !apiKey.isEmpty else {
callback.onError(OpenAIError("OpenAI服务未初始化")) callback.onError(OpenAIError("OpenAI服务未初始化"))
return return
} }
// 构建完整消息,添加系统提示
var fullMessages: [[String: Any]] = [
["role": "system", "content": systemPrompt]
]
fullMessages.append(contentsOf: messages)
// 构建请求体 // 构建请求体
var requestDict: [String: Any] = [ var requestBody: [String: Any] = [
"model": model, "model": model,
"messages": fullMessages,
"temperature": 0.7, "temperature": 0.7,
"max_tokens": 2000, "max_tokens": 2000,
"stream": true "stream": true
] ]
// 如果有注册的函数,添加到请求中 // 构建完整消息数组,添加系统提示
if !registeredFunctions.isEmpty { var fullMessages: [[String: Any]] = [
["role": "system", "content": systemPrompt]
]
fullMessages.append(contentsOf: messages)
requestBody["messages"] = fullMessages
// 添加工具列表
if let toolMaps = mcpClient?.getToolMaps(), !toolMaps.isEmpty {
var tools: [[String: Any]] = []
for toolMap in toolMaps {
if let tool = toolMap as? [String: Any] {
tools.append(tool)
}
}
if !tools.isEmpty {
requestBody["tools"] = tools
}
} else if !registeredFunctions.isEmpty {
var tools: [[String: Any]] = [] var tools: [[String: Any]] = []
for function in registeredFunctions { for function in registeredFunctions {
let tool: [String: Any] = [ let tool: [String: Any] = [
"type": "function", "type": "function",
@ -268,11 +285,14 @@ public class OpenAIService {
] ]
tools.append(tool) tools.append(tool)
} }
requestDict["tools"] = tools
if !tools.isEmpty {
requestBody["tools"] = tools
}
} }
// 将请求数据转换为JSON数据 // 转换为JSON数据
guard let jsonData = try? JSONSerialization.data(withJSONObject: requestDict) else { guard let jsonData = try? JSONSerialization.data(withJSONObject: requestBody) else {
callback.onError(OpenAIError("无法序列化请求数据")) callback.onError(OpenAIError("无法序列化请求数据"))
return return
} }
@ -290,102 +310,10 @@ public class OpenAIService {
request.addValue("text/event-stream", forHTTPHeaderField: "Accept") request.addValue("text/event-stream", forHTTPHeaderField: "Accept")
request.httpBody = jsonData request.httpBody = jsonData
// 用于存储函数调用的各个部分 // 创建流式会话任务
var finalToolCalls: [Int: ToolCallInfo] = [:] let delegate = SSEStreamDelegate(callback: callback)
let session = URLSession(configuration: .default, delegate: delegate, delegateQueue: nil)
// 创建数据任务 let task = session.dataTask(with: request)
let task = session.dataTask(with: request) { data, response, error in
if let error = error {
callback.onError(OpenAIError("请求失败: \(error.localizedDescription)"))
return
}
guard let httpResponse = response as? HTTPURLResponse else {
callback.onError(OpenAIError("无效的HTTP响应"))
return
}
guard httpResponse.statusCode == 200 else {
callback.onError(OpenAIError("API调用失败: \(httpResponse.statusCode)"))
return
}
guard let data = data else {
callback.onError(OpenAIError("响应数据为空"))
return
}
// 处理SSE数据流
if let text = String(data: data, encoding: .utf8) {
let lines = text.components(separatedBy: "\n")
for line in lines {
if line.isEmpty { continue }
if line.hasPrefix("data: ") {
let dataContent = line.dropFirst(6)
// 处理[DONE]消息
if dataContent == "[DONE]" {
self.processToolCalls(finalToolCalls, callback: callback)
callback.onComplete()
break
}
// 解析JSON数据
do {
if let data = dataContent.data(using: .utf8),
let jsonData = try JSONSerialization.jsonObject(with: data) as? [String: Any] {
// 处理消息内容
if let choices = jsonData["choices"] as? [[String: Any]], !choices.isEmpty,
let choice = choices.first {
if let delta = choice["delta"] as? [String: Any] {
// 处理普通文本内容
if let content = delta["content"] as? String {
callback.onToken(content)
}
// 处理工具调用(函数调用)
if let toolCalls = delta["tool_calls"] as? [[String: Any]] {
for toolCall in toolCalls {
if let index = toolCall["index"] as? Int {
// 创建或获取现有的工具调用信息
let toolCallInfo = finalToolCalls[index] ?? ToolCallInfo()
// 更新ID
if let id = toolCall["id"] as? String {
toolCallInfo.id = id
}
// 更新函数信息
if let function = toolCall["function"] as? [String: Any] {
if let name = function["name"] as? String {
toolCallInfo.name = name
}
if let arguments = function["arguments"] as? String {
toolCallInfo.arguments += arguments
}
}
finalToolCalls[index] = toolCallInfo
}
}
}
}
}
}
} catch {
NSLog("解析JSON出错: \(error.localizedDescription)")
// 忽略解析错误,继续处理其他行
}
}
}
}
}
task.resume() task.resume()
} }
@ -447,12 +375,147 @@ public class OpenAIService {
} }
} }
/// SSE流委托实现
private class SSEStreamDelegate: NSObject, URLSessionDataDelegate {
let callback: StreamCallback
private var buffer = Data()
private var finalToolCalls: [Int: ToolCallInfo] = [:]
init(callback: @escaping StreamCallback) {
self.callback = callback
super.init()
}
// 接收数据流
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
buffer.append(data)
// 处理可能包含多行的数据
processBuffer()
}
// 处理缓冲区数据
private func processBuffer() {
// 按行分割
while let newlineIndex = buffer.firstIndex(of: 10) { // 10是换行符的ASCII码
let lineData = buffer.prefix(upTo: newlineIndex)
buffer.removeSubrange(0...newlineIndex) // 移除已处理的行,包括换行符
// 解析行数据
if let line = String(data: lineData, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines) {
processLine(line)
}
}
}
// 处理单行数据
private func processLine(_ line: String) {
guard !line.isEmpty else { return }
if line.hasPrefix("data: ") {
let dataContent = line.dropFirst(6)
// 处理[DONE]消息
if dataContent == "[DONE]" {
let hasToolCalls = self.processToolCalls()
// 只有在没有工具调用时才认为对话真正完成
if !hasToolCalls {
callback.onComplete()
}
return
}
// 解析JSON数据
do {
if let data = dataContent.data(using: .utf8),
let jsonData = try JSONSerialization.jsonObject(with: data) as? [String: Any] {
// 处理消息内容
if let choices = jsonData["choices"] as? [[String: Any]], !choices.isEmpty,
let choice = choices.first {
if let delta = choice["delta"] as? [String: Any] {
// 处理普通文本内容
if let content = delta["content"] as? String {
callback.onToken(content)
}
// 处理工具调用(函数调用)
if let toolCalls = delta["tool_calls"] as? [[String: Any]] {
for toolCall in toolCalls {
if let index = toolCall["index"] as? Int {
// 创建或获取现有的工具调用信息
let toolCallInfo = finalToolCalls[index] ?? ToolCallInfo()
// 更新ID
if let id = toolCall["id"] as? String {
toolCallInfo.id = id
}
// 更新函数信息
if let function = toolCall["function"] as? [String: Any] {
if let name = function["name"] as? String {
toolCallInfo.name = name
}
if let arguments = function["arguments"] as? String {
toolCallInfo.arguments += arguments
}
}
finalToolCalls[index] = toolCallInfo
}
}
}
}
}
}
} catch {
NSLog("解析JSON出错: \(error.localizedDescription)")
// 忽略解析错误,继续处理其他行
}
}
}
// 处理工具调用
private func processToolCalls() -> Bool {
if finalToolCalls.isEmpty { return false }
// 只处理第一个工具调用
guard let firstToolCall = finalToolCalls.values.first, firstToolCall.isValid else { return false }
// 创建函数调用字典
let functionCall: [String: Any] = [
"name": firstToolCall.name,
"arguments": firstToolCall.arguments,
"id": firstToolCall.id
]
// 回调
callback.onFunctionCall(functionCall)
return true
}
// 处理完成
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
if let error = error {
callback.onError(OpenAIError("请求失败: \(error.localizedDescription)"))
}
}
}
/// 流式输出回调协议
public typealias StreamCallback = (onToken: (String) -> Void,
onComplete: () -> Void,
onError: (Error) -> Void,
onFunctionCall: ([String: Any]) -> Void)
/// 处理工具调用(函数调用)并回调 /// 处理工具调用(函数调用)并回调
private func processToolCalls(_ toolCalls: [Int: ToolCallInfo], callback: StreamCallback) { private func processToolCalls(_ toolCalls: [Int: ToolCallInfo], callback: StreamCallback) -> Bool {
if toolCalls.isEmpty { return } if toolCalls.isEmpty { return false }
// 只处理第一个工具调用 // 只处理第一个工具调用
guard let firstToolCall = toolCalls.values.first, firstToolCall.isValid else { return } guard let firstToolCall = toolCalls.values.first, firstToolCall.isValid else { return false }
// 创建函数调用字典 // 创建函数调用字典
let functionCall: [String: Any] = [ let functionCall: [String: Any] = [
@ -463,11 +526,6 @@ public class OpenAIService {
// 回调 // 回调
callback.onFunctionCall(functionCall) callback.onFunctionCall(functionCall)
return true
} }
/// 流式输出回调协议
public typealias StreamCallback = (onToken: (String) -> Void,
onComplete: () -> Void,
onError: (Error) -> Void,
onFunctionCall: ([String: Any]) -> Void)
} }

23
local_plugins/open_ai_service/ios/open_ai_service.podspec

@ -0,0 +1,23 @@
#
# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html.
# Run `pod lib lint open_ai_service.podspec` to validate before publishing.
#
Pod::Spec.new do |s|
s.name = 'open_ai_service'
s.version = '0.0.1'
s.summary = 'OpenAI Service plugin'
s.description = <<-DESC
A Flutter plugin for OpenAI services.
DESC
s.homepage = 'http://example.com'
s.license = { :file => '../LICENSE' }
s.author = { 'Your Company' => 'email@example.com' }
s.source = { :path => '.' }
s.source_files = 'Classes/**/*'
s.dependency 'Flutter'
s.platform = :ios, '12.0'
# Flutter.framework does not contain a i386 slice.
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' }
s.swift_version = '5.0'
end

0
local_plugins/volcano_speech/LICENSE

0
local_plugins/volcano_speech/ios/Classes/.gitkeep

23
local_plugins/volcano_speech/ios/volcano_speech.podspec

@ -0,0 +1,23 @@
#
# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html.
# Run `pod lib lint volcano_speech.podspec` to validate before publishing.
#
Pod::Spec.new do |s|
s.name = 'volcano_speech'
s.version = '0.0.1'
s.summary = 'Volcano Speech plugin'
s.description = <<-DESC
A Flutter plugin for Volcano Speech services.
DESC
s.homepage = 'http://example.com'
s.license = { :file => '../LICENSE' }
s.author = { 'Your Company' => 'email@example.com' }
s.source = { :path => '.' }
s.source_files = 'Classes/**/*'
s.dependency 'Flutter'
s.platform = :ios, '12.0'
# Flutter.framework does not contain a i386 slice.
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' }
s.swift_version = '5.0'
end

4
pubspec.yaml

@ -68,8 +68,8 @@ dependencies:
path: local_plugins/volcano_speech path: local_plugins/volcano_speech
jieli: jieli:
path: local_plugins/jieli path: local_plugins/jieli
chat_storage_sqlite: chat_storage:
path: local_plugins/chat_storage_sqlite path: local_plugins/chat_storage
dev_dependencies: dev_dependencies:

204
test.json

@ -1,57 +1,163 @@
curl -v -X POST -H 'Content-Type: application/json' -H 'Authorization: Bearer 168deb3d-fd0c-4912-b9f1-aaee5c6743e6' -H 'Accept: text/event-stream' -d '{ curl 'https://ark.cn-beijing.volces.com/api/v3/bots/chat/completions' \
"model": "bot-20250405211523-l7c9r", -H "Authorization: Bearer 168deb3d-fd0c-4912-b9f1-aaee5c6743e6" \
"messages": [ -H 'Content-Type: application/json' \
{ -d '{
"role": "system", "model": "bot-20250405211523-l7c9r",
"content": " 你是一个智能语音助手,能够简洁明了地回答用户的问题。\n时刻关心用户的情绪和需求,主动提供鼓励和温暖。\n\n语言风格活泼、亲切,能够幽默地互动,陪伴用户,缓解压力,增添生活乐趣。\n\n请始终以用户为中心,保持回应的高效性、准确性和温暖体贴,成为用户真正的灵魂伴侣。\n \n 当用户说\"退出\"、\"再见\"、\"结束对话\"等类似意图时,你应该使用exit_interaction函数来结束对话,\n 并在结束前说一句友好的告别语,例如\"再见,有需要随时找我\"。" "stream": true,
}, "stream_options": {"include_usage": true},
"messages": [
{ {
"role": "system", "role": "system",
"content": " 你是一个智能语音助手,能够简洁明了地回答用户的问题。\n时刻关心用户的情绪和需求,主动提供鼓励和温暖。\n\n语言风格活泼、亲切,能够幽默地互动,陪伴用户,缓解压力,增添生活乐趣。\n\n请始终以用户为中心,保持回应的高效性、准确性和温暖体贴,成为用户真正的灵魂伴侣。\n \n 当用户说\"退出\"、\"再见\"、\"结束对话\"等类似意图时,你应该使用exit_interaction函数来结束对话,\n 并在结束前说一句友好的告别语,例如\"再见,有需要随时找我\"。" "content": "You are a helpful assistant."
}, },
{ {
"role": "user", "role": "user",
"content": "退下吧。" "content": "Hello!"
},
{
"role": "assistant",
"content": "",
"tool_calls": [
{
"id": "call_8k680azmfc4thqrrnwpwqxah",
"type": "function",
"function": {
"name": "exit_interaction",
"arguments": " {}"
}
}
]
},
{
"role": "tool",
"content": "{\"result\": \"已退出语音交互\"}",
"tool_call_id": "call_8k680azmfc4thqrrnwpwqxah"
} }
], ]
"temperature": 0.7, }'
"max_tokens": 2000,
"stream": true,
"tools": [
{
"type": "function", curl 'https://ark.cn-beijing.volces.com/api/v3/bots/chat/completions' \
"function": { -H 'Content-Type: application/json' \
"name": "exit_interaction", -H 'Authorization: Bearer 168deb3d-fd0c-4912-b9f1-aaee5c6743e6' \
"description": "退出当前语音交互", -d '{
"parameters": { "model": "bot-20250405211523-l7c9r",
"type": "object", "stream": true,
"properties": {}, "messages": [
"required": [] {
} "role": "assistant",
"content": "你好呀!今天想和我聊些什么呢😄 是生活趣事、学习心得,还是其他好玩的话题?"
},
{
"role": "assistant",
"content": ""
},
{
"role": "assistant",
"content": "你好呀!今天过得怎么样?有什么需要我帮忙的吗?或者只是想聊聊天?😊"
},
{
"role": "assistant",
"content": "明白啦!有什么需要我帮忙的吗?无论是查资料、聊天解闷,还是帮你处理手机上的任务,我随时待命哦~ 😊"
},
{
"role": "assistant",
"content": "好的!随时准备陪你测试,有什么需要尽管说哦~ 😊 你希望测试什么功能呢?"
},
{
"role": "assistant",
"content": "哎呀,听起来你有点小困扰呢!可能是因为网络波动、系统更新或者我偶尔“打瞌睡”了(开玩笑啦~)。不过别担心,我会努力保持最佳状态陪伴你!如果有什么具体问题,随时告诉我,我一定帮你解决得妥妥的!😊\n\n(或者你想测试什么功能?我随时待命哦!)"
},
{
"role": "assistant",
"content": "嘿嘿,我是你的贴心语音助理小语呀!随时为你提供帮助、陪你聊天、解答问题,还能给你加油打气哦~有什么需要尽管告诉我吧! 😊"
},
{
"role": "assistant",
"content": "我是小语呀,那个随时准备和你畅聊、给你排忧解难、陪你度过各种时光的手机全能语音助理😜 怎么啦,是不是刚才没记住呀🧐"
},
{
"role": "assistant",
"content": ""
},
{
"role": "user",
"content": "你好呀,你是谁呀?"
}
],
"temperature": 0.7,
"max_tokens": 2000,
"stream": true,
"tools": [
{
"type": "function",
"function": {
"name": "exit_interaction",
"description": "退出当前语音交互",
"parameters": {
"type": "object",
"properties": {},
"required": []
}
}
},
{
"type": "function",
"function": {
"name": "enter_translation_mode",
"description": "用户请求进入实时翻译模式时,启动实时翻译功能",
"parameters": {
"type": "object",
"properties": {},
"required": []
}
}
},
{
"type": "function",
"function": {
"name": "send_text_message",
"description": "发送短信",
"parameters": {
"type": "object",
"properties": {
"contact": {
"type": "string",
"description": "联系人姓名或电话号码"
},
"message": {
"type": "string",
"description": "短信内容"
} }
},
"required": [
"contact",
"message"
]
} }
] }
}' 'https: //ark.cn-beijing.volces.com/api/v3/bots/chat/completions' },
{ {
"code": 0, "type": "function",
"message": "Error during WebSocket handshake: Unexpected response code: 400", "function": {
"request_log":"{\"ab_test\":{},\"base\":{\"has_upload\":false,\"is_pending\":true,\"load_flags\":18,\"load_state\":0,\"method\":\"GET\",\"net_error\":-320,\"network_isolation_key\":\"wss://openspeech.bytedance.com wss://openspeech.bytedance.com\",\"origin_url\":\"wss://openspeech.bytedance.com/api/v3/sauc/bigmodel\",\"redirect_times\":0,\"status\":\"FAILED\",\"traffic_annotation\":77012883},\"dns\":{\"address_list\":[\"163.181.50.229:443\",\"163.181.50.223:443\",\"163.181.50.221:443\",\"163.181.50.224:443\",\"163.181.50.226:443\",\"163.181.50.222:443\",\"163.181.50.228:443\",\"163.181.50.227:443\"],\"async_batch_httpdns\":0,\"cache_source\":6,\"cache_stale_reason\":0,\"from_stale_cache\":0,\"hijacked_error\":0,\"is_auth\":false,\"nameservers\":[],\"net_type\":0,\"preload_batch_httpdns\":0,\"queried_with_cip\":\"\",\"race_dns_stale_cache\":0,\"skip_prefer_ip_error\":0,\"source\":2},\"event\":{\"engine_startup\":1744019237.421243,\"network_change\":0.0},\"header\":{\"X-Tt-Logid\":\"2025040717474815E3294F926C737834FA\",\"server-timing\":\"inner; dur=5, cdn-cache;desc=MISS,edge;dur=653,origin;dur=14\",\"x-tt-trace-host\":\"012d6b6cae97bb056dd2540a62f7f6e52ccfb433febe931be5ddf1ac674ca77c57b7dee570790477696b7b5c17bbdd77bf841100de6d411ef8ccc26681b9799e4aee97897e0681b3a8b304853d1848fcbd9fec458fdd375cddc4a7873cdbcef3a0\",\"x-tt-trace-id\":\"00-0fa6216304269466036ad7b0703104d2-0fa6216304269466-01\",\"x-tt-trace-tag\":\"id=03;cdn-cache=miss;type=dyn\"},\"ifconfig\":{\"ipv4_reachable\":true,\"ipv6_reachable\":false},\"multinetwork\":{},\"nqe\":{\"bs_sys_state\":false,\"downlink_throughput\":2658,\"effective_net_type\":5,\"group_rtt_estimate\":[],\"http_rtt\":116,\"pending_requests\":\"1\",\"real_net_type\":2,\"tcp_rtt\":46,\"total_requests\":\"6\"},\"other\":{\"core_ver\":\"87.0.4273.1\",\"gateway\":\"0.0.0.0\",\"is_background\":false,\"is_internal_retry\":false,\"is_main_process\":true,\"libcore\":\"cronet\",\"net_thread_priority\":0,\"quic_commit\":\"6ea2111b/2023-03-16\",\"resume_time\":\"-1\",\"retry_attempts\":0,\"ttnet_build_timestamp\":\"1689596445\",\"ttnet_version\":\"4.1.127.2-tob\",\"version\":\"534436af/2023-07-17\"},\"response\":{\"cached\":false,\"code\":400,\"connection_info\":1,\"network_accessed\":true,\"received_bytes\":0,\"sent_bytes\":0,\"via_proxy\":false},\"session\":{},\"socket\":{\"address_family\":1,\"cip\":\"\",\"connection_attemps\":[],\"happy_eyeballs_result\":2,\"remote\":\"163.181.50.229:443\",\"socket_reused\":false},\"socketpool\":{\"assign_type\":0,\"consume_time\":0,\"is_backup_job\":false,\"pending_reason\":0,\"pending_time\":0,\"reuse_type\":0,\"socket_idle_time\":0},\"ssl\":{\"cert_status\":0,\"connection_status\":6296321,\"handshake_type\":1,\"is_issued_by_known_root\":true,\"server_cert_common_name\":\"*.bytedance.com\"},\"store_idc\":{\"init\":1},\"timing\":{\"detailed_duration\":{\"before_stream\":0,\"body_recv\":-1,\"cdn-cache\":\"miss\",\"connect_delay\":0,\"create_stream\":98,\"dispatch\":0,\"dns\":0,\"edge\":653,\"header_recv\":-1,\"inner\":5,\"origin\":9,\"proxy\":0,\"rtt\":43,\"security_callback\":0,\"send\":0,\"ssl\":48,\"tcp\":49,\"ttfb\":710},\"request\":{\"create_stream_finish\":1744019267.633028,\"create_stream_start\":1744019267.534112,\"duration\":812,\"http_transaction_start\":1744019267.534081,\"request_sent_time\":1744019267.633497,\"response_recv_time\":1744019268.344466,\"start_time\":1744019267.533685}},\"tnc\":{\"data\":{\"canary\":\"\",\"cold_start\":1,\"config\":\"\",\"config_updated\":0,\"local_probe_version\":\"\",\"rules\":\"\",\"source\":0,\"update_time\":\"-1\"},\"probe\":{\"cmd\":\"0\",\"get_time\":\"-1\",\"send_time\":\"-1\",\"state\":1,\"version\":\"0\"},\"request\":{\"probe_version\":\"0\",\"source\":0,\"start_time\":\"-1\",\"state\":1}},\"t "name": "make_phone_call",
"description": "拨打电话",
"parameters": {
"type": "object",
"properties": {
"contact": {
"type": "string",
"description": "联系人姓名或电话号码"
}
},
"required": [
"contact"
]
}
}
},
{
"type": "function",
"function": {
"name": "open_app",
"description": "当用户请求打开特定应用时,启动相应应用",
"parameters": {
"type": "object",
"properties": {
"app_name": {
"type": "string",
"description": "应用名称"
}
},
"required": [
"app_name"
]
}
}
}
]
}'

Loading…
Cancel
Save