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
@@ -1,3 +1,5 @@
import 'dart:typed_data';
import 'package:bloc_test/bloc_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:image_picker/image_picker.dart';
@@ -9,8 +11,17 @@ import 'package:social_app/features/chat/presentation/bloc/chat_bloc.dart';
class MockAgUiService extends AgUiService {
MockAgUiService() : super(onEvent: (_) {});
int previewCalls = 0;
@override
Future<void> sendMessage(String content, {List<XFile>? images}) async {}
@override
Future<Uint8List> fetchAttachmentPreview(String previewPath) async {
previewCalls += 1;
await Future<void>.delayed(const Duration(milliseconds: 10));
return Uint8List.fromList(<int>[1, 2, 3]);
}
}
class _ThrowingAgUiService extends AgUiService {
@@ -182,6 +193,23 @@ void main() {
],
);
blocTest<ChatBloc, ChatState>(
'step events update currentStage',
build: () => chatBloc,
act: (bloc) {
service.onEvent(StepStartedEvent(stepName: 'execution'));
service.onEvent(StepFinishedEvent(stepName: 'execution'));
},
expect: () => [
isA<ChatState>().having(
(s) => s.currentStage,
'currentStage',
AgentStage.execution,
),
isA<ChatState>().having((s) => s.currentStage, 'currentStage', isNull),
],
);
blocTest<ChatBloc, ChatState>(
'runError sets error message',
build: () => chatBloc,
@@ -325,5 +353,58 @@ void main() {
),
],
);
blocTest<ChatBloc, ChatState>(
'state snapshot user message keeps attachments',
build: () => chatBloc,
act: (bloc) {
service.onEvent(
StateSnapshotEvent(
snapshot: {
'scope': 'history_day',
'messages': [
{
'id': 'u1',
'role': 'user',
'content': '请分析这张图',
'attachments': [
{'bucket': 'b', 'path': 'p', 'mimeType': 'image/png'},
],
},
],
},
),
);
},
expect: () => [
isA<ChatState>().having(
(s) {
final item = s.items.first;
return item is TextMessageItem && item.attachments.length == 1;
},
'user attachment count',
true,
),
],
);
test(
'loadAttachmentPreview deduplicates in-flight and caches result',
() async {
final mock = service as MockAgUiService;
final results = await Future.wait<Uint8List?>([
chatBloc.loadAttachmentPreview('/api/preview/1'),
chatBloc.loadAttachmentPreview('/api/preview/1'),
]);
final secondRound = await chatBloc.loadAttachmentPreview(
'/api/preview/1',
);
expect(results.first, isNotNull);
expect(results.last, isNotNull);
expect(secondRound, isNotNull);
expect(mock.previewCalls, 1);
},
);
});
}