顶顶通呼叫中心中间件(mod_cti基于FreeSWITCH)-http api 接口

介绍

顶顶通呼叫中心中间件(mod_cti)实现了一套http api 接口,方便web前端(浏览器直接和mod_cti通讯)直接调用,用来管理和配置系统。后端开发(java、php、c#)等建议直接操作redis,实现系统管理和配置。

基于这个接口开发的管理后台演示地址 http://ddcti.com:88 密码 abc

获取token

为了安全性,建议后端先获取token,然后传递给前端。如果300秒内不使用会自动过期,如果使用这个token调用了任意API,过期时间会延期300秒。

接口

/api?action=GetToken&key=${key}&organization=租户&acl=

  • key 的获取请看 httpcli
  • organization 关联的租户,如果organization为空,就是不使用多租户功能。如果使用了多租户功能,分机名字,线路名字等都会自动添加@${organization} 的后缀,并且只能访问这个租户的下的信息。系统级别的配置通过这个token是没权限访问的。
  • acl 访问控制,就是哪些IP可以使用这个token,不设置,就是任意IP都可以。acl格式,支持fs的acl.conf.xml预定的,也可以 “192.168.1.1/32,192.168.0.0/24” ip/掩码这样的格式。
  • inactivity 最大不活动时间(单位秒),多久不使用这个token就自动过期,默认300秒。
  • expire 最大有效期 (单位秒),超过这个时间 token强制过期,默认0永不强制过期。
  • 响应
{"token":"C797707B-2A70-44cb-BA3D-29EFA6C946C7"}

提示:后端直接调用本接口的API也可以直接用key认证,不用获取token,前段调用,强烈建议用token认证,key认证的方法就是把 token=${token} 改成key=${key}

错误信息

如果请求接口错误,返回json的错误信息,并且http的响应代码不是200。http错误代码 400:请求参数不对, 401:认证失败。

{“error”:"错误信息"}

获取cti模块信息

/api?action=GetCtiState&token=${token}

获取系统函数列表

/api?action=GetAssist&token=${token}&name=api|application

获取分机名列表

/api?action=GetExtenNames&token=${token}

  • action 请求的动作,其他类似动作有以下这些:

    • GetExtenNames 获取分机列表
    • GetLineNames 获取线路列表
    • GetLineGroupNames 获取线路组列表
    • GetQueueDialerTaskNames 获队列外呼任务列表
    • GetScheduleDialerConfigNames 获取定时外呼配置列表
    • GetAcdNames 获取ACD排队组列表
    • GetGatewayNames 获取网关配置列表
    • GetTemplateNames 获取配置模板列表
    • GetDialplanExtensionNames 获取拨号方案列表
    • GetDialplanContextNames 获取路由列表
    • GetSipProfileNames 获取sip配置列表
    • GetConfigFileNames 获取配置文件列表
  • 响应

{
"action": "getextennames",
"result": [
"997",
"122",
"s",
"998",
"124",
"100@顶顶通",
"997@顶顶通",
"123",
"120",
"999",
"121",
"rg"
]
}
  • action 执行的动作
  • result 动作返回的信息,本例子就是分机列表。

获取分机信息

/api?action=GetExtenInfos&token=${token}&name=分机名

  • name 要查询的分机名字

    多个分机名可以用逗号隔开比如name=120,121。也可以用post 方式提交请求的分机名列表,可以同时请求多个分机信息,如果只请求一个也需要用数组的格式。

    ["120","121"]
  • action 请求的动作,其他类似动作有以下这些:

    • GetExtenInfos 获取分机信息
    • GetLineInfos 获取线路信息
    • GetLineGroupInfos 获取线路组信息
    • GetQueueDialerTaskInfos 获取队列外呼信息
    • GetScheduleDialerConfigInfos 获取定时外呼配置信息
    • GetAcdInfos 获取ACD排队信息
    • GetGatewayInfos 获取网关配置信息
    • GetTemplateInfos 获取配置模板信息
    • GetDialplanExtensionInfos 获取拨号方案信息
    • GetDialplanContextInfos 获取拨路由信息
    • GetSipProfileInfos 获取sip配置信息
    • GetConfigFileInfos 获取配置文件信息
  • 响应

{
"action": "getexteninfos",
"result": {
"120": {
"param": {
"allow-empty-password": false,
"password": "",
"auth-acl": ""
},
"variable": {
"sip-force-contact": "NDLB-connectile-dysfunction-2.0",
"sip-force-expires": "60",
"sip-expires-max-deviation": "10",
"sip-force-expires-min": "50",
"sip-force-expires-max": "600"
},
"description": "",
"attribute": {
"cidr": "",
"cacheable": ""
}
},
"121": {
"param": {
},
"variable": {

},
"attribute": {
},
"description": ""
}
}
}
  • action 执行的动作

  • result 动作返回的信息,本例子就是分机信息。返回的格式是 “名字”:”json对象的信息”。

设置分机信息

/api?token=${token}&action=setexteninfo&name=分机名

  • name 要设置的分机名字

  • post 内容是分机信息的JSON格式数据。

{
"param": {
"allow-empty-password": false,
"password": "",
"auth-acl": ""
},
"variable": {
"sip-force-contact": "NDLB-connectile-dysfunction-2.0",
},
"description": "",
"attribute": {
"cidr": "",
"cacheable": ""
}
}
  • action 请求的动作,其他类似动作有以下这些:

    • SetExtenInfo 设置分机信息
    • SetLineInfo 设置线路信息
    • SetLineGroupInfo 设置线路组信息
    • SetQueueDialerTaskInfo 设置队列外呼信息
    • SetScheduleDialerConfigInfo 设置定时外呼配置信息
    • SetAcdInfo 设置ACD排队信息
    • SetGatewayInfo 设置网关配置信息
    • SetTemplateInfo 设置配置模板信息
    • SetDialplanExtensionInfo 设置拨号方案信息
    • SetDialplanContextInfo 设置路由信息
    • SetSipProfileInfo 设置sip配置信息
    • SetConfigFileInfo 设置配置文件信息
  • 响应

{
"action": "setexteninfo",
"message": "update succeed"
}
  • action 执行的命令
  • message 如果分机之前已经存在,是更新信息 message是 “update succeed”,如果之前不存在是增加分级 message是”added succeed”。

删除分机

/api?action=DelExtens&token=${token}&name=分机名

  • name 要查询的分机名字

    多个分机名可以用逗号隔开比如name=120,121。也可以用post 方式提交删除的分机名列表,可以同时删除多个分机,如果只删除一个也需要用数组的格式。

    ["120","121"]
  • action 请求的动作,其他类似动作有以下这些:

    • DelExtens 删除分机
    • DelLines 删除线路
    • DelLineGroups 删除线路组
    • DelQueueDialerTasks 删除列外呼任务
    • DelScheduleDialerConfigs 删除定时外呼配置
    • DelAcds 删除ACD排队组
    • DelGateways 删除网关配置
    • DelTemplates 删除配置模板
    • DelDialplanExtensions 删除拨号方案
    • DelDialplanContexts 删除路由
    • DelSipProfiles 删除sip配置
    • DelConfigFiles 删除配置文件
  • 响应

{
"action": "DelExtens",
"message": "del succeed",
"result": 1
}
  • action 执行的动作
  • result 成功删除的个数。

获取线路状态

/api?token=${token}&action=LineState&name=分机名

  • name 要查询的线路名

  • action 请求的动作,其他类似动作有以下这些:

    • ExtenState 查询分机注册状态
    • LineState 查询线路状态
    • LineGroupState 查询线路组状态
    • AcdState 查询排队状态
    • QueueDialerState 查询队列外呼状态
    • ScheduleDialerState 查询定时外呼状态
    • QcState 查询质检状态
    • GatewayState 查询网关注册状态
    • SipProfileState 查询sip状态
    • SipStatus 查询所有sip信息
  • 响应 内容是 线路配置信息和线路状态信息的集合

    {
    "params": {
    "count": 2,
    "use": 0,
    "rest": 0,
    "fault_threshold": 0,
    "fault_try_interval": 0,
    "fault_count": 0,
    "fault_condition": {
    "connect_time": 0
    },
    "available": false,
    "dialstring": "user/120",
    "cluster_dialstring": "",
    "active_time": "-23.89",
    "areacode": [],
    "type": "user",
    "device": "120"
    },
    "variables": {
    "originate_timeout": "60",
    "employee_id": "120",
    "cti_line_name": "120"
    },
    "owner_groups": {
    "2000": {
    "priority": 0
    }
    }
    }
  • owner_groups 加入的线路组
  • priority 在线路组的优先级

声音文件管理

storage 有 redis和db两种类型,建议使用db存储。

存储在redis的文件 细节可以看 放音文件说明 ,上传会自动从redis同步到硬盘,存储目录为 cti.json配置 sounds_dir 指定。

存储在db的文件 通过放音路径 db://文件路径/文件名 的方式访问。放音的时候会缓存到 fs的storage_dir/db_file_cache 目录。

列出声音文件

/api?token=${token}&action=ListAudiofilenames&dir=目录&type=类型&storage=存储
也支持post发送请求参数

{
"dir":"",
"type":"",
"storage":"db"
}

  • dir 如果不设置就是列出根目录
  • type 列出文件还是文件夹,不设置就是全部
    • file 文件
    • folder 文件夹
  • storage

    • redis 从redis读取数据
    • db 从数据库读取数据
  • 响应

    [
    {
    "name": "2.wav",
    "type": "file",
    "filemtime": "2023-08-27T15:33:20",
    "filesize": "97324",
    "description": ""
    },
    {
    "name": "a2",
    "type": "folder",
    "filemtime": "2023-08-27T15:39:40",
    "filesize": "",
    "description": ""
    }
    ]
  • name 名字

  • filemtime 修改时间

  • filesize 文件大小(type:folder 是目录的意思,没有大小)

  • description 描述

创建声音文件目录

/api?token=${token}&action=MakeAudiofileDir&dir=父目录&storage=db&description=描述

上传声音文件

  1. 表单方式上传 Content-Type 为 multipart/form-data

    /api?token=${token}&action=UploadAudiofile&dir=父目录&storage=db&description=描述

    post内容为表格格式的文件数据

  2. 二进制上传 Content-Type 为 application/binary

    /api?token=${token}&action=UploadAudiofile&dir=父目录&storage=db&description=描述&filename=文件名
    post内容为二进制的文件内容

下载声音文件

/api?token=${token}&action=DownloadAudiofile&dir=父目录&name=文件名&storage=db

删除声音文件

/api?token=${token}&action=DeleteAudiofile&dir=父目录&name=文件名&storage=db&type=file|folder

改名声音文件,(storage=redis,不支持改名操作)

/api?token=${token}&action=RenamedAudiofile&dir=父目录&name=文件名&newdir=新目录&newname=新名字&storage=db&type=file|folder

修改备注

/api?token=${token}&action=ChangeDescriptionAudiofile&dir=父目录&name=文件或者目录名&storage=db&description=描述

列出redis全部文件

/api?token=${token}&action=ListAllAudiofilenamesFromRedis

列出db全部文件

/api?token=${token}&action=ListAllAudiofilenamesFromDB

CDR查询接口

获取CDR字段

/api?token=${token}&action=listCdrField

响应

{
"field": "uuid",
"type":"number|string"
}

  • type
    • numbr 对应 int,double,float
    • string 对应 char,varchar
    • datatime 支持的比较方法和number一样

查询CDR记录

/api?token=${token}&action=listCdrRecord
post查询条件

{
"begin_time": "2000-1-1 00:00:00",
"end_time": "2030-1-1 00:00:00",
"mode": 0,
"field": ["uuid", "destination_number"],
"offset": 0,
"limit": 100,
"order_field": "created_time",
"order_method": "desc",
"filter_mode": "and",
"filter_condition": [{
"field": "destination_number",
"method": "=",
"value": "abc"
},
{
"field": "duration",
"method": ">",
"value": 60
}
]
}
  • mode 查询模式
    1. 查询单条记录
    2. 按主叫查询(主叫记录和查询条件匹配就输出记录)
    3. 按被叫查询(被叫记录和查询条件匹配就输出记录)
    4. 按主叫或被叫查询(主叫或被叫和查询条件匹配就输出记录)
    5. 按被叫和被叫查询(主叫和被叫都和查询条件匹配就输出记录)
  • field 要显示的字段,不设置就显示所有字段
  • offset 分页开始记录
  • limit 最大返回多少套记录
  • order_field 排序字段
  • order_method 排序方法
    • asc 顺序
    • desc 逆序
  • filter_mode 多个filter_condition条件的组合方式。
    • and 所有条件满足
    • or 任意一个条件满足
    • xor 所有条件都满足或者所有条件都不满足
  • filter_condition 条件,就是sql的where
    • field 要匹配的数据库字段
    • value 比较的值,如果字段是int或者浮点型,value要用number类型,日期时间和字符串,都是字符串类型。
    • method 比较方法
      • = 等于number和string都支持
      • != 不等于number和string都支持
      • >= 只能用于number
      • <= 只能用于number
      • contain 只能用于string,filed的包含value
      • prefix 只能用于string,filed的前缀等于value
      • suffix 只能用于string,filed的后缀等于value
      • regexp 只能用于string,filed和value符合正则表达式,注意不是perl正则语法是POSIX标准,不支持\d,改用[0-9]或者[:digit:],
        • [:alpha:] 代表任意英文大小写字符 a-z A-Z
        • \w – [:alnum:] 代表所有的大小写英文字符和数字0-9 A-Z a-z
        • \W – ^[:alnum:]
        • \s – [:space:]
        • \S – ^[:space:]
      • null 为空 number和string都支持
      • notnull 不为空 number和string都支持

返回

{
"message": "succeed",
"action": "ListCdrRecord",
"result": [
{
"uuid": "cf121f93-d510-460a-b995-b70463f777fa",
"channel": "sofia/internal/121@192.168.31.57",
//mode 1或者2的时候_brother是另外一段的记录
"_brother":{
"uuid":"",
"channel":""
}
},
{
"uuid": "3e6dbf04-caa1-4a1a-8ff1-dcf8d113253b",
"channel": "sofia/internal/121@192.168.31.57"
}
],
"count": 592
}

  • result CDR记录
  • count 符合条件总的记录数,用于分页。

保存CDR查询条件记录

/api?token=${token}&action=SetCdrHistory&name=记录名字

获取CDR查询条件记录

/api?token=${token}&action=GetCdrHistory

下载通话录音文件

/api?token=${token}&action=DownloadRecordfile&name=cdr查询到的uuid

导入号码到外呼队列

/api?token=${token}&action=ImportQueueDialerNumber&name=队列名字

post内容 要导入的号码数组。

[
{
"params": {
"number":"10001"
},
"variables": {
"numberid": "1"
}
},
{
"params": {
"number":"10002"
},
"variables": {
"numberid": "2"
}
},
"1003"
]

响应

{
"action": "ImportQueueDialerNumber",
"message": "succeed",
"result": 1
}
  • result 导入成功的号码总数

终端监控

需要ws连接才支持
/api?token=${token}&action=FsTerminal&inactivity =600

inactivity ws连接不活动时间,就是多久不执行命令连接就断开,默认600秒。

打开日志

发送

{
"cmd":"log_subscribe",
"level":"debug",
"filter":"",
"expire":"0",
"inactivity":"0",
}
  • level
    • “DISABLE”,
    • “CONSOLE”,
    • “ALERT”,
    • “CRIT”,
    • “ERR”,
    • “WARNING”,
    • “NOTICE”,
    • “INFO”,
    • “DEBUG”,
  • filter 过滤关键词,就是日志中包含了指定关键词才输出
  • expire 订阅日志最大时间,时间到了自动停止日志,单位秒

关闭日志

发送

{
"cmd":"log_unsubscribe"
}

执行FreeSWITCH 命令

发送

{
"cmd":"fs_api",
"commandline":"FreeSWITCH 命令"
}

常用FreeSWITCH命令

发起呼叫

/api?token=${token}&action=call

例子1. 点击拨号

{
"dial": {
"line": "line/121",
"number": "121",
"variables": ["origination_caller_id_number=999","ignore_early_media=true"]
},
"bridge": {
"line": "line/default",
"number": "5555",
"variables": ["a=b"]
},
"notifyurl":"http://127.0.0.1?customparam=myparam"
}

例子2. 语音通知

{
"dial": {
"line": "line/121",
"number": "121",
"variables": ["origination_caller_id_number=999","ignore_early_media=true"]
},
"actions": ["playback:放音文件.wav"],
"notifyurl":"http://127.0.0.1?customparam=myparam"
}

例子3. 语音通知,开启录音和限制最大通话时间

{
"dial": {
"line": "line/121", //外呼使用的线路
"number": "121", //被叫号码
"variables": [
"origination_caller_id_number=999", //主叫号码
"ignore_early_media=true",//忽略早期媒体
"origination_nested_vars=true",//支持变量嵌套
"execute_on_answer_record='record_session $${recordings_dir}/${strftime(%Y-%m-%d)}/${uuid}.wav'",//设置录音文件名
"execute_on_answer_limit_time='sched_hangup +60 ALLOTTED_TIMEOUT'" //设置最大通话时间
]
},
"actions": ["playback:放音文件.wav"], //设置放音文件
"notifyurl":"http://127.0.0.1?customparam=myparam" //设置接通后回掉地址
}

例子4. 呼叫分机接通后执行拨号方案

{
"dial": {
"line": "line/121",
"number": "121",
"variables": ["origination_caller_id_number=999", "ignore_early_media=true"]
},
"actions": ["transfer:5555 XML internal"] //接通后 执行到路由 internal,路由条件(destination_number)设置为5555
}
  • dial
    • line 发起呼叫使用的线路或者线路中 line/线路名 linegroup/线路组名
    • number 被叫号码
    • variables 变量列表
  • bridge 用于先呼叫一个号码A,A接通后呼叫号码B。
  • actions 电话接通后要执行的命令列表,比如放音。如果设置了bridge,actions会被忽略。
    • 动作名字:动作参数,多个参数空格隔开,如果参数中包含逗号,可以用 动作名:^^|参数1|参数2 这样的方法转义。
  • notifyurl 呼叫进度通知回掉
    • 呼叫失败,失败原因的含义看CDR文档,呼叫失败没办法返回UUID,可以通过设置变量 origination_uuid=自定义UUID,来自己定义UUID,然后把UUID放到回掉URL的参数里面来解决呼叫失败通知无UUID。
    • {"call":"failed","cause":"呼叫失败原因"}
    • 收到183
      {"call":"early","uuid":"4c613373-aeda-484a-8ce3-11f1ed6e8b96"}
    • 接通
      {"call":"answer","uuid":"6ab1403d-de06-4ac3-8a9c-6e5960c0aa6b"}
    • bridge端收到183
      {"call":"bridge","uuid":"6ab1403d-de06-4ac3-8a9c-6e5960c0aa6b","peer_uuid":"cb089768-a192-40af-89af-8d9c37b75344"}
      • uuid dial端的UUID
      • peer_uuid bridge端的UUID
    • bridge端接通
      {"call":"peer_anser","uuid":"6ab1403d-de06-4ac3-8a9c-6e5960c0aa6b","peer_uuid":"cb089768-a192-40af-89af-8d9c37b75344"}
    • 挂断
      {"call":"hangup","uuid":"6ab1403d-de06-4ac3-8a9c-6e5960c0aa6b","hangup_cause":"NORMAL_CLEARING","peer_hangup_code":"200","hangup_code":"","time":{"created":1695717657898439,"progress":1695717658416816,"progress_media":1695717657996699,"answered":1695717666178472,"hungup":1695717675921398}}
      • hangup_code dial端的挂机代码,如果挂机代码为空,就是fs主动发起的挂机
      • peer_hangup_code bridge端的挂机代码,如果挂机代码为空,就是fs主动发起的挂机
  • 常用变量说明
    • origination_caller_id_number 设置主叫号码
    • origination_uuid 如果需要使用自定义的呼叫UUID,通过这个参数传给FS。
    • record_filename 写入CDR表的录音路径
    • execute_on_answer=’record_session $${recordings_dir}/20210303/${uuid}.wav’ 接通后开始录音,$${recordings_dir}/日期目录/${uuid}.wav 这个是录音文件路径
      • $${recordings_dir} vars.xml 配置的录音文件路径 默认是 fs安装目录 recording
      • ${caller_id_number} 来电号码
      • ${uuid} 通话callid
      • ${strftime(%Y-%m-%d)} 日期
    • ignore_early_media=true 忽略早期媒体,如果坐席使用手机接听,需要这个参数,也就是先呼叫的号码,如果是手机,需要加上这个参数,如果需要录制早期媒体,需要改成 ignore_early_media=consume。
    • absolute_codec_string=g729 指定g729编码,如果需要同时指定多个编码可以设置为 ^^:G729:PCMA:PCMU ^^:意思是把逗号用:代替,类似转义符。
    • originate_timeout 呼叫超时(比如分机30秒不接听就停止呼叫),需要和ignore_early_media=true 一起使用。
    • instant_ringback=true 安装自定义彩铃
    • transfer_ringback=$${cn-ring} 设置自定义彩铃声音,可以指定具体的文件名,$${cn-ring}使用嘟嘟声音,如果不和ignore_early_media=true一起使用,线路响应了183,就会播放线路的提示音。
    • origination_nested_vars=true 允许origiante参数不解析变量,变量格式\${变量名}。
    • export_vars 把变量传递到后续桥接的通道,比如export_vars=‘record_filename,其他变量名’ ,如果只需要设置变量到后续桥接的通道,用前缀nolocal:例子:{nolocal:sip_h_X-AutoAccept=true,export_vars=’nolocal:sip_h_X-AutoAccept’}
    • sip_h_* 添加自定义sip头到 INVITE ;如果值里面又逗号用\转义,例子 sip_h_X-My-Header=one\,two\,three。
    • sip_rh_* 添加自定义sip头到 200
    • sip_ph_* 添加自定义sip头到 180 183
    • sip_bye_h_* 添加自定义sip头到 bye

获取帮助信息

/api?token=${token}&action=GetAssist

获取系统信息

/api?token=${token}&action=SystemStatus

响应

{
"recordings_dir": "D:/src/utility/freeswitch/x64/Debug/recordings",
"sounds_dir": "D:/src/utility/freeswitch/x64/Debug/sounds",
"sessions_total": 0,
"sessions_count": 0,
"sessions_peak": 0,
"sessions_peak_fivemin": 0,
"sessions_limit": 10000,
"sps": 0,
"sps_limit": 1000,
"sps_speak": 0,
"sps_peak_fivemin": 0,
"loadavg": 0,
"idle_cpu": 82.25260416666667,
"uptime": 169382,
"total_memroy": 34211385344,
"avail_memory": 15465250816
}

  • recordings_dir 录音路径,默认录音文件路径
  • sounds_dir 放音路径,默认放音文件路径
  • sessions_total 累计呼叫,启动以来总的呼叫
  • sessions_count 当前并发,当前总呼叫
  • sessions_peak 并发峰值,最高呼叫并发
  • sessions_peak_fivemin 最近并发峰值, 最近5分钟最高呼叫并发
  • sessions_limit 最大并发,最大允许的呼叫并发
  • sps 当前CPS,最后1秒呼叫数
  • sps_speak CPS峰值, 最高每秒呼叫数
  • sps_peak_fivemin 最近CPS峰值,最近5分钟 最高每秒呼叫数
  • sps_limit 最大CPS,最大允许每秒呼叫数
  • uptime 运行时间,系统运行时间(秒)
  • loadavg 负载,系统最近1分钟负载
  • idle_cpu 空闲CPU
  • total_memroy 系统总内存
  • avail_memory 系统可用内存

列出所有API

/api?token=${token}&action=GetAssist&name=api

列出所有APP

/api?token=${token}&action=GetAssist&name=application

获取cti配置

/api?token=${token}&action=GetCtiConfig

返回cti.json配置,会自动去了注释。

设置cti配置

/api?token=${token}&action=SetCtiConfig

post内容就是cti.json的内容。

设置cti.json配置,配置会自动保存到fs安装目录conf/cti.json。

导出redis配置备份

/api?token=${token}&action=ExportRedisConfig&audiofile=no&sip=yes

可以通过传入参数控制导出那些配置 yes导出no不导出,没设置的参数,默认no。

  • audiofile 声音文件

  • sip sip配置

  • exten 分机配置

  • gateway 网关配置

  • dialplan 拨号方案和路由

  • line 线路和线路组

  • acd 排队

  • queuedialer 队列外呼

  • scheduledialer 定时外呼

  • robotflow 话术流程

  • callqc 质检配置

  • configfile 配置文件

  • template 模板文件

导入之前备份的配置

/api?token=${token}&action=ImportRedisConfig&reset=false&audiofile=no&sip=yes

可以通过传入参数控制导出那些配置 yes导出no不导出,没设置的参数,默认no。

  • reset 导入配置的时候是否先清空之前的配置项目。yes 先清空在写入,no 覆盖同名和插入新的项目。