# 多步流程：網站的 UI，OpenDashboard 的規則

**新報名網站請先使用[流程接入與自訂 UI](workflow-ui.md)**：在流程內啟用專屬 API，不需要應用管理金鑰。本頁保留既有應用 API 的詳細合約與進階用法。


你可以在「流程管理 → 報名流程」建立步驟及發佈版本。自己的網站讀取 JSON 後，按 `current_actions` 顯示操作；新增步驟、增加提交次數或調整條件，不必在網站寫死階段名稱。

此功能需先部署對應版本及完成流程資料庫遷移。舊競賽及普通表單 API 保持原有合約，**不會自動轉成自訂流程**。

外站 UI 和 OpenDashboard 私人流程頁可以共用同一 run、版本和規則，符合[三種填表／查看組合](integration-modes.md)的中央保存接法。免登入新報名可以使用 `/start/{slug}` 公開入口，系統建立 run 並提供 `/w/<token>` 私人連結。已有申請仍須經可信身份核對發連結；私人連結不等於公開新建入口。

## 1. 身份、資料和登入怎樣分工

| 工作 | 負責方 |
| --- | --- |
| 網站登入、確認使用者可管理哪宗申請 | 你的網站 |
| 表單、答案、目前步驟及審核歷史 | OpenDashboard |
| 角色、提交數量、期限及分支驗證 | OpenDashboard 伺服器 |
| 自己設計的 UI | 你的網站，根據流程 JSON 動態顯示 |
| 沒有登入系統的私人入口 | 指定參與人的 `/w/<token>` 連結 |

API 金鑰只保留在網站伺服器。瀏覽器不能直接持有應用金鑰。網站先驗證已登入使用者與申請的歸屬，再把該使用者的 `actor` 匿名代碼傳給 OpenDashboard。不能接受瀏覽器自行指定別人的 `actor`。

一宗申請可代表個人、隊伍或一般申請。同一聯絡人可以有多宗申請，共同管理的人各有代碼及角色；這些人不需要註冊 OpenDashboard 帳號。

## 2. 步驟設定及版本

