## ADDED Requirements ### Requirement: 智能外呼自动触发 系统在首页加载案件数据完成后,SHALL 自动发起 AI 智能外呼,无需用户手动操作。外呼请求应基于案件数据(mediationId、caseId)构建,并确保幂等性(页面刷新时不重复触发)。 #### Scenario: 首次进入首页自动触发外呼 - **GIVEN** 用户首次进入首页,localStorage 中无活跃外呼任务 - **WHEN** `CaseDataContext.loadCaseData()` 完成并返回 `timelineData` - **THEN** 系统自动调用 `OutboundBotAPIService.makeCallV2({ mediationId, caseId, callAuto: 0, callPersonId: null })` - **AND** 成功响应后,从 `response.data` 中提取所有 `errorCode === 0` 的记录 - **AND** 将每条记录的 `jobId`、`callStatus`、`personId`、`mediationId` 存储到 localStorage(键名:`outbound_call_jobs`) - **AND** 右下角智能外呼气泡组件自动弹出,展示"正在与XX电话沟通中..." #### Scenario: 页面刷新时检测活跃任务 - **GIVEN** localStorage 中存在活跃状态的外呼任务(`callStatus` 为 `Scheduling`、`Executing`、`Paused` 或 `Drafted`) - **WHEN** 用户刷新页面,`CaseDataContext.loadCaseData()` 执行 - **THEN** 系统检测到活跃任务,跳过发起新外呼 - **AND** 继续轮询查询现有任务的通话状态 - **AND** 右下角气泡组件根据 localStorage 中的 jobId 恢复显示 #### Scenario: 外呼发起失败处理 - **GIVEN** 用户进入首页,数据加载完成 - **WHEN** 调用 `makeCallV2` 失败(如网络错误、API 返回 500) - **THEN** 系统展示 Ant Design 错误提示:`message.error('发起外呼失败,请稍后重试')` - **AND** 在浏览器控制台输出详细错误日志:`console.error('makeCallV2 failed:', err)` - **AND** 不阻塞页面其他功能,用户可正常使用首页 #### Scenario: API 返回部分成功记录 - **GIVEN** `makeCallV2` 响应包含多条记录,其中部分 `errorCode === 0`(成功),部分 `errorCode !== 0`(失败) - **WHEN** 系统解析响应数据 - **THEN** 仅提取并存储 `errorCode === 0` 的记录到 localStorage - **AND** 对于失败记录(如"外呼次数已达上限"),在控制台输出警告日志 - **AND** 右下角气泡仅展示成功发起的外呼任务 --- ### Requirement: 外呼状态实时监控 系统 SHALL 对活跃外呼任务进行定时轮询,查询通话状态并实时更新右下角气泡组件显示。轮询策略应平衡实时性与服务器压力,采用 10 秒间隔、最大 2 小时轮询时长、失败重试 10 次的机制。 #### Scenario: 定时轮询查询通话状态 - **GIVEN** localStorage 中存在活跃状态的 jobId(`callStatus` 为 `Scheduling`、`Executing`、`Paused` 或 `Drafted`) - **WHEN** `OutboundCallWidget` 组件挂载后启动轮询定时器(间隔 10 秒) - **THEN** 遍历所有活跃 jobId,调用 `OutboundBotAPIService.getCallStatus({ jobId })` - **AND** 解析每个响应的 `callStatus`,更新组件内部 `calls` 状态数组 - **AND** 气泡组件实时显示通话人姓名、通话状态(中文)、通话时长(格式:MM:SS) #### Scenario: 检测终态状态并停止轮询 - **GIVEN** 轮询过程中某个 jobId 的 `callStatus` 变为 `Succeeded`(成功) - **WHEN** 系统检测到终态状态 - **THEN** 立即从 localStorage 的 `outbound_call_jobs` 中移除该 jobId - **AND** 更新 `calls` 状态数组,移除对应记录 - **AND** 如果所有 jobId 均为终态,右下角气泡自动隐藏(`isVisible=false`) - **AND** 停止该 jobId 的后续轮询 #### Scenario: 轮询超时自动停止 - **GIVEN** 某个 jobId 的轮询已持续 2 小时(从 `pollStartTime` 开始计时) - **WHEN** 系统检测到轮询时长超过 7200 秒 - **THEN** 自动停止该 jobId 的轮询,从 localStorage 中移除 - **AND** 在控制台输出警告日志:`console.warn('轮询超时,jobId: xxx')` - **AND** 不展示错误提示,避免打扰用户 #### Scenario: 单次查询失败重试机制 - **GIVEN** 调用 `getCallStatus` 失败(如网络抖动、API 返回 500) - **WHEN** 系统检测到单次查询失败 - **THEN** 累加该 jobId 的 `retryCount` 计数器 - **AND** 保留该 jobId 在 localStorage 中,下次轮询继续重试 - **AND** 在控制台输出日志:`console.warn('查询失败,重试次数: X/10, jobId: xxx')` #### Scenario: 连续失败 10 次后移除任务 - **GIVEN** 某个 jobId 的 `retryCount` 已达到 10 - **WHEN** 第 10 次查询失败 - **THEN** 展示 Ant Design 错误提示:`message.error('获取通话状态失败,请检查网络连接')` - **AND** 从 localStorage 中移除该 jobId - **AND** 在控制台输出错误日志:`console.error('重试次数超限,jobId: xxx')` - **AND** 更新气泡组件,移除该任务显示 --- ### Requirement: 多任务并行支持 系统 SHALL 支持同时展示多个外呼任务的状态(如申请人、被申请人同时外呼),气泡组件采用纵向堆叠布局,每个任务独立显示通话人和状态信息。 #### Scenario: 同时展示多个外呼任务 - **GIVEN** `makeCallV2` 响应返回 2 条成功记录(2 个 jobId) - **WHEN** 系统存储到 localStorage 并启动轮询 - **THEN** 右下角气泡组件纵向堆叠展示 2 个气泡 - **AND** 每个气泡独立显示:通话人姓名、通话状态、通话时长 - **AND** 第一个气泡位于下方,第二个气泡位于上方(堆叠顺序) #### Scenario: 单个任务结束后部分气泡消失 - **GIVEN** 存在 2 个活跃外呼任务 - **WHEN** 其中 1 个任务状态变为 `Succeeded`(成功) - **THEN** 对应气泡立即消失 - **AND** 另一个任务的气泡继续显示并轮询 - **AND** localStorage 中仅保留 1 个活跃 jobId #### Scenario: 所有任务结束后气泡完全隐藏 - **GIVEN** 所有外呼任务均已结束(`callStatus` 为 `Succeeded`、`Failed` 或 `Cancelled`) - **WHEN** 系统检测到 localStorage 中无活跃 jobId - **THEN** 右下角气泡完全隐藏(包括最小化的 AI 客服图标) - **AND** 停止轮询定时器 --- ### Requirement: 组件生命周期管理 系统 SHALL 在组件卸载或用户离开页面时正确清理定时器和资源,避免内存泄漏和控制台报错。 #### Scenario: 组件卸载时清理定时器 - **GIVEN** `OutboundCallWidget` 组件已挂载并启动轮询定时器 - **WHEN** 用户导航到其他页面,组件卸载 - **THEN** 在 `useEffect` 的清理函数中调用 `clearInterval(intervalId)` - **AND** 停止所有正在进行的轮询 - **AND** 不抛出任何错误或警告到控制台 #### Scenario: 路由切换时保留 localStorage 状态 - **GIVEN** 用户在首页查看外呼气泡,然后切换到其他页面 - **WHEN** `OutboundCallWidget` 组件卸载 - **THEN** localStorage 中的 `outbound_call_jobs` 数据保持不变 - **AND** 用户返回首页时,组件重新挂载并从 localStorage 恢复状态 - **AND** 继续轮询之前的活跃任务 #### Scenario: 避免组件卸载后的异步回调执行 - **GIVEN** 组件发起了 `getCallStatus` 异步请求 - **WHEN** 请求尚未返回时,组件被卸载 - **THEN** 使用 `useRef` 记录 `isMounted` 状态 - **AND** 异步回调中检查 `isMounted`,如果组件已卸载则不执行状态更新 - **AND** 不抛出"Can't perform a React state update on an unmounted component"警告 --- ### Requirement: 通话状态中文映射 系统 SHALL 将 API 返回的英文通话状态映射为中文,便于用户理解当前外呼进度。 #### Scenario: 状态中文映射规则 - **GIVEN** API 返回的 `callStatus` 为英文值 - **WHEN** 气泡组件渲染通话状态 - **THEN** 按照以下规则显示中文: - `Scheduling` → "拨号中" - `Executing` → "通话中" - `Paused` → "已暂停" - `Drafted` → "草稿" - `Succeeded` → "已完成" - `Failed` → "失败" - `Cancelled` → "已取消" - **AND** 未知状态显示原始值 #### Scenario: 通话时长计算 - **GIVEN** API 响应包含 `startTime` 字段(时间戳或 ISO 格式字符串) - **WHEN** 气泡组件显示通话时长 - **THEN** 计算当前时间与 `startTime` 的差值(秒) - **AND** 格式化为"MM:SS"格式(如"02:35"表示 2 分 35 秒) - **AND** 每次轮询更新时重新计算并刷新显示 --- ### Requirement: 错误提示与日志记录 系统 SHALL 在关键操作失败时提供友好的错误提示,并在浏览器控制台输出详细日志,便于开发调试。 #### Scenario: makeCallV2 失败提示 - **GIVEN** 调用 `makeCallV2` 失败(如网络错误、API 返回 500) - **WHEN** 捕获到异常 - **THEN** 展示 Ant Design 错误提示:`message.error('发起外呼失败,请稍后重试')` - **AND** 在控制台输出错误日志:`console.error('makeCallV2 failed:', err)` #### Scenario: getCallStatus 重试超限提示 - **GIVEN** 某个 jobId 的查询连续失败 10 次 - **WHEN** 第 10 次失败后 - **THEN** 展示 Ant Design 错误提示:`message.error('获取通话状态失败,请检查网络连接')` - **AND** 在控制台输出错误日志:`console.error('重试次数超限,jobId: xxx')` #### Scenario: 关键步骤日志记录 - **GIVEN** 系统执行外呼触发、轮询、状态更新等关键步骤 - **WHEN** 每个步骤完成 - **THEN** 在控制台输出相应日志: - `console.log('发起外呼,mediationId: X, caseId: Y')` - `console.log('存储 jobId: X, callStatus: Y')` - `console.log('轮询查询 jobId: X')` - `console.log('检测到终态,移除 jobId: X')`