## 目录

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
<!-- conf/sip_profiles/internal.xml -->
<profile name="internal">
  <settings>
    <!-- 监听地址和端口 -->
    <param name="sip-ip" value="$${local_ip_v4}"/>
    <param name="sip-port" value="5060"/>
    <param name="rtp-ip" value="$${local_ip_v4}"/>

    <!-- 编解码 -->
    <param name="inbound-codec-prefs" value="PCMA,PCMU,G729"/>
    <param name="outbound-codec-prefs" value="PCMA,PCMU,G729"/>

    <!-- RTP端口范围 -->
    <param name="rtp-start-port" value="16384"/>
    <param name="rtp-end-port" value="32768"/>

    <!-- 注册相关 -->
    <param name="accept-blind-reg" value="false"/>
    <param name="accept-blind-auth" value="false"/>

    <!-- NAT穿透 -->
    <param name="local-network-acl" value="localnet.auto"/>
    <param name="apply-nat-acl" value="nat.auto"/>

    <!-- 媒体相关 -->
    <param name="inbound-late-negotiation" value="true"/>
    <param name="inbound-zrtp-passthru" value="true"/>

    <!-- 安全相关 -->
    <param name="auth-calls" value="true"/>
    <param name="log-auth-failures" value="true"/>
  </settings>
</profile>
```

**External Profile（中继对接）：**

```xml
<!-- conf/sip_profiles/external.xml -->
<profile name="external">
  <gateways>
    <!-- 运营商SIP中继 -->
    <gateway name="carrier_primary">
      <param name="username" value="your_username"/>
      <param name="password" value="your_password"/>
      <param name="realm" value="sip.carrier.com"/>
      <param name="proxy" value="sip.carrier.com"/>
      <param name="register" value="true"/>
      <param name="retry-seconds" value="30"/>
      <param name="caller-id-in-from" value="true"/>
    </gateway>

    <!-- 备用中继 -->
    <gateway name="carrier_backup">
      <param name="username" value="backup_username"/>
      <param name="password" value="backup_password"/>
      <param name="realm" value="sip.backup-carrier.com"/>
      <param name="proxy" value="sip.backup-carrier.com"/>
      <param name="register" value="true"/>
    </gateway>
  </gateways>

  <settings>
    <param name="sip-ip" value="$${local_ip_v4}"/>
    <param name="sip-port" value="5080"/>
    <param name="rtp-ip" value="$${local_ip_v4}"/>
    <param name="context" value="public"/>
  </settings>
</profile>
```

### 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
<!-- conf/dialplan/public.xml -->
<context name="public">

  <!-- 400号码呼入 - 道路救援 -->
  <extension name="inbound_rescue">
    <condition field="destination_number" expression="^(4001234561)$">
      <!-- 设置业务线标识 -->
      <action application="set" data="business_type=rescue"/>
      <action application="set" data="skill_group=rescue_group"/>

      <!-- 进入IVR -->
      <action application="transfer" data="main_ivr XML default"/>
    </condition>
  </extension>

  <!-- 400号码呼入 - 家政服务 -->
  <extension name="inbound_housekeeping">
    <condition field="destination_number" expression="^(4001234562)$">
      <action application="set" data="business_type=housekeeping"/>
      <action application="set" data="skill_group=housekeeping_group"/>
      <action application="transfer" data="main_ivr XML default"/>
    </condition>
  </extension>

  <!-- 400号码呼入 - 代驾服务 -->
  <extension name="inbound_designated_driver">
    <condition field="destination_number" expression="^(4001234563)$">
      <action application="set" data="business_type=driver"/>
      <action application="set" data="skill_group=driver_group"/>
      <action application="transfer" data="main_ivr XML default"/>
    </condition>
  </extension>

</context>
```

**IVR和呼叫处理（default.xml）：**

```xml
<!-- conf/dialplan/default.xml -->
<context name="default">

  <!-- 主IVR入口 -->
  <extension name="main_ivr">
    <condition field="destination_number" expression="^main_ivr$">
      <action application="answer"/>
      <action application="sleep" data="500"/>

      <!-- 播放欢迎语 -->
      <action application="playback" data="ivr/welcome.wav"/>

      <!-- 语音识别或按键导航 -->
      <action application="play_and_get_digits"
              data="1 1 3 5000 # ivr/menu_prompt.wav ivr/invalid.wav
                    ivr_selection \d 3000"/>

      <!-- 根据按键转接 -->
      <action application="transfer" data="route_by_selection"/>
    </condition>
  </extension>

  <!-- 按键路由 -->
  <extension name="route_by_selection">
    <condition field="destination_number" expression="^route_by_selection$">

      <!-- 按1转道路救援 -->
      <condition field="${ivr_selection}" expression="^1$">
        <action application="set" data="skill_group=rescue_group"/>
        <action application="transfer" data="queue_call"/>
      </condition>

      <!-- 按2转家政服务 -->
      <condition field="${ivr_selection}" expression="^2$">
        <action application="set" data="skill_group=housekeeping_group"/>
        <action application="transfer" data="queue_call"/>
      </condition>

      <!-- 按0转人工 -->
      <condition field="${ivr_selection}" expression="^0$">
        <action application="transfer" data="queue_call"/>
      </condition>

    </condition>
  </extension>

  <!-- 排队等待（ACD由ESL处理） -->
  <extension name="queue_call">
    <condition field="destination_number" expression="^queue_call$">
      <!-- 通知ESL有新呼叫需要排队 -->
      <action application="set" data="api_hangup_hook=uuid_broadcast ${uuid}
              queue_exit.wav both"/>

      <!-- 播放等待音乐，等待ESL分配坐席 -->
      <action application="playback" data="local_stream://moh"/>
    </condition>
  </extension>

  <!-- 坐席外呼 -->
  <extension name="agent_outbound">
    <condition field="destination_number" expression="^9(\d{11})$">
      <action application="set" data="effective_caller_id_number=
              ${outbound_caller_id}"/>
      <action application="set" data="effective_caller_id_name=公司名称"/>

      <!-- 录音 -->
      <action application="set" data="RECORD_STEREO=true"/>
      <action application="set" data="recording_path=/recordings/${strftime(%Y/%m/%d)}"/>
      <action application="record_session" data="${recording_path}/${uuid}.wav"/>

      <!-- 通过网关外呼 -->
      <action application="bridge" data="sofia/gateway/carrier_primary/$1"/>
    </condition>
  </extension>

  <!-- 坐席互转 -->
  <extension name="transfer_to_agent">
    <condition field="destination_number" expression="^(\d{4})$">
      <action application="bridge" data="user/$1@${domain_name}"/>
    </condition>
  </extension>

</context>
```

### 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
<!-- conf/autoload_configs/event_socket.conf.xml -->
<configuration name="event_socket.conf" description="Socket Client">
  <settings>
    <!-- 监听地址（生产环境建议限制IP） -->
    <param name="nat-map" value="false"/>
    <param name="listen-ip" value="0.0.0.0"/>
    <param name="listen-port" value="8021"/>

    <!-- 认证密码 -->
    <param name="password" value="your_secure_password"/>

    <!-- ACL限制（可选） -->
    <!-- <param name="apply-inbound-acl" value="lan"/> -->
  </settings>
