伺服器端 Action、排程與 Webhook
Action 是 Custom App 的伺服器端邏輯。這一章說明怎麼寫、怎麼被觸發、以及執行時的實際限制。
為什麼需要 Action
前端程式碼跑在使用者的瀏覽器裡——這代表任何寫在前端的東西,使用者都看得到、也改得動。
因此以下三類邏輯必須放進 Action:
- 需要金鑰的:呼叫第三方 API、簽章、加解密
- 需要高權限的:觸發 ERP 業務動作、寫入敏感資料
- 不該被看到的:計價規則、風控邏輯、內部演算法
Action 在隔離的伺服器環境執行,使用者拿不到原始碼,也無法繞過裡面的檢查。
Action 的基本結構
每個 Action 是 actions/ 底下的一支 Python 檔,必須提供 execute(ctx) 函式:
# actions/confirm_orders.py
def execute(ctx):
ids = ctx.params.get('ids', [])
if 'sale.write' not in ctx.user_permissions:
return ctx.response.json({'error': '沒有權限'})
confirmed = []
for oid in ids:
ctx.erp.confirm_sale_order(oid)
confirmed.append(oid)
ctx.response.json({'confirmed': confirmed})
actions/manifest.json
清單檔宣告有哪些 Action 以及它們的設定:
{
"actions": [
{
"name": "confirm_orders",
"description": "批次確認銷售訂單",
"meta": { "webhook": false }
},
{
"name": "receive_payment_callback",
"description": "接收金流回呼",
"meta": { "webhook": true }
}
]
}
共用程式碼:actions/_shared/
Action 之間要共用邏輯時,放進 actions/_shared/:
# actions/_shared/money.py —— 不是 Action,不需要 execute(ctx)
def round_to_cents(value):
return round(value + 1e-9, 2)
# actions/settle.py
from _shared.money import round_to_cents
def execute(ctx):
total = round_to_cents(sum(x['amount'] for x in ctx.params['items']))
...
注意:Action 彼此之間不能互相 import——from confirm_orders import ... 不會work。要共用就把邏輯移到 _shared/,兩邊都從那裡匯入。
三種觸發方式
1. 前端呼叫
最常見的方式。使用者在介面上操作,前端透過 src/action.ts 呼叫:
import { callAction } from './action';
const res = await callAction('confirm_orders', { ids: [...] });
2. 排程(App Cron)
讓 Action 在指定時間自動執行,不需要有人在線上。
設定入口有兩個,各自的授權面不同:
| 入口 | 誰用 | 權限 |
|---|---|---|
| Builder 的「排程」分頁 | 應用開發者 | builder.app_cron_manage |
| 主控台「系統與維運管理 › App 排程」 | 組織管理者 | system.admin 或 settings.write |
前者讓開發者在開發自己的應用時就能配好排程;後者讓管理者總覽全組織的所有排程。
典型場景:
- 每天凌晨彙整前一日銷售並產生報表
- 每小時檢查庫存低於警戒值的品項並通知
- 每月為月結客戶批次合併開票
執行身分:排程觸發的 Action 沒有觸發者。ctx.user_id 為空、ctx.user_permissions 為空清單。這代表:
- 依賴使用者身分的 SDK(如
ctx.approval.list_pending)在排程中不可用 ctx.knowledge.search會以「無角色成員」的權限檢索- 簽核守衛仍會生效,簽核介面上的申請人會顯示為 API
自動暫停:連續失敗的排程會被自動暫停,避免一個壞掉的任務持續消耗配額。暫停後需要人工檢視並重新啟用。
3. Webhook(外部系統呼叫)
在 manifest.json 中把某個 Action 的 meta 加上 "webhook": true,即開啟對外端點:
POST https://{app 子網域}/webhook/{action_name}
可以對多個 Action 分別宣告,各自成為獨立端點。發布後生效。
典型場景:金流回呼、物流狀態推送、第三方系統的事件通知。
安全提醒:Webhook 端點是公開的——任何人知道網址都能打。請務必在 Action 內驗證來源,常見做法是用 ctx.crypto.hmac_sign 比對簽章:
def execute(ctx):
body = ctx.params.get('_raw_body', '')
expected = ctx.crypto.hmac_sign('webhook_secret', body)
if ctx.params.get('signature') != expected:
return ctx.response.json({'error': 'invalid signature'})
...
執行環境與限制
隔離
每個 Action 在獨立的沙箱環境執行。應用程式之間、組織之間互不可見。
逾時
Action 有執行時間上限。超時的處理分三層:
- 軟逾時:達到設定的時限時,平台嘗試中斷執行並回報逾時
- 硬逾時:軟逾時無效時,執行環境會直接切斷請求
- 殘留偵測:若執行緒無法回收,該執行個體會標記為不健康並被替換
實務意義:Action 不適合做長時間運算。需要跑很久的工作,請拆成多次排程執行,或把重活交給外部服務再以 Webhook 接回結果。
併發
平台會依用量自動擴充執行個體,並受組織的運算配額約束。
執行紀錄
每次執行都留下紀錄,包含參數摘要、耗時、結果狀態與錯誤訊息。可在 Builder 中檢視,AI 開發助手也能讀取它來協助排錯。
若執行結果因基礎設施問題無法確認,紀錄會標記為「結果不明」——這種狀態永不計費、不重跑,並在真實結果遲到時自動覆蓋。
撰寫 Action 的實務建議
檢查權限,不要假設。前端藏起來的按鈕不構成安全邊界:
if 'accounting.write' not in ctx.user_permissions:
return ctx.response.json({'error': '沒有權限執行此操作'})
檢查外部呼叫的回傳狀態。ctx.http.call 不拋例外:
resp = ctx.http.call('svc', '/path')
if resp['status'] != 200:
return ctx.response.json({'error': resp['data'].get('fix', '外部服務錯誤')})
避免阻塞式寫法。長時間的同步等待會佔住執行個體。平台的靜態檢查會警告這類寫法。
先試跑再發布。Builder 的試跑功能會在開發環境真的執行一次,用真實的權限與資料驗證——比讀程式碼可靠得多。
簽核會攔截寫入
如果管理者為某張表設定了簽核流程,Action 的寫入會被自動攔截:
- 新增:資料保持為待審核狀態,直到所有關卡通過
- 更新/刪除:不執行,並拋出待簽核例外(帶申請識別碼)。更新的內容會暫存,簽核通過後由平台自動執行
Action 不需要也不應該重試——重試只會產生重複的簽核申請。正確做法是捕捉例外並告訴使用者「已送出簽核」。
詳見第 13 章。