refactor(chat): 重构聊天模块并集成历史消息加载功能
- 删除冗余的 chat_history_repository 和 home_mock_data - 简化 ag_ui_event fromJson 使用工厂映射表 - 提取 ChatBloc 事件处理方法,添加 loadHistory/loadMoreHistory - HomeScreen 集成 ChatBloc 实现历史消息加载和下拉刷新 - 更新 AGENTS.md 文档约束
This commit is contained in:
@@ -34,6 +34,7 @@ enum AgUiEventType {
|
||||
unknown,
|
||||
}
|
||||
|
||||
// wire 类型到枚举的映射
|
||||
const _wireToTypeMap = {
|
||||
AgUiEventTypeWire.runStarted: AgUiEventType.runStarted,
|
||||
AgUiEventTypeWire.runFinished: AgUiEventType.runFinished,
|
||||
@@ -49,6 +50,7 @@ const _wireToTypeMap = {
|
||||
AgUiEventTypeWire.messagesSnapshot: AgUiEventType.messagesSnapshot,
|
||||
};
|
||||
|
||||
// 枚举到 wire 类型的映射
|
||||
const _typeToWireMap = {
|
||||
AgUiEventType.runStarted: AgUiEventTypeWire.runStarted,
|
||||
AgUiEventType.runFinished: AgUiEventTypeWire.runFinished,
|
||||
@@ -70,6 +72,23 @@ AgUiEventType agUiEventTypeFromWire(String wire) =>
|
||||
|
||||
String agUiEventTypeToWire(AgUiEventType type) => _typeToWireMap[type] ?? '';
|
||||
|
||||
// 类型到工厂函数的映射,用于简化 fromJson
|
||||
final _typeToFactory = {
|
||||
AgUiEventType.runStarted: RunStartedEvent.fromJson,
|
||||
AgUiEventType.runFinished: RunFinishedEvent.fromJson,
|
||||
AgUiEventType.runError: RunErrorEvent.fromJson,
|
||||
AgUiEventType.textMessageStart: TextMessageStartEvent.fromJson,
|
||||
AgUiEventType.textMessageContent: TextMessageContentEvent.fromJson,
|
||||
AgUiEventType.textMessageEnd: TextMessageEndEvent.fromJson,
|
||||
AgUiEventType.toolCallStart: ToolCallStartEvent.fromJson,
|
||||
AgUiEventType.toolCallArgs: ToolCallArgsEvent.fromJson,
|
||||
AgUiEventType.toolCallEnd: ToolCallEndEvent.fromJson,
|
||||
AgUiEventType.toolCallResult: ToolCallResultEvent.fromJson,
|
||||
AgUiEventType.toolCallError: ToolCallErrorEvent.fromJson,
|
||||
AgUiEventType.messagesSnapshot: MessagesSnapshotEvent.fromJson,
|
||||
AgUiEventType.unknown: UnknownAgUiEvent.fromJson,
|
||||
};
|
||||
|
||||
@JsonSerializable()
|
||||
class AgUiEvent {
|
||||
final AgUiEventType type;
|
||||
@@ -79,35 +98,7 @@ class AgUiEvent {
|
||||
factory AgUiEvent.fromJson(Map<String, dynamic> json) {
|
||||
final typeStr = json['type'] as String? ?? '';
|
||||
final type = agUiEventTypeFromWire(typeStr);
|
||||
|
||||
switch (type) {
|
||||
case AgUiEventType.runStarted:
|
||||
return RunStartedEvent.fromJson(json);
|
||||
case AgUiEventType.runFinished:
|
||||
return RunFinishedEvent.fromJson(json);
|
||||
case AgUiEventType.runError:
|
||||
return RunErrorEvent.fromJson(json);
|
||||
case AgUiEventType.textMessageStart:
|
||||
return TextMessageStartEvent.fromJson(json);
|
||||
case AgUiEventType.textMessageContent:
|
||||
return TextMessageContentEvent.fromJson(json);
|
||||
case AgUiEventType.textMessageEnd:
|
||||
return TextMessageEndEvent.fromJson(json);
|
||||
case AgUiEventType.toolCallStart:
|
||||
return ToolCallStartEvent.fromJson(json);
|
||||
case AgUiEventType.toolCallArgs:
|
||||
return ToolCallArgsEvent.fromJson(json);
|
||||
case AgUiEventType.toolCallEnd:
|
||||
return ToolCallEndEvent.fromJson(json);
|
||||
case AgUiEventType.toolCallResult:
|
||||
return ToolCallResultEvent.fromJson(json);
|
||||
case AgUiEventType.toolCallError:
|
||||
return ToolCallErrorEvent.fromJson(json);
|
||||
case AgUiEventType.messagesSnapshot:
|
||||
return MessagesSnapshotEvent.fromJson(json);
|
||||
case AgUiEventType.unknown:
|
||||
return UnknownAgUiEvent.fromJson(json);
|
||||
}
|
||||
return _typeToFactory[type]?.call(json) ?? UnknownAgUiEvent.fromJson(json);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => _$AgUiEventToJson(this);
|
||||
@@ -322,6 +313,7 @@ class SnapshotMessage {
|
||||
final String? content;
|
||||
final String? toolCallId;
|
||||
final UiCard? ui;
|
||||
final DateTime? timestamp;
|
||||
|
||||
SnapshotMessage({
|
||||
required this.id,
|
||||
@@ -329,6 +321,7 @@ class SnapshotMessage {
|
||||
this.content,
|
||||
this.toolCallId,
|
||||
this.ui,
|
||||
this.timestamp,
|
||||
});
|
||||
|
||||
factory SnapshotMessage.fromJson(Map<String, dynamic> json) =>
|
||||
|
||||
@@ -25,6 +25,7 @@ const _$AgUiEventTypeEnumMap = {
|
||||
AgUiEventType.toolCallEnd: 'toolCallEnd',
|
||||
AgUiEventType.toolCallResult: 'toolCallResult',
|
||||
AgUiEventType.toolCallError: 'toolCallError',
|
||||
AgUiEventType.messagesSnapshot: 'messagesSnapshot',
|
||||
AgUiEventType.unknown: 'unknown',
|
||||
};
|
||||
|
||||
@@ -157,3 +158,39 @@ Map<String, dynamic> _$ToolCallErrorEventToJson(ToolCallErrorEvent instance) =>
|
||||
'error': instance.error,
|
||||
'code': instance.code,
|
||||
};
|
||||
|
||||
MessagesSnapshotEvent _$MessagesSnapshotEventFromJson(
|
||||
Map<String, dynamic> json,
|
||||
) => MessagesSnapshotEvent(
|
||||
messages: (json['messages'] as List<dynamic>)
|
||||
.map((e) => SnapshotMessage.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$MessagesSnapshotEventToJson(
|
||||
MessagesSnapshotEvent instance,
|
||||
) => <String, dynamic>{'messages': instance.messages};
|
||||
|
||||
SnapshotMessage _$SnapshotMessageFromJson(Map<String, dynamic> json) =>
|
||||
SnapshotMessage(
|
||||
id: json['id'] as String,
|
||||
role: json['role'] as String,
|
||||
content: json['content'] as String?,
|
||||
toolCallId: json['toolCallId'] as String?,
|
||||
ui: json['ui'] == null
|
||||
? null
|
||||
: UiCard.fromJson(json['ui'] as Map<String, dynamic>),
|
||||
timestamp: json['timestamp'] == null
|
||||
? null
|
||||
: DateTime.parse(json['timestamp'] as String),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$SnapshotMessageToJson(SnapshotMessage instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'role': instance.role,
|
||||
'content': instance.content,
|
||||
'toolCallId': instance.toolCallId,
|
||||
'ui': instance.ui,
|
||||
'timestamp': instance.timestamp?.toIso8601String(),
|
||||
};
|
||||
|
||||
@@ -21,7 +21,7 @@ Map<String, dynamic> _$ToolResultToJson(ToolResult instance) =>
|
||||
|
||||
UiCard _$UiCardFromJson(Map<String, dynamic> json) => UiCard(
|
||||
cardType: json['type'] as String,
|
||||
schemaVersion: json['version'] as String? ?? 'v1',
|
||||
schemaVersion: json['version'] as String? ?? _defaultSchemaVersion,
|
||||
data: json['data'] as Map<String, dynamic>,
|
||||
actions: (json['actions'] as List<dynamic>?)
|
||||
?.map((e) => CardAction.fromJson(e as Map<String, dynamic>))
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
import 'dart:convert';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class ChatHistoryRepository {
|
||||
static const String _messagesKey = 'chat_messages_';
|
||||
static const String _lastRunIdKey = 'chat_last_run_id_';
|
||||
static const String _calendarEventsKey = 'calendar_events';
|
||||
|
||||
final String threadId;
|
||||
|
||||
ChatHistoryRepository({this.threadId = 'default'});
|
||||
|
||||
String get _msgKey => '$_messagesKey$threadId';
|
||||
String get _runIdKey => '$_lastRunIdKey$threadId';
|
||||
|
||||
Future<void> saveMessages(List<Map<String, dynamic>> messages) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(_msgKey, jsonEncode(messages));
|
||||
}
|
||||
|
||||
Future<List<Map<String, dynamic>>?> loadMessages() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final data = prefs.getString(_msgKey);
|
||||
if (data == null) return null;
|
||||
final list = jsonDecode(data) as List;
|
||||
return list.cast<Map<String, dynamic>>();
|
||||
}
|
||||
|
||||
Future<void> saveLastRunId(String runId) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(_runIdKey, runId);
|
||||
}
|
||||
|
||||
Future<String?> loadLastRunId() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getString(_runIdKey);
|
||||
}
|
||||
|
||||
Future<void> clear() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.remove(_msgKey);
|
||||
await prefs.remove(_runIdKey);
|
||||
}
|
||||
|
||||
Future<void> saveCalendarEvent(Map<String, dynamic> event) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final eventsJson = prefs.getString(_calendarEventsKey);
|
||||
final events = eventsJson != null
|
||||
? jsonDecode(eventsJson) as Map<String, dynamic>
|
||||
: <String, dynamic>{};
|
||||
events[event['id']] = event;
|
||||
await prefs.setString(_calendarEventsKey, jsonEncode(events));
|
||||
}
|
||||
|
||||
Future<List<Map<String, dynamic>>> loadCalendarEvents() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final eventsJson = prefs.getString(_calendarEventsKey);
|
||||
if (eventsJson == null) return [];
|
||||
final events = jsonDecode(eventsJson) as Map<String, dynamic>;
|
||||
return events.values.cast<Map<String, dynamic>>().toList();
|
||||
}
|
||||
}
|
||||
@@ -44,8 +44,6 @@ class AgUiService {
|
||||
Future<void> loadHistory({DateTime? beforeDate}) async {
|
||||
if (Env.isMockApi) {
|
||||
await _mockLoadHistory(beforeDate: beforeDate);
|
||||
} else {
|
||||
throw UnimplementedError('Real API not implemented');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,8 +56,10 @@ class AgUiService {
|
||||
final runId = '$_runIdPrefix${DateTime.now().millisecondsSinceEpoch}';
|
||||
|
||||
onEvent(RunStartedEvent(threadId: threadId, runId: runId));
|
||||
await Future.delayed(const Duration(milliseconds: 10));
|
||||
|
||||
DateTime targetDate;
|
||||
// Determine target date, end early if no earlier history
|
||||
final DateTime targetDate;
|
||||
if (beforeDate != null) {
|
||||
final prevDate = _historyService.getPreviousDay(beforeDate);
|
||||
if (prevDate == null) {
|
||||
@@ -72,9 +72,8 @@ class AgUiService {
|
||||
}
|
||||
|
||||
final messages = _historyService.getHistoryForDay(targetDate);
|
||||
|
||||
onEvent(MessagesSnapshotEvent(messages: messages));
|
||||
|
||||
await Future.delayed(const Duration(milliseconds: 10));
|
||||
onEvent(RunFinishedEvent(threadId: threadId, runId: runId));
|
||||
}
|
||||
|
||||
|
||||
@@ -6,40 +6,35 @@ class MockHistoryService {
|
||||
factory MockHistoryService() => _instance;
|
||||
MockHistoryService._internal();
|
||||
|
||||
/// Normalize DateTime to date-only (midnight)
|
||||
DateTime _toDateOnly(DateTime date) =>
|
||||
DateTime(date.year, date.month, date.day);
|
||||
|
||||
List<SnapshotMessage> getHistoryForDay(DateTime date) {
|
||||
final dayStart = DateTime(date.year, date.month, date.day);
|
||||
final dayStart = _toDateOnly(date);
|
||||
final allHistory = _generateAllHistory();
|
||||
|
||||
return allHistory.where((msg) {
|
||||
if (msg.ui != null) {
|
||||
final data = msg.ui!.data;
|
||||
final startAtStr = data['startAt'] as String?;
|
||||
if (startAtStr != null) {
|
||||
try {
|
||||
final startAt = DateTime.parse(startAtStr);
|
||||
final msgDate = DateTime(startAt.year, startAt.month, startAt.day);
|
||||
return msgDate == dayStart;
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
if (msg.timestamp == null) return false;
|
||||
final msgDate = _toDateOnly(msg.timestamp!);
|
||||
return msgDate == dayStart;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
DateTime? getLatestHistoryDate() {
|
||||
final now = DateTime.now();
|
||||
return DateTime(now.year, now.month, now.day);
|
||||
final allHistory = _generateAllHistory();
|
||||
if (allHistory.isEmpty) return null;
|
||||
|
||||
return allHistory
|
||||
.where((msg) => msg.timestamp != null)
|
||||
.map((msg) => _toDateOnly(msg.timestamp!))
|
||||
.reduce((a, b) => a.isAfter(b) ? a : b);
|
||||
}
|
||||
|
||||
DateTime? getPreviousDay(DateTime currentDate) {
|
||||
final allDates = _getAllHistoryDates();
|
||||
final sortedDates = allDates.toList()..sort((a, b) => b.compareTo(a));
|
||||
|
||||
final currentDateOnly = DateTime(
|
||||
currentDate.year,
|
||||
currentDate.month,
|
||||
currentDate.day,
|
||||
);
|
||||
final currentDateOnly = _toDateOnly(currentDate);
|
||||
|
||||
for (final date in sortedDates) {
|
||||
if (date.isBefore(currentDateOnly)) {
|
||||
@@ -51,29 +46,35 @@ class MockHistoryService {
|
||||
|
||||
bool hasEarlierHistory(DateTime fromDate) {
|
||||
final allDates = _getAllHistoryDates();
|
||||
final fromDateOnly = DateTime(fromDate.year, fromDate.month, fromDate.day);
|
||||
final fromDateOnly = _toDateOnly(fromDate);
|
||||
|
||||
return allDates.any((date) => date.isBefore(fromDateOnly));
|
||||
}
|
||||
|
||||
Set<DateTime> _getAllHistoryDates() {
|
||||
final now = DateTime.now();
|
||||
final today = DateTime(now.year, now.month, now.day);
|
||||
final today = _toDateOnly(now);
|
||||
final yesterday = today.subtract(const Duration(days: 1));
|
||||
return {today, yesterday};
|
||||
}
|
||||
|
||||
List<SnapshotMessage> _generateAllHistory() {
|
||||
final now = DateTime.now();
|
||||
final today = DateTime(now.year, now.month, now.day);
|
||||
final today = _toDateOnly(now);
|
||||
final yesterday = today.subtract(const Duration(days: 1));
|
||||
|
||||
return [
|
||||
SnapshotMessage(id: 'hist-m1', role: 'user', content: '明天提醒我开会'),
|
||||
SnapshotMessage(
|
||||
id: 'hist-m1',
|
||||
role: 'user',
|
||||
content: '明天提醒我开会',
|
||||
timestamp: today.add(const Duration(hours: 10)),
|
||||
),
|
||||
SnapshotMessage(
|
||||
id: 'hist-t1',
|
||||
role: 'tool',
|
||||
toolCallId: 'hist-tc1',
|
||||
timestamp: today.add(const Duration(hours: 10)),
|
||||
ui: UiCard(
|
||||
cardType: 'calendar_card.v1',
|
||||
data: CalendarCardData(
|
||||
@@ -104,21 +105,26 @@ class MockHistoryService {
|
||||
id: 'hist-m2',
|
||||
role: 'assistant',
|
||||
content: '已为你创建日程"产品评审会议",明天上午10:00。我还会提前15分钟提醒你。',
|
||||
timestamp: today.add(const Duration(hours: 10)),
|
||||
),
|
||||
SnapshotMessage(
|
||||
id: 'hist-m3',
|
||||
role: 'user',
|
||||
content: '下周一之前提交项目报告',
|
||||
timestamp: yesterday.add(const Duration(hours: 14)),
|
||||
),
|
||||
SnapshotMessage(id: 'hist-m3', role: 'user', content: '下周一之前提交项目报告'),
|
||||
SnapshotMessage(
|
||||
id: 'hist-t2',
|
||||
role: 'tool',
|
||||
toolCallId: 'hist-tc2',
|
||||
timestamp: yesterday.add(const Duration(hours: 14)),
|
||||
ui: UiCard(
|
||||
cardType: 'calendar_card.v1',
|
||||
data: CalendarCardData(
|
||||
id: 'hist-s2',
|
||||
title: '提交项目报告',
|
||||
description: '完成并提交Q2项目报告',
|
||||
startAt: yesterday
|
||||
.subtract(const Duration(days: 3))
|
||||
.toIso8601String(),
|
||||
startAt: yesterday.add(const Duration(days: 5)).toIso8601String(),
|
||||
endAt: null,
|
||||
timezone: 'Asia/Shanghai',
|
||||
location: null,
|
||||
@@ -138,11 +144,13 @@ class MockHistoryService {
|
||||
id: 'hist-m4',
|
||||
role: 'assistant',
|
||||
content: '好的,我已帮你创建待办事项"提交项目报告",截止日期为下周一。我还会提醒你完成这项任务。',
|
||||
timestamp: yesterday.add(const Duration(hours: 14)),
|
||||
),
|
||||
SnapshotMessage(
|
||||
id: 'hist-m5',
|
||||
role: 'assistant',
|
||||
content: '你好,我有什么可以帮你的?',
|
||||
timestamp: yesterday.add(const Duration(hours: 9)),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user