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
+54 -6
View File
@@ -12,6 +12,15 @@ class MockAgUiService extends AgUiService {
Future<void> sendMessage(String content) async {}
}
class _ThrowingAgUiService extends AgUiService {
_ThrowingAgUiService() : super(onEvent: (_) {});
@override
Future<void> sendMessage(String content) async {
throw StateError('network down');
}
}
void main() {
late ChatBloc chatBloc;
late AgUiService service;
@@ -29,6 +38,9 @@ void main() {
test('initial state is empty', () {
expect(chatBloc.state.items, isEmpty);
expect(chatBloc.state.isLoading, false);
expect(chatBloc.state.isSending, false);
expect(chatBloc.state.isWaitingFirstToken, false);
expect(chatBloc.state.isStreaming, false);
expect(chatBloc.state.currentMessageId, isNull);
expect(chatBloc.state.error, isNull);
});
@@ -40,6 +52,12 @@ void main() {
expect: () => [
isA<ChatState>()
.having((state) => state.items.length, 'items length', 1)
.having((state) => state.isSending, 'isSending', true)
.having(
(state) => state.isWaitingFirstToken,
'isWaitingFirstToken',
true,
)
.having(
(state) => state.items.first,
'first item',
@@ -56,15 +74,13 @@ void main() {
'textMessageStart event adds AI message with streaming',
build: () => chatBloc,
act: (bloc) {
bloc.emit(chatBloc.state.copyWith(isLoading: true));
bloc.emit(chatBloc.state.copyWith(isStreaming: true));
service.onEvent(
TextMessageStartEvent(messageId: 'msg_1', role: 'assistant'),
);
},
expect: () => [
isA<ChatState>()
.having((s) => s.isLoading, 'isLoading', true)
.having((s) => s.isLoading, 'isLoading', true),
isA<ChatState>().having((s) => s.isStreaming, 'isStreaming', true),
isA<ChatState>()
.having((s) => s.items.length, 'items length', 1)
.having((s) => s.currentMessageId, 'currentMessageId', 'msg_1')
@@ -128,6 +144,7 @@ void main() {
expect: () => [
isA<ChatState>()
.having((s) => s.currentMessageId, 'currentMessageId', isNull)
.having((s) => s.isStreaming, 'isStreaming', false)
.having(
(s) => (s.items.first as TextMessageItem).isStreaming,
'isStreaming',
@@ -145,6 +162,7 @@ void main() {
expect: () => [
isA<ChatState>()
.having((s) => s.isLoading, 'isLoading', true)
.having((s) => s.isWaitingFirstToken, 'isWaitingFirstToken', true)
.having((s) => s.error, 'error', isNull),
],
);
@@ -152,7 +170,7 @@ void main() {
blocTest<ChatBloc, ChatState>(
'runFinished sets isLoading to false',
build: () => chatBloc,
seed: () => const ChatState(isLoading: true),
seed: () => const ChatState(isWaitingFirstToken: true),
act: (bloc) {
service.onEvent(RunFinishedEvent(threadId: 't1', runId: 'r1'));
},
@@ -166,7 +184,7 @@ void main() {
blocTest<ChatBloc, ChatState>(
'runError sets error message',
build: () => chatBloc,
seed: () => const ChatState(isLoading: true),
seed: () => const ChatState(isWaitingFirstToken: true),
act: (bloc) {
service.onEvent(
RunErrorEvent(message: 'Something went wrong', code: 'ERR'),
@@ -175,10 +193,40 @@ void main() {
expect: () => [
isA<ChatState>()
.having((s) => s.isLoading, 'isLoading', false)
.having((s) => s.currentMessageId, 'currentMessageId', isNull)
.having((s) => s.error, 'error', 'Something went wrong'),
],
);
blocTest<ChatBloc, ChatState>(
'cancelCurrentRun exits waiting states',
build: () => chatBloc,
seed: () => const ChatState(isWaitingFirstToken: true),
act: (bloc) => bloc.cancelCurrentRun(),
expect: () => [
isA<ChatState>().having((s) => s.isCancelling, 'isCancelling', true),
isA<ChatState>()
.having((s) => s.isWaitingFirstToken, 'isWaitingFirstToken', false)
.having((s) => s.isStreaming, 'isStreaming', false)
.having((s) => s.isCancelling, 'isCancelling', false),
],
);
blocTest<ChatBloc, ChatState>(
'sendMessage failure emits error and exits waiting state',
build: () => ChatBloc(service: _ThrowingAgUiService()),
act: (bloc) => bloc.sendMessage('hello'),
expect: () => [
isA<ChatState>()
.having((s) => s.isSending, 'isSending', true)
.having((s) => s.isWaitingFirstToken, 'isWaitingFirstToken', true),
isA<ChatState>()
.having((s) => s.isSending, 'isSending', false)
.having((s) => s.isWaitingFirstToken, 'isWaitingFirstToken', false)
.having((s) => s.error, 'error', contains('network down')),
],
);
blocTest<ChatBloc, ChatState>(
'clearError removes error',
build: () => chatBloc,