feat: 添加 Agent 步骤事件与图片附件功能
- 新增 stepStarted/stepFinished 事件类型支持 - 前端实现图片附件上传和预览功能 - 后端增强工具结果存储和事件处理 - 完善相关单元测试和集成测试
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
@@ -10,6 +11,8 @@ import '../../data/models/ag_ui_event.dart';
|
||||
import '../../data/models/chat_list_item.dart';
|
||||
import '../../data/services/ag_ui_service.dart';
|
||||
|
||||
enum AgentStage { intent, execution, report }
|
||||
|
||||
class ChatState {
|
||||
final List<ChatListItem> items;
|
||||
final bool isSending;
|
||||
@@ -21,6 +24,7 @@ class ChatState {
|
||||
final String? error;
|
||||
final DateTime? oldestLoadedDate;
|
||||
final bool hasEarlierHistory;
|
||||
final AgentStage? currentStage;
|
||||
|
||||
const ChatState({
|
||||
this.items = const [],
|
||||
@@ -33,6 +37,7 @@ class ChatState {
|
||||
this.error,
|
||||
this.oldestLoadedDate,
|
||||
this.hasEarlierHistory = false,
|
||||
this.currentStage,
|
||||
});
|
||||
|
||||
bool get isLoading =>
|
||||
@@ -55,6 +60,7 @@ class ChatState {
|
||||
Object? error = _unset,
|
||||
Object? oldestLoadedDate = _unset,
|
||||
bool? hasEarlierHistory,
|
||||
Object? currentStage = _unset,
|
||||
}) {
|
||||
return ChatState(
|
||||
items: items ?? this.items,
|
||||
@@ -71,6 +77,9 @@ class ChatState {
|
||||
? this.oldestLoadedDate
|
||||
: oldestLoadedDate as DateTime?,
|
||||
hasEarlierHistory: hasEarlierHistory ?? this.hasEarlierHistory,
|
||||
currentStage: currentStage == _unset
|
||||
? this.currentStage
|
||||
: currentStage as AgentStage?,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -78,6 +87,9 @@ class ChatState {
|
||||
class ChatBloc extends Cubit<ChatState> {
|
||||
final AgUiService _service;
|
||||
final Map<String, String> _toolCallArgsBuffer = {};
|
||||
final Map<String, Uint8List> _attachmentPreviewCache = <String, Uint8List>{};
|
||||
final Map<String, Future<Uint8List?>> _attachmentPreviewInflight =
|
||||
<String, Future<Uint8List?>>{};
|
||||
|
||||
ChatBloc({AgUiService? service, IApiClient? apiClient})
|
||||
: _service =
|
||||
@@ -102,6 +114,7 @@ class ChatBloc extends Cubit<ChatState> {
|
||||
isWaitingFirstToken: true,
|
||||
isCancelling: false,
|
||||
error: null,
|
||||
currentStage: null,
|
||||
),
|
||||
);
|
||||
case AgUiEventType.runFinished:
|
||||
@@ -112,6 +125,7 @@ class ChatBloc extends Cubit<ChatState> {
|
||||
isStreaming: false,
|
||||
isCancelling: false,
|
||||
currentMessageId: null,
|
||||
currentStage: null,
|
||||
),
|
||||
);
|
||||
case AgUiEventType.runError:
|
||||
@@ -124,8 +138,13 @@ class ChatBloc extends Cubit<ChatState> {
|
||||
isCancelling: false,
|
||||
currentMessageId: null,
|
||||
error: errorEvent.message,
|
||||
currentStage: null,
|
||||
),
|
||||
);
|
||||
case AgUiEventType.stepStarted:
|
||||
_handleStepStarted(event as StepStartedEvent);
|
||||
case AgUiEventType.stepFinished:
|
||||
_handleStepFinished(event as StepFinishedEvent);
|
||||
case AgUiEventType.textMessageStart:
|
||||
_handleTextMessageStart(event as TextMessageStartEvent);
|
||||
case AgUiEventType.textMessageContent:
|
||||
@@ -151,6 +170,16 @@ class ChatBloc extends Cubit<ChatState> {
|
||||
}
|
||||
}
|
||||
|
||||
void _handleStepStarted(StepStartedEvent event) {
|
||||
emit(state.copyWith(currentStage: _stageFromName(event.stepName)));
|
||||
}
|
||||
|
||||
void _handleStepFinished(StepFinishedEvent event) {
|
||||
if (state.currentStage == _stageFromName(event.stepName)) {
|
||||
emit(state.copyWith(currentStage: null));
|
||||
}
|
||||
}
|
||||
|
||||
void _handleTextMessageStart(TextMessageStartEvent startEvent) {
|
||||
final newMessage = TextMessageItem(
|
||||
id: startEvent.messageId,
|
||||
@@ -327,6 +356,7 @@ class ChatBloc extends Cubit<ChatState> {
|
||||
content: msg.content ?? '',
|
||||
timestamp: timestamp,
|
||||
sender: MessageSender.user,
|
||||
attachments: msg.attachments ?? const [],
|
||||
);
|
||||
case 'assistant':
|
||||
return TextMessageItem(
|
||||
@@ -369,11 +399,20 @@ class ChatBloc extends Cubit<ChatState> {
|
||||
}
|
||||
|
||||
Future<void> sendMessage(String content, {List<XFile>? images}) async {
|
||||
final attachments = (images ?? const <XFile>[])
|
||||
.map(
|
||||
(image) => <String, dynamic>{
|
||||
"path": image.path,
|
||||
"mimeType": "image/*",
|
||||
},
|
||||
)
|
||||
.toList();
|
||||
final userMessage = TextMessageItem(
|
||||
id: 'user-${DateTime.now().millisecondsSinceEpoch}',
|
||||
content: content,
|
||||
timestamp: DateTime.now(),
|
||||
sender: MessageSender.user,
|
||||
attachments: attachments,
|
||||
);
|
||||
emit(
|
||||
state.copyWith(
|
||||
@@ -509,7 +548,43 @@ class ChatBloc extends Cubit<ChatState> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<Uint8List?> loadAttachmentPreview(String previewPath) async {
|
||||
final cached = _attachmentPreviewCache[previewPath];
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
final pending = _attachmentPreviewInflight[previewPath];
|
||||
if (pending != null) {
|
||||
return pending;
|
||||
}
|
||||
final future = _service
|
||||
.fetchAttachmentPreview(previewPath)
|
||||
.then((bytes) {
|
||||
_attachmentPreviewCache[previewPath] = bytes;
|
||||
return bytes;
|
||||
})
|
||||
.catchError((_) => null)
|
||||
.whenComplete(() {
|
||||
_attachmentPreviewInflight.remove(previewPath);
|
||||
});
|
||||
_attachmentPreviewInflight[previewPath] = future;
|
||||
return future;
|
||||
}
|
||||
|
||||
void clearError() {
|
||||
emit(state.copyWith(error: null));
|
||||
}
|
||||
}
|
||||
|
||||
AgentStage? _stageFromName(String value) {
|
||||
switch (value) {
|
||||
case 'intent':
|
||||
return AgentStage.intent;
|
||||
case 'execution':
|
||||
return AgentStage.execution;
|
||||
case 'report':
|
||||
return AgentStage.report;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user