Files

72 lines
4.0 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
id: frontend.react.scenario.long-task-ui
title: 长任务界面:Agent 任务的状态机、事件去重、乐观更新和恢复
type: scenario
domain: frontend
stack: [react, typescript, fastapi]
tags: [任务状态, 状态机, useReducer, SSE, 幂等, 乐观更新, 异常恢复]
level: advanced
prerequisites: [frontend.react.hooks-rendering]
est_hours: 2.5
related: [frontend.react.interview, ai.langgraph.scenario.refund-approval, frontend.vue.scenario.ai-chat-stream]
week: [7, 12]
---
# 长任务界面:Agent 任务的状态机、事件去重、乐观更新和恢复
## 业务背景与问题
报表 Agent 生成一份报表要 1~3 分钟,中间可能需要用户补充信息(“要按大区还是按城市?”)。前端第一版的问题:
1. 用户刷新页面后,看不到正在运行的任务,以为丢了,又提交了一次;
2. 网络重连后收到重复的进度事件,步骤列表出现重复项;
3. 任务失败只显示“出错了”,用户不知道能不能重试;
4. 点击“取消”后界面立即显示已取消,但后端其实还在跑;
5. 同时打开两个标签页,状态不一致。
## 约束
- 后端是 LangGraph + FastAPI,任务状态存在数据库中,事件通过 SSE 推送。
## 方案设计
1. **任务以服务端为准**:提交后后端返回任务 ID;页面加载时先查询“我的进行中任务”并恢复显示;提交接口带幂等键(防止重复提交)。
2. **前端状态机**:`useReducer` 集中管理状态转换(排队 / 运行 / 等待输入 / 成功 / 失败 / 取消),事件带递增序号,旧事件和终态后的事件丢弃。
3. **重连**:断开后先调用查询接口拿到最新快照(包含当前序号),再从该序号之后订阅事件。
4. **失败分类**:后端返回 `retryable`,前端据此显示“重试”按钮或“联系管理员”。
5. **取消**:点击后进入“取消中”,后端确认(事件或接口返回)后再显示“已取消”;不做乐观的终态显示。
6. **等待输入**:显示问题和输入框,提交后通过接口恢复任务(对应后端的 `interrupt` / `Command(resume=...)`)。
7. **多标签页**:都以服务端事件为准;必要时用 `BroadcastChannel` 同步。
## 关键代码
状态机和组件见 [examples/task-panel](../examples/task-panel/)。重连时先取快照再订阅(示意):
```ts
async function connect(taskId: string, dispatch: (e: TaskEvent) => void, signal: AbortSignal) {
const snap = await fetchJSON<TaskSnapshot>(`/api/tasks/${taskId}`, { signal });
dispatch({ type: "snapshot", snapshot: snap }); // 用快照覆盖本地状态
await streamEvents(`/api/tasks/${taskId}/events?after=${snap.seq}`, dispatch, signal);
}
```
## 取舍
| 选择 | 优点 | 缺点 |
| --- | --- | --- |
| **服务端为准 + 前端状态机(本方案)** | 刷新、重连、多标签页都一致 | 需要快照接口和事件序号 |
| 只靠前端内存状态 | 简单 | 刷新即丢失 |
| 轮询任务状态 | 实现简单、天然可恢复 | 延迟和请求量;可作为 SSE 不可用时的降级 |
| 乐观更新所有操作 | 体验快 | 对“取消”这类不一定成功的操作会误导用户 |
## 易错点
- 状态转换分散在多个 `useState` 和事件处理函数中,难以保证一致。
- 不区分“已取消”和“取消中”。
- 重连后不取快照,直接订阅,错过断线期间的事件。
- `useEffect` 中订阅没有清理,切换任务后旧任务的事件串进来。
## 面试怎么讲
> “长任务界面我以服务端为准:提交带幂等键,页面加载时恢复进行中的任务。前端用 useReducer 做状态机,事件带序号,重复、乱序、终态后的事件都丢弃;断线重连先取快照再从序号之后订阅。失败按 retryable 显示重试,取消要等后端确认,不做乐观的终态。等待用户输入时,提交后调用接口恢复后端的 LangGraph 任务。reducer 是纯函数,状态转换都有单元测试。”