# 流程資料接駁與變更通知

在 OpenDashboard 建立流程，網站使用連結、iframe 或自訂 UI 填表。OpenDashboard 保存答案及推進流程；外站需要副本時，使用本頁的表單對應、申請讀取及資料變更通知。這不是雙向同步，也不會接入既有外站資料。

## 在哪裡設定

進入「流程管理 → 選擇流程 → 網站接入 → 資料通知 → 開發者設定」。已發佈流程可查看表單對應，設定 HTTPS 接收網址、啟用通知、取得一次顯示的驗證密鑰及查看傳送紀錄。設定需要查看個人資料及編輯權限；查看紀錄需要查看個人資料權限。

通知預設關閉。不指定接收網站就不會向外傳送。接收網址須為公開 DNS 名稱、HTTPS 443，不能包含帳密、查詢參數或片段；不跟隨轉址，目前需有公開 IPv4。每次傳送重新核對 DNS 並固定連線地址，拒絕私人網絡及保留地址。

## 表單關聯和版本

| 名稱 | 代表什麼 |
| --- | --- |
| workflow_id | 一套流程，跨發佈版本不變 |
| version | 一宗申請固定使用的發佈版本 |
| step_id | 流程中的填表步驟 |
| form_id | 該發佈版本儲存答案的表單 |
| source.kind = workflow_owned | 題目在流程內建立 |
| source.kind = question_reference | 引用既有題目；source.form_id 和 revision 指向來源及題目版本 |
| application_id | 同一宗申請的固定 OpenDashboard 編號，等於 workflow run id |
| external_id | 外站建立申請時指定的識別值；OpenDashboard 公開入口會自動產生 |
| revision | 申請每次成功變更後遞增，用於衝突檢查及避免舊資料覆蓋新資料 |

引用問題不等於連接既有答案。發佈後的 form_id 是流程自己的版本表單；原表單、原答案、電郵及 QR 設定保持原狀。新版本不會搬移舊申請。改題目後須重新發佈，新入口選用新版本，舊申請仍使用原版本。

舊資料接入是另行核對的遷移工作，目前不提供把任意外站答案自動變成流程申請的操作。全新活動可直接在流程建立表單，無須使用舊資料。

## 讀取對應和完整申請

以下是網站後端使用的應用管理 API，使用連接應用的伺服器金鑰。它不是參加者填表金鑰，絕不可放到瀏覽器、iframe 或公開代碼中。參加者仍用[流程接入 API](workflow-ui.md)及私人憑證，只能操作自己的申請。

```javascript
const base = 'https://registration.konnec.ai/api/apps/v1';
const headers = { Authorization: `Bearer ${process.env.OPENDASHBOARD_APP_KEY}` };
// version 選填；省略時取最新發佈版本。同步申請時應使用該申請的固定版本。
async function read(path) {
  const response = await fetch(base + path, { headers, redirect: 'error', signal: AbortSignal.timeout(15000) });
  const data = await response.json();
  if (!response.ok) throw Object.assign(new Error(data.message || 'Read refused'), { code: data.error });
  return data;
}
const bindings = await read('/workflows/wfl_example/bindings?version=1');
const snapshot = await read('/workflow-runs/wru_example/export');
```

申請回傳 application_id、workflow_id、version、revision、external_id、status、current、updated_at 及 forms。每份表單有 step_id、form_id、source 和 items；items 保留答案、提交狀態、附件 ID 及回覆編號。未填寫的步驟 items 為空；draft 不能當作已提交。附件取得方式見下節，通知不傳相片或公開下載網址。

所有讀取限制在金鑰所屬應用及正式／測試環境。跨應用、跨環境或不存在的申請回傳 404。私人填表連結和流程填表金鑰不提供這個完整匯出端點。

## 同步附件與初次補齊

export 不包含參與人的 actor。若使用 `/workflow-runs/:id/files/:uploadId?actor=:ref`，ref 必須是建立申請時由可信後端分配、且有權查看該附件的參與人；缺少 actor 會回 invalid_actor。流程填表 API 開始的申請由平台建立參與人，不能猜 actor 或拿 application_id 代替。

已有管理金鑰的接收程序可用 `GET /api/apps/v1/uploads/:uploadId` 讀取同應用／模式的已完成私人附件，回傳 `url`、`sha256`、`size`、`content_type`。只下載本次受信任 export 答案實際引用的 upload id，核對 bytes 與摘要、大小及格式，再寫入本地副本。此廣泛應用附件端點不是限一宗申請的接口，也不被 publication.data_sync 自動撤銷；不可把它代理成任意訪客可輸入 upload id 的下載接口。