| `type` | 功能 | 主要設定 |
| --- | --- | --- |
| `information` | 須知或參考資料，不產生回覆 | `blocks`、`placement`、`consent`、`next`；見[資訊頁](workflow-ui.md#information) |
| `form` | 填寫及提交 | `questions`、`roles`、`window`、`submission`、`next` |
| `review` | 人工審核、退回補件 | `source`、`roles`、`approve`、`reject`、`return_to` |
| `condition` | 按已提交答案分流 | `rule`、`yes`、`no` |
| `notification` | 私人頁內通知，然後繼續 | `role`、`message`、`next` |
| `complete` | 結束流程 | `outcome`: `completed`、`accepted` 或 `rejected` |

表單可使用短答、長答、數值、日期、選擇題、私人附件及重複群組。角色為 `participant`、`leader`、`teacher`、`reviewer` 或 `operator`；角色名稱不會自動授權，建立申請時須明確分配。主辦方操作另外檢查工作區權限。

`window` 可省略；省略代表不限時間。填寫時使用有時區的 ISO 時間，例如 `2026-12-01T09:00:00+08:00`。開始時間包含該時刻，截止時刻已不接受寫入。

`submission: {"mode":"multiple","min":1,"max":3}` 代表同一步可獨立提交 1–3 項；`single` 的 `max` 必須為 1。所有草稿須完成提交，才可 `continue`。`min:0` 允許不提交便繼續。

審核的 `source` 必須是每條進入路徑都已完成的表單；`return_to` 只可回到同一來源表單。條件目前比較單次表單的頂層欄位，支援 `eq`、`neq`、`gt`、`gte`、`lt`、`lte`、`contains` 及 `not_empty`。不執行任意程式、遠端 URL 或 JavaScript。

草稿可以暫時不連接全部步驟。發佈時會檢查起點、所有連接、終點、條件資料來源及循環；補件回路以外的循環會被拒絕。每份流程最多 50 步。

已發佈版本固定不變。直接用管理 API 建 run 時，省略 version 會選最新版本；公開入口與流程填表金鑰固定其入口版本。已有申請固定使用開始時的版本。修改不會把新要求偷偷加到已有申請；目前不提供執行中申請的版本遷移。

## 3. 完整例子：建立、發佈、開始申請

以下為合成例子。[下載完整流程 JSON](workflow-definition.json)。所有請求都從你自己的伺服器發出，沿用連接應用的測試／正式金鑰。

```js
const base = 'https://registration.konnec.ai/api/apps/v1';
async function call(path, method = 'GET', body) {
  const response = await fetch(base + path, {
    method,
    headers: {
      Authorization: `Bearer ${process.env.OPENDASHBOARD_API_KEY}`,
      'Content-Type': 'application/json'
    },
    ...(body ? { body: JSON.stringify(body) } : {})
  });
  const data = await response.json();
  if (!response.ok) throw Object.assign(new Error(data.message), { data });
  return data;
}
const definition = {
  title: '項目申請', participant_kind: 'team', start: 'registration',
  nodes: [
    {
      id: 'registration', type: 'form', title: '申請資料',
      roles: ['leader', 'teacher'],
      questions: [{ key: 'project_title', type: 'short_text', title: '項目名稱', required: true, personal: false }],
      submission: { mode: 'single', min: 1, max: 1 }, next: 'review'
    },
    { id: 'review', type: 'review', title: '審核', roles: ['operator'], source: 'registration', return_to: 'registration', approve: 'accepted', reject: 'rejected' },
    { id: 'accepted', type: 'complete', title: '通過', outcome: 'accepted' },
    { id: 'rejected', type: 'complete', title: '未通過', outcome: 'rejected' }
  ]
};
const workflow = await call('/workflows', 'POST', definition);
// 保存此 operation_id；網路失敗時使用相同 id 及相同請求重試。
const publication = await call(`/workflows/${workflow.id}/publish`, 'POST', {
  draft_revision: workflow.draft_revision, operation_id: crypto.randomUUID()
});
// external_id 是你的穩定申請代碼；重試時保持全部內容相同。
const run = await call(`/workflows/${workflow.id}/runs`, 'POST', {
  version: publication.version,
  external_id: 'application_0001', respondent: 'contact_0001',
  participants: [
    { ref: 'contact_0001', roles: ['leader'] },
    { ref: 'teacher_0001', roles: ['teacher'] },
    { ref: 'reviewer_0001', roles: ['operator'] }
  ]
});
```

如在管理介面建立及發佈，網站只需保存 `workflow.id`，不用透過 API 再建立一份。此管理 API 的 start 不會代替參加者確認須知；有前置資訊頁時，run 先停在資訊頁，顯示內容後以正常 continue 操作記錄確認。流程填表 API 則在 start 前完成須知確認，見[資訊頁](workflow-ui.md#information)。

`external_id`、`respondent`、`ref` 和 `operation_id` 用 4–64 個英文字母、數字、底線或連字號，不可使用姓名或電郵。

## 4. 自己的網站怎樣顯示及提交

```js
// 你的後端先驗證 contact_0001 可管理此 run。
let current = await call(`/workflow-runs/${run.id}?actor=contact_0001`);
const step = current.definition.nodes.find(n => n.id === current.state.current);
// step.questions 是欄位 JSON；使用與普通表單相同的欄位型別及 key。
// 用 current.current_actions 決定是否顯示儲存、提交、繼續等操作。
const submit = {
  actor: 'contact_0001', node: step.id, action: 'submit',
  revision: current.revision, operation_id: crypto.randomUUID(),
  answers: { project_title: '示範項目' }
};
const result = await call(`/workflow-runs/${run.id}/actions`, 'POST', submit);
// result.item 是此項目的 id；儲存草稿、編輯及重新提交時傳 item。
current = await call(`/workflow-runs/${run.id}?actor=contact_0001`);
await call(`/workflow-runs/${run.id}/actions`, 'POST', {
  actor: 'contact_0001', node: current.state.current, action: 'continue',
  revision: current.revision, operation_id: crypto.randomUUID()
});
```

每個成功操作都增加 `revision`。如兩人同時修改，只有符合目前版本的操作會成功；收到 `run_changed` 時，重新讀取，再讓使用者比較／重送，不能自動把舊答案覆蓋上去。

| `action` | 傳入欄位 | 用途 |
| --- | --- | --- |
| `save` | `answers`，已有項目另帶 `item` | 保存未完成草稿 |
| `submit` | `answers`，已有項目另帶 `item` | 驗證並提交項目 |
| `reopen` | `item` | 在目前表單步驟把已提交項目改回草稿 |
| `remove` | `item` | 移除草稿；歷史仍保留 |
| `continue` | 表單不需額外欄位；資訊頁可帶 `accepted` | 全部項目提交且達到最少數量後，進入下一步 |
| `approve` / `reject` | `message` 可選 | 審核角色決定下一步 |
| `return` | `message` 必填 | 退回來源表單補件，保留答案及歷史 |

每個操作必須帶 `actor`、`node`、`revision` 及 `operation_id`。私人連結的 API 由連結決定身份，不接受 `actor`。

網路失敗時用**原 `operation_id`、原 `revision`、原內容**重試。相同請求取得原結果；相同操作編號卻改了內容會收到 `operation_id_reused`。請保留尚未確認成功的請求，勿每次重試產生新編號。

需要多次提交時，第一次不傳 `item` 就建立新項目；保存回傳的 `item`，之後編輯同一項目必須傳入。達到 `max` 不可再新增。

## 5. 審核、補件及私人連結

```js
const review = await call(`/workflow-runs/${run.id}?actor=reviewer_0001`);
await call(`/workflow-runs/${run.id}/actions`, 'POST', {
  actor: 'reviewer_0001', node: review.state.current, action: 'return',
  message: '請補充項目的實作內容。',
  revision: review.revision, operation_id: crypto.randomUUID()
});
const link = await call(`/workflow-runs/${run.id}/links`, 'POST', {
  actor: 'contact_0001', expires_at: '2027-01-01T00:00:00+08:00'
});
// link.url 可由網站顯示或由主辦方自行轉交；此 API 不發送電郵。
// 網站伺服器撤銷：
await call(`/workflow-links/${link.id}`, 'DELETE');
```

私人連結只允許該人對該宗申請的角色操作。資料庫只保存 token 雜湊，網址只在建立時回傳；預設 30 日到期，可另指定未來到期時間。失效、撤銷或應用停用後不能讀寫。連結持有人即獲授權，不能把它當作公開報名網址。沒有自己登入系統時，主辦方或網站後端仍須先建立申請並分配參與人。

審核補件會把來源表單項目改回草稿，`attempt` 增加。舊答案及操作紀錄保留。參與人修改及重新提交後，仍需 `continue` 才再進入審核。尚待補件的草稿不會計入已提交資料表。

## 6. 附件、重複群組與儀表板

長答 `paragraph` 可設定 `max_length`，整數 1–100,000；省略時維持 5,000。
流程編輯器的長答問題及重複群組長答欄位都有「最多字元數」。此設定隨版本保存，
既有申請不會自動改用新限制。自訂 UI 應讀取該值並顯示字數；後端仍會拒絕超長內容，
不會自動截斷。計數與 JavaScript `string.length`／HTML `maxlength` 一致（UTF-16
碼元；部分 emoji 佔兩個或更多字元），空格與換行也計算。這不是整宗申請的容量限制；
整宗申請仍受原有大小上限約束。

流程的私人附件型別為 `file`，答案是已完成的 `apu_…` 上傳代碼，不能傳外部網址或競賽的 `upl_…` 代碼。需要應用已開啟實名模式。先沿用[私人上傳 API](reference.html)建立上傳、傳送位元組並完成驗證，再把代碼放到答案。儲存容量最大 50 MiB，始終屬個人資料。流程欄位採已發佈的 `upload.max_bytes`；未設定此限制的舊欄位仍為 20 MiB。提升容量不會改寫既有流程版本。

重複群組使用 `repeating_group` 及 `fields`，格式見[自己的填表介面](custom-ui.html)。同一組內可以有 `file` 欄位；所有附件都檢查應用及測試環境歸屬、大小及 digest。私人頁的附件下載網址短期有效，不應存入公開儀表板。

實名應用的原生流程可以用普通文字問題收集證件資料，包括重複群組中的隊員問題；請用 `short_text`／`text` 保留前置零及字母，並設定 `personal: true`。在流程表單編輯器中，對應操作是新增文字問題並勾選「個人資料」。這不會建立登入身份或授予申請存取權，網站後端仍須驗證使用者與申請的歸屬；個人資料不會進入公開報表。未實名的應用不能收集這些欄位，既有競賽 API 的欄位限制也不因此改變。

發佈回傳 `collection_id`；每個表單步驟有自己的 `form_id`。提交項目進入該合集，可在既有「連接應用」報告工作區匯入，或呼叫 `/collections/<collection_id>/tables`。繼續使用既有表中表分析，明確分開申請／提交項目及群組成員的計數。個人欄位和附件排除於報告資料表之外。

同一表單有多次提交時，合集 `submissions` 是**每個已提交項目**，不是申請數。申請數及各步狀態應讀取 `/workflows/<id>/runs` 的操作資料，在你的受保護 UI 分頁顯示；不要以提交項目數冒充申請數。

流程的表單不能透過普通 `/forms/<id>/responses` 提交，會收到 `workflow_form_use_run_api`。必須使用 run actions，才能檢查流程、身份、期限和提交數量。

## 7. 端點及常見問題

所有路徑以下列前綴開始：`/api/apps/v1`。

| 方法／路徑 | 回傳或用途 |
| --- | --- |
| `GET /workflows` | `workflows`、`next_offset` |
| `POST /workflows` | 建立草稿，回傳 `id`、`draft_revision` |
| `GET /workflows/:id` | `draft`、`draft_revision`、`published_version` |
| `PUT /workflows/:id` | `{definition, draft_revision}`，版本檢查後儲存 |
| `POST /workflows/:id/publish` | `{draft_revision, operation_id}` → `version`、`collection_id` |
| `GET /workflows/:id/versions` | 發佈紀錄及 `next_offset` |
| `GET /workflows/:id/versions/:version` | 該版本的 `definition`、`collection_id` |
| `POST /workflows/:id/runs` | `{external_id,respondent,participants,version?}` → `id`、`revision`、`version` |
| `GET /workflows/:id/runs` | `runs`、`next_offset` |
| `GET /workflows/:id/bindings?version=1` | 發佈版本與步驟／表單對應 |
| `GET /workflow-runs/:id/export` | 完整申請副本；須允許 data_sync |
| `POST /workflow-runs/:id/processing-receipts` | 通知處理結果；見[同步教學](workflow-sync.md) |
| `GET /workflow-runs/:id?actor=:ref` | `definition`、`state`、`revision`、`current_actions` |
| `POST /workflow-runs/:id/actions` | 操作結果含 `revision`、`current`、`status`、`item` |
| `GET /workflow-runs/:id/events?actor=:ref` | 操作摘要及 `next_offset` |
| `GET /workflow-runs/:id/links?actor=:ref` | 連結摘要，不含 token |
| `POST /workflow-runs/:id/links` | `{actor,expires_at?}` → `id`、`url`、`expires_at` |
| `DELETE /workflow-links/:id` | 撤銷連結 |
| `GET /workflow-runs/:id/files/:uploadId?actor=:ref` | 此申請附件的短期下載網址 |

列表帶 `offset=0&limit=25`，`limit` 最大 100。`next_offset:null` 表示已到最後一頁。私人入口的權限投影及 API 是平台內部使用；外部網站使用上表的應用 API。

**為何不能操作？** 查看 `current_actions` 是否有此動作，並確認角色分配、現在步驟及開放時間。只有 server 接受的操作才會改動資料，隱藏按鈕不是權限控制。

**修改流程是否影響已報名的人？** 不會。只有新申請使用新版本。查看 run 的 `version` 取得其表單及規則，不能用最新草稿渲染舊申請。

**`invalid_workflow` 怎樣處理？** 回傳 `fields` 以路徑指出問題，例如 `nodes.2.source`。先修正連接、來源、角色或欄位，再發佈。

**`workflow_unavailable` 是什麼？** 伺服器尚未完成流程資料庫遷移，不能開始收取申請。管理員須完成部署及資料庫更新。

**通知會發送電郵嗎？** 目前 `notification` 只在授權角色的私人頁顯示。郵件、短信和外部 webhook 不是此步驟的功能。

**資料是即時的嗎？** 成功操作直接保存到 OpenDashboard。畫面須重新讀取取得新進度；儀表板沿用開啟／重新整理時更新的方式，不是自動推送。

**網站一定要改用 OpenDashboard 的 UI 嗎？** 不需要。自訂 UI 與私人頁都使用同一套規則。網站也可以保存本地副本，再使用穩定代碼及 operation_id 同步；同步排隊和重試由網站自己的伺服器負責。

## 公開新報名入口：設定一次，使用同一套流程

管理員可在「流程管理 → 選擇流程 → 網站接入」建立及管理入口。網站後端也可使用以下 API；金鑰不可出現在瀏覽器：

```http
POST /api/apps/v1/workflow-entries
Authorization: Bearer YOUR_APP_KEY
Content-Type: application/json

{
  "id": "wen_demo_signup",
  "workflow_id": "wfl_demo",
  "version": 1,
  "slug": "demo-signup",
  "title": "示範活動報名",
  "description": "請先填寫隊伍資料，再提交作品。",
  "participant_role": "leader",
  "link_days": 90,
  "opens_at": null,
  "closes_at": null
}
```

先發佈 workflow，再填入實際回傳的 workflow_id 和 version。id 由接入方建立並保留；同 id、相同設定重試回傳原入口，變更設定則回傳 `entry_exists`。slug 全平台唯一，使用 3–80 個英文小寫、數字或連字號。入口初始 `enabled=false`、`revision=1`。

```http
PATCH /api/apps/v1/workflow-entries/wen_demo_signup
Authorization: Bearer YOUR_APP_KEY
Content-Type: application/json

{"enabled": true, "revision": 1}
```

回傳新 revision。完全相同的重試不重複變更；過期 revision 回傳 `entry_changed`。GET `/workflow-entries` 支援 `offset`、每頁 25 筆及 `next_offset`；GET `/workflow-entries/{id}` 讀取設定。測試金鑰只操作測試入口，正式金鑰只操作正式入口。

入口公開網址為 `/start/demo-signup`。網站可以直接放這個連結，不必實作匿名申請建立及 token 管理。公開頁有每小時每 IP 10 次新建上限；成功與未知結果的重試使用同一 token，不會重建申請。run 和私人連結在同一資料庫交易建立；連結建立失敗時不留下孤立申請。token 僅以雜湊保存，私人頁按角色限制資料及操作。

**匿名入口只適合單一隊長或參加者能完成所有填表步驟的流程。** `participant_role` 只能是 leader 或 participant；所有表單必須允許該角色，審核必須由 operator 執行，不能讓匿名報名者審核自己。需要多位指定老師或外部評審參與時，由可信後端使用原有 runs／links API 安排身份與角色。

入口固定使用已發佈版本。標題、網址、角色、時間、有效期和版本建立後不修改；需要新配置時建立新入口並停用舊入口。新流程版本不會暗中搬動既有申請。停收或截止只阻止新申請；既有私人連結仍依其有效期及各步驟期限運作。link_days 為 1–365 天，預設 30 天。應用停用、連結過期或被撤銷都會拒絕繼續存取。

### 兩個填表介面怎樣接同一宗申請？

公開入口只建立新申請，不使用姓名或電郵認領舊資料。外站登入用戶需要兩邊繼續修改時，最簡單的接法是由外站可信後端先建立 run、保存 user → run／actor 對應，再發該 run 的私人連結。外站 UI 用 workflow-runs 操作 API，OpenDashboard 私人頁也更新該 run；雙方都帶目前 revision，衝突時核對並重試。

從匿名公開入口開始的申請，不會自動連到外站帳戶。若後續要綁定，必須另做可信的歸屬核驗；不能僅憑客戶端傳來的 run_id 授權。若外站只是「開始報名」按鈕，直接跳轉公開入口便不需要這項帳戶綁定。

### 表單數量、資料及查看權限

每個表單步驟在發佈時產生一張版本專用表單。需要兩張表便設定「隊伍報名」「作品」兩個表單步驟；作品一、二放同一張作品表的欄位／分區。多個 UI 不增加表單或申請副本。來源 intake 則保留其原本綁定的表單，不混入 workflow 產生的表單。

管理入口設定需要 `edit_boards`；讀取申請狀態、答案及附件需要 `read_personal`。公開頁只回傳報名說明和開放狀態，不回傳隊伍名單。對外展示資料使用安全報表 API／報告連結，不能把管理員答案端點直接公開。


## 流程資料副本

外站需要接收 OpenDashboard 報名與作品資料時，使用[流程資料同步](workflow-sync.md)：核對表單與步驟對應、沿用固定申請編號，並在「網站接入」設定資料變更通知。

## 流程儀表板

在「流程管理 → 選取流程 → 儀表板」按「建立流程儀表板」，可建立初始統計，之後在同一分頁開啟已有儀表板。它沿用既有儀表板編輯器及工作區儲存，不需要外站再傳一份分析資料。建立後等候「已儲存」才離開；若有人同時編輯工作區，請重新載入後再試，系統不會覆蓋對方版本。

- **申請總覽**：每個 workflow run 一行，跨該流程的已發佈版本。尚未提交的申請仍在總數中；「已有表單提交」不代表報名完整。
- **步驟進度**：每宗申請的每個表單步驟一行，分開未填、草稿、部分提交、已提交及待補件。條件分支未經過的步驟不視為已提交。
- **提交明細**：每個正式提交項目一行。可多次提交的步驟可能有多行，不能把行數直接當隊伍數。
- **重複項目明細**：每個頂層重複群組項目一行，例如隊員；不是全流程的去重人數。附件內容不會進入分析。

申請透過不可反查的分析識別碼關聯；版本欄位可供圖表篩選。答案只包含其發佈版本允許分析的數字、日期、布林及簡單文字／選項欄位，姓名、聯絡資料、私人欄位及附件不作分析欄位。預設統計不需解除私人欄位設定。內層巢狀群組及多選展開目前不提供分析明細。

開啟工作區或按「更新流程資料」時，使用當前登入者的權限讀取。來源資料列不寫入儀表板工作區；只保存版面、欄位定義和指標。`read_personal` 允許查看，建立／編輯另外需要 `edit_boards`。外站填表金鑰不能讀取管理員的分析端點。既有對外分享仍使用確認後的固定快照，不提供即時流程資料；個人報告連結不能使用這類職員儀表板。

一般使用者毋須操作 API。管理介面使用登入保護的 `GET /api/admin-workflow-analytics?app=APP&workflow=WORKFLOW&test=0`，回傳 `workflow`、`versions`、`readAt`、`tables`；`view=summary` 只列出已關聯的儀表板。此端點不是 `/api/apps/v1` 的外站合約。每宗申請從一次已保存的狀態讀取；跨申請讀取期間仍可能有新提交。單次分析最多 10,000 明細列，超出時明確拒絕，不能將截斷結果當成全部統計。

這項功能不會建立參加者申請、不發佈流程、不改動公開入口或通知設定，也不遷移原站主導的既有接駁。舊式原站主導的接駁保持原有合集及同步；新建的中央流程使用標準流程儀表板。
