# 專案（projects）

一個專案是一棵**有版本的檔案樹**，放「完成品與跨票共用的資訊」，例如定稿的人設圖、風格包、每集成品、要避開的題目清單。
草稿和中間產物仍然當票的流轉附件（見「附件」主題），只有定稿才寫進專案。

所有請求都要帶：

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

路徑裡的 `{code}` 可以填專案代號（例如 `lin-manman`），也可以填數字 id。

## 權限

權限**只看專案成員名單**，跟票目前在哪一站無關。等級由低到高：

| 等級 | 是誰 |
|---|---|
| `read`（可讀） | 成員 |
| `write`（可寫） | 成員 |
| `owner`（負責人） | 專案負責人，只有一位，不在成員名單裡 |
| `admin`（組織管理員） | 組織 admin，所有專案全權 |

每個動作至少要哪一級：

| 動作（`can` 的 key） | 最低等級 | 包含 |
|---|---|---|
| `view` | read | 讀專案資訊、列目錄、搜尋、讀檔與舊版 |
| `write_files` | write | 新增檔、寫新版、改檔案說明標籤、搬移、刪除／還原、建刪目錄 |
| `manage_members` | owner | 改成員名單、看完整成員名單 |
| `edit_settings` | owner | 改專案說明、一句話說明、標籤、保留規則 |
| `view_audit` | owner | 看稽核紀錄 |
| `rename` | owner | 改專案名稱 |
| `archive` | owner | 封存／解除封存 |
| `export` | owner | 匯出 zip |
| `transfer_owner` | admin | 轉交負責人 |
| `delete` | admin | 刪除專案 |
| `import` | admin | 匯入專案 |
| `create` | admin | 建立專案（組織層級；`can.create` 在每個專案都會回，值只看你是不是 admin） |

- **不要自己比等級**：`GET https://station-flow.staging.quanhox.com.tw/api/v1/projects` 與專案詳情都會回 `my_access`（你是哪一級）和 `can`（`{動作: true/false}`），要不要做某件事看 `can`。等級表之後可能調整，`can` 會跟著變。
- 不是成員、專案不存在、屬於別的組織：一律 **404**（刻意不區分，不洩漏專案是否存在）。
- 等級不夠：**403**，`message` 寫明需要哪一級（例如「此操作需要「負責人」以上權限」）。
- 專案已封存：寫檔與改專案設定（`write_files`、`edit_settings`、`rename`、`transfer_owner`）一律 **423**，讀取照常；這時 `can` 裡這幾項也是 false。

## 讀

| 端點 | 說明 |
|---|---|
| `GET https://station-flow.staging.quanhox.com.tw/api/v1/projects` | 我看得到的專案。`?status=active`（預設）／`archived`／`all` |
| `GET https://station-flow.staging.quanhox.com.tw/api/v1/projects/{code}` | 專案說明（`description`）、`tags`、保留規則、`member_count`、`my_access` |
| `GET https://station-flow.staging.quanhox.com.tw/api/v1/projects/{code}/files` | 列目錄。`?prefix=資料夾路徑` 只列那一層；`?recursive=1` 遞迴；`?deleted=1` 列已軟刪的檔 |
| `GET https://station-flow.staging.quanhox.com.tw/api/v1/projects/{code}/search` | 依檔名、說明、標籤找候選檔與目錄。`?q=關鍵字`、`?tag=標籤`（至少給一個）、`?limit=`（預設 10，最多 50），依 `score` 排序 |
| `GET https://station-flow.staging.quanhox.com.tw/api/v1/projects/{code}/files/by-path` | `?path=a/b.md` 依路徑找。檔案被搬走時仍回 200，多帶 `redirected_from`（舊路徑） |
| `GET https://station-flow.staging.quanhox.com.tw/api/v1/projects/{code}/files/{id}` | 檔案 metadata 與 `versions` 版本清單 |
| `GET https://station-flow.staging.quanhox.com.tw/api/v1/projects/{code}/files/{id}/content` | 讀內容，預設最新版；`?version=N` 讀舊版；`?inline=1` 預覽 |