</configuration>
```

---

## 第三部分 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 <password>` | 连接认证 |
| **事件订阅** | `event plain <event_types>` | 订阅事件 |
| | `event plain ALL` | 订阅所有事件 |
| | `event plain CHANNEL_CREATE CHANNEL_ANSWER CHANNEL_HANGUP` | 订阅指定事件 |
| | `nixevent <event_type>` | 取消订阅 |
| | `noevents` | 取消所有订阅 |
| **API命令** | `api <command> [args]` | 同步执行API |
| | `bgapi <command> [args]` | 异步执行API |
| **过滤器** | `filter <header> <value>` | 过滤特定事件 |
| | `filter Unique-ID <uuid>` | 只接收指定UUID事件 |
| **呼叫控制** | `sendmsg <uuid>` | 向指定通道发送消息 |
| | `execute` | 在通道上执行应用 |
| **其他** | `log <level>` | 设置日志级别 |
| | `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
<!-- pom.xml -->
<dependencies>
    <!-- Spring Boot -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-websocket</artifactId>
    </dependency>

    <!-- FreeSWITCH ESL Client -->
    <dependency>
        <groupId>org.freeswitch.esl.client</groupId>
        <artifactId>org.freeswitch.esl.client</artifactId>
        <version>0.9.2</version>
    </dependency>

    <!-- Netty (ESL底层通信) -->
    <dependency>
        <groupId>io.netty</groupId>
        <artifactId>netty-all</artifactId>
        <version>4.1.86.Final</version>
    </dependency>

    <!-- Redis -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>

    <!-- JSON处理 -->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>fastjson</artifactId>
        <version>2.0.23</version>
    </dependency>

    <!-- 工具类 -->
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-lang3</artifactId>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
    </dependency>
</dependencies>
```

#### 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<String> 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<String, String> 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<String, String> 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<String, String> 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<String, String> 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<String, String> 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<String, String> 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<String, String> 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<String, String> 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<String, String> 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<String, String> headers) {
        String uuid = headers.get("Unique-ID");
        String recordPath = headers.get("Record-File-Path");

        log.info("停止录音: uuid={}, path={}", uuid, recordPath);
    }

    /**
     * 处理自定义事件
     */
    private void handleCustomEvent(Map<String, String> 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<String, Object> 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<String, Object> 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<String, Integer> 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<Agent> availableAgents = agentService.getAvailableAgents(skillGroup);
        if (availableAgents.isEmpty()) {
            log.debug("技能组{}无可用坐席", skillGroup);
            return;
        }

        // 获取队列中的呼叫
        Set<Object> queueItems = redisTemplate.opsForZSet().range(queueKey, 0, 0);
        if (queueItems == null || queueItems.isEmpty()) {
            log.debug("技能组{}队列为空", skillGroup);
            return;
        }

        QueueItem item = (QueueItem) queueItems.iterator().next();
        Call call = callService.getCallByUuid(item.getCallUuid());

        if (call == null || call.getState() == CallState.HANGUP) {
            // 呼叫已结束，移除队列
            redisTemplate.opsForZSet().remove(queueKey, item);
            tryAssign(skillGroup);  // 继续处理下一个
            return;
        }

        // 选择坐席
        Agent selectedAgent = selectAgent(availableAgents, call);

        if (selectedAgent != null) {
            // 从队列移除
            redisTemplate.opsForZSet().remove(queueKey, item);

            // 分配呼叫
            assignCallToAgent(call, selectedAgent);
        }
    }

    /**
     * 选择坐席（多策略）
     */
    private Agent selectAgent(List<Agent> agents, Call call) {
        if (agents.isEmpty()) {
            return null;
        }

        // 策略1: 历史服务优先
        Agent lastAgent = findLastServingAgent(call.getCallerNumber(), agents);
        if (lastAgent != null) {
            log.info("历史服务优先: 客户{}分配给上次服务坐席{}",
                    call.getCallerNumber(), lastAgent.getId());
            return lastAgent;
        }

        // 策略2: 最长空闲优先
        Agent longestIdleAgent = findLongestIdleAgent(agents);
        if (longestIdleAgent != null) {
            log.info("最长空闲优先: 选择坐席{}", longestIdleAgent.getId());
            return longestIdleAgent;
        }

        // 策略3: 轮询
        Agent roundRobinAgent = selectByRoundRobin(agents, call.getSkillGroup());
        log.info("轮询分配: 选择坐席{}", roundRobinAgent.getId());
        return roundRobinAgent;
    }

    /**
     * 查找上次服务该客户的坐席
     */
    private Agent findLastServingAgent(String customerNumber, List<Agent> agents) {
        String lastAgentId = (String) redisTemplate.opsForHash()
            .get(AGENT_CUSTOMER_MAP_KEY, customerNumber);

        if (lastAgentId != null) {
            return agents.stream()
                .filter(a -> a.getId().equals(lastAgentId))
                .findFirst()
                .orElse(null);
        }
        return null;
    }

    /**
     * 查找空闲时间最长的坐席
     */
    private Agent findLongestIdleAgent(List<Agent> agents) {
        return agents.stream()
            .max(Comparator.comparing(agent -> {
                String key = AGENT_IDLE_TIME_KEY + agent.getId();
                Long idleTime = (Long) redisTemplate.opsForValue().get(key);
                return idleTime != null ? idleTime : 0L;
            }))
            .orElse(null);
    }

    /**
     * 轮询选择坐席
     */
    private Agent selectByRoundRobin(List<Agent> agents, String skillGroup) {
        int currentIndex = roundRobinIndex.getOrDefault(skillGroup, 0);
        int nextIndex = (currentIndex + 1) % agents.size();
        roundRobinIndex.put(skillGroup, nextIndex);
        return agents.get(currentIndex);
    }

    /**
     * 分配呼叫给坐席
     */
    private void assignCallToAgent(Call call, Agent agent) {
        log.info("分配呼叫: uuid={} -> agent={}", call.getUuid(), agent.getId());

        // 更新坐席状态
        agentService.updateAgentState(agent.getId(), AgentState.RINGING);

        // 更新呼叫信息
        call.setAgentId(agent.getId());
        call.setAgentExtension(agent.getExtension());
        callService.updateCall(call);

        // 将呼叫转接到坐席分机
        // 方式1: 使用uuid_transfer
        eslManager.sendSyncApiCommand("uuid_transfer",
            call.getUuid() + " " + agent.getExtension() + " XML default");

        // 方式2: 使用originate + bridge（更灵活）
        // String bridgeCmd = String.format(
        //     "{origination_uuid=%s,agent_id=%s}user/%s",
        //     UUID.randomUUID(), agent.getId(), agent.getExtension());
        // eslManager.sendSyncApiCommand("uuid_broadcast",
        //     call.getUuid() + " bridge::" + bridgeCmd + " both");

        // 记录坐席-客户关系（用于历史服务优先）
        redisTemplate.opsForHash().put(AGENT_CUSTOMER_MAP_KEY,
            call.getCallerNumber(), agent.getId());
    }

    /**
     * 通话结束处理
     */
    public void onCallEnd(String agentId, Call call) {
        log.info("通话结束: agent={}, call={}", agentId, call.getUuid());

        // 更新坐席状态为后处理
        agentService.updateAgentState(agentId, AgentState.WRAP_UP);

        // 设置后处理超时（30秒后自动置闲）
        // 由定时任务处理
    }

    /**
     * 坐席置闲时记录空闲时间
     */
    public void onAgentReady(String agentId, String skillGroup) {
        String key = AGENT_IDLE_TIME_KEY + agentId;
        redisTemplate.opsForValue().set(key, System.currentTimeMillis());

        // 尝试分配队列中的呼叫
        tryAssign(skillGroup);
    }

    /**
     * 定时处理队列
     */
    @Scheduled(fixedDelay = 1000)
    public void processQueues() {
        // 获取所有技能组
        Set<String> skillGroups = agentService.getAllSkillGroups();
        for (String skillGroup : skillGroups) {
            tryAssign(skillGroup);
        }
    }

    /**
     * 获取队列状态
     */
    public QueueStatus getQueueStatus(String skillGroup) {
        String queueKey = QUEUE_KEY_PREFIX + skillGroup;

        QueueStatus status = new QueueStatus();
        status.setSkillGroup(skillGroup);
        status.setWaitingCount(redisTemplate.opsForZSet().size(queueKey));
        status.setAvailableAgents(agentService.getAvailableAgents(skillGroup).size());

        // 计算最长等待时间
        Set<Object> items = redisTemplate.opsForZSet().range(queueKey, 0, 0);
        if (items != null && !items.isEmpty()) {
            QueueItem firstItem = (QueueItem) items.iterator().next();
            long waitSeconds = java.time.Duration.between(
                firstItem.getEnqueueTime(), LocalDateTime.now()).getSeconds();
            status.setLongestWaitTime(waitSeconds);
        }

        return status;
    }

    /**
     * 溢出处理
     * 当技能组无可用坐席且等待时间超过阈值时，溢出到其他技能组
     */
    @Scheduled(fixedDelay = 5000)
    public void handleOverflow() {
        // 获取溢出配置
        Map<String, String> overflowConfig = getOverflowConfig();

        for (Map.Entry<String, String> entry : overflowConfig.entrySet()) {
            String sourceGroup = entry.getKey();
            String targetGroup = entry.getValue();

            String queueKey = QUEUE_KEY_PREFIX + sourceGroup;
            Set<Object> items = redisTemplate.opsForZSet().range(queueKey, 0, -1);

            if (items == null || items.isEmpty()) {
                continue;
            }

            // 检查是否需要溢出
            List<Agent> sourceAgents = agentService.getAvailableAgents(sourceGroup);
            List<Agent> targetAgents = agentService.getAvailableAgents(targetGroup);

            if (sourceAgents.isEmpty() && !targetAgents.isEmpty()) {
                for (Object obj : items) {
                    QueueItem item = (QueueItem) obj;
                    long waitSeconds = java.time.Duration.between(
                        item.getEnqueueTime(), LocalDateTime.now()).getSeconds();

                    // 等待超过60秒，溢出
                    if (waitSeconds > 60) {
                        log.info("溢出处理: uuid={}, {}->{}",
                                item.getCallUuid(), sourceGroup, targetGroup);

                        // 移出原队列
                        redisTemplate.opsForZSet().remove(queueKey, item);

                        // 加入目标队列
                        item.setSkillGroup(targetGroup);
                        String targetQueueKey = QUEUE_KEY_PREFIX + targetGroup;
                        double score = item.getPriority() * 1000000000L +
                            System.currentTimeMillis() / 1000;
                        redisTemplate.opsForZSet().add(targetQueueKey, item, score);

                        // 尝试分配
                        tryAssign(targetGroup);
                    }
                }
            }
        }
    }

    private Map<String, String> getOverflowConfig() {
        // 配置：源技能组 -> 目标技能组
        Map<String, String> config = new HashMap<>();
        config.put("rescue_group", "default_group");
        config.put("housekeeping_group", "default_group");
        config.put("driver_group", "default_group");
        return config;
    }
}
```

#### 3.2.7 坐席服务

```java
package com.callcenter.service;

