refactor: 重构聊天模块支持 SSE 断线重连及用户上下文隔离
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
// ignore_for_file: invalid_use_of_protected_member, invalid_use_of_visible_for_testing_member
|
||||
|
||||
part of 'chat_bloc.dart';
|
||||
|
||||
extension _ChatBlocSend on ChatBloc {
|
||||
Future<void> _sendMessage(String content, {List<XFile>? images}) async {
|
||||
final epoch = _sessionEpoch;
|
||||
final assistantBaselineAtSend = chatBlocLatestAssistantTimestamp(
|
||||
state.items,
|
||||
);
|
||||
final sendStartedAt = DateTime.now();
|
||||
final messageId = 'user-${sendStartedAt.millisecondsSinceEpoch}';
|
||||
final localEchoAttachments = (images ?? const <XFile>[])
|
||||
.map(
|
||||
(image) => <String, dynamic>{
|
||||
'path': image.path,
|
||||
'mimeType': image.mimeType ?? 'image/jpeg',
|
||||
'uploading': true,
|
||||
},
|
||||
)
|
||||
.toList();
|
||||
final localEcho = TextMessageItem(
|
||||
id: messageId,
|
||||
content: content,
|
||||
timestamp: sendStartedAt,
|
||||
sender: MessageSender.user,
|
||||
isLocalEcho: true,
|
||||
attachments: localEchoAttachments,
|
||||
);
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
items: [...state.items, localEcho],
|
||||
isSending: true,
|
||||
isWaitingFirstToken: true,
|
||||
isStreaming: false,
|
||||
isCancelling: false,
|
||||
error: null,
|
||||
),
|
||||
);
|
||||
|
||||
try {
|
||||
final uploadInputs = await Future.wait(
|
||||
(images ?? const <XFile>[]).map(
|
||||
(image) async => AttachmentUploadInput(
|
||||
name: image.name,
|
||||
mimeType: image.mimeType ?? 'image/jpeg',
|
||||
bytes: await image.readAsBytes(),
|
||||
localPath: image.path,
|
||||
),
|
||||
),
|
||||
);
|
||||
final sendResult = await _service.sendMessage(
|
||||
content,
|
||||
attachments: uploadInputs,
|
||||
);
|
||||
if (epoch != _sessionEpoch) {
|
||||
return;
|
||||
}
|
||||
_syncUploadedAttachments(
|
||||
messageId: messageId,
|
||||
uploadedAttachments: sendResult.uploadedAttachments,
|
||||
);
|
||||
} catch (error) {
|
||||
if (epoch != _sessionEpoch) {
|
||||
return;
|
||||
}
|
||||
final sseClosedBeforeTerminal = chatBlocIsSseClosedBeforeTerminalError(
|
||||
error,
|
||||
);
|
||||
var recoveredFromHistory = false;
|
||||
if (sseClosedBeforeTerminal) {
|
||||
recoveredFromHistory = await _recoverFromAbnormalSseClose(
|
||||
epoch: epoch,
|
||||
localEchoMessage: localEcho,
|
||||
sendStartedAt: sendStartedAt,
|
||||
assistantBaselineAtSend: assistantBaselineAtSend,
|
||||
);
|
||||
}
|
||||
if (epoch != _sessionEpoch) {
|
||||
return;
|
||||
}
|
||||
_markAttachmentUploadDone(messageId);
|
||||
emit(
|
||||
state.copyWith(
|
||||
isSending: false,
|
||||
isWaitingFirstToken: false,
|
||||
isStreaming: false,
|
||||
isCancelling: false,
|
||||
currentStage: null,
|
||||
error: sseClosedBeforeTerminal
|
||||
? (recoveredFromHistory
|
||||
? null
|
||||
: L10n.current.chatSseInterruptedRetry)
|
||||
: error.toString(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> _recoverFromAbnormalSseClose({
|
||||
required int epoch,
|
||||
required TextMessageItem localEchoMessage,
|
||||
required DateTime sendStartedAt,
|
||||
required DateTime? assistantBaselineAtSend,
|
||||
}) async {
|
||||
try {
|
||||
final deadline = DateTime.now().add(_recoveryTimeout);
|
||||
|
||||
while (DateTime.now().isBefore(deadline)) {
|
||||
final snapshot = await _service.loadHistory(forceRefresh: true);
|
||||
if (epoch != _sessionEpoch) {
|
||||
return false;
|
||||
}
|
||||
final merged = _mergeWithHistory(state.items, snapshot.messages);
|
||||
emit(
|
||||
state.copyWith(
|
||||
items: merged,
|
||||
oldestLoadedDate: _extractDateFromItems(merged),
|
||||
hasEarlierHistory: snapshot.hasMore,
|
||||
),
|
||||
);
|
||||
|
||||
final persistedUserTimestamp = chatBlocFindPersistedUserTimestamp(
|
||||
merged,
|
||||
localEchoMessage,
|
||||
sendStartedAt,
|
||||
);
|
||||
final assistantCaughtUp = chatBlocHasAssistantAfterBaseline(
|
||||
merged,
|
||||
assistantBaselineAtSend,
|
||||
notBefore: persistedUserTimestamp ?? sendStartedAt,
|
||||
);
|
||||
if (persistedUserTimestamp != null && assistantCaughtUp) {
|
||||
return true;
|
||||
}
|
||||
|
||||
final remaining = deadline.difference(DateTime.now());
|
||||
if (remaining <= Duration.zero) {
|
||||
break;
|
||||
}
|
||||
await Future<void>.delayed(
|
||||
remaining < _recoveryPollInterval ? remaining : _recoveryPollInterval,
|
||||
);
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
List<ChatListItem> _mergeWithHistory(
|
||||
List<ChatListItem> localItems,
|
||||
List<HistoryMessage> historyMessages,
|
||||
) {
|
||||
final historyItems = _convertHistoryMessages(historyMessages);
|
||||
return ChatTimelineReconciler.merge(
|
||||
localItems: localItems,
|
||||
remoteItems: historyItems,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user