# 附件

Station Flow 的附件**一律掛在某一筆流轉紀錄（task log）上**，沒有獨立的「上傳附件到任務」動作。
要交檔，就在推進、退回、進異常、跳回或留言的那一次請求裡一起送檔案。

所有請求都要帶：

```
Authorization: Bearer <你的 API key>
Accept: application/json
```

## 哪些端點可以帶附件

| 端點 | 用途 | `comment` |
|---|---|---|
| `POST https://station-flow.staging.quanhox.com.tw/api/v1/tasks/{id}/transition` | 走一個出口推進 | 出口設了 `comment_required` 時必填 |
| `POST https://station-flow.staging.quanhox.com.tw/api/v1/tasks/{id}/rollback` | 退回上一站 | 必填 |
| `POST https://station-flow.staging.quanhox.com.tw/api/v1/tasks/{id}/exception` | 送進異常站 | 必填 |
| `POST https://station-flow.staging.quanhox.com.tw/api/v1/tasks/{id}/jumpback` | 從異常站跳回 | 必填 |
| `POST https://station-flow.staging.quanhox.com.tw/api/v1/tasks/{id}/comments` | 只留言、不推進 | 只帶 `files[]` 時必填；只帶複製參數時可省略 |

授權照各端點原本的規則：流轉要屬於目前站綁定的角色；留言比較寬，目前站角色或曾經處理過這張票的人都可以（`GET https://station-flow.staging.quanhox.com.tw/api/v1/tasks/{id}` 回的 `can_comment` 就是這個判斷結果）。

## 上傳檔案：multipart `files[]`

不帶檔案時，上面這些端點收一般 JSON body。要附檔就改送 `multipart/form-data`，原本的欄位（`transition_id`、`comment`、`target_station_id`…）照樣用 form 欄位送，檔案放 `files[]`：

- 一次 0–10 個檔，單檔 ≤ 20 MB。
- 超過會回 422，例如「附件最多 10 個」「第 N 個附件超過 20 MB」。
- 檔案和那筆流轉紀錄寫在同一個交易裡：流轉失敗（403、422）就不會留下任何檔案，也不會寫紀錄。

```bash
curl -s -X POST "https://station-flow.staging.quanhox.com.tw/api/v1/tasks/55/transition" \
  -H "Authorization: Bearer $SF_KEY" -H "Accept: application/json" \
  -F transition_id=12 \
  -F "comment=附上初稿與截圖" \
  -F "files[]=@draft.pdf" \
  -F "files[]=@screen.png"
```

- 檔案要附在 multipart 裡就一定要用 POST（這五個端點本來就是 POST）。
- 某些代理伺服器對 `Expect: 100-continue` 處理不好，大檔上傳卡住時可加 `-H "Expect:"`。

## 任務層附件端點已下線（410）

以下三個舊端點保留路徑，但一律回 **410 Gone**（就算沒帶認證也是 410）：

- `GET https://station-flow.staging.quanhox.com.tw/api/v1/tasks/{id}/attachments`
- `POST https://station-flow.staging.quanhox.com.tw/api/v1/tasks/{id}/attachments`
- `DELETE https://station-flow.staging.quanhox.com.tw/api/v1/tasks/{id}/attachments/{attachmentId}`

回應的 `message` 是「任務層附件已移除，請在流轉時附檔（files[]）…」，並指引你到流轉紀錄的 `attachments` 找附件。

附件屬於流轉歷史，**不能刪除、不能修改**。要交新版就在下一次流轉或留言時再附一次。

## 查看一張票有哪些附件

`GET https://station-flow.staging.quanhox.com.tw/api/v1/tasks/{id}/logs` 與 `GET https://station-flow.staging.quanhox.com.tw/api/v1/tasks/{id}` 的每一筆 log 都有 `attachments` 陣列（沒有就是空陣列）：

```json
{
  "id": 1498,
  "name": "draft.md",
  "size": 10240,
  "mime_type": "text/plain",
  "previewable": true,
  "copied_from_task_id": null,
  "copied_from_attachment_id": null,
  "copied_from_project_file": null,
  "sha256": "9f2c..."
}
```

- `mime_type` 由伺服器依**檔案內容**判斷，不是看副檔名。例如 `.md`、`.yaml`、短的 `.csv` 常被判成 `text/plain`。
- `sha256`：新上傳的檔都有；很舊的資料可能是 `null`。
- `copied_from_*`：這個附件是複製來的才有值，見下一節。

## 跨票複製附件

把別張票（或專案）的檔做成本筆 log 的附件。五個可帶附件的端點都收以下參數，JSON 或 multipart 都可以，也能和 `files[]` 一起用：

