feat: 应用名称更新为灵可析并增强 Chat 功能

- 更新 Android/iOS 应用名称和图标为灵可析
- Chat 支持取消正在运行的 Agent 对话
- 改进 ChatBloc 状态管理(区分发送/等待/流式/取消状态)
- HomeScreen 支持外部注入 ChatBloc 和显示等待指示器
- 后端 Agent 运行服务优化(消息处理、usage 追踪)
- 补充相关单元测试和 Widget 测试
This commit is contained in:
qzl
2026-03-10 18:39:53 +08:00
parent b48f7abf72
commit 487405aa5b
50 changed files with 768 additions and 284 deletions
@@ -5,6 +5,9 @@ import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:lucide_icons/lucide_icons.dart';
import 'package:social_app/core/api/api_exception.dart';
import 'package:social_app/features/chat/data/models/ag_ui_event.dart';
import 'package:social_app/features/chat/data/services/ag_ui_service.dart';
import 'package:social_app/features/chat/presentation/bloc/chat_bloc.dart';
import 'package:social_app/features/home/data/voice_recorder.dart';
import 'package:social_app/features/home/ui/screens/home_screen.dart';
@@ -29,7 +32,26 @@ class _FakeVoiceRecorder implements VoiceRecorder {
Future<void> dispose() async {}
}
class _WaitingAgUiService extends AgUiService {
_WaitingAgUiService() : super(onEvent: (_) {});
final Completer<void> _pending = Completer<void>();
@override
Future<void> sendMessage(String content) async {
onEvent(RunStartedEvent(threadId: 't1', runId: 'r1'));
return _pending.future;
}
}
void main() {
IconData _inputActionIcon(WidgetTester tester) {
final icon = tester.widget<Icon>(
find.byKey(const ValueKey('home_input_action_icon')),
);
return icon.icon!;
}
group('HomeScreen Widget Tests', () {
testWidgets('displays input field', (WidgetTester tester) async {
await tester.pumpWidget(
@@ -79,8 +101,7 @@ void main() {
expect(fakeRecorder.started, true);
expect(find.text('正在聆听...'), findsOneWidget);
expect(find.byIcon(LucideIcons.square), findsOneWidget);
expect(find.byIcon(LucideIcons.send), findsOneWidget);
expect(_inputActionIcon(tester), LucideIcons.square);
});
testWidgets('tap send while recording transcribes and auto sends message', (
@@ -105,9 +126,9 @@ void main() {
);
await tester.pumpAndSettle();
await tester.tap(find.byIcon(LucideIcons.mic));
await tester.tap(find.byKey(const ValueKey('home_input_action_button')));
await tester.pump();
await tester.tap(find.byIcon(LucideIcons.send));
await tester.tap(find.byKey(const ValueKey('home_input_action_button')));
await tester.pump(const Duration(milliseconds: 300));
expect(sentTranscript, '语音自动发送');
@@ -127,18 +148,19 @@ void main() {
expect(filePath.endsWith('.wav'), true);
return '语音转文字结果';
},
onAutoSendTranscript: (_) async {},
),
),
);
await tester.pumpAndSettle();
await tester.tap(find.byIcon(LucideIcons.mic));
await tester.tap(find.byKey(const ValueKey('home_input_action_button')));
await tester.pump();
await tester.tap(find.byIcon(LucideIcons.square));
await tester.tap(find.byKey(const ValueKey('home_input_action_button')));
await tester.pump();
expect(find.text('语音识别中...'), findsOneWidget);
expect(find.byType(CircularProgressIndicator), findsOneWidget);
expect(find.byType(CircularProgressIndicator), findsAtLeastNWidgets(1));
});
testWidgets('tap stop shows readable unauthorized message', (
@@ -158,9 +180,9 @@ void main() {
);
await tester.pumpAndSettle();
await tester.tap(find.byIcon(LucideIcons.mic));
await tester.tap(find.byKey(const ValueKey('home_input_action_button')));
await tester.pump();
await tester.tap(find.byIcon(LucideIcons.square));
await tester.tap(find.byKey(const ValueKey('home_input_action_button')));
await tester.pump(const Duration(milliseconds: 300));
expect(find.text('请重新登录'), findsOneWidget);
@@ -182,9 +204,9 @@ void main() {
);
await tester.pumpAndSettle();
await tester.tap(find.byIcon(LucideIcons.mic));
await tester.tap(find.byKey(const ValueKey('home_input_action_button')));
await tester.pump();
await tester.tap(find.byIcon(LucideIcons.square));
await tester.tap(find.byKey(const ValueKey('home_input_action_button')));
await tester.pump(const Duration(milliseconds: 300));
expect(find.text('未识别到有效语音,请靠近麦克风并连续说话后重试'), findsOneWidget);
@@ -203,14 +225,15 @@ void main() {
voiceRecorder: fakeRecorder,
autoLoadHistory: false,
onTranscribeAudio: (_) => completer.future,
onAutoSendTranscript: (_) async {},
),
),
);
await tester.pumpAndSettle();
await tester.tap(find.byIcon(LucideIcons.mic));
await tester.tap(find.byKey(const ValueKey('home_input_action_button')));
await tester.pump();
await tester.tap(find.byIcon(LucideIcons.square));
await tester.tap(find.byKey(const ValueKey('home_input_action_button')));
await tester.pump();
expect(find.text('语音识别中...'), findsOneWidget);
@@ -237,7 +260,7 @@ void main() {
);
expect(editableBefore.widget.focusNode.hasFocus, isTrue);
await tester.tap(find.byIcon(LucideIcons.send));
await tester.tap(find.byKey(const ValueKey('home_input_action_button')));
await tester.pump();
final editableAfter = tester.state<EditableTextState>(
@@ -247,5 +270,33 @@ void main() {
await tester.pump(const Duration(milliseconds: 300));
});
testWidgets('shows stop icon and waiting indicator while waiting agent', (
WidgetTester tester,
) async {
final chatBloc = ChatBloc(service: _WaitingAgUiService());
await tester.pumpWidget(
MaterialApp(
home: HomeScreen(autoLoadHistory: false, chatBloc: chatBloc),
),
);
await tester.pumpAndSettle();
await tester.enterText(find.byType(TextField), 'hello');
await tester.pump();
await tester.tap(find.byKey(const ValueKey('home_input_action_button')));
await tester.pump();
expect(_inputActionIcon(tester), LucideIcons.square);
expect(find.text('正在思考...'), findsOneWidget);
await tester.tap(find.byKey(const ValueKey('home_input_action_button')));
await tester.pump();
expect(find.text('已停止等待回复'), findsOneWidget);
await tester.pump(const Duration(seconds: 3));
await chatBloc.close();
});
});
}