http connector
一个http connector主要由connectorType和action两部分组成,其中connectorType主要是对第三方网站的认证,并返回相应的认证信息,action主要是拿着认证信息对第三方网站做相应的操作。

1. connectorType
connectorType主要有5部分组成,分别是Name,Base,Description,Parameters,Script
1.1 Name
顾名思义,就是你给当前connectorType取个响亮名字,原则上看到名字,就知道这个connectorType是对应的那个网站
1.2 Base
既然是http connector,选择http就好了
1.3 Description
connectorType的描述部分
1.4 Parameters
action那里也有Parameters,原理,用法和connectorType一样的,所以后续就不做赘述了

1.4.1 Name
Name是在当前层级的Parameters下唯一的,原则上用小驼峰形式
1.4.2 Label
Label是当前输入的展示名称,由于目前没有国际化,原则面对的客户是什么语言,就用什么语言
1.4.3 Description
Description对当前输入的描述
1.4.4 Data Type
输入的数据类型,当前支持,string(字符串),number(数值),bool(true/false),file(文件链接),date_time('2024-01-23T08:07:10.398Z'),date('2024-01-23'),object(kv类型),array(数组),如果dataType是数组类型,需要填写of,of的是object,则需要编辑object的kv,如果dataType是object,需要编辑object的kv
1.4.5 Control Type
输入的展示类型,当前支持,text,text_area,password,number,checkbox,select,multiselect,date,date_time
1.4.6 Pick list
一般用于select和multiselect,写groovy用于返回一个数组,groovy中由一个输入$parameters(代表的是当前所有inputParameters的value),原则上文档里描述了该inputField有那些输入值,都返回一个数组,而不应该让使用者去填写,写了两个展示效果
()->"POST"==$parameters.b?["POST"]:["PUT","DELETE","GET"]

包含了label和value,使用更友好
()->[["label":"增加","value":"POST"],["label":"更新","value":"PUT"]]

1.4.7 Pick list dependency
表示当前的Pick list的发生更依赖于其他的哪个inputField.name
1.4.8 Visible
表示当前inputField是否展示,写groovy用于返回一个true/false,groovy中由一个输入$parameters(代表的是当前所有inputParameters的value),写了个展示效果
()->"POST"==$parameters.b

1.4.9 Visible dependency
表示当前的Visible的发生更依赖于其他的哪个inputField.name
1.4.10 Optional
表示当前inputField是否必填,默认是false必填,写groovy用于返回一个true/false,groovy中由一个输入$parameters(代表的是当前所有inputParameters的value),写了个展示效果
()->"POST"==$parameters.b

