From 6f344a5292739e21d0c8f06c346be44c31c38552 Mon Sep 17 00:00:00 2001
From: tony.cheng <chengmingwei_1984122@126.com>
Date: Wed, 11 Feb 2026 10:54:23 +0800
Subject: [PATCH] 修复智能外呼气泡组件变量初始化错误和时间格式问题
---
openspec/changes/integrate-auto-outbound-call/specs/outbound-call-auto-trigger/spec.md | 203 ++++++++++++++++++++++++++++++++++++++++++++++++++
1 files changed, 203 insertions(+), 0 deletions(-)
diff --git a/openspec/changes/integrate-auto-outbound-call/specs/outbound-call-auto-trigger/spec.md b/openspec/changes/integrate-auto-outbound-call/specs/outbound-call-auto-trigger/spec.md
new file mode 100644
index 0000000..47f50db
--- /dev/null
+++ b/openspec/changes/integrate-auto-outbound-call/specs/outbound-call-auto-trigger/spec.md
@@ -0,0 +1,203 @@
+## 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')`
--
Gitblit v1.8.0