伺服器端 Action、排程與 Webhook

Action 是 Custom App 的伺服器端邏輯。這一章說明怎麼寫、怎麼被觸發、以及執行時的實際限制。


為什麼需要 Action

前端程式碼跑在使用者的瀏覽器裡——這代表任何寫在前端的東西,使用者都看得到、也改得動。

因此以下三類邏輯必須放進 Action:

  1. 需要金鑰的:呼叫第三方 API、簽章、加解密
  2. 需要高權限的:觸發 ERP 業務動作、寫入敏感資料
  3. 不該被看到的:計價規則、風控邏輯、內部演算法

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.adminsettings.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 有執行時間上限。超時的處理分三層:

  1. 軟逾時:達到設定的時限時,平台嘗試中斷執行並回報逾時
  2. 硬逾時:軟逾時無效時,執行環境會直接切斷請求
  3. 殘留偵測:若執行緒無法回收,該執行個體會標記為不健康並被替換

實務意義: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 章。