1.4.11 Optional dependency
表示当前的Optional的发生更依赖于其他的哪个inputField.name
1.5 Script
主要是写groovy,该脚本主要有两个输入参数inputs(map结构,就是当前connectorType填写的值)和instanceJson(string,就是后面存储的instanceJsonToDb这个字符串),构建一个connectorInstance,最终return一个map,有如下key
baseUrl string action需要访问的域名
headers map 加在ation的请求头里
queryParam map 加在ation的请求路径
proxyHost string
proxyPort int proxyPort和proxyHost一起使用,有时候要访问一些网站要使用代理
ignoreSSL bool 默认false,是否忽略https证书
cache bool是否缓存
expires long 缓存时间
timeUnit string时间单位(DAYS,HOURS,MINUTES,SECONDS,MILLISECONDS,MICROSECONDS,NANOSECONDS) cache,expires,timeUnit string三者都是合法的值,才做缓存
refresh bool 是否支持刷新token
instanceJsonToDb string 存一些关键信息到db,当connector实例的数据发生改变时,刷新了instanceJsonToDb,此时就会为空
whenActionFailExeRefreshConditionScript bool 默认是true,是否只有action执行失败时才执行refreshConditionScript这段groovy检测是否刷新Refresh
refreshConditionScript string 检测是否刷新connectorInstance,此groovy需要返回一个true或者false,此groovy有一个如输入值actionResult(执行action的结果)实例代码
if (null != actionResult && actionResult.get("statusCode") == 401 && actionResult.containsKey("body") != null) {
def body = actionResult.get("body")
Map bodyBean = JSONUtil.toBean(body, Map.class)
if ("INVALID_TOKEN".equals(bodyBean.get("code")))
return true
}
return false
map的实例
return ["baseUrl": bean.api_domain, "headers": ["Authorization": "Zoho-oauthtoken " + bean.access_token, "Content-Type": "application/json"], cache: true, "expires": (bean.expires_in-600), "timeUnit": "SECONDS","refresh":true,"refreshConditionScript":refreshConditionScript]
但是在根据paramters中关键信息获取认证信息的时候,很多时候要做http请求才能得到认证信息,所以要先做http请求,再组装返回的map
要做一个http请求的一般格式 connector.http{xxx}是个固定写法
def connectorResult =connector.http{
headers map请求头
path string请求路径
queryParams map路径参数
body object请求体
method string 请求方式 "POST,GET,PUT,DELETE等"
contentType string(rest就是raw-json默认这个,form-data,x-www-form-urlencoded,参照postman)
proxyHost string
proxyPort int proxyPort和proxyHost一起使用,有时候要访问一些网站要使用代理
ignoreSSL bool 默认false,是否忽略https证书
baseUrl string 当前http请求的域名
}
connectorResult 返回值有
status int http状态码
body string http请求返回body
headers map返回的请求headers
msg 如果失败,失败信息
给个比较完整且复杂的案例
import cn.hutool.json.JSONUtil
import com.bot.common.exception.CustomException
import cn.hutool.core.util.NumberUtil
String refreshConditionScript = "import cn.hutool.json.JSONUtil\n" +
" if (null != actionResult && actionResult.get(\"statusCode\") == 401 && actionResult.containsKey(\"body\") != null) {\n" +
" def body = actionResult.get(\"body\")\n" +
" Map bodyBean = JSONUtil.toBean(body, Map.class)\n" +
" if (\"INVALID_TOKEN\".equals(bodyBean.get(\"code\")))\n" +
" return true\n" +
" }\n" +
" return false";
if (null != instanceJson && JSONUtil.isTypeJSONObject(instanceJson)) {
def map = JSONUtil.toBean(instanceJson, Map.class)
def refreshToken = map.get("refresh_token")
def connectorResult = connector.http {
method "POST"
baseUrl inputs.host
path "/oauth/v2/token?refresh_token=" + refreshToken + "&client_id=" + inputs.client_id + "&client_secret=" + inputs.client_secret + "&grant_type=refresh_token"
}
if(connectorResult.status==200){
def bean = JSONUtil.toBean(connectorResult.body, Map.class)
if(null!=bean.access_token&&bean.access_token.trim().length()>0){
return ["baseUrl": bean.api_domain, "headers": ["Authorization": "Zoho-oauthtoken " + bean.access_token, "Content-Type": "application/json"], cache: true, "expires": (bean.expires_in-600), "timeUnit": "SECONDS","refresh":true,"refreshConditionScript":refreshConditionScript]
}else{
throw new CustomException(JSONUtil.toJsonStr(connectorResult))
}
}else{
throw new CustomException(JSONUtil.toJsonStr(connectorResult))
}
}else{
def connectorResult =connector.http{
headers (["Content-Type":"application/x-www-form-urlencoded"])
method "POST"
contentType "x-www-form-urlencoded"
path "/oauth/v2/token"
baseUrl inputs.host
body (["grant_type":inputs.grant_type, "client_id":inputs.client_id, "client_secret":inputs.client_secret, "redirect_uri":inputs.redirect_uri, "code":inputs.code])
}
if(connectorResult.status==200){
def bean = JSONUtil.toBean(connectorResult.body, Map.class)
if(null!=bean.access_token&&bean.access_token.trim().length()>0){
return ["baseUrl": bean.api_domain, "headers": ["Authorization": "Zoho-oauthtoken " + bean.access_token, "Content-Type": "application/json"], cache: true, "expires": (bean.expires_in-600), "timeUnit": "SECONDS","refresh":true,"instanceJsonToDb":connectorResult.body,"refreshConditionScript":refreshConditionScript]
}else{
throw new CustomException(JSONUtil.toJsonStr(connectorResult))
}
}else{
throw new CustomException(JSONUtil.toJsonStr(connectorResult))
}
}
2. Action
action 就是对网站操作的一种描述,主要Name,Connector-Type,Input Parameters,Output Parameters,Action Script几部分组成