如果需要使用限申請的 files 端點，先採用有明確參與人對應的管理 API 建立方式，見[流程管理 API](workflows.md)。填表者自己下載則使用流程金鑰＋私人憑證的 `?action=file`，不需要 actor。兩種下載不可混用。

初次同步用 `GET /workflows/:id/runs?offset=0&limit=25`，讀取回傳 runs 的 id，逐宗取得 export；帶 next_offset 讀下一頁，直到 null。名單不是整批一致快照：同時開啟持久通知收件，再用 revision 去重並於完成後對帳。不能只處理第一次 25 筆，亦不可把 offset 當成永久增量游標。

## 同一宗申請不重複建立

外站用應用 API 建立申請時，對同一流程重用相同 external_id、respondent、participants 和 version。建議首次明確指定 version 並保存原請求；省略時首次採用最新發佈版本，已存在的 external_id 重試則沿用原版本。相同請求重試會返回同一個 OpenDashboard 編號；相同 external_id 卻不同內容會拒絕。external_id 的唯一範圍是該流程，不能拿 email 當跨網站認領憑證。

用公開入口或自訂 UI 填表 API 時，開始後保存回傳的申請編號和私人憑證，之後沿用它們。每次重新產生憑證並開始報名會建立新申請。跨裝置續填須使用既有私人連結；系統不會只憑姓名或電郵合併隊伍。

答案更新沿用既有 operation_id 和 revision 規則：網絡失敗時重試完全相同的操作，不另產生新 operation_id。版本衝突先讀取最新申請，再由使用者確認要提交的內容。

## 通知內容

每次建立申請或成功保存、提交、補件、審核和推進步驟，都會在同一個資料庫交易中建立通知。通知不是畫布的「站內通知」節點，也不是電郵。操作重試不會建立第二筆相同版本通知。

```json
{
  "id": "wfe_wru_example_2",
  "type": "workflow.application.changed",
  "app_id": "example_app",
  "test": true,
  "workflow_id": "wfl_example",
  "application_id": "wru_example",
  "version": 1,
  "revision": 2,
  "occurred_at": "2026-10-11T13:00:00+00:00"
}
```

沒有答案、隊員資料、私人憑證或附件內容。接收者按 application_id 讀取最新申請。這是最新狀態同步：若收到 revision 2 通知時已更新至 4，讀取可返回 4，不保證取回每次變更當下的完整答案快照。

## 驗證簽章

標頭包括 X-OpenDashboard-Event-Id、X-OpenDashboard-Timestamp 及 X-OpenDashboard-Signature。簽章是 HMAC-SHA256(secret, timestamp + '.' + 原始請求 body) 的十六進位值。驗證前不要重新 JSON.stringify body。

```javascript
import crypto from 'node:crypto';
function verify(rawBody, timestamp, signature, secret) {
  if (!Buffer.isBuffer(rawBody) || !/^\d+$/.test(timestamp || '') ||
      Math.abs(Date.now() / 1000 - Number(timestamp)) > 300 ||
      !/^[a-f0-9]{64}$/.test(signature || '')) return false;
  const expected = crypto.createHmac('sha256', secret)
    .update(timestamp).update('.').update(rawBody).digest();
  return crypto.timingSafeEqual(expected, Buffer.from(signature, 'hex'));
}
```

可直接使用[後端範例](workflow-client.mjs)中的 verifyWorkflowNotice，輸入原始 Buffer、三個標頭、私密簽章密鑰及預期 app_id／workflow_id／test。簽章密鑰不是流程或應用 API 金鑰。

接收網站還須核對 type、app_id、test、workflow_id，以及標頭 event id 與 body.id 一致。將通知寫入有唯一 event id 的持久佇列後才回傳 2xx；不要只放入程序記憶體便確認成功。後台讀取申請、按 application_id 更新副本，只有較新的 revision 才覆蓋舊版本。處理副本寫入失敗，也要由接收網站自己的佇列重試。

## 重試和停用

通知會在 API 保存後嘗試傳送；每分鐘的背景工作只處理已產生的待送通知，不重新查詢外站或掃描全部答案。單次最多處理 5 筆，HTTP 最多等待 8 秒；非 2xx、轉址和網絡錯誤按退避間隔重試。最多 8 次後標示失敗，可在流程設定按「重試」。傳送採至少一次，不能假設每筆只到達一次或順序固定。

