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
@@ -11,7 +11,11 @@ import '../../data/services/ag_ui_service.dart';
class ChatState {
final List<ChatListItem> items;
final bool isLoading;
final bool isSending;
final bool isWaitingFirstToken;
final bool isStreaming;
final bool isCancelling;
final bool isLoadingHistory;
final String? currentMessageId;
final String? error;
final DateTime? oldestLoadedDate;
@@ -19,18 +23,33 @@ class ChatState {
const ChatState({
this.items = const [],
this.isLoading = false,
this.isSending = false,
this.isWaitingFirstToken = false,
this.isStreaming = false,
this.isCancelling = false,
this.isLoadingHistory = false,
this.currentMessageId,
this.error,
this.oldestLoadedDate,
this.hasEarlierHistory = false,
});
bool get isLoading =>
isSending ||
isWaitingFirstToken ||
isStreaming ||
isCancelling ||
isLoadingHistory;
static const _unset = Object();
ChatState copyWith({
List<ChatListItem>? items,
bool? isLoading,
bool? isSending,
bool? isWaitingFirstToken,
bool? isStreaming,
bool? isCancelling,
bool? isLoadingHistory,
Object? currentMessageId = _unset,
Object? error = _unset,
Object? oldestLoadedDate = _unset,
@@ -38,7 +57,11 @@ class ChatState {
}) {
return ChatState(
items: items ?? this.items,
isLoading: isLoading ?? this.isLoading,
isSending: isSending ?? this.isSending,
isWaitingFirstToken: isWaitingFirstToken ?? this.isWaitingFirstToken,
isStreaming: isStreaming ?? this.isStreaming,
isCancelling: isCancelling ?? this.isCancelling,
isLoadingHistory: isLoadingHistory ?? this.isLoadingHistory,
currentMessageId: currentMessageId == _unset
? this.currentMessageId
: currentMessageId as String?,
@@ -72,12 +95,36 @@ class ChatBloc extends Cubit<ChatState> {
void _handleEvent(AgUiEvent event) {
switch (event.type) {
case AgUiEventType.runStarted:
emit(state.copyWith(isLoading: true, error: null));
emit(
state.copyWith(
isSending: false,
isWaitingFirstToken: true,
isCancelling: false,
error: null,
),
);
case AgUiEventType.runFinished:
emit(state.copyWith(isLoading: false, currentMessageId: null));
emit(
state.copyWith(
isSending: false,
isWaitingFirstToken: false,
isStreaming: false,
isCancelling: false,
currentMessageId: null,
),
);
case AgUiEventType.runError:
final errorEvent = event as RunErrorEvent;
emit(state.copyWith(isLoading: false, error: errorEvent.message));
emit(
state.copyWith(
isSending: false,
isWaitingFirstToken: false,
isStreaming: false,
isCancelling: false,
currentMessageId: null,
error: errorEvent.message,
),
);
case AgUiEventType.textMessageStart:
_handleTextMessageStart(event as TextMessageStartEvent);
case AgUiEventType.textMessageContent:
@@ -115,6 +162,8 @@ class ChatBloc extends Cubit<ChatState> {
state.copyWith(
items: [...state.items, newMessage],
currentMessageId: startEvent.messageId,
isWaitingFirstToken: false,
isStreaming: true,
),
);
}
@@ -136,7 +185,13 @@ class ChatBloc extends Cubit<ChatState> {
}
return item;
}).toList();
emit(state.copyWith(items: updatedItems, currentMessageId: null));
emit(
state.copyWith(
items: updatedItems,
currentMessageId: null,
isStreaming: false,
),
);
}
void _handleToolCallStart(ToolCallStartEvent startEvent) {
@@ -319,20 +374,50 @@ class ChatBloc extends Cubit<ChatState> {
timestamp: DateTime.now(),
sender: MessageSender.user,
);
emit(state.copyWith(items: [...state.items, userMessage]));
await _service.sendMessage(content);
emit(
state.copyWith(
items: [...state.items, userMessage],
isSending: true,
isWaitingFirstToken: true,
isStreaming: false,
isCancelling: false,
error: null,
),
);
try {
await _service.sendMessage(content);
} catch (error) {
emit(
state.copyWith(
isSending: false,
isWaitingFirstToken: false,
isStreaming: false,
isCancelling: false,
error: error.toString(),
),
);
}
}
Future<void> loadHistory() async {
if (state.isLoading) return;
await _service.loadHistory();
if (state.isLoadingHistory) return;
emit(state.copyWith(isLoadingHistory: true));
try {
await _service.loadHistory();
} finally {
emit(state.copyWith(isLoadingHistory: false));
}
}
Future<void> loadMoreHistory() async {
if (state.isLoading || !state.hasEarlierHistory) return;
if (state.isLoadingHistory || !state.hasEarlierHistory) return;
if (state.oldestLoadedDate == null) return;
await _service.loadHistory(beforeDate: state.oldestLoadedDate);
emit(state.copyWith(isLoadingHistory: true));
try {
await _service.loadHistory(beforeDate: state.oldestLoadedDate);
} finally {
emit(state.copyWith(isLoadingHistory: false));
}
}
Future<void> approveToolCall(String toolCallId) async {
@@ -355,7 +440,16 @@ class ChatBloc extends Cubit<ChatState> {
}
return item;
}).toList();
emit(state.copyWith(items: updatedItems, isLoading: true, error: null));
emit(
state.copyWith(
items: updatedItems,
isSending: false,
isWaitingFirstToken: true,
isStreaming: false,
isCancelling: false,
error: null,
),
);
try {
await _service.approveToolCall(
toolCallId: target.callId,
@@ -375,7 +469,10 @@ class ChatBloc extends Cubit<ChatState> {
emit(
state.copyWith(
items: failedItems,
isLoading: false,
isSending: false,
isWaitingFirstToken: false,
isStreaming: false,
isCancelling: false,
error: error.toString(),
),
);
@@ -386,6 +483,31 @@ class ChatBloc extends Cubit<ChatState> {
return _service.transcribeAudio(filePath);
}
Future<bool> cancelCurrentRun() async {
if (!(state.isWaitingFirstToken ||
state.isStreaming ||
state.isCancelling)) {
return false;
}
emit(state.copyWith(isCancelling: true, error: null));
try {
await _service.cancelCurrentRun();
emit(
state.copyWith(
isSending: false,
isWaitingFirstToken: false,
isStreaming: false,
isCancelling: false,
currentMessageId: null,
),
);
return true;
} catch (error) {
emit(state.copyWith(isCancelling: false, error: error.toString()));
return false;
}
}
void clearError() {
emit(state.copyWith(error: null));
}