开放平台函数化
Overview
我们即将上线FXD-28271插件函数化任务,该任务的核心改动是将 基于事件自动触发执行代码并提供事件对应的上下文变量 调整为 插件提供函数,用户配置调用方式和参数绑定关系。
通过上述改动, 为您带来以下好处:
- 此前如果您的插件在多个功能场景下的行为类似或一致,您不再需要拷贝针对不同触发事件的多份配置模板和代码,而可以合并为一个函数并提供给用户。
- 您不再需要关注插件代码触发执行的功能场景,只要您能够妥善处理不同类型的参数,您的代码就可以在所有简道云支持插件的功能场景中复用。
如果您是新的开发者,您现在就可以阅读GetStarted章节并开始开发第一个插件。
如果您此前已经是一名插件开发者并创建了插件,那么本次更新包含的一些破坏性调整需要您额外注意,以避免对您和您创建的插件的用户造成影响,请阅读Changelog。
Get Started
简道云开放平台以插件为载体,允许用户在多个功能场景中,通过可插拔的自定义代码逻辑,实现一些定制需求。插件可以被分发,插件的用户无需关注代码等技术细节。
创建插件后,系统会自动为您在插件内创建一个函数。一个插件可以包含一个或多个函数。每个函数主要包含3个属性: 代码, 输入参数声明(以下简称入参声明)和输出参数声明(以下简称出参声明)。
您通过输入参数声明定义用户如何向您的函数传递参数,通过输出参数声明定义您将为用户输出哪些结果,并最终编写代码来实现从参数到结果的转换或其他自定义逻辑。
以一个翻译插件为例,您可以定义用户输入一个文本,您通过编写代码,以调用第三方API的方式将用户的文本翻译为日语并将结果返回给用户。用户可以将该结果回填到表单的其他字段。
插件设计页面介绍
序号 | 对应功能 |
1 | 插件名称 |
2 | 通用参数:在这里添加每个函数都需要依赖的参数,比如指定平台的秘钥。 |
3 | 函数:由入参声明、出参声明和代码组成。 |
4 | 可选控件:您通过拖拽控件设计入参声明/出参声明,不同类型的控件将在用户配置/使用时产生不同的效果。 |
5 | 入参声明/出参声明:您设计的入参声明,用户在简道云内使用您的插件和函数时, 需要为这里的每个参数指定值,这些值可以由用户直接填写,也可以配置为引用其他功能中的数据,简道云将为您完成数据绑定,您无需关注参数的来源(如右图)。 您设计的出参声明,用户在使用插件和函数时,可以配置是否使用、存储至指定字段。 |
6 | 单个参数的详细配置:设定 ID 便于在代码中对变量进行引用。设定相关提示和默认值便于您的用户理解该配置的含义。 |
下面以一个企业微信群机器人为例,演示如何开发一个简单的插件:
参数/接口设计
您首先需要设计函数的入参声明。该过程与您设计简道云表单类似,但通常更简单。
在设计入参声明时(上图5),您同样可以选择不同类型的控件(上图4)。不同的控件类型将影响插件用户在配置该参数时的界面样式,可选项以及部分场景下您在代码中获取到的值的类型(上图/下图5)。
举例,您在入参声明中添加了一个「日期时间」类型的参数,那么您的用户在配置时,将只能在界面上填写日期时间类型的值,或引用表单数据中支持转换为该类型的字段,比如「提交时间」。
在设计出参声明时,您需要定义函数将返回哪些属性给用户。根据使用场景的不同,用户能够将您返回的属性用于回填至表单,或传递给其他插件的其他函数。
您的函数也可以没有任何出参,比如我们的示例是帮助用户将文本内容推送到企业微信聊天群,不需要向用户返回任何结果。
参数设计的过程中,您可以保存并启用开发中的插件并到表单前端事件、智能助手等支持配置插件的功能场景体验配置的效果。
编写代码
参数设计确定后,您就需要为函数编写代码来实现其功能。在代码中您可以通过triggerConf这一全局变量来获取您在入参声明中定义的属性。如前文所述,这些属性将有您的用户决定其来源。
您的代码将被包裹于对应编程语言的一个函数之中,因此您可以通过return关键字来返回最终的结果。返回的结果需要与您在出参声明中的定义一致,否则插件的用户将不能读取该属性。您的函数也可以不返回任何结果。
出于安全性的考虑,部分系统库和函数将被禁用,您可以查看Reference了解相关说明。
在编程语言选择处(上图1)选择您熟悉的编程语言,在代码编辑器(上图2)中编写函数代码。在您的代码中,可以引用您在入参声明中定义的参数,参数列表和值的类型可以参考右侧的(上图3)中的提示。
示例代码及说明
Python 3.6
# 您可以引用一些第三方库
import requests
# 您可以通过读取预定义的全局变量中的属性来获取您定义的参数
# 如 triggerConf 是一个字典(dict)结构, key 为您在入参模板中为控件定义的 ID, 值为用户指定的配置值或表单字段的值.
botUrl = str(triggerConf.get("botUrl"))
messageText = str(triggerConf.get("messageText"))
# 您需要对输入参数的类型, 格式做校验, 以增强函数逻辑的健壮性.
if botUrl == '' or messageText == '':
raise ValueError('必须传递 botUrl 和 messageText')
# 您可以利用第三方库和输入参数, 在服务端环境中调用相关接口并获取结果.
requests.post(botUrl, json={
"msgtype": "text",
"text": {
"content": messageText
}
})
Node.js 12
# 您可以引用一些第三方库
const _ = require('lodash');
const axios = require('axios');
# 您可以通过读取预定义的全局变量中的属性来获取您定义的参数
# 如 triggerConf 是一个 Object 结构, key 为您在入参模板中为控件定义的 ID, 值为用户指定的配置值或表单字段的值.
botUrl = _.trim(triggerConf.botUrl);
messageText = _.trim(triggerConf.messageText);
# 您需要对输入参数的类型, 格式做校验, 以增强函数逻辑的健壮性.
if (!botUrl || !messageText) {
throw new Error('必须传递 botUrl 和 messageText');
}
# 您可以利用第三方库和输入参数, 在服务端环境中调用相关接口并获取结果.
# 支持 ES6 async/await 语法.
await axios.post(botUrl, {
msgtype: 'text',
text: {
content: messageText
}
});
Reference
入参声明
简道云会根据您在入参声明中选择的控件类型和用户配置该参数值的来源,在运行时尝试为您做必要的类型转换,以使您在插件代码中能够取到格式合理的值。但这个过程不是严格保证的,在复杂场景中,您可能会读取到各种预期内或预期外类型的参数,您应当在编写代码时考虑这一情况,并在不能处理相关类型时抛出合理的错误以指引用户调整其配置。
举例,您正在开发一个图像识别类的插件,并在该插件的入参声明中加入了一个文本字段用以接受用户的图片附件,那么基于不同的用户配置,您有可能得到不同的值:
- 用户可能直接填写了一个文件的URL,此时您将得到一个内容为"https://。。。"的字符串。
- 用户可能关联到简道云表单数据中的图片/附件控件值上,此时您将得到一个内容为'[{"url":"https://。。。","name":"。。。"},{"url":"https://。。。","name":"。。。"}]'的JSON字符串。
- 用户可能关联到简道云表单数据中的手写签名/微信头像控件值上,此时您将得到一个'{"url":"https://。。。","name":"。。。"}'的JSON字符串。
- 用户可能关联到另一个插件的返回值的一个属性上,此时您可能得到一个类型不确定的值。
您应该结合自己插件的功能和预期服务的用户场景,在代码中妥善处理上述情况,并在不计划支持该类型时通过抛出错误并附加消息向用户提示原因。
控件数据存储格式
1.保存自定义值
当用户直接保存「自定义值」时(如上图),入参声明中各个控件的储值格式如下表:
控件 | 储值结构 |
文本 | "你好, 世界" |
下拉框 | "选项1" |
数字 | 3.14 |
日期时间 | "2022-09-09T08:00:00.000Z" |
成员选择 | [{ "_id": "5b433bf80118dc44bcb9183b", "name": "lizijun", "username": "lizijun", "status": 1, "type": 0 }] |
部门选择 | [{ "_id": "602f77c86aee9d2dd4c04bfc", "name": "一级部门", "dept_no": 3, "type": 0 }] |
2.保存字段
当用户在简道云表单数据场景中使用插件时,可保存字段值(如上图),入参声明中各个控件的储值格式如下表:
表单控件 \ 入参声明控件 | 字段值 | 模板字符串 | |||||
文本 | 下拉框 | 数字 | 日期时间 | 成员选择 | 部门选择 | ||
单行文本 | "你好, 世界" | ❌ | ❌ | ❌ | ❌ | ❌ | 值: "你好, 世界" ID: "_widget_1653375890324" |
多行文本 | "你好, 世界\n我能吞下玻璃而不伤身体" | ❌ | ❌ | ❌ | ❌ | ❌ | 值: "你好, 世界\n我能吞下玻璃而不伤身体" ID: "_widget_1653375890324" |
数字 | 3.14 | ❌ | 3.14 | ❌ | ❌ | ❌ | 值: 3.14 ID: "_widget_1653375890324" |
日期时间 | "2022-09-09T08:00:00.000Z" | ❌ | ❌ | "2022-09-09T08:00:00.000Z" | ❌ | ❌ | 值: "2022-09-09 16:00:00" ID: "_widget_1653375890324" |
单选按钮组 | "选项1" | ❌ | ❌ | ❌ | ❌ | ❌ | 值: "选项1" ID: "_widget_1653375890324" |
复选框组 | '["选项1","选项3"]' | ❌ | ❌ | ❌ | ❌ | ❌ | 值: "选项1, 选项3" ID: "_widget_1653375890324" |
下拉框 | "选项1" | ❌ | ❌ | ❌ | ❌ | ❌ | 值: "选项1" ID: "_widget_1653375890324" |
下拉复选框 | '["选项1","选项3"]' | ❌ | ❌ | ❌ | ❌ | ❌ | 值: "选项1, 选项3" ID: "_widget_1653375890324" |
地址 | '{"province":"山西省","city":"太原市","district":"尖草坪区","detail":"迎新街俱乐部"}' | ❌ | ❌ | ❌ | ❌ | ❌ | 值: "山西省太原市尖草坪区迎新街俱乐部" ID: "_widget_1653375890324" |
定位 | '{"province":"山西省","city":"太原市","district":"尖草坪区","detail":"迎新街俱乐部", "lnglatXY":[112.549656,37.870451}' | ❌ | ❌ | ❌ | ❌ | ❌ | 值: "山西省太原市尖草坪区迎新街俱乐部" ID: "_widget_1653375890324" |
图片 | '[{"name":"emoji.gif","size":10168,"mime":"image/gif","url":"https://..."}]' | ❌ | ❌ | ❌ | ❌ | ❌ | 值: ID: "_widget_1653375890324" |
附件 | '[{"name":"emoji.jpg","size":10168,"mime":"image/jpeg","url":"https://..."}]' | ❌ | ❌ | ❌ | ❌ | ❌ | 值: ID: "_widget_1653375890324" |
手写签名 | {"name":"signature_1663590240652.png","size":8800,"mime":"image/png","url":"https://..."} | ❌ | ❌ | ❌ | ❌ | ❌ | 值: ID: "_widget_1653375890324" |
流水号 | "00005" | ❌ | ❌ | ❌ | ❌ | ❌ | 值: "00005" ID: "_widget_1653375890324" |
手机 | '{"verified":false,"phone":"155..."}' | ❌ | ❌ | ❌ | ❌ | ❌ | 值: "15536090976" ID: "_widget_1653375890324" |
成员单选 | '[{"_id":"5b433bf80118dc44bcb9183b","name":"lizijun","username":"lizijun","status":1,"type":0}]' | ❌ | ❌ | ❌ | '[{"_id":"5b433bf80118dc44bcb9183b","name":"lizijun","username":"lizijun","status":1,"type":0}]' | ❌ | 值:(name) "lizijun" ID: "_widget_1653375890324" |
成员多选 | '[{"_id":"5b433bf80118dc44bcb9183b","name":"lizijun","username":"lizijun","status":1,"type":0}]' | ❌ | ❌ | ❌ | '[{"_id":"5b433bf80118dc44bcb9183b","name":"lizijun","username":"lizijun","status":1,"type":0}]' | ❌ | 值:(name) "lizijun, springmoon" ID: "_widget_1653375890324" |
部门单选 | '[{"_id":"602f77c86aee9d2dd4c04bfc","name":"一级部门","dept_no":3,"type":0}]' | ❌ | ❌ | ❌ | ❌ | '[{"_id":"602f77c86aee9d2dd4c04bfc","name":"一级部门","dept_no":3,"type":0}]' | 值:(name) "帆软软件有限公司" ID: "_widget_1653375890324" |
部门多选 | '[{"_id":"602f77c86aee9d2dd4c04bfc","name":"一级部门","dept_no":3,"type":0}]' | ❌ | ❌ | ❌ | ❌ | '[{"_id":"602f77c86aee9d2dd4c04bfc","name":"一级部门","dept_no":3,"type":0}]' | 值:(name) "帆软软件有限公司, springmoon的企业/团队" ID: "_widget_1653375890324" |
出参声明
相较于入参声明,出参声明更加简单。出参即您在插件函数代码的返回值。
全局变量
您在代码中可以直接引用全局变量 triggerConf 和 triggerContext。
triggerConf 结构
triggerConf即用户调用插件函数的入参。其结构为对应编程语言的键值数据结构,如Python的dict或Node.js的Object。在该结构中,键为您在入参声明中为变量指定的ID,值为用户配置或引用的数据(具体结构参考"入参声明")。
triggerContext 结构
triggerContext 中存储了运行环境的基本信息,如触发插件的租户 ID,简道云的开放 API 域名等。
返回值
您的返回值应当是一个字典,并和定义的出参对应,例如:
return {
"result":msgResult
}
中断和抛出错误
插件执行失败时,不会扣插件执行的费用。您可以自己在代码里抛出错误,定义某些情况算做插件执行失败,抛出的错误信息会展示给用户,让用户明确插件执行失败原因。比如:第三方接口调用失败,此类情况实际未产生接口成本,应当算做插件执行失败。
相关方法:
- Node.js :throw new Error("错误信息")
- Python :raise Exception("错误信息")
内置库/函数支持列表
待补充
运行时限制
- 插件本身的超时时间为60s,但是不同使用场景本身也有自己的超时时间,比如前端事件的20s。
Best Practice
待补充