jim800121chen b9c228df4f docs(autoflow): Phase 0.8b ADR-015 + TDD 修訂 — server-to-server 改 API key
新增 ADR-015:visionA → converter / FAA 從 OAuth client_credentials 改 pre-shared API key
- §1-§9 決策、4 個替代方案、後果分析、合規性
- §3.5 reference middleware snippet(Go converter + C# FAA 兩種寫法)+ 部署檢查清單
- 部分 supersede ADR-014 §5/§6/§7(service token / scope / MC retry rows)
- 觸發背景:Phase 0.8 stage e2e 撞 4 個 blocker,1:1 internal trust 用 OAuth client_credentials 過度設計

3 份 TDD 配合修訂:
- conversion.md:重寫 §3 服務間認證、§4.1 download 退回 server-side stream proxy、刪 §2.4 mc_token_client、§5.3 補 cancel 鏈、§10.3 改 pre-shared key 保護
- api-conversion.md:error code idp_unavailable → converter_auth_failed/faa_auth_failed;download response 從 302 redirect 改 200 + Content-Disposition: attachment + NEF stream
- oidc-tdd.md:標廢棄 service client env 兩 row、新增 API key env 兩 row、§13.1.3 user login 與 server-to-server 脫鉤說明、v0.2 changelog

未動:source code(步驟 2 由 backend agent 處理;範圍含 mc_token_client 刪除、TenantID 移除、API key 改造,含 test files)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 06:39:45 +08:00

359 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# API — Conversion轉檔功能Phase 0.8 / Phase 0.8b
> **base URL**`https://stage-9527.innovedus.com:9527/`stage / `http://localhost:3721`dev
> **Authuser → visionA**OIDC cookie session`visiona_session`),參見 `oidc-tdd.md` — 與 Phase 0.8 完全一致,未變
> **服務間認證visionA → converter / FAA****Phase 0.8b 已改為 pre-shared API key**(取代 OAuth client_credentials— 對 frontend 透明,不影響本 API 契約;詳見 `conversion.md` §3、[`adr/adr-015-server-to-server-api-key.md`](../adr/adr-015-server-to-server-api-key.md)
> **同層**`api/api-spec.md`(總覽)、`conversion.md`(內部設計)、`adr/adr-014-conversion-integration.md`(仍有效部分)、`adr/adr-015-server-to-server-api-key.md`Phase 0.8b 認證機制)
> **角色**:給 visionA-frontend 實作時的 API 契約
---
## 通用約定
| 項目 | 值 |
|------|-----|
| 通用回應格式 | `{ "success": true, "data": {...} }` / `{ "success": false, "error": {code, message, details?} }` |
| Auth | 走 cookiefrontend 用 `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_idbackend 會從 cookie 灌**
| Field | Type | 必填 | 說明 |
|-------|------|-----|------|
| `model` | file | ✓ | `.onnx` / `.tflite`,≤ 500MB |
| `ref_images[]` | file × N | — | 可 0100 張,每張 ≤ 10MB |
| `model_id` | text | ✓ | 165535使用者自訂編號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 | 提示「轉檔服務暫時無法使用」+ 重試按鈕 |
| 502 | `converter_auth_failed` | visionAPhase 0.8b 新增)| 同上文字 — frontend 看不出差別SRE 從 log 排查 API key 同步 |
| 503 | `service_busy` | 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_chipvisionA 既有 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 | `converter_auth_failed` | converter API key 不同步(運維事件)|
| 502 | `faa_unavailable` | FAA pull 失敗,可重試 |
| 502 | `faa_auth_failed` | FAA API key 不同步(運維事件)|
**冪等性**:對同一 `job_id` 重複呼叫;若已建過 model record回 200 + 既有 model 詳情(不重新建)。
---
## 4. `GET /api/conversion/{job_id}/download`
「下載」 — **Phase 0.8bvisionA-backend server-side stream proxy**(從 FAA pull NEF stream 後中轉回 browser。Phase 0.8 原本的「302 redirect to FAA delegated URL」設計因服務間認證改 API key 而退回 proxy 模式,詳見 [ADR-015](../adr/adr-015-server-to-server-api-key.md) §7、`conversion.md` §4.1。
對 frontend 而言**呼叫方式完全一致**`<a href>` / `window.location.href`),但 response 從「302 Location」變成「200 + NEF binary stream + Content-Disposition: attachment」。
### Request
```
GET /api/conversion/{job_id}/download HTTP/1.1
Cookie: visiona_session=...
```
無 query string、無 body。
### Response 200成功 — Phase 0.8b 變更)
```
HTTP/1.1 200 OK
Content-Type: application/octet-stream
Content-Length: 12345678
Content-Disposition: attachment; filename="yolov5s_kl720.nef"
Cache-Control: no-store, no-cache, must-revalidate, max-age=0
<NEF binary stream...>
```
browser 收到 `Content-Disposition: attachment` 自動觸發下載對話框 / 直接存到 Downloads。
### Frontend 使用方式(與 Phase 0.8 完全一致)
```html
<!-- 推薦anchor tagbrowser 自動處理 attachment download -->
<a href={`/api/conversion/${jobId}/download`} download>下載</a>
```
或:
```ts
// 程式化觸發
window.location.href = `/api/conversion/${jobId}/download`;
```
Frontend **不需處理 token、不需處理 redirect**`Content-Disposition: attachment` 觸發 browser 原生 download 行為。
**Phase 0.8b 不需要 FAA CORS**visionA backend → FAA 是 server-side 同進程 outbound HTTP call完全不適用 CORSCORS 只管 browser JS fetch / XHR。同源 endpoint + server-side stream + attachment header = 無 CORS 議題。
### 錯誤(依 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 | `converter_auth_failed` | converter API key 不同步運維事件frontend 不需區分)|
| 502 | `faa_unavailable` | FAA pull 失敗 |
| 502 | `faa_auth_failed` | FAA API key 不同步運維事件frontend 不需區分)|
**錯誤回應格式**:依 `Accept` header
- `Accept: application/json``{success:false, error:{code, message}}`
- `Accept: text/html`(一般 anchor 觸發) → HTML 錯誤頁browser 直接顯示
**注意**
- 每次「下載」按鈕都直接打 `/download` endpoint不要前端 cache 任何中間狀態
- Phase 0.8b 退回 server-side proxy 後visionA backend 變 streaming bottleneck — Phase 1 量大評估升級ADR-015 §7 選項 B
- 不會與 `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` shapewireframe §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=<sub>&status=in_progress` 並重建 ownershiplazy 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.<short-name>`
> **Phase 0.8b 變更**:移除 4 個 MC 相關錯誤碼(`download_token_failed` / `mc_token_unavailable` / `idp_unavailable` / `idp_misconfigured`)— 服務間認證取消 MC 依賴。新增 2 個 `*_auth_failed` 錯誤碼對應 API key 不同步的運維事件frontend 不需區分UX 文字仍是「服務暫時無法使用」)。
| 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` | 轉檔服務暫時無法使用 |
| `converter_auth_failed` | 502 | `conversion.error.converter_down` | 轉檔服務暫時無法使用Phase 0.8b 新增 — frontend 文字同 converter_unavailable|
| `faa_unavailable` | 502 | `conversion.error.faa_down` | 檔案存取服務暫時無法使用 |
| `faa_auth_failed` | 502 | `conversion.error.faa_down` | 檔案存取服務暫時無法使用Phase 0.8b 新增)|
| `service_busy` | 503 | `conversion.error.busy` | 系統繁忙,請稍後再試 |
**Phase 0.8b 移除(不會再出現的舊 code**
| 已移除 | 原 HTTP | 原語意 |
|------|---------|------|
| `idp_misconfigured` | 500 | MC token endpoint 4xx |
| `idp_unavailable` | 503 | MC token endpoint 5xx |
| `download_token_failed` | 502 | MC delegated token 4xx |
| `mc_token_unavailable` | 502 | MC 持續失敗 |
frontend i18n 字典可保留 `conversion.error.idp_down` / `conversion.error.token_failed` 兩個 key 暫不刪除(防舊版 client 拿到舊 error code 時還能翻譯),但新版 backend 已不再回這 4 個 code。
---
## 版本記錄
| 日期 | 版本 | 變更 |
|------|------|------|
| 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` patterntoken 不過 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 |
| 2026-05-11 | 0.4 | **Phase 0.8b** 對應 [ADR-015](../adr/adr-015-server-to-server-api-key.md)(1) Header / 文件 metadata 標示服務間認證改 pre-shared API key對 frontend 透明);(2) §4 download endpoint response 從「302 Location」改為「200 + NEF binary + `Content-Disposition: attachment`」— frontend 呼叫方式(`<a href>` / `window.location.href`)完全一致;(3) 錯誤碼總覽:移除 4 個 MC 相關 code`idp_misconfigured` / `idp_unavailable` / `download_token_failed` / `mc_token_unavailable`),新增 2 個 `*_auth_failed``converter_auth_failed` / `faa_auth_failed`)對應 API key 不同步的運維事件 |