重試保持相同 event id 和原始 body，時間戳與簽章更新。若外站已保存但回應遺失，必須以 event id 去重。不要用 body 的 occurred_at 代替簽章時間戳。

更改網址、啟停或重新產生密鑰，會取消尚未送達的舊設定通知，不會把舊通知轉送到新網址。正在傳送中的請求仍可能到達。新設定只接收之後的變更；不自動補發歷史資料。需要初始副本時，可用流程 runs 分頁 API 列出申請，再逐宗讀取 export。

如果要更換接收網站，先準備新網站並完成資料核對，再更改設定。正式及測試流程分開設定；通知 test 欄位也必須驗證。


## 接駁驗收與開放報名

先完成 OpenDashboard 自己的填表路徑，再驗證外站副本：

1. 在同一流程建立報名、作品等表單步驟，發佈版本並保留入口為未開放。
2. 在獨立驗收流程使用合成資料測試草稿、必填、群組、附件、私人連結、重試和兩個步驟的固定申請編號。
3. 接收網站驗證簽章及流程範圍，先把通知可靠保存，再回應成功；背景工作讀取最新申請，只更新這宗申請對應的副本。
4. 核對重複通知不會多建資料，舊 revision 不會覆蓋新資料，附件屬於同一申請，外站自身管理的評分等欄位保持不變。
5. 確認兩邊成功後才設定正式日期及開放入口。新增外站活動不應自動切換正在使用的活動或改動既有報名。

「通知已送達」代表接收網址已回應 2xx。若外站使用背景工作，它不代表外站資料庫已更新；接收網站仍須保留待處理、完成、失敗及重試紀錄。OpenDashboard 的通知重試與接收網站的處理重試是兩項分開的責任。網站只把已提交項目交給後續業務使用，不能把草稿當成正式提交。

## 外站處理完成的回報

管理員在「流程 → 網站接入 → 資料通知」查看兩個不同結果：**通知已送達**代表通知端點接受；**網站處理完成**代表外站已保存需要的資料及附件。外站尚未回報時顯示等待確認，不能把送達當成資料已落地。

外站驗證通知、讀取最新 export，並完成其本地交易及附件保存後，使用同一應用的伺服器管理金鑰回報：

```http
POST /api/apps/v1/workflow-runs/<application-id>/processing-receipts
Authorization: Bearer <app-key>
Content-Type: application/json

{"delivery_id":"<notification-id>","revision":12,"status":"processed"}
```

失敗時回報 `status: "failed"` 並帶 `error`，僅接受 `attachments_invalid`、`source_unavailable` 或 `processing_failed`。不得上傳原始例外、檔名、答案或個人資料。`revision` 是實際處理的 export 版本，必須不早於通知版本且不晚於現有申請版本。回報只能針對同一應用、測試模式及申請的通知；參加者／流程填表金鑰不能回報。

重複回報安全；較舊或已處理成功的結果不能被失敗降級。回報失敗時外站應重試同一回報，不重建隊伍。管理員可在傳送紀錄對「需要處理」按重試，會重新傳送原通知；接收端仍須按通知 id 去重，並讓已失敗的工作可恢復，不能僅因曾收到便永久忽略。


## 網站保存資料副本 {#copy}

發佈前勾選「通知我的網站，讓兩邊都有資料」。這允許新版本的變更通知及同步讀取，不會直接啟用接收端。
在流程的「網站接入 → 資料通知」設定 HTTPS 接收網址、啟用通知並安全保存簽章密鑰。
外站後端按照本頁的簽章驗證 → 持久化通知 → 讀取 export 與 files → 以 application_id/revision 更新副本 → processing-receipts 回報結果接駁。

填表使用的流程金鑰沒有同步匯出權限。讀取副本沿用連接應用的伺服器端金鑰，其權限範圍是該應用；不要交給瀏覽器或不可信的第三方。
發佈選項不會把應用金鑰變成只讀或限單一流程的金鑰，也不會撤銷它其他既有管理權限。
若不願提供該應用的後端權限，先不要啟用副本接駁，改用 OpenDashboard 管理介面查看資料。

三種常見組合：OpenDashboard 頁面填表＋外站保存副本；自己的網站填表＋OpenDashboard 管理；兩邊填表＋外站保存副本。
所有組合都由 OpenDashboard 確認正式答案及流程進度；外站修改答案也必須經 OpenDashboard 提交，不可直接修改副本再當作已同步。
通知已送達和資料已保存是不同狀態。接駁驗收須檢查「處理完成」、附件可讀、失敗重試，以及相同通知不產生重複資料。