列目錄的回應：`data` 是檔案陣列（`id`、`path`、`description`、`tags`、`current_version`、`size`、`mime_type`、`updated_at`、`updated_by`），`folders` 是子目錄路徑字串陣列，`folder_details` 帶每個目錄的 `readme_file_id` 與 `readme_summary`。

讀內容的回應 header **`X-File-Version`** 是這次讀到的版本號，**寫回時要帶這個值**。inline、Range、`nosniff` 的規則和流轉附件下載相同。舊版可能已依專案的保留規則刪除，此時回 404「版本不存在」。

### 建議的找檔順序

1. 先讀根目錄或目標目錄的 `README.md`（擺放規則）。
2. 用 `search` 依用途標籤或關鍵字找候選。
3. 讀 metadata 判斷要用哪一個。
4. 讀 content。

**記檔案 id，不要寫死路徑**：路徑會因為搬移、改名而變，檔案 id 不會變。找不到檔時不要讓票卡住，照站說明處理（例如在票上留言列出搜尋過的關鍵字）。

### md 裡的內嵌圖片

在網頁上預覽專案裡的 `.md` 檔時，`![說明](相對路徑)` 會顯示成專案裡的圖片：

- 相對路徑**以這個 md 檔所在的目錄為基準**解析，可用 `./`、`../`。例如 `docs/spec.md` 裡寫 `![流程圖](img/flow.png)`，對應專案內的 `docs/img/flow.png`。
- 圖片要**一起上傳到同一個專案**，路徑對得上才會顯示。
- 只顯示圖片（PNG、JPEG、GIF、WebP）。找不到檔、不是圖片、或路徑跑出專案根目錄時，顯示灰框「圖片不存在：路徑」。
- `http(s)://` 開頭或 `/` 開頭的網址不會改寫，照原樣顯示。
- 這只影響網頁預覽；用 API 讀內容拿到的仍是 md 原文。

## 寫（需要 write）

| 端點 | 說明 |
|---|---|
| `POST https://station-flow.staging.quanhox.com.tw/api/v1/projects/{code}/files` | 新增檔，回 201。路徑已存在回 409 |
| `POST https://station-flow.staging.quanhox.com.tw/api/v1/projects/{code}/files/{id}/content` | 寫新版，`base_version` 必填 |
| `PATCH https://station-flow.staging.quanhox.com.tw/api/v1/projects/{code}/files/{id}` | 只改 `description`／`tags`，不產生新版 |
| `POST https://station-flow.staging.quanhox.com.tw/api/v1/projects/{code}/files/{id}/move` | `{"path":"新路徑","base_version":N}`。id 不變，舊路徑留轉址 |
| `DELETE https://station-flow.staging.quanhox.com.tw/api/v1/projects/{code}/files/{id}` | 軟刪除 |
| `POST https://station-flow.staging.quanhox.com.tw/api/v1/projects/{code}/files/{id}/restore` | 還原軟刪除的檔；原路徑已被佔用回 409 |
| `POST https://station-flow.staging.quanhox.com.tw/api/v1/projects/{code}/folders` | `{"path":"…","readme":"…"}` 建目錄（上層自動補建）；帶 `readme` 會同時建 `README.md`。已存在回 409 |
| `DELETE https://station-flow.staging.quanhox.com.tw/api/v1/projects/{code}/folders` | `?path=…`，只能刪空目錄，否則 409 |
| `POST https://station-flow.staging.quanhox.com.tw/api/v1/projects/{code}/folders/move` | `{"from":"…","to":"…"}` 整個目錄搬移或改名；有任何衝突回 409，整批不搬 |

### 新增檔與寫新版的欄位

| 欄位 | 說明 |
|---|---|
| `path` | 只有新增時要給。1–512 字，用 `/` 分層；不可用 `/` 開頭或結尾，不可有空段、`.`、`..`、反斜線、控制字元；每段最長 128 字；大小寫有別 |
| `base_version` | 只有寫新版時要給，填你讀到的 `X-File-Version` |
| 內容（三選一） | multipart `file`（≤ 20 MB）；`upload_id`（大檔分段上傳）；`from_attachment_id`（同組織某張票的附件，做快照，不重複佔空間） |
| `description` | 選填，≤ 200 字 |
| `tags[]` | 選填，必須是專案 `tags` 裡已定義的標籤，否則 422 |
| `task_id` | 選填。帶了會記在這個版本上（哪張票寫的）；這張票必須綁在同一個專案，否則 422 |
| `note` | 選填，≤ 500 字，版本備註 |