import com.callcenter.esl.EslConnectionManager;
import com.callcenter.model.*;
import com.callcenter.websocket.EventPushService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;

/**
 * 坐席服务
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class AgentService {

    private final RedisTemplate<String, Object> redisTemplate;
    private final EslConnectionManager eslManager;
    private final EventPushService eventPushService;
    private final AcdService acdService;

    private static final String AGENT_KEY_PREFIX = "agent:";
    private static final String AGENT_SET_KEY = "agents:all";
    private static final String SKILL_GROUP_AGENTS_KEY = "skillgroup:agents:";

    /**
     * 坐席签入
     */
    public void signIn(String agentId, String extension, List<String> skillGroups) {
        log.info("坐席签入: agentId={}, extension={}, skills={}",
                agentId, extension, skillGroups);

        // 检查分机是否注册
        String regStatus = eslManager.sendSyncApiCommand("sofia_contact",
            "internal/" + extension + "@${domain}");
        if (regStatus.contains("error")) {
            throw new RuntimeException("分机未注册: " + extension);
        }

        Agent agent = new Agent();
        agent.setId(agentId);
        agent.setExtension(extension);
        agent.setSkillGroups(skillGroups);
        agent.setState(AgentState.NOT_READY);
        agent.setSignInTime(LocalDateTime.now());

        // 保存坐席信息
        String key = AGENT_KEY_PREFIX + agentId;
        redisTemplate.opsForValue().set(key, agent);

        // 加入坐席集合
        redisTemplate.opsForSet().add(AGENT_SET_KEY, agentId);

        // 加入技能组
        for (String skillGroup : skillGroups) {
            redisTemplate.opsForSet().add(SKILL_GROUP_AGENTS_KEY + skillGroup, agentId);
        }

        // 推送状态变更
        eventPushService.pushAgentState(agent);
    }

    /**
     * 坐席签出
     */
    public void signOut(String agentId) {
        log.info("坐席签出: agentId={}", agentId);

        Agent agent = getAgent(agentId);
        if (agent == null) {
            return;
        }

        // 检查是否在通话中
        if (agent.getState() == AgentState.BUSY ||
            agent.getState() == AgentState.RINGING) {
            throw new RuntimeException("坐席正在通话中，无法签出");
        }

        // 从技能组移除
        for (String skillGroup : agent.getSkillGroups()) {
            redisTemplate.opsForSet().remove(SKILL_GROUP_AGENTS_KEY + skillGroup, agentId);
        }

        // 从坐席集合移除
        redisTemplate.opsForSet().remove(AGENT_SET_KEY, agentId);

        // 删除坐席信息
        String key = AGENT_KEY_PREFIX + agentId;
        redisTemplate.delete(key);

        // 推送签出事件
        agent.setState(AgentState.OFFLINE);
        eventPushService.pushAgentState(agent);
    }

    /**
     * 坐席置闲
     */
    public void ready(String agentId) {
        log.info("坐席置闲: agentId={}", agentId);

        Agent agent = getAgent(agentId);
        if (agent == null) {
            throw new RuntimeException("坐席不存在");
        }

        if (agent.getState() == AgentState.BUSY) {
            throw new RuntimeException("坐席正在通话中");
        }

        updateAgentState(agentId, AgentState.READY);

        // 通知ACD有坐席可用
        for (String skillGroup : agent.getSkillGroups()) {
            acdService.onAgentReady(agentId, skillGroup);
        }
    }

    /**
     * 坐席置忙
     */
    public void notReady(String agentId, String reason) {
        log.info("坐席置忙: agentId={}, reason={}", agentId, reason);

        Agent agent = getAgent(agentId);
        if (agent == null) {
            throw new RuntimeException("坐席不存在");
        }

        if (agent.getState() == AgentState.BUSY) {
            throw new RuntimeException("坐席正在通话中");
        }

        agent.setNotReadyReason(reason);
        updateAgentState(agentId, AgentState.NOT_READY);
    }

    /**
     * 坐席小休
     */
    public void pause(String agentId, String reason) {
        log.info("坐席小休: agentId={}, reason={}", agentId, reason);

        Agent agent = getAgent(agentId);
        if (agent == null) {
            throw new RuntimeException("坐席不存在");
        }

        if (agent.getState() == AgentState.BUSY) {
            throw new RuntimeException("坐席正在通话中");
        }

        agent.setPauseReason(reason);
        updateAgentState(agentId, AgentState.PAUSED);
    }

    /**
     * 更新坐席状态
     */
    public void updateAgentState(String agentId, AgentState newState) {
        Agent agent = getAgent(agentId);
        if (agent == null) {
            return;
        }

        AgentState oldState = agent.getState();
        agent.setState(newState);
        agent.setStateTime(LocalDateTime.now());

        // 保存
        String key = AGENT_KEY_PREFIX + agentId;
        redisTemplate.opsForValue().set(key, agent);

        log.info("坐席状态变更: agent={}, {} -> {}", agentId, oldState, newState);

        // 推送状态变更
        eventPushService.pushAgentState(agent);
    }

    /**
     * 获取坐席信息
     */
    public Agent getAgent(String agentId) {
        String key = AGENT_KEY_PREFIX + agentId;
        return (Agent) redisTemplate.opsForValue().get(key);
    }

    /**
     * 获取技能组可用坐席
     */
    public List<Agent> getAvailableAgents(String skillGroup) {
        Set<Object> agentIds = redisTemplate.opsForSet()
            .members(SKILL_GROUP_AGENTS_KEY + skillGroup);

        if (agentIds == null || agentIds.isEmpty()) {
            return Collections.emptyList();
        }

        return agentIds.stream()
            .map(id -> getAgent((String) id))
            .filter(agent -> agent != null && agent.getState() == AgentState.READY)
            .collect(Collectors.toList());
    }

    /**
     * 获取所有技能组
     */
    public Set<String> getAllSkillGroups() {
        Set<String> keys = redisTemplate.keys(SKILL_GROUP_AGENTS_KEY + "*");
        if (keys == null) {
            return Collections.emptySet();
        }
        return keys.stream()
            .map(key -> key.replace(SKILL_GROUP_AGENTS_KEY, ""))
            .collect(Collectors.toSet());
    }

    /**
     * 获取所有坐席状态（监控大屏）
     */
    public List<Agent> getAllAgents() {
        Set<Object> agentIds = redisTemplate.opsForSet().members(AGENT_SET_KEY);
        if (agentIds == null) {
            return Collections.emptyList();
        }
        return agentIds.stream()
            .map(id -> getAgent((String) id))
            .filter(Objects::nonNull)
            .collect(Collectors.toList());
    }

    /**
     * 获取坐席状态统计
     */
    public Map<AgentState, Long> getAgentStateStats() {
        return getAllAgents().stream()
            .collect(Collectors.groupingBy(Agent::getState, Collectors.counting()));
    }
}
```

### 3.3 Go ESL开发

如果您的团队更熟悉Go语言，以下是Go版本的ESL开发示例：

#### 3.3.1 项目结构

```
callcenter-cti-go/
├── go.mod
├── go.sum
├── main.go
├── config/
│   └── config.go
├── esl/
│   ├── client.go           # ESL客户端
│   ├── event.go            # 事件定义
│   └── handler.go          # 事件处理器
├── service/
│   ├── agent.go            # 坐席服务
│   ├── call.go             # 呼叫服务
│   └── acd.go              # ACD服务
├── model/
│   └── models.go           # 数据模型
├── api/
│   ├── router.go           # 路由
│   ├── agent_handler.go    # 坐席API
│   └── call_handler.go     # 呼叫API
└── websocket/
    └── hub.go              # WebSocket推送
