693 lines
20 KiB
Markdown
693 lines
20 KiB
Markdown
# AI Agent 系统任务恢复与推送机制分析
|
||
|
||
## 概述
|
||
|
||
本文档详细分析 AI Agent 系统中**正在运行中的对话恢复机制和推送机制**,重点关注页面刷新后如何恢复任务状态、重放历史事件以及维护 UI 一致性。
|
||
|
||
---
|
||
|
||
## 1. 页面刷新后的任务恢复流程
|
||
|
||
### 1.1 恢复触发时机
|
||
|
||
任务恢复在页面挂载时立即触发:
|
||
|
||
**文件**: `/static/src/app/lifecycle.ts`
|
||
|
||
```typescript
|
||
export async function mounted() {
|
||
// ... 其他初始化代码
|
||
|
||
// 注册全局事件处理器(用于任务轮询)
|
||
(window as any).__taskEventHandler = (event: any) => {
|
||
if (typeof this.handleTaskEvent === 'function') {
|
||
this.handleTaskEvent(event);
|
||
}
|
||
};
|
||
|
||
// 立即尝试恢复运行中的任务(不延迟)
|
||
if (typeof this.restoreTaskState === 'function') {
|
||
this.restoreTaskState();
|
||
}
|
||
}
|
||
```
|
||
|
||
**关键点**:
|
||
- 在 `mounted()` 生命周期钩子中立即调用 `restoreTaskState()`
|
||
- 不延迟执行,确保用户刷新后能快速看到任务恢复
|
||
- 先注册全局事件处理器 `__taskEventHandler`,用于后续轮询事件分发
|
||
|
||
### 1.2 查找运行中的任务
|
||
|
||
**文件**: `/static/src/stores/task.ts` - `loadRunningTask()`
|
||
|
||
```typescript
|
||
async loadRunningTask(conversationId: string | null = null) {
|
||
try {
|
||
debugLog('[Task] 查找运行中的任务');
|
||
|
||
// 1. 获取所有任务列表
|
||
const response = await fetch('/api/tasks');
|
||
const result = await response.json();
|
||
|
||
// 2. 查找运行中的任务(可选按对话ID过滤)
|
||
const runningTask = result.data.find((task: any) =>
|
||
task.status === 'running' &&
|
||
(!conversationId || task.conversation_id === conversationId)
|
||
);
|
||
|
||
if (runningTask) {
|
||
// 3. 更新任务状态
|
||
this.currentTaskId = runningTask.task_id;
|
||
this.taskStatus = runningTask.status;
|
||
this.taskCreatedAt = runningTask.created_at;
|
||
this.taskUpdatedAt = runningTask.updated_at;
|
||
|
||
// 4. 获取任务详情,计算已处理的事件数量
|
||
const detailResponse = await fetch(`/api/tasks/${runningTask.task_id}`);
|
||
if (detailResponse.ok) {
|
||
const detailResult = await detailResponse.json();
|
||
if (detailResult.success && detailResult.data.events) {
|
||
// 设置为当前事件数量,只获取新事件
|
||
this.lastEventIndex = detailResult.data.next_offset || detailResult.data.events.length;
|
||
debugLog('[Task] 设置起始偏移量:', this.lastEventIndex);
|
||
}
|
||
}
|
||
|
||
return runningTask;
|
||
}
|
||
|
||
return null;
|
||
} catch (error) {
|
||
console.error('[Task] 加载运行中任务失败:', error);
|
||
return null;
|
||
}
|
||
}
|
||
```
|
||
|
||
**关键逻辑**:
|
||
1. 调用 `/api/tasks` 获取用户的所有任务
|
||
2. 筛选 `status === 'running'` 且匹配当前对话ID的任务
|
||
3. 获取任务详情,计算 `lastEventIndex`(已处理的事件数量)
|
||
4. 返回运行中的任务信息
|
||
|
||
---
|
||
|
||
## 2. `_rebuildingFromScratch` 标志机制
|
||
|
||
### 2.1 标志的作用
|
||
|
||
`_rebuildingFromScratch` 是一个关键的布尔标志,用于区分两种恢复模式:
|
||
|
||
- **从头重建模式** (`true`): 历史消息不完整或缺失,需要从头重放所有事件来重建 assistant 响应
|
||
- **增量恢复模式** (`false`): 历史消息完整,只需恢复状态并继续轮询新事件
|
||
|
||
### 2.2 何时设置为 true
|
||
|
||
**文件**: `/static/src/app/methods/taskPolling.ts` - `restoreTaskState()`
|
||
|
||
```typescript
|
||
// 检查是否需要从头重建
|
||
const historyActionsCount = lastMessage?.actions?.length || 0;
|
||
const eventCount = allEvents.length;
|
||
const historyIncomplete = eventCount > historyActionsCount + 5; // 允许5个事件的误差
|
||
|
||
const needsRebuild = !isAssistantMessage ||
|
||
(isAssistantMessage && (!lastMessage.actions || lastMessage.actions.length === 0)) ||
|
||
historyIncomplete;
|
||
|
||
if (needsRebuild) {
|
||
debugLog('[TaskPolling] 需要从头重建 assistant 响应');
|
||
|
||
// 清空不完整的 assistant 消息
|
||
if (isAssistantMessage) {
|
||
this.messages.pop();
|
||
}
|
||
|
||
// 重置偏移量为 0,从头获取所有事件
|
||
taskStore.lastEventIndex = 0;
|
||
|
||
// 标记正在从头重建
|
||
this._rebuildingFromScratch = true;
|
||
this._rebuildingEventCount = allEvents.length;
|
||
|
||
// 启动轮询
|
||
taskStore.startPolling((event: any) => {
|
||
this.handleTaskEvent(event);
|
||
});
|
||
|
||
// 延迟清除重建标记(2秒后)
|
||
setTimeout(() => {
|
||
this._rebuildingFromScratch = false;
|
||
this._rebuildingEventCount = 0;
|
||
}, 2000);
|
||
}
|
||
```
|
||
|
||
**触发条件**:
|
||
1. **最后一条消息不是 assistant 消息**: 说明历史加载有问题
|
||
2. **最后一条是空的 assistant 消息**: `actions` 数组为空或不存在
|
||
3. **历史不完整**: 事件数量远大于历史中的 actions 数量(允许5个事件的误差)
|
||
|
||
### 2.3 对事件处理的影响
|
||
|
||
`_rebuildingFromScratch` 标志会影响多个事件处理函数的行为:
|
||
|
||
#### 2.3.1 思考块处理
|
||
|
||
```typescript
|
||
handleThinkingStart(data: any, eventIdx: number) {
|
||
// ... 创建思考块
|
||
|
||
// 只在非历史恢复时展开思考块
|
||
if (!this._rebuildingFromScratch) {
|
||
this.chatExpandBlock(blockId);
|
||
}
|
||
}
|
||
|
||
handleThinkingEnd(data: any) {
|
||
// ... 完成思考块
|
||
|
||
// 只在非历史恢复时延迟折叠思考块
|
||
if (!this._rebuildingFromScratch) {
|
||
setTimeout(() => {
|
||
this.chatCollapseBlock(blockId);
|
||
}, 1000);
|
||
} else {
|
||
// 历史恢复时立即折叠,不需要动画
|
||
this.chatCollapseBlock(blockId);
|
||
}
|
||
}
|
||
```
|
||
|
||
**差异**:
|
||
- **从头重建**: 不展开思考块,立即折叠,跳过动画
|
||
- **正常流式**: 展开思考块,延迟1秒后折叠,有动画效果
|
||
|
||
#### 2.3.2 工具意图打字机效果
|
||
|
||
```typescript
|
||
handleToolIntent(data: any) {
|
||
const isHistoryRestore = this._rebuildingFromScratch;
|
||
|
||
if (isHistoryRestore) {
|
||
// 历史恢复,直接显示完整 intent
|
||
action.tool.intent_rendered = newIntent;
|
||
} else {
|
||
// 新工具块,逐字符显示(打字机效果)
|
||
action.tool.intent_rendered = '';
|
||
action.tool._intentTyping = true;
|
||
|
||
// 逐字符显示动画
|
||
const typeNextChar = () => {
|
||
if (charIndex < newIntent.length && action.tool._intentTyping) {
|
||
action.tool.intent_rendered += newIntent[charIndex];
|
||
charIndex++;
|
||
this.$forceUpdate();
|
||
action.tool._intentTimer = setTimeout(typeNextChar, charInterval);
|
||
}
|
||
};
|
||
action.tool._intentTimer = setTimeout(typeNextChar, 50);
|
||
}
|
||
}
|
||
```
|
||
|
||
**差异**:
|
||
- **从头重建**: 直接显示完整 intent,无打字机效果
|
||
- **正常流式**: 逐字符显示,有打字机动画
|
||
|
||
#### 2.3.3 AI 消息开始处理
|
||
|
||
```typescript
|
||
handleAiMessageStart(data: any, eventIdx: number) {
|
||
// 如果是从头重建,标记消息为静默恢复
|
||
if (this._rebuildingFromScratch) {
|
||
const newMessage = this.messages[this.messages.length - 1];
|
||
if (newMessage && newMessage.role === 'assistant') {
|
||
newMessage.awaitingFirstContent = false;
|
||
newMessage.generatingLabel = '';
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
**差异**:
|
||
- **从头重建**: 清除"生成中"标签,静默恢复
|
||
- **正常流式**: 显示"生成中"动画
|
||
|
||
---
|
||
|
||
## 3. 历史事件的重放机制
|
||
|
||
### 3.1 事件获取
|
||
|
||
**后端**: `/server/tasks.py` - `get_task_api()`
|
||
|
||
```python
|
||
@tasks_bp.route("/api/tasks/<task_id>", methods=["GET"])
|
||
@api_login_required
|
||
def get_task_api(task_id: str):
|
||
username = get_current_username()
|
||
rec = task_manager.get_task(username, task_id)
|
||
if not rec:
|
||
return jsonify({"success": False, "error": "任务不存在"}), 404
|
||
|
||
# 获取偏移量参数
|
||
try:
|
||
offset = int(request.args.get("from", 0))
|
||
except Exception:
|
||
offset = 0
|
||
|
||
# 过滤事件(只返回 idx >= offset 的事件)
|
||
events = [e for e in rec.events if e["idx"] >= offset]
|
||
next_offset = events[-1]["idx"] + 1 if events else offset
|
||
|
||
return jsonify({
|
||
"success": True,
|
||
"data": {
|
||
"task_id": rec.task_id,
|
||
"status": rec.status,
|
||
"events": events,
|
||
"next_offset": next_offset,
|
||
}
|
||
})
|
||
```
|
||
|
||
**关键点**:
|
||
- 支持 `from` 参数,只返回 `idx >= from` 的事件
|
||
- 返回 `next_offset`,用于下次轮询
|
||
- 事件存储在 `deque(maxlen=1000)` 中,最多保留1000个事件
|
||
|
||
### 3.2 事件重放顺序
|
||
|
||
**文件**: `/static/src/stores/task.ts` - `pollTaskEvents()`
|
||
|
||
```typescript
|
||
async pollTaskEvents(eventHandler: (event: any) => void) {
|
||
const response = await fetch(
|
||
`/api/tasks/${this.currentTaskId}?from=${this.lastEventIndex}`
|
||
);
|
||
const result = await response.json();
|
||
const data = result.data;
|
||
|
||
// 处理新事件(按顺序)
|
||
if (data.events && data.events.length > 0) {
|
||
for (const event of data.events) {
|
||
try {
|
||
eventHandler(event);
|
||
} catch (err) {
|
||
console.error('[Task] 处理事件失败:', err, event);
|
||
}
|
||
}
|
||
|
||
// 更新偏移量
|
||
this.lastEventIndex = data.next_offset;
|
||
}
|
||
}
|
||
```
|
||
|
||
**重放顺序**:
|
||
1. 按事件的 `idx` 顺序(从小到大)
|
||
2. 同步处理,确保顺序正确
|
||
3. 每个事件处理完后才处理下一个
|
||
|
||
### 3.3 重放时的特殊处理
|
||
|
||
**文件**: `/static/src/app/methods/taskPolling.ts` - `handleTaskEvent()`
|
||
|
||
```typescript
|
||
handleTaskEvent(event: any) {
|
||
// 1. 检查事件的 conversation_id 是否匹配
|
||
if (eventData.conversation_id && this.currentConversationId) {
|
||
if (eventData.conversation_id !== this.currentConversationId) {
|
||
debugLog(`忽略不匹配的事件`);
|
||
return;
|
||
}
|
||
}
|
||
|
||
// 2. 根据事件类型调用对应的处理方法
|
||
switch (eventType) {
|
||
case 'ai_message_start':
|
||
this.handleAiMessageStart(eventData, eventIdx);
|
||
break;
|
||
case 'thinking_start':
|
||
this.handleThinkingStart(eventData, eventIdx);
|
||
break;
|
||
// ... 其他事件类型
|
||
}
|
||
|
||
// 注意:不要在这里清除 _rebuildingFromScratch 标记
|
||
// 该标记应该在恢复完所有历史事件后才清除
|
||
}
|
||
```
|
||
|
||
**特殊处理**:
|
||
- **对话ID过滤**: 忽略不匹配当前对话的事件
|
||
- **跳过动画**: 通过 `_rebuildingFromScratch` 标志跳过动画效果
|
||
- **静默恢复**: 不显示"生成中"标签,不展开思考块
|
||
|
||
---
|
||
|
||
## 4. 恢复时的 UI 状态管理
|
||
|
||
### 4.1 思考块状态恢复
|
||
|
||
**文件**: `/static/src/app/methods/taskPolling.ts` - `restoreTaskState()`
|
||
|
||
```typescript
|
||
// 恢复思考块状态
|
||
if (lastMessage.actions) {
|
||
const thinkingActions = lastMessage.actions.filter(a => a.type === 'thinking');
|
||
|
||
if (inThinking && thinkingActions.length > 0) {
|
||
// 正在思考中,检查最后一个思考块是否正在流式输出
|
||
const lastThinking = thinkingActions[thinkingActions.length - 1];
|
||
|
||
// 只有当思考块正在流式输出时才设置锁定状态(但不展开)
|
||
if (lastThinking.streaming && lastThinking.blockId) {
|
||
this.$nextTick(() => {
|
||
this.chatSetThinkingLock(lastThinking.blockId, true);
|
||
});
|
||
}
|
||
}
|
||
|
||
// 确保所有思考块都是折叠状态
|
||
for (const thinking of thinkingActions) {
|
||
if (thinking.blockId) {
|
||
thinking.collapsed = true;
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
**状态恢复**:
|
||
- **折叠状态**: 所有思考块默认折叠
|
||
- **锁定状态**: 如果正在流式输出,设置锁定(防止用户手动展开)
|
||
- **不展开**: 即使正在流式输出,也不自动展开(避免干扰用户)
|
||
|
||
### 4.2 工具块状态恢复
|
||
|
||
```typescript
|
||
// 注册历史中的工具块到 toolActionIndex
|
||
const toolActions = lastMessage.actions.filter(a => a.type === 'tool');
|
||
|
||
for (const toolAction of toolActions) {
|
||
if (toolAction.tool && toolAction.tool.id) {
|
||
// 注册到 toolActionIndex
|
||
this.toolRegisterAction(toolAction, toolAction.tool.id);
|
||
|
||
// 如果有 executionId,也注册
|
||
if (toolAction.tool.executionId) {
|
||
this.toolRegisterAction(toolAction, toolAction.tool.executionId);
|
||
}
|
||
|
||
// 追踪工具调用
|
||
if (toolAction.tool.name) {
|
||
this.toolTrackAction(toolAction.tool.name, toolAction);
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
**关键点**:
|
||
- **注册工具块**: 将历史中的工具块注册到 `toolActionIndex`
|
||
- **支持更新**: 后续的 `update_action` 事件可以找到对应的块进行状态更新
|
||
- **双重注册**: 同时注册 `id` 和 `executionId`,确保能找到
|
||
|
||
### 4.3 停止按钮状态
|
||
|
||
```typescript
|
||
// 标记状态为进行中
|
||
this.streamingMessage = true;
|
||
this.taskInProgress = true;
|
||
```
|
||
|
||
**状态设置**:
|
||
- `streamingMessage = true`: 显示停止按钮
|
||
- `taskInProgress = true`: 禁用输入框
|
||
- `stopRequested = false`: 重置停止请求标志
|
||
|
||
### 4.4 输入框状态
|
||
|
||
输入框状态由 `taskInProgress` 控制:
|
||
|
||
```typescript
|
||
// 在模板中
|
||
<input :disabled="taskInProgress" />
|
||
```
|
||
|
||
**状态**:
|
||
- `taskInProgress = true`: 输入框禁用
|
||
- `taskInProgress = false`: 输入框启用
|
||
|
||
---
|
||
|
||
## 5. 轮询的恢复机制
|
||
|
||
### 5.1 轮询启动
|
||
|
||
**文件**: `/static/src/stores/task.ts` - `startPolling()`
|
||
|
||
```typescript
|
||
startPolling(eventHandler?: (event: any) => void) {
|
||
if (this.isPolling) {
|
||
return;
|
||
}
|
||
|
||
this.isPolling = true;
|
||
|
||
// 获取事件处理器
|
||
const handler = eventHandler || ((window as any).__taskEventHandler);
|
||
|
||
// 立即执行一次
|
||
this.pollTaskEvents(handler);
|
||
|
||
// 设置定时轮询(150ms 间隔)
|
||
this.pollingInterval = window.setInterval(() => {
|
||
this.pollTaskEvents(handler);
|
||
}, 150);
|
||
}
|
||
```
|
||
|
||
**关键点**:
|
||
- **立即执行**: 启动轮询后立即执行一次,不等待第一个间隔
|
||
- **150ms 间隔**: 接近流式输出效果
|
||
- **全局处理器**: 使用 `window.__taskEventHandler` 作为默认处理器
|
||
|
||
### 5.2 从哪个 offset 开始轮询
|
||
|
||
**两种模式**:
|
||
|
||
#### 5.2.1 从头重建模式
|
||
|
||
```typescript
|
||
// 重置偏移量为 0,从头获取所有事件
|
||
taskStore.lastEventIndex = 0;
|
||
```
|
||
|
||
- **offset = 0**: 从第一个事件开始
|
||
- **目的**: 重建完整的 assistant 响应
|
||
|
||
#### 5.2.2 增量恢复模式
|
||
|
||
```typescript
|
||
// 获取任务详情,计算已处理的事件数量
|
||
const detailResponse = await fetch(`/api/tasks/${runningTask.task_id}`);
|
||
const detailResult = await detailResponse.json();
|
||
if (detailResult.success && detailResult.data.events) {
|
||
// 设置为当前事件数量,只获取新事件
|
||
this.lastEventIndex = detailResult.data.next_offset || detailResult.data.events.length;
|
||
}
|
||
```
|
||
|
||
- **offset = next_offset**: 从历史事件之后开始
|
||
- **目的**: 只获取新事件,避免重复处理
|
||
|
||
### 5.3 避免重复处理事件
|
||
|
||
**机制**:
|
||
|
||
1. **事件索引**: 每个事件有唯一的 `idx`
|
||
2. **偏移量过滤**: 后端只返回 `idx >= offset` 的事件
|
||
3. **偏移量更新**: 处理完事件后更新 `lastEventIndex = next_offset`
|
||
|
||
```typescript
|
||
// 处理新事件
|
||
if (data.events && data.events.length > 0) {
|
||
for (const event of data.events) {
|
||
eventHandler(event);
|
||
}
|
||
|
||
// 更新偏移量,下次轮询从这里开始
|
||
this.lastEventIndex = data.next_offset;
|
||
}
|
||
```
|
||
|
||
**保证**:
|
||
- 每个事件只处理一次
|
||
- 不会遗漏事件
|
||
- 不会重复处理
|
||
|
||
---
|
||
|
||
## 6. WebSocket vs 轮询的恢复差异
|
||
|
||
### 6.1 WebSocket 模式(已废弃)
|
||
|
||
**特点**:
|
||
- **实时推送**: 服务器主动推送事件
|
||
- **连接断开**: 刷新页面后 WebSocket 连接断开
|
||
- **无法恢复**: 断开期间的事件丢失
|
||
|
||
**问题**:
|
||
- 页面刷新后无法恢复任务状态
|
||
- 需要复杂的重连和事件补偿机制
|
||
|
||
### 6.2 轮询模式(当前实现)
|
||
|
||
**特点**:
|
||
- **主动拉取**: 客户端主动轮询事件
|
||
- **无连接状态**: 不依赖持久连接
|
||
- **完整恢复**: 可以从任意 offset 开始拉取事件
|
||
|
||
**优势**:
|
||
- 页面刷新后可以完整恢复任务状态
|
||
- 实现简单,无需处理重连逻辑
|
||
- 支持从头重建和增量恢复两种模式
|
||
|
||
### 6.3 恢复流程对比
|
||
|
||
| 特性 | WebSocket 模式 | 轮询模式 |
|
||
|------|---------------|---------|
|
||
| 连接状态 | 有状态(需要重连) | 无状态 |
|
||
| 事件恢复 | 困难(需要补偿机制) | 简单(从 offset 拉取) |
|
||
| 实时性 | 高(服务器推送) | 中(150ms 轮询) |
|
||
| 可靠性 | 低(连接断开丢失事件) | 高(事件持久化) |
|
||
| 刷新恢复 | 不支持 | 完全支持 |
|
||
|
||
---
|
||
|
||
## 7. 完整恢复流程图
|
||
|
||
```
|
||
页面刷新
|
||
↓
|
||
mounted() 生命周期
|
||
↓
|
||
注册全局事件处理器 __taskEventHandler
|
||
↓
|
||
调用 restoreTaskState()
|
||
↓
|
||
检查是否已有任务在进行 ──→ 是 ──→ 跳过恢复
|
||
↓ 否
|
||
调用 taskStore.loadRunningTask()
|
||
↓
|
||
查询 /api/tasks 获取任务列表
|
||
↓
|
||
筛选 status === 'running' 的任务 ──→ 无 ──→ 恢复子智能体等待状态
|
||
↓ 有
|
||
获取任务详情 /api/tasks/{task_id}
|
||
↓
|
||
计算 lastEventIndex(已处理的事件数量)
|
||
↓
|
||
等待历史加载完成
|
||
↓
|
||
检查历史完整性
|
||
↓
|
||
├─→ 历史不完整 ──→ 从头重建模式
|
||
│ ↓
|
||
│ 删除不完整的 assistant 消息
|
||
│ ↓
|
||
│ 设置 _rebuildingFromScratch = true
|
||
│ ↓
|
||
│ 设置 lastEventIndex = 0
|
||
│ ↓
|
||
│ 启动轮询(从头重放所有事件)
|
||
│ ↓
|
||
│ 2秒后清除 _rebuildingFromScratch 标志
|
||
│
|
||
└─→ 历史完整 ──→ 增量恢复模式
|
||
↓
|
||
恢复思考块状态(折叠、锁定)
|
||
↓
|
||
恢复工具块状态(注册到 toolActionIndex)
|
||
↓
|
||
恢复文本块状态(streaming 标志)
|
||
↓
|
||
设置 streamingMessage = true
|
||
↓
|
||
设置 taskInProgress = true
|
||
↓
|
||
启动轮询(只获取新事件)
|
||
↓
|
||
显示"任务恢复"提示
|
||
```
|
||
|
||
---
|
||
|
||
## 8. 关键代码文件索引
|
||
|
||
### 8.1 前端文件
|
||
|
||
| 文件路径 | 功能 |
|
||
|---------|------|
|
||
| `/static/src/app/lifecycle.ts` | 页面生命周期,触发恢复 |
|
||
| `/static/src/stores/task.ts` | 任务状态管理,轮询控制 |
|
||
| `/static/src/app/methods/taskPolling.ts` | 事件处理,状态恢复 |
|
||
| `/static/src/app/methods/history.ts` | 历史消息加载和渲染 |
|
||
| `/static/src/app/methods/conversation.ts` | 对话管理 |
|
||
|
||
### 8.2 后端文件
|
||
|
||
| 文件路径 | 功能 |
|
||
|---------|------|
|
||
| `/server/tasks.py` | 任务 API,事件存储和查询 |
|
||
| `/server/chat_flow.py` | 聊天流程,事件生成 |
|
||
|
||
---
|
||
|
||
## 9. 最佳实践和注意事项
|
||
|
||
### 9.1 事件设计
|
||
|
||
1. **每个事件必须有唯一的 idx**: 用于偏移量过滤
|
||
2. **事件必须包含 conversation_id**: 用于过滤不匹配的事件
|
||
3. **事件必须按顺序生成**: 确保 idx 递增
|
||
4. **事件必须持久化**: 存储在 `deque(maxlen=1000)` 中
|
||
|
||
### 9.2 状态恢复
|
||
|
||
1. **先加载历史,再恢复任务**: 确保有完整的消息列表
|
||
2. **检查历史完整性**: 对比事件数量和 actions 数量
|
||
3. **注册工具块**: 确保后续的 `update_action` 事件能找到对应的块
|
||
4. **设置正确的偏移量**: 避免重复处理或遗漏事件
|
||
|
||
### 9.3 UI 体验
|
||
|
||
1. **跳过动画**: 从头重建时跳过所有动画效果
|
||
2. **静默恢复**: 不显示"生成中"标签
|
||
3. **折叠思考块**: 默认折叠,避免干扰用户
|
||
4. **显示提示**: 恢复成功后显示"任务恢复"提示
|
||
|
||
### 9.4 错误处理
|
||
|
||
1. **任务不存在**: 恢复子智能体等待状态
|
||
2. **历史加载失败**: 延迟重试(最多5次)
|
||
3. **事件处理失败**: 捕获异常,继续处理下一个事件
|
||
4. **对话切换**: 忽略不匹配的事件
|
||
|
||
---
|
||
|
||
## 10. 总结
|
||
|
||
AI Agent 系统的任务恢复机制通过以下关键技术实现:
|
||
|
||
1. **事件持久化**: 所有事件存储在后端,支持从任意 offset 查询
|
||
2. **轮询模式**: 客户端主动拉取事件,无需维护连接状态
|
||
3. **双模式恢复**: 支持从头重建和增量恢复两种模式
|
||
4. **智能判断**: 根据历史完整性自动选择恢复模式
|
||
5. **UI 优化**: 跳过动画、静默恢复,提升用户体验
|
||
6. **状态同步**: 恢复思考块、工具块、文本块的完整状态
|
||
|
||
这套机制确保了用户在页面刷新后能够无缝恢复任务,不会丢失任何进度,同时保持了良好的用户体验。
|