207 lines
6.4 KiB
Dart
207 lines
6.4 KiB
Dart
// 负责SSE通信
|
|
import 'dart:convert';
|
|
import 'dart:io';
|
|
import 'dart:math';
|
|
|
|
import 'package:ai_chat_assistant/utils/tts_util.dart';
|
|
|
|
import '../utils/common_util.dart';
|
|
|
|
class ChatSseService {
|
|
// 缓存用户ID和会话ID
|
|
String? _cachedUserId;
|
|
String? _cachedConversationId;
|
|
|
|
HttpClient? _currentClient;
|
|
HttpClientResponse? _currentResponse;
|
|
bool _isAborted = true;
|
|
bool _isFirstData = true;
|
|
bool _isTtsStarted = false;
|
|
|
|
String? get conversationId => _cachedConversationId;
|
|
|
|
Future<void>? _startTtsFuture;
|
|
|
|
void request({
|
|
required String messageId,
|
|
required String text,
|
|
required bool isChinese,
|
|
required Function(String, String, bool) onStreamResponse,
|
|
}) async {
|
|
print("----------------------SSE Start");
|
|
_isAborted = false;
|
|
if (_cachedUserId == null) {
|
|
_cachedUserId = _generateRandomUserId(6);
|
|
print('初始化用户ID: $_cachedUserId');
|
|
}
|
|
String responseText = '';
|
|
StringBuffer buffer = StringBuffer();
|
|
// 英文分句符
|
|
final enEnders = RegExp(r'[.!?]');
|
|
// 中文分句符
|
|
final zhEnders = RegExp(r'[。!?]');
|
|
// 专有名词保护
|
|
String protectIDUNYX(String text) => text.replaceAll('ID.UNYX', 'ID_UNYX');
|
|
String restoreIDUNYX(String text) => text.replaceAll('ID_UNYX', 'ID.UNYX');
|
|
Future<void> processBuffer(bool isChinese) async {
|
|
String txt = buffer.toString();
|
|
// 先过滤 markdown 图片语法
|
|
int imgStart = txt.indexOf('![');
|
|
while (imgStart != -1) {
|
|
int imgEnd = txt.indexOf(')', imgStart);
|
|
if (imgEnd == -1) {
|
|
// 图片语法未闭合,缓存剩余部分
|
|
buffer.clear();
|
|
buffer.write(txt.substring(imgStart));
|
|
return;
|
|
}
|
|
// 移除图片语法
|
|
txt = txt.substring(0, imgStart) + txt.substring(imgEnd + 1);
|
|
imgStart = txt.indexOf('![');
|
|
}
|
|
|
|
txt = txt.replaceAll(RegExp(r'(^|\n)[ \t]*[-\*\+][ \t]+'), '\n');
|
|
// 分句符
|
|
RegExp enders = isChinese ? zhEnders : enEnders;
|
|
int lastEnd = 0;
|
|
// 本地缓存句子
|
|
List<String> sentences = [];
|
|
for (final match in enders.allMatches(txt)) {
|
|
int endIndex = match.end;
|
|
String sentence = txt.substring(lastEnd, endIndex).trim();
|
|
if (sentence.isNotEmpty) {
|
|
sentences.add(sentence);
|
|
}
|
|
lastEnd = endIndex;
|
|
}
|
|
// 只在达到完整句子时调用 TtsUtil.send
|
|
for (final s in sentences) {
|
|
String ttsStr=CommonUtil.cleanText(s, true)+"\n";
|
|
|
|
ttsStr = ttsStr.replaceAllMapped(
|
|
RegExp(r'(?<!\s)(\d+)(?!\s)'),
|
|
(m) => ' ${m.group(1)} ',
|
|
);
|
|
|
|
print("发送数据到TTS: $ttsStr");
|
|
TtsUtil.send(ttsStr);
|
|
}
|
|
// 缓存剩余不完整部分
|
|
String remain = txt.substring(lastEnd).trim();
|
|
buffer.clear();
|
|
if (remain.isNotEmpty) {
|
|
buffer.write(remain);
|
|
}
|
|
}
|
|
_currentClient = HttpClient();
|
|
try {
|
|
final chatUri = Uri.parse('http://143.64.185.20:18606/chat');
|
|
final request = await _currentClient!.postUrl(chatUri);
|
|
request.headers.set('Content-Type', 'application/json');
|
|
request.headers.set('Accept', 'text/event-stream');
|
|
final body = {
|
|
'message': text,
|
|
'user': _cachedUserId,
|
|
};
|
|
if (_cachedConversationId != null) {
|
|
body['conversation_id'] = _cachedConversationId;
|
|
}
|
|
request.add(utf8.encode(json.encode(body)));
|
|
_currentResponse = await request.close();
|
|
if (_currentResponse!.statusCode == 200) {
|
|
_startTtsFuture = startTts(isChinese);
|
|
await for (final line in _currentResponse!
|
|
.transform(utf8.decoder)
|
|
.transform(const LineSplitter())) {
|
|
if (_isAborted) {
|
|
break;
|
|
}
|
|
if (line.startsWith('data:')) {
|
|
final jsonStr = line.substring(5).trim();
|
|
if (jsonStr == '[DONE]' || jsonStr.contains('message_end')) {
|
|
TtsUtil.complete();
|
|
onStreamResponse(messageId, responseText, true);
|
|
break;
|
|
}
|
|
try {
|
|
final jsonData = json.decode(jsonStr);
|
|
if (jsonData.containsKey('conversation_id') &&
|
|
_cachedConversationId == null) {
|
|
_cachedConversationId = jsonData['conversation_id'];
|
|
}
|
|
if (jsonData['event'].toString().contains('message')) {
|
|
final textChunk =
|
|
jsonData.containsKey('answer') ? jsonData['answer'] : '';
|
|
if (_isAborted) {
|
|
break;
|
|
}
|
|
buffer.write(textChunk);
|
|
await processBuffer(isChinese);
|
|
responseText += textChunk;
|
|
onStreamResponse(messageId, responseText, false);
|
|
}
|
|
} catch (e) {
|
|
print('解析 SSE 数据出错: $e, 原始数据: $jsonStr');
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
print('SSE 连接失败,状态码: ${_currentResponse!.statusCode}');
|
|
}
|
|
} catch (e) {
|
|
// todo
|
|
} finally {
|
|
resetRequest();
|
|
}
|
|
}
|
|
|
|
Future<void> startTts(bool isChinese) async {
|
|
print("----------------------TTS Start");
|
|
if (!_isTtsStarted) {
|
|
if (await TtsUtil.start(isChinese) == true) {
|
|
_isTtsStarted = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<void> abort() async {
|
|
_isAborted = true;
|
|
TtsUtil.stop();
|
|
resetRequest();
|
|
}
|
|
|
|
void resetRequest() {
|
|
_currentResponse?.detachSocket().then((socket) {
|
|
socket.destroy();
|
|
});
|
|
_currentClient?.close(force: true);
|
|
_currentClient = null;
|
|
_currentResponse = null;
|
|
_isFirstData = true;
|
|
_isTtsStarted = false;
|
|
_startTtsFuture = null;
|
|
}
|
|
|
|
int _getCompleteTextEndIndex(String buffer) {
|
|
// 支持句号、问号、感叹号和换行符作为分割依据
|
|
final sentenceEnders = RegExp(r'[,.!?:,。!?:\n]');
|
|
final matches = sentenceEnders.allMatches(buffer);
|
|
return matches.isEmpty ? 0 : matches.last.end;
|
|
}
|
|
|
|
// 新增:重置会话方法
|
|
void resetSession() {
|
|
_cachedUserId = null;
|
|
_cachedConversationId = null;
|
|
print('SSE会话已重置');
|
|
}
|
|
|
|
String _generateRandomUserId(int length) {
|
|
const chars =
|
|
'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
|
final random = Random();
|
|
return String.fromCharCodes(Iterable.generate(
|
|
length, (_) => chars.codeUnitAt(random.nextInt(chars.length))));
|
|
}
|
|
}
|