```

#### 3.3.2 ESL客户端

```go
// esl/client.go
package esl

import (
    "bufio"
    "fmt"
    "log"
    "net"
    "net/textproto"
    "strconv"
    "strings"
    "sync"
    "time"
)

// ESL客户端
type Client struct {
    conn       net.Conn
    reader     *textproto.Reader
    writer     *bufio.Writer
    mu         sync.Mutex
    connected  bool
    password   string
    eventChan  chan *Event
    handlers   []EventHandler
    reconnect  bool
}

// 事件处理器接口
type EventHandler interface {
    Handle(event *Event)
}

// 创建ESL客户端
func NewClient(host string, port int, password string) (*Client, error) {
    client := &Client{
        password:  password,
        eventChan: make(chan *Event, 1000),
        handlers:  make([]EventHandler, 0),
        reconnect: true,
    }

    err := client.connect(host, port)
    if err != nil {
        return nil, err
    }

    // 启动事件读取协程
    go client.readLoop()

    // 启动事件分发协程
    go client.dispatchLoop()

    return client, nil
}

// 连接FreeSWITCH
func (c *Client) connect(host string, port int) error {
    addr := fmt.Sprintf("%s:%d", host, port)

    conn, err := net.DialTimeout("tcp", addr, 10*time.Second)
    if err != nil {
        return fmt.Errorf("连接失败: %v", err)
    }

    c.conn = conn
    c.reader = textproto.NewReader(bufio.NewReader(conn))
    c.writer = bufio.NewWriter(conn)

    // 读取欢迎消息
    _, err = c.readMessage()
    if err != nil {
        return err
    }

    // 认证
    err = c.auth()
    if err != nil {
        return err
    }

    c.connected = true
    log.Println("ESL连接成功")

    return nil
}

// 认证
func (c *Client) auth() error {
    _, err := c.sendCommand(fmt.Sprintf("auth %s", c.password))
    if err != nil {
        return fmt.Errorf("认证失败: %v", err)
    }
    return nil
}

// 发送命令
func (c *Client) sendCommand(command string) (*Message, error) {
    c.mu.Lock()
    defer c.mu.Unlock()

    _, err := c.writer.WriteString(command + "\n\n")
    if err != nil {
        return nil, err
    }
    c.writer.Flush()

    return c.readMessage()
}

// 发送API命令
func (c *Client) API(command string, args string) (string, error) {
    cmd := fmt.Sprintf("api %s %s", command, args)
    msg, err := c.sendCommand(cmd)
    if err != nil {
        return "", err
    }
    return msg.Body, nil
}

// 发送后台API命令
func (c *Client) BgAPI(command string, args string) (string, error) {
    cmd := fmt.Sprintf("bgapi %s %s", command, args)
    msg, err := c.sendCommand(cmd)
    if err != nil {
        return "", err
    }
    return msg.GetHeader("Job-UUID"), nil
}

// 订阅事件
func (c *Client) Subscribe(events ...string) error {
    eventStr := strings.Join(events, " ")
    _, err := c.sendCommand(fmt.Sprintf("event plain %s", eventStr))
    return err
}

// 订阅所有事件
func (c *Client) SubscribeAll() error {
    _, err := c.sendCommand("event plain ALL")
    return err
}

// 读取消息
func (c *Client) readMessage() (*Message, error) {
    msg := &Message{
        Headers: make(map[string]string),
    }

    // 读取headers
    for {
        line, err := c.reader.ReadLine()
        if err != nil {
            return nil, err
        }

        if line == "" {
            break
        }

        parts := strings.SplitN(line, ": ", 2)
        if len(parts) == 2 {
            msg.Headers[parts[0]] = parts[1]
        }
    }

    // 读取body
    if lenStr := msg.GetHeader("Content-Length"); lenStr != "" {
        length, _ := strconv.Atoi(lenStr)
        if length > 0 {
            body := make([]byte, length)
            _, err := c.reader.R.Read(body)
            if err != nil {
                return nil, err
            }
            msg.Body = string(body)
        }
    }

    return msg, nil
}

// 事件读取循环
func (c *Client) readLoop() {
    for c.connected {
        msg, err := c.readMessage()
        if err != nil {
            log.Printf("读取消息错误: %v", err)
            c.connected = false
            if c.reconnect {
                c.tryReconnect()
            }
            return
        }

        contentType := msg.GetHeader("Content-Type")
        if contentType == "text/event-plain" {
            event := parseEvent(msg)
            c.eventChan <- event
        }
    }
}

// 事件分发循环
func (c *Client) dispatchLoop() {
    for event := range c.eventChan {
        for _, handler := range c.handlers {
            go handler.Handle(event)
        }
    }
}

// 尝试重连
func (c *Client) tryReconnect() {
    for i := 0; i < 10; i++ {
        time.Sleep(5 * time.Second)
        log.Printf("尝试重连ESL (第%d次)...", i+1)

        // TODO: 重新连接
    }
}

// 添加事件处理器
func (c *Client) AddHandler(handler EventHandler) {
    c.handlers = append(c.handlers, handler)
}

// 关闭连接
func (c *Client) Close() {
    c.connected = false
    c.reconnect = false
    if c.conn != nil {
        c.conn.Close()
    }
    close(c.eventChan)
}

// 消息结构
type Message struct {
    Headers map[string]string
    Body    string
}

func (m *Message) GetHeader(name string) string {
    if v, ok := m.Headers[name]; ok {
        return v
    }
    return ""
}

