# 任務操作

本篇說明怎麼建立、查詢、推進任務，以及退回、異常、跳回、留言、認領、已讀與屬性。認證、回傳格式與錯誤碼見[總覽](https://station-flow.staging.quanhox.com.tw/agent/overview.md)。

以下範例都假設已設定：

```bash
export SF_KEY="<你的 API key>"
H=(-H "Authorization: Bearer $SF_KEY" -H "Accept: application/json")
J=(-H "Content-Type: application/json")
```

## 權限速查

「目前站角色」指：你在任務所屬 routine 裡，屬於任務**目前所在站**綁定的角色。任務換站後，權限跟著換到新站的角色。

| 動作 | 誰可以 | admin 可以略過嗎 |
|---|---|---|
| 讀任務、流轉紀錄、可走出口、跳回候選站、屬性 | 同組織都可以 | — |
| 建立任務 | 開始站的角色 | 不行 |
| 修改票面、推進、退回、送異常、跳回 | 目前站角色 | **不行**，admin 沒綁角色也是 403 |
| 留言 | 目前站角色，或曾處理過這張票的人，或經手過的站的角色 | 不行 |
| 認領（claim）、已讀、寫入屬性 | 目前站角色 | 可以 |
| 刪除任務 | 只有 admin | — |

角色不對時回 403，例如 `權限不足：你不屬於『主管審核』站綁定的角色`。換參數沒用，請組織管理者把你加進該站的角色。

## 建立任務

**別在正式業務的 routine 開測試票**，會通知並喚醒處理的人或 agent；測試請用測試 routine。怎麼找能開票的 routine 見[總覽](https://station-flow.staging.quanhox.com.tw/agent/overview.md)範例第 0 步。

回應的 `current_station_role_id`、`process_version` 說明見下方「查詢任務」。

`POST https://station-flow.staging.quanhox.com.tw/api/v1/tasks`

| 欄位 | 必填 | 說明 |
|---|---|---|
| `routine_id` | 是 | 任務屬於哪個 routine（`GET https://station-flow.staging.quanhox.com.tw/api/v1/routines` 查） |
| `name` | 否 | 任務名稱，最多 128 字 |
| `priority` | 否 | `high`／`medium`／`low` |
| `task_deadline_at` | 否 | 任務期限，日期時間字串 |

任務一律從流程的**開始站**起跑，無法指定起始站；要到中途站，建好後再一站站推進。

```bash
curl -s -X POST "https://station-flow.staging.quanhox.com.tw/api/v1/tasks" "${H[@]}" "${J[@]}" \
  -d '{"routine_id": 1, "name": "A 客戶報價單", "priority": "high"}'
```

回 201，`data` 內有新任務的 `id`。

修改票面（名稱、優先度、期限）用 `PATCH https://station-flow.staging.quanhox.com.tw/api/v1/tasks/{id}`，至少帶 `name`、`priority`、`task_deadline_at` 其中一個；這支不會換站。刪除任務用 `DELETE https://station-flow.staging.quanhox.com.tw/api/v1/tasks/{id}`（只有 admin）。

## 查詢任務

| 端點 | 回傳 |
|---|---|
| `GET https://station-flow.staging.quanhox.com.tw/api/v1/tasks/{id}` | 單一任務完整資料 |
| `GET https://station-flow.staging.quanhox.com.tw/api/v1/me/tasks` | 目前停在「你負責的站」的任務（跨所有 routine） |
| `GET https://station-flow.staging.quanhox.com.tw/api/v1/routines/{routineId}/tasks` | 某個 routine 的任務 |
| `GET https://station-flow.staging.quanhox.com.tw/api/v1/tasks` | 組織內全部任務，沒有篩選參數 |

`GET https://station-flow.staging.quanhox.com.tw/api/v1/tasks/{id}` 除了票面欄位，還有：

- `current_station_role_id`：目前站綁定的角色 id，沒綁是 null。你要屬於這個角色才能推進。
- `process_version`：開票當下的流程版本（之後改版不變）。
- `current_station`：`id`、`name`、`type`、`checklist`（這站要確認的項目，沒設是 null）、`entry_criteria`（進站條件，沒設是 null）、`execution_instructions`（站說明）、`process_common_instructions`（流程每站共通說明）。兩段說明都要讀，站說明較具體時以它為準。
- `process`、`routine`：所屬流程與 routine 的 `id`、`name`。
- `current_station_assignees`：目前站的負責角色與人員。
- `logs`：流轉紀錄（也可單獨打 `GET https://station-flow.staging.quanhox.com.tw/api/v1/tasks/{id}/logs`）。
- `claim`：目前誰認領中（見「認領」）。
- `can_comment`：你能不能在這張票留言。
- `rollback_target_station_name`：現在退回會退到哪一站；不能退回時是 null。
- 已讀欄位 `is_read`、`read_at`、`read_version`、`station_version`、`last_log_id`（見「已讀」）。

### 指派給我的任務：`/me/tasks`

這是 agent 撿工作的主要入口。它回傳「目前停在你所屬角色負責的站」的任務，一次涵蓋所有 routine。

```bash
curl -s -X GET "https://station-flow.staging.quanhox.com.tw/api/v1/me/tasks?actionable=1&unread=1" "${H[@]}"
```

| query 參數 | 作用 |
|---|---|
| `actionable=1` | 只回目前站還有出口可走的任務，排除停在結束、存查等終點站的票 |
| `unread=1` | 只回你還沒標已讀的任務 |
| `station_id=N` | 只回停在某站的任務 |
| `priority=high` | 依優先度篩選 |
| `for_user_id=N` | 只有 admin 有效，查某人的任務；一般成員帶了會被忽略，一律回自己的 |

每筆任務除了票面欄位，還有：

- `current_station`：`id`、`name`、`type`、`execution_instructions`、`process_common_instructions`、`next_action_template`（下一步建議文案）。
- `claim`、`is_claimed_by_me`：認領狀態。
- 已讀欄位 `is_read`、`station_version` 等。

`GET https://station-flow.staging.quanhox.com.tw/api/v1/routines/{routineId}/tasks` 支援 `station_id`、`priority`、`for_user_id`。一般成員一律只回自己負責站上的票；admin 不帶 `for_user_id` 時回該 routine 全部任務。這支不含 `current_station` 物件。

## 推進（normal transition）

先查目前站有哪些出口，再選一個走。

`GET https://station-flow.staging.quanhox.com.tw/api/v1/tasks/{id}/transitions`：每筆有 `id`、`name`、`transition_type`、`next_station_id`、`to_station_id`（與 `next_station_id` 相同，兩個欄名都有）、`to_station_name`、`tone`（`positive`／`negative`）、`is_return`（`tone=negative` 的一般出口，也就是「退件」類出口時為 true）、`comment_required`、`required_data`。這裡只列一般出口；退回、異常、跳回每站都有，不會出現在這份清單。

`POST https://station-flow.staging.quanhox.com.tw/api/v1/tasks/{id}/transition`

| 欄位 | 必填 | 說明 |
|---|---|---|
| `transition_id` | 是 | 必須是目前站的出口 |
| `comment` | 視出口 | 出口的 `comment_required` 為 true 時必填 |

```bash
curl -s -X GET "https://station-flow.staging.quanhox.com.tw/api/v1/tasks/57/transitions" "${H[@]}"
curl -s -X POST "https://station-flow.staging.quanhox.com.tw/api/v1/tasks/57/transition" "${H[@]}" "${J[@]}" \
  -d '{"transition_id": 21, "comment": "報價已試算完成"}'
```

常見 422：

- `此轉移不屬於目前工作站`：`transition_id` 不在目前站的出口清單。重新查一次再送。
- `『送審』需要填寫意見／原因`：這個出口要求 comment。
- `『送審』送出前缺少必要資料：…`：出口的 `required_data` 沒補齊。回應的 `missing` 陣列列出缺什麼：`type=attribute` 看 `key`，先用屬性端點寫入；`type=attachment` 要附上符合命名規則的檔案。若是 `送出前資料不完整`，代表屬性有值但格式不對（`reason=invalid_format`）。

推進時可以同時附檔，見[附件](https://station-flow.staging.quanhox.com.tw/agent/attachments.md)。推進成功後，任務的認領會自動解除。

「退件」出口（`tone=negative`）也是一般出口，照上面的方式走即可；它和下面的「退回」不同。

## 退回（rollback）

退回到**流轉紀錄裡的上一站**（任務是從哪一站來的，就回哪一站），不是流程圖上的上游站。`comment` 必填，請寫清楚哪裡不合格、要怎麼改。

`POST https://station-flow.staging.quanhox.com.tw/api/v1/tasks/{id}/rollback`

```bash
curl -s -X POST "https://station-flow.staging.quanhox.com.tw/api/v1/tasks/57/rollback" "${H[@]}" "${J[@]}" \
  -d '{"comment": "單價欄位缺小數點，請修正後再送"}'
```

不能退回的情況（422）：

- `開始站不可退回`
- `異常站不可退回，請用跳回（jumpback）指定目標站`
- `無法推導上一站（沒有歷史記錄）`

注意：連續退回兩次不會一路往前退，而是在兩站之間來回。要退到更前面的站，改用「送異常 → 跳回」。

## 異常（exception）

把任務送到流程的異常站，交給事務負責人處理。適用於要跳好幾站回去、要撤回，或你判斷不了該怎麼走的情況。`comment` 必填。

`POST https://station-flow.staging.quanhox.com.tw/api/v1/tasks/{id}/exception`

```bash
curl -s -X POST "https://station-flow.staging.quanhox.com.tw/api/v1/tasks/57/exception" "${H[@]}" "${J[@]}" \
  -d '{"comment": "客戶改需求，需重新估價，請負責人決定退回哪一站"}'
```

422：`異常站不可再跳異常`、`此流程未設定異常站`。

## 跳回（jumpback）

只有任務停在異常站時可用，把任務送回同流程的任一非異常站。先查候選站，再跳。`target_station_id` 與 `comment` 都必填。

`GET https://station-flow.staging.quanhox.com.tw/api/v1/tasks/{id}/jumpback-candidates`：回 `[{id, name, type, sort_order}]`。任務不在異常站時回 422 `只有位於異常站的任務可以跳回`。候選清單不檢查權限，列出來不代表你有權跳。

`POST https://station-flow.staging.quanhox.com.tw/api/v1/tasks/{id}/jumpback`

```bash
curl -s -X GET "https://station-flow.staging.quanhox.com.tw/api/v1/tasks/57/jumpback-candidates" "${H[@]}"
curl -s -X POST "https://station-flow.staging.quanhox.com.tw/api/v1/tasks/57/jumpback" "${H[@]}" "${J[@]}" \
  -d '{"target_station_id": 10, "comment": "已和客戶確認新需求，回開始站重估"}'
```

`target_station_id` 不在候選清單時回 422。跳回需要你屬於異常站綁定的角色。

## 留言

在任務上留言，不換站。

`POST https://station-flow.staging.quanhox.com.tw/api/v1/tasks/{id}/comments`，body `{"comment": "..."}`，回 201。

```bash
curl -s -X POST "https://station-flow.staging.quanhox.com.tw/api/v1/tasks/57/comments" "${H[@]}" "${J[@]}" \
  -d '{"comment": "補充：客戶要求 11/5 前回覆"}'
```

留言的回應格式**和 `/logs` 的紀錄不同**：有 `station_name`（留言當下所在站），沒有 `from_station_name`／`to_station_name`／`transition_name`／`transition_tone`／`is_return`。同一筆在 `/logs` 裡則是前後站同名、`transition_name` 為「留言」、`transition_tone` 為 null：

```json
{"data": {"id": 301, "task_id": 57, "station_name": "主管審核", "transition_type": "comment",
          "comment": "補充：客戶要求 11/5 前回覆", "actor": {"id": 12, "name": "報價助理"},
          "attachments": [], "created_at": "2026-10-30T02:05:00.000000Z"}}
```

`/logs` 每筆的 `transition_tone` 是當時走的出口的 tone（`positive`／`negative`，非一般出口是 null）；`is_return` 為 true 代表這筆是走「退件」類出口（一般出口且 `tone=negative`），和退回（`transition_type=rollback`）是兩回事。

- 權限比推進寬：任務離開你的站之後，處理過它的人仍可以留言補正。可先看 `GET https://station-flow.staging.quanhox.com.tw/api/v1/tasks/{id}` 的 `can_comment`。
- 留言在流轉紀錄中是 `transition_type=comment`，不會讓任務變回未讀。
- 留言也可以附檔或複製別張票的附件，見[附件](https://station-flow.staging.quanhox.com.tw/agent/attachments.md)。

## 認領（claim）

`/me/tasks` 是無狀態的：同一個角色的多個 agent，或同一個 agent 的兩輪輪詢，可能撿到同一張票。認領是一把**建議性的軟鎖**，讓大家不要重複做同一件事。

| 端點 | 作用 | 回傳 |
|---|---|---|
| `GET https://station-flow.staging.quanhox.com.tw/api/v1/tasks/{id}/claim` | 讀目前認領狀態 | 200 `{"task_id", "claim"}` |
| `POST https://station-flow.staging.quanhox.com.tw/api/v1/tasks/{id}/claim` | 認領或續租 | 200 `{"task_id", "claim"}`；別人持有中回 409 |
| `DELETE https://station-flow.staging.quanhox.com.tw/api/v1/tasks/{id}/claim` | 釋放 | 204 |

`claim` 物件：`{"claimed_by", "claimed_by_name", "claimed_at", "expires_at"}`，沒有有效認領時四個欄位都是 null。

- 一次認領 30 分鐘。自己再 POST 一次就是續租，延長到期時間但保留原認領時間。
- 過期就等於沒人認領，別人可以直接搶，不需要先釋放。
- 別人持有中時 POST 回 409 `任務已被其他使用者 claim`，回應附上目前的 `claim`。
- 釋放別人有效的認領回 403 `無法釋放他人持有的 claim`（admin 例外）。
- 推進、退回、異常、跳回成功後，認領自動解除。
- 認領**不會**阻擋別人推進。真正的互斥靠推進本身：兩個人同時推進，只有一個會成功，另一個因為任務已經不在原站而失敗。

建議的撿票流程：

```bash
# 1. 撿「可推進、沒讀過」的票；跳過 claim 有效且 is_claimed_by_me=false 的
curl -s -X GET "https://station-flow.staging.quanhox.com.tw/api/v1/me/tasks?actionable=1&unread=1" "${H[@]}"
# 2. 認領，409 就換下一張
curl -s -X POST "https://station-flow.staging.quanhox.com.tw/api/v1/tasks/57/claim" "${H[@]}"
# 3. 做完推進；若決定不處理就釋放
curl -s -X DELETE "https://station-flow.staging.quanhox.com.tw/api/v1/tasks/57/claim" "${H[@]}"
```

## 已讀（read）

已讀狀態**只由你明確設定**，讀取任務不會自動標已讀。每個人對每張票各自一份。

- `station_version`：任務最後一筆非留言流轉紀錄的 id，沒有流轉過時為 0。每次換站、退回、異常、跳回都會前進，任務就對所有人自動變回未讀。純留言不會推進版本。
- `is_read`：你標已讀時的版本 ≥ 目前 `station_version`。
- `last_log_id`：含留言的最後一筆紀錄 id，可用來判斷有沒有新留言。

| 端點 | 作用 | 回傳 |
|---|---|---|
| `GET https://station-flow.staging.quanhox.com.tw/api/v1/tasks/{id}/read` | 讀已讀狀態，不會改變它 | 200 |
| `POST https://station-flow.staging.quanhox.com.tw/api/v1/tasks/{id}/read` | 標已讀；可帶 `{"version": N}` | 200；版本不符回 409 |
| `DELETE https://station-flow.staging.quanhox.com.tw/api/v1/tasks/{id}/read` | 設回未讀 | 204 |

```bash
# 這一站處理完但先不推進：帶撿票時看到的 station_version 標已讀
curl -s -X POST "https://station-flow.staging.quanhox.com.tw/api/v1/tasks/57/read" "${H[@]}" "${J[@]}" -d '{"version": 671}'
# 需要重看就設回未讀
curl -s -X DELETE "https://station-flow.staging.quanhox.com.tw/api/v1/tasks/57/read" "${H[@]}"
```

- 務必帶 `version`。若處理期間任務已經換站，會回 409 `版本已變動，請重新讀取任務`（附上最新 `station_version`），避免把你沒看過的新狀態標成已讀。
- 推進之後不必標已讀：任務到了新站本來就該是未讀，而且你多半已不屬於新站的角色（會 403）。
- 已讀和認領互相獨立，不需要先認領。

## 屬性（attributes）

任務上的 key-value 資料，用來存業務欄位（例如客戶名稱、金額）、或對應到其他系統的編號。出口的 `required_data` 也會檢查這裡的值。

| 端點 | 作用 | 回傳 |
|---|---|---|
| `GET https://station-flow.staging.quanhox.com.tw/api/v1/tasks/{id}/attributes` | 讀全部 | 200 `{"data": {"key": value, ...}}` |
| `GET https://station-flow.staging.quanhox.com.tw/api/v1/tasks/{id}/attributes/{key}` | 讀一個 | 200 `{"data": {"key", "value"}}`；不存在回 404 `屬性不存在` |
| `PUT https://station-flow.staging.quanhox.com.tw/api/v1/tasks/{id}/attributes/{key}` | 新增或更新，body `{"value": <任意 JSON>}` | 新增 201、更新 200 |
| `DELETE https://station-flow.staging.quanhox.com.tw/api/v1/tasks/{id}/attributes/{key}` | 刪除 | 200 `{"message": "已刪除"}` |

```bash
curl -s -X PUT "https://station-flow.staging.quanhox.com.tw/api/v1/tasks/57/attributes/customer_name" "${H[@]}" "${J[@]}" \
  -d '{"value": "A 公司"}'
curl -s -X PUT "https://station-flow.staging.quanhox.com.tw/api/v1/tasks/57/attributes/quote" "${H[@]}" "${J[@]}" \
  -d '{"value": {"amount": 120000, "currency": "TWD"}}'
curl -s -X GET "https://station-flow.staging.quanhox.com.tw/api/v1/tasks/57/attributes" "${H[@]}"
curl -s -X DELETE "https://station-flow.staging.quanhox.com.tw/api/v1/tasks/57/attributes/quote" "${H[@]}"
```

規則：

- key 只能用小寫英數、底線、點、連字號，長度 1–64（`^[a-z0-9_.-]{1,64}$`），否則 422。
- value 可以是字串、數字、物件或陣列；序列化後單一 key 不得超過 8KB，否則 422。沒帶 `value` 回 422 `缺少 value 欄位`。
- 讀取：同組織都可以讀，任務離開你的站後仍讀得到你寫過的值。
- 寫入與刪除：需要目前站角色（admin 可略過），否則 403。
