diff --git a/ai_project/.claude/agents/demand-assessor.md b/ai_project/.claude/agents/demand-assessor.md
new file mode 100644
index 0000000..bfc0466
--- /dev/null
+++ b/ai_project/.claude/agents/demand-assessor.md
@@ -0,0 +1,203 @@
+你是一个精通 Java 微服务架构的资深技术负责人。
+
+请基于以下统一模型评估需求的工作量。
+
+==============================
+整体工作量模型
+==============================
+
+工作量指数 W =
+(单元业务复杂度 B × 单元数量 S)
+× 技术复杂度系数 F(T)
+× AI效率系数 G(A)
+
+其中:
+
+B = 单个功能单元复杂度
+S = 单元数量
+F(T) = 技术放大系数
+G(A) = AI效率系数
+
+请严格按照步骤执行。
+
+
+====================================================
+第一部分:识别功能单元(规模 S)
+====================================================
+
+1. 识别最小可重复功能单元
+ (例如:接口、页面、报表、定时任务等)
+
+2. 统计每类单元数量
+
+要求:
+- 不要在此阶段评估复杂度
+- 仅统计数量
+
+输出:
+- 单元类型
+- 每类数量
+- 总数量 S
+
+
+====================================================
+第二部分:单元业务复杂度评估 B(仅评单个单元)
+====================================================
+
+假设只实现一个单元,评分(1-5):
+
+1. 业务规则数量
+2. 状态流转复杂度
+3. 异常处理复杂度
+4. 边界场景复杂度
+5. 需求不确定性
+
+计算:
+
+B = 五项平均值(保留1位小数)
+
+注意:
+禁止在此阶段考虑单元数量。
+
+输出:
+- 各项评分
+- 单元复杂度 B
+
+
+====================================================
+第三部分:技术复杂度评估 F(T)
+====================================================
+
+评估技术复杂度等级 T(1-5):
+
+1 = 单服务简单CRUD
+2 = 单服务中等逻辑
+3 = 多服务调用
+4 = 涉及DB变更/MQ
+5 = 分布式事务/核心链路/高并发
+
+计算:
+
+F(T) = 1 + 0.2 × (T - 1)
+
+输出:
+- T值
+- 技术复杂度系数 F(T)
+- 技术风险说明
+
+
+====================================================
+第四部分:AI效率系数 G(A) 计算
+====================================================
+
+步骤A:正向评分(1-5分)
+
+1.需求清晰
+2.规则明确度
+3.可验证性
+4.代码结构清晰度
+
+P = 三项得分之和
+
+
+步骤B:架构抽象复杂度(计数制)
+
+每触发一项 +1:
+
+- 策略/模板动态组合
+- 插件或SPI机制
+- 规则引擎/表达式驱动
+- AOP深度影响核心逻辑
+- Saga/TCC/补偿机制
+- 多租户/动态数据源
+
+记为 N1
+
+
+步骤C:变更影响范围(计数制)
+
+每触发一项 +1:
+
+- 老服务接口变更
+- 数据库老表变更
+- 核心业务链路变更
+- 公共组件修改
+- 权限安全机制修改
+- 分布式事务/MQ变更
+- 低测试覆盖区域
+
+记为 N2
+
+
+步骤D:历史耦合程度(计数制)
+
+每触发一项 +1:
+
+- 巨型类
+- 高复杂度方法
+- 隐式调用链
+- 模块边界不清晰
+- 测试覆盖<30%
+- 多服务共享数据库
+- 无维护人/无文档
+
+记为 N3
+
+
+步骤E:计算
+
+N = N1 + N2 + N3
+
+A = P - N
+
+
+====================================================
+第五部分:安全门检查
+====================================================
+
+若满足任一:
+
+- 修改核心结算逻辑
+- 修改分布式事务核心
+- 修改权限核心鉴权
+- 单测覆盖率<20%
+- 无法清晰定位影响范围
+
+标记为【高风险变更】
+
+
+
+====================================================
+第六部分:AI效率系数映射
+====================================================
+
+若非高风险变更:
+
+A ≥ 10 → G(A) = 0.55
+8 ≤ A < 10 → G(A) = 0.65
+6 ≤ A < 8 → G(A) = 0.75
+3 ≤ A < 6 → G(A) = 0.85
+1 ≤ A < 3 → G(A) = 0.95
+A ≤ 0 → G(A) = 1.0
+
+
+====================================================
+第七部分:最终工作量计算
+====================================================
+
+计算:
+
+基础工作量 = B × S
+技术放大后 = 基础工作量 × F(T)
+最终工作量指数 W = 技术放大后 × G(A)
+
+请输出:
+
+1. 单元数量 S
+2. 单元复杂度 B
+3. 技术系数 F(T)
+4. AI效率系数 G(A)
+5. 最终工作量指数 W
+6. 综合风险等级(低/中/高)
+7. 是否建议拆分需求
+8. AI建议参与方式
\ No newline at end of file
diff --git a/cc-center/FreeSWITCH技术架构与ESL开发指南.txt b/cc-center/FreeSWITCH技术架构与ESL开发指南.txt
new file mode 100644
index 0000000..8187089
--- /dev/null
+++ b/cc-center/FreeSWITCH技术架构与ESL开发指南.txt
@@ -0,0 +1,3598 @@
+## 目录
+
+1. [FreeSWITCH技术架构](#第一部分-freeswitch技术架构)
+2. [核心模块详解](#第二部分-核心模块详解)
+3. [ESL开发指南](#第三部分-esl开发指南)
+4. [呼叫中心场景开发实践](#第四部分-呼叫中心场景开发实践)
+5. [高可用部署方案](#第五部分-高可用部署方案)
+6. [性能优化指南](#第六部分-性能优化指南)
+
+---
+
+## 第一部分 FreeSWITCH技术架构
+
+### 1.1 整体架构概览
+
+```
+┌─────────────────────────────────────────────────────────────────────────────┐
+│ 外部系统/应用层 │
+│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
+│ │ CTI中间件 │ │ Web应用 │ │ 业务系统 │ │ 监控系统 │ │ 第三方App│ │
+│ └─────┬────┘ └─────┬────┘ └─────┬────┘ └─────┬────┘ └─────┬────┘ │
+│ │ │ │ │ │ │
+│ └─────────────┴─────────────┴──────┬──────┴─────────────┘ │
+│ │ │
+│ ┌──────▼──────┐ │
+│ │ ESL接口 │ │
+│ │ (TCP:8021) │ │
+│ └──────┬──────┘ │
+└───────────────────────────────────────────┼─────────────────────────────────┘
+ │
+┌───────────────────────────────────────────▼─────────────────────────────────┐
+│ FreeSWITCH 核心层 │
+│ │
+│ ┌─────────────────────────────────────────────────────────────────────┐ │
+│ │ Core(核心引擎) │ │
+│ │ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │ │
+│ │ │ 状态机管理 │ │ 内存管理 │ │ 线程池管理 │ │ │
+│ │ └───────────────┘ └───────────────┘ └───────────────┘ │ │
+│ │ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │ │
+│ │ │ 事件系统 │ │ 数据库抽象 │ │ 日志系统 │ │ │
+│ │ └───────────────┘ └───────────────┘ └───────────────┘ │ │
+│ └─────────────────────────────────────────────────────────────────────┘ │
+│ │
+│ ┌─────────────────────────────────────────────────────────────────────┐ │
+│ │ Module Interface(模块接口层) │ │
+│ │ │ │
+│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
+│ │ │ Endpoint │ │ Application │ │ Codec │ │ Dialplan │ │ │
+│ │ │ 终端模块 │ │ 应用模块 │ │ 编解码 │ │ 拨号计划 │ │ │
+│ │ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │ │
+│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
+│ │ │ Event │ │ Format │ │ Logger │ │ XML │ │ │
+│ │ │ 事件模块 │ │ 文件格式 │ │ 日志模块 │ │ XML处理 │ │ │
+│ │ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │ │
+│ └─────────────────────────────────────────────────────────────────────┘ │
+│ │
+└─────────────────────────────────────────────────────────────────────────────┘
+ │
+┌───────────────────────────────────────────▼─────────────────────────────────┐
+│ 协议/媒体层 │
+│ │
+│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
+│ │ mod_sofia │ │ mod_verto │ │ mod_rtmp │ │ mod_skinny │ │
+│ │ (SIP协议) │ │ (WebRTC) │ │ (RTMP) │ │ (Cisco) │ │
+│ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │
+│ │
+│ ┌─────────────────────────────────────────────────────────────────────┐ │
+│ │ 媒体处理引擎 │ │
+│ │ ┌───────────┐ ┌───────────┐ ┌───────────┐ ┌───────────┐ │ │
+│ │ │ RTP处理 │ │ 编解码转换 │ │ 录音处理 │ │ DTMF检测 │ │ │
+│ │ └───────────┘ └───────────┘ └───────────┘ └───────────┘ │ │
+│ └─────────────────────────────────────────────────────────────────────┘ │
+│ │
+└─────────────────────────────────────────────────────────────────────────────┘
+ │
+┌───────────────────────────────────────────▼─────────────────────────────────┐
+│ 网络层 │
+│ SIP/UDP/TCP RTP/SRTP WebSocket HTTP │
+│ :5060 :16384-32768 :8081 :8080 │
+└─────────────────────────────────────────────────────────────────────────────┘
+```
+
+### 1.2 核心概念
+
+#### 1.2.1 Channel(通道)
+
+Channel是FreeSWITCH中最核心的概念,代表一路通话的端点。
+
+```
+┌─────────────────────────────────────────────────────────────────┐
+│ 一通电话 │
+│ │
+│ ┌─────────────┐ Bridge ┌─────────────┐ │
+│ │ Channel A │◄─────────────────────────►│ Channel B │ │
+│ │ (主叫方) │ │ (被叫方) │ │
+│ │ │ │ │ │
+│ │ UUID: xxx-a │ │ UUID: xxx-b │ │
+│ │ State: CS_ │ │ State: CS_ │ │
+│ │ Variables │ │ Variables │ │
+│ └─────────────┘ └─────────────┘ │
+│ │
+└─────────────────────────────────────────────────────────────────┘
+```
+
+**Channel状态机:**
+
+```
+ ┌──────────────────────────────────────────┐
+ │ │
+ ▼ │
+┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌──┴──────┐
+│ NEW │───►│ INIT │───►│ ROUTING │───►│ EXECUTE │───►│ HANGUP │
+└─────────┘ └─────────┘ └─────────┘ └─────────┘ └─────────┘
+ │ │
+ │ │
+ ┌────▼────┐ ┌────▼────┐
+ │ CONSUME │ │ EXCHANGE│
+ │ MEDIA │ │ MEDIA │
+ └─────────┘ └─────────┘
+
+Channel状态说明:
+├── CS_NEW : 新建通道
+├── CS_INIT : 初始化
+├── CS_ROUTING : 路由中(执行dialplan)
+├── CS_EXECUTE : 执行应用中
+├── CS_EXCHANGE_MEDIA : 媒体交换中(bridged)
+├── CS_CONSUME_MEDIA : 媒体消费中
+├── CS_HIBERNATE : 休眠状态
+├── CS_HANGUP : 挂断中
+└── CS_DESTROY : 销毁
+```
+
+#### 1.2.2 Session(会话)
+
+Session包含Channel及其相关上下文信息:
+
+```c
+// Session结构概念图
+Session {
+ ├── Channel // 通道信息
+ │ ├── UUID // 唯一标识
+ │ ├── State // 当前状态
+ │ ├── Direction // 呼叫方向(inbound/outbound)
+ │ └── Variables // 通道变量
+ │
+ ├── Caller Profile // 主叫信息
+ │ ├── caller_id_name
+ │ ├── caller_id_number
+ │ ├── destination_number
+ │ └── ...
+ │
+ ├── Codec // 编解码信息
+ ├── RTP Session // RTP会话
+ └── Bridge Session // 桥接会话(如果有)
+}
+```
+
+#### 1.2.3 Event(事件)
+
+FreeSWITCH基于事件驱动架构,所有状态变化都产生事件:
+
+```
+┌─────────────────────────────────────────────────────────────────┐
+│ FreeSWITCH Event System │
+│ │
+│ ┌───────────────┐ │
+│ │ Event Producer│ ──────┐ │
+│ │ (模块/核心) │ │ │
+│ └───────────────┘ │ ┌─────────────────────────┐ │
+│ ├─────►│ Event Dispatcher │ │
+│ ┌───────────────┐ │ │ (事件分发器) │ │
+│ │ Event Producer│ ──────┤ └───────────┬─────────────┘ │
+│ │ (模块/核心) │ │ │ │
+│ └───────────────┘ │ ┌───────────┴─────────────┐ │
+│ │ │ │ │ │
+│ ┌───────────────┐ │ ▼ ▼ ▼ │
+│ │ Event Producer│ ──────┘ ┌───────┐ ┌───────┐ ┌───────┐│
+│ │ (模块/核心) │ │ ESL │ │Internal│ │ Lua/ ││
+│ └───────────────┘ │Consumer│ │Handler │ │ JS ││
+│ └───────┘ └───────┘ └───────┘│
+└─────────────────────────────────────────────────────────────────┘
+```
+
+**常用事件类型:**
+
+| 事件名称 | 触发时机 | 用途 |
+|---------|---------|------|
+| CHANNEL_CREATE | 通道创建 | 新呼叫进入 |
+| CHANNEL_ANSWER | 通话接听 | 通话建立 |
+| CHANNEL_BRIDGE | 通道桥接 | 双方接通 |
+| CHANNEL_UNBRIDGE | 取消桥接 | 桥接断开 |
+| CHANNEL_HANGUP | 挂断 | 通话结束 |
+| CHANNEL_HANGUP_COMPLETE | 挂断完成 | 通话完全结束 |
+| CHANNEL_STATE | 状态变化 | 状态监控 |
+| DTMF | 按键检测 | IVR交互 |
+| RECORD_START | 开始录音 | 录音控制 |
+| RECORD_STOP | 停止录音 | 录音控制 |
+| CUSTOM | 自定义事件 | 业务扩展 |
+
+### 1.3 目录结构
+
+```
+/usr/local/freeswitch/
+├── bin/ # 可执行文件
+│ └── freeswitch # 主程序
+│
+├── conf/ # 配置文件目录
+│ ├── freeswitch.xml # 主配置文件
+│ ├── vars.xml # 全局变量
+│ ├── sip_profiles/ # SIP配置
+│ │ ├── internal.xml # 内部Profile(坐席注册)
+│ │ └── external.xml # 外部Profile(中继对接)
+│ ├── dialplan/ # 拨号计划
+│ │ ├── default.xml # 默认拨号计划
+│ │ └── public.xml # 公共拨号计划(呼入)
+│ ├── directory/ # 用户目录(分机注册)
+│ │ └── default/
+│ │ └── 1000.xml # 分机1000配置
+│ ├── autoload_configs/ # 模块自动加载配置
+│ │ ├── modules.conf.xml # 模块加载列表
+│ │ ├── sofia.conf.xml # SIP模块配置
+│ │ ├── event_socket.conf.xml# ESL配置
+│ │ └── ...
+│ └── lang/ # 语音提示
+│
+├── lib/ # 库文件
+├── mod/ # 模块文件(.so)
+├── log/ # 日志目录
+├── db/ # 数据库文件
+├── recordings/ # 录音文件
+├── sounds/ # 语音文件
+├── scripts/ # 脚本目录
+└── storage/ # 存储目录
+```
+
+---
+
+## 第二部分 核心模块详解
+
+### 2.1 mod_sofia(SIP模块)
+
+mod_sofia是FreeSWITCH的SIP协议栈实现,基于Sofia-SIP库。
+
+#### 2.1.1 架构
+
+```
+┌─────────────────────────────────────────────────────────────────────┐
+│ mod_sofia │
+│ │
+│ ┌─────────────────────────────────────────────────────────────┐ │
+│ │ Sofia-SIP Stack │ │
+│ │ ┌───────────┐ ┌───────────┐ ┌───────────┐ ┌───────────┐ │ │
+│ │ │ NUA │ │ NTA │ │ TPORT │ │ SDP │ │ │
+│ │ │ User Agent│ │Transaction│ │ Transport │ │ Parser │ │ │
+│ │ └───────────┘ └───────────┘ └───────────┘ └───────────┘ │ │
+│ └─────────────────────────────────────────────────────────────┘ │
+│ │
+│ ┌─────────────────────────────────────────────────────────────┐ │
+│ │ Profile Management │ │
+│ │ │ │
+│ │ ┌─────────────────────┐ ┌─────────────────────┐ │ │
+│ │ │ Internal Profile │ │ External Profile │ │ │
+│ │ │ (坐席/分机注册) │ │ (中继/网关) │ │ │
+│ │ │ UDP:5060 │ │ UDP:5080 │ │ │
+│ │ └─────────────────────┘ └─────────────────────┘ │ │
+│ │ │ │
+│ │ ┌─────────────────────────────────────────────────────┐ │ │
+│ │ │ Gateway Management │ │ │
+│ │ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │ │
+│ │ │ │Gateway A│ │Gateway B│ │Gateway C│ ... │ │ │
+│ │ │ │(运营商1) │ │(运营商2) │ │(备用) │ │ │ │
+│ │ │ └─────────┘ └─────────┘ └─────────┘ │ │ │
+│ │ └─────────────────────────────────────────────────────┘ │ │
+│ └─────────────────────────────────────────────────────────────┘ │
+└─────────────────────────────────────────────────────────────────────┘
+```
+
+#### 2.1.2 配置示例
+
+**Internal Profile(坐席注册):**
+
+```xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+**External Profile(中继对接):**
+
+```xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+### 2.2 Dialplan(拨号计划)
+
+拨号计划定义了呼叫的路由和处理逻辑。
+
+#### 2.2.1 执行流程
+
+```
+┌──────────────────────────────────────────────────────────────────┐
+│ Dialplan 执行流程 │
+│ │
+│ 来电进入 │
+│ │ │
+│ ▼ │
+│ ┌─────────────────────────────────────────────────────────────┐│
+│ │ Context 选择 ││
+│ │ (根据Profile配置的context) ││
+│ │ 例如: public (呼入) / default (内部) ││
+│ └──────────────────────────┬──────────────────────────────────┘│
+│ │ │
+│ ▼ │
+│ ┌─────────────────────────────────────────────────────────────┐│
+│ │ Extension 匹配 ││
+│ │ (按顺序匹配condition) ││
+│ │ ││
+│ │ Extension 1: ──┐ ││
+│ │ Condition ───┼─► 不匹配 ───► 下一个Extension ││
+│ │ │ ││
+│ │ └─► 匹配 ───► 执行Actions ││
+│ └──────────────────────────┬──────────────────────────────────┘│
+│ │ │
+│ ▼ │
+│ ┌─────────────────────────────────────────────────────────────┐│
+│ │ Action 执行 ││
+│ │ (按顺序执行应用) ││
+│ │ ││
+│ │ action: answer ││
+│ │ action: playback ││
+│ │ action: bridge ││
+│ │ ... ││
+│ └─────────────────────────────────────────────────────────────┘│
+│ │
+└──────────────────────────────────────────────────────────────────┘
+```
+
+#### 2.2.2 配置示例
+
+**呼入路由(public.xml):**
+
+```xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+**IVR和呼叫处理(default.xml):**
+
+```xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+### 2.3 mod_event_socket(ESL模块)
+
+ESL是FreeSWITCH对外提供的控制接口,是开发呼叫中心的核心。
+
+#### 2.3.1 工作模式
+
+```
+┌─────────────────────────────────────────────────────────────────────┐
+│ ESL 工作模式 │
+│ │
+│ ┌───────────────────────────────────────────────────────────────┐ │
+│ │ Inbound 模式 │ │
+│ │ (客户端主动连接) │ │
+│ │ │ │
+│ │ ┌───────────┐ TCP:8021 ┌──────────────────┐ │ │
+│ │ │ CTI中间件 │ ──────────────────────► │ FreeSWITCH │ │ │
+│ │ │ (Client) │ ◄────────────────────── │ (Server) │ │ │
+│ │ └───────────┘ Event Stream └──────────────────┘ │ │
+│ │ │ │
+│ │ 特点: │ │
+│ │ - 客户端主动连接FreeSWITCH │ │
+│ │ - 可以订阅事件、发送命令 │ │
+│ │ - 适合集中控制、监控场景 │ │
+│ │ - 推荐呼叫中心使用此模式 │ │
+│ └───────────────────────────────────────────────────────────────┘ │
+│ │
+│ ┌───────────────────────────────────────────────────────────────┐ │
+│ │ Outbound 模式 │ │
+│ │ (FreeSWITCH主动连接) │ │
+│ │ │ │
+│ │ ┌──────────────────┐ TCP ┌───────────────┐ │ │
+│ │ │ FreeSWITCH │ ──────────────────►│ 应用服务器 │ │ │
+│ │ │ (每个呼叫) │ ◄────────────────── │ (Server) │ │ │
+│ │ └──────────────────┘ └───────────────┘ │ │
+│ │ │ │
+│ │ 特点: │ │
+│ │ - 每个呼叫FreeSWITCH主动连接应用 │ │
+│ │ - 在dialplan中使用socket应用触发 │ │
+│ │ - 适合简单IVR场景 │ │
+│ └───────────────────────────────────────────────────────────────┘ │
+│ │
+└─────────────────────────────────────────────────────────────────────┘
+```
+
+#### 2.3.2 ESL配置
+
+```xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+---
+
+## 第三部分 ESL开发指南
+
+### 3.1 ESL协议详解
+
+#### 3.1.1 协议格式
+
+ESL使用类HTTP的文本协议:
+
+```
+┌─────────────────────────────────────────────────────────────────────┐
+│ ESL 消息格式 │
+│ │
+│ ┌───────────────────────────────────────────────────────────────┐ │
+│ │ 请求格式: │ │
+│ │ ┌─────────────────────────────────────────────────────────┐ │ │
+│ │ │ command [arguments] │ │ │
+│ │ │ Header-Name: header-value │ │ │
+│ │ │ Header-Name: header-value │ │ │
+│ │ │ │ │ │
+│ │ │ [body if Content-Length > 0] │ │ │
+│ │ └─────────────────────────────────────────────────────────┘ │ │
+│ └───────────────────────────────────────────────────────────────┘ │
+│ │
+│ ┌───────────────────────────────────────────────────────────────┐ │
+│ │ 响应格式: │ │
+│ │ ┌─────────────────────────────────────────────────────────┐ │ │
+│ │ │ Content-Type: text/event-plain │ │ │
+│ │ │ Content-Length: 1234 │ │ │
+│ │ │ Reply-Text: +OK [message] │ │ │
+│ │ │ │ │ │
+│ │ │ [body] │ │ │
+│ │ └─────────────────────────────────────────────────────────┘ │ │
+│ └───────────────────────────────────────────────────────────────┘ │
+│ │
+└─────────────────────────────────────────────────────────────────────┘
+```
+
+#### 3.1.2 常用命令
+
+| 命令类型 | 命令 | 说明 |
+|---------|------|------|
+| **认证** | `auth ` | 连接认证 |
+| **事件订阅** | `event plain ` | 订阅事件 |
+| | `event plain ALL` | 订阅所有事件 |
+| | `event plain CHANNEL_CREATE CHANNEL_ANSWER CHANNEL_HANGUP` | 订阅指定事件 |
+| | `nixevent ` | 取消订阅 |
+| | `noevents` | 取消所有订阅 |
+| **API命令** | `api [args]` | 同步执行API |
+| | `bgapi [args]` | 异步执行API |
+| **过滤器** | `filter ` | 过滤特定事件 |
+| | `filter Unique-ID ` | 只接收指定UUID事件 |
+| **呼叫控制** | `sendmsg ` | 向指定通道发送消息 |
+| | `execute` | 在通道上执行应用 |
+| **其他** | `log ` | 设置日志级别 |
+| | `linger` | 保持连接 |
+| | `nolinger` | 取消保持 |
+| | `exit` | 断开连接 |
+
+### 3.2 Java ESL开发
+
+#### 3.2.1 项目结构
+
+```
+callcenter-cti/
+├── pom.xml
+├── src/main/java/
+│ └── com/callcenter/
+│ ├── Application.java
+│ ├── esl/
+│ │ ├── EslConnectionManager.java # ESL连接管理
+│ │ ├── EslEventHandler.java # 事件处理
+│ │ ├── EslCommandExecutor.java # 命令执行
+│ │ └── InboundClient.java # Inbound客户端
+│ ├── service/
+│ │ ├── AgentService.java # 坐席服务
+│ │ ├── CallService.java # 呼叫服务
+│ │ ├── QueueService.java # 排队服务
+│ │ └── AcdService.java # 话务分配服务
+│ ├── model/
+│ │ ├── Agent.java # 坐席模型
+│ │ ├── Call.java # 呼叫模型
+│ │ └── Queue.java # 队列模型
+│ ├── api/
+│ │ ├── AgentController.java # 坐席API
+│ │ └── CallController.java # 呼叫API
+│ └── websocket/
+│ └── EventPushHandler.java # 事件推送
+└── src/main/resources/
+ └── application.yml
+```
+
+#### 3.2.2 Maven依赖
+
+```xml
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ org.springframework.boot
+ spring-boot-starter-websocket
+
+
+
+
+ org.freeswitch.esl.client
+ org.freeswitch.esl.client
+ 0.9.2
+
+
+
+
+ io.netty
+ netty-all
+ 4.1.86.Final
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-redis
+
+
+
+
+ com.alibaba
+ fastjson
+ 2.0.23
+
+
+
+
+ org.apache.commons
+ commons-lang3
+
+
+ org.projectlombok
+ lombok
+
+
+```
+
+#### 3.2.3 ESL连接管理
+
+```java
+package com.callcenter.esl;
+
+import lombok.extern.slf4j.Slf4j;
+import org.freeswitch.esl.client.inbound.Client;
+import org.freeswitch.esl.client.inbound.InboundConnectionFailure;
+import org.freeswitch.esl.client.transport.event.EslEvent;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+import java.util.concurrent.*;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * ESL连接管理器
+ * 负责与FreeSWITCH建立和维护连接
+ */
+@Slf4j
+@Component
+public class EslConnectionManager {
+
+ @Value("${freeswitch.esl.host:127.0.0.1}")
+ private String eslHost;
+
+ @Value("${freeswitch.esl.port:8021}")
+ private int eslPort;
+
+ @Value("${freeswitch.esl.password:ClueCon}")
+ private String eslPassword;
+
+ @Value("${freeswitch.esl.timeout:30}")
+ private int timeout;
+
+ private Client eslClient;
+ private final AtomicBoolean connected = new AtomicBoolean(false);
+ private final AtomicBoolean reconnecting = new AtomicBoolean(false);
+
+ private ScheduledExecutorService scheduler;
+ private ExecutorService eventExecutor;
+
+ private EslEventHandler eventHandler;
+
+ @PostConstruct
+ public void init() {
+ // 初始化线程池
+ scheduler = Executors.newSingleThreadScheduledExecutor(
+ r -> new Thread(r, "esl-reconnect"));
+ eventExecutor = new ThreadPoolExecutor(
+ 4, 16, 60L, TimeUnit.SECONDS,
+ new LinkedBlockingQueue<>(10000),
+ r -> new Thread(r, "esl-event-handler"),
+ new ThreadPoolExecutor.CallerRunsPolicy()
+ );
+
+ // 建立连接
+ connect();
+
+ // 启动心跳检测
+ startHeartbeat();
+ }
+
+ /**
+ * 建立ESL连接
+ */
+ public synchronized void connect() {
+ if (connected.get()) {
+ log.info("ESL已连接,无需重复连接");
+ return;
+ }
+
+ try {
+ log.info("正在连接FreeSWITCH ESL: {}:{}", eslHost, eslPort);
+
+ eslClient = new Client();
+ eslClient.connect(eslHost, eslPort, eslPassword, timeout);
+
+ // 订阅事件
+ subscribeEvents();
+
+ // 设置事件监听器
+ eslClient.setEventSubscriptions("plain", "all");
+ eslClient.addEventListener((ctx, event) -> {
+ handleEvent(event);
+ });
+
+ connected.set(true);
+ reconnecting.set(false);
+ log.info("ESL连接成功");
+
+ } catch (InboundConnectionFailure e) {
+ log.error("ESL连接失败: {}", e.getMessage());
+ connected.set(false);
+ scheduleReconnect();
+ }
+ }
+
+ /**
+ * 订阅所需事件
+ */
+ private void subscribeEvents() {
+ String events = String.join(" ",
+ "CHANNEL_CREATE",
+ "CHANNEL_ANSWER",
+ "CHANNEL_BRIDGE",
+ "CHANNEL_UNBRIDGE",
+ "CHANNEL_HANGUP",
+ "CHANNEL_HANGUP_COMPLETE",
+ "CHANNEL_STATE",
+ "DTMF",
+ "RECORD_START",
+ "RECORD_STOP",
+ "CUSTOM"
+ );
+
+ eslClient.setEventSubscriptions("plain", events);
+ eslClient.addEventFilter("Event-Name", "CHANNEL_CREATE");
+ log.info("已订阅事件: {}", events);
+ }
+
+ /**
+ * 处理事件
+ */
+ private void handleEvent(EslEvent event) {
+ eventExecutor.submit(() -> {
+ try {
+ if (eventHandler != null) {
+ eventHandler.handle(event);
+ }
+ } catch (Exception e) {
+ log.error("事件处理异常: {}", e.getMessage(), e);
+ }
+ });
+ }
+
+ /**
+ * 启动心跳检测
+ */
+ private void startHeartbeat() {
+ scheduler.scheduleAtFixedRate(() -> {
+ if (connected.get()) {
+ try {
+ // 发送status命令检测连接
+ eslClient.sendSyncApiCommand("status", "");
+ } catch (Exception e) {
+ log.warn("心跳检测失败,连接可能已断开");
+ connected.set(false);
+ scheduleReconnect();
+ }
+ }
+ }, 30, 30, TimeUnit.SECONDS);
+ }
+
+ /**
+ * 计划重连
+ */
+ private void scheduleReconnect() {
+ if (reconnecting.compareAndSet(false, true)) {
+ scheduler.schedule(() -> {
+ log.info("尝试重新连接ESL...");
+ connect();
+ }, 5, TimeUnit.SECONDS);
+ }
+ }
+
+ /**
+ * 同步执行API命令
+ */
+ public String sendSyncApiCommand(String command, String args) {
+ checkConnection();
+ try {
+ return eslClient.sendSyncApiCommand(command, args).getBodyLines().get(0);
+ } catch (Exception e) {
+ log.error("执行API命令失败: {} {}", command, args, e);
+ throw new RuntimeException("ESL命令执行失败", e);
+ }
+ }
+
+ /**
+ * 异步执行API命令
+ */
+ public CompletableFuture sendAsyncApiCommand(String command, String args) {
+ return CompletableFuture.supplyAsync(() ->
+ sendSyncApiCommand(command, args), eventExecutor);
+ }
+
+ /**
+ * 向指定UUID发送消息执行应用
+ */
+ public void sendExecute(String uuid, String application, String args) {
+ checkConnection();
+ try {
+ eslClient.sendSyncApiCommand("uuid_broadcast",
+ uuid + " " + application + "::" + args + " both");
+ } catch (Exception e) {
+ log.error("发送Execute失败: uuid={}, app={}", uuid, application, e);
+ }
+ }
+
+ /**
+ * 检查连接状态
+ */
+ private void checkConnection() {
+ if (!connected.get()) {
+ throw new RuntimeException("ESL未连接");
+ }
+ }
+
+ /**
+ * 获取连接状态
+ */
+ public boolean isConnected() {
+ return connected.get();
+ }
+
+ /**
+ * 设置事件处理器
+ */
+ public void setEventHandler(EslEventHandler handler) {
+ this.eventHandler = handler;
+ }
+
+ @PreDestroy
+ public void destroy() {
+ try {
+ connected.set(false);
+ if (eslClient != null) {
+ eslClient.close();
+ }
+ if (scheduler != null) {
+ scheduler.shutdown();
+ }
+ if (eventExecutor != null) {
+ eventExecutor.shutdown();
+ }
+ log.info("ESL连接已关闭");
+ } catch (Exception e) {
+ log.error("关闭ESL连接异常", e);
+ }
+ }
+}
+```
+
+#### 3.2.4 事件处理器
+
+```java
+package com.callcenter.esl;
+
+import com.callcenter.model.Call;
+import com.callcenter.model.CallDirection;
+import com.callcenter.model.CallState;
+import com.callcenter.service.AcdService;
+import com.callcenter.service.CallService;
+import com.callcenter.websocket.EventPushService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.freeswitch.esl.client.transport.event.EslEvent;
+import org.springframework.stereotype.Component;
+
+import java.time.LocalDateTime;
+import java.util.Map;
+
+/**
+ * ESL事件处理器
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class EslEventHandler {
+
+ private final CallService callService;
+ private final AcdService acdService;
+ private final EventPushService eventPushService;
+
+ /**
+ * 处理ESL事件
+ */
+ public void handle(EslEvent event) {
+ String eventName = event.getEventName();
+ Map headers = event.getEventHeaders();
+
+ log.debug("收到事件: {}, UUID: {}", eventName, headers.get("Unique-ID"));
+
+ switch (eventName) {
+ case "CHANNEL_CREATE":
+ handleChannelCreate(headers);
+ break;
+ case "CHANNEL_ANSWER":
+ handleChannelAnswer(headers);
+ break;
+ case "CHANNEL_BRIDGE":
+ handleChannelBridge(headers);
+ break;
+ case "CHANNEL_UNBRIDGE":
+ handleChannelUnbridge(headers);
+ break;
+ case "CHANNEL_HANGUP":
+ handleChannelHangup(headers);
+ break;
+ case "CHANNEL_HANGUP_COMPLETE":
+ handleChannelHangupComplete(headers);
+ break;
+ case "DTMF":
+ handleDtmf(headers);
+ break;
+ case "RECORD_START":
+ handleRecordStart(headers);
+ break;
+ case "RECORD_STOP":
+ handleRecordStop(headers);
+ break;
+ case "CUSTOM":
+ handleCustomEvent(headers);
+ break;
+ default:
+ log.trace("未处理的事件类型: {}", eventName);
+ }
+ }
+
+ /**
+ * 处理通道创建事件(新呼叫)
+ */
+ private void handleChannelCreate(Map headers) {
+ String uuid = headers.get("Unique-ID");
+ String direction = headers.get("Call-Direction");
+ String callerNumber = headers.get("Caller-Caller-ID-Number");
+ String calleeNumber = headers.get("Caller-Destination-Number");
+
+ log.info("新呼叫: uuid={}, direction={}, caller={}, callee={}",
+ uuid, direction, callerNumber, calleeNumber);
+
+ // 创建呼叫记录
+ Call call = new Call();
+ call.setUuid(uuid);
+ call.setDirection("inbound".equals(direction) ?
+ CallDirection.INBOUND : CallDirection.OUTBOUND);
+ call.setCallerNumber(callerNumber);
+ call.setCalleeNumber(calleeNumber);
+ call.setState(CallState.RINGING);
+ call.setCreateTime(LocalDateTime.now());
+
+ // 获取业务标识(从通道变量)
+ String businessType = headers.get("variable_business_type");
+ String skillGroup = headers.get("variable_skill_group");
+ call.setBusinessType(businessType);
+ call.setSkillGroup(skillGroup);
+
+ callService.saveCall(call);
+
+ // 如果是呼入,触发ACD排队
+ if ("inbound".equals(direction)) {
+ acdService.enqueue(call);
+ }
+
+ // 推送事件到前端
+ eventPushService.pushCallEvent("CALL_RINGING", call);
+ }
+
+ /**
+ * 处理通话接听事件
+ */
+ private void handleChannelAnswer(Map headers) {
+ String uuid = headers.get("Unique-ID");
+
+ log.info("通话接听: uuid={}", uuid);
+
+ Call call = callService.getCallByUuid(uuid);
+ if (call != null) {
+ call.setState(CallState.ANSWERED);
+ call.setAnswerTime(LocalDateTime.now());
+ callService.updateCall(call);
+
+ // 推送事件
+ eventPushService.pushCallEvent("CALL_ANSWERED", call);
+ }
+ }
+
+ /**
+ * 处理通道桥接事件(双方接通)
+ */
+ private void handleChannelBridge(Map headers) {
+ String uuid = headers.get("Unique-ID");
+ String otherUuid = headers.get("Other-Leg-Unique-ID");
+
+ log.info("通道桥接: uuid={}, otherUuid={}", uuid, otherUuid);
+
+ Call call = callService.getCallByUuid(uuid);
+ if (call != null) {
+ call.setState(CallState.BRIDGED);
+ call.setBridgeUuid(otherUuid);
+ call.setBridgeTime(LocalDateTime.now());
+ callService.updateCall(call);
+
+ // 推送事件
+ eventPushService.pushCallEvent("CALL_BRIDGED", call);
+ }
+ }
+
+ /**
+ * 处理取消桥接事件
+ */
+ private void handleChannelUnbridge(Map headers) {
+ String uuid = headers.get("Unique-ID");
+
+ log.info("取消桥接: uuid={}", uuid);
+
+ Call call = callService.getCallByUuid(uuid);
+ if (call != null) {
+ call.setState(CallState.UNBRIDGED);
+ callService.updateCall(call);
+
+ eventPushService.pushCallEvent("CALL_UNBRIDGED", call);
+ }
+ }
+
+ /**
+ * 处理挂断事件
+ */
+ private void handleChannelHangup(Map headers) {
+ String uuid = headers.get("Unique-ID");
+ String hangupCause = headers.get("Hangup-Cause");
+
+ log.info("通话挂断: uuid={}, cause={}", uuid, hangupCause);
+
+ Call call = callService.getCallByUuid(uuid);
+ if (call != null) {
+ call.setState(CallState.HANGUP);
+ call.setHangupCause(hangupCause);
+ call.setHangupTime(LocalDateTime.now());
+
+ // 计算通话时长
+ if (call.getAnswerTime() != null) {
+ long duration = java.time.Duration.between(
+ call.getAnswerTime(), call.getHangupTime()).getSeconds();
+ call.setDuration(duration);
+ }
+
+ callService.updateCall(call);
+
+ // 推送事件
+ eventPushService.pushCallEvent("CALL_HANGUP", call);
+
+ // 更新坐席状态
+ if (call.getAgentId() != null) {
+ acdService.onCallEnd(call.getAgentId(), call);
+ }
+ }
+ }
+
+ /**
+ * 处理挂断完成事件(获取完整话单)
+ */
+ private void handleChannelHangupComplete(Map headers) {
+ String uuid = headers.get("Unique-ID");
+ String billsec = headers.get("variable_billsec");
+ String duration = headers.get("variable_duration");
+ String recordingPath = headers.get("variable_recording_path");
+
+ log.info("通话结束: uuid={}, billsec={}, recording={}",
+ uuid, billsec, recordingPath);
+
+ Call call = callService.getCallByUuid(uuid);
+ if (call != null) {
+ call.setBillsec(billsec != null ? Long.parseLong(billsec) : 0);
+ call.setRecordingPath(recordingPath);
+ callService.updateCall(call);
+
+ // 推送话单
+ eventPushService.pushCdr(call);
+ }
+ }
+
+ /**
+ * 处理DTMF按键事件
+ */
+ private void handleDtmf(Map headers) {
+ String uuid = headers.get("Unique-ID");
+ String digit = headers.get("DTMF-Digit");
+
+ log.info("DTMF按键: uuid={}, digit={}", uuid, digit);
+
+ // 推送按键事件
+ eventPushService.pushDtmf(uuid, digit);
+ }
+
+ /**
+ * 处理录音开始事件
+ */
+ private void handleRecordStart(Map headers) {
+ String uuid = headers.get("Unique-ID");
+ String recordPath = headers.get("Record-File-Path");
+
+ log.info("开始录音: uuid={}, path={}", uuid, recordPath);
+
+ Call call = callService.getCallByUuid(uuid);
+ if (call != null) {
+ call.setRecordingPath(recordPath);
+ callService.updateCall(call);
+ }
+ }
+
+ /**
+ * 处理录音停止事件
+ */
+ private void handleRecordStop(Map headers) {
+ String uuid = headers.get("Unique-ID");
+ String recordPath = headers.get("Record-File-Path");
+
+ log.info("停止录音: uuid={}, path={}", uuid, recordPath);
+ }
+
+ /**
+ * 处理自定义事件
+ */
+ private void handleCustomEvent(Map headers) {
+ String subclass = headers.get("Event-Subclass");
+ log.debug("自定义事件: subclass={}", subclass);
+
+ // 处理特定的自定义事件
+ if ("callcenter::info".equals(subclass)) {
+ // 处理呼叫中心相关事件
+ }
+ }
+}
+```
+
+#### 3.2.5 呼叫服务
+
+```java
+package com.callcenter.service;
+
+import com.callcenter.esl.EslConnectionManager;
+import com.callcenter.model.*;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.stereotype.Service;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 呼叫服务
+ * 提供呼叫控制相关功能
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class CallService {
+
+ private final EslConnectionManager eslManager;
+ private final RedisTemplate redisTemplate;
+
+ private static final String CALL_KEY_PREFIX = "call:";
+ private static final int CALL_EXPIRE_HOURS = 24;
+
+ /**
+ * 发起外呼
+ * @param agentId 坐席ID
+ * @param callerNumber 主叫号码(外显号码)
+ * @param calleeNumber 被叫号码
+ * @return 呼叫UUID
+ */
+ public String originate(String agentId, String callerNumber, String calleeNumber) {
+ log.info("发起外呼: agent={}, caller={}, callee={}",
+ agentId, callerNumber, calleeNumber);
+
+ // 获取坐席分机号
+ Agent agent = getAgent(agentId);
+ if (agent == null || agent.getState() != AgentState.READY) {
+ throw new RuntimeException("坐席不可用");
+ }
+
+ // 构建originate命令
+ // originate {变量}呼叫字符串 &应用(参数)
+ StringBuilder cmd = new StringBuilder();
+ cmd.append("{");
+ cmd.append("origination_caller_id_number=").append(callerNumber).append(",");
+ cmd.append("origination_caller_id_name=公司名称").append(",");
+ cmd.append("agent_id=").append(agentId).append(",");
+ cmd.append("direction=outbound").append(",");
+ cmd.append("RECORD_STEREO=true");
+ cmd.append("}");
+
+ // A-leg: 先呼叫坐席分机
+ cmd.append("user/").append(agent.getExtension());
+
+ // B-leg: 坐席接听后呼叫客户
+ cmd.append(" &bridge({origination_caller_id_number=")
+ .append(callerNumber)
+ .append("}sofia/gateway/carrier_primary/")
+ .append(calleeNumber)
+ .append(")");
+
+ String result = eslManager.sendSyncApiCommand("originate", cmd.toString());
+
+ if (result.startsWith("+OK")) {
+ String uuid = result.substring(4).trim();
+ log.info("外呼成功: uuid={}", uuid);
+ return uuid;
+ } else {
+ log.error("外呼失败: {}", result);
+ throw new RuntimeException("外呼失败: " + result);
+ }
+ }
+
+ /**
+ * 接听来电
+ */
+ public void answer(String uuid) {
+ log.info("接听来电: uuid={}", uuid);
+ eslManager.sendSyncApiCommand("uuid_answer", uuid);
+ }
+
+ /**
+ * 挂断通话
+ */
+ public void hangup(String uuid) {
+ log.info("挂断通话: uuid={}", uuid);
+ eslManager.sendSyncApiCommand("uuid_kill", uuid);
+ }
+
+ /**
+ * 挂断通话(带原因)
+ */
+ public void hangup(String uuid, String cause) {
+ log.info("挂断通话: uuid={}, cause={}", uuid, cause);
+ eslManager.sendSyncApiCommand("uuid_kill", uuid + " " + cause);
+ }
+
+ /**
+ * 保持通话
+ */
+ public void hold(String uuid) {
+ log.info("保持通话: uuid={}", uuid);
+ eslManager.sendSyncApiCommand("uuid_hold", uuid);
+ }
+
+ /**
+ * 取回保持
+ */
+ public void unhold(String uuid) {
+ log.info("取回通话: uuid={}", uuid);
+ eslManager.sendSyncApiCommand("uuid_hold", "off " + uuid);
+ }
+
+ /**
+ * 静音
+ */
+ public void mute(String uuid) {
+ log.info("静音: uuid={}", uuid);
+ eslManager.sendSyncApiCommand("uuid_audio", uuid + " start mute");
+ }
+
+ /**
+ * 取消静音
+ */
+ public void unmute(String uuid) {
+ log.info("取消静音: uuid={}", uuid);
+ eslManager.sendSyncApiCommand("uuid_audio", uuid + " stop");
+ }
+
+ /**
+ * 盲转(直接转接)
+ */
+ public void blindTransfer(String uuid, String destination) {
+ log.info("盲转: uuid={}, dest={}", uuid, destination);
+ eslManager.sendSyncApiCommand("uuid_transfer", uuid + " " + destination);
+ }
+
+ /**
+ * 咨询转(先咨询再转接)
+ */
+ public String consultTransfer(String uuid, String destination) {
+ log.info("咨询转: uuid={}, dest={}", uuid, destination);
+
+ // 先保持当前通话
+ hold(uuid);
+
+ // 发起咨询呼叫
+ String consultCmd = "{origination_uuid=consult_" + uuid + "}user/" + destination;
+ String result = eslManager.sendSyncApiCommand("originate",
+ consultCmd + " &park()");
+
+ if (result.startsWith("+OK")) {
+ return result.substring(4).trim();
+ }
+ throw new RuntimeException("咨询转失败");
+ }
+
+ /**
+ * 完成咨询转接
+ */
+ public void completeTransfer(String originalUuid, String consultUuid) {
+ log.info("完成咨询转: original={}, consult={}", originalUuid, consultUuid);
+ eslManager.sendSyncApiCommand("uuid_bridge", originalUuid + " " + consultUuid);
+ }
+
+ /**
+ * 三方通话
+ */
+ public void conference(String uuid1, String uuid2, String conferenceName) {
+ log.info("三方通话: uuid1={}, uuid2={}, conf={}", uuid1, uuid2, conferenceName);
+
+ // 将两个通道加入会议
+ eslManager.sendSyncApiCommand("uuid_transfer",
+ uuid1 + " conference:" + conferenceName + " inline");
+ eslManager.sendSyncApiCommand("uuid_transfer",
+ uuid2 + " conference:" + conferenceName + " inline");
+ }
+
+ /**
+ * 播放语音
+ */
+ public void playback(String uuid, String filePath) {
+ log.info("播放语音: uuid={}, file={}", uuid, filePath);
+ eslManager.sendSyncApiCommand("uuid_broadcast",
+ uuid + " " + filePath + " both");
+ }
+
+ /**
+ * 停止播放
+ */
+ public void stopPlayback(String uuid) {
+ log.info("停止播放: uuid={}", uuid);
+ eslManager.sendSyncApiCommand("uuid_break", uuid);
+ }
+
+ /**
+ * 开始录音
+ */
+ public void startRecording(String uuid, String filePath) {
+ log.info("开始录音: uuid={}, file={}", uuid, filePath);
+ eslManager.sendSyncApiCommand("uuid_record", uuid + " start " + filePath);
+ }
+
+ /**
+ * 停止录音
+ */
+ public void stopRecording(String uuid, String filePath) {
+ log.info("停止录音: uuid={}", uuid);
+ eslManager.sendSyncApiCommand("uuid_record", uuid + " stop " + filePath);
+ }
+
+ /**
+ * 桥接两个通道
+ */
+ public void bridge(String uuid1, String uuid2) {
+ log.info("桥接通道: uuid1={}, uuid2={}", uuid1, uuid2);
+ eslManager.sendSyncApiCommand("uuid_bridge", uuid1 + " " + uuid2);
+ }
+
+ /**
+ * 设置通道变量
+ */
+ public void setVariable(String uuid, String name, String value) {
+ eslManager.sendSyncApiCommand("uuid_setvar",
+ uuid + " " + name + " " + value);
+ }
+
+ /**
+ * 获取通道变量
+ */
+ public String getVariable(String uuid, String name) {
+ return eslManager.sendSyncApiCommand("uuid_getvar", uuid + " " + name);
+ }
+
+ // ============ 呼叫记录管理 ============
+
+ /**
+ * 保存呼叫记录
+ */
+ public void saveCall(Call call) {
+ String key = CALL_KEY_PREFIX + call.getUuid();
+ redisTemplate.opsForValue().set(key, call, CALL_EXPIRE_HOURS, TimeUnit.HOURS);
+ }
+
+ /**
+ * 获取呼叫记录
+ */
+ public Call getCallByUuid(String uuid) {
+ String key = CALL_KEY_PREFIX + uuid;
+ return (Call) redisTemplate.opsForValue().get(key);
+ }
+
+ /**
+ * 更新呼叫记录
+ */
+ public void updateCall(Call call) {
+ saveCall(call);
+ }
+
+ /**
+ * 获取坐席信息
+ */
+ private Agent getAgent(String agentId) {
+ // TODO: 从坐席服务获取
+ return null;
+ }
+}
+```
+
+#### 3.2.6 ACD话务分配服务
+
+```java
+package com.callcenter.service;
+
+import com.callcenter.esl.EslConnectionManager;
+import com.callcenter.model.*;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Service;
+
+import java.time.LocalDateTime;
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+/**
+ * ACD话务分配服务
+ * 实现多种分配策略:历史服务优先、最长空闲优先、轮询
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class AcdService {
+
+ private final EslConnectionManager eslManager;
+ private final RedisTemplate redisTemplate;
+ private final AgentService agentService;
+ private final CallService callService;
+
+ // 队列Key前缀
+ private static final String QUEUE_KEY_PREFIX = "queue:";
+ // 坐席最后服务客户映射
+ private static final String AGENT_CUSTOMER_MAP_KEY = "agent:customer:map";
+ // 坐席最后空闲时间
+ private static final String AGENT_IDLE_TIME_KEY = "agent:idle:time:";
+ // 轮询索引
+ private final Map roundRobinIndex = new ConcurrentHashMap<>();
+
+ /**
+ * 呼叫入队
+ */
+ public void enqueue(Call call) {
+ String skillGroup = call.getSkillGroup();
+ String queueKey = QUEUE_KEY_PREFIX + skillGroup;
+
+ QueueItem item = new QueueItem();
+ item.setCallUuid(call.getUuid());
+ item.setCallerNumber(call.getCallerNumber());
+ item.setSkillGroup(skillGroup);
+ item.setEnqueueTime(LocalDateTime.now());
+ item.setPriority(calculatePriority(call));
+
+ // 使用SortedSet,按优先级和入队时间排序
+ double score = item.getPriority() * 1000000000L +
+ System.currentTimeMillis() / 1000;
+ redisTemplate.opsForZSet().add(queueKey, item, score);
+
+ log.info("呼叫入队: uuid={}, queue={}, priority={}",
+ call.getUuid(), skillGroup, item.getPriority());
+
+ // 尝试立即分配
+ tryAssign(skillGroup);
+ }
+
+ /**
+ * 计算优先级(数字越小优先级越高)
+ */
+ private int calculatePriority(Call call) {
+ // VIP客户优先级高
+ if ("VIP".equals(call.getCustomerLevel())) {
+ return 1;
+ }
+ // 投诉优先
+ if ("complaint".equals(call.getBusinessType())) {
+ return 2;
+ }
+ // 普通客户
+ return 5;
+ }
+
+ /**
+ * 尝试分配队列中的呼叫
+ */
+ public void tryAssign(String skillGroup) {
+ String queueKey = QUEUE_KEY_PREFIX + skillGroup;
+
+ // 获取可用坐席
+ List availableAgents = agentService.getAvailableAgents(skillGroup);
+ if (availableAgents.isEmpty()) {
+ log.debug("技能组{}无可用坐席", skillGroup);
+ return;
+ }
+
+ // 获取队列中的呼叫
+ Set