// 解析事件
func parseEvent(msg *Message) *Event {
    event := &Event{
        Headers: make(map[string]string),
    }

    // 解析body中的headers
    lines := strings.Split(msg.Body, "\n")
    for _, line := range lines {
        parts := strings.SplitN(line, ": ", 2)
        if len(parts) == 2 {
            // URL解码
            value := strings.ReplaceAll(parts[1], "%20", " ")
            event.Headers[parts[0]] = value
        }
    }

    event.Name = event.GetHeader("Event-Name")
    event.UUID = event.GetHeader("Unique-ID")

    return event
}
```

#### 3.3.3 事件定义

```go
// esl/event.go
package esl

// 事件结构
type Event struct {
    Name    string
    UUID    string
    Headers map[string]string
}

func (e *Event) GetHeader(name string) string {
    if v, ok := e.Headers[name]; ok {
        return v
    }
    return ""
}

// 事件名称常量
const (
    EventChannelCreate         = "CHANNEL_CREATE"
    EventChannelAnswer         = "CHANNEL_ANSWER"
    EventChannelBridge         = "CHANNEL_BRIDGE"
    EventChannelUnbridge       = "CHANNEL_UNBRIDGE"
    EventChannelHangup         = "CHANNEL_HANGUP"
    EventChannelHangupComplete = "CHANNEL_HANGUP_COMPLETE"
    EventChannelState          = "CHANNEL_STATE"
    EventDTMF                  = "DTMF"
    EventRecordStart           = "RECORD_START"
    EventRecordStop            = "RECORD_STOP"
    EventCustom                = "CUSTOM"
)
```

#### 3.3.4 事件处理器

```go
// esl/handler.go
package esl

import (
    "callcenter/service"
    "log"
)

// 呼叫中心事件处理器
type CallCenterHandler struct {
    callService  *service.CallService
    agentService *service.AgentService
    acdService   *service.AcdService
}

func NewCallCenterHandler(
    callService *service.CallService,
    agentService *service.AgentService,
    acdService *service.AcdService,
) *CallCenterHandler {
    return &CallCenterHandler{
        callService:  callService,
        agentService: agentService,
        acdService:   acdService,
    }
}

func (h *CallCenterHandler) Handle(event *Event) {
    switch event.Name {
    case EventChannelCreate:
        h.handleChannelCreate(event)
    case EventChannelAnswer:
        h.handleChannelAnswer(event)
    case EventChannelBridge:
        h.handleChannelBridge(event)
    case EventChannelHangup:
        h.handleChannelHangup(event)
    case EventChannelHangupComplete:
        h.handleChannelHangupComplete(event)
    case EventDTMF:
        h.handleDTMF(event)
    }
}

func (h *CallCenterHandler) handleChannelCreate(event *Event) {
    uuid := event.UUID
    direction := event.GetHeader("Call-Direction")
    callerNumber := event.GetHeader("Caller-Caller-ID-Number")
    calleeNumber := event.GetHeader("Caller-Destination-Number")

    log.Printf("新呼叫: uuid=%s, direction=%s, caller=%s, callee=%s",
        uuid, direction, callerNumber, calleeNumber)

    // 创建呼叫记录
    call := &service.Call{
        UUID:         uuid,
        Direction:    direction,
        CallerNumber: callerNumber,
        CalleeNumber: calleeNumber,
        State:        "ringing",
    }

    h.callService.SaveCall(call)

    // 呼入排队
    if direction == "inbound" {
        skillGroup := event.GetHeader("variable_skill_group")
        call.SkillGroup = skillGroup
        h.acdService.Enqueue(call)
    }
}

func (h *CallCenterHandler) handleChannelAnswer(event *Event) {
    uuid := event.UUID
    log.Printf("通话接听: uuid=%s", uuid)

    call := h.callService.GetCall(uuid)
    if call != nil {
        call.State = "answered"
        h.callService.UpdateCall(call)
    }
}

func (h *CallCenterHandler) handleChannelBridge(event *Event) {
    uuid := event.UUID
    otherUUID := event.GetHeader("Other-Leg-Unique-ID")
    log.Printf("通道桥接: uuid=%s, other=%s", uuid, otherUUID)

    call := h.callService.GetCall(uuid)
    if call != nil {
        call.State = "bridged"
        call.BridgeUUID = otherUUID
        h.callService.UpdateCall(call)
    }
}

func (h *CallCenterHandler) handleChannelHangup(event *Event) {
    uuid := event.UUID
    hangupCause := event.GetHeader("Hangup-Cause")
    log.Printf("通话挂断: uuid=%s, cause=%s", uuid, hangupCause)

    call := h.callService.GetCall(uuid)
    if call != nil {
        call.State = "hangup"
        call.HangupCause = hangupCause
        h.callService.UpdateCall(call)

        // 更新坐席状态
        if call.AgentID != "" {
            h.acdService.OnCallEnd(call.AgentID, call)
        }
    }
}

func (h *CallCenterHandler) handleChannelHangupComplete(event *Event) {
    uuid := event.UUID
    billsec := event.GetHeader("variable_billsec")
    recordingPath := event.GetHeader("variable_recording_path")

    log.Printf("通话结束: uuid=%s, billsec=%s, recording=%s",
        uuid, billsec, recordingPath)

    call := h.callService.GetCall(uuid)
    if call != nil {
        call.Billsec = billsec
        call.RecordingPath = recordingPath
        h.callService.UpdateCall(call)
    }
}

func (h *CallCenterHandler) handleDTMF(event *Event) {
    uuid := event.UUID
    digit := event.GetHeader("DTMF-Digit")
    log.Printf("DTMF按键: uuid=%s, digit=%s", uuid, digit)
}
```

---

## 第四部分 呼叫中心场景开发实践

### 4.1 预测式外呼实现

```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.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;