- 用 multipart 傳檔**一定要用 POST**；寫新版的路由也接受 PUT，但只適用於 JSON body（`upload_id`、`from_attachment_id`）。
- 寫入的檔案若是文字類且 ≤ 5 MB，會先掃描是否含密鑰（API key、`sk-`、`AKIA…`、私鑰等），命中回 422「內容疑似包含密鑰」。密鑰不要放進專案。

### 版本號樂觀鎖

寫新版與搬移都要帶 `base_version`。如果它不是目前最新版，回 **409**：

```json
{"message":"檔案已被更新，請重讀最新版","current_version":5,"updated_by":12,"updated_at":"2026-10-30T02:15:00+00:00"}
```

收到 409：**重讀最新版 → 把你的改動合併進去 → 用新的版本號再寫一次**。不要直接拿 `current_version` 重送，那會蓋掉別人的改動。

```bash
# 讀最新版並記下版本號
curl -s -D headers.txt -o draft.md "https://station-flow.staging.quanhox.com.tw/api/v1/projects/lin-manman/files/12/content" \
  -H "Authorization: Bearer $SF_KEY" -H "Accept: application/json"
V=$(grep -i '^x-file-version' headers.txt | tr -dc 0-9)

# 改完寫回
curl -s -X POST "https://station-flow.staging.quanhox.com.tw/api/v1/projects/lin-manman/files/12/content" \
  -H "Authorization: Bearer $SF_KEY" -H "Accept: application/json" \
  -F base_version=$V -F task_id=230 -F "note=第 3 集定稿" -F "file=@draft.md"
```

成功回 `{"data":{"file":{…},"version":{"version":6,"size":…,"sha256":…,"task_id":230,"note":…}}}`。

### 大檔：分段上傳（> 20 MB，最大 2 GB）

1. `POST https://station-flow.staging.quanhox.com.tw/api/v1/uploads`，body `{"total_size":<bytes>,"sha256":"<選填>"}` → 回 `id`、`part_size`、`part_count`、`expires_at`。
2. 每一段 `PUT https://station-flow.staging.quanhox.com.tw/api/v1/uploads/{id}/parts/{n}`，body 是該段原始位元組，`n` 從 1 開始；除最後一段外每段大小必須剛好是 `part_size`。
3. `POST https://station-flow.staging.quanhox.com.tw/api/v1/uploads/{id}/complete` → 回 `upload_id`。
4. 用 `upload_id` 呼叫新增檔或寫新版。

- 只有建立這個 upload 的本人能用，別人一律 404。逾期（24 小時）回 410；放棄可 `DELETE https://station-flow.staging.quanhox.com.tw/api/v1/uploads/{id}`。
- 主機磁碟可用空間不足時，建立 upload 回 **507**，請稍後再試或通知組織管理員。

## 票與專案

- `GET https://station-flow.staging.quanhox.com.tw/api/v1/tasks/{id}`、`GET https://station-flow.staging.quanhox.com.tw/api/v1/me/tasks`、`GET https://station-flow.staging.quanhox.com.tw/api/v1/routines/{routineId}/tasks` 每張票都有 `project: {id, code, name, status}` 或 `null`。`null` 表示這張票沒有綁專案，照原本做法跑即可。
- **開票時綁**：`POST https://station-flow.staging.quanhox.com.tw/api/v1/tasks` 帶 `project`（代號或 id）。要開「衍生票」改帶 `from_task_id`，會沿用來源票的專案（來源票沒綁就不綁）。兩個都帶回 422。專案必須是 active，而且你至少有 read 權，否則 422「專案不存在、已封存或你沒有權限」。
- **事後改**：`PUT https://station-flow.staging.quanhox.com.tw/api/v1/tasks/{id}/project`，body `{"project":"lin-manman"}`。只有票在**開始站**、而且你屬於該站綁定的角色時可以改；不在角色回 403，不在開始站回 422。
- **出口必填專案**：流程設計者可在出口的 `required_data` 加 `{"type":"project","label":"專案"}`。票沒有專案或專案已封存時，走這個出口回 422，`missing[].type` 為 `project`。
- **專案檔附到票上**：流轉四端點與留言端點都收 `copy_project_files: [{"file_id":12,"version":3}]`（`version` 省略＝最新版），做成快照附件，附件帶 `copied_from_project_file`。你要有該專案的 read 權。
- **票附件存回專案**：`POST https://station-flow.staging.quanhox.com.tw/api/v1/projects/{code}/files` 帶 `from_attachment_id` ＋ `task_id`（＋ `path`）。

