AIAPI开发文档

AIAPI 接口

AIAPI 是一套类似于 HTTAPI(mod_httapi)的接口,在简单接口基础上增强了交互能力。

mod_ai是实现为 XSwitch 里面的一个模块, 跟类似于mod_httapi, 让应用服务器用 HTTP 协议跟 XSwitch 进行交互, 以便控制呼叫和媒体。不同的是, mod_httapi基于 XML 格式, 而mod_ai则基于 JSON 格式。此外, mod_ai还提供外呼功能。

呼入配置

对于呼入电话,在 XSwitch 中做如下配置。添加一条路由,参数如下:

  • 被叫字冠:如8888
  • 最大号长:如4
  • 目的地类型:系统
  • 内容:ai {url=http://192.168.2.100/ivr}

然后呼叫8888即可在 HTTP 服务器端收到请求。

如果没有图形界面,在 Dialplan 中使用如下配置:

<extension name="ai">
	<condition field="destination_number" expression="^8888$">
		<action application="answer"/>
		<action application="ai" data="{url=http://192.168.2.100/ivr}"/>
	</condition>
</extension>

如上所示, 用户呼叫8888, XSwitch 在路由呼叫时向指定的应用服务器(WebServer)发 HTTP 请求, 然后根据返回内容(Body)做相应的动作, 诸如语音播放/TTS/语音识别/录音/转移呼叫/挂机/会议等。

HTTP 协议细节方面, 是这样的:

  • mod_ai发 HTTP POST 到应用服务器,Content-Type 是application/json
  • 应用服务器回应 200 OK,Content-Type 也必须是application/json

响应 Body 的组成一般是action + next + variables + privateData。编码必须是 UTF8。

这里有一个例子:

mod_ai向应用服务器发 HTTP POST 请求:

POST /ivr_entry HTTP/1.1
Host: 192.168.2.100
Content-Type: application/json

{
	"uuid": "eb7d45e0-4eb5-11e8-99df-53f214a93242",
	"url": "http://192.168.2.117:4567/ivr_entry",
	"hostname": "debian",
	"cid_number": "1018",
	"dest_number": "8888",
	"call_status": "ringing",
	"direction": "inbound",
	"channel_data":
	{
		"Caller-Direction": "inbound",
		"Caller-Logical-Direction": "inbound",
		"Caller-Username": "1018",
		"Caller-Dialplan": "XML",

		"Caller-Caller-ID-Name": "1018",
		"Caller-Caller-ID-Number": "1018",
		.
		.
		.
	}
}

这里的uuid指的是 XSwitch 内部通道的uuidurl来自 Dialplan 配置。channel_data包含的内容是 XSwitch 通道变量。

应用服务器回应 200 OK:

HTTP/1.1 200 OK
Content-Type: application/json

{
	"action": "play",
	"next": "http://192.168.2.100/play_ack",
	"file": "say:您好!请说出您要办理的业务",
	"name": "dialed_digits",
	"input_timeout": "5000",
	"asr_engine": "iflyrest",
	"asr_grammar": "{accent=mandarin, barge-in=true, speech-timeout=30000}default",
	"variables":
	{
		"tts_engine": "iflyrest",
		"tts_voice": "xiaoyan"
	},
	"privateData":
	{
		"myseq": "1"
	}
}

其中:

asr_grammar中补充参数如下:

  • speech-timeout为通话时长(包括说话时间和一句话结束后的等待时间,单位毫秒)。
  • no-input-timeout是没有任何语音输入时等待时长,单位毫秒。
  • silence-ms是一句话结束后等待多少时间,单位毫秒。
  • silence-ms是一句话结束后等待多少时间,单位毫秒。
  • threshold声音阈值,越低越灵敏。

上述应用中参数解释:

  • action是通知mod_ai要做的动作, 比如play(语音播音), execute(执行 App), hangup(挂机)等等。
  • next指定下次请求的 URL, mod_ai做完指定动作之后向next指定的 URL 发送 HTTP POST 请求。
  • variables是 XSwitch 通道变量, 比如做 TTS 时候指定 TTS 引擎(tts_engine)、播放多个语音文件时指定文件分隔符(playback_delimiter)等。上面的例子就是指定了 TTS 引擎。 通道变量可参考这里, https://freeswitch.org/confluence/display/XSwitch/Channel+Variables
  • privateData是自定义数据, mod_ai一般不做任何解释, 下次 HTTP POST 时自动回传。

很显然,mod_ai就是在 FreeWITCH 和应用服务器之间提供了一层 HTTP 接口, 让应用服务器控制呼叫和媒体。

支持的 action 说明

play

作用: 播放语音文件(或者 tts), 启动 asr(可选) 属性:

  • file: 要播放的语音文件, 可以是本地文件, 也可以是 HTTP 路径, 比如http://www.xxx.com/abc.wav
  • error_file: 如果输入错误(与正则表达式不匹配), 则播放此文件
  • name: 参数名称, 用于存储收到的 DTMF 字符串, mod_ai下次发 HTTP 请求时自动带回来
  • digit_timeout: 等待 DTMF 的超时时间
  • input_timeout: 多个 DTMF 输入的最大间隔时间
  • loops: 输入错误(与正则表达式不匹配)情况下的最大重试次数
  • asr_engine: ASR 引擎
  • asr_grammar: ASR 语法, 如果要启动语音识别, 那么需要同时指定 asr_engine 和 asr_grammar)
  • bind: 定义类似 bind_digit_action App 一样绑定一个正则表达式, 如果输入与正则表达式不匹配, 就播放 error_file(例如 invalid.wav)

bind支持的属性:

  • strip: 在 DTMF 串中去掉相关字符, 一般是#, 我们经常用作结束符但实际并不需要它。
  • input: 定义输入的正则表达式

常用的组合,播放放获取 DTMF。例子:

{
  "action": "play",
  "next": "http://192.168.2.100/play_ack",
  "file": "welcome.wav",
  "error_file": "error.wav",
  "loops": "3",
  "name": "dialed_digits",
  "input_timeout": "5000",
  "digit_timeout": "3000",
  "bind": {
    "strip": "#",
    "input": "~\\d+"
  }
}

