外掛設計
簡介
Jodoo 開放平台以外掛為載體,允許使用者在多個功能場景中,透過可插拔的自訂程式碼邏輯,實現一些客製化需求。外掛可以被分發,外掛的使用者無需關注程式碼等技術細節。
建立外掛後,系統會自動為您在外掛內建立一個函數。一個外掛可以包含一個或多個函數。每個函數主要包含三個屬性: 程式碼、請求參數聲明(以下簡稱入參聲明)和返回參數聲明(以下簡稱出參聲明)。
透過請求參數聲明定義外掛使用者需要向您的函數傳遞哪些參數,透過返回參數聲明定義您將為使用者輸出哪些結果,並最終編寫程式碼來實現從參數到結果的轉換或其他自訂邏輯。
以一個翻譯外掛為例,您可以定義使用者輸入一個文字,您透過編寫程式碼,以調用第三方 API 的方式將使用者的文字翻譯為日語並將結果返回給使用者。使用者可以將該結果回填到表單的其他欄位。
1. 外掛設計頁面及各部分功能見下表:
序號 | 對應功能 | 功能說明 |
❶ | 外掛名稱 | / |
❷ | 通用參數 | 在這裡新增每個函數都需要依賴的參數,如指定平台的密鑰 |
❸ | 後端函數/前端擴展 | 由入參聲明、出參聲明和程式碼組成。
|
❹ | 可選欄位 | 您透過拖曳欄位設計參數,不同類型的欄位將在使用者配置/使用時產生不同的效果。 |
❺ | 請求參數/返回參數聲明 | 您設計的請求參數,使用者在使用外掛時,需要為這裡的每個參數指定值,這些值可以由使用者直接填寫,也可以配置為引用其他功能中的資料,Jodoo 將為您完成資料綁定,您無需關注參數的來源。 您設計的返回參數,使用者在使用外掛時,可以配置是否使用、儲存至指定欄位。 |
❻ | 單個參數的詳細配置 |
|
2. 設計頁面中的相關配置項,在前台使用外掛時對應如下:
新增函數
外掛新建完成後,需要新增對應的函數。外掛中的函數分為以下兩種類型,在「外掛設計」頁面中,點擊左下角的「新增函數」,您可根據自己的需求選擇不同的函數進行建立:
函數類型 | 函數釋義 | 可選位置 | 備註 |
前端擴展 | 若設定動作為前端擴展,則在前端執行外掛,可調用一些前端行為。 比如:彈出 iframe 彈窗。 場景舉例:彈出地圖頁面;錄音。 | 只有前端事件處可以選到該動作。 目前僅支援作為按鈕欄位的執行動作。 | 不計費、不參與後台觸發次數統計。 |
後端函數 | 若設定動作為後端函數,則後端完成外掛執行過程。 場景舉例:excel 解析。 | 所有調用外掛的位置均可選到該動作。 | / |
附註:
1. 前端擴展與後端函數均包含:請求參數、返回參數、程式碼部分。請求參數和返回參數規則一致,程式碼部分各自支援的方法見下文【程式碼編輯】。
2. 單個自建外掛內的函數數量上限為 30 個。
參數設計
外掛有四種類型的參數可以配置,四者都非必須,按場景所需新增即可:
參數類型 | 含義 | 數量限制 | |
1 | 通用參數 | 通用參數是使用外掛時要配置的參數針對整個外掛的參數。通常是在啟用外掛前,對外掛進行的整體配置,比如調用Jodoo API 的 API Key。 | 1. 通用參數/單個請求參數內/單個返回參數內欄位數量上限為 128 個。 2. 請求參數中,提示內容欄位長度上限為 4096。 |
2 | 請求參數 | 請求參數是在不同位置(比如前端事件/智慧助手)中使用外掛時,外掛使用者需要提供的內容。比如:調用 Jodoo 部門API 時,需要使用者提供部門資訊。 | |
3 | 返回參數 | 返回參數是指外掛執行完成後,返回給外掛使用者的內容。外掛使用者可以選擇需要的內容回填至表單中。 |
通用參數
通用參數是使用外掛時要配置的參數針對整個外掛的參數。通常是在啟用外掛前,對外掛進行的配置。
如,對接第三方平台需要配置的 API Key 等參數均可以在通用參數中設定。
附註:
1. 根據參數需要填寫的內容選擇欄位,通用參數僅支援基礎欄位的設定:文字、數字、日期時間、下拉單選。
2. 通用參數內的欄位數量上限為 128 個。
請求參數
請求參數是在不同位置(比如前端事件/智慧助手)中使用外掛時,外掛使用者需要提供的內容。比如:調用Jodoo 部門 API 時,需要使用者提供部門資訊。
在設計請求參數時,可以選擇不同類型的欄位。不同的欄位類型影響外掛使用者在配置外掛時的輸入方式,以及您在程式碼中取得到的值的類型。
舉例,您在入參聲明中新增了一個「部門選擇」類型的參數,那麼您的使用者在配置外掛時,將只能在界面上選擇部門類型的值,或引用表單資料中支援轉換為該類型的欄位,比如「部門單選」/「部門多選」。
附註:
1. 請求參數不同類型對於表單欄位儲存格式的支援,可以查看 【相關資訊】 瞭解。
2. 單個請求參數內欄位上限數量為 128 個。
3. 請求參數中,提示內容欄位長度上限為 4096。
返回參數
返回參數是指外掛執行完成後,返回給外掛使用者的內容。外掛使用者可以選擇需要的內容回填至表單中,或傳遞給其他外掛的其他函數。比如:調用部門 API 後,將會返回部門下成員資訊,可以將成員資訊定義成返回參數。
附註:
單個返回參數內欄位上限數量為 128 個。
2. 參數類型可選擇 any、object[]。
預設為 any,適用於請求參數與返回參數一對一的情況。
object[],適用於請求參數與返回參數一對多的情況,返回資訊為陣列,比如透過部門查詢部門下成員時,可能有多個成員。
若設定了參數類型為 object[],則可透過點擊列表頭前的「+ 號」按鈕,向下新增一行,作為子節點,如下所示。子節點的含義為陣列內的具體元素,比如:返回多個成員,每個成員資訊包含成員暱稱、成員編號等。
3. 返回參數在前端事件外掛中,配置效果如下所示:
表單校驗提示資訊
表單檢視下配置通用參數、請求參數和返回參數時,支援對輸入內容進行格式提示和即時校驗,以降低輸入錯誤的可能性,便於開發者校驗修改表單配置,提高開發效率。效果如下圖所示:
程式碼編輯
參數設計確定後,您就需要為函數編寫程式碼來實現其功能。
您的程式碼將被包裹於對應編程語言的一個函數之中,在程式碼中可以根據需要,透過 triggerConf 這一全局變數來取得定義的請求參數值,透過 return 關鍵字來返回最終的結果。
在編程語言選擇處(上圖 1)選擇您熟悉的編程語言,在程式碼編輯器(上圖 2)中編寫函數程式碼。在您的程式碼中,可以引用您在入參聲明中定義的參數,參數列表和值的類型可以參考右側的(上圖 3)中的提示。
後端函數-範例程式碼及說明
Python 3.6
# 可以引用一些第三方庫.
import json
import requests
# 可以透過讀取預定義的全局變數中的屬性來取得您定義的參數.
# agentConf 含義是通用參數, 其結構為一個字典(dict), key 為您在入參範本中為控件定義的 ID, 值為使用者指定的配置值.
# triggerConf 含義是請求參數, 其結構為一個字典(dict), key 為您在入參範本中為控件定義的 ID, 值為使用者指定的配置值或表單欄位的值.
api_key = str(agentConf.get('apiKey'))
dept_no = triggerConf.get('deptNo')
# 您需要對輸入參數的類型, 格式做校驗, 以增強函數邏輯的健壯性.
# 如下方對可能拿到的不同值格式進行處理,保證最終dept_no的準確性,具體值格式見本文「5.1 triggerConf (請求參數)結構」
try:
if isinstance(dept_no, str):
dept_no = json.loads(dept_no)
if isinstance(dept_no, list):
dept_no = dept_no[0] if 0 < len(dept_no) else None
if isinstance(dept_no, dict):
dept_no = dept_no.get('dept_no')
if dept_no is None:
dept_no = 1
dept_no = int(dept_no)
except:
raise ValueError('部門編號格式錯誤')
# 可以利用第三方庫和請求參數, 在服務端環境中調用相關API並取得結果.
url = ' https://api.jodoo.com/api/v5/corp/department/user/list'
payload = {'dept_no': dept_no}
headers = {'Authorization': 'Bearer ' + api_key}
response = requests.post(url, json=payload, headers=headers)
if 300 <= response.status_code < 500:
# 對特定結果主動拋錯, 定義拋錯文案.
body = response.json()
code = body.get('code', -1)
message = body.get('message', '未知錯誤')
message = '參數錯誤(%s): %s' % (code, message)
raise ValueError(message)
# 對結果進行處理, 定義返回參數值.
# 您需要返回一個dict, dict的key和返回參數ID一一對應. 若返回為陣列, 則對應出參需設定為object[]類型.
body = response.json()
users = [{'name': user.get('name'), 'username': user.get(
'username')} for user in body.get('users', [])]
count = len(users)
return {
"users": users,
"count": count
}
# 可引用的全局變數, 支援的第三方庫, 請求參數資料儲存格式, 見本文「相關資訊」.
Node.js 12
// 可以引用一些第三方庫.
const _ = require('lodash');
const axios = require('axios');
// 可以透過讀取預定義的全局變數中的屬性來取得您定義的參數.
// agentConf 含義是通用參數, 其結構為一個對象(Object), key 為您在入參範本中為控件定義的 ID, 值為使用者指定的配置值.
// triggerConf 含義是請求參數, 其結構為一個對象(Object), key 為您在入參範本中為控件定義的 ID, 值為使用者指定的配置值或表單欄位的值.
const apiKey = _.chain(agentConf).get(['apiKey']).trim().value();
let deptNo = _.get(triggerConf, ['deptNo']);
// 您需要對輸入參數的類型, 格式做校驗, 以增強函數邏輯的健壯性.
// 如下方對可能拿到的不同值格式進行處理,保證最終dept_no的準確性,具體值格式見本文「5.1 triggerConf (請求參數)結構」
try {
if (_.isString(deptNo)) deptNo = JSON.parse(deptNo);
if (_.isArray(deptNo)) deptNo = _.first(deptNo);
if (_.has(deptNo, ['dept_no'])) deptNo = _.get(deptNo, ['dept_no']);
deptNo = _.toInteger(deptNo);
} catch (e) {
throw new Error('請輸入正確的部門編號');
}
// 可以利用第三方庫和請求參數, 在服務端環境中調用相關API並取得結果.
try {
const response = await axios({
method: 'post',
url: ' https://api.jodoo.com/api/v5/corp/department/user/list',
headers: {
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json'
},
data: { dept_no: deptNo }
});
const { data } = response;
// 對結果進行處理, 定義返回參數值.
// 您需要返回一個object, object的key和返回參數ID一一對應. 若返回為陣列, 則對應出參需設定為object[]類型.
const users = _.chain(data)
.get(['users'])
.map((it) => ({ name: it.name, username: it.username }))
.value();
return { users, count: users.length };
} catch (e) {
// 對特定結果主動拋錯, 定義拋錯文案.
const code =
_.get(e, ['response', 'data', 'code']) || _.get(e, ['code']) || -1;
let message =
_.get(e, ['response', 'data', 'msg']) ||
_.get(e, ['message']) ||
'未知錯誤';
message = `參數錯誤(${code}): ${message}`;
throw new Error(message);
}
// 可引用的全局變數, 支援的第三方庫, 請求參數資料儲存格式, 見本文「相關資訊」
前端擴展-範例程式碼及說明
// closeModal
const close = () => {
$g.utils.closeModal();
console.log('4s後模態框關閉');
};
const reportUrl = 'https://t6ixa9nyl6.jodoo.com/f/6005518bc9f3660006e9d764';
setTimeout(() => close(), 4000);
// GET
let response2 = await fetch(reportUrl, {method: 'GET'});
console.log(await response2.text());
// openModal
// openModal:彈窗後繼續程式碼執行,執行完結束 Worker。
// await openModal:彈窗後阻塞程式碼執行,按照開發者程式碼規定執行後續動作。
await $g.utils.openModal({
title: '如果您對外掛有任何需求,歡迎在本表單填寫',
url: reportUrl
});
// 調用後端函數
await $g.utils.callFunction({
name: 'func.ID',
data: {}
})
// 主動拋錯語法和後端函數相同
參數提示的使用
程式碼編輯器右側可展開參數提示,點擊對應參數/函數,將新增對應調用語法到程式碼編輯器中。
參數校驗提示資訊
編寫請求參數/返回參數/通用參數時,若出現程式碼錯誤,對應程式碼將用紅色波浪線標註,且當滑鼠放在對應程式碼上時,會顯示詳細的配置錯誤資訊,便於開發者校驗修改程式碼內容,提升編輯體驗。效果如下所示:
語法錯誤提示資訊
在前端函數/後端擴展的 Python 程式碼檢視下,編輯器中支援在儲存程式碼時提示語法錯誤。幫助開發者快速定位具體問題,為開發者提供更為直觀、高效的開發體驗。效果如下圖所示:
效果展示
1. 後端函數外掛配置及執行效果如下所示:
2. 前端擴展外掛配置及執行效果如下所示:
相關資訊
全局變數
您在程式碼中可以直接引用全局變數 triggerConf、agentConf、triggerContext。
agentConf (通用參數)結構
agentConf 即外掛對應的通用參數。
其結構為對應編程語言的鍵值資料結構,如 Python 的 dict 或 Node.js 的 Object。在該結構中,鍵為您在入參聲明中為變數指定的 ID,值為使用者配置或引用的資料。
triggerConf (請求參數)結構
triggerConf 即外掛函數對應的請求參數。
其結構為對應編程語言的鍵值資料結構,如 Python 的 dict 或 Node.js 的 Object。在該結構中,鍵為您在入參聲明中為變數指定的 ID,值為使用者配置或引用的資料(具體結構見下文)。
Jodoo 會根據您在入參聲明中選擇的控件類型和使用者配置該參數值的來源,在運行時嘗試為您做必要的類型轉換,以使您在外掛程式碼中能夠取到格式合理的值。但這個過程不是嚴格保證的,在複雜場景中,您可能會讀取到各種預期內或預期外類型的參數,您應當在編寫程式碼時考慮這一情況,並在不能處理相關類型時拋出合理的錯誤以指引使用者調整其配置。
舉例,您正在開發一個圖像辨識類的外掛,並在該外掛的請求參數中加入了一個文字欄位用以接受使用者的圖片附件,那麼基於不同的使用者配置,您有可能得到不同的值:
- 使用者可能直接填寫了一個文件的 URL,此時您將得到一個內容為"https://。。。"的字串。
- 使用者可能關聯到Jodoo表單資料中的圖片/附件控件值上,此時您將得到一個內容為'[{"url":"https://。。。","name":"。。。"},{"url":"https://。。。","name":"。。。"}]'的 JSON 字串。
- 使用者可能關聯到Jodoo表單資料中的手寫簽名/微信頭像控件值上,此時您將得到一個'{"url":"https://。。。","name":"。。。"}'的 JSON 字串。
- 使用者可能關聯到另一個外掛的返回值的一個屬性上,此時您可能得到一個類型不確定的值。
您應該結合自己外掛的功能和預期服務的使用者場景,在程式碼中妥善處理上述情況,並在不計劃支援該類型時透過拋出錯誤並附加訊息向使用者提示原因。
請求參數儲值格式
1. 儲存自訂值
當使用者在外掛配置時直接儲存「自訂值」時(如上圖),請求參數中不同控件的儲值格式如下表:
控件 | 儲值結構 |
文字 | "你好, 世界" |
下拉單選 | "選項1" |
數字 | 3.14 |
日期時間 | "2022-09-09T08:00:00.000Z" |
成員選擇 | [{ "_id": "5b433bf80118dc44bcb9183b", "name": "Jodoo", "username": "Jodoo", "status": 1, "type": 0 }] |
部門選擇 | [{ "_id": "602f77c86aee9d2dd4c04bfc", "name": "一級部門", "dept_no": 3, "type": 0 }] |
2. 儲存欄位
當使用者在Jodoo表單資料場景中使用外掛時,可儲存欄位值(如上圖),入參聲明中各個控件的儲值格式如下表:
表單控件 \ 入參聲明控件 | 欄位值 | 範本字串 | |||||
文字 | 下拉單選 | 數字 | 日期時間 | 成員選擇 | 部門選擇 | ||
單行文字 | "你好, 世界" | ❌ | ❌ | ❌ | ❌ | ❌ | 值: "你好, 世界" 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" |
圖片 | '[{"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" |
triggerContext 結構
triggerContext 中儲存了運行環境的基本資訊,如Jodoo的開放 API 域名等。
內置庫支援列表
Node.js
- axios(0.21.0);
- form-data(2.3.3);
- lodash(4.17.20);
- moment-timezone(0.5.32)、mongodb(3.6.3)、mssql(8.1.2)、mysql 2 (2.2.5);
- pg(8.5.1)、pg-hstore(2.3.3);
- sequelize(6.3.5)
Python
以下列表裏包括可調用的內置庫和第三方庫。
- abc、 argparse、 array、asynchat、asyncio、asyncore、audioop;
- base64、binascii、binhex、bisect、bz2;
- calendar、cgi、cgitb、chunk、cmath、cmd、codecs、collections、colorsys、configparser、contextlib、contextvars、copy、copyreg、crypt、csv、ctypes;
- dataclasses、datetime、dbm、decimal、difflib、doctest;
- email、encodings、enum、errno;
- fnmatch、formatter、fractions、ftplib、functools;
- getopt、gettext、glob、graphlib、grp、gzip;
- hashlib、heapq、hmac、html、http;
- imaplib、 imghdr、 ipaddress、 itertools;
- json;
- keyword;
- linecache、 locale、 logging、 lzma;
- mailbox、 mailcap、 math、 mimetypes、 mmap;
- netrc、 nis、 nntplib、 numbers;
- operator、 optparse;
- parser、 poplib、 pprint;
- quopri;
- random、 re、 reprlib;
- secrets、 select、 selectors、 shlex、 smtpd、 smtplib、 sndhdr、 socket、 socketserver、sqlite3、 ssl、 statistics、 string、 stringprep、 struct、 sunau;
- tarfile、 telnetlib、 test、 textwrap、 time、 timeit、 types、 typing;
- unicodedata、 unittest、 urllib、 uu、 uuid;
- warnings、 wave、 weakref、 wsgiref;
- xml、 xmlrpc;
- zlib、 zoneinfo;
- requests(2.26.0)、 pymysql(1.0.2)、pymssql(2.2.5)、pymongo(3.12.1)、psycopg2_binary(2.9.3);
- paramiko(2.8.0)、 pyjwt(2.3.0)、lxml(4.6.3)、xmltodict(0.12.0)、jinja2(2.11.3);
- cryptography(35.0.0)。
可用前端擴展 API
可用API名稱 | 含義 | 程式碼範例 |
$g.utils.openModal | 打開彈窗 | $g.utils.openModal({ title: '彈窗標題', url: '彈窗打開連結' }) |
$g.utils.closeModal | 關閉彈窗 | $g.utils.closeModal() |
$g.utils.callFunction | 調用後端函數 | $g.utils.callFunction({ name: 'func.ID', data: {} }) |
$g.utils.openUrl | 新標籤頁打開 URL | $g.utils.openUrl({ url: '連結地址' }) |
$g.ui.onmessage | 接收彈窗 iframe 內發送的訊息 | // 外掛 $g.ui.onmessage = (message) => { // 對應 postMessage 中 pluginMessage 的值 console.log(message) } //彈窗 iframe parent.postMessage({pluginMessage: 'any'}, '*')
|
附註:
歷史 openModal、closeModal 和 callFunction 等用法仍兼容。
注意事項
外掛在運行時有相應的限制。外掛本身的超時時間為 60 s,但是不同使用場景本身也有自己的超時時間,比如前端事件的 20 s。