Browse Source

add

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

3
android/app/build.gradle.kts

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

6
android/settings.gradle.kts

@ -34,10 +34,12 @@ include(":app")
include(":azure_speech")
include(":open_ai_service")
include(":volcano_speech")
include(":chat_storage_sqlite")
include(":chat_storage")
include(":jieli")
// 设置azure_speech项目的路径
project(":azure_speech").projectDir = file("../local_plugins/azure_speech/android")
project(":open_ai_service").projectDir = file("../local_plugins/open_ai_service/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: EdgeInsets.only(top: message.content.isNotEmpty ? 8.h : 0),
child: _buildLoadingIndicator(),

105
lib/data/services/chat_history_service.dart

@ -1,16 +1,13 @@
import 'dart:convert';
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import '../../modules/chat/models/message_model.dart';
import '../../core/utils/logger.dart';
import 'package:chat_storage/chat_storage.dart';
/// 聊天记录管理服务,负责保存和加载聊天记录
class ChatHistoryService extends GetxService {
final GetStorage _storage = GetStorage();
// 存储键名
static const String _historyKeyPrefix = 'chat_history_';
static const int _maxHistoryCount = 10; // 每个对话最多保存的消息数
// 最大历史记录数量
static const int _maxHistoryCount = 200; // 每个对话最多保存的消息数
@override
void onInit() {
@ -18,50 +15,95 @@ class ChatHistoryService extends GetxService {
Logger.info('ChatHistoryService 已初始化');
}
/// 生成历史记录存储键
String _getHistoryKey(String agentId) => '$_historyKeyPrefix$agentId';
/// 保存聊天记录
///
/// [agentId] 代理ID
/// [messages] 消息列表
Future<void> saveHistory(String agentId, List<Message> messages) async {
try {
// 过滤掉加载中的消息
final filteredMessages = messages.where((m) => !m.isLoading).toList();
if (messages.isEmpty) return;
// 如果消息超过最大数量,只保留最新的消息
final messagesToSave = filteredMessages.length > _maxHistoryCount
? filteredMessages.sublist(filteredMessages.length - _maxHistoryCount)
: filteredMessages;
// 获取最新的消息
final latestMessage = messages.last;
// 转换为JSON
final jsonList = messagesToSave.map((m) => m.toJson()).toList();
final jsonString = jsonEncode(jsonList);
// 如果是加载中的消息则跳过
if (latestMessage.isIntermediate) return;
// 保存到存储
await _storage.write(_getHistoryKey(agentId), jsonString);
// Logger.info('已保存聊天记录,代理: $agentId,消息数: ${messagesToSave.length}');
// 直接插入最新的消息到数据库
await ChatStorage.saveMessage(
sessionId: agentId,
message: latestMessage.content,
sender: latestMessage.role,
);
// 检查并清理过多的历史记录
await _cleanupOldMessages(agentId);
} catch (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
/// 返回消息列表,如果没有找到或解析错误则返回空列表
List<Message> loadHistory(String agentId) {
Future<List<Message>> loadHistory(String agentId) async {
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 [];
}
final jsonList = jsonDecode(jsonString) as List;
final messages = jsonList.map((json) => Message.fromJson(json)).toList();
// 将ChatStorage.ChatMessage转换为Message
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;
} catch (e) {
@ -73,7 +115,7 @@ class ChatHistoryService extends GetxService {
/// 清除特定代理的聊天记录
Future<void> clearHistory(String agentId) async {
try {
await _storage.remove(_getHistoryKey(agentId));
await ChatStorage.deleteMessages(sessionId: agentId);
Logger.info('已清除聊天记录,代理: $agentId');
} catch (e) {
Logger.error('清除聊天记录失败', e);
@ -83,12 +125,7 @@ class ChatHistoryService extends GetxService {
/// 清除所有聊天记录
Future<void> clearAllHistory() async {
try {
final keys = _storage.getKeys();
for (final key in keys) {
if (key.startsWith(_historyKeyPrefix)) {
await _storage.remove(key);
}
}
await ChatStorage.clearDatabase();
Logger.info('已清除所有聊天记录');
} catch (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 'ai_service.dart';
/// 定义流事件类型,用于区分不同类型的事件
enum StreamEventType { token, complete, error }
/// 流事件包装类
class StreamEvent {
final StreamEventType type;
final String? content;
StreamEvent(this.type, {this.content});
}
/// OpenAI服务适配器 - 连接AiService接口与OpenAIService插件
class OpenAIServiceAdapter implements AiService {
final OpenAIService _openAIService = OpenAIService();
StreamSubscription<OpenAIEvent>? _eventSubscription;
final StreamController<String> _tokenStreamController = StreamController<String>.broadcast();
bool _isProcessingStream = false;
final StreamController<StreamEvent> _tokenStreamController = StreamController<StreamEvent>.broadcast();
/// 构造函数
OpenAIServiceAdapter() {
@ -26,46 +36,51 @@ class OpenAIServiceAdapter implements AiService {
// 设置事件处理
_eventSubscription = _openAIService.eventStream.listen(
(event) {
if (!_isProcessingStream) return;
try {
switch (event.type) {
case OpenAIEventType.token:
if (event.content is String) {
_tokenStreamController.add(event.content as String);
_tokenStreamController.add(StreamEvent(
StreamEventType.token,
content: event.content as String
));
} else {
printInfo(info: '收到非字符串类型的token: ${event.content}');
}
break;
case OpenAIEventType.complete:
_isProcessingStream = false;
_tokenStreamController.add(StreamEvent(StreamEventType.complete));
break;
case OpenAIEventType.error:
_tokenStreamController.addError('未知错误: ${event.content}');
_isProcessingStream = false;
_tokenStreamController.add(StreamEvent(
StreamEventType.error,
content: '未知错误: ${event.content}'
));
break;
case OpenAIEventType.functionCall:
_tokenStreamController.addError('收到函数调用,该流仅支持文本响应');
_isProcessingStream = false;
_tokenStreamController.add(StreamEvent(
StreamEventType.error,
content: '收到函数调用,该流仅支持文本响应'
));
break;
}
} catch (e) {
printError(info: '处理事件出错: $e');
_tokenStreamController.addError('处理事件失败: $e');
_isProcessingStream = false;
_tokenStreamController.add(StreamEvent(
StreamEventType.error,
content: '处理事件失败: $e'
));
}
},
onError: (error) {
printError(info: '事件流错误: $error');
_tokenStreamController.addError('事件流错误: $error');
_isProcessingStream = false;
_tokenStreamController.add(StreamEvent(
StreamEventType.error,
content: '事件流错误: $error'
));
},
onDone: () {
printInfo(info: '事件流已关闭');
_isProcessingStream = false;
},
);
} catch (e) {
@ -147,20 +162,28 @@ class OpenAIServiceAdapter implements AiService {
// 创建用于接收token的控制器
final localController = StreamController<String>();
// 标记开始处理流
_isProcessingStream = true;
// 添加从广播流到本地流的订阅
final subscription = _tokenStreamController.stream.listen(
(token) => localController.add(token),
onError: (error) {
printError(info: '令牌流错误: $error');
localController.addError(error);
localController.close();
},
onDone: () {
printInfo(info: '令牌流已完成');
localController.close();
(streamEvent) {
switch (streamEvent.type) {
case StreamEventType.token:
if (streamEvent.content != null && !localController.isClosed) {
localController.add(streamEvent.content!);
}
break;
case StreamEventType.complete:
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) {
printError(info: '启动消息流失败: $e');
localController.addError('启动消息流失败: $e');
localController.close();
_isProcessingStream = false;
if (!localController.isClosed) {
localController.addError('启动消息流失败: $e');
localController.close();
}
throw '启动消息流失败: $e';
}
if (!started) {
printError(info: '无法启动消息流');
localController.addError('无法启动消息流');
localController.close();
_isProcessingStream = false;
if (!localController.isClosed) {
localController.addError('无法启动消息流');
localController.close();
}
throw '无法启动消息流';
}
@ -224,7 +249,6 @@ class OpenAIServiceAdapter implements AiService {
/// 释放资源
void dispose() {
try {
_isProcessingStream = false;
_eventSubscription?.cancel();
_tokenStreamController.close();
printInfo(info: 'OpenAIServiceAdapter资源已释放');
@ -232,5 +256,4 @@ class OpenAIServiceAdapter implements AiService {
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 {
// 获取ChatHistoryService实例
final chatHistoryService = Get.find<ChatHistoryService>();
if (userMessage.isEmpty) {
return;
}
// 创建用户消息和助手消息
final userMsg = Message(
role: 'user',
content: userMessage,
@ -285,14 +285,17 @@ class AndroidVoiceInteractionService extends GetxService {
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]);
// 保存更新后的历史记录
chatHistoryService.saveHistory(agentId, existingMessages);
await chatHistoryService.saveHistory(agentId, existingMessages);
} catch (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);
@ -424,16 +424,16 @@ class IosVoiceInteractionService extends GetxService {
}
// 保存聊天历史
void _saveChatHistory(Message userMsg, Message assistantMsg) {
Future<void> _saveChatHistory(Message userMsg, Message assistantMsg) async {
try {
// 加载现有历史记录
final existingMessages = _chatHistoryService.loadHistory(_agentId);
final existingMessages = await _chatHistoryService.loadHistory(_agentId);
// 添加新消息
existingMessages.addAll([userMsg, assistantMsg]);
// 保存更新后的历史记录
_chatHistoryService.saveHistory(_agentId, existingMessages);
await _chatHistoryService.saveHistory(_agentId, existingMessages);
Logger.info('保存聊天历史记录成功');
} catch (e) {

13
lib/data/services/voice_interaction_service.dart

@ -107,7 +107,7 @@ class VoiceInteractionService extends GetxService {
_eventStreamController.add(chatHistoryEvent);
// 保存聊天历史到ChatHistoryService
_saveChatHistory(agentId, userMessage, assistantMessage, DateTime.now().millisecondsSinceEpoch);
_saveChatHistory(agentId, userMessage, assistantMessage);
break;
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 {
// 检查参数有效性
if (userMessage.isEmpty) {
return;
}
final timestamp = DateTime.now().millisecondsSinceEpoch;
// 创建用户消息和助手消息
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]);
// 保存更新后的历史记录
_chatHistoryService.saveHistory(agentId, existingMessages);
await _chatHistoryService.saveHistory(agentId, existingMessages);
Logger.info('已保存聊天历史: agentId=$agentId');
} catch (e) {

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

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

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

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

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

@ -34,7 +34,7 @@ class SplashController extends GetxController {
// 进度条动画(持续2秒)
void _startLoadingAnimation() {
const totalDuration = 2000; // 动画总时长2秒
const totalDuration = 500; // 动画总时长2秒
const steps = totalDuration ~/ 50; // 40次更新(50ms间隔)
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
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',
message: '你好,这是一条测试消息',
sender: 'user',
@ -35,7 +35,7 @@ if (result['success']) {
```dart
// 获取指定会话的聊天记录,支持分页
final pagedResult = await ChatStorageSqlite.getMessages(
final pagedResult = await ChatStorage.getMessages(
sessionId: 'session_123',
page: 1, // 当前页码,从1开始
pageSize: 20, // 每页记录数
@ -60,17 +60,17 @@ for (var message in pagedResult.messages) {
```dart
// 方式1:删除指定ID的消息
final deleteResult1 = await ChatStorageSqlite.deleteMessages(
final deleteResult1 = await ChatStorage.deleteMessages(
messageIds: [1, 2, 3],
);
// 方式2:删除整个会话的所有消息
final deleteResult2 = await ChatStorageSqlite.deleteMessages(
final deleteResult2 = await ChatStorage.deleteMessages(
sessionId: 'session_123',
);
// 方式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 {
// 命名空间,对应你插件的包名(需与代码内包名保持一致)
namespace = "com.yunqiinnovation.chat_storage_sqlite"
namespace = "com.yunqiinnovation.chat_storage"
// 目标 SDK 版本
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"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.yunqiinnovation.chat_storage_sqlite">
package="com.yunqiinnovation.chat_storage">
<!-- 添加sqlite相关权限 -->
<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.database.Cursor
@ -65,14 +65,14 @@ private class ChatDatabaseHelper(context: Context) :
}
/** ChatStorageSqlitePlugin */
class ChatStorageSqlitePlugin: FlutterPlugin, MethodCallHandler {
class ChatStoragePlugin: FlutterPlugin, MethodCallHandler {
private lateinit var channel : MethodChannel
private lateinit var context: Context
private lateinit var dbHelper: ChatDatabaseHelper
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
context = flutterPluginBinding.applicationContext
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "chat_storage_sqlite")
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "chat_storage")
channel.setMethodCallHandler(this)
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聊天记录存储插件
class ChatStorageSqlite {
static const MethodChannel _channel = MethodChannel('chat_storage_sqlite');
class ChatStorage {
static const MethodChannel _channel = MethodChannel('chat_storage');
/// 保存一条聊天记录
static Future<Map<String, dynamic>> saveMessage({
@ -77,7 +77,6 @@ class ChatStorageSqlite {
'sessionId': sessionId,
'message': message,
'sender': sender,
'metadata': metadata,
});
return Map<String, dynamic>.from(result);
} catch (e) {

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

@ -1,7 +1,7 @@
name: chat_storage_sqlite
description: SQLite存储插件,用于存储聊天记录并支持分页查询
name: chat_storage
description: A Flutter plugin for storing chat messages using SQLite.
version: 0.0.1
homepage:
# homepage:
environment:
sdk: ">=2.17.0 <3.0.0"
@ -24,7 +24,7 @@ flutter:
plugin:
platforms:
android:
package: com.yunqiinnovation.chat_storage_sqlite
pluginClass: ChatStorageSqlitePlugin
package: com.yunqiinnovation.chat_storage
pluginClass: ChatStoragePlugin
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) {
companion object {
private const val TAG = "JieliBleCoreService"
private const val TAG = "JieliBleService"
// 服务和特征UUID
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.MediaType.Companion.toMediaTypeOrNull
import okhttp3.RequestBody.Companion.toRequestBody
import okio.Buffer
import org.json.JSONArray
import org.json.JSONObject
import java.io.IOException
@ -389,8 +390,8 @@ class OpenAIService(private val context: Context? = null) {
.build()
// 输出用于测试的curl命令
// val curlCommand = buildCurlCommand(request, requestBody.toString())
// Log.d(TAG, "curl command: $curlCommand")
val curlCommand = buildCurlCommand(request, requestBody.toString())
Log.d(TAG, "curl command: $curlCommand")
client.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
@ -406,81 +407,95 @@ class OpenAIService(private val context: Context? = null) {
val responseBody = response.body ?: return
val source = responseBody.source()
val buffer = Buffer()
try {
// 预取数据到缓冲区
source.request(Long.MAX_VALUE)
val bufferedSource = source.buffer
// 用于存储函数调用的各个部分
val finalToolCalls = mutableMapOf<Int, ToolCallInfo>()
val lineBuffer = StringBuilder()
while (!bufferedSource.exhausted()) {
val line = bufferedSource.readUtf8Line()?.trim() ?: continue
if (line.isEmpty()) continue
if (line.startsWith("data:")) {
val data = line.substring(5).trim()
// 处理[DONE]消息
if (data == "[DONE]" || data == "[\"DONE\"]") {
processToolCalls(finalToolCalls, callback, messages)
callback.onComplete()
break
}
// 逐块读取数据,而不是一次性预取
val BUFFER_SIZE = 8192L // 8KB的块大小
while (!Thread.currentThread().isInterrupted) {
// 读取一小块数据
val bytesRead = source.read(buffer, BUFFER_SIZE)
if (bytesRead == -1L) break // 流结束
// 处理接收到的数据
while (true) {
val line = buffer.readUtf8Line() ?: break
if (line.trim().isEmpty()) continue
try {
val jsonData = JSONObject(data)
if (line.startsWith("data:")) {
val data = line.substring(5).trim()
Log.d("OpenAIService", "$data")
// 处理消息内容
if (jsonData.has("choices")) {
val choices = jsonData.getJSONArray("choices")
if (choices.length() > 0) {
val choice = choices.getJSONObject(0)
if (choice.has("delta")) {
val delta = choice.getJSONObject("delta")
// 处理普通文本内容
if (delta.has("content")) {
val content = delta.getString("content")
callback.onToken(content)
}
// 处理[DONE]消息
if (data == "[DONE]" || data == "[\"DONE\"]") {
Log.d(TAG, "收到[DONE]消息")
val hasToolCalls = processToolCalls(finalToolCalls, callback, messages)
// 只有在没有工具调用时才认为对话真正完成
if (!hasToolCalls) {
callback.onComplete()
}
return
}
try {
val jsonData = JSONObject(data)
// 处理消息内容
if (jsonData.has("choices")) {
val choices = jsonData.getJSONArray("choices")
if (choices.length() > 0) {
val choice = choices.getJSONObject(0)
// 处理工具调用(函数调用)
if (delta.has("tool_calls")) {
val toolCalls = delta.getJSONArray("tool_calls")
for (i in 0 until toolCalls.length()) {
val toolCall = toolCalls.getJSONObject(i)
val index = toolCall.getInt("index")
// 创建或获取现有的工具调用信息
val toolCallInfo = finalToolCalls.getOrPut(index) { ToolCallInfo() }
// 更新ID
if (toolCall.has("id")) {
toolCallInfo.id = toolCall.getString("id")
}
// 更新函数信息
if (toolCall.has("function")) {
val function = toolCall.getJSONObject("function")
if (choice.has("delta")) {
val delta = choice.getJSONObject("delta")
// 处理普通文本内容
if (delta.has("content")) {
val content = delta.getString("content")
callback.onToken(content)
}
// 处理工具调用(函数调用)
if (delta.has("tool_calls")) {
val toolCalls = delta.getJSONArray("tool_calls")
for (i in 0 until toolCalls.length()) {
val toolCall = toolCalls.getJSONObject(i)
val index = toolCall.getInt("index")
// 创建或获取现有的工具调用信息
val toolCallInfo = finalToolCalls.getOrPut(index) { ToolCallInfo() }
if (function.has("name")) {
toolCallInfo.name = function.getString("name")
// 更新ID
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) {
if (toolCalls.isEmpty()) return
private fun processToolCalls(toolCalls: Map<Int, ToolCallInfo>, callback: StreamCallback, messages: JSONArray? = null): Boolean {
if (toolCalls.isEmpty()) return false
// 只处理第一个工具调用
val firstToolCall = toolCalls.entries.firstOrNull()?.value ?: return
val firstToolCall = toolCalls.entries.firstOrNull()?.value ?: return false
if (firstToolCall.isValid()) {
// 创建函数调用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
import android.content.Context
import android.os.Handler
import android.os.Looper
import android.util.Log
import androidx.annotation.NonNull
import io.flutter.embedding.engine.plugins.FlutterPlugin
@ -43,8 +45,11 @@ class OpenAIServicePlugin : FlutterPlugin, MethodCallHandler, StreamHandler {
/// 事件接收器(用于流式输出)
private var eventSink: EventSink? = null
/// 执行器(用于后台线程)
/// 执行器(用于真正需要在后台线程处理的任务)
private val executor = Executors.newSingleThreadExecutor()
/// 主线程Handler
private val mainHandler = Handler(Looper.getMainLooper())
override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
// 保存上下文
@ -98,17 +103,17 @@ class OpenAIServicePlugin : FlutterPlugin, MethodCallHandler, StreamHandler {
messages.put(JSONObject(message))
}
// 在后台线程执行请求
// 对于需要在后台执行的同步阻塞操作,仍然使用executor
executor.execute {
try {
val response = openAIService.sendMessage(messages)
// 在主线程返回结果
android.os.Handler(android.os.Looper.getMainLooper()).post {
mainHandler.post {
result.success(response)
}
} catch (e: Exception) {
// 在主线程返回错误
android.os.Handler(android.os.Looper.getMainLooper()).post {
mainHandler.post {
result.error("OPENAI_ERROR", e.message, null)
}
}
@ -130,53 +135,47 @@ class OpenAIServicePlugin : FlutterPlugin, MethodCallHandler, StreamHandler {
messages.put(JSONObject(message))
}
// 在后台线程执行请求
executor.execute {
try {
openAIService.sendMessageStream(
messages = messages,
callback = object : OpenAIService.StreamCallback {
override fun onToken(token: String) {
// 发送token事件
android.os.Handler(android.os.Looper.getMainLooper()).post {
eventSink?.success(mapOf("type" to "token", "content" to token))
}
// 不再在后台线程包装OkHttp的异步调用
try {
openAIService.sendMessageStream(
messages = messages,
callback = object : OpenAIService.StreamCallback {
override fun onToken(token: String) {
// 发送token事件
mainHandler.post {
eventSink?.success(mapOf("type" to "token", "content" to token))
}
override fun onComplete() {
// 发送完成事件
android.os.Handler(android.os.Looper.getMainLooper()).post {
eventSink?.success(mapOf("type" to "complete"))
}
}
override fun onComplete() {
// 发送完成事件
mainHandler.post {
eventSink?.success(mapOf("type" to "complete"))
}
override fun onError(e: Exception) {
// 发送错误事件
android.os.Handler(android.os.Looper.getMainLooper()).post {
eventSink?.success(mapOf("type" to "error", "content" to e.message))
}
}
override fun onError(e: Exception) {
// 发送错误事件
mainHandler.post {
eventSink?.success(mapOf("type" to "error", "content" to e.message))
}
override fun onFunctionCall(functionCall: JSONObject) {
// 发送函数调用事件
android.os.Handler(android.os.Looper.getMainLooper()).post {
val functionCallMap = functionCall.toMap()
eventSink?.success(mapOf("type" to "functionCall", "content" to functionCallMap))
}
}
override fun onFunctionCall(functionCall: JSONObject) {
// 发送函数调用事件
mainHandler.post {
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)
// 在后台线程执行请求
executor.execute {
try {
openAIService.sendFunctionCallResult(
messages = messages,
functionCall = functionCall,
functionResult = functionResult,
callback = object : OpenAIService.StreamCallback {
override fun onToken(token: String) {
// 发送token事件
android.os.Handler(android.os.Looper.getMainLooper()).post {
eventSink?.success(mapOf("type" to "token", "content" to token))
}
// 不再在后台线程包装OkHttp的异步调用
try {
openAIService.sendFunctionCallResult(
messages = messages,
functionCall = functionCall,
functionResult = functionResult,
callback = object : OpenAIService.StreamCallback {
override fun onToken(token: String) {
// 发送token事件
mainHandler.post {
eventSink?.success(mapOf("type" to "token", "content" to token))
}
override fun onComplete() {
// 发送完成事件
android.os.Handler(android.os.Looper.getMainLooper()).post {
eventSink?.success(mapOf("type" to "complete"))
}
}
override fun onComplete() {
// 发送完成事件
mainHandler.post {
eventSink?.success(mapOf("type" to "complete"))
}
override fun onError(e: Exception) {
// 发送错误事件
android.os.Handler(android.os.Looper.getMainLooper()).post {
eventSink?.success(mapOf("type" to "error", "content" to e.message))
}
}
override fun onError(e: Exception) {
// 发送错误事件
mainHandler.post {
eventSink?.success(mapOf("type" to "error", "content" to e.message))
}
override fun onFunctionCall(nestedFunctionCall: JSONObject) {
// 发送函数调用事件
android.os.Handler(android.os.Looper.getMainLooper()).post {
val functionCallMap = nestedFunctionCall.toMap()
eventSink?.success(mapOf("type" to "functionCall", "content" to functionCallMap))
}
}
override fun onFunctionCall(nestedFunctionCall: JSONObject) {
// 发送函数调用事件
mainHandler.post {
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) {
client.registerLocalFunction(
"enter_translation_mode",
"进入实时翻译模式",
"用户请求进入实时翻译模式时,启动实时翻译功能",
JSONObject("""
{
"type": "object",
@ -167,7 +167,7 @@ class SystemFunctionHandler(private val context: Context) {
private fun registerOpenAppFunction(client: MCPClient) {
client.registerLocalFunction(
"open_app",
"打开应用",
"当用户请求打开特定应用时,启动相应应用",
JSONObject("""
{
"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 {
callback.onError(OpenAIError("OpenAI服务未初始化"))
return
}
// 构建完整消息,添加系统提示
var fullMessages: [[String: Any]] = [
["role": "system", "content": systemPrompt]
]
fullMessages.append(contentsOf: messages)
// 构建请求体
var requestDict: [String: Any] = [
var requestBody: [String: Any] = [
"model": model,
"messages": fullMessages,
"temperature": 0.7,
"max_tokens": 2000,
"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]] = []
for function in registeredFunctions {
let tool: [String: Any] = [
"type": "function",
@ -268,11 +285,14 @@ public class OpenAIService {
]
tools.append(tool)
}
requestDict["tools"] = tools
if !tools.isEmpty {
requestBody["tools"] = tools
}
}
// 将请求数据转换为JSON数据
guard let jsonData = try? JSONSerialization.data(withJSONObject: requestDict) else {
// 转换为JSON数据
guard let jsonData = try? JSONSerialization.data(withJSONObject: requestBody) else {
callback.onError(OpenAIError("无法序列化请求数据"))
return
}
@ -290,102 +310,10 @@ public class OpenAIService {
request.addValue("text/event-stream", forHTTPHeaderField: "Accept")
request.httpBody = jsonData
// 用于存储函数调用的各个部分
var finalToolCalls: [Int: ToolCallInfo] = [:]
// 创建数据任务
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)")
// 忽略解析错误,继续处理其他行
}
}
}
}
}
// 创建流式会话任务
let delegate = SSEStreamDelegate(callback: callback)
let session = URLSession(configuration: .default, delegate: delegate, delegateQueue: nil)
let task = session.dataTask(with: request)
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) {
if toolCalls.isEmpty { return }
private func processToolCalls(_ toolCalls: [Int: ToolCallInfo], callback: StreamCallback) -> Bool {
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] = [
@ -463,11 +526,6 @@ public class OpenAIService {
// 回调
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
jieli:
path: local_plugins/jieli
chat_storage_sqlite:
path: local_plugins/chat_storage_sqlite
chat_storage:
path: local_plugins/chat_storage
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 '{
"model": "bot-20250405211523-l7c9r",
"messages": [
{
"role": "system",
"content": " 你是一个智能语音助手,能够简洁明了地回答用户的问题。\n时刻关心用户的情绪和需求,主动提供鼓励和温暖。\n\n语言风格活泼、亲切,能够幽默地互动,陪伴用户,缓解压力,增添生活乐趣。\n\n请始终以用户为中心,保持回应的高效性、准确性和温暖体贴,成为用户真正的灵魂伴侣。\n \n 当用户说\"退出\"、\"再见\"、\"结束对话\"等类似意图时,你应该使用exit_interaction函数来结束对话,\n 并在结束前说一句友好的告别语,例如\"再见,有需要随时找我\"。"
},
curl 'https://ark.cn-beijing.volces.com/api/v3/bots/chat/completions' \
-H "Authorization: Bearer 168deb3d-fd0c-4912-b9f1-aaee5c6743e6" \
-H 'Content-Type: application/json' \
-d '{
"model": "bot-20250405211523-l7c9r",
"stream": true,
"stream_options": {"include_usage": true},
"messages": [
{
"role": "system",
"content": " 你是一个智能语音助手,能够简洁明了地回答用户的问题。\n时刻关心用户的情绪和需求,主动提供鼓励和温暖。\n\n语言风格活泼、亲切,能够幽默地互动,陪伴用户,缓解压力,增添生活乐趣。\n\n请始终以用户为中心,保持回应的高效性、准确性和温暖体贴,成为用户真正的灵魂伴侣。\n \n 当用户说\"退出\"、\"再见\"、\"结束对话\"等类似意图时,你应该使用exit_interaction函数来结束对话,\n 并在结束前说一句友好的告别语,例如\"再见,有需要随时找我\"。"
"content": "You are a helpful assistant."
},
{
"role": "user",
"content": "退下吧。"
},
{
"role": "assistant",
"content": "",
"tool_calls": [
{
"id": "call_8k680azmfc4thqrrnwpwqxah",
"type": "function",
"function": {
"name": "exit_interaction",
"arguments": " {}"
}
}
]
},
{
"role": "tool",
"content": "{\"result\": \"已退出语音交互\"}",
"tool_call_id": "call_8k680azmfc4thqrrnwpwqxah"
"content": "Hello!"
}
],
"temperature": 0.7,
"max_tokens": 2000,
"stream": true,
"tools": [
{
"type": "function",
"function": {
"name": "exit_interaction",
"description": "退出当前语音交互",
"parameters": {
"type": "object",
"properties": {},
"required": []
}
]
}'
curl 'https://ark.cn-beijing.volces.com/api/v3/bots/chat/completions' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer 168deb3d-fd0c-4912-b9f1-aaee5c6743e6' \
-d '{
"model": "bot-20250405211523-l7c9r",
"stream": true,
"messages": [
{
"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,
"message": "Error during WebSocket handshake: Unexpected response code: 400",
"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
}
},
{
"type": "function",
"function": {
"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