目的是播放语音 welcome.wav, 输入任意数字键

  • tts_and_asr

例子:

{
  "action": "play",
  "next": "http://192.168.2.100/play_ack",
  "file": "say:您好!请说出您要办理的业务",
  "name": "dialed_digits",
  "input_timeout": "5000",
  "digit_timeout": "3000",
  "asr_engine": "iflyrest",
  "asr_grammar": "{accent=mandarin, barge-in=true, speech-timeout=30000}default",
  "variables": {
    "tts_engine": "iflyrest",
    "tts_voice": "xiaoyan"
  }
}
  • file_and_asr

例子:

{
  "action": "play",
  "next": "http://192.168.2.100/play_ack",
  "file": "welcome.wav!silence_stream://1000!greet.wav",
  "name": "dialed_digits",
  "input_timeout": "5000",
  "asr_engine": "iflyrest",
  "asr_grammar": "{accent=mandarin, barge-in=true, speech-timeout=30000}default",
  "variables": {
    "playback_delimiter": "!"
  }
}

这里要播放多个文件, 所以要指定文件名的分隔符。

  • 只播放文件, 不启动语音识别, 不等待 DTMF

例子:

{
  "action": "play",
  "next": "http://192.168.2.100/play_ack",
  "file": "welcome.wav",
  "name": "playonly"
}

record

作用: 录音, 一般做留言用, 呼叫结束前将录音文件 HTTP 上传回应用服务器。 属性:

  • file: 文件名称, 可以是本地文件或 HTTP 路径
  • next: 指定一个新的 URL(后续请求使用)
  • name: 参数名称, mod_ai 下次发 HTTP 请求时自动带回来。默认是 attached_file
  • error_file: 如果输入错误(与正则表达式不匹配), 则播放此文件
  • digit_timeout: 等待 DTMF 的超时时间, 单位是毫秒
  • input_timeout: 多个 DTMF 输入的最大间隔时间, 单位是毫秒
  • limit: 最大时长,单位是秒, 0 是不限制
  • beep_file: 录音开始之前播放 beep 提示
  • silence_hits: 静音时间,单位是秒。默认是 2 秒,这意味着如果检查 2 秒都是静音, 那么录音就自动结束
  • bind: 定义类似bind_digit_action App 一样绑定一个正则表达式, 如果输入与正则表达式不匹配, 就播放 error_file(例如 invalid.wav)

bind 支持的属性:

  • strip: 在 DTMF 串中去掉相关字符, 一般是#, 我们经常用作结束符但实际并不需要它。
  • input: 定义输入的正则表达式

例子:

{
  "action": "record",
  "next": "http://192.168.2.100/record_ack",
  "file": "010203.wav",
  "name": "recordName",
  "limit": "20",
  "silence_hits": "5",
  "bind": {
    "strip": "#",
    "input": "~\\d|*#"
  }
}

最大时长 20 秒, 任意 DTMF 结束录音

record_call

作用: 录音, 但跟之前的 record 不同, record 一般用作留言, 只录进来的声音。record_call 是录 session, 一般采用双声道录制, 分别保存进来和出去两个方向的声音。 属性:

  • limit: 最大时长, 0 是不限制
  • file: 录音文件名称
  • name: 参数名称, mod_ai 下次发 HTTP 请求时自动带回来
  • next: 指定一个新的 URL(后续请求使用)

说明: mod_ai 收到 record_call 之后, 马上给 next URL 发一次 HTTP POST 在 reporting 阶段(呼叫结束前)再给 next URL 发一次 HTTP POST 后面的这次用的是 form fileupload 方式上传录音文件(详情可参考测试案例)

例子:

{
	"action": "record_call",
	"next": "http://192.168.2.100/record_call_ack",
	"file": "20180516/010203.wav", #建议使用相对路径
	"name": "record_callName"
}

或者

{
	"action": "record_call",
	"next": "http://192.168.2.100/record_call_ack",
	"file": "20180516/010203.wav",
	"name": "record_callName",
	"variables":
	{
		"RECORD_STEREO": "false",
		"RECORD_READ_ONLY": "true"
	}
}

这样只录进来的语音。

execute

作用: 执行 XSwitch Application

属性:

  • next: 指定一个新的 URL(后续请求使用)
  • application: XSwitch Application
  • data: Application 参数

默认情况下只允许执行 answer, sleep, hangup, info。如果要执行别的 Application, 则需要修改配置文件

例子:

{
	"action": "execute",
	"next": "http://192.168.2.100/execute_ack",
	"application": "hangup"
}

这是另外一个例子

{
	"action": "execute",
	"next": "http://192.168.2.100/sleep",
	"application": "sleep",
	"data": "1000"
}

transfer

作用: 根据 callee-id-numbe 的不同或者执行外呼(bridge), 或者执行转移呼叫(transfer 到 dialplan 重新路由)

属性:

  • next: 指定一个新的 URL(后续请求使用)
  • context: Dialplan context, 默认是 default
  • dialplan: Dialplan dialplan, 默认是 XML
  • caller-id-name: 主叫名称
  • caller-id-number: 主叫号码
  • callee-id-number: 指定被叫号码(不包含"/")或者呼叫字符串(包含"/")

说明:

  • 如果 callee-id-number 是"1001"这种形式的号码(也就是不包含"/"), mod_ai 会 transfer 到 dialplan 里面路由
  • 如果是这种形式"user/1001"或者"sofia/gateway/gw1/137xxx"(包含了"/"), 那么其实就是执行 bridge

一般的建议是, 呼叫外线可直接指定网关名称, 比如"sofia/gateway/gw1/137xxx"(走 gw1)。 呼叫内线则在内线号码前加上字冠, 然后 transfer 到 dialplan 里面路由, 比如"a1001"。这样更灵活一些。(在 dialplan 里面去掉字冠, 呼叫内线。如果呼叫 1001 失败还可以继续呼叫 1002, 等等)

