feat(chat): add ChatBubble widget and mock data for home screen
- Add ChatBubble reusable widget for chat messages - Add HomeMockData for chat list mock data - Add HomeScreen widget tests - Add AG-UI chat design and implementation plan docs - Add friendship design docs - Ignore backend/logs directory
This commit is contained in:
@@ -0,0 +1,567 @@
|
||||
# AG-UI 聊天功能设计文档
|
||||
|
||||
## 1. 概述
|
||||
|
||||
本文档描述如何使用 AG-UI 协议实现 AI 聊天功能,包括:
|
||||
- 消息的发送与接收(通过 AG-UI 事件流)
|
||||
- AI 工具调用(Tool Call)机制
|
||||
- 日历卡片作为 Tool Result 渲染
|
||||
- 前端工具注册与执行
|
||||
- 本地持久化
|
||||
|
||||
## 2. 架构设计
|
||||
|
||||
### 2.1 整体流程
|
||||
|
||||
```
|
||||
用户输入消息
|
||||
↓
|
||||
AgUiService.sendMessage()
|
||||
↓
|
||||
[Mock Mode] 规则引擎决策 → 事件流模拟
|
||||
[Real Mode] POST /api/chat → SSE 监听
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ AG-UI Event Stream (按序处理) │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ TEXT_MESSAGE_START → TEXT_MESSAGE_CONTENT* → TEXT_MESSAGE_END │
|
||||
│ TOOL_CALL_START → TOOL_CALL_ARGS* → TOOL_CALL_END │
|
||||
│ TOOL_CALL_RESULT │
|
||||
│ RUN_STARTED → ... → RUN_FINISHED │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
ChatListItem 渲染
|
||||
```
|
||||
|
||||
### 2.2 核心组件
|
||||
|
||||
| 组件 | 职责 |
|
||||
|------|------|
|
||||
| `AgUiEvent` | AG-UI 事件数据模型 |
|
||||
| `AgUiService` | 事件流处理:发送消息、解析事件 |
|
||||
| `ToolRegistry` | 前端工具注册表:定义工具 + handler |
|
||||
| `AiDecisionEngine` | Mock 模式:规则引擎决定是否调用工具 |
|
||||
| `UiSchemaParser` | 解析 tool result 中的 UI Schema |
|
||||
| `UiSchemaRenderer` | 根据 schema 渲染对应组件 |
|
||||
| `ChatHistoryRepository` | 本地持久化:IndexedDB/localStorage |
|
||||
|
||||
### 2.3 状态管理
|
||||
|
||||
```
|
||||
ChatState {
|
||||
messages: ChatListItem[] // 渲染列表
|
||||
pendingToolCalls: Map<call_id, ToolCallState>
|
||||
isLoading: bool
|
||||
runId: string | null
|
||||
}
|
||||
```
|
||||
|
||||
## 3. 数据模型
|
||||
|
||||
### 3.1 AG-UI 事件模型
|
||||
|
||||
```dart
|
||||
// 基类
|
||||
abstract class AgUiEvent {
|
||||
final String type;
|
||||
final String? timestamp;
|
||||
}
|
||||
|
||||
// 生命周期事件
|
||||
class RunStartedEvent extends AgUiEvent {
|
||||
final String threadId;
|
||||
final String runId;
|
||||
final String? parentRunId;
|
||||
}
|
||||
|
||||
class RunFinishedEvent extends AgUiEvent {
|
||||
final String threadId;
|
||||
final String runId;
|
||||
final dynamic result;
|
||||
}
|
||||
|
||||
// 文本消息事件
|
||||
class TextMessageStartEvent extends AgUiEvent {
|
||||
final String messageId;
|
||||
final String role; // "user" | "assistant" | "system"
|
||||
}
|
||||
|
||||
class TextMessageContentEvent extends AgUiEvent {
|
||||
final String messageId;
|
||||
final String delta;
|
||||
}
|
||||
|
||||
class TextMessageEndEvent extends AgUiEvent {
|
||||
final String messageId;
|
||||
}
|
||||
|
||||
// 工具调用事件
|
||||
class ToolCallStartEvent extends AgUiEvent {
|
||||
final String toolCallId;
|
||||
final String toolCallName;
|
||||
final String? parentMessageId;
|
||||
}
|
||||
|
||||
class ToolCallArgsEvent extends AgUiEvent {
|
||||
final String toolCallId;
|
||||
final String delta; // JSON fragment
|
||||
}
|
||||
|
||||
class ToolCallEndEvent extends AgUiEvent {
|
||||
final String toolCallId;
|
||||
}
|
||||
|
||||
class ToolCallResultEvent extends AgUiEvent {
|
||||
final String messageId;
|
||||
final String toolCallId;
|
||||
final ToolResult result; // 给 AI 的原始结果
|
||||
final UiCard? ui; // 给 UI 的渲染数据
|
||||
}
|
||||
|
||||
class ToolCallErrorEvent extends AgUiEvent {
|
||||
final String toolCallId;
|
||||
final String error;
|
||||
final String? code;
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 Tool Result Schema(v1)
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "tool_result",
|
||||
"version": "v1",
|
||||
"call_id": "call_abc123",
|
||||
"tool_name": "create_calendar_event",
|
||||
"result": {
|
||||
"eventId": "evt_xxx",
|
||||
"ok": true,
|
||||
"message": "日程已创建"
|
||||
},
|
||||
"ui": {
|
||||
"type": "card",
|
||||
"cardType": "calendar_card.v1",
|
||||
"data": {
|
||||
"id": "evt_xxx",
|
||||
"title": "产品评审会议",
|
||||
"description": "讨论Q2路线图",
|
||||
"startAt": "2026-03-01T10:00:00+08:00",
|
||||
"endAt": "2026-03-01T11:00:00+08:00",
|
||||
"timezone": "Asia/Shanghai",
|
||||
"location": "会议室A",
|
||||
"color": "#4F46E5",
|
||||
"sourceType": "agentGenerated"
|
||||
},
|
||||
"actions": [
|
||||
{"type": "open", "label": "打开", "target": "calendar/evt_xxx"},
|
||||
{"type": "edit", "label": "编辑", "action": "edit_event"},
|
||||
{"type": "delete", "label": "删除", "action": "delete_event"}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 工具定义(前端 Tool Registry)
|
||||
|
||||
```dart
|
||||
// 工具定义
|
||||
class ToolDefinition {
|
||||
final String name;
|
||||
final String description;
|
||||
final Map<String, dynamic> parameters;
|
||||
final ToolHandler handler;
|
||||
}
|
||||
|
||||
// create_calendar_event 工具
|
||||
{
|
||||
"name": "create_calendar_event",
|
||||
"description": "创建一个日历事件或待办事项",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "string",
|
||||
"description": "事件标题",
|
||||
"minLength": 1,
|
||||
"maxLength": 100
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "事件描述"
|
||||
},
|
||||
"startAt": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"description": "开始时间 (ISO8601)"
|
||||
},
|
||||
"endAt": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"description": "结束时间 (ISO8601)"
|
||||
},
|
||||
"timezone": {
|
||||
"type": "string",
|
||||
"default": "Asia/Shanghai"
|
||||
},
|
||||
"location": {
|
||||
"type": "string"
|
||||
},
|
||||
"notes": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["title", "startAt"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.4 ChatListItem 模型
|
||||
|
||||
```dart
|
||||
enum ChatItemType {
|
||||
message, // 纯文本消息
|
||||
toolCall, // 工具调用中
|
||||
toolResult, // 工具结果卡片
|
||||
schedule // 日历事件(兼容旧数据)
|
||||
}
|
||||
|
||||
abstract class ChatListItem {
|
||||
String get id;
|
||||
DateTime get timestamp;
|
||||
ChatItemType get type;
|
||||
MessageSender get sender;
|
||||
}
|
||||
|
||||
class TextMessageItem extends ChatListItem {
|
||||
final String id;
|
||||
final String content;
|
||||
final DateTime timestamp;
|
||||
final MessageSender sender;
|
||||
final bool isStreaming; // 是否正在流式输出
|
||||
}
|
||||
|
||||
class ToolCallItem extends ChatListItem {
|
||||
final String id;
|
||||
final String callId;
|
||||
final String toolName;
|
||||
final Map<String, dynamic> args; // 解析后的参数
|
||||
final ToolCallStatus status; // pending | executing | completed | error
|
||||
final ToolResult? result;
|
||||
final UiCard? uiCard;
|
||||
}
|
||||
|
||||
class CalendarCardItem extends ChatListItem {
|
||||
final String id;
|
||||
final String callId; // 关联的 tool call
|
||||
final CalendarCardData data;
|
||||
final List<CardAction> actions;
|
||||
}
|
||||
```
|
||||
|
||||
## 4. 核心流程
|
||||
|
||||
### 4.1 发送消息
|
||||
|
||||
```dart
|
||||
Future<void> sendMessage(String content) async {
|
||||
// 1. 添加用户消息到列表
|
||||
final userMessage = TextMessageItem(
|
||||
id: generateId(),
|
||||
content: content,
|
||||
timestamp: DateTime.now(),
|
||||
sender: MessageSender.user,
|
||||
);
|
||||
_chatItems.add(userMessage);
|
||||
|
||||
// 2. 发起请求
|
||||
if (Env.isMockApi) {
|
||||
await _mockEventStream(content);
|
||||
} else {
|
||||
await _realEventStream(content);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 Mock 事件流(规则引擎)
|
||||
|
||||
```dart
|
||||
class AiDecisionEngine {
|
||||
// 意图关键词映射
|
||||
static final Map<Intent, List<Pattern>> _intentPatterns = {
|
||||
Intent.createEvent: [
|
||||
RegExp(r'提醒|开会|预约|日程|安排'),
|
||||
RegExp(r'明天|今天|后天|下周'),
|
||||
RegExp(r'\d{1,2}点|\d{1,2}:\d{2}'),
|
||||
],
|
||||
Intent.searchEvent: [
|
||||
RegExp(r'查看|有什么|今天.*日程|明天.*安排'),
|
||||
],
|
||||
};
|
||||
|
||||
Intent? matchIntent(String text) {
|
||||
for (final entry in _intentPatterns.entries) {
|
||||
for (final pattern in entry.value) {
|
||||
if (pattern.hasMatch(text)) {
|
||||
return entry.key;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// 支持强制触发:#tool:create_calendar_event {"title": "test"}
|
||||
bool tryForceTrigger(String text) {...}
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3 事件解析与处理
|
||||
|
||||
```dart
|
||||
Future<void> _processEvent(AgUiEvent event) async {
|
||||
switch (event.type) {
|
||||
case 'TEXT_MESSAGE_START':
|
||||
_handleTextMessageStart(event);
|
||||
break;
|
||||
case 'TEXT_MESSAGE_CONTENT':
|
||||
_handleTextMessageContent(event);
|
||||
break;
|
||||
case 'TEXT_MESSAGE_END':
|
||||
_handleTextMessageEnd(event);
|
||||
break;
|
||||
case 'TOOL_CALL_START':
|
||||
_handleToolCallStart(event);
|
||||
break;
|
||||
case 'TOOL_CALL_ARGS':
|
||||
_handleToolCallArgs(event);
|
||||
break;
|
||||
case 'TOOL_CALL_END':
|
||||
await _handleToolCallEnd(event);
|
||||
break;
|
||||
case 'TOOL_CALL_RESULT':
|
||||
_handleToolCallResult(event);
|
||||
break;
|
||||
case 'TOOL_CALL_ERROR':
|
||||
_handleToolCallError(event);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void _handleToolCallStart(ToolCallStartEvent event) {
|
||||
// 创建 pending 状态的 tool call item
|
||||
final item = ToolCallItem(
|
||||
id: event.toolCallId,
|
||||
callId: event.toolCallId,
|
||||
toolName: event.toolCallName,
|
||||
args: {},
|
||||
status: ToolCallStatus.pending,
|
||||
);
|
||||
_chatItems.add(item);
|
||||
}
|
||||
|
||||
Future<void> _handleToolCallEnd(ToolCallEndEvent event) async {
|
||||
// 1. 找到对应的 pending tool call
|
||||
final toolCall = _findPendingToolCall(event.toolCallId);
|
||||
if (toolCall == null) return;
|
||||
|
||||
// 2. 校验参数 JSON Schema
|
||||
final validation = validateToolArgs(toolCall.toolName, toolCall.args);
|
||||
if (!validation.ok) {
|
||||
_emitToolCallError(event.toolCallId, validation.error);
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. 执行工具 handler
|
||||
toolCall.status = ToolCallStatus.executing;
|
||||
final result = await ToolRegistry.execute(
|
||||
toolCall.toolName,
|
||||
toolCall.args,
|
||||
);
|
||||
|
||||
// 4. 构建 tool result(包含 result + ui)
|
||||
final toolResult = ToolResult(
|
||||
eventId: result['eventId'],
|
||||
ok: result['ok'] ?? true,
|
||||
message: result['message'],
|
||||
);
|
||||
|
||||
final uiCard = _buildUiCard(toolCall.toolName, result);
|
||||
|
||||
// 5. 发送 TOOL_CALL_RESULT 事件
|
||||
_emitToolCallResult(event.toolCallId, toolResult, uiCard);
|
||||
}
|
||||
```
|
||||
|
||||
### 4.4 UI Schema 渲染
|
||||
|
||||
```dart
|
||||
class UiSchemaRenderer {
|
||||
static final Map<String, Widget Function(UiCard)> _renderers = {
|
||||
'calendar_card.v1': (card) => CalendarCardWidget(
|
||||
data: CalendarCardData.fromJson(card.data),
|
||||
actions: card.actions,
|
||||
),
|
||||
};
|
||||
|
||||
static Widget render(UiCard card) {
|
||||
final renderer = _renderers[card.cardType];
|
||||
if (renderer != null) {
|
||||
return renderer(card);
|
||||
}
|
||||
// Unknown card type fallback
|
||||
return _renderUnknownCard(card);
|
||||
}
|
||||
|
||||
static Widget _renderUnknownCard(UiCard card) {
|
||||
return GenericCardWidget(
|
||||
rawJson: jsonEncode(card.toJson()),
|
||||
cardType: card.cardType,
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4.5 日历卡片组件
|
||||
|
||||
```dart
|
||||
class CalendarCardWidget extends StatelessWidget {
|
||||
final CalendarCardData data;
|
||||
final List<CardAction> actions;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final color = ColorExt.parse(data.color ?? '#4F46E5');
|
||||
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [...],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 颜色条
|
||||
Container(
|
||||
height: 4,
|
||||
color: color,
|
||||
),
|
||||
// 内容
|
||||
Padding(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(data.title, style: ...),
|
||||
if (data.description != null) ...,
|
||||
_buildTimeRow(),
|
||||
if (data.location != null) ...,
|
||||
],
|
||||
),
|
||||
),
|
||||
// Actions
|
||||
if (actions.isNotEmpty) _buildActions(actions),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 5. 持久化设计
|
||||
|
||||
### 5.1 存储结构
|
||||
|
||||
```dart
|
||||
// localStorage / IndexedDB
|
||||
{
|
||||
"chat_sessions": {
|
||||
"current_thread_id": {
|
||||
"messages": [...], // ChatListItem JSON
|
||||
"lastRunId": "run_xxx",
|
||||
"updatedAt": "2026-02-28T12:00:00Z"
|
||||
}
|
||||
},
|
||||
"calendar_events": {
|
||||
"evt_xxx": {...} // 独立存储的日历事件
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 恢复逻辑
|
||||
|
||||
```dart
|
||||
Future<void> restoreSession() async {
|
||||
final session = await ChatHistoryRepository.load('current_thread_id');
|
||||
if (session != null) {
|
||||
_chatItems.clear();
|
||||
_chatItems.addAll(session.messages);
|
||||
_runId = session.lastRunId;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 6. 错误处理
|
||||
|
||||
### 6.1 Tool Call 错误
|
||||
|
||||
```dart
|
||||
void _emitToolCallError(String callId, String error) {
|
||||
// 1. 更新 item 状态
|
||||
final item = _findToolCallItem(callId);
|
||||
item?.status = ToolCallStatus.error;
|
||||
item?.errorMessage = error;
|
||||
|
||||
// 2. 渲染错误卡片
|
||||
final errorCard = UiCard(
|
||||
cardType: 'error_card.v1',
|
||||
data: {'message': error},
|
||||
);
|
||||
|
||||
// 3. 触发 UI 更新
|
||||
notifyListeners();
|
||||
}
|
||||
```
|
||||
|
||||
### 6.2 事件流重连
|
||||
|
||||
```dart
|
||||
// 断线重连时从 snapshot 恢复
|
||||
Future<void> reconnect() async {
|
||||
final snapshot = await _fetchMessagesSnapshot();
|
||||
_chatItems.clear();
|
||||
_chatItems.addAll(snapshot.messages);
|
||||
|
||||
// 重新订阅事件流
|
||||
_subscribeToEvents();
|
||||
}
|
||||
```
|
||||
|
||||
## 7. 实施计划
|
||||
|
||||
### Phase 1: 基础框架
|
||||
- [ ] 定义 AG-UI 事件模型
|
||||
- [ ] 实现 AgUiService 基础结构
|
||||
- [ ] 实现 ToolRegistry
|
||||
|
||||
### Phase 2: Mock 实现
|
||||
- [ ] 实现 AiDecisionEngine 规则引擎
|
||||
- [ ] 实现 Mock 事件流
|
||||
- [ ] 集成现有 HomeScreen
|
||||
|
||||
### Phase 3: UI 渲染
|
||||
- [ ] 实现 UiSchemaParser
|
||||
- [ ] 实现 CalendarCardWidget
|
||||
- [ ] 实现 ToolPending / ToolError 状态卡片
|
||||
|
||||
### Phase 4: 持久化
|
||||
- [ ] 实现 ChatHistoryRepository
|
||||
- [ ] 实现会话恢复
|
||||
|
||||
### Phase 5: 真实后端对接
|
||||
- [ ] 实现 SSE 客户端
|
||||
- [ ] 实现事件流解析器
|
||||
|
||||
## 8. 版本历史
|
||||
|
||||
| 版本 | 日期 | 变更 |
|
||||
|------|------|------|
|
||||
| v1.0 | 2026-02-28 | 初始版本 |
|
||||
Reference in New Issue
Block a user