### 讀寫原則

一般站只讀。只有產出最終成品或跨票資訊的站（例如歸檔、交付）才寫回專案；寫入時順便更新所在目錄的 `README.md` 與檔案的 `description`，讓下一個讀的人找得到。


## 管理端點

各端點要的等級見上面的動作表：

- `POST https://station-flow.staging.quanhox.com.tw/api/v1/projects` 建立：只限組織 admin（其他人 403「只有組織管理員可以建立專案」）。`owner_user_id` 指定負責人，沒帶就是 admin 自己。`members` 不能包含負責人本人（422）。
- `PATCH https://station-flow.staging.quanhox.com.tw/api/v1/projects/{code}` 改設定：`name` 要 `rename`；`owner_user_id` 要 `transfer_owner`；description／summary／tags／retention 要 `edit_settings`；代號不可改。
- 轉交負責人（`PATCH` 帶 `owner_user_id`）：新負責人必須是組織成員，而且不能是現任負責人（422「新負責人不能是目前的負責人」，不會記 `owner_transfer`）。轉交只限 admin；轉交後原負責人自動以 `write` 留在成員名單，新負責人如果原本是成員就移出名單；稽核記一筆 `owner_transfer`。
- `PUT https://station-flow.staging.quanhox.com.tw/api/v1/projects/{code}/members` 整包覆寫成員名單，`access` 只能填 `read`／`write`（其他值含 `manage` 一律 422）；名單不能包含負責人本人（422）。
- `POST https://station-flow.staging.quanhox.com.tw/api/v1/projects/{code}/archive`／`POST https://station-flow.staging.quanhox.com.tw/api/v1/projects/{code}/unarchive`
- `GET https://station-flow.staging.quanhox.com.tw/api/v1/projects/{code}/export` 匯出 zip；`POST https://station-flow.staging.quanhox.com.tw/api/v1/projects/import` 匯入（只限 admin）
- `DELETE https://station-flow.staging.quanhox.com.tw/api/v1/projects/{code}`：只限 admin；必須先封存，且最後一次寫入後已匯出過，否則 422
- `GET https://station-flow.staging.quanhox.com.tw/api/v1/projects/{code}/audit` 稽核紀錄

## 錯誤碼

| HTTP | 意思 | 怎麼處理 |
|---|---|---|
| 403 | 等級不夠（`message` 寫明需要哪一級）、改票專案時不在目前站角色 | 先看 `can`；需要更高權限請專案負責人或組織管理員調整成員；不要借用別人的 key |
| 404 | 專案或檔案不存在、你不是成員、屬於別組織（刻意不區分）、指定版本已刪除 | 先 `GET https://station-flow.staging.quanhox.com.tw/api/v1/projects` 確認看得到這個專案 |
| 409 | `base_version` 過期、路徑已存在、目錄不是空的、搬移或還原衝突 | 重讀最新版再寫；路徑衝突就換名字或改用寫新版 |
| 410 | 分段上傳已逾期 | 重新開一個 upload |
| 422 | 欄位驗證失敗、轉交給現任負責人、成員名單含負責人、成員等級不是 read／write、標籤不在專案清單、內容疑似含密鑰、`task_id` 不是本專案的票、刪除前未封存或未匯出 | 依 `message`／`errors` 修正 |
| 423 | 專案已封存，檔案唯讀 | 讀照常；需要寫入請負責人或組織管理員解除封存 |
| 507 | 主機磁碟空間不足，拒收大檔上傳 | 稍後再試並通知組織管理員 |