例子:

{
	"action": "transfer",
	"next": "http://192.168.2.117:4567/transfer_ack",
	"context": "default",
	"dialplan": "XML",
	"caller_id_number": "1002",
	"callee_id_number": "a1001"
	"variables": {
		"ringback": "${cn-ring}",
		"transfer_ringback": "local_stream://moh",
		"instant_ringback": "true"
	}

例子中部分参数说明:

  • ringback: 回铃音(即,通话后用户听到的等待音)。
  • transfer_ringback: 转接音(如果将通话转接,则,转接过程中听到的音乐),也可填入自定义的语音文件(填入语音文件所在绝对路径即可)。
  • instant_ringback:是配合 ringback 使用,目的在于 bridge 后立即回回铃音(可用也可不用)。

conference

作用: 会议

属性:

  • profile: 指定 Conference profile, 默认值是 default
  • next: 指定一个新的 URL(后续请求使用)
  • pin: 指定会议密码
  • name: 会议的名称, 这个参数不能缺少, 否则就做挂机处理,会议名称为英文、下划线和数字,不能有空格和特殊字符。

例子:

{
	"action": "conference",
	"next": "http://192.168.2.117:4567/conference_ack",
	"pin": "8888",
	"name": "cnftest"

answer

作用: 应答

属性:

  • next: 指定一个新的 URL(后续请求使用)

例子:

{
	"action": "answer",
	"next": "http://192.168.2.117:4567/answered"
}

hangup

作用: 挂机

属性:

  • next: 指定一个新的 URL(后续请求使用)
  • cause: 挂机原因

例子:

{
	"action": "hangup",
	"next": "http://192.168.2.117:4567/hangup_ack"
}

uuid_kill

作用:对某一路通话进行挂机

获取到想要挂机的通话的唯一uuid,执行如下指令,具体如下:

POST /v1 HTTP/1.1       //url是/v1
Host: 192.168.2.129:8081 //一般是8081端口
Content-Type: application/json
[HTTP request 1/1]

{
	"id":"1",
	"jsonrpc":"2.0",
	"method":"ai.api",
	"params":
		{
			"command":"uuid_kill",
			"params": "toggle 0898bb2f-dc57-4b05-af5e-d0bdf774ca4f", //需要挂机的通话的`uuid`
			"url":"http://localhost:4567/asr",
			"private_data":
			{
				"aaa": "1",
				"bbb": "2"
			}
		}
}

log

作用: 写日志

属性:

  • next: 指定一个新的 URL(后续请求使用)
  • level: 日志级别 CRIT ERR WARNING NOTICE INFO DEBUG
  • clean: 如果为 true, 那么不添加时间
  • data: 日志内容

例子:

{
	"action": "log",
	"next": "http://192.168.2.117:4567/log_ack",
	"level": "DEBUG",
	"data": "you are welcome!"

break

作用: 退出 ai 模块, 执行 dialplan 里面的后续动作

属性:

  • next: 指定一个新的 URL(后续请求使用)

例子:

{
	"action": "break",
	"next": "http://192.168.2.117:4567/break_ack"

dialplan 如果是这样的话:

<extension name="ai">
  <condition field="destination_number" expression="^8888$">
    <action application="answer" />
    <action application="ai" data="{url=http://192.168.2.100/ivr_entry}" />
    <action application="playback" data="bye.wav" />
  </condition>
</extension>

退出 ai 模块之后, 自动播放 bye.wav

continue

作用: 继续, 什么也不做, 一般用来过度一下。

属性:

  • next: 指定一个新的 URL(后续请求使用)

例子:

{
	"action": "continue",
	"next": "http://192.168.2.117:4567/continue_ack"

getVar

作用: 获取通道变量

属性:

  • next: 指定一个新的 URL(后续请求使用)
  • permanet: 永久参数或者仅仅一次

例子:

{
	"action": "getVar",
	"next": "http://192.168.2.117:4567/getVar_ack"

voicemail

作用: 检查或执行语音邮箱

属性:

  • check: 如果为 true 则检查邮箱中的语音邮箱
  • authOnly: 仅鉴权
  • profile: 指定 Profile, 默认为 default
  • domain: 域, 默认为全局变量 domain
  • id: ID, 如果为空则会询问 ID

例子:

{
	"action": "voicemail",
	"next": "http://192.168.2.117:4567/voicemail_ack",
	"check": "true",
	"authOnly": "1",
	"profile": "default",
	"domain": "192.168.1.101",
	"id": "1010"

speak

作用: 用 TTS 播放语音

属性:

  • next: 指定一个新的 URL(后续请求使用)
  • name: 参数名称, mod_ai 下次发 HTTP 请求时自动带回来
  • tts_engine: TTS 引擎
  • tts_voice: TTS 嗓音
  • text: 要播放的文本
  • digit_timeout: 等待 DTMF 的超时时间
  • input_timeout: 多个 DTMF 输入的最大间隔时间
  • loops: 输入错误(与正则表达式不匹配)情况下的最大重试次数
  • bind: 定义类似 bind_digit_action App 一样绑定一个正则表达式, 如果输入与正则表达式不匹配, 就播放 error_file(例如 invalid.wav)

bind 支持的属性:

  • strip: 在 DTMF 串中去掉相关字符, 一般是#, 我们经常用作结束符但实际并不需要它。
  • input: 定义输入的正则表达式

例子:

{
	"action": "speak",
	"next": "http://192.168.2.117:4567/speak_ack",
	"name": "name-tts",
	"tts_engine": "baidu",
	"tts_voice": "0",
	"text": "您好!欢迎来到xxx"

呼出

上面列举的全部是呼入后的处理。如果要让mod_ai呼出, 那么应用服务器给 XSwitch 发 JSON-RPC 请求即可。

有关 JSON-RPC 的资料见附件。

呼出分ai.dialai.dial2两种,其中前者适用一路呼叫,或呼叫后再转接的场景,后者可同时发起两路呼叫并桥接。

ai.dial

适用单路外呼的场景。

JSON-RPC 的内容如下:

{
	"jsonrpc": "2.0",
	"method": "ai.dial",
	"id": 1, //id可以自己指定
	"params":
	{
		"application": "ai",
		"dial_string":"user/1006", //可选
		"outbound_gateway": "可选, 网关名称, 平台指定, 默认为default",
		"user_gateway": "可选, 网关sip注册到XSwitch",
		"from": "主叫号码",
		"to": "被叫号码",
		"url": "呼叫成功后要访问的url",
		"external_tracking_id":"1234556778", //应用侧ID, 任意字符串, 在应用侧应该唯一
		"apps":[	// 可选
			{
				"app":"需要调用的app名称",
				"data":"app对应的参数"
			},
			...
		]
		"private_data":
		{
			随路数据(私有数据, 目前只支持string类型)
			.
			.
			.
		}
	}
}
  • id: id 由应用服务器自己指定。
  • application: 呼叫成功后自动执行的 application, 目前支持 ai 和 httapi, 建议使用 ai。
  • from: 主叫号码。
  • to: 被叫号码。
  • dial_string: XSwitch 呼叫字符串, 优先级最高, 可选。
  • user_gateway: 可选, 网关 sip 注册到 XSwitch。用这种方式处理被叫难以传递的问题
  • outbound_gateway: 可选, 外呼网关, 平台指定, 默认是 default。
  • url: 呼叫成功后要访问的 URL。
  • apps: 指定呼叫前需要执行的 app 或者需要设置的参数({"app":"set","data":"需要设置的参数"}), 可选
  • private_data: 随路数据, 应用服务器自己指定。需要注意的是, 目前仅支持 string 类型。

特别注意:dial_stringuser_gatewayoutbound_gateway为三选一,根据实际呼叫类型不同,选择不同呼叫方式,比如,呼叫内部分机可使用dial_string,此时无须再增加outbound_gateway的使用。

下面是一个呼叫内线(user)的实际例子,使用dial_string,没有使用outbound_gateway,如下:

POST /v1 HTTP/1.1       //url是/v1
Host: 192.168.2.129:8081 //一般是8081端口
Content-Type: application/json
[HTTP request 1/1]

{
	"jsonrpc": "2.0",
	"method": "ai.dial",
	"id": 1,
	"params":
	{
		"application": "ai",
		"dial_string": "{leg_timeout=30,execute_on_answer='sched_hangup +10'}user/1007",
		"from": "8888",
		"url": "http://192.168.2.117:4567/resp",
		"external_tracking_id":"123453",
		"private_data":
		{
		  "param1": "1",
		  "seq": "2"
		}
	}
}

下面是一个通过用户网关呼叫的实际例子,使用user_gateway,如下:

POST /v1 HTTP/1.1       //url是/v1
Host: 192.168.2.129:8081 //一般是8081端口
Content-Type: application/json
[HTTP request 1/1]
{
	"jsonrpc": "2.0",
	"method": "ai.dial",
	"params": {
		"from": "gaofei", //主叫名称/号码
		"user_gateway": "1007", //用户网关
		"originate_timeout":"10", //呼叫超时时长设置,比如10秒后如果未接听自动挂断
		"max_seconds":"30", //通话最大时长,比如30秒后通话自动挂断
		"to":"1009", //被叫号码
		"application": "ai", //固定模块调用值
		"url":"http://192.168.3.100:4567/gaofei", //回调地址
		"external_tracking_id":"12333" //应用侧ID, 任意字符串, 在应用侧应该唯一
	},
	"id": 7888
}

下面是一个透过 XSwitch gateway 呼叫外线的实际例子,也使用dial_string,如下:

POST /v1 HTTP/1.1       //url是/v1
Host: 192.168.2.129:8081
Content-Type: application/json
[HTTP request 1/1]

{
	"jsonrpc": "2.0",
	"method": "ai.dial",
	"id": 7888,
	"params":
	{
		"application": "ai",
		"dial_string": "sofia/gateway/vos/10000", #XSwitch事先配置好vos网关
		"from": "8888",
		"url": "http://192.168.2.117:4567/resp",
		"external_tracking_id":"123330999", //应用侧ID, 任意字符串, 在应用侧应该唯一
		"private_data":
		{
		  "param1": "1",
		  "seq": "2"
		}
	}
}

或者使用outbound_gateway呼叫外线,效果和上面例子相同,如下所示:

POST /v1 HTTP/1.1       //url是/v1
Host: 192.168.2.129:8081
Content-Type: application/json
[HTTP request 1/1]

{
	"jsonrpc": "2.0",
	"method": "ai.dial",
	"id": 1,
	"params":
	{
		"application": "ai",
		"to": "10000"
		"outbound_gateway": "vos", #XSwitch事先配置好vos网关
		"from": "8888",
		"url": "http://192.168.2.117:4567/resp",
		"external_tracking_id":"1777333",
		"private_data":
		{
		  "param1": "1",
		  "seq": "2"
		}
	}
}

ai.dial2

呼叫双方,并桥接通话。

JSON-RPC 的内容如下:

{
	"jsonrpc": "2.0",
	"method": "ai.dial2",
	"id": 1, //id可以自己指定
	"params":
	{
		"external_tracking_id": "应用侧ID, 任意字符串, 在应用侧应该唯一",
		"from": "主叫, 在回呼中为第一路的被叫",
		"to": "被叫, 在回呼中为第一路的主叫",
		"cid_name": "主叫名称, 可选",
		"cid_number": "主叫号码, 可选, 如有则覆盖to",
		"auto_answer": bool, 可选, 默认为true, 需要话机支持,
		"dial_string": "可选, XSwitch呼叫字符串, 优先级最高, 可选",
		"user_gateway": "可选, 注册上来的网关名称, 优先级次之",
		"outbound_gateway": "可选, 网关, 优先级低",
		"dial_flag":"fifo", "可选, 通话标志, 可根据这个设置,定义每路通话是ai还是fifo",
		"record_session": "可选, 是否录音,如果是,true",
		"early_media": 可选, 默认为false,
		"callback_url": "回调地址",
		"callback_method": "回调方法, 默认为POST",
		"b": { // 第二路参数, 全部可选
			"auto_routing": "可选, 自动路由, 如果为true 就自动进行路由, 自动路由无法跟踪第二路状态",
			"cid_name": "可选, 主叫号码, 默认为from",
			"cid_number": "可选, 被叫号码, 默认为to",
			"auto_answer": "bool, 可选, 默认为false",
			"dial_string": "可选, XSwitch 呼叫字符串",
			"user_gateway": "可选, 注册上来的网关名称",
			"outbound_gateway": "可选, 外呼网关",
			"early_media": true, "可选, 默认为true"
		},
		"private_data": {
			"随路数据, 会在回调中带回", 仅接收字符串键值对, 如:
			"a": "aaaa",
			"b": "bbbb"
		}
	}
}

下面是一个通过gateway呼叫外线再桥接到内线(user)的实际例子如下:

Hypertext Transfer Protocol
POST /v1 HTTP/1.1       //url是/v1
Host: 192.168.2.129:8081 //一般是8081端口
Content-Type: application/json
[HTTP request 1/1]

{
	"jsonrpc": "2.0",
	"method": "ai.dial2",
	"id": 1,
	"params":
	{
		"to":"1001",
		"from": "15666092344",
		"outbound_gateway":"vos1",
		"url": "http://192.168.2.117:4567/resp",
		"private_data":
		{
		  "param1": "1",
		  "seq": "2"
		}
	}
}

结果为:通过网关呼叫15666092344,手机接通后,呼叫账号 1001,1001 接通后,双方正常通话。

先通过gateway呼叫外线再桥接到内线(user)情况下,如果你想修改坐席看到的号码显示(有些时候,不想让坐席人员看到客户信息),请参考如下使用:

Hypertext Transfer Protocol
POST /v1 HTTP/1.1       //url是/v1
Host: 192.168.2.129:8081 //一般是8081端口
Content-Type: application/json
[HTTP request 1/1]

{
	"id": 15,
	"jsonrpc": "2.0",
	"method": "ai.dial2",
	"params": {
		"callback_url": "http://192.168.3.1:8000/seat/dial/callback",
		"from": "1566669999",
		"originate_params":"effective_caller_id_name=156xxxxx999,effective_caller_id_number=156xxxxx999",
		"outbound_gateway":"vos1",
		"private_data": {
			"callType": "2",
			"connectSort": 1,
			"conversationId": "123060001",
			"taskId": "12306"
		},
		"record_session": true,
		"to": "819"
	}
}

下面是先呼通坐席,再通过gateway外呼,且启动录音,然后桥接的例子。具体指令如下:

{
	"jsonrpc":"2.0",
	"method":"ai.dial2",
	"params":
	{
		"early_media":true,
		"dial_flag":"fifo",     //坐席话单标志,通过话单可查询是否为人工坐席话单
		"record_session"true, //坐席是否录音,true为是,false为否
		"from":"1006",
		"to":"1008",
		"dial_string": "{RECORD_BRIDGE_REQ=true}user/1001", //其中,RECORD_BRIDGE_REQ=true,为当前通道桥接成功后才启动录音。
		"b":{
			"outbound_gateway":"vos1"
		},
		"url":"http://192.168.3.45:4567/record",
		"external_tracking_id":"fdfdgegegfgvcxcvs"
	}
}

同理,如果在先呼通坐席再呼叫手机的情况下,想修改坐席看到的号码显示(有些时候,不想让坐席人员看到客户信息),请参考如下:

{
	"id": 16,
	"jsonrpc": "2.0",
	"method": "ai.dial2",
	"params": {
		"callback_url": "http://192.168.3.1:888/seat/dial/callback",
		"from": "1566669999",
		"outbound_gateway":"vos1",
		"b":{
			"cid_name":"15xxxx9999",
			"cid_number":"156xxxx9999",
			"dial_string":"user/888"
		},
		"record_session": true,
		"to":"15xxxx9999"
	}
}

或者


{
	"id": 16,
	"jsonrpc": "2.0",
	"method": "ai.dial2",
	"params": {
		"callback_url": "http://192.168.3.1:888/seat/dial/callback",
		"from": "1130",
		"b":{
			"outbound_gateway":"vos1"
			},
		"dial_string":"{RECORD_BRIDGE_REQ=true,origination_caller_id_name=156xxxx9999,origination_caller_id_number=xxx}user/1130
		"record_session": true,
		"to":"15666669999",
		"external_tracking_id":"123060001"
	}
}

监听

下面是先拨通用户账号再监听指定uuid通话的实际例子,具体如下:

POST /v1 HTTP/1.1       //url是/v1
Host: 192.168.2.129:8081 //一般是8081端口
Content-Type: application/json
[HTTP request 1/1]

{
	"jsonrpc":"2.0",
	"method":"ai.dial",
	"id":1,
	"params":
	{
		"application":"eavesdrop",
		"app_data":"2f80ff31-aac3-4b28-9c8c-b8c7ce20c578",    // 填入需要监听的uuid
		"from":"1008",
		"dial_string":"user/1008",                           //监听者的用户号码
		"url":"http://192.168.3.45:4567/record",
		"external_tracking_id":"fdfdgegegfgvcxcvs"
	}
}

三方通话

将指定uuid进行三方通话,下面例子是针对呼叫内部分机进行三方通话,具体如下:

Hypertext Transfer Protocol
POST /v1 HTTP/1.1       //url是/v1
Host: 192.168.2.129:8081 //一般是8081端口
Content-Type: application/json
[HTTP request 1/1]

{
	"jsonrpc":"2.0",
	"method":"ai.dial",
	"id":1,
	"params":
	{
		"application":"three_way",
		"app_data":"2f80ff31-aac3-4b28-9c8c-b8c7ce20c578",    // 填入uuid
		"from":"1008",
		"dial_string":"user/1008", //1008为注册到本机的内部分机
		"url":"http://192.168.3.45:4567/record",
		"external_tracking_id":"fdfdgegegfgvcxcvs"
	}
}

下面例子是针对呼叫外部服务器分机或手机等进行三方通话,dial_string以及outbound_gateway使用方法参考外呼,具体如下

Hypertext Transfer Protocol
POST /v1 HTTP/1.1       //url是/v1
Host: 192.168.2.129:8081 //一般是8081端口
Content-Type: application/json
[HTTP request 1/1]

{
	"jsonrpc":"2.0",
	"method":"ai.dial",
	"id":1,
	"params":
	{
		"application":"three_way",
		"app_data":"2f80ff31-aac3-4b28-9c8c-b8c7ce20c578",    // 填入uuid
		"from":"1008",
		"outbound_gateway":"2999", // 2999为用户网关
		"url":"http://192.168.3.45:4567/record",
		"external_tracking_id":"fdfdgegegfgvcxcvs"
	}
}

url中收到的回调消息如下:

{
"cause"=>"SUCCESS",
"uuid"=>"eb4db8c2-91da-4cac-863c-4143ede38c51",
"ai.dial"=>"success",
"external_tracking_id"=>"fdfdgegegfgvcxcvs",
"app_data"=>"b973f8a4-3ae2-4194-b5d9-e76cd48f3c3e"
}

转接

通过api接口将指定uuid转接到指定路由。例子,具体如下:

POST /v1 HTTP/1.1       //url是/v1
Host: 192.168.2.129:8081 //一般是8081端口
Content-Type: application/json
[HTTP request 1/1]

{
	"id":"1",
	"jsonrpc":"2.0",
	"method":"ai.api",
	"params":
		{
			"command":"uuid_transfer",
			"params": "0898bb2f-dc57-4b05-af5e-d0bdf774ca4f 1008", //将通话转接到1008上
			"url":"http://localhost:4567/asr"
		}

}

呼叫保持/解除保持

通过api接口将指定uuid处在保持状态。例子,具体如下:

POST /v1 HTTP/1.1       //url是/v1
Host: 192.168.2.129:8081 //一般是8081端口
Content-Type: application/json
[HTTP request 1/1]

{
	"id":"1",
	"jsonrpc":"2.0",
	"method":"ai.api",
	"params":
		{
			"command":"uuid_hold",
			"params": "toggle 0898bb2f-dc57-4b05-af5e-d0bdf774ca4f", //需要保持的账号uuid
			"url":"http://localhost:4567/asr"
		}

}

再次执行上一步指令即可实现解除呼叫保持:具体如下:

POST /v1 HTTP/1.1       //url是/v1
Host: 192.168.2.129:8081 //一般是8081端口
Content-Type: application/json
[HTTP request 1/1]

{
	"id":"1",
	"jsonrpc":"2.0",
	"method":"ai.api",
	"params":
		{
			"command":"uuid_hold",
			"params": "toggle 0898bb2f-dc57-4b05-af5e-d0bdf774ca4f", //相同账号的uuid
			"url":"http://localhost:4567/asr"
		}

}

通过api接口获取指定账号,比如 1006 的通道状态,例子,具体如下:

POST /v1 HTTP/1.1       //url是/v1
Host: 192.168.2.129:8081 //一般是8081端口
Content-Type: application/json
[HTTP request 1/1]

{
	"id":"1",
	"jsonrpc":"2.0",
	"method":"ai.api",
	"params":
		{
			"command":"show",
			"params": "channels 1006", //查询1006账号的uuid
			"url":"http://localhost:4567/asr" //此url为接收查询信息的url,即,查询信息送往此url
		}

}

分机注册状态查询

通过api接口获取分机注册信息,具体如下:

POST /v1 HTTP/1.1       //url是/v1
Host: 192.168.2.129:8081 //一般是8081端口
Content-Type: application/json
[HTTP request 1/1]

{
	"id":"1",
	"jsonrpc":"2.0",
	"method":"ai.api",
	"params":
		{
			"command":"sofia",
			"params": "status profile internal reg", //其中internal为用户注册源,默认为internal,也可根据实际情况自行命名。
			"external_tracking_id":""
			"url":"http://localhost:4567/asr" //此url为接收查询信息的url,即,查询信息送往此url
		}

}

其中回调消息如下:

{
  "result": "\nRegistrations:\n=================================================================================================\nCall-ID:    \t60d36a4572774328b38bc960d8f0cf86\nUser:       \t1001@192.168.3.59\nContact:    \t\"\" <sip:1001@192.168.3.27:55460;ob>\nAgent:      \tMicroSIP/3.19.7\nStatus:     \tRegistered(UDP)(unknown) EXP(2019-07-19 10:18:23) EXPSECS(110)\nPing-Status:\tReachable\nPing-Time:\t0.00\nHost:       \tdebian\nIP:         \t192.168.3.27\nPort:       \t55460\nAuth-User:  \t1001\nAuth-Realm: \t192.168.3.59\nMWI-Account:\t1001@192.168.3.59\n\nTotal items returned: 1\n=================================================================================================\n"
}

通道状态查询

通过api接口获取通话通道状态例子,具体如下:

POST /v1 HTTP/1.1       //url是/v1
Host: 192.168.2.129:8081 //一般是8081端口
Content-Type: application/json

{
	"id":"1",
	"jsonrpc":"2.0",
	"method":"ai.api",
	"params": {
		"command":"show",
		"params": "channels",
		"url":"http://localhost:4567/asr" //此url为接收查询信息的url,即,查询信息送往此url
	}
}

通过api接口获取指定账号,比如 1006 的通道状态,例子,具体如下:

POST /v1 HTTP/1.1       //url是/v1
Host: 192.168.2.129:8081 //一般是8081端口
Content-Type: application/json
[HTTP request 1/1]

{
	"id":"1",
	"jsonrpc":"2.0",
	"method":"ai.api",
	"params":
		{
			"command":"show",
			"params": "channels 1006", //查询1006账号的uuid
			"url":"http://localhost:4567/asr" //此url为接收查询信息的url,即,查询信息送往此url
		}

}

挂机接口

{
  "id": "1",
  "jsonrpc": "2.0",
  "method": "ai.api",
  "params": {
    "command": "uuid_kill",
    "params": "af937190-26ba-4a8a-bed7-1e0417948dbf", //需要挂机的uuid
    "url": "http://localhost:4567/kill"
  }
}

通话查询

通过api接口获取当前通话数及通话状态例子,具体如下:

POST /v1 HTTP/1.1       //url是/v1
Host: 192.168.2.129:8081 //一般是8081端口
Content-Type: application/json
[HTTP request 1/1]

{
	"id":"1",
	"jsonrpc":"2.0",
	"method":"ai.api",
	"params":{
		"command":"show",
		"params": "calls", //所有通话状态信息
		"url":"http://localhost:4567/asr" //此url为接收查询信息的url,即,查询信息送往此url
	}
}

坐席队列信息查询

通过api接口获取系统队列信息,具体如下:

POST /v1 HTTP/1.1       //url是/v1
Host: 192.168.2.129:8081 //一般是8081端口
Content-Type: application/json
[HTTP request 1/1]

{
	"id":"1",
	"jsonrpc":"2.0",
	"method":"ai.api",
	"params": {
		"command":"fifo",
		"params": "list", //所有通话状态信息
		"url":"http://localhost:4567/asr" //此url为接收查询信息的url,即,查询信息送往此url
	}
}

坐席队列状态查询

通过api接口获取系统队列状态信息,具体如下:

POST /v1 HTTP/1.1       //url是/v1
Host: 192.168.2.129:8081 //一般是8081端口
Content-Type: application/json

{
	"id":"1",
	"jsonrpc":"2.0",
	"method":"ai.api",
	"params": {
		"command":"fifo",
		"params": "status", //所有坐席状态信息
		"url":"http://localhost:4567/asr" //此url为接收查询信息的url,即,查询信息送往此url
	}
}

fsapi 调用

所有上述调用 ai.api 的查询接口均可直接使用 fsapi 调用获取。fsapi 和 ai.api 区别在于,调用 fsapi 后可以直接获取信息,无需将获取到的信息传给指定的 url,如下面所示

通道状态查询

通过api接口获取通话通道状态例子,具体如下:

POST /v1 HTTP/1.1       //url是/v1
Host: 192.168.2.129:8081 //一般是8081端口
Content-Type: application/json

{
	"id":"1",
	"jsonrpc":"2.0",
	"method":"fsapi",
	"params": {
		"cmd": "show",
		"arg": "channels as json"
	}
}

补充说明

关于呼入的应答处理

<extension name="ai">
	<condition field="destination_number" expression="^8888$">
		<action application="answer"/>
		<action application="ai" data="{url=http://192.168.2.100/ivr_entry}"/>
	</condition>
</extension>

在上面的例子中, 可以在路由里面执行完 answer 之后再执行 ai, 发 HTTP 请求到应用服务器。 也可以在路由里面不执行 answer, 直接执行 ai 这个 application。 dialplan 这么写:

<extension name="ai">
	<condition field="destination_number" expression="^8888$">
		<action application="ai" data="{url=http://192.168.2.100/ivr_entry}"/>
	</condition>
</extension>

应用服务器回一个这样的 200 OK:

HTTP/1.1 200 OK
Content-Type: application/json

{
	"action":"execute",
	"application", "answer",
	"next":"http://xxx"
}

让应用服务器控制应答, 好处是可以先播放完彩铃再应答, 更加灵活。 当然, 如果不想应答直接拒绝呼叫, 也没问题。

关于 session 结束

应用服务器应在每次收到 mod_ai 的 HTTP POST 消息的时候检查是否有 exiting 字段, 如果有, 那么就说明这个 session 已经结束。

关于 bind

  • <bind strip="#">~\\d+</bind> 输入任意一个数字, 正确。
  • <bind strip="#">~\\d{6}</bind> 输入 6 个数字, 正确, 不需要输入#
  • <bind strip="#">~\\d{6}#</bind> 输入 6 个数字, 再加上#, 正确。

ai.api

这里举个例子:

{
  "jsonrpc": "2.0",
  "method": "ai.api",
  "id": 100,
  "params": {
    "command": "sofia_contact",
    "params": "1001",
    "url": "http://192.168.2.140:4567/xxx",
    "private_data": {
      "aaa": "111",
      "bbb": "2222"
    }
  }
}

mod_ai回复:

{
	.
	.
	.
	"result": "sofia/default/sip:1002@192.168.101.58:5060;fs_nat=yes;fs_path=sip%3A1002%40180.173.70.168%3A1921"
	.
	.
	.
}

关于录音

mod_ai有 2 个action: recordrecord_call, 前者一般称作为留言,是阻塞的。后者可同时录通道发送和接收的语音,是非阻塞的,可用在一对一通话的场景。

record

record 把录音文件保存在/tmp目录下, 等到留言结束时(到达了 limit 或者收到 DTMF)发送 http fileupload 请求,把录音文件发送出去。 目前本地保留了录音文件的副本,在以后的版本里面可能会删除副本。

record_call

record_call 把录音文件保存在$$recordings_dir目录, 一般是/usr/local/freeswitch/recordings

http 方面,record_call 是 2 次提交。一次是 record_call 执行成功后马上发送 http 请求,还有一次是在呼叫结束 reporting 阶段, record_call 发送 http fileupload 请求, 跟 record 一样,目前本地保留了录音文件的副本。

分段录音

分段录音是否开启通过slice-record决定,当asr中设置slice-record=true则开启分段录音,录音存储到原录音文件下的以uuid创建的文件夹里。

回调信息中通过record_file查询分段录音地址。

例子:

{
  "action": "play",
  "next": "http://192.168.2.100/play_ack",
  "file": "say:您好!请说出您要办理的业务",
  "name": "dialed_digits",
  "input_timeout": "5000",
  "digit_timeout": "3000",
  "asr_engine": "aliyun",
  "asr_grammar": "{accent=mandarin,threshold=800,silence-ms=500,no-input-timeout=3000,speech-timeout=30000,slice-record=true}default",
  "variables": {
    "tts_engine": "aliyun",
    "tts_voice": "ting ting"
  }
}

fifo.api

fifo.api 主要实现增加和删除坐席。

JSON-RPC 的内容如下:

{
	"jsonrpc": "2.0",
	"method": "fifo.api",
	"id": 100,   //id可以自己指定
	"params": {
		"action": "add",
		"fifo_name": "fifo队列名称",
		"user": "需要添加的成员",
		"simo": "设置并发数",
		"timeout": "设置超时数",
		"lag": "设置延迟数",
		"url": "http://localhost:4567/xxx",
	}
}

其中 action 为 add 或者 del,若为 del 时,simo,timeout,lag 等值不需要设置,若为 add 时,这些值可以自己设置,也可以不设置采取默认值。
添加或删除后可以去 freeswitch 后台使用 fifo list 命令查看效果。

配置文件

配置文件是 ai.conf.xml, 一般情况下不需要修改。

http 服务例子

Ruby 语言示例:

require 'sinatra'
require 'json'

BASEURL="http://192.168.3.87:4567"

before do
  if (request.content_type && request.content_type.include?("application/json") && (request.content_length.to_i > 1))
    request.body.rewind
    @params = JSON.parse(request.body.read.to_s)
  end
end

get '/' do
  'Hello AI'
end

post '/httapi' do # httapi test
	content_type 'text/xml'

	xml = <<EOF
<document type="xml/freeswitch-httapi">
 <params>
   <someparam>someval</someparam>
 </params>
 <variables>
   <somevar>someval</somevar>
 </variables>
 <work>
  <playback name="test" file="/wav/vacation.wav" xerror-file="/tmp/invalid.wav" digit-timeout="3000" input-timeout="5000">
   <bind strip="#">~[0-9*#]{4}|#</bind>
  </playback>
 </work>
</document>

EOF

	puts xml
	xml
end

post '/httasr' do # httapi test
	content_type 'text/xml'

	xml = <<EOF
<document type="xml/freeswitch-httapi">
 <params>
   <someparam>someval</someparam>
 </params>
 <variables>
   <somevar>someval</somevar>
 </variables>
 <work>
  <playback name="test" file="#{BASEURL}/welcome.wav" xerror-file="/tmp/invalid.wav" asr-engine="baidu" asr-grammar="default">
   <bind strip="#">~[0-9*#]{4}|#</bind>
  </playback>
 </work>
</document>

EOF
end

post '/json' do # httapi test
	content_type 'application/json'

	# puts request.inspect
	puts "---------- request  -------------"
	puts params.to_s

	obj = {:action => "record_call",
		:next => "#{BASEURL}/asr",
		:file => "#{BASEURL}/vacation.wav",
		:name => "record_callName"
	}

	puts "========== response ============="
	puts obj.to_json
	puts

	obj.to_json
 end

post '/asr' do # httapi test
	content_type 'application/json'

	puts "---------- request  -------------"
	puts params.to_s

	obj = {
		:action => "play",
		:file => "say:尊敬的客户,您好,请说一句话",
		:loops => 1,
		:breakable => true,
		:"asr_engine" => "baidu",
		:"asr_grammar" => "{accent=mandarin, barge_in=true,threshold=1000,silence_ms=3000, no_input_timeout=5000,speech_timeout=100000}default",

		:voice=> "0",
		:asr => true,
		:vad_mode => 0,
		:next => "#{BASEURL}/asr_result",
		"variables":
		{
			"tts_engine": "baidu",
			"tts_voice": "baidu"
		},
		:private_data => {
			:data1 => "a",
			:data2 => 2
		}
	}

	puts "========== response ============="
	puts obj.to_json
	puts

	obj.to_json
end

post '/asr_result' do # httapi test
	content_type 'application/json'
	text = nil

	puts "---------- request  -------------"
	puts params.to_s

	if params["asr_result"] && params["asr_result"]["text"]
		text = params["asr_result"]["text"];
	end

	if text == nil || text == ""
		text = "对不起,我没听清您说什么"
	else
		text = "您说的是:" + text
	end

	obj = {
		:action => "play",
		:file => "say:" + text,
		:loops => 1,
		:vadMode => -1,
		:next => "#{BASEURL}/callback",
		"variables":
		{
			"tts_engine": "baidu",
			"tts_voice": "baidu"
		},
		:private_data => {
			:data1 => "a",
			:data2 => 2
		}
	}
	#obj = {:action => "hangup"}

	puts "========== response ============="
	puts obj.to_json

	obj.to_json
end

post '/play' do # 播放指定语音文件及次数
	content_type 'application/json'

	puts "---------- request  -------------"
	puts params.to_s

	obj = {
		:action => "speak",
		:file => "http://xswitch.cn/download/sounds/huawei.wav", //语音文件来源地址
		:play_loops => "5", //播放次数
		:next => "#{BASEURL}/asr_result"
	}

	puts "========== response ============="
	puts obj.to_json
	puts

	obj.to_json
end

post '/tts' do # 使用tts播放文字
	content_type 'application/json'

	puts "---------- request  -------------"
	puts params.to_s

	obj = {
		:action => "speak",
		:text => "烟台小樱桃致力于建设新的基于云计算的下一代互联网通信系统,帮助大型企业构建复杂的内外部音、视频通信系统,帮助中小企业使用最新的通信技术并向新的IP通信技术转型等",
		"tts_engine": "baidu",
		"tts_voice": "baidu"
	}

	puts "========== response ============="
	puts obj.to_json
	puts

	obj.to_json
end

post '/hangup' do # 挂机指令
	content_type 'application/json'

	puts "---------- request  -------------"
	puts params.to_s

	obj = {:action => "hangup"}

	puts "========== response ============="
	puts obj.to_json

	obj.to_json
end


post '/xswitch/event' do # 接收通话事件
	puts params.to_s

	""
end
简单机器人接口协议