import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * 预测式外呼服务
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class PredictiveDialerService {

    private final EslConnectionManager eslManager;
    private final AgentService agentService;
    private final CallService callService;

    // 外呼任务
    private final Map<String, DialTask> activeTasks = new ConcurrentHashMap<>();
    // 外呼线程池
    private final ExecutorService dialerExecutor = Executors.newFixedThreadPool(10);

    /**
     * 创建外呼任务
     */
    public String createTask(DialTaskRequest request) {
        String taskId = UUID.randomUUID().toString();

        DialTask task = new DialTask();
        task.setId(taskId);
        task.setName(request.getName());
        task.setSkillGroup(request.getSkillGroup());
        task.setCallerIdNumber(request.getCallerIdNumber());
        task.setNumbers(new LinkedBlockingQueue<>(request.getNumbers()));
        task.setMaxConcurrent(request.getMaxConcurrent());
        task.setState(TaskState.CREATED);
        task.setStats(new DialStats());

        activeTasks.put(taskId, task);
        log.info("创建外呼任务: taskId={}, name={}, numbers={}",
                taskId, request.getName(), request.getNumbers().size());

        return taskId;
    }

    /**
     * 启动任务
     */
    public void startTask(String taskId) {
        DialTask task = activeTasks.get(taskId);
        if (task == null) {
            throw new RuntimeException("任务不存在");
        }

        task.setState(TaskState.RUNNING);
        log.info("启动外呼任务: {}", taskId);

        // 启动外呼循环
        dialerExecutor.submit(() -> dialLoop(task));
    }

    /**
     * 暂停任务
     */
    public void pauseTask(String taskId) {
        DialTask task = activeTasks.get(taskId);
        if (task != null) {
            task.setState(TaskState.PAUSED);
            log.info("暂停外呼任务: {}", taskId);
        }
    }

    /**
     * 恢复任务
     */
    public void resumeTask(String taskId) {
        DialTask task = activeTasks.get(taskId);
        if (task != null && task.getState() == TaskState.PAUSED) {
            task.setState(TaskState.RUNNING);
            dialerExecutor.submit(() -> dialLoop(task));
            log.info("恢复外呼任务: {}", taskId);
        }
    }

    /**
     * 停止任务
     */
    public void stopTask(String taskId) {
        DialTask task = activeTasks.get(taskId);
        if (task != null) {
            task.setState(TaskState.STOPPED);
            log.info("停止外呼任务: {}", taskId);
        }
    }

    /**
     * 外呼循环
     */
    private void dialLoop(DialTask task) {
        log.info("开始外呼循环: taskId={}", task.getId());

        while (task.getState() == TaskState.RUNNING && !task.getNumbers().isEmpty()) {
            try {
                // 计算应该发起的呼叫数
                int dialCount = calculateDialCount(task);

                if (dialCount <= 0) {
                    // 没有空闲坐席，等待
                    Thread.sleep(1000);
                    continue;
                }

                // 发起外呼
                for (int i = 0; i < dialCount; i++) {
                    String number = task.getNumbers().poll();
                    if (number == null) {
                        break;
                    }

                    dial(task, number);
                }

                // 短暂等待，控制拨打速率
                Thread.sleep(500);

            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                break;
            } catch (Exception e) {
                log.error("外呼异常: {}", e.getMessage(), e);
            }
        }

        // 任务完成
        if (task.getNumbers().isEmpty()) {
            task.setState(TaskState.COMPLETED);
        }

        log.info("外呼循环结束: taskId={}, state={}", task.getId(), task.getState());
    }

    /**
     * 计算应该发起的呼叫数（预测算法）
     */
    private int calculateDialCount(DialTask task) {
        // 获取可用坐席数
        List<Agent> availableAgents = agentService.getAvailableAgents(task.getSkillGroup());
        int availableAgentCount = availableAgents.size();

        if (availableAgentCount == 0) {
            return 0;
        }

        // 当前正在拨打的数量
        int currentDialing = task.getStats().getCurrentDialing().get();

        // 历史接通率（动态计算）
        double connectRate = calculateConnectRate(task);

        // 预测式算法：
        // 目标：让坐席接到电话时，刚好有客户接通
        // 预测需要拨打数 = 空闲坐席数 / 接通率 * 系数
        double factor = 1.2; // 超拨系数，避免坐席空闲
        int predictedDial = (int) Math.ceil(availableAgentCount / connectRate * factor);

        // 限制最大并发
        int maxAllowed = task.getMaxConcurrent() - currentDialing;

        return Math.min(predictedDial, maxAllowed);
    }

    /**
     * 计算接通率
     */
    private double calculateConnectRate(DialTask task) {
        DialStats stats = task.getStats();
        int total = stats.getTotalDialed().get();
        int connected = stats.getConnected().get();

        if (total < 10) {
            // 样本太少，使用默认值
            return 0.3;
        }

        double rate = (double) connected / total;
        // 限制范围
        return Math.max(0.1, Math.min(0.8, rate));
    }

    /**
     * 发起单个外呼
     */
    private void dial(DialTask task, String number) {
        log.info("外呼: taskId={}, number={}", task.getId(), number);

        task.getStats().getTotalDialed().incrementAndGet();
        task.getStats().getCurrentDialing().incrementAndGet();

        try {
            // 构建originate命令
            // 先拨打客户，客户接听后转到队列等待坐席
            StringBuilder cmd = new StringBuilder();
            cmd.append("{");
            cmd.append("origination_caller_id_number=").append(task.getCallerIdNumber()).append(",");
            cmd.append("task_id=").append(task.getId()).append(",");
            cmd.append("dial_number=").append(number).append(",");
            cmd.append("dialer_mode=predictive");
            cmd.append("}");

            // 通过网关外呼
            cmd.append("sofia/gateway/carrier_primary/").append(number);

            // 客户接听后的处理
            cmd.append(" &lua(predictive_handler.lua)");

            String result = eslManager.sendSyncApiCommand("originate", cmd.toString());

            if (!result.startsWith("+OK")) {
                log.warn("外呼失败: number={}, result={}", number, result);
                task.getStats().getFailed().incrementAndGet();
            }

        } catch (Exception e) {
            log.error("外呼异常: number={}", number, e);
            task.getStats().getFailed().incrementAndGet();
        } finally {
            task.getStats().getCurrentDialing().decrementAndGet();
        }
    }

    /**
     * 客户接听回调（由事件触发）
     */
    public void onCustomerAnswer(String uuid, String taskId) {
        DialTask task = activeTasks.get(taskId);
        if (task == null) {
            eslManager.sendSyncApiCommand("uuid_kill", uuid + " NO_ROUTE_DESTINATION");
            return;
        }

        task.getStats().getConnected().incrementAndGet();

        // 查找可用坐席
        List<Agent> agents = agentService.getAvailableAgents(task.getSkillGroup());

        if (agents.isEmpty()) {
            // 无可用坐席，播放等待音乐
            eslManager.sendSyncApiCommand("uuid_broadcast",
                uuid + " ivr/please_wait.wav both");

            // 放入等待队列
            // TODO: 实现等待逻辑

            // 超时处理
            task.getStats().getAbandoned().incrementAndGet();
        } else {
            // 转接坐席
            Agent agent = agents.get(0);
            agentService.updateAgentState(agent.getId(), AgentState.RINGING);

            eslManager.sendSyncApiCommand("uuid_transfer",
                uuid + " " + agent.getExtension() + " XML default");

            task.getStats().getTransferred().incrementAndGet();
        }
    }

    /**
     * 获取任务统计
     */
    public DialStats getTaskStats(String taskId) {
        DialTask task = activeTasks.get(taskId);
        return task != null ? task.getStats() : null;
    }
}

/**
 * 外呼任务
 */
@Data
class DialTask {
    private String id;
    private String name;
    private String skillGroup;
    private String callerIdNumber;
    private BlockingQueue<String> numbers;
    private int maxConcurrent;
    private TaskState state;
    private DialStats stats;
}

/**
 * 任务状态
 */
enum TaskState {
    CREATED, RUNNING, PAUSED, STOPPED, COMPLETED
}

/**
 * 外呼统计
 */
@Data
class DialStats {
    private AtomicInteger totalDialed = new AtomicInteger(0);
    private AtomicInteger currentDialing = new AtomicInteger(0);
    private AtomicInteger connected = new AtomicInteger(0);
    private AtomicInteger transferred = new AtomicInteger(0);
    private AtomicInteger failed = new AtomicInteger(0);
    private AtomicInteger abandoned = new AtomicInteger(0);
}
```

### 4.2 机器人外呼实现

```java
package com.callcenter.service;

import com.callcenter.esl.EslConnectionManager;
import com.callcenter.model.*;
import com.callcenter.tts.TtsService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.util.*;
import java.util.concurrent.*;