| 參數 | 說明 |
|---|---|
| `copy_attachment_ids[]` | 直接指定同組織任一張票的 attachment id |
| `copy_from_task_id` ＋ `copy_from_station_name` | 取來源票**最後一次離開該站**那筆流轉（不含留言）上的全部附件。站名可用流轉當時的名字或該站現在的名字 |
| `copy_from_task_id` ＋ `copy_from_station_id` | 同上，改用站 id 指定（不受改名影響；站必須屬於來源票的流程） |
| `copy_project_files` | 專案檔，`[{"file_id":12,"version":3}]`，`version` 省略就是最新版。詳見「專案」主題 |

規則：

- `copy_from_station_name` 與 `copy_from_station_id` 只能擇一；指定來源站時一定要有 `copy_from_task_id`，反之亦然。
- 幾組參數可以並用，同一個來源附件只算一次。合併後一次最多 100 個。
- 複製的檔**不受單檔 20 MB 限制**，也不重複佔空間。
- 這是**快照**：來源票之後交了新版，已複製的附件不會跟著變；來源票刪掉，複本仍可下載。
- 來源只要求同組織看得到；本票照原本的流轉／留言授權。
- 留言只帶複製參數時，`comment` 可省略，系統會寫「複製自 #來源票」。
- 來源 log 找到了但上面沒有附件時，複製 0 個，不會報錯。
- 如果目前出口設了「必附附件」，複製進來的檔和這次上傳的檔一樣算數。

以下情況回 **422**，整筆不寫 log、不留檔：

| `errors` 欄位 | 訊息 |
|---|---|
| `copy_attachment_ids` | 附件 #N 不存在或不屬於本組織 |
| `copy_from_task_id` | 來源任務 #N 不存在或不屬於本組織 |
| `copy_from_station_name` | 來源任務 #N 沒有離開『站名』的流轉紀錄 |
| `copy_from_station_id` | 來源任務 #N 的流程沒有站 id S ／ 沒有離開該站的流轉紀錄 |
| `copy_project_files` | 專案檔 #N 不存在或你沒有讀取權限 ／ 沒有第 V 版 |

範例：在票 #57 留言，帶入票 #42 最後一次離開「交付」站時附的所有檔：

```bash
curl -s -X POST "https://station-flow.staging.quanhox.com.tw/api/v1/tasks/57/comments" \
  -H "Authorization: Bearer $SF_KEY" -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{"copy_from_task_id":42,"copy_from_station_name":"交付"}'
```

複製後的附件帶 `copied_from_task_id`、`copied_from_attachment_id`（直接來源，來源刪除後數字仍保留）；從專案複製的則帶 `copied_from_project_file: {project_code, file_id, version}`。

## 下載與預覽

```
GET https://station-flow.staging.quanhox.com.tw/api/v1/tasks/{id}/attachments/{attachmentId}
```

- 只要同組織看得到這張票就能下載。
- **一定要用附件所在那張票的 id**。用 A 票的路徑抓 B 票的 attachment id 會回 404；複製來的附件請用本票路徑抓本票上的 attachment id。
- 預設回 `Content-Disposition: attachment`（下載），支援中文檔名。
- 加 `?inline=1`：只有 `previewable: true` 的檔會回 `inline`；其他型別（svg、html、zip…）一律仍是 attachment。
- 文字類檔案帶 `?inline=1` 時 `Content-Type` 一律是 `text/plain; charset=utf-8`；不帶 inline 時維持原 mime。
- 支援 HTTP Range：帶 `Range: bytes=0-1023` 回 206 與 `Content-Range`；範圍完全超出回 416。
- 所有下載都帶 `X-Content-Type-Options: nosniff`。

`previewable` 的判斷：

- 直接預覽：`image/png`、`image/jpeg`、`image/gif`、`image/webp`、`application/pdf`、`audio/mpeg`、`video/mp4`。
- 以純文字預覽：`text/plain`、`text/csv`、`text/markdown`、`application/json`、yaml 類 mime；另外副檔名是 txt/json/yaml/yml/md/markdown/csv/log、且內容被判成任何 `text/*` 的也算。
- 二進位檔不會因為副檔名改成 `.json` 就變成可預覽。

```bash
curl -s -o draft.md "https://station-flow.staging.quanhox.com.tw/api/v1/tasks/57/attachments/1498" \
  -H "Authorization: Bearer $SF_KEY" -H "Accept: application/json"
```

## 常見錯誤

| HTTP | 情況 | 怎麼處理 |
|---|---|---|
| 403 | 不屬於目前站角色（流轉）或沒有留言權 | 確認票目前在哪一站、你是否被指派 |
| 404 | 任務不存在、附件不屬於這張票、實體檔遺失 | 改用附件所在票的 id；先查 `/logs` |
| 410 | 打了已下線的任務層附件端點 | 改在流轉或留言時帶 `files[]` |
| 422 | 檔案太多／太大、comment 缺、複製來源找不到、出口必附資料不足 | 看 `errors` 與 `missing` 欄位修正後重送 |
