新增 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>
15 KiB
API — Conversion(轉檔功能,Phase 0.8 / Phase 0.8b)
base URL:
https://stage-9527.innovedus.com:9527/(stage) /http://localhost:3721(dev) Auth(user → 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同層: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 | 走 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
{
"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 |
visionA(Phase 0.8b 新增) | 同上文字 — frontend 看不出差別;SRE 從 log 排查 API key 同步 |
| 503 | service_busy |
converter | 提示稍後重試 |
2. GET /api/conversion/{job_id}
查 job 狀態。Frontend 用 polling,建議間隔 2 秒。
Response 200
{
"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
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
{
"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.Modelschema(沿用),其target_chip用"kl520"小寫格式。 跟 §2 / §5 conversion job 的target_chip用"720"(converterplatformenum)不同欄位、不同來源:
- 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.tsnormalizeTargetChip)。 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.8b:visionA-backend server-side stream proxy(從 FAA pull NEF stream 後中轉回 browser)。Phase 0.8 原本的「302 redirect to FAA delegated URL」設計因服務間認證改 API key 而退回 proxy 模式,詳見 ADR-015 §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 完全一致)
<!-- 推薦:anchor tag,browser 自動處理 attachment download -->
<a href={`/api/conversion/${jobId}/download`} download>下載</a>
或:
// 程式化觸發
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,完全不適用 CORS(CORS 只管 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 直接顯示
注意:
- 每次「下載」按鈕都直接打
/downloadendpoint,不要前端 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)
{
"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}回傳同一個Jobshape;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=<sub>&status=in_progress 並重建 ownership(lazy rebuild)。對 frontend 完全透明(同樣 endpoint、同樣 response shape)。詳見 conversion.md §2.6.1。
Response 200(無 active)
{
"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 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) |
| 2026-05-11 | 0.4 | Phase 0.8b 對應 ADR-015:(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 不同步的運維事件 |