/**
 * 机器人外呼服务
 * 用于批量通知、回访、满意度调查
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class RobotDialerService {

    private final EslConnectionManager eslManager;
    private final TtsService ttsService;

    private final Map<String, RobotTask> activeTasks = new ConcurrentHashMap<>();
    private final ExecutorService executor = Executors.newFixedThreadPool(20);

    /**
     * 创建机器人外呼任务
     */
    public String createTask(RobotTaskRequest request) {
        String taskId = UUID.randomUUID().toString();

        RobotTask task = new RobotTask();
        task.setId(taskId);
        task.setType(request.getType()); // NOTIFICATION, SURVEY, CALLBACK
        task.setNumbers(new LinkedBlockingQueue<>(request.getNumbers()));
        task.setTtsText(request.getTtsText());
        task.setVariables(request.getVariables()); // 每个号码的变量
        task.setMaxConcurrent(request.getMaxConcurrent());
        task.setRetryTimes(request.getRetryTimes());
        task.setCollectDtmf(request.isCollectDtmf());
        task.setState(TaskState.CREATED);
        task.setResults(new ConcurrentHashMap<>());

        activeTasks.put(taskId, task);
        return taskId;
    }

    /**
     * 启动任务
     */
    public void startTask(String taskId) {
        RobotTask task = activeTasks.get(taskId);
        if (task == null) {
            throw new RuntimeException("任务不存在");
        }

        task.setState(TaskState.RUNNING);
        executor.submit(() -> robotDialLoop(task));
    }

    /**
     * 机器人外呼循环
     */
    private void robotDialLoop(RobotTask task) {
        log.info("开始机器人外呼: taskId={}", task.getId());

        Semaphore semaphore = new Semaphore(task.getMaxConcurrent());

        while (task.getState() == TaskState.RUNNING) {
            String number = task.getNumbers().poll();
            if (number == null) {
                break;
            }

            try {
                semaphore.acquire();
                executor.submit(() -> {
                    try {
                        robotCall(task, number);
                    } finally {
                        semaphore.release();
                    }
                });
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                break;
            }
        }

        // 等待所有呼叫完成
        try {
            semaphore.acquire(task.getMaxConcurrent());
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }

        task.setState(TaskState.COMPLETED);
        log.info("机器人外呼完成: taskId={}", task.getId());
    }

    /**
     * 单个机器人呼叫
     */
    private void robotCall(RobotTask task, String number) {
        log.info("机器人呼叫: taskId={}, number={}", task.getId(), number);

        RobotCallResult result = new RobotCallResult();
        result.setNumber(number);
        result.setStartTime(System.currentTimeMillis());

        try {
            // 替换TTS文本中的变量
            String ttsText = replaceTtsVariables(task.getTtsText(),
                task.getVariables().get(number));

            // 生成语音文件
            String audioFile = ttsService.synthesize(ttsText);

            // 构建外呼命令
            StringBuilder cmd = new StringBuilder();
            cmd.append("{");
            cmd.append("task_id=").append(task.getId()).append(",");
            cmd.append("dial_number=").append(number).append(",");
            cmd.append("robot_mode=true").append(",");
            cmd.append("audio_file=").append(audioFile);
            cmd.append("}");

            cmd.append("sofia/gateway/carrier_primary/").append(number);

            if (task.isCollectDtmf()) {
                // 需要收集按键
                cmd.append(" &lua(robot_survey.lua)");
            } else {
                // 纯播报
                cmd.append(" &playback(").append(audioFile).append(")");
            }

            String apiResult = eslManager.sendSyncApiCommand("originate", cmd.toString());

            if (apiResult.startsWith("+OK")) {
                result.setStatus("SUCCESS");
                result.setCallUuid(apiResult.substring(4).trim());
            } else {
                result.setStatus("FAILED");
                result.setError(apiResult);

                // 重试逻辑
                handleRetry(task, number);
            }

        } catch (Exception e) {
            log.error("机器人呼叫异常: number={}", number, e);
            result.setStatus("ERROR");
            result.setError(e.getMessage());
            handleRetry(task, number);
        }

        result.setEndTime(System.currentTimeMillis());
        task.getResults().put(number, result);
    }

    /**
     * 替换TTS变量
     */
    private String replaceTtsVariables(String template, Map<String, String> vars) {
        if (vars == null || vars.isEmpty()) {
            return template;
        }

        String result = template;
        for (Map.Entry<String, String> entry : vars.entrySet()) {
            result = result.replace("${" + entry.getKey() + "}", entry.getValue());
        }
        return result;
    }

    /**
     * 处理重试
     */
    private void handleRetry(RobotTask task, String number) {
        int retryCount = task.getRetryCount().getOrDefault(number, 0);
        if (retryCount < task.getRetryTimes()) {
            task.getRetryCount().put(number, retryCount + 1);
            task.getNumbers().offer(number);
            log.info("加入重试队列: number={}, retry={}", number, retryCount + 1);
        }
    }

    /**
     * 处理DTMF按键结果（由事件触发）
     */
    public void onDtmfResult(String uuid, String taskId, String number, String digit) {
        RobotTask task = activeTasks.get(taskId);
        if (task == null) {
            return;
        }

        RobotCallResult result = task.getResults().get(number);
        if (result != null) {
            result.setDtmfDigit(digit);
            log.info("收到按键: taskId={}, number={}, digit={}", taskId, number, digit);
        }
    }

    /**
     * 获取任务结果
     */
    public List<RobotCallResult> getTaskResults(String taskId) {
        RobotTask task = activeTasks.get(taskId);
        if (task == null) {
            return Collections.emptyList();
        }
        return new ArrayList<>(task.getResults().values());
    }
}
```

### 4.3 班长监控实现

```java
package com.callcenter.service;

import com.callcenter.esl.EslConnectionManager;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

/**
 * 班长监控服务
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class SupervisorService {

    private final EslConnectionManager eslManager;

    /**
     * 监听坐席通话
     * 班长可以听到双方说话，但双方听不到班长
     */
    public void spy(String supervisorExtension, String targetUuid) {
        log.info("开始监听: supervisor={}, target={}", supervisorExtension, targetUuid);

        // 使用eavesdrop应用
        // originate user/supervisor &eavesdrop(target_uuid)
        String cmd = String.format(
            "{origination_caller_id_name=监听}user/%s &eavesdrop(%s)",
            supervisorExtension, targetUuid
        );

        String result = eslManager.sendSyncApiCommand("originate", cmd);

        if (!result.startsWith("+OK")) {
            throw new RuntimeException("监听失败: " + result);
        }
    }

    /**
     * 强插通话
     * 班长加入通话，三方都可以听到
     */
    public void barge(String supervisorExtension, String targetUuid) {
        log.info("强插通话: supervisor={}, target={}", supervisorExtension, targetUuid);

        // 使用三方会议实现
        String confName = "barge_" + targetUuid;

        // 1. 将目标通话转入会议
        eslManager.sendSyncApiCommand("uuid_transfer",
            targetUuid + " conference:" + confName + "+flags{mute} inline");

        // 2. 呼叫班长加入会议
        String cmd = String.format(
            "{origination_caller_id_name=强插}user/%s &conference(%s)",
            supervisorExtension, confName
        );

        eslManager.sendSyncApiCommand("originate", cmd);
    }

    /**
     * 强拆通话
     * 强制挂断坐席通话
     */
    public void hangup(String targetUuid, String reason) {
        log.info("强拆通话: target={}, reason={}", targetUuid, reason);

        eslManager.sendSyncApiCommand("uuid_kill", targetUuid + " " + reason);
    }

    /**
     * 密语（耳语）
     * 班长对坐席说话，客户听不到
     */
    public void whisper(String supervisorExtension, String agentUuid) {
        log.info("密语: supervisor={}, agent={}", supervisorExtension, agentUuid);

        // 使用eavesdrop的whisper模式
        String cmd = String.format(
            "{origination_caller_id_name=密语}user/%s &eavesdrop(%s whisper)",
            supervisorExtension, agentUuid
        );

        eslManager.sendSyncApiCommand("originate", cmd);
    }

    /**
     * 获取实时通话列表
     */
    public String getActiveCalls() {
        return eslManager.sendSyncApiCommand("show", "calls");
    }

    /**
     * 获取坐席通道信息
     */
    public String getChannelInfo(String uuid) {
        return eslManager.sendSyncApiCommand("uuid_dump", uuid);
    }
}
```

---

## 第五部分 高可用部署方案

### 5.1 主备架构

```
                           ┌─────────────────┐
                           │   Keepalived    │
                           │   VIP漂移管理    │
                           └────────┬────────┘
                                    │
        ┌───────────────────────────┴───────────────────────────┐
        │                      VIP: 10.10.1.100                 │
        │                    (SIP/ESL统一入口)                   │
        └───────────────────────────┬───────────────────────────┘
                                    │
              ┌─────────────────────┴─────────────────────┐
              │                                           │
              ▼                                           ▼
   ┌────────────────────┐                    ┌────────────────────┐
   │   Master节点        │    心跳检测        │   Backup节点        │
   │   10.10.1.10       │◄──────────────────►│   10.10.1.11       │
   ├────────────────────┤                    ├────────────────────┤
   │  FreeSWITCH        │                    │  FreeSWITCH        │
   │  CTI中间件         │                    │  CTI中间件         │
   │  API服务           │                    │  API服务           │
   └────────────────────┘                    └────────────────────┘
              │                                           │
              │         共享存储 / 数据同步                │
              │                                           │
              └─────────────────────┬─────────────────────┘
                                    │
         ┌──────────────────────────┼──────────────────────────┐
         │                          │                          │
         ▼                          ▼                          ▼
