# API — Conversion(轉檔功能,Phase 0.8)
> **base URL**:`https://stage-9527.innovedus.com:9527/`(stage) / `http://localhost:3721`(dev)
> **Auth**:OIDC cookie session(`visiona_session`),參見 `oidc-tdd.md`
> **同層**:`api/api-spec.md`(總覽)、`conversion.md`(內部設計)、`adr/adr-014-conversion-integration.md`
> **角色**:給 visionA-frontend 實作時的 API 契約
---
## 通用約定
| 項目 | 值 |
|------|-----|
| 通用回應格式 | `{ "success": true, "data": {...} }` / `{ "success": false, "error": {code, message, details?} }` |
| Auth | 走 cookie;frontend 用 `credentials: "include"` |
| Request ID | header `X-Request-Id`(visionA-backend 沒收到會自動產生) |
| Content-Type | 除 `init` 用 `multipart/form-data` 外,其他 JSON |
---
## 1. `POST /api/conversion/init`
啟動轉檔 job — 把 multipart body streaming proxy 到 converter。
### Request
```
POST /api/conversion/init HTTP/1.1
Cookie: visiona_session=...
Content-Type: multipart/form-data; boundary=----xyz
```
multipart fields(**注意:不要帶 user_id,backend 會從 cookie 灌**):
| Field | Type | 必填 | 說明 |
|-------|------|-----|------|
| `model` | file | ✓ | `.onnx` / `.tflite`,≤ 500MB |
| `ref_images[]` | file × N | — | 可 0–100 張,每張 ≤ 10MB |
| `model_id` | text | ✓ | 1–65535,使用者自訂編號(converter 要求) |
| `version` | text | ✓ | 例 `v1.0.0` |
| `platform` | text | ✓ | `520` / `720` |
| `enable_evaluate` | text | — | `true`/`false`,預設 `false` |
| `enable_sim_fp` | text | — | 同上 |
| `enable_sim_fixed` | text | — | 同上 |
| `enable_sim_hw` | text | — | 同上 |
### Response 200
```json
{
"success": true,
"data": {
"job_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "running",
"stage": "onnx",
"progress": 0,
"stage_progress": 0,
"created_at": "2026-04-30T12:00:00Z",
"expires_at": "2026-05-07T12:00:00Z"
}
}
```
> `expires_at` = `created_at + 7d`(converter 7 天 GC 截止時間)。frontend 用於顯示倒數與切「已過期」狀態。詳見 `conversion.md` §2.6.2。
### 錯誤
| HTTP | code | 來源 | 處理建議 |
|------|------|-----|---------|
| 400 | `validation_failed` | converter | 顯示 details.fields |
| 401 | `unauthorized` | visionA | redirect `/login` |
| 409 | `active_job_exists` | visionA pre-check / converter | 顯示「你已有進行中任務」+ details.job |
| 413 | `payload_too_large` | converter | 提示檔案大小限制 |
| 502 | `converter_unavailable` | visionA | 提示「轉檔服務暫時無法使用」+ 重試按鈕 |
| 503 | `idp_unavailable` / `service_busy` | visionA / converter | 提示稍後重試 |
---
## 2. `GET /api/conversion/{job_id}`
查 job 狀態。Frontend 用 polling,建議間隔 2 秒。
### Response 200
```json
{
"success": true,
"data": {
"job_id": "550e8400-...",
"status": "running",
"stage": "bie",
"progress": 45,
"stage_progress": 60,
"created_at": "2026-04-30T12:00:00Z",
"updated_at": "2026-04-30T12:05:30Z",
"expires_at": "2026-05-07T12:00:00Z",
"source_filename": "yolov5s.onnx",
"target_chip": "720",
"error_code": null,
"error_message": null
}
}
```
`status` enum:`created` / `running` / `completed` / `failed`
`stage` enum:`onnx` / `bie` / `nef`
| 欄位 | 用途 |
|------|------|
| `expires_at` | `created_at + 7d`,frontend 顯示倒數 |
| `source_filename` | 原始檔名(顯示用,例 wireframe success card 「yolov5s.onnx → yolov5s_kl720.nef」)|
| `target_chip` | 從 init 時的 `platform` 欄回傳(`520` / `720` / `630` / `730`) |
### 錯誤
| HTTP | code | 處理 |
|------|------|-----|
| 403 | `forbidden` | job 不屬於當前 user |
| 404 | `not_found` | job_id 不存在 / 已過期 |
| 502 | `converter_unavailable` | 持續失敗 → 提示重試 |
### Polling 建議
- Frontend 收到 `status=running` → 2s 後再 poll
- `status=completed` / `failed` → 停止 polling
- 連續 5 次 5xx → 停止 polling 並顯示錯誤
---
## 3. `POST /api/conversion/{job_id}/promote-to-models`
「加到模型庫」 — 完整流程:promote → FAA pull → 寫進 visionA model store。
### Request
```json
POST /api/conversion/{job_id}/promote-to-models
Content-Type: application/json
{
"name": "yolov5s_kl720"
}
```
| Field | 必填 | 說明 |
|-------|-----|------|
| `name` | ✓ | 在 model 庫顯示的名字。Design Phase 0.8 wireframe §7.1 要求此欄位,預設 `{job.source_filename_stem}_{target_chip.lower()}` |
| `description` | — | (Phase 0.8 不送,留 Phase 1)— backend 接受但忽略;Phase 1 開放 |
> **與 Design 對齊(議題 #4)**:Phase 0.8 wireframe §7.1 的 import Dialog **只有名稱欄位**(不含描述);backend Phase 0.8 也只用 `name`,`description` 雖在 schema 內但不顯示給使用者填寫。Phase 1 Design 開放描述欄位時 backend 已 ready,無需改 API。
### Response 201
```json
{
"success": true,
"data": {
"model_id": "abc-123",
"source": "converted",
"source_job_id": "550e8400-...",
"name": "YOLOv5 Face KL520",
"target_chip": "kl520",
"file_size": 12345678,
"status": "ready",
"created_at": "2026-04-30T12:30:00Z"
}
}
```
> **格式註記**:這個 response 是既有 `internal/model.Model` schema(沿用),其 `target_chip` 用 `"kl520"` 小寫格式。
> 跟 §2 / §5 conversion job 的 `target_chip` 用 `"720"`(converter `platform` enum)**不同欄位、不同來源**:
> - conversion job:來自 converter scheduler 的 `platform` 欄位(`"520"` / `"630"` / `"720"` / `"730"`)
> - model.target_chip:visionA 既有 model schema(`"kl520"` / `"kl720"` / etc)
>
> visionA-frontend 統一 normalize 成 UI 內部形式 `KL520` / `KL720` 顯示(見 `lib/api/conversion.ts` `normalizeTargetChip`)。
> Phase 1 評估是否值得在 backend 把兩邊統一(可能影響既有 model store 多處 caller,動範圍大)。
### 錯誤
| HTTP | code | 處理 |
|------|------|-----|
| 403 | `forbidden` | 不是該 user 的 job |
| 404 | `not_found` | job_id 不存在 |
| 409 | `job_not_completed` | job 還沒 completed,不能 promote |
| 502 | `converter_unavailable` | promote 失敗,可重試 |
| 502 | `faa_unavailable` | FAA pull 失敗,可重試 |
**冪等性**:對同一 `job_id` 重複呼叫;若已建過 model record,回 200 + 既有 model 詳情(不重新建)。
---
## 4. `GET /api/conversion/{job_id}/download`
「下載」 — visionA-backend server-side HTTP 302 redirect 到 FAA delegated URL。**Token 永遠不過 frontend JS**。
仿 FAA TestSite `DownloadFileDirect`(`FileAccessAgent.TestSite/Controllers/HomeController.cs:255-282`)pattern。
### Request
```
GET /api/conversion/{job_id}/download HTTP/1.1
Cookie: visiona_session=...
```
無 query string、無 body。
### Response 302(成功)
```
HTTP/1.1 302 Found
Location: http://192.168.0.130:5081/files/jobs/550e8400-.../result.nef?access_token=opaque-token-xxx
Cache-Control: no-store, no-cache, must-revalidate, max-age=0
Pragma: no-cache
```
browser 自動 follow Location,直連 FAA 下載 NEF。
### Frontend 使用方式
```html
下載
```
或:
```ts
// 程式化觸發
window.location.href = `/api/conversion/${jobId}/download`;
```
Frontend **不需要也看不到** download URL / token / object_key — 全在 server-side + browser navigation 中流轉。
**為什麼不需要 FAA CORS**:browser navigation request(包含 `` click 與 `window.location.href`)不適用 CORS;CORS 只管 JS 發起的 fetch / XHR。Server-side 302 redirect + 同源 endpoint 完全在 CORS 範圍外。
### 錯誤(不 redirect,依 Accept header 回 JSON 或 HTML 錯誤頁)
| HTTP | code | 處理 |
|------|------|-----|
| 401 | `unauthorized` | 沒登入;redirect /login(前端攔截)|
| 403 | `forbidden` | 不是該 user 的 job |
| 404 | `not_found` | job_id 不存在 / 已過期 |
| 409 | `job_not_completed` | job 還沒 completed,不能下載 |
| 502 | `converter_unavailable` | promote 失敗(首次下載且尚未 promote 過時可能發生)|
| 502 | `mc_token_unavailable` / `download_token_failed` | MC 換 delegated token 失敗,提示重試 |
**錯誤回應格式**:依 `Accept` header:
- `Accept: application/json` → `{success:false, error:{code, message}}`
- `Accept: text/html`(一般 anchor 觸發) → HTML 錯誤頁;browser 直接顯示
**注意**:
- 每次「下載」按鈕都直接打 `/download` endpoint,不要前端 cache 任何中間狀態
- Token TTL 短(5 分鐘預設),不過反正 frontend 也碰不到 token
- 不會與 `promote-to-models` 衝突;兩者內部都會 ensurePromoted(冪等),兩條路徑都拿同一個 target_object_key
---
## 5. `GET /api/conversion/active`
查當前 user 是否有 active job — 給 frontend 在跳出「上傳」UI 前 pre-check。
### Response 200(有 active)
```json
{
"success": true,
"data": {
"has_active": true,
"job": {
"job_id": "550e8400-...",
"status": "running",
"stage": "bie",
"progress": 45,
"created_at": "2026-04-30T12:00:00Z",
"expires_at": "2026-05-07T12:00:00Z",
"source_filename": "yolov5s.onnx",
"target_chip": "720"
}
}
}
```
> 此 endpoint 與 `GET /api/conversion/{job_id}` 回傳同一個 `Job` shape;wireframe §3.3、flow-conversion.md §5.1 依賴此 shape 做「進入頁面就直接落 processing 畫面」的恢復邏輯。
**重啟恢復行為(Phase 0.8 強化)**:當 visionA-backend 重啟導致 in-memory ownership 丟失時,此 endpoint 會 fallback 對 converter 查 `GET /api/v1/jobs?user_id=&status=in_progress` 並重建 ownership(lazy rebuild)。對 frontend 完全透明(同樣 endpoint、同樣 response shape)。詳見 `conversion.md` §2.6.1。
### Response 200(無 active)
```json
{
"success": true,
"data": {
"has_active": false,
"job": null
}
}
```
### 用法
Frontend 在「轉檔」入口的 `/conversion` 頁載入時打這個 endpoint:
- `has_active=true` → 顯示「你目前有進行中的任務」+ 跳轉到該 job 的進度頁
- `has_active=false` → 顯示上傳表單
---
## 錯誤碼總覽
對齊 `conversion.md` §6。前端 i18n key 統一 `conversion.error.`。
| code | HTTP | i18n key | 預設訊息(zh-TW) |
|------|------|----------|------------------|
| `validation_failed` | 400 | `conversion.error.validation` | 上傳的內容不符合要求 |
| `unauthorized` | 401 | `common.error.unauthorized` | 請先登入 |
| `forbidden` | 403 | `conversion.error.forbidden` | 你無權存取此任務 |
| `not_found` | 404 | `conversion.error.not_found` | 任務不存在 |
| `active_job_exists` | 409 | `conversion.error.active_job` | 你目前已有進行中的轉檔任務 |
| `job_not_completed` | 409 | `conversion.error.not_completed` | 任務尚未完成(`promote-to-models` 與 `download` 共用) |
| `payload_too_large` | 413 | `conversion.error.too_large` | 檔案超過大小限制 |
| `converter_unavailable` | 502 | `conversion.error.converter_down` | 轉檔服務暫時無法使用 |
| `faa_unavailable` | 502 | `conversion.error.faa_down` | 檔案存取服務暫時無法使用 |
| `download_token_failed` | 502 | `conversion.error.token_failed` | 無法取得下載授權(MC 4xx)|
| `mc_token_unavailable` | 502 | `conversion.error.token_failed` | 無法取得下載授權(MC 5xx / 持續失敗) |
| `idp_unavailable` | 503 | `conversion.error.idp_down` | 認證服務暫時無法使用 |
| `service_busy` | 503 | `conversion.error.busy` | 系統繁忙,請稍後再試 |
---
## 版本記錄
| 日期 | 版本 | 變更 |
|------|------|------|
| 2026-04-30 | 0.1 | 初稿(Phase 0.8 MVP 範圍) |
| 2026-04-30 | 0.2 | §4 download endpoint 從 `POST /{job}/download-token`(回 JSON `{download_url, expires_at}`)改為 `GET /{job}/download`(HTTP 302 redirect),仿 FAA TestSite `DownloadFileDirect` pattern;token 不過 frontend JS、不需 FAA CORS;`job_not_completed` HTTP code 從 400 改為 409 + 補 `mc_token_unavailable` |
| 2026-04-30 | 0.3 | Phase 0.8 三方交叉審閱回饋整合:Job response shape 補 `expires_at` / `source_filename` / `target_chip`(議題 #7);`/api/conversion/active` 行為文件化 lazy rebuild 機制(議題 #2 重啟恢復);`promote-to-models` request body 對齊 Design 單欄位(議題 #4,`description` 留 Phase 1) |