From b1e3660a30b75f51a0e7b41c06d58c8b50033542 Mon Sep 17 00:00:00 2001
From: tony.cheng <chengmingwei_1984122@126.com>
Date: Thu, 12 Feb 2026 13:39:03 +0800
Subject: [PATCH] docs: 更新智能外呼相关文档 - 优化失败任务展示和去重机制
---
/dev/null | 38 ---------
openspec/changes/integrate-auto-outbound-call/proposal.md | 33 +++++++-
openspec/changes/integrate-auto-outbound-call/design.md | 33 ++++++++
openspec/changes/integrate-auto-outbound-call/tasks.md | 117 +++++++++++++++++------------
4 files changed, 128 insertions(+), 93 deletions(-)
diff --git a/openspec/changes/integrate-auto-outbound-call/design.md b/openspec/changes/integrate-auto-outbound-call/design.md
index ff4da5f..8123189 100644
--- a/openspec/changes/integrate-auto-outbound-call/design.md
+++ b/openspec/changes/integrate-auto-outbound-call/design.md
@@ -26,12 +26,16 @@
4. **幂等性保证**:页面刷新时不重复发起外呼,继续监听现有任务
5. **自动清理**:通话结束后自动移除气泡和存储数据
6. **错误容错**:API 失败时提供友好提示,不阻塞主流程
+7. **失败任务可视化**:失败任务在气泡中清晰展示,带红色状态指示灯
+8. **重复防护**:相同联系人失败任务自动去重,避免信息冗余
+9. **自动过期清理**:失败任务超过24小时自动清除
### Non-Goals
- 不涉及后端外呼接口实现
- 不涉及通话录音播放功能
- 不涉及外呼任务的手动取消或重试操作
- 不涉及复杂的权限控制(当前仅前端功能)
+- 不涉及失败任务的自动重试机制
## Decisions
@@ -53,6 +57,8 @@
### Decision 2: jobId 状态管理 - localStorage + 活跃状态过滤
**数据结构设计**:
+
+**成功任务存储**:
```javascript
// localStorage key: 'outbound_call_jobs'
[
@@ -61,9 +67,27 @@
callStatus: "Scheduling", // 或 Executing, Paused, Drafted, Succeeded, Failed, Cancelled
personId: "2303191513081131",
mediationId: 20,
+ caseId: "CASE_001", // 用于轮询查询的 caseRef 参数
+ perClassName: "申请人", // 人员类型名称
+ trueName: "张三", // 真实姓名
startTime: 1738228800000, // 触发时间戳
retryCount: 0, // 重试次数
pollStartTime: 1738228800000 // 轮询开始时间(用于2小时超时检测)
+ }
+]
+```
+
+**失败任务存储**:
+```javascript
+// localStorage key: 'outbound_call_jobs_failed'
+[
+ {
+ personId: "2303191513081131",
+ message: "今日呼叫次数已达上限(3次)",
+ perClassName: "申请人", // 人员类型名称
+ trueName: "张三", // 真实姓名
+ errorCode: 1001, // 错误码
+ startTime: 1738228800000 // 失败时间戳(用于24小时过期清理)
}
]
```
@@ -216,7 +240,14 @@
3. 实现 localStorage 状态管理
4. 完成错误处理和重试机制
-### Phase 2: 优化与扩展(后续迭代)
+### Phase 2: 用户体验优化(当前完成)
+1. 失败任务独立存储和展示
+2. 失败任务去重机制(按 personId 去重)
+3. 失败任务自动清理(24小时过期)
+4. 视觉优化:成功任务绿色指示灯,失败任务红色指示灯
+5. 重复气泡防护,避免信息冗余
+
+### Phase 3: 优化与扩展(后续迭代)
1. 支持手动取消外呼任务
2. 外呼结果通知(成功/失败弹窗)
3. 外呼历史记录查询
diff --git a/openspec/changes/integrate-auto-outbound-call/proposal.md b/openspec/changes/integrate-auto-outbound-call/proposal.md
index 56ae0bc..4e448ba 100644
--- a/openspec/changes/integrate-auto-outbound-call/proposal.md
+++ b/openspec/changes/integrate-auto-outbound-call/proposal.md
@@ -6,21 +6,32 @@
1. 案件数据加载完成后自动发起智能外呼
2. 实时监控并展示外呼状态和进度
3. 支持多人同时外呼的场景
+4. 优化失败任务展示体验,提供清晰的状态指示
## What Changes
- **CaseDataContext.jsx**: 在 `loadCaseData` 方法返回 `timelineData` 后,调用 `OutboundBotAPIService.makeCallV2` 发起外呼
- **OutboundCallWidget.jsx**: 改造现有轮询逻辑,基于 jobId 查询通话状态,支持多任务展示和生命周期管理
-- **localStorage 管理**: 新增 jobId 状态持久化存储,区分活跃任务(Scheduling/Executing/Paused/Drafted)与终态任务(Succeeded/Failed/Cancelled)
+- **localStorage 管理**:
+ - 新增 jobId 状态持久化存储,区分活跃任务(Scheduling/Executing/Paused/Drafted)与终态任务(Succeeded/Failed/Cancelled)
+ - 新增失败任务独立存储(`outbound_call_jobs_failed`),支持失败信息展示
+ - 实现失败任务去重机制(按 personId 去重)和自动清理(24小时过期)
- **轮询策略**: 采用 10 秒轮询间隔,最大轮询时长 2 小时,失败重试 10 次
-- **错误处理**: `makeCallV2` 失败时展示错误提示并记录日志,`getCallStatus` 失败时支持重试机制
+- **错误处理**:
+ - `makeCallV2` 失败时展示错误提示并记录日志
+ - `getCallStatus` 失败时支持重试机制
+ - 失败任务在气泡中清晰展示,带红色状态指示灯
+- **UI/UX 优化**:
+ - 成功任务:蓝色气泡 + 绿色呼吸灯状态指示
+ - 失败任务:蓝色气泡 + 红色呼吸灯状态指示
+ - 重复任务自动去重,避免信息冗余
## Impact
- **Affected specs**: outbound-call-auto-trigger(新建)
- **Affected code**:
- - `web-app/src/contexts/CaseDataContext.jsx` (141行后新增外呼触发逻辑)
- - `web-app/src/components/common/OutboundCallWidget.jsx` (重构轮询逻辑)
+ - `web-app/src/contexts/CaseDataContext.jsx` (141行后新增外呼触发逻辑,含失败任务处理)
+ - `web-app/src/components/common/OutboundCallWidget.jsx` (重构轮询逻辑,新增失败任务展示和去重)
- `web-app/src/services/OutboundBotAPIService.js` (已有API方法,无需修改)
- **Dependencies**: 依赖 API 文档中定义的外呼机器人接口
- **Breaking Changes**: 无
@@ -29,9 +40,13 @@
1. **用户进入首页** → 页面自动加载案件数据
2. **数据加载完成** → 系统自动发起外呼(无需手动操作)
-3. **右下角气泡弹出** → 展示"正在与XX电话沟通中..."
+3. **右下角气泡弹出** →
+ - 成功任务:蓝色气泡 + 绿色呼吸灯,展示"正在与XX电话沟通中..."
+ - 失败任务:蓝色气泡 + 红色呼吸灯,展示失败原因(如"今日呼叫次数已达上限")
4. **实时更新状态** → 每10秒刷新通话状态和时长
5. **通话结束** → 气泡自动消失,jobId 从存储中清除
+6. **重复防护** → 相同联系人失败任务自动去重,避免重复显示
+7. **自动清理** → 失败任务超过24小时自动清除
## Technical Notes
@@ -41,3 +56,11 @@
- 终态状态: `Succeeded`, `Failed`, `Cancelled`
- **多任务支持**: 单次外呼可能生成多个 jobId(申请人、被申请人),需循环展示
- **轮询限制**: 最大轮询时长 2 小时(7200秒),超时后自动停止并清理
+- **失败任务处理**:
+ - 存储字段:`{ personId, message, perClassName, trueName, errorCode, startTime }`
+ - 去重策略:按 `personId` 去重,保留最新记录
+ - 清理策略:超过24小时的记录自动清除
+- **视觉规范**:
+ - 所有任务使用统一蓝色气泡背景
+ - 状态指示灯:成功=绿色(#52c41a),失败=红色(#ff4d4f)
+ - 呼吸动画:pulse 2s infinite
diff --git a/openspec/changes/integrate-auto-outbound-call/tasks.md b/openspec/changes/integrate-auto-outbound-call/tasks.md
index d73ba4b..868df30 100644
--- a/openspec/changes/integrate-auto-outbound-call/tasks.md
+++ b/openspec/changes/integrate-auto-outbound-call/tasks.md
@@ -1,66 +1,85 @@
# Implementation Tasks
## 1. 数据存储与状态管理
-- [ ] 1.1 创建 localStorage 键 `outbound_call_jobs` 用于存储 jobId 数组和状态
-- [ ] 1.2 实现 jobId 数据结构设计:`{ jobId, callStatus, startTime, personId, mediationId }`
-- [ ] 1.3 实现状态过滤逻辑:活跃状态保留,终态清除
+- [x] 1.1 创建 localStorage 键 `outbound_call_jobs` 用于存储 jobId 数组和状态
+- [x] 1.2 实现 jobId 数据结构设计:`{ jobId, callStatus, startTime, personId, mediationId }`
+- [x] 1.3 实现状态过滤逻辑:活跃状态保留,终态清除
+- [x] 1.4 新增失败任务存储:创建 `outbound_call_jobs_failed` 键存储失败记录
+- [x] 1.5 实现失败任务去重:按 personId 去重,避免重复显示
+- [x] 1.6 实现自动清理:失败任务超过24小时自动清除
## 2. CaseDataContext 外呼触发逻辑
-- [ ] 2.1 在 `loadCaseData` 方法的第 141 行 `saveToStorage(timelineData)` 之后添加外呼触发调用
-- [ ] 2.2 检查 localStorage 中是否有活跃的 jobId,如有则跳过发起新外呼
-- [ ] 2.3 提取 `mediationId`(timelineData.id)和 `caseId`(timelineData.case_id)构建请求参数
-- [ ] 2.4 调用 `OutboundBotAPIService.makeCallV2({ mediationId, caseId, callAuto: 0, callPersonId: null })`
-- [ ] 2.5 处理 `makeCallV2` 响应:
- - [ ] 2.5.1 解析 `response.data` 数组,提取所有 `errorCode === 0` 的记录
- - [ ] 2.5.2 存储 jobId、callStatus、personId 等信息到 localStorage
- - [ ] 2.5.3 失败时(errorCode !== 0)展示 `message.error()` 并记录控制台日志
-- [ ] 2.6 错误捕获:`makeCallV2` 调用失败时展示友好提示,不阻塞页面加载
+- [x] 2.1 在 `loadCaseData` 方法的第 141 行 `saveToStorage(timelineData)` 之后添加外呼触发调用
+- [x] 2.2 检查 localStorage 中是否有活跃的 jobId,如有则跳过发起新外呼
+- [x] 2.3 提取 `mediationId`(timelineData.id)和 `caseId`(timelineData.case_id)构建请求参数
+- [x] 2.4 调用 `OutboundBotAPIService.makeCallV2({ mediationId, caseId, callAuto: 0, callPersonId: null })`
+- [x] 2.5 处理 `makeCallV2` 响应:
+ - [x] 2.5.1 解析 `response.data` 数组,提取所有 `errorCode === 0` 的记录
+ - [x] 2.5.2 存储 jobId、callStatus、personId 等信息到 localStorage
+ - [x] 2.5.3 失败时(errorCode !== 0)展示 `message.error()` 并记录控制台日志
+- [x] 2.6 错误捕获:`makeCallV2` 调用失败时展示友好提示,不阻塞页面加载
+- [x] 2.7 失败任务处理:存储失败任务到独立的 localStorage 键,支持后续显示
## 3. OutboundCallWidget 轮询改造
-- [ ] 3.1 移除现有 `getCallStatus` 基于 `caseRef` 的轮询逻辑
-- [ ] 3.2 从 localStorage 读取 `outbound_call_jobs`,过滤活跃状态的 jobId
-- [ ] 3.3 实现新的轮询逻辑:
- - [ ] 3.3.1 遍历所有活跃 jobId,调用 `OutboundBotAPIService.getCallStatus({ jobId })`
- - [ ] 3.3.2 更新 `calls` 状态数组,展示通话人和状态信息
- - [ ] 3.3.3 检测终态状态(Succeeded/Failed/Cancelled),从 localStorage 移除对应 jobId
-- [ ] 3.4 设置轮询间隔为 10 秒(`setInterval(fetchCallStatus, 10000)`)
-- [ ] 3.5 实现最大轮询时长限制(2小时):
- - [ ] 3.5.1 记录轮询开始时间
- - [ ] 3.5.2 每次轮询检查是否超时,超时则停止轮询并清理 jobId
-- [ ] 3.6 实现失败重试机制:
- - [ ] 3.6.1 为每个 jobId 维护重试计数器
- - [ ] 3.6.2 单次查询失败时累加计数,最多重试 10 次
- - [ ] 3.6.3 超过 10 次失败后展示 `message.error()` 并移除该 jobId
+- [x] 3.1 移除现有 `getCallStatus` 基于 `caseRef` 的轮询逻辑
+- [x] 3.2 从 localStorage 读取 `outbound_call_jobs`,过滤活跃状态的 jobId
+- [x] 3.3 实现新的轮询逻辑:
+ - [x] 3.3.1 遍历所有活跃 jobId,调用 `OutboundBotAPIService.getCallStatus({ jobId })`
+ - [x] 3.3.2 更新 `calls` 状态数组,展示通话人和状态信息
+ - [x] 3.3.3 检测终态状态(Succeeded/Failed/Cancelled),从 localStorage 移除对应 jobId
+- [x] 3.4 设置轮询间隔为 10 秒(`setInterval(fetchCallStatus, 10000)`)
+- [x] 3.5 实现最大轮询时长限制(2小时):
+ - [x] 3.5.1 记录轮询开始时间
+ - [x] 3.5.2 每次轮询检查是否超时,超时则停止轮询并清理 jobId
+- [x] 3.6 实现失败重试机制:
+ - [x] 3.6.1 为每个 jobId 维护重试计数器
+ - [x] 3.6.2 单次查询失败时累加计数,最多重试 10 次
+ - [x] 3.6.3 超过 10 次失败后展示 `message.error()` 并移除该 jobId
+- [x] 3.7 失败任务显示:读取失败任务并展示在气泡中
+- [x] 3.8 重复任务去重:读取时按 personId 去重,避免重复显示
## 4. 气泡组件显示优化
-- [ ] 4.1 支持多任务显示:当 `calls.length > 1` 时,纵向堆叠展示多个气泡
-- [ ] 4.2 显示字段映射:
- - [ ] 4.2.1 通话人姓名:从 API 响应的 `personId` 或 Mock 数据获取
- - [ ] 4.2.2 通话状态:中文映射(Scheduling→拨号中,Executing→通话中等)
- - [ ] 4.2.3 通话时长:根据 `startTime` 和当前时间计算(格式:MM:SS)
-- [ ] 4.3 终态处理:当所有 jobId 均为终态时,自动隐藏气泡(设置 `isVisible=false`)
+- [x] 4.1 支持多任务显示:当 `calls.length > 1` 时,纵向堆叠展示多个气泡
+- [x] 4.2 显示字段映射:
+ - [x] 4.2.1 通话人姓名:从 API 响应的 `personId` 或 Mock 数据获取
+ - [x] 4.2.2 通话状态:中文映射(Scheduling→拨号中,Executing→通话中等)
+ - [x] 4.2.3 通话时长:根据 `startTime` 和当前时间计算(格式:MM:SS)
+- [x] 4.3 终态处理:当所有 jobId 均为终态时,自动隐藏气泡(设置 `isVisible=false`)
+- [x] 4.4 失败任务视觉优化:蓝色气泡 + 红色呼吸灯状态指示
+- [x] 4.5 成功任务视觉优化:蓝色气泡 + 绿色呼吸灯状态指示
## 5. 组件卸载与清理
-- [ ] 5.1 在 `OutboundCallWidget` 的 `useEffect` 清理函数中停止轮询定时器
-- [ ] 5.2 用户离开页面时(如路由切换),确保定时器被清除
-- [ ] 5.3 避免内存泄漏:组件卸载时取消所有待处理的 API 请求(如使用 AbortController)
+- [x] 5.1 在 `OutboundCallWidget` 的 `useEffect` 清理函数中停止轮询定时器
+- [x] 5.2 用户离开页面时(如路由切换),确保定时器被清除
+- [x] 5.3 避免内存泄漏:组件卸载时取消所有待处理的 API 请求(如使用 AbortController)
## 6. 错误提示与日志
-- [ ] 6.1 `makeCallV2` 失败时:`message.error('发起外呼失败,请稍后重试')` + `console.error(err)`
-- [ ] 6.2 `getCallStatus` 重试超限时:`message.error('获取通话状态失败,请检查网络连接')` + `console.error()`
-- [ ] 6.3 所有关键步骤记录 `console.log`,便于前端调试
+- [x] 6.1 `makeCallV2` 失败时:`message.error('发起外呼失败,请稍后重试')` + `console.error(err)`
+- [x] 6.2 `getCallStatus` 重试超限时:`message.error('获取通话状态失败,请检查网络连接')` + `console.error()`
+- [x] 6.3 所有关键步骤记录 `console.log`,便于前端调试
+- [x] 6.4 失败任务提示:`message.warning('部分联系人外呼失败')` + 控制台日志
## 7. 测试与验证
-- [ ] 7.1 测试场景1:首次进入首页,自动发起外呼,气泡正常弹出
-- [ ] 7.2 测试场景2:刷新页面,检测到活跃 jobId,继续监听不发起新外呼
-- [ ] 7.3 测试场景3:多人外呼(2个jobId),气泡同时展示两条记录
-- [ ] 7.4 测试场景4:通话成功(Succeeded),气泡消失,jobId 清除
-- [ ] 7.5 测试场景5:通话失败(Failed),展示错误提示,jobId 清除
-- [ ] 7.6 测试场景6:API 连续失败 10 次,展示重试超限提示
-- [ ] 7.7 测试场景7:轮询超过 2 小时,自动停止并清理
-- [ ] 7.8 测试场景8:组件卸载时定时器正常清除,无报错
+- [x] 7.1 测试场景1:首次进入首页,自动发起外呼,气泡正常弹出
+- [x] 7.2 测试场景2:刷新页面,检测到活跃 jobId,继续监听不发起新外呼
+- [x] 7.3 测试场景3:多人外呼(2个jobId),气泡同时展示两条记录
+- [x] 7.4 测试场景4:通话成功(Succeeded),气泡消失,jobId 清除
+- [x] 7.5 测试场景5:通话失败(Failed),展示错误提示,jobId 清除
+- [x] 7.6 测试场景6:API 连续失败 10 次,展示重试超限提示
+- [x] 7.7 测试场景7:轮询超过 2 小时,自动停止并清理
+- [x] 7.8 测试场景8:组件卸载时定时器正常清除,无报错
+- [x] 7.9 测试场景9:失败任务正确显示在气泡中,带红色状态指示灯
+- [x] 7.10 测试场景10:重复失败任务自动去重,不重复显示
+- [x] 7.11 测试场景11:失败任务超过24小时自动清理
+
+## 8. 性能优化
+- [x] 8.1 localStorage 读写优化:减少不必要的序列化/反序列化操作
+- [x] 8.2 轮询性能优化:只对活跃任务进行轮询查询
+- [x] 8.3 内存泄漏防护:组件卸载时正确清理所有定时器和事件监听器
## 9. 文档与注释
-- [ ] 9.1 为新增代码添加 JSDoc 注释
-- [ ] 9.2 更新 `OutboundCallWidget.jsx` 头部组件说明
-- [ ] 9.3 在关键逻辑处添加行内注释(jobId 状态判断、轮询逻辑等)
+- [x] 9.1 为新增代码添加 JSDoc 注释
+- [x] 9.2 更新 `OutboundCallWidget.jsx` 头部组件说明
+- [x] 9.3 在关键逻辑处添加行内注释(jobId 状态判断、轮询逻辑等)
+- [x] 9.4 更新 tasks.md 文档,标记完成状态并添加新任务项
+- [x] 9.5 创建开发规范记忆:外呼失败状态指示圆点颜色规范
diff --git a/web-app/src/services/caseService.js b/web-app/src/services/caseService.js
deleted file mode 100644
index 497d066..0000000
--- a/web-app/src/services/caseService.js
+++ /dev/null
@@ -1,38 +0,0 @@
-import { mockCaseList } from '../mocks/caseMocks';
-
-/**
- * 案例服务层
- * 当前使用 Mock 数据,后续可替换为真实 API 调用
- */
-
-/**
- * 获取案例列表
- * @param {Object} params - 查询参数
- * @returns {Promise} - 返回案例列表数据
- */
-export const fetchCaseList = (params) => {
- console.log('fetchCaseList params:', params);
-
- // 模拟 API 延迟
- return new Promise((resolve) => {
- setTimeout(() => {
- resolve(mockCaseList);
- }, 300);
- });
-};
-
-/**
- * 获取案例详情
- * @param {string} id - 案例ID
- * @returns {Promise} - 返回案例详情数据
- */
-export const fetchCaseDetail = (id) => {
- console.log('fetchCaseDetail id:', id);
-
- return new Promise((resolve) => {
- setTimeout(() => {
- const caseDetail = mockCaseList.list.find((item) => item.id === id);
- resolve(caseDetail || null);
- }, 300);
- });
-};
--
Gitblit v1.8.0