2.1 Name
Action的名字,一般是操作的资源,原则上通过名字,就可以让人知道做什么操作,操作啥
2.2 Description
对操作的进一步描述
2.3 Input Parameters
用法和connectorType的 Input Parameters一致
2.4 Output Parameters
输出的描述
2.5 Action Script
主要是写groovy,输入参数有inputs(Input Parameters的输入值)和connectorInstance(conectorType.inputParameters的输入值)
在action里做一个http请求的一般格式 context.http{xxx}是个固定写法
def actionResult=context.http{
headers map 请求头
path string请求路径
method string请求方法,取值范围GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE,都是大写的
queryParams map 路径参数
body object 请求体
contentType string(rest就是raw-json默认这个,form-data,x-www-form-urlencoded,参照postman)
returnType 返回类型,可以不申明(json,xml,file,html)
resultScriptCode 主要用于对返回结果处理脚本,目前支持html类型的,该脚本的输入参数为body(string 类型),就是请求成功后返回body
}
actionResult 返回的信息有,
body(string)请求返回的内容
statusCode(int)http状态码
headers(map)response返回的headers
data(object)根据returnType返回的结果,json,xml和file返回的是对象,html根据resultScriptCode脚本
success(bool) 是否成功
message(string)一般是请求失败的信息
你可以根据statusCode,message,success等信息设置是否actionResult.refresh=true/false,在connectorType中构建connectorInstance是就有refresh,whenActionFailExeRefreshConditionScript,refreshConditionScript这三个关于是否刷新connectorInstance的参数,整体逻辑如下
首先connectorInstance.refresh==true
如果actionResult.refresh==true,可以进行刷新
如果connectorInstance.whenActionFailExeRefreshConditionScript==true,actionResult.success==false,则根据执行refreshConditionScript脚本根据返回结果是否为true来决定是否,进行刷新
如果connectorInstance.whenActionFailExeRefreshConditionScript==false,则根据执行refreshConditionScript脚本根据返回结果是否为true来决定是否,进行刷新,为了更好的性能whenActionFailExeRefreshConditionScript尽量设置成true。
示例代码
示例一 针对某个网站,你不想对每个restapi写action,可以写一个类似的通用的http action
import cn.hutool.json.JSONUtil
def strBody=inputs.body;
def objBody = [:]
if (null != strBody) {
if (JSONUtil.isTypeJSONObject(strBody)) {
objBody = JSONUtil.toBean(strBody, Map.class);
} else if (JSONUtil.isTypeJSONObject(strBody)) {
objBody = JSONUtil.toList(strBody, Map.class);
} else {
objBody = strBody
}
}
return context.http{
path inputs.path
method inputs.method
body (objBody)
}
示例二 针对一些分页的api,在数据量可控的情况下,可以在action的内部一次性全部取出,不需要再flow中循环了
import java.util.stream.Collectors
def page_size=inputs.pageSize
if (page_size==null){
page_size=10
}else{
page_size=Integer.valueOf(page_size)
}
def hasMore=true
def userList=new ArrayList()
def queryParams = ["user_id_type": inputs.userIdType, "department_id_type": inputs.departmentIdType,"department_id": inputs.departmentId,"page_size":page_size,"page_token":inputs.pageToken]
while(hasMore){
def queryUrl= queryParams.entrySet().stream().
filter(entry -> null != entry.getValue()).
map(entry -> entry.getKey() + "=" + entry.getValue()).
collect(Collectors.joining("&"))
def page=context.http{
path "open-apis/contact/v3/users/find_by_department"+"?"+queryUrl
method "GET"
}
if(page.statusCode==200){
hasMore=page.data.data.has_more
queryParams.put("page_token",page.data.data.page_token)
if(page.data.data.items!=null){
userList.addAll(page.data.data.items)
}
}else{
throw new RuntimeException(page.data.message)
}
}
return ["userList":userList]
示例三 类似的这种显示设置refresh=true
def actionResult=context.http{
method "DELETE"
path "delete/user/"+inputs.id
}
if(actionResult.stastusCode=401){
actionResult.refresh=true
}
return actionResult
上面是怎么新建connectorType和action,根据规则,我们建了相应的connectorType和action,再展示一下使用
第一步:新建connector类型的pbc,如果已有connector类型的pbc,可以忽略当前步骤
第二步:在connector类型的pbc,新建connector,选择你需要connectorType
第三步:在flow中使用action和connector
在应用中选择相应的action,并填写相应的参数