┌─────────────────┐      ┌─────────────────┐      ┌─────────────────┐
│  MySQL 主从     │      │ Redis Sentinel  │      │    NAS存储      │
│  10.10.2.10/11  │      │  10.10.2.20-22  │      │   10.10.2.30    │
└─────────────────┘      └─────────────────┘      └─────────────────┘
```

### 5.2 Keepalived配置

**Master节点配置：**

```bash
# /etc/keepalived/keepalived.conf (Master)

global_defs {
    router_id FS_MASTER
}

# 检测FreeSWITCH状态的脚本
vrrp_script check_freeswitch {
    script "/etc/keepalived/check_freeswitch.sh"
    interval 2
    weight -20
    fall 3
    rise 2
}

vrrp_instance VI_FS {
    state MASTER
    interface eth0
    virtual_router_id 51
    priority 100
    advert_int 1

    authentication {
        auth_type PASS
        auth_pass your_password
    }

    virtual_ipaddress {
        10.10.1.100/24 dev eth0
    }

    track_script {
        check_freeswitch
    }

    notify_master "/etc/keepalived/notify_master.sh"
    notify_backup "/etc/keepalived/notify_backup.sh"
    notify_fault "/etc/keepalived/notify_fault.sh"
}
```

**检测脚本：**

```bash
#!/bin/bash
# /etc/keepalived/check_freeswitch.sh

# 检测FreeSWITCH进程
if ! pgrep -x "freeswitch" > /dev/null; then
    exit 1
fi

# 检测ESL端口
if ! nc -z localhost 8021; then
    exit 1
fi

# 检测SIP端口
if ! nc -z localhost 5060; then
    exit 1
fi

# 执行简单API命令测试
result=$(fs_cli -x "status" 2>/dev/null)
if [[ $? -ne 0 ]]; then
    exit 1
fi

exit 0
```

**主节点切换通知脚本：**

```bash
#!/bin/bash
# /etc/keepalived/notify_master.sh

# 记录日志
echo "$(date): Becoming MASTER" >> /var/log/keepalived-state.log

# 启动FreeSWITCH（如果未运行）
systemctl start freeswitch

# 启动CTI服务
systemctl start callcenter-cti

# 发送告警通知
curl -X POST "http://your-alert-api/notify" \
    -d "message=FreeSWITCH切换到Master节点"
```

### 5.3 配置同步方案

```bash
#!/bin/bash
# sync_config.sh - 配置同步脚本

MASTER_HOST="10.10.1.10"
BACKUP_HOST="10.10.1.11"
FS_CONF_DIR="/usr/local/freeswitch/conf"

# 使用rsync同步配置
sync_config() {
    rsync -avz --delete \
        ${FS_CONF_DIR}/ \
        ${BACKUP_HOST}:${FS_CONF_DIR}/

    # 通知备机重载配置
    ssh ${BACKUP_HOST} "fs_cli -x 'reloadxml'"
}

# 监控配置变化
inotifywait -m -r -e modify,create,delete ${FS_CONF_DIR} | while read path action file; do
    echo "配置变化: $path$file ($action)"
    sync_config
done
```

### 5.4 FreeSWITCH集群配置

对于需要更高并发的场景，可以部署FreeSWITCH集群：

```xml
<!-- conf/autoload_configs/sofia.conf.xml -->
<configuration name="sofia.conf">
  <global_settings>
    <!-- 启用分布式设备状态 -->
    <param name="debug-presence" value="0"/>
  </global_settings>

  <profiles>
    <profile name="internal">
      <!-- ... -->

      <!-- 分布式配置 -->
      <gateways>
        <!-- 集群内其他节点 -->
        <gateway name="cluster_node_2">
          <param name="realm" value="10.10.1.12"/>
          <param name="register" value="false"/>
        </gateway>
      </gateways>
    </profile>
  </profiles>
</configuration>
```

---

## 第六部分 性能优化指南

### 6.1 FreeSWITCH优化

**系统级优化：**

```bash
# /etc/security/limits.conf
freeswitch soft nofile 65536
freeswitch hard nofile 65536
freeswitch soft core unlimited
freeswitch hard core unlimited

# /etc/sysctl.conf
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216
net.core.rmem_default = 1048576
net.core.wmem_default = 1048576
net.core.netdev_max_backlog = 30000
net.ipv4.tcp_max_syn_backlog = 10240
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_fin_timeout = 15
```

**FreeSWITCH配置优化：**

```xml
<!-- conf/autoload_configs/switch.conf.xml -->
<configuration name="switch.conf">
  <settings>
    <!-- 核心线程数（根据CPU核心数调整） -->
    <param name="min-idle-cpu" value="25"/>
    <param name="max-sessions" value="2000"/>
    <param name="sessions-per-second" value="100"/>

    <!-- 日志级别（生产环境降低日志级别） -->
    <param name="loglevel" value="warning"/>

    <!-- RTP端口范围 -->
    <param name="rtp-start-port" value="16384"/>
    <param name="rtp-end-port" value="32768"/>
  </settings>
</configuration>
```

### 6.2 CTI中间件优化

```java
// 连接池配置
@Configuration
public class ConnectionPoolConfig {

    @Bean
    public ThreadPoolTaskExecutor eslEventExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(8);
        executor.setMaxPoolSize(32);
        executor.setQueueCapacity(10000);
        executor.setThreadNamePrefix("esl-event-");
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.initialize();
        return executor;
    }

    @Bean
    public RedissonClient redissonClient() {
        Config config = new Config();
        config.useClusterServers()
            .addNodeAddress("redis://10.10.2.20:6379")
            .addNodeAddress("redis://10.10.2.21:6379")
            .addNodeAddress("redis://10.10.2.22:6379")
            .setMasterConnectionPoolSize(64)
            .setSlaveConnectionPoolSize(64);
        return Redisson.create(config);
    }
}
```

### 6.3 监控指标

```yaml
# prometheus配置
scrape_configs:
  - job_name: 'freeswitch'
    static_configs:
      - targets: ['10.10.1.10:9282', '10.10.1.11:9282']

  - job_name: 'callcenter-cti'
    static_configs:
      - targets: ['10.10.1.10:8080', '10.10.1.11:8080']
    metrics_path: '/actuator/prometheus'
```

**关键监控指标：**

| 指标 | 说明 | 阈值 |
|------|------|------|
| freeswitch_sessions_active | 当前活跃会话数 | <80%最大值 |
| freeswitch_sessions_per_second | 每秒新建会话数 | <100 |
| freeswitch_cpu_usage | CPU使用率 | <70% |
| freeswitch_memory_usage | 内存使用率 | <80% |
| esl_event_queue_size | 事件队列大小 | <5000 |
| acd_queue_wait_time | 平均等待时间 | <30s |
| agent_utilization | 坐席利用率 | 目标>70% |

---

## 总结

本文档详细介绍了FreeSWITCH的技术架构和ESL开发实践，涵盖了：

1. **核心概念**：Channel、Session、Event等基础概念
2. **模块详解**：mod_sofia、Dialplan、ESL等核心模块
3. **ESL开发**：Java和Go两种语言的开发示例
4. **场景实践**：ACD、预测式外呼、机器人外呼、班长监控
5. **高可用部署**：主备架构、Keepalived配置
6. **性能优化**：系统优化、应用优化、监控指标

建议开发团队在正式开发前：
- 搭建FreeSWITCH测试环境熟悉操作
- 仔细研读FreeSWITCH官方文档
- 从简单场景（如点击拨号）开始，逐步实现复杂功能
- 重视测试，特别是高可用切换测试和压力测试
