feat: 添加 Agent 步骤事件与图片附件功能

- 新增 stepStarted/stepFinished 事件类型支持
- 前端实现图片附件上传和预览功能
- 后端增强工具结果存储和事件处理
- 完善相关单元测试和集成测试
This commit is contained in:
zl-q
2026-03-12 09:29:57 +08:00
parent 87215f9d41
commit 7b8865e256
45 changed files with 3869 additions and 308 deletions
@@ -9,6 +9,8 @@ class AgUiEventTypeWire {
static const runStarted = 'RUN_STARTED';
static const runFinished = 'RUN_FINISHED';
static const runError = 'RUN_ERROR';
static const stepStarted = 'STEP_STARTED';
static const stepFinished = 'STEP_FINISHED';
static const textMessageStart = 'TEXT_MESSAGE_START';
static const textMessageContent = 'TEXT_MESSAGE_CONTENT';
static const textMessageEnd = 'TEXT_MESSAGE_END';
@@ -25,6 +27,8 @@ enum AgUiEventType {
runStarted,
runFinished,
runError,
stepStarted,
stepFinished,
textMessageStart,
textMessageContent,
textMessageEnd,
@@ -43,6 +47,8 @@ const _wireToTypeMap = {
AgUiEventTypeWire.runStarted: AgUiEventType.runStarted,
AgUiEventTypeWire.runFinished: AgUiEventType.runFinished,
AgUiEventTypeWire.runError: AgUiEventType.runError,
AgUiEventTypeWire.stepStarted: AgUiEventType.stepStarted,
AgUiEventTypeWire.stepFinished: AgUiEventType.stepFinished,
AgUiEventTypeWire.textMessageStart: AgUiEventType.textMessageStart,
AgUiEventTypeWire.textMessageContent: AgUiEventType.textMessageContent,
AgUiEventTypeWire.textMessageEnd: AgUiEventType.textMessageEnd,
@@ -60,6 +66,8 @@ const _typeToWireMap = {
AgUiEventType.runStarted: AgUiEventTypeWire.runStarted,
AgUiEventType.runFinished: AgUiEventTypeWire.runFinished,
AgUiEventType.runError: AgUiEventTypeWire.runError,
AgUiEventType.stepStarted: AgUiEventTypeWire.stepStarted,
AgUiEventType.stepFinished: AgUiEventTypeWire.stepFinished,
AgUiEventType.textMessageStart: AgUiEventTypeWire.textMessageStart,
AgUiEventType.textMessageContent: AgUiEventTypeWire.textMessageContent,
AgUiEventType.textMessageEnd: AgUiEventTypeWire.textMessageEnd,
@@ -83,6 +91,8 @@ final _typeToFactory = {
AgUiEventType.runStarted: RunStartedEvent.fromJson,
AgUiEventType.runFinished: RunFinishedEvent.fromJson,
AgUiEventType.runError: RunErrorEvent.fromJson,
AgUiEventType.stepStarted: StepStartedEvent.fromJson,
AgUiEventType.stepFinished: StepFinishedEvent.fromJson,
AgUiEventType.textMessageStart: TextMessageStartEvent.fromJson,
AgUiEventType.textMessageContent: TextMessageContentEvent.fromJson,
AgUiEventType.textMessageEnd: TextMessageEndEvent.fromJson,
@@ -170,6 +180,34 @@ class RunErrorEvent extends AgUiEvent {
Map<String, dynamic> toJson() => _$RunErrorEventToJson(this);
}
@JsonSerializable()
class StepStartedEvent extends AgUiEvent {
final String stepName;
StepStartedEvent({required this.stepName})
: super(type: AgUiEventType.stepStarted);
factory StepStartedEvent.fromJson(Map<String, dynamic> json) =>
_$StepStartedEventFromJson(json);
@override
Map<String, dynamic> toJson() => _$StepStartedEventToJson(this);
}
@JsonSerializable()
class StepFinishedEvent extends AgUiEvent {
final String stepName;
StepFinishedEvent({required this.stepName})
: super(type: AgUiEventType.stepFinished);
factory StepFinishedEvent.fromJson(Map<String, dynamic> json) =>
_$StepFinishedEventFromJson(json);
@override
Map<String, dynamic> toJson() => _$StepFinishedEventToJson(this);
}
@JsonSerializable()
class TextMessageStartEvent extends AgUiEvent {
final String messageId;
@@ -310,10 +348,33 @@ class ToolCallResultEvent extends AgUiEvent {
factory ToolCallResultEvent.fromJson(Map<String, dynamic> json) {
final rawContent = json['content'];
final content = rawContent is String ? rawContent : '';
final hasStructuredFields =
json['ui'] != null || json['result'] != null || json['error'] != null;
final content = switch (rawContent) {
String value when value.trim().startsWith('{') => value,
String value when value.trim().startsWith('[') => value,
String value when hasStructuredFields => jsonEncode({
'toolName': json['toolName'],
'result': json['result'],
'error': json['error'],
'ui': json['ui'],
'content': value,
}),
String value => value,
_ => jsonEncode({
'toolName': json['toolName'],
'result': json['result'],
'error': json['error'],
'ui': json['ui'],
'content': json['content'],
}),
};
final toolCallId =
json['toolCallId'] as String? ?? json['callId'] as String? ?? '';
final messageId = json['messageId'] as String? ?? 'tool-result-$toolCallId';
return ToolCallResultEvent(
messageId: json['messageId'] as String,
toolCallId: json['toolCallId'] as String,
messageId: messageId,
toolCallId: toolCallId,
content: content,
);
}
@@ -388,6 +449,7 @@ class SnapshotMessage {
final String? toolCallId;
final UiCard? ui;
final DateTime? timestamp;
final List<Map<String, dynamic>>? attachments;
SnapshotMessage({
required this.id,
@@ -396,6 +458,7 @@ class SnapshotMessage {
this.toolCallId,
this.ui,
this.timestamp,
this.attachments,
});
factory SnapshotMessage.fromJson(Map<String, dynamic> json) =>
@@ -14,6 +14,8 @@ const _$AgUiEventTypeEnumMap = {
AgUiEventType.runStarted: 'runStarted',
AgUiEventType.runFinished: 'runFinished',
AgUiEventType.runError: 'runError',
AgUiEventType.stepStarted: 'stepStarted',
AgUiEventType.stepFinished: 'stepFinished',
AgUiEventType.textMessageStart: 'textMessageStart',
AgUiEventType.textMessageContent: 'textMessageContent',
AgUiEventType.textMessageEnd: 'textMessageEnd',
@@ -53,6 +55,18 @@ RunErrorEvent _$RunErrorEventFromJson(Map<String, dynamic> json) =>
Map<String, dynamic> _$RunErrorEventToJson(RunErrorEvent instance) =>
<String, dynamic>{'message': instance.message, 'code': instance.code};
StepStartedEvent _$StepStartedEventFromJson(Map<String, dynamic> json) =>
StepStartedEvent(stepName: json['stepName'] as String);
Map<String, dynamic> _$StepStartedEventToJson(StepStartedEvent instance) =>
<String, dynamic>{'stepName': instance.stepName};
StepFinishedEvent _$StepFinishedEventFromJson(Map<String, dynamic> json) =>
StepFinishedEvent(stepName: json['stepName'] as String);
Map<String, dynamic> _$StepFinishedEventToJson(StepFinishedEvent instance) =>
<String, dynamic>{'stepName': instance.stepName};
TextMessageStartEvent _$TextMessageStartEventFromJson(
Map<String, dynamic> json,
) => TextMessageStartEvent(
@@ -170,6 +184,9 @@ SnapshotMessage _$SnapshotMessageFromJson(Map<String, dynamic> json) =>
timestamp: json['timestamp'] == null
? null
: DateTime.parse(json['timestamp'] as String),
attachments: (json['attachments'] as List<dynamic>?)
?.whereType<Map<String, dynamic>>()
.toList(),
);
Map<String, dynamic> _$SnapshotMessageToJson(SnapshotMessage instance) =>
@@ -180,4 +197,5 @@ Map<String, dynamic> _$SnapshotMessageToJson(SnapshotMessage instance) =>
'toolCallId': instance.toolCallId,
'ui': instance.ui,
'timestamp': instance.timestamp?.toIso8601String(),
'attachments': instance.attachments,
};
@@ -22,6 +22,7 @@ class TextMessageItem extends ChatListItem {
@override
final MessageSender sender;
final bool isStreaming;
final List<Map<String, dynamic>> attachments;
TextMessageItem({
required this.id,
@@ -29,6 +30,7 @@ class TextMessageItem extends ChatListItem {
required this.timestamp,
required this.sender,
this.isStreaming = false,
this.attachments = const [],
});
@override
@@ -40,12 +42,14 @@ class TextMessageItem extends ChatListItem {
DateTime? timestamp,
MessageSender? sender,
bool? isStreaming,
List<Map<String, dynamic>>? attachments,
}) => TextMessageItem(
id: id ?? this.id,
content: content ?? this.content,
timestamp: timestamp ?? this.timestamp,
sender: sender ?? this.sender,
isStreaming: isStreaming ?? this.isStreaming,
attachments: attachments ?? this.attachments,
);
}