docs: migrate Autoflow shared documents to docs/autoflow/
Move PRD, design specs, architecture docs, and TDD from .autoflow/ (personal/per-branch layer) to docs/autoflow/ (shared layer that goes into git) per the new Autoflow workspace layout. Files moved: - 02-prd/PRD.md - 03-design/design-review.md - 03-design/user-flow-cross-system.md - 04-architecture/TDD.md - 04-architecture/design-doc.md - 04-architecture/security.md The originals were never tracked, so git mv reduced to a filesystem rename with no history to preserve. .autoflow/ remains for personal notes (progress.md, review reports, testing logs). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
7404ca9bc8
commit
cff9236699
1134
docs/autoflow/02-prd/PRD.md
Normal file
1134
docs/autoflow/02-prd/PRD.md
Normal file
File diff suppressed because it is too large
Load Diff
460
docs/autoflow/03-design/design-review.md
Normal file
460
docs/autoflow/03-design/design-review.md
Normal file
@ -0,0 +1,460 @@
|
|||||||
|
# Design Review — API 對外開放 L 級功能
|
||||||
|
|
||||||
|
> **審查範圍**:2026-04-25 的 L 級新功能「開放 Kneron Model Converter 轉檔能力為對外 REST API 供 VisionA 使用」。
|
||||||
|
>
|
||||||
|
> **審查者**:Design Agent(Autoflow)
|
||||||
|
>
|
||||||
|
> **審查時機**:三方聯合討論階段(與 PM Agent / Architect Agent 同步作業)
|
||||||
|
>
|
||||||
|
> **特別聲明**:這次任務的直接產出物是 **後端 API 介面**,不是新 UI 畫面。本次 Review 的焦點為:
|
||||||
|
> 1. 對既有 Web UI 使用者體驗的間接影響
|
||||||
|
> 2. API 設計對下游消費者(visionA-backend)能做出什麼樣的終端 UX 的限制
|
||||||
|
> 3. 從 UX 觀點給 PM / Architect 的建議
|
||||||
|
>
|
||||||
|
> **不在本次範圍**:新 UI 畫面設計、Wireframe、Prototype、Design Tokens 調整。這些本次都不需要做。
|
||||||
|
|
||||||
|
## 變更歷程
|
||||||
|
|
||||||
|
| 日期 | 變更內容 |
|
||||||
|
|------|---------|
|
||||||
|
| 2026-04-25 | 原始模型上傳路徑改為 visionA-backend → Converter multipart 直連(不經過 File Access Agent)。同步更新:(1)錯誤情境表移除「原始模型在 FAA 找不到 `input_object_not_found`」,改為 multipart 相關錯誤 `invalid_multipart` / `file_too_large`;(2)相依圖中 Converter 對 FAA 的依賴只保留 promote PUT,移除讀原始模型的箭頭;(3)衝突 response 範例保持不變(不受上傳路徑影響)。|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 摘要
|
||||||
|
|
||||||
|
### 1.1 整體評估結論
|
||||||
|
|
||||||
|
**Phase 1 對既有 Web UI 使用者體驗的影響:幾乎無感(✅ 優秀)**
|
||||||
|
|
||||||
|
- 既有 Web UI 走的 API 路徑(`/jobs`、`/jobs/:id`、`/jobs/:id/events`、`/jobs/:id/download/:filename`)**不改動**
|
||||||
|
- 對外 API 走全新路徑 `/api/v1/*`,兩套並存
|
||||||
|
- 既有 Web UI 使用者繼續以 multipart 上傳、SSE 追蹤、GET 下載,**流程零改動**
|
||||||
|
|
||||||
|
**主要 UX 風險:集中在「我們給 visionA 前端團隊的操作彈性」上(⚠️ 要注意)**
|
||||||
|
|
||||||
|
我們不實作 VisionA 的 UI,但 **我們的 API 設計會限制他們能做出什麼樣的體驗**。如果 API 設計得太死板,visionA 前端團隊就算想做更好的 UX 也做不到。本次 Review 會針對這點給 Architect 一份「API 設計的 UX 彈性需求」清單。
|
||||||
|
|
||||||
|
### 1.2 Phase 1 vs Phase 2 對使用者體驗的差異
|
||||||
|
|
||||||
|
| 情境 | Phase 1 體驗 | Phase 2 體驗 | UX 差距 |
|
||||||
|
|------|------------|------------|---------|
|
||||||
|
| VisionA 使用者上傳模型並轉檔 | ✅ 在 VisionA 平台內完成 | ✅ 同 Phase 1 | 無差 |
|
||||||
|
| VisionA 使用者把轉檔結果加進模型庫 | ✅ 按一下按鈕即完成 | ✅ 同 Phase 1 | 無差 |
|
||||||
|
| VisionA 使用者下載已轉檔的模型 | ⚠️ **阻塞 / 折衷方案** — VisionA 前端目前能做的:(a) 顯示「下載功能即將上線」占位;(b) 從 VisionA backend proxy 下載(違反原架構);(c) 導向舊的 Kneron Converter Web UI 下載(會混亂) | ✅ 瀏覽器直連 File Access Agent,速度快、省流量 | **中等** — 這是 UX 上的明顯缺口,必須在 Phase 1 上線時和 VisionA 產品團隊協調 messaging |
|
||||||
|
|
||||||
|
**Design Agent 的強烈建議**:Phase 1 上線時必須有「使用者下載 messaging 策略」,不能讓使用者上傳完轉檔後發現東西下載不了(UX 上的死胡同)。**這是需要使用者決策的議題 #1,詳見第 6 節。**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 既有 Web UI 影響分析
|
||||||
|
|
||||||
|
### 2.1 既有 Web UI 使用者旅程是否改變?
|
||||||
|
|
||||||
|
**結論:Phase 1 完全不改,Web UI 使用者無感。**
|
||||||
|
|
||||||
|
| 旅程階段 | 既有行為(會繼續保留)| 是否受影響 |
|
||||||
|
|---------|----------------------|----------|
|
||||||
|
| 進入 Web UI 主頁 | 直接打開網頁,不需登入([推測] 現況)| ❌ 不受影響 |
|
||||||
|
| 上傳模型(multipart)| 瀏覽器表單 → `POST /jobs` with FormData | ❌ 不受影響 |
|
||||||
|
| 追蹤進度(SSE)| 瀏覽器開 SSE 連線 `GET /jobs/:id/events`,自動 fallback 到每 3 秒 polling | ❌ 不受影響 |
|
||||||
|
| 下載結果 | 直接點 `GET /jobs/:id/download/:filename` | ❌ 不受影響 |
|
||||||
|
| 任務列表 | `GET /jobs` 列出全部 job | ❌ 不受影響(但查不到 VisionA 使用者透過新 API 建立的 job — **見 2.3**)|
|
||||||
|
|
||||||
|
**核心設計選擇(已由 PM 決策)**:
|
||||||
|
- 既有 `/jobs` 路徑保留,**不**加 OAuth 驗證
|
||||||
|
- 對外 API 走新路徑 `/api/v1/*`,**獨立**加 OAuth middleware
|
||||||
|
- 兩套 API 並存,互不影響
|
||||||
|
|
||||||
|
✅ **Design Agent 同意此決策**,理由:
|
||||||
|
1. 避免破壞現有使用者(零 regression 風險)
|
||||||
|
2. 讓 Web UI 和對外 API 的演進節奏脫鉤(Web UI 是內部工具,對外 API 需要穩定性承諾)
|
||||||
|
3. 未來若要合併,可在 Phase 3 評估,不用現在壓時程決定
|
||||||
|
|
||||||
|
### 2.2 既有 Web UI 的 API 呼叫路徑是否受影響?
|
||||||
|
|
||||||
|
**結論:不受影響。**
|
||||||
|
|
||||||
|
既有 Web UI 呼叫的所有端點(在 `apps/task-scheduler/server.js` 中驗證過):
|
||||||
|
|
||||||
|
```
|
||||||
|
POST /jobs (multipart 上傳)
|
||||||
|
GET /jobs (列表)
|
||||||
|
GET /jobs/:jobId (查詢單一 job)
|
||||||
|
GET /jobs/:jobId/events (SSE)
|
||||||
|
GET /jobs/:jobId/download/:filename (下載)
|
||||||
|
GET /health
|
||||||
|
```
|
||||||
|
|
||||||
|
這些端點 Phase 1 **保留原樣**。對外 API 是新增,不是取代。
|
||||||
|
|
||||||
|
### 2.3 既有 Web UI 要不要也改走新 OAuth 流程?
|
||||||
|
|
||||||
|
**Design Agent 建議:Phase 1 不改,維持現況。** 但有一個 UX 面的警示要給使用者知道。
|
||||||
|
|
||||||
|
#### 建議不改的理由(UX 觀點)
|
||||||
|
|
||||||
|
1. **內部工具的 UX 定位**:既有 Web UI 的 persona 是 **Kneron 內部 AI 工程師**,在可信網段內使用,不需要 OAuth 這層摩擦
|
||||||
|
2. **加上 OAuth 等於要加登入流程**:會多出「登入頁 → 選 tenant → 授權」等步驟,對內部工具體驗是倒退
|
||||||
|
3. **風險可控**:Web UI 目前應該是在內網 / VPN 後面運行,不直接暴露公網(這需要由 Architect 與 DevOps 確認,見下方警示)
|
||||||
|
|
||||||
|
#### ⚠️ 警示 — 需要使用者與 Architect 釐清
|
||||||
|
|
||||||
|
**如果 Web UI 和對外 API 部署在同一個 Task Scheduler instance 上**(例如同一個 Express app 綁兩套 route),那麼:
|
||||||
|
|
||||||
|
- 對外 API 的 public endpoint(`/api/v1/*`)會和 Web UI 用的 `/jobs` **共用同一個 TCP port / Nginx vhost**
|
||||||
|
- 若未來公開 `/api/v1/*` 到公網,`/jobs` 路徑也會曝光
|
||||||
|
- 有心人可以直接打 `POST /jobs`(不需 OAuth)繞過對外 API 的 rate limit / scope 檢查
|
||||||
|
|
||||||
|
**緩解方案(建議交給 Architect 評估)**:
|
||||||
|
- 方案 A:**部署層級隔離** — Web UI 的 `/jobs` 只綁 internal network interface,`/api/v1/*` 綁 public interface
|
||||||
|
- 方案 B:**Nginx 路由控制** — 同一個 Task Scheduler,但 public Nginx 只 proxy `/api/v1/*`,internal Nginx 才 proxy `/jobs`
|
||||||
|
- 方案 C:**在 `/jobs` 加基本 IP allowlist**(內網 CIDR)
|
||||||
|
|
||||||
|
### 2.4 既有 SSE (`/jobs/:id/events`) 機制在新架構下是否保留?
|
||||||
|
|
||||||
|
**結論:保留,但只給 Web UI 用。對外 API 走 polling,不對外開放 SSE。**
|
||||||
|
|
||||||
|
#### Design Agent 的觀察
|
||||||
|
|
||||||
|
1. **Web UI 繼續用 SSE**:既有 Web UI 的即時性體驗(看到 stage 切換、進度百分比即時跳動)是 UX 亮點,不能砍
|
||||||
|
2. **對外 API 不做 SSE**(PM 已決策):polling 已足夠,理由充分:
|
||||||
|
- 下游消費者是另一個 backend 服務(visionA-backend),polling 對它們而言是標準做法
|
||||||
|
- 不做 Webhook / SSE 能簡化 Phase 1 的 surface area
|
||||||
|
- visionA-backend 自己再決定要不要把進度以 SSE / WebSocket 推給 VisionA 前端
|
||||||
|
3. **但要確保 SSE endpoint 不意外被對外 API 消費者呼叫**(沒意義且資源浪費):
|
||||||
|
- `/jobs/:id/events` 保留在非 OAuth 路徑下,visionA-backend 沒有理由呼叫它
|
||||||
|
- 若要防禦,可以在 Nginx 層封堵 public access 到 `/jobs/*/events`
|
||||||
|
|
||||||
|
#### UX 風險監測
|
||||||
|
|
||||||
|
visionA-backend 的 polling 策略會直接影響 VisionA 使用者看到的進度「順暢度」。建議 API 規格中明確給出指引(見第 4 節「給 Architect 的建議」):
|
||||||
|
- 建議的 polling 間隔(2-5 秒)
|
||||||
|
- 鼓勵 `stage_changed` 時立即 poll(讓 VisionA 能快速反應階段切換)
|
||||||
|
- 回應中的 `etag` / `updated_at` 讓消費者知道何時真的有變
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 新呼叫方(visionA-backend)間接影響分析
|
||||||
|
|
||||||
|
### 3.1 間接 UX 影響總覽
|
||||||
|
|
||||||
|
雖然我們不設計 VisionA 的 UI,但以下 API 設計決策會 **直接限制** visionA 前端能做出的體驗:
|
||||||
|
|
||||||
|
| Converter API 設計點 | 對 VisionA 終端使用者 UX 的影響 | Design Agent 建議 |
|
||||||
|
|--------------------|----------------------------|------------------|
|
||||||
|
| 同使用者同時一個轉檔限制(US-11)| 使用者想同時轉兩個不同模型會被擋 | 回 409 時附清晰的 `active_job_id` + 人類可讀的訊息,讓 VisionA 能做「你有一個轉檔進行中,要切過去看嗎?」的 UX |
|
||||||
|
| Polling 模式(不做 Webhook)| 進度更新延遲 = polling 間隔 | API 文件明示建議 polling 間隔;回應要快(p95 < 200ms)避免 VisionA 排隊等待 |
|
||||||
|
| `POST /api/v1/jobs` 採 multipart/form-data(原始模型直接上傳)| 大檔上傳期間使用者需要「可取消 / 可重試」的體感 | API 文件建議 visionA-backend 在上傳到 Converter 時使用支援 progress event 的 HTTP client(原生 `net/http` + `io.TeeReader` 或等效),並在 visionA 前端以 progress bar 呈現上傳百分比;建議 Architect 在 TDD 中明確:收檔失敗 / 網路中斷時回 4xx/5xx 足以讓 visionA-backend 判斷是否重試(不要只 reset connection)|
|
||||||
|
| 原始模型檔案大小上限 500MB | 超過上限的使用者會被擋在建 job 前 | API 在 413 `file_too_large` 回應中附 `details.limit_bytes` / `details.actual_bytes`,讓 visionA 前端能顯示具體原因;VisionA 前端可在上傳前做 client-side 大小檢查提前攔截 |
|
||||||
|
| promote 需要另一個 API 呼叫 | 使用者要按兩次(轉完 + 加進模型庫)| **建議 visionA 前端把兩步驟做成一次操作**(使用者按一次「轉檔並加進模型庫」,VisionA backend 內部自動 chain),但這是 VisionA 側的決定 |
|
||||||
|
| Converter Bucket 7 天 lifecycle | 使用者轉完沒 promote,7 天後檔案消失 | API 回應中要暴露 `expires_at`,讓 VisionA 能在 UI 顯示「檔案將於 X 天後自動清除」|
|
||||||
|
| Recovery API 是 list 模式 | 使用者離開後回來 → VisionA 前端要決定是否自動跳轉到 job 頁 | API 要回足夠多資訊(job_id、stage、progress、created_at),讓 VisionA 前端做智慧決策 |
|
||||||
|
| Phase 1 沒有 delegated download | 使用者在 VisionA 模型庫看到模型,但不能下載 | **嚴重 UX 缺口,見第 6 節議題 #1** |
|
||||||
|
|
||||||
|
### 3.2 錯誤情境的 UX 投射
|
||||||
|
|
||||||
|
visionA 前端能把 Converter 回的錯誤轉成什麼樣的訊息,完全取決於我們回了什麼。以下是 Design Agent 建議 API 要清楚區分的錯誤類型:
|
||||||
|
|
||||||
|
| 情境 | HTTP 狀態 | 錯誤碼建議 | VisionA 能做出的 UX |
|
||||||
|
|------|---------|----------|------------------|
|
||||||
|
| token 無效 / 過期 | 401 | `unauthorized` | 背景 refresh token 重試 |
|
||||||
|
| token 有效但 scope 不足 | 403 | `insufficient_scope` + `required_scope` | 提示 admin 去 Member Center 補授權 |
|
||||||
|
| 已有 in-progress job | 409 | `user_has_active_job` + `active_job_id` | 「你有一個轉檔進行中,要切過去看嗎?」 |
|
||||||
|
| job 狀態不對(promote 時 job 還沒 completed)| 409 | `job_not_ready_for_promote` + `current_status` | 「轉檔還沒完成,請等進度條到 100% 再加進模型庫」 |
|
||||||
|
| multipart body 格式錯 / `model` 檔案欄位缺失 / 必填 field 缺失(例如 `user_id`、`model_id`)| 400 | `invalid_multipart` + `details.missing_field` 或 `details.reason` | 「上傳失敗,請確認檔案格式與欄位」+ 具體指出缺哪個欄位 |
|
||||||
|
| 原始模型超過 500MB 上限 | 413 | `file_too_large` + `details.limit_bytes` + `details.actual_bytes` | 「檔案過大(限制 500MB),請確認上傳的模型大小」|
|
||||||
|
| 轉檔失敗(模型本身的問題)| — | `job.error.reason` 要人類可讀 | 「在 BIE 量化階段失敗:[具體原因],建議檢查參考圖片是否足夠」 |
|
||||||
|
| File Access Agent 不可用(promote 時)| 502 | `file_gateway_unavailable` | 「模型庫服務暫時不可用,我們會自動重試」+ 顯示 retry 按鈕 |
|
||||||
|
| Member Center JWKS 取用失敗 | 503 | `auth_service_unavailable` | 系統層錯誤,顯示 maintenance banner |
|
||||||
|
|
||||||
|
> **備註**:Phase 1 的原始模型改由 visionA-backend 以 multipart 直接上傳到 Converter,Converter 不再從 File Access Agent 拉原始模型,因此原本的 `input_object_not_found`(422)錯誤碼在 Phase 1 **不會出現**。取而代之的是上傳階段的 `invalid_multipart`(400)與 `file_too_large`(413)。File Access Agent 相關錯誤在 Phase 1 只會發生在 promote 階段。
|
||||||
|
|
||||||
|
### 3.3 API 設計上要給 visionA 留下哪些彈性?
|
||||||
|
|
||||||
|
為了讓 visionA 前端能做出更好的 UX,API 要預留以下能力(即使 Phase 1 可以先不實作,但 schema 要設計得可擴展):
|
||||||
|
|
||||||
|
#### 必要(Phase 1)
|
||||||
|
1. **狀態細節**:不只 `status`,要有 `stage` + `progress`(0-100)
|
||||||
|
2. **錯誤碼結構化**:`error.code` + `error.message` + `error.details`(可擴展)
|
||||||
|
3. **時間戳完整**:`created_at`、`updated_at`、各階段的 `stage_started_at` / `stage_completed_at`(用於 VisionA 做階段耗時顯示)
|
||||||
|
4. **保留欄位**:`metadata: {}`(讓未來能加欄位不破壞 API 契約)
|
||||||
|
5. **job_id 可識別**:使用者若要截圖 / 反映問題,能清楚引用 job_id
|
||||||
|
|
||||||
|
#### 建議(未必 Phase 1,但設計時要預留)
|
||||||
|
6. **ETA 欄位**:`estimated_completion_at`(Phase 1 可回 null,未來再實作)
|
||||||
|
7. **取消能力**:即使 Phase 1 不實作,API 路徑 `DELETE /api/v1/jobs/:id` 要預留(避免 Phase 2 需要時得改契約)
|
||||||
|
8. **Webhook 註冊**:同上,即使 Phase 1 不實作,資料模型要考量未來可擴展
|
||||||
|
9. **progress 的顆粒度**:每個 stage 內部的 progress(例如「BIE 階段 45%」而不只是「整體 33%」)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 建議與風險
|
||||||
|
|
||||||
|
### 4.1 給 Architect 的 API 設計建議(UX 觀點)
|
||||||
|
|
||||||
|
以下建議從「下游消費者能做出什麼樣的 UX」角度出發,請 Architect 在 TDD 中採納或說明取捨。
|
||||||
|
|
||||||
|
#### 4.1.1 Response Schema 建議
|
||||||
|
|
||||||
|
**`GET /api/v1/jobs/:id` 回應範例(建議)**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"job_id": "uuid-v4",
|
||||||
|
"user_id": "visionA-user-id",
|
||||||
|
"status": "running", // created / running / completed / failed
|
||||||
|
"stage": "bie", // onnx / bie / nef / null
|
||||||
|
"progress": 45, // 0-100(整體)
|
||||||
|
"stage_progress": 60, // 0-100(當前階段內)—— 建議有
|
||||||
|
"created_at": "2026-04-25T12:00:00Z",
|
||||||
|
"updated_at": "2026-04-25T12:05:30Z",
|
||||||
|
"stage_timings": { // 建議有,讓 VisionA 能顯示階段耗時
|
||||||
|
"onnx": {"started_at": "...", "completed_at": "..."},
|
||||||
|
"bie": {"started_at": "...", "completed_at": null},
|
||||||
|
"nef": null
|
||||||
|
},
|
||||||
|
"estimated_completion_at": null, // Phase 1 可為 null
|
||||||
|
"result_object_keys": null, // completed 時才有
|
||||||
|
"expires_at": "2026-05-02T12:00:00Z", // Converter Bucket 7 天後過期
|
||||||
|
"error": null, // 失敗時結構化錯誤
|
||||||
|
"metadata": {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**`POST /api/v1/jobs` 衝突回應(建議)**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"error": {
|
||||||
|
"code": "user_has_active_job",
|
||||||
|
"message": "使用者目前已有進行中的轉檔任務",
|
||||||
|
"details": {
|
||||||
|
"active_job_id": "uuid-v4",
|
||||||
|
"active_job_status": "running",
|
||||||
|
"active_job_stage": "bie",
|
||||||
|
"active_job_progress": 45
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
這讓 visionA 前端能直接顯示「你有一個轉檔進行中(BIE 階段 45%),要切過去看嗎?」而不用再多打一次 API 查詳情。
|
||||||
|
|
||||||
|
#### 4.1.2 Polling 效能考量
|
||||||
|
|
||||||
|
- **p95 < 200ms**(已在 PRD §9.2.1):visionA-backend 可能每 2-5 秒 polling 一次,如果 API 慢 visionA 的 UI 會跟著卡
|
||||||
|
- **建議加 `ETag` / `If-None-Match` 支援**:visionA-backend 可以在 304 時跳過資料傳輸,省流量
|
||||||
|
- **避免無必要的 DB 查詢**:`GET /api/v1/jobs/:id` 應該只讀 Redis,不做任何外部 HTTP 呼叫(否則 polling × N 個使用者會變擴增 load)
|
||||||
|
|
||||||
|
#### 4.1.3 promote API 的 UX 考量
|
||||||
|
|
||||||
|
**建議 `POST /api/v1/jobs/:id/promote` 是同步呼叫並回等候 result**:
|
||||||
|
- 好處:visionA 前端可以直接顯示 loading → 成功 / 失敗
|
||||||
|
- 風險:PUT 到 File Access Agent 若慢(大檔),API 會 block 幾秒(p95 < 3s 已在 SLA)
|
||||||
|
- 備案:若 PUT 超過某個 timeout(例如 10s),API 回 202 + 新的 `promote_job_id`,visionA polling 查 promote 進度
|
||||||
|
- Architect 決策:Phase 1 建議先做同步版本(簡單),失敗率觀察後再決定是否需要 async 模式
|
||||||
|
|
||||||
|
#### 4.1.4 其他
|
||||||
|
|
||||||
|
- **版本化**:`/api/v1/` 是對的,建議 OpenAPI spec 明確標注「breaking change 會走 `/api/v2/`」
|
||||||
|
- **error response 統一格式**:所有 4xx/5xx 都用同一個 `{error: {code, message, details}}` 結構,避免 visionA 要寫多套 parser
|
||||||
|
- **job_id 格式固定**:建議用 UUID v4,不要用 snowflake 之類的自訂 ID(visionA 端的 log 比較好看)
|
||||||
|
|
||||||
|
### 4.2 給 PM 的需求補強建議
|
||||||
|
|
||||||
|
PM 的 PRD 已經相當完整,以下是 Design Agent 從 UX 觀點發現可以補強的地方:
|
||||||
|
|
||||||
|
#### 4.2.1 建議新增:使用者下載 messaging 策略(§15.1 或 §12.2)
|
||||||
|
|
||||||
|
Phase 1 上線時 VisionA 使用者會遇到「我的模型在模型庫裡但下不下來」的情境。建議在 PRD 中增加一段:
|
||||||
|
|
||||||
|
> **Phase 1 使用者下載缺口的 UX 處理方案(待確認)**:
|
||||||
|
> Phase 1 上線時,delegated download 尚未可用。VisionA 需在其 UI 中明確告知使用者「下載功能於 Phase 2 上線」,或採 fallback proxy 方案。此取捨由 VisionA 產品團隊主導,Kneron Converter 不需額外做事,但雙方需在 Phase 1 上線 kickoff 前對齊 messaging。
|
||||||
|
|
||||||
|
#### 4.2.2 建議新增:VisionA 前端 UX 對我們 API 的期望(§4.4 或新增 §4.5)
|
||||||
|
|
||||||
|
目前 PRD §4.4 的 User Story 是從「visionA-backend」視角寫的。建議補一組 **從 VisionA 終端使用者視角** 的 UX acceptance criteria,例如:
|
||||||
|
|
||||||
|
- 「轉檔成功後,使用者能在 5 秒內看到模型出現在自己的模型庫」
|
||||||
|
- 「同使用者同時轉檔限制觸發時,使用者能在 2 秒內看到『你有一個轉檔進行中』的提示,並能一鍵跳過去」
|
||||||
|
- 「轉檔失敗時,使用者能看到具體原因(不只是 generic error)」
|
||||||
|
|
||||||
|
這些是 UX 標準,不是技術標準,寫進 PRD 可以讓 Architect 在設計 API 時有依據。
|
||||||
|
|
||||||
|
#### 4.2.3 建議補強:`[推測]` 標記的清理策略
|
||||||
|
|
||||||
|
PRD 中保留了既有的 `[推測]` 標記,但 §1.2 新增的 Persona C 和 §4.3~4.4 的 User Stories 沒有 `[推測]` 標記(因為是基於本次討論確認的)。建議使用者審閱時:
|
||||||
|
- 新增章節(§1.2 Persona C、§4.3~4.4、§14、§15):請使用者確認後正式定案
|
||||||
|
- 舊章節的 `[推測]` 標記:獨立一個工作項,逐條請使用者確認或刪除
|
||||||
|
|
||||||
|
這不是本次 L 級範圍,但建議 PM 在下一版 PRD 中處理。
|
||||||
|
|
||||||
|
### 4.3 給 Architect 的一般性 UX 風險提醒
|
||||||
|
|
||||||
|
#### 4.3.1 Polling 間隔太短可能讓 VisionA UI 卡頓?
|
||||||
|
|
||||||
|
**風險等級:低**
|
||||||
|
- visionA-backend 是 polling 方(不是 visionA 前端直接 polling Converter),所以 Converter 端壓力可控
|
||||||
|
- 但若有很多 VisionA 使用者同時轉檔,visionA-backend 可能每秒對 Converter 打幾十次 `GET /jobs/:id`
|
||||||
|
- **建議**:Architect 在 TDD 中明確給出 rate limit 策略(對 visionA-backend 的 client_credentials token 設 rate limit),並在 API 文件中建議 polling 間隔
|
||||||
|
|
||||||
|
#### 4.3.2 同時一個轉檔的限制可能引起使用者困惑
|
||||||
|
|
||||||
|
**風險等級:中**
|
||||||
|
- 「我剛剛明明建立了一個 job,為什麼再按一次就 409」可能讓使用者以為系統壞了
|
||||||
|
- **建議**:429 或 409 的 response body 要帶完整的 active job 資訊(見 4.1.1),讓 visionA 前端能做出「你有 X 在轉中,要看嗎?」的友善提示
|
||||||
|
- **護欄指標**:PRD §9.2.2 已經設計了「同使用者 409 比率 < 5%」的指標追蹤,Design Agent 同意這個目標
|
||||||
|
|
||||||
|
#### 4.3.3 Converter Bucket 7 天 lifecycle 和使用者期待不符
|
||||||
|
|
||||||
|
**風險等級:中**
|
||||||
|
- 使用者可能誤以為「轉完就在那裡了」,7 天後突然不見會很驚訝
|
||||||
|
- **建議**:
|
||||||
|
1. API 回應明確標注 `expires_at`(見 4.1.1)
|
||||||
|
2. visionA 前端在轉檔結果頁顯示「請於 X 天內加進模型庫,否則檔案將被自動清除」
|
||||||
|
3. `POST /promote` 應該是非常顯眼的 primary action(但這是 VisionA 的 UI 決定)
|
||||||
|
|
||||||
|
#### 4.3.4 使用者中途關掉頁面再回來(Recovery)是否會遺失 SSE?
|
||||||
|
|
||||||
|
**風險等級:低(已由 PM 決策接受)**
|
||||||
|
- PRD 已明確 Phase 1 對外 API 是 polling 模式,visionA-backend 重新進入頁面時會呼叫 `GET /api/v1/jobs?user_id=...&status=in_progress`
|
||||||
|
- Converter 本身的 Redis 狀態在這段時間是持續更新的,所以 recovery 不會因為瀏覽器關掉而遺失資料
|
||||||
|
- **風險**:若 Converter 在使用者離開期間 Crash(符合設計哲學),使用者回來會看不到任何 job
|
||||||
|
- PRD 已在 US-12 明示這個限制(「Crash 即 Reset」不保證跨 Crash recovery)
|
||||||
|
- Design Agent 接受這個設計取捨,但建議 API 回應中區分「沒有 in-progress job」vs「job 已不存在(可能因為 Crash / 過期)」讓 visionA 能給使用者不同提示
|
||||||
|
|
||||||
|
### 4.4 潛在 UX 風險清單(彙整)
|
||||||
|
|
||||||
|
| # | 風險 | 嚴重度 | 建議處理 |
|
||||||
|
|---|------|--------|---------|
|
||||||
|
| 1 | Phase 1 使用者下載功能缺口 | **高** | 需 VisionA 產品團隊協調 messaging 策略(見議題 #1) |
|
||||||
|
| 2 | Web UI `/jobs` 路徑在公網意外曝露 | 中 | 部署層或 Nginx 層隔離(見 2.3 警示) |
|
||||||
|
| 3 | 7 天 lifecycle 讓使用者驚訝 | 中 | API 要暴露 `expires_at`,visionA UI 要顯示 |
|
||||||
|
| 4 | 同使用者 409 的使用者困惑 | 中 | 回應要帶完整 active job 資訊 |
|
||||||
|
| 5 | 錯誤訊息不夠人類可讀 | 中 | 結構化 error code + 可讀 message |
|
||||||
|
| 6 | Polling 效能對 visionA 的影響 | 低 | API p95 < 200ms + 建議 ETag |
|
||||||
|
| 7 | Crash 後 job 消失 | 低(已接受)| API 能區分「無 job」vs「job 不存在」 |
|
||||||
|
| 8 | 轉檔階段耗時資訊不足,使用者無法預期 | 低 | 回應中帶 `stage_timings`,未來可做 ETA |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 其他觀察
|
||||||
|
|
||||||
|
### 5.1 既有 Web UI 的非本次議題(延伸觀察,不在本次範圍)
|
||||||
|
|
||||||
|
在審查過程中 Design Agent 觀察到既有 Web UI 有以下 UX 債,但 **不屬於本次 L 級範圍**,僅作記錄供未來討論:
|
||||||
|
|
||||||
|
1. **前後端 API 契約不一致**(已在 PRD §7.3 列出):Web UI 的單階段表單呼叫的 `/api/onnx/upload` 等端點後端沒實作。UX 影響:使用者按了按鈕會失敗
|
||||||
|
2. **無時間預估**:使用者只能看到「BIE 階段 45%」,不知道總共還要多久
|
||||||
|
3. **無批次上傳**:一次只能轉一個模型
|
||||||
|
4. **錯誤訊息品質**:`job.error.reason` 欄位存在但文字品質未知
|
||||||
|
5. **無登入系統**:內部工具現況,但若未來要加 OAuth 要從頭設計登入 UX
|
||||||
|
|
||||||
|
### 5.2 Mermaid 相依圖(從 UX 視角)
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
subgraph UsersWorld["使用者世界"]
|
||||||
|
KneronUser["Kneron 內部 AI 工程師<br/>(既有 Web UI 使用者)"]
|
||||||
|
VisionAUser["VisionA 終端使用者<br/>(新的 間接受益者)"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph InnovedusEcosystem["Innovedus 生態"]
|
||||||
|
WebUI["Kneron Converter Web UI<br/>(保留不動)"]
|
||||||
|
VisionAFrontend["VisionA 前端<br/>(另一個團隊實作)"]
|
||||||
|
VisionABackend["visionA-backend<br/>(Persona C)"]
|
||||||
|
MemberCenter["Member Center"]
|
||||||
|
FileAccessAgent["File Access Agent"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph Converter["Kneron Model Converter(本專案)"]
|
||||||
|
OldAPI["舊 API<br/>POST /jobs<br/>GET /jobs/:id<br/>GET /jobs/:id/events (SSE)<br/>不加 OAuth"]
|
||||||
|
NewAPI["新 API<br/>POST /api/v1/jobs<br/>GET /api/v1/jobs/:id<br/>POST /api/v1/jobs/:id/promote<br/>OAuth2 Bearer"]
|
||||||
|
end
|
||||||
|
|
||||||
|
KneronUser -->|multipart 上傳 / SSE 看進度| WebUI
|
||||||
|
WebUI -->|保持既有呼叫| OldAPI
|
||||||
|
|
||||||
|
VisionAUser -->|平台內操作| VisionAFrontend
|
||||||
|
VisionAFrontend -->|VisionA 自家協議| VisionABackend
|
||||||
|
VisionABackend -->|取 service token| MemberCenter
|
||||||
|
VisionABackend -->|Bearer token + multipart<br/>(含原始模型 + ref_images)| NewAPI
|
||||||
|
|
||||||
|
NewAPI -.->|promote PUT 結果檔| FileAccessAgent
|
||||||
|
NewAPI -.->|驗 token / 取 token| MemberCenter
|
||||||
|
|
||||||
|
classDef newPath fill:#e1f5ff,stroke:#0288d1,stroke-width:2px
|
||||||
|
classDef oldPath fill:#fff9c4,stroke:#fbc02d,stroke-width:2px
|
||||||
|
class NewAPI,VisionAUser,VisionAFrontend,VisionABackend,MemberCenter,FileAccessAgent newPath
|
||||||
|
class OldAPI,KneronUser,WebUI oldPath
|
||||||
|
```
|
||||||
|
|
||||||
|
**圖例**:黃色 = Phase 1 保留不動的既有路徑;藍色 = Phase 1 新增 / 整合的部分。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 需要使用者決策的 UX 議題
|
||||||
|
|
||||||
|
### 議題 #1(最重要):Phase 1 使用者下載的 messaging 策略
|
||||||
|
|
||||||
|
**背景**:Phase 1 上線後,VisionA 使用者能完成「上傳 → 轉檔 → 加進模型庫」,但 **不能下載** 已搬進模型庫的模型檔。這是因為 Phase 2 的 delegated download 阻塞於 Member Center 未實作 `POST /file-access/download-tokens`。
|
||||||
|
|
||||||
|
**為什麼這是重要 UX 議題**:
|
||||||
|
- 使用者的心智模型會是「我的模型在我的模型庫裡 → 我當然可以下載它」
|
||||||
|
- 上線時如果下載按鈕不能按、或按了沒反應,會是明顯的 UX 死胡同
|
||||||
|
- 使用者可能繞道去舊 Kneron Web UI 下載,但那邊資料和 VisionA 模型庫不同步(混亂)
|
||||||
|
|
||||||
|
**可選方案**:
|
||||||
|
|
||||||
|
| 方案 | 做法 | UX 優劣 | 工程成本 |
|
||||||
|
|------|------|--------|---------|
|
||||||
|
| A | 隱藏下載按鈕,標示「下載功能 Phase 2 上線」| UX 誠實,但缺口明顯 | 低(VisionA 前端) |
|
||||||
|
| B | VisionA backend 做 proxy 下載(VisionA backend 從 File Access Agent 拉檔再回給瀏覽器)| 暫時堪用,但違反原架構(大檔過 VisionA backend)| 中(VisionA backend) |
|
||||||
|
| C | 使用者點下載 → 跳轉到舊 Kneron Converter Web UI 下載 | 技術最簡單,但資料不同步,使用者會混亂 | 低(導流)|
|
||||||
|
| D | 等 Member Center 實作完才上線 Phase 1 | UX 完整,但時程被別人卡住 | 阻塞 |
|
||||||
|
|
||||||
|
**Design Agent 傾向方案 A**(隱藏 + messaging),但這需要 **使用者 + VisionA 產品團隊** 一起決定,不是 Converter 單方能決定。
|
||||||
|
|
||||||
|
**建議 Orchestrator 協調**:Phase 1 kickoff 前召一次 VisionA 產品團隊 + Kneron Converter PM + Member Center owner 的跨團隊會議,明確 messaging 策略與時程。
|
||||||
|
|
||||||
|
### 議題 #2(次要):既有 Web UI 的公開曝光風險
|
||||||
|
|
||||||
|
**背景**:見第 2.3 節。若未來 `/api/v1/*` 對外公開而 `/jobs` 沒做任何隔離,可能被繞過。
|
||||||
|
|
||||||
|
**需要使用者確認**:
|
||||||
|
1. 本專案部署目標是什麼?(純內網 / VPN 後面 / 公網)
|
||||||
|
2. Web UI 是否會和對外 API 在同一個 public entry 後面?
|
||||||
|
|
||||||
|
這個議題交由 Architect Agent 在 TDD 中提出具體部署策略,使用者確認即可。Design Agent 只提出警示。
|
||||||
|
|
||||||
|
### 議題 #3(低優先):既有 PRD 的 `[推測]` 標記清理
|
||||||
|
|
||||||
|
見 4.2.3。建議在本次 L 級之後單獨開一個工作項處理。不阻塞 Phase 1。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 結論
|
||||||
|
|
||||||
|
### 7.1 對三方聯合討論的 Design 立場
|
||||||
|
|
||||||
|
- **✅ 同意** PM 的 Phase 1 / Phase 2 切分(§15)
|
||||||
|
- **✅ 同意** user_id 以 multipart form field 帶入(方式 A),而非放 token claim
|
||||||
|
- **✅ 同意** 原始模型採 multipart 直連上傳(visionA-backend → Converter),不經過 File Access Agent(2026-04-25 更新)
|
||||||
|
- **✅ 同意** polling 模式不做 Webhook
|
||||||
|
- **✅ 同意** Web UI 不改(Phase 1 保留既有路徑和 UX)
|
||||||
|
- **✅ 同意** 搬檔做法 2(Converter 自己 PUT 到 File Access Agent,僅限 promote 階段)
|
||||||
|
- **⚠️ 要求補強**:Phase 1 使用者下載 messaging 策略(議題 #1)
|
||||||
|
- **⚠️ 要求補強**:API response schema 要符合第 4.1 節的 UX 期望(錯誤結構化、stage_timings、expires_at 等)
|
||||||
|
- **⚠️ 新增 UX 關切**:multipart 上傳的 progress bar 呈現與 `invalid_multipart` / `file_too_large` 錯誤碼細節結構化(見 3.1、3.2)
|
||||||
|
|
||||||
|
### 7.2 Design Agent 對其他兩方文件的審閱(待三方產出後執行)
|
||||||
|
|
||||||
|
- 審閱 PRD 時的重點:Persona C 是否清楚描述服務對服務的互動?終端使用者的 UX 期望是否納入 acceptance criteria?
|
||||||
|
- 審閱 TDD 時的重點:API response schema 是否提供足夠彈性讓 visionA 做好 UX?錯誤碼是否結構化?polling 效能是否達 SLA?
|
||||||
|
|
||||||
|
### 7.3 本次 Design Review 的直接產出
|
||||||
|
|
||||||
|
1. 本份 `design-review.md`
|
||||||
|
2. `user-flow-cross-system.md`(跨系統使用者流程圖)
|
||||||
|
3. **不產出** Wireframe、Prototype、Design Tokens(本次無新 UI 需求)
|
||||||
353
docs/autoflow/03-design/user-flow-cross-system.md
Normal file
353
docs/autoflow/03-design/user-flow-cross-system.md
Normal file
@ -0,0 +1,353 @@
|
|||||||
|
# 跨系統使用者流程圖 — VisionA 終端使用者視角
|
||||||
|
|
||||||
|
> **視角**:VisionA 終端使用者(Edge AI 應用開發者)在 VisionA 平台內完成模型轉檔的完整體驗。
|
||||||
|
>
|
||||||
|
> **重點**:從使用者能「感知到什麼」切入,而非技術細節。Token 種類、錯誤情境等只標示關鍵分支,詳細 API 規格見 `04-architecture/TDD.md`。
|
||||||
|
>
|
||||||
|
> **涵蓋情境**:
|
||||||
|
> - 情境 A:首次上傳模型並轉檔(主要 Happy Path)
|
||||||
|
> - 情境 B:離開頁面後回來看未完成的 job(Recovery)
|
||||||
|
> - 情境 C:Phase 2 使用者下載模型(阻塞中 — 僅供未來參考)
|
||||||
|
>
|
||||||
|
> **服務標記慣例**:
|
||||||
|
> - 🧑 = VisionA 終端使用者(人類)
|
||||||
|
> - 🖥️ = VisionA 前端(瀏覽器)
|
||||||
|
> - ⚙️ = visionA-backend(Go 服務)
|
||||||
|
> - 🔐 = Member Center(Auth 中心)
|
||||||
|
> - 📦 = File Access Agent(檔案閘道)
|
||||||
|
> - 🏭 = Kneron Converter API(本專案)
|
||||||
|
> - 🗄️ = Converter Bucket(MinIO 暫存)
|
||||||
|
> - 💾 = NAS Bucket(模型庫長期儲存)
|
||||||
|
|
||||||
|
## 變更歷程
|
||||||
|
|
||||||
|
| 日期 | 變更內容 |
|
||||||
|
|------|---------|
|
||||||
|
| 2026-04-25 | 原始模型上傳路徑改為 visionA-backend → Converter multipart 直連(不經過 File Access Agent)。`POST /api/v1/jobs` 改為 multipart/form-data,移除 `input_object_key` 欄位,`user_id` 改以 multipart field 帶入。Phase 1 Converter 不再從 File Access Agent 讀取原始模型,只在 promote 階段 PUT 結果檔。階段編號:原「階段 2(上傳 FAA)+ 階段 3(建 job)」合併為新「階段 2(建 job + 上傳)」,後續階段編號往前挪一格。|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 情境 A:使用者上傳新模型並轉檔(Happy Path)
|
||||||
|
|
||||||
|
### A.1 使用者視角的流程摘要(非技術)
|
||||||
|
|
||||||
|
```
|
||||||
|
🧑 使用者在 VisionA 平台
|
||||||
|
│
|
||||||
|
├─ 1. 在「模型庫」按「新增模型」→ 選本機 ONNX 檔
|
||||||
|
│
|
||||||
|
├─ 2. 填寫 model_id / version / 選擇 Kneron 晶片平台(520/720/...)
|
||||||
|
│
|
||||||
|
├─ 3. 按「上傳並轉檔」
|
||||||
|
│ ↓
|
||||||
|
│ [系統顯示進度條:ONNX 優化 0% → 33% → BIE 量化 33% → 66% → NEF 編譯 66% → 100%]
|
||||||
|
│
|
||||||
|
├─ 4. 轉檔完成!顯示成功訊息 + 「加進我的模型庫」按鈕
|
||||||
|
│
|
||||||
|
└─ 5. 按「加進我的模型庫」→ 短暫 loading → 顯示「已加入模型庫」
|
||||||
|
↓
|
||||||
|
使用者在模型庫看到新的已轉檔模型
|
||||||
|
```
|
||||||
|
|
||||||
|
**使用者感知到的總體體驗**:從選檔到加進模型庫,一條流水線,使用者不需要知道背後有多個服務在協作(建 job 時涉及 visionA-backend / Member Center / Converter,promote 時才會再呼叫 File Access Agent)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### A.2 完整跨系統流程圖(Mermaid Sequence)
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
autonumber
|
||||||
|
participant U as 🧑 使用者
|
||||||
|
participant FE as 🖥️ VisionA 前端
|
||||||
|
participant BE as ⚙️ visionA-backend
|
||||||
|
participant MC as 🔐 Member Center
|
||||||
|
participant FA as 📦 File Access Agent
|
||||||
|
participant CV as 🏭 Converter API
|
||||||
|
participant CB as 🗄️ Converter Bucket
|
||||||
|
participant NB as 💾 NAS Bucket
|
||||||
|
|
||||||
|
Note over U,FE: 階段 1:選檔 + 填表
|
||||||
|
U->>FE: 選本機 ONNX 檔
|
||||||
|
U->>FE: 填 model_id / version / platform
|
||||||
|
U->>FE: 按「上傳並轉檔」
|
||||||
|
FE->>BE: 送出 (multipart: 檔案 + 參數)
|
||||||
|
|
||||||
|
Note over BE,CB: 階段 2:visionA-backend 呼叫 Converter 建 job(multipart 同時帶原始模型)
|
||||||
|
BE->>MC: POST /oauth/token (client_credentials, scope=converter:job.write)
|
||||||
|
MC-->>BE: service token (aud=kneron_converter_api)
|
||||||
|
BE->>CV: POST /api/v1/jobs<br/>Authorization: Bearer token<br/>Content-Type: multipart/form-data<br/>files: model (必填, ≤500MB), ref_images[] (optional, maxCount 100)<br/>fields: user_id, model_id, version, platform, enable_evaluate, enable_sim_fp, enable_sim_fixed, enable_sim_hw
|
||||||
|
CV->>MC: 驗 token (JWKS)
|
||||||
|
MC-->>CV: 驗證通過
|
||||||
|
CV->>CV: 檢查:user_id 是否有 in-progress job?<br/>(US-11 同使用者一個轉檔限制)
|
||||||
|
|
||||||
|
alt 有 in-progress job
|
||||||
|
CV-->>BE: 409 Conflict + {code: user_has_active_job, active_job_id, active_job_status, active_job_stage}
|
||||||
|
BE-->>FE: 409 + active job 詳情
|
||||||
|
FE-->>U: 顯示「你有一個轉檔進行中,要切過去看嗎?」
|
||||||
|
Note over U: 走情境 B(Recovery)
|
||||||
|
else 沒有 in-progress job
|
||||||
|
CV->>CB: 寫入原始模型 (jobs/{job_id}/input/{filename})<br/>(含 ref_images[] 如有)
|
||||||
|
CV->>CV: 建 job 記錄,塞進 Redis<br/>Enqueue 到 onnx-worker
|
||||||
|
CV-->>BE: 201 Created + {job_id, status: created}
|
||||||
|
BE-->>FE: 201 + job_id
|
||||||
|
end
|
||||||
|
|
||||||
|
Note over FE,CV: 階段 3:Polling 進度(每 2-5 秒一次)
|
||||||
|
loop 直到 status=completed 或 failed
|
||||||
|
FE->>BE: GET /api/jobs/{job_id}/status
|
||||||
|
BE->>CV: GET /api/v1/jobs/{job_id}<br/>Authorization: Bearer token
|
||||||
|
CV-->>BE: {status, stage, progress, stage_timings, expires_at, ...}
|
||||||
|
BE-->>FE: 轉譯後的進度 JSON
|
||||||
|
FE->>U: 更新進度條 + 階段顯示
|
||||||
|
end
|
||||||
|
|
||||||
|
Note over CV: Worker 背景處理:ONNX → BIE → NEF<br/>(Crash 即 Reset 設計,不保證跨 Crash 存活)
|
||||||
|
CV->>CB: 儲存各階段結果(out.onnx / out.bie / out.nef)
|
||||||
|
|
||||||
|
Note over FE,U: 階段 4:轉檔完成,顯示「加進模型庫」按鈕
|
||||||
|
CV-->>BE: {status: completed, result_object_keys: [...], expires_at: "2026-05-02"}
|
||||||
|
BE-->>FE: 已完成 + result 資訊
|
||||||
|
FE->>U: 顯示「轉檔完成!」<br/>+「加進我的模型庫」按鈕<br/>+「檔案將於 7 天後自動清除」提示
|
||||||
|
|
||||||
|
Note over U,NB: 階段 5:使用者按「加進模型庫」→ promote
|
||||||
|
U->>FE: 按「加進我的模型庫」
|
||||||
|
FE->>BE: POST /api/models/{job_id}/add-to-library
|
||||||
|
BE->>CV: POST /api/v1/jobs/{job_id}/promote<br/>Authorization: Bearer token<br/>Body: {target_object_key: "visionA/models/{user_id}/{model_id}/v{version}/out.nef", ...}
|
||||||
|
CV->>MC: 取 files:upload.write token(若 cache 過期)
|
||||||
|
MC-->>CV: service token
|
||||||
|
CV->>CB: 讀結果檔
|
||||||
|
CV->>FA: PUT /files/{target_object_key}<br/>Authorization: Bearer token<br/>Body: out.nef
|
||||||
|
FA->>MC: 驗 token
|
||||||
|
MC-->>FA: 驗證通過
|
||||||
|
FA->>NB: 儲存到 NAS(使用者的模型庫區)
|
||||||
|
FA-->>CV: 200 OK
|
||||||
|
CV-->>BE: 200 OK + promoted_object_keys
|
||||||
|
BE-->>FE: 成功
|
||||||
|
FE->>U: 顯示「已加入模型庫」
|
||||||
|
Note over U: 使用者在模型庫看到新模型
|
||||||
|
```
|
||||||
|
|
||||||
|
### A.3 使用者感知到什麼 vs 背後發生什麼
|
||||||
|
|
||||||
|
| 使用者看到 / 做的事 | 背後發生的事(簡述)|
|
||||||
|
|------------------|------------------|
|
||||||
|
| 選檔並按「上傳並轉檔」 | visionA-backend 以 multipart/form-data 直接把原始模型上傳到 Converter(經 Member Center auth 取 `converter:job.write` service token),Converter 收檔後暫存在自己的 Bucket |
|
||||||
|
| 進度條跳動 | visionA-backend 每 2-5 秒 polling Converter,拿到新進度就更新 UI |
|
||||||
|
| 「ONNX 優化 → BIE 量化 → NEF 編譯」階段切換 | Converter 的 Task Scheduler 在 Redis 中推進 job stage,Worker 消耗 stage queue |
|
||||||
|
| 「檔案將於 7 天後自動清除」 | Converter Bucket 有 7 天 lifecycle,促使使用者做 promote |
|
||||||
|
| 按「加進我的模型庫」 | visionA-backend 呼叫 Converter `/promote`,Converter 自己把檔案 PUT 到 File Access Agent,不經 visionA-backend(省流量)|
|
||||||
|
| 模型出現在使用者的模型庫 | NAS Bucket 已經收到檔案(在 VisionA 使用者模型庫的 objectKey path 下)|
|
||||||
|
|
||||||
|
### A.4 關鍵時間點與可能的等待體驗
|
||||||
|
|
||||||
|
| 時間點 | 使用者感受 | 影響 UX 的因素 |
|
||||||
|
|-------|---------|--------------|
|
||||||
|
| 按「上傳並轉檔」後 | Loading / 上傳進度條 | 單一 multipart 上傳從 visionA-backend 到 Converter 的時間,**與檔案尺寸相關**(上限 500MB)。**建議 UI 顯示 upload progress bar**:multipart 上傳可追蹤 byte 進度(XHR upload progress event 或等效機制),不再像舊設計有「上傳 FAA + 拉檔入 Bucket」兩段式等待 |
|
||||||
|
| 進度條階段切換 | 期待每階段的時間均等,但實際上 NEF 通常最久 | 建議 UI 顯示各階段預估時間(可從 `stage_timings` 歷史推算)|
|
||||||
|
| 轉檔完成 → 按「加進模型庫」 | 短暫 loading | API p95 < 3s,使用者體感應該 OK |
|
||||||
|
| 轉檔失敗 | 希望看到具體原因 | 錯誤碼要結構化,`error.details.stage` + 人類可讀的 `error.details.reason` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 情境 B:使用者離開頁面後回來(Recovery)
|
||||||
|
|
||||||
|
### B.1 使用者視角
|
||||||
|
|
||||||
|
```
|
||||||
|
🧑 使用者在轉檔進行中...
|
||||||
|
│
|
||||||
|
├─ 按上一頁 / 關閉瀏覽器 / 切到其他 app
|
||||||
|
│ [Converter 背景繼續轉檔,不受影響]
|
||||||
|
│
|
||||||
|
├─ 15 分鐘後回來 VisionA,進入「模型庫」或「轉檔中心」
|
||||||
|
│
|
||||||
|
├─ 系統自動偵測到「你有一個轉檔進行中」
|
||||||
|
│ └─ 顯示:「模型 XYZ 正在轉檔(BIE 階段 60%),要繼續追蹤嗎?」
|
||||||
|
│
|
||||||
|
└─ 使用者按「繼續追蹤」→ 回到進度頁,正常 polling 進度
|
||||||
|
```
|
||||||
|
|
||||||
|
### B.2 跨系統流程(Mermaid Sequence)
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
autonumber
|
||||||
|
participant U as 🧑 使用者
|
||||||
|
participant FE as 🖥️ VisionA 前端
|
||||||
|
participant BE as ⚙️ visionA-backend
|
||||||
|
participant MC as 🔐 Member Center
|
||||||
|
participant CV as 🏭 Converter API
|
||||||
|
|
||||||
|
Note over U,FE: 使用者重新打開 VisionA 模型庫頁
|
||||||
|
U->>FE: 進入頁面
|
||||||
|
FE->>BE: GET /api/models/in-progress?user_id=...
|
||||||
|
BE->>MC: 取 service token(若 cache 過期)<br/>scope=converter:job.read
|
||||||
|
MC-->>BE: service token
|
||||||
|
BE->>CV: GET /api/v1/jobs?user_id=...&status=in_progress<br/>Authorization: Bearer token
|
||||||
|
CV->>MC: 驗 token
|
||||||
|
MC-->>CV: 驗證通過
|
||||||
|
CV->>CV: 從 Redis 查 user_id 對應的所有 in-progress job
|
||||||
|
CV-->>BE: [{job_id, status, stage, progress, created_at}, ...]
|
||||||
|
|
||||||
|
alt 有 in-progress job
|
||||||
|
BE-->>FE: job 列表
|
||||||
|
FE->>U: 顯示「你有一個轉檔進行中<br/>模型 XYZ - BIE 階段 60%<br/>[繼續追蹤] [放著不管]」
|
||||||
|
U->>FE: 按「繼續追蹤」
|
||||||
|
FE->>U: 進入進度頁,開始 polling<br/>(同情境 A 階段 4)
|
||||||
|
else 沒有 in-progress job
|
||||||
|
BE-->>FE: 空陣列
|
||||||
|
FE->>U: 正常顯示模型庫頁,不打擾使用者
|
||||||
|
end
|
||||||
|
|
||||||
|
Note over CV: ⚠️ 特別情境:Converter 在使用者離開期間 Crash
|
||||||
|
Note over CV: Redis 被重置 → in-progress job 消失<br/>(符合「Crash 即 Reset」設計)
|
||||||
|
Note over U: 使用者回來看不到 job<br/>需要重新上傳(US-12 已明示此限制)
|
||||||
|
```
|
||||||
|
|
||||||
|
### B.3 Recovery 的 UX 考量
|
||||||
|
|
||||||
|
| 考量點 | 處理建議 |
|
||||||
|
|-------|---------|
|
||||||
|
| 使用者不記得自己有 job 在跑 | 進入頁面時主動查詢並顯示提示 |
|
||||||
|
| 使用者有多個裝置(手機+電腦)都登入 | 每個裝置都會看到相同的 in-progress job(因為是 server-side 查詢)|
|
||||||
|
| Converter Crash 導致 job 消失 | VisionA 前端顯示「上次的轉檔記錄已遺失,建議重新上傳」(需 visionA-backend 決定是否保留本地 cache)|
|
||||||
|
| 回來時 job 已完成 | 可查 `GET /api/v1/jobs?user_id=...&status=completed` 看最近完成的 job(可選功能)|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 情境 C:Phase 2 — 使用者直接下載模型庫檔案(阻塞中)
|
||||||
|
|
||||||
|
> ⚠️ **重要提示**:本情境的 `POST /file-access/download-tokens` endpoint 在 Member Center **尚未實作**,Phase 1 **不做此情境**。
|
||||||
|
>
|
||||||
|
> 以下流程圖僅供未來 Phase 2 啟動時參考。Phase 1 上線時使用者如何處理下載需求,見 `design-review.md` 第 6 節「議題 #1」。
|
||||||
|
|
||||||
|
### C.1 Phase 2 目標使用者體驗
|
||||||
|
|
||||||
|
```
|
||||||
|
🧑 使用者在 VisionA 模型庫
|
||||||
|
│
|
||||||
|
├─ 看到已轉檔的模型
|
||||||
|
│
|
||||||
|
├─ 按「下載 .nef 檔」
|
||||||
|
│
|
||||||
|
└─ 瀏覽器直接下載(快速,因為直連 File Access Agent,不經 visionA-backend)
|
||||||
|
```
|
||||||
|
|
||||||
|
### C.2 Phase 2 跨系統流程(Mermaid Sequence,尚未實作)
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
autonumber
|
||||||
|
participant U as 🧑 使用者
|
||||||
|
participant FE as 🖥️ VisionA 前端
|
||||||
|
participant BE as ⚙️ visionA-backend
|
||||||
|
participant MC as 🔐 Member Center
|
||||||
|
participant FA as 📦 File Access Agent
|
||||||
|
participant NB as 💾 NAS Bucket
|
||||||
|
|
||||||
|
Note over U,FE: 使用者按下載按鈕
|
||||||
|
U->>FE: 按「下載 .nef」
|
||||||
|
FE->>BE: POST /api/models/{model_id}/download-token
|
||||||
|
|
||||||
|
Note over BE,MC: ⚠️ Phase 2 阻塞中:Member Center 尚未實作此 endpoint
|
||||||
|
BE->>MC: POST /file-access/download-tokens<br/>Body: {tenant_id, user_id, object_key, scope: "files:download.delegate"}
|
||||||
|
MC-->>BE: {delegated_token (opaque, exp <= 5min), expires_at}
|
||||||
|
BE-->>FE: delegated_token + File Access Agent 的 download URL
|
||||||
|
|
||||||
|
Note over FE,FA: 瀏覽器直接下載(不經 visionA-backend,省流量)
|
||||||
|
FE->>FA: GET /files/{object_key}?token=<delegated_token>
|
||||||
|
FA->>MC: POST /validate-delegated-token<br/>Body: {token}
|
||||||
|
MC-->>FA: {valid: true, user_id, object_key, tenant_id}
|
||||||
|
FA->>NB: 讀檔
|
||||||
|
FA-->>FE: 檔案(stream 下載)
|
||||||
|
FE->>U: 瀏覽器儲存檔案
|
||||||
|
```
|
||||||
|
|
||||||
|
### C.3 Phase 2 體驗 vs Phase 1 折衷方案的 UX 落差
|
||||||
|
|
||||||
|
| 項目 | Phase 1 折衷方案(任一)| Phase 2 完整方案 |
|
||||||
|
|------|----------------------|----------------|
|
||||||
|
| 下載速度 | 慢(大檔走 visionA-backend proxy)或無法下載(隱藏按鈕)| 快(瀏覽器直連 File Access Agent)|
|
||||||
|
| 對 visionA-backend 的負擔 | 高(若 proxy)| 零 |
|
||||||
|
| 檔案流量路徑 | 使用者 → visionA-backend → File Access Agent → 使用者 | 使用者 ↔ File Access Agent(直連)|
|
||||||
|
| 使用者 UX | 有 spinner,可能 timeout | 瀏覽器原生下載體驗 |
|
||||||
|
| 安全性 | 同 Phase 2(都走 Member Center auth)| 同 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 綜合:三個情境的狀態機
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
stateDiagram-v2
|
||||||
|
[*] --> 選擇模型: 使用者進入 VisionA
|
||||||
|
|
||||||
|
選擇模型 --> 上傳中: 選檔 + 填表 + 提交
|
||||||
|
上傳中 --> 建 job: multipart 直接上傳到 Converter(含原始模型 + ref_images)
|
||||||
|
|
||||||
|
建 job --> 有衝突: 檢查 user_id 已有 in-progress
|
||||||
|
有衝突 --> 繼續追蹤舊 job: 使用者選擇查看既有 job
|
||||||
|
|
||||||
|
建 job --> 轉檔中: 無衝突,Converter 收檔並建立 job
|
||||||
|
轉檔中 --> ONNX 階段
|
||||||
|
ONNX 階段 --> BIE 階段: 完成
|
||||||
|
BIE 階段 --> NEF 階段: 完成
|
||||||
|
NEF 階段 --> 轉檔完成: 完成
|
||||||
|
|
||||||
|
ONNX 階段 --> 轉檔失敗: 失敗
|
||||||
|
BIE 階段 --> 轉檔失敗: 失敗
|
||||||
|
NEF 階段 --> 轉檔失敗: 失敗
|
||||||
|
|
||||||
|
轉檔中 --> 使用者離開: 使用者關頁面
|
||||||
|
使用者離開 --> 繼續追蹤舊 job: 使用者回來(走情境 B Recovery)
|
||||||
|
使用者離開 --> Job 消失: Converter Crash(符合設計哲學)
|
||||||
|
Job 消失 --> [*]
|
||||||
|
|
||||||
|
轉檔完成 --> 已 promote: 使用者按「加進模型庫」(Converter PUT 到 File Access Agent)
|
||||||
|
已 promote --> 下載: 使用者要用模型
|
||||||
|
|
||||||
|
下載 --> Phase 1 折衷: Phase 1(下載功能缺口)
|
||||||
|
下載 --> Phase 2 直連: Phase 2(等 Member Center 補完)
|
||||||
|
|
||||||
|
轉檔失敗 --> [*]: 使用者看錯誤訊息
|
||||||
|
Phase 1 折衷 --> [*]
|
||||||
|
Phase 2 直連 --> [*]
|
||||||
|
繼續追蹤舊 job --> 轉檔中
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 附錄:Token 類型快速對照
|
||||||
|
|
||||||
|
| Token | 誰持有 | 誰簽發 | 誰驗 | 用途 |
|
||||||
|
|-------|-------|-------|------|------|
|
||||||
|
| visionA-backend service token(aud=kneron_converter_api, scope=converter:job.write / converter:job.read)| visionA-backend | Member Center(client_credentials grant)| Converter API(JWKS 驗簽)| visionA-backend 呼叫 Converter API(建 job / 查詢 / promote)|
|
||||||
|
| Converter service token(aud=file_access_api, scope=files:upload.write)| Converter API | Member Center(client_credentials grant)| File Access Agent | Converter 在 promote 階段 PUT 結果檔到 NAS。**Phase 1 不再需要 `files:download.read` / `files:metadata.read`**,因為原始模型已改由 visionA-backend multipart 直接上傳到 Converter,Converter 不再從 FAA 拉檔 |
|
||||||
|
| Delegated download token(Phase 2)| 瀏覽器 | Member Center(代 user 簽)| File Access Agent(線上驗)| 使用者瀏覽器直連 File Access Agent 下載 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 附錄:錯誤情境快速對照
|
||||||
|
|
||||||
|
| 使用者看到的訊息 | 技術原因 | 對應 HTTP 狀態 + error code |
|
||||||
|
|---------------|---------|--------------------------|
|
||||||
|
| 「你有一個轉檔進行中,要切過去看嗎?」| 同使用者同時一個轉檔限制觸發 | `POST /api/v1/jobs` 回 409 `user_has_active_job` |
|
||||||
|
| 「轉檔失敗:BIE 量化階段 — [具體原因]」| Worker 內部失敗 | Job 的 `status=failed`, `error.details.stage=bie`, `error.details.reason=...` |
|
||||||
|
| 「上傳失敗,請確認檔案格式與欄位」| multipart body 格式錯 / `model` 欄位缺失 / 必填 field 缺失(例如 `user_id`、`model_id`)| `POST /api/v1/jobs` 回 400 `invalid_multipart` |
|
||||||
|
| 「檔案過大,請確認模型不超過 500MB」| 上傳檔案超過 Converter 的大小上限 | `POST /api/v1/jobs` 回 413 `file_too_large` |
|
||||||
|
| 「模型庫服務暫時不可用,請稍後再試」| promote 時 File Access Agent 不可用 | `POST /promote` 回 502 `file_gateway_unavailable` |
|
||||||
|
| 「轉檔還沒完成,請等進度條到 100% 再加進模型庫」| promote 時 job 還沒 `completed` | `POST /promote` 回 409 `job_not_ready_for_promote` |
|
||||||
|
| 「你的登入已過期,請重新登入」| visionA-backend 的 user session 過期(visionA 自己的 auth,不是 Converter 的問題)| visionA-backend 自己處理 |
|
||||||
|
| 「系統錯誤,請聯絡客服」| Member Center 或 File Access Agent 完全無法連線 | `POST /api/v1/jobs` 回 503 `auth_service_unavailable` 或類似(Phase 1 建 job 本身不依賴 FAA,FAA 錯誤集中在 promote)|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 結語
|
||||||
|
|
||||||
|
這份跨系統流程圖揭露的關鍵 UX 訊息:
|
||||||
|
|
||||||
|
1. **使用者只看到「在 VisionA 平台內按幾下就轉完」**,背後有多個服務協作(visionA-backend / Member Center / Converter / File Access Agent)— 這是架構要盡力維持的體驗。值得注意的是 Phase 1 **建 job 階段只涉及 3 方**(visionA-backend / Member Center / Converter),File Access Agent 僅在 promote 階段參與,簡化了上傳時的故障面
|
||||||
|
2. **Phase 1 的 UX 閉環卡在「下載」**,需要 VisionA 產品團隊協調 messaging(見 `design-review.md` 議題 #1)
|
||||||
|
3. **Recovery 體驗是 VisionA 的 UX 優勢**,但受限於「Crash 即 Reset」設計,不保證跨 Crash 存活 — 要誠實告知使用者
|
||||||
|
4. **錯誤訊息的人類可讀性直接決定使用者對系統的信任**,Architect 在 TDD 中要重視 error code 的結構化設計
|
||||||
|
5. **multipart 直連上傳的 UX 優勢**:相較於舊設計「visionA-backend → FAA → Converter 拉檔」兩段式,新設計從使用者按下「上傳並轉檔」到 Converter 建 job 成功,只有一次網路傳輸;上傳時間可用 progress bar 精準呈現,不再有中間「看不見的 Converter 拉檔等待」
|
||||||
1392
docs/autoflow/04-architecture/TDD.md
Normal file
1392
docs/autoflow/04-architecture/TDD.md
Normal file
File diff suppressed because it is too large
Load Diff
724
docs/autoflow/04-architecture/design-doc.md
Normal file
724
docs/autoflow/04-architecture/design-doc.md
Normal file
@ -0,0 +1,724 @@
|
|||||||
|
# Design Doc — Kneron Model Converter 對外 API(L 級新功能)
|
||||||
|
|
||||||
|
## 作者:Architect Agent
|
||||||
|
## 狀態:Draft(三方交叉審閱前)
|
||||||
|
## 最後更新:2026-04-25
|
||||||
|
## 範圍:Phase 1(對外 API、OAuth2、File Access Agent 整合、promote)+ Phase 2 規格預留
|
||||||
|
|
||||||
|
## 變更歷程
|
||||||
|
|
||||||
|
| 日期 | 變更 | 作者 |
|
||||||
|
|------|------|------|
|
||||||
|
| 2026-04-25 | 初版 Draft | Architect Agent |
|
||||||
|
| 2026-04-25 | 原始模型上傳路徑改為 visionA-backend multipart 直接上傳 Converter;移除 File Access Agent 的 GET/HEAD S2S 需求(R1 / TBD-1 / §5.5 / ADR-002 input 部分);user_id 改放 multipart 欄位 | Architect Agent |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. 文件導讀
|
||||||
|
|
||||||
|
本 Design Doc 聚焦「系統層級架構決策」。若你是工程師要開始寫程式,請看 `TDD.md`(或本專案若拆分為 `TDD-*.md`)。
|
||||||
|
|
||||||
|
對應文件:
|
||||||
|
- 產品需求:`../02-prd/PRD.md`(§1.2、§4.3、§4.4、§5.5、§5.6、§14、§15)
|
||||||
|
- 使用者流程:`../03-design/user-flow-cross-system.md`
|
||||||
|
- 設計審閱:`../03-design/design-review.md`
|
||||||
|
- 專案健檢:`../00-onboarding/health-check.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 背景與範圍 (Context and Scope)
|
||||||
|
|
||||||
|
### 1.1 背景
|
||||||
|
|
||||||
|
Kneron Model Converter(下稱 Converter)目前是一個「只有 Web UI 的內部工具」,支援 AI 工程師以圖形化介面執行 ONNX → BIE → NEF 三階段模型轉檔。本次 L 級新功能將其擴展為「對外提供 OAuth2 保護 REST API 的服務」,讓 Innovedus 生態中的其他服務(首個消費者為 VisionA)能以程式化方式整合轉檔能力。
|
||||||
|
|
||||||
|
關鍵生態組成:
|
||||||
|
- **Converter(本專案)**:Node.js + Python Worker,部署在靠近 NAS 網段的位置
|
||||||
|
- **visionA-backend**:Go 服務,Converter API 的消費者(Persona C)
|
||||||
|
- **Member Center**:OAuth2 / OIDC authorization server(C# / OpenIddict)
|
||||||
|
- **File Access Agent**:tenant 邊界內檔案閘道(C# / ASP.NET Core),駐守 NAS 側,單一 tenant per instance
|
||||||
|
|
||||||
|
### 1.2 系統定位(新架構)
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TB
|
||||||
|
subgraph AWS["AWS 側"]
|
||||||
|
VisionAFE["VisionA 前端"]
|
||||||
|
VisionABE["visionA-backend<br/>(Go, Phase 1)"]
|
||||||
|
MC["Member Center<br/>(OAuth2 + JWKS)"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph NAS["NAS 側(內部網段)"]
|
||||||
|
subgraph ConverterNode["Converter 部署節點"]
|
||||||
|
Nginx["Nginx<br/>(public + internal vhost)"]
|
||||||
|
Scheduler["Task Scheduler<br/>(Node.js Express)"]
|
||||||
|
Redis["Redis<br/>(job state + user index)"]
|
||||||
|
Workers["Workers<br/>(onnx / bie / nef)"]
|
||||||
|
ConvBucket["Converter Bucket<br/>(MinIO, 7d lifecycle)"]
|
||||||
|
end
|
||||||
|
FAA["File Access Agent<br/>(C# ASP.NET Core)"]
|
||||||
|
NasBucket["NAS Bucket<br/>(模型庫長期儲存)"]
|
||||||
|
FAA --- NasBucket
|
||||||
|
end
|
||||||
|
|
||||||
|
VisionAFE -->|HTTPS| VisionABE
|
||||||
|
VisionABE -->|1. token<br/>(client_credentials)| MC
|
||||||
|
VisionABE -->|2. POST /api/v1/jobs<br/>multipart: model + user_id<br/>(aud=kneron_converter_api)| Nginx
|
||||||
|
Nginx -->|public vhost| Scheduler
|
||||||
|
Scheduler -->|驗 token<br/>(JWKS)| MC
|
||||||
|
Scheduler -->|multer memory<br/>寫入 input| ConvBucket
|
||||||
|
Scheduler -->|取 token<br/>(client_credentials,<br/>僅 promote 需要)| MC
|
||||||
|
Scheduler -->|put / get job state| Redis
|
||||||
|
Scheduler -->|enqueue stage| Redis
|
||||||
|
Workers -->|consume queue| Redis
|
||||||
|
Workers -->|read input / write output| ConvBucket
|
||||||
|
Scheduler -->|讀 MinIO 暫存| ConvBucket
|
||||||
|
Scheduler -->|promote: PUT 結果檔<br/>(files:upload.write)| FAA
|
||||||
|
|
||||||
|
classDef aws fill:#ffe0b2,stroke:#ef6c00
|
||||||
|
classDef nas fill:#c8e6c9,stroke:#2e7d32
|
||||||
|
classDef new fill:#bbdefb,stroke:#1565c0
|
||||||
|
class AWS aws
|
||||||
|
class NAS nas
|
||||||
|
class Nginx,Scheduler new
|
||||||
|
```
|
||||||
|
|
||||||
|
**關鍵資料流(Happy Path)**:
|
||||||
|
|
||||||
|
1. **取 token**(visionA-backend → Member Center)
|
||||||
|
visionA-backend 以 `client_credentials` 取得 `aud=kneron_converter_api` 的 access token(scope=`converter:job.write`)。
|
||||||
|
2. **建 job**(visionA-backend → Converter,multipart/form-data)
|
||||||
|
visionA-backend 以 `POST /api/v1/jobs` 直接把原始模型 multipart 上傳到 Converter;Converter 驗 OAuth token 後把檔案寫入 Converter Bucket(`jobs/{job_id}/input/{filename}`),流程與既有 Web UI `POST /jobs` multipart 上傳一致(`multer.memoryStorage()`,`fileSize: 500MB`)。
|
||||||
|
3. **轉檔**(Worker pool 處理,順序固定 onnx → bie → nef)
|
||||||
|
Workers 從 Converter Bucket 讀檔、處理、寫回結果檔。Phase 1 Converter **完全不從 File Access Agent 讀任何東西**。
|
||||||
|
4. **polling 進度**(visionA-backend → Converter)
|
||||||
|
每 2-5 秒 `GET /api/v1/jobs/:id`。
|
||||||
|
5. **promote 到 NAS**(visionA-backend → Converter → File Access Agent)
|
||||||
|
使用者在 VisionA 前端按「加進模型庫」時,visionA-backend 呼叫 `POST /api/v1/jobs/:id/promote`,Converter 以自己的 OAuth client 身分取 `files:upload.write` token,把 Converter Bucket 中的結果檔 PUT 到 File Access Agent。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 目標與非目標 (Goals and Non-Goals)
|
||||||
|
|
||||||
|
### Goals(Phase 1 必達)
|
||||||
|
|
||||||
|
- [ ] 對外 API 以 OAuth2 Bearer 驗證,對齊 Innovedus Member Center(JWKS 驗簽)
|
||||||
|
- [ ] Converter 同時具備 **Resource Server**(驗他人 token)與 **OAuth Client**(取自己 token)雙重身分
|
||||||
|
- [ ] 提供四個對外端點:`POST /api/v1/jobs`、`GET /api/v1/jobs`、`GET /api/v1/jobs/:id`、`POST /api/v1/jobs/:id/promote`
|
||||||
|
- [ ] 同使用者同時一個轉檔限制(以 `user_id` 為界,不是 `client_id`)
|
||||||
|
- [ ] Recovery 支援(`GET /api/v1/jobs?user_id=...&status=in_progress`)
|
||||||
|
- [ ] 既有 `/jobs/*` 舊路徑保留不動,Web UI 零影響
|
||||||
|
- [ ] 部署分流:公網只開 `/api/v1/*`,`/jobs/*` 只在內部網段可達
|
||||||
|
- [ ] OpenAPI 3.0 規格產出,供下游整合
|
||||||
|
- [ ] API SLA 可觀測(p95、錯誤率、token failure rate)
|
||||||
|
|
||||||
|
### Non-Goals(Phase 1 明確不做)
|
||||||
|
|
||||||
|
- [ ] 使用者直連下載(delegated download)— 阻塞於 Member Center endpoint,延至 Phase 2
|
||||||
|
- [ ] Webhook / SSE 對外推送(polling 已足夠,見 ADR-004)
|
||||||
|
- [ ] Job 取消 / 重試(非本次範圍,API 僅保留路徑)
|
||||||
|
- [ ] Job 持久化 / 跨 Crash recovery(維持「Crash 即 Reset」哲學)
|
||||||
|
- [ ] Web UI 改走新 OAuth 流程(本次 Phase 1 不動,見 ADR-006)
|
||||||
|
- [ ] 單階段轉換 API 後端對齊(既有 backlog,與本次獨立)
|
||||||
|
- [ ] 使用者層級 ACL(Converter 不管,責任邊界在 visionA-backend)
|
||||||
|
- [ ] 跨 tenant 隔離的複雜授權模型(本次設計為 single-tenant per Converter deployment,見 §5.3)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 架構設計 (The Actual Design)
|
||||||
|
|
||||||
|
### 3.1 架構模式選擇
|
||||||
|
|
||||||
|
- **選擇**:維持現有 **單體 Task Scheduler(Node.js Express)+ Worker Pool(Python)** 架構。對外 API 以新增路由群的方式加入,不另開新服務。
|
||||||
|
- **理由**:
|
||||||
|
1. Phase 1 範圍聚焦「多一層 auth + 多一組 API 端點 + promote 時對 File Access Agent 一次寫入」,不足以撐起新服務的運維複雜度。
|
||||||
|
2. 既有 Crash 即 Reset 哲學對單體有利:Scheduler stateless,重啟 = 復原。
|
||||||
|
3. 新舊路徑共用同一份 Redis job record,不需跨服務同步。
|
||||||
|
- **取捨**:
|
||||||
|
- 代價:Scheduler 單體變胖(預估 +600 行)。可接受,因為 API 介面屬於 I/O 密集,不是 CPU 密集,Node.js 單 process 足以負擔。
|
||||||
|
- 若未來 QPS 需求爆增(例如 > 500 RPS),可把 auth middleware 與 OAuth client 抽出為獨立 sidecar,但 Phase 1 不做。
|
||||||
|
|
||||||
|
### 3.2 分階段架構演進
|
||||||
|
|
||||||
|
#### Phase 1 架構(本次)
|
||||||
|
|
||||||
|
見 §1.2 的圖。重點變化:
|
||||||
|
|
||||||
|
| 元件 | 是否新增 / 修改 |
|
||||||
|
|------|---------------|
|
||||||
|
| Nginx | 新增 public vhost(`/api/v1/*`)與 internal vhost(`/jobs/*`)的分流設定(見 §7) |
|
||||||
|
| Task Scheduler | 新增 auth middleware、OAuth client、新 `/api/v1/*` 路由群、user 索引、promote 實作 |
|
||||||
|
| Redis | 新增 `user:{user_id}:jobs` Set 索引、job record 新增欄位 |
|
||||||
|
| Workers | **不需要大改**(Phase 1 保持從 Converter Bucket 讀寫,見 §3.4 關鍵設計決定) |
|
||||||
|
| MinIO(Converter Bucket) | 不變 |
|
||||||
|
| File Access Agent | 不在本專案部署範圍(由 Innovedus 生態團隊部署)|
|
||||||
|
| Member Center | 不在本專案部署範圍 |
|
||||||
|
|
||||||
|
**月度基礎設施成本預估**(本專案側):Phase 1 沒有新增基礎設施,與現況相同。跨團隊依賴的 Member Center / File Access Agent 成本由對方團隊吸收。
|
||||||
|
|
||||||
|
#### Phase 2 架構(預留)
|
||||||
|
|
||||||
|
Phase 2 對 Converter 本體無架構變更。Delegated download 的流程完全發生在「visionA-backend ↔ Member Center ↔ 使用者瀏覽器 ↔ File Access Agent」,不經 Converter。
|
||||||
|
|
||||||
|
唯一可能的 Converter 變化:Phase 2 上線後可考慮讓 Converter 在 `promote` 完成時回傳更多資訊(例如 `download_hint_object_key`)方便 visionA-backend 直接拿來換 delegated token,但這是 nice-to-have,Phase 1 API 契約已足夠支撐。
|
||||||
|
|
||||||
|
### 3.3 技術選型(Technology Radar)
|
||||||
|
|
||||||
|
| 層級 | 技術選擇 | 狀態 | 選型理由 | 退出成本 |
|
||||||
|
|------|---------|------|---------|---------|
|
||||||
|
| Auth 驗 JWT | `jose`(npm)| Adopt | 零依賴純 JS,支援 JWKS remote + cache,主流專案廣泛採用 | 低(抽 1 個 middleware 即可換 `jsonwebtoken` + `jwks-rsa`)|
|
||||||
|
| Auth 取 token | 自寫輕量 HTTP client(`node-fetch` 或 Node 18 原生 fetch)+ in-memory cache | Adopt | client_credentials 只是一個 HTTP POST,不需引入 `openid-client` 這種大套件 | 低 |
|
||||||
|
| HTTP client 對 File Access Agent | Node 18 原生 fetch + stream | Adopt | 支援大檔 stream,無需額外 deps | 低 |
|
||||||
|
| Rate Limit | `express-rate-limit`(既有)+ per-client_id key 擴展 | Adopt | 既有套件擴展即可 | 低 |
|
||||||
|
| OpenAPI 產出 | 手寫 YAML + `@redocly/cli` 或 `swagger-ui-express` 提供 `/openapi.json` 檢視 | Adopt | Phase 1 手寫可控,避免 code-first 產出不穩定 | 低 |
|
||||||
|
| Redis 索引 | Redis Set(`user:{user_id}:jobs`)+ 原有 `job:{id}` | Adopt | Phase 1 量級不足以需要 PostgreSQL;維持 stateless 設計一致 | 中(未來要遷 PG 需雙寫) |
|
||||||
|
| 觀測工具 | 結構化 log(JSON)+ Nginx access log | Trial | Phase 1 先不引入 Prometheus;留待 Phase 2 | — |
|
||||||
|
|
||||||
|
### 3.4 關鍵設計決定:原始模型的上傳路徑
|
||||||
|
|
||||||
|
**背景**:
|
||||||
|
- visionA-backend 需要把使用者上傳的原始模型交給 Converter 轉檔。
|
||||||
|
- 原本考慮過「檔案先上傳 File Access Agent,Converter 再從 File Access Agent 拉」的方案,但發現:
|
||||||
|
1. 原始模型在轉檔成功、使用者按「加進模型庫」前**不屬於 NAS 模型庫**,沒必要先進 File Access Agent。
|
||||||
|
2. File Access Agent 的 `GET /files/{objectKey}` 只接受 delegated download token,Converter 以 S2S JWT 無法下載(除非對方擴充 API,這會是額外的跨團隊阻塞)。
|
||||||
|
3. Converter 既有 Web UI `POST /jobs` 已經是 multipart 上傳架構(`multer.memoryStorage()`, `fileSize: 500MB`),對外 API 直接沿用同一條路徑即可。
|
||||||
|
|
||||||
|
**決定**:
|
||||||
|
**Phase 1 採「visionA-backend 直接 multipart 上傳 Converter」的策略,與既有 Web UI 行為完全對齊。Converter Phase 1 完全不從 File Access Agent 讀任何東西。**
|
||||||
|
|
||||||
|
具體流程:
|
||||||
|
1. `POST /api/v1/jobs`(multipart/form-data)進來時,Scheduler 驗 OAuth token、以 `multer.memoryStorage()` 接收檔案(`model` required ≤500MB、`ref_images[]` optional maxCount 100)。
|
||||||
|
2. Scheduler 檢查 `user_id` 是否已有 in-progress job;若無則把 buffer 寫入 Converter Bucket(`jobs/{job_id}/input/{filename}`、`jobs/{job_id}/ref_images/*`)。
|
||||||
|
3. 建 job record、enqueue 到第一階段。
|
||||||
|
4. Worker 從 Converter Bucket 讀 input(**和既有 MinIO 模式完全一致**),產出結果寫回 Converter Bucket。
|
||||||
|
5. `promote` 時 Scheduler 以自己的 OAuth client 身分取 `files:upload.write` token,從 Converter Bucket 讀結果、PUT 到 File Access Agent。
|
||||||
|
|
||||||
|
**為什麼這樣選**:
|
||||||
|
- Worker 程式零改動(現有 `STORAGE_BACKEND=minio` 模式直接沿用)
|
||||||
|
- 對外 API 上傳路徑與既有 Web UI 程式幾乎 100% 共享(`multer` 中介層、500MB 限制、儲存路徑約定)
|
||||||
|
- Phase 1 Converter 只需要「當 OAuth client 打 File Access Agent」的單一場景(promote 寫入),不需要 `files:download.read` / `files:metadata.read`
|
||||||
|
- 避免阻塞於 File Access Agent GET 授權模型的擴充(原 ADR-002 的待確認項已解除)
|
||||||
|
|
||||||
|
**代價**:
|
||||||
|
- `POST /api/v1/jobs` 的 p95 會受 multipart 上傳大小影響(500MB 在一般網路環境約 5-30s),需調整 SLA(見 §6)。
|
||||||
|
- 大檔 multipart 會暫時佔用 Scheduler 記憶體(`multer.memoryStorage()`),與既有 Web UI 一致的風險模型。
|
||||||
|
- visionA-backend 必須自行處理上傳超時與重傳(和一般檔案上傳 API 行為一致)。
|
||||||
|
|
||||||
|
**替代方案**(見 ADR-002):
|
||||||
|
- A. 檔案先進 File Access Agent,Converter 再拉 — 需要跨團隊擴充 File Access Agent GET S2S 授權,Phase 1 被阻塞
|
||||||
|
- B. 檔案流經 visionA-backend 兩次上傳(VisionA 前端 → backend → Converter)— visionA-backend 要扛兩次大檔流量,浪費頻寬
|
||||||
|
- C. 使用者瀏覽器 direct-to-Converter presigned URL — 沒有對應基礎設施,Phase 1 不做
|
||||||
|
|
||||||
|
### 3.5 API 設計概覽
|
||||||
|
|
||||||
|
- **API 風格**:REST + JSON
|
||||||
|
- **Base Path**:`/api/v1/*`
|
||||||
|
- **認證**:所有端點(除 `/health`)都要 `Authorization: Bearer <JWT>`
|
||||||
|
- **錯誤格式**:統一 `{error: {code, message, details, request_id}}`
|
||||||
|
- **版本策略**:breaking change 走 `/api/v2/*`,小變更在 `/api/v1/*` 內向後相容新增欄位
|
||||||
|
- **Rate Limit**:以 `client_id`(來自 token claim)為 key,預設 300 requests / 5 min(可調)
|
||||||
|
- **ETag 支援**:`GET /api/v1/jobs/:id` 支援 `If-None-Match`,304 Not Modified 省流量(採納 Design 建議)
|
||||||
|
|
||||||
|
詳細 API spec 見 `TDD.md §1`。
|
||||||
|
|
||||||
|
### 3.6 資料架構
|
||||||
|
|
||||||
|
#### 核心資料(Redis)
|
||||||
|
|
||||||
|
| Key | 類型 | 內容 | TTL |
|
||||||
|
|-----|------|------|-----|
|
||||||
|
| `job:{job_id}` | String (JSON) | Job 完整 record(新增 `user_id`、`tenant_id`、`created_by_client_id`、`metadata`、`stage_timings`、`expires_at` 欄位)| 7 天 |
|
||||||
|
| `user:{user_id}:jobs` | Set | 該 user 的 job_id 集合 | 隨最新 job 延長,建議 7 天 |
|
||||||
|
| `user:{user_id}:active_job` | String | 當前 in-progress job_id(存在即代表有 active)| 隨 job 完成時刪除 |
|
||||||
|
| `ratelimit:client:{client_id}` | 由 `express-rate-limit` 管理 | — | 5 min |
|
||||||
|
|
||||||
|
不引入 PostgreSQL 的理由:Phase 1 資料量不大(單個 user 7 天內通常 < 10 個 job),Redis 足以承擔。未來若需要歷史任務持久化、跨 Crash recovery,再評估 PG。
|
||||||
|
|
||||||
|
#### 資料流(Phase 1)
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
BE[visionA-backend] -->|1. POST /api/v1/jobs<br/>multipart: model + user_id| Sched[Scheduler]
|
||||||
|
Sched -->|2. 驗 token| MC[Member Center]
|
||||||
|
Sched -->|3. 檢查 user:active_job| Redis[(Redis)]
|
||||||
|
Sched -->|4. multer 寫入 input| CB[(Converter Bucket)]
|
||||||
|
Sched -->|5. 建 job + 索引| Redis
|
||||||
|
Sched -->|6. enqueue onnx| Redis
|
||||||
|
Workers[Workers] -->|consume| Redis
|
||||||
|
Workers -->|read/write| CB
|
||||||
|
Workers -->|done event| Redis
|
||||||
|
BE -->|7. poll GET /api/v1/jobs/:id| Sched
|
||||||
|
Sched -->|8. 讀 job| Redis
|
||||||
|
BE -->|9. POST /promote| Sched
|
||||||
|
Sched -->|10. 取 Converter token<br/>(files:upload.write, cache)| MC
|
||||||
|
Sched -->|11. 讀結果| CB
|
||||||
|
Sched -->|12. PUT 結果| FAA[File Access Agent]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 可靠性設計 (Reliability)
|
||||||
|
|
||||||
|
### 4.1 SLI / SLO
|
||||||
|
|
||||||
|
| 服務 | SLI | SLO | 依據 |
|
||||||
|
|------|-----|-----|------|
|
||||||
|
| `/api/v1/*` 可用率 | 2xx+3xx 請求 / 總請求 | ≥ 99.5%(工作時段)| PRD §9.2.1 |
|
||||||
|
| `GET /api/v1/jobs/:id` p95 | 回應時間 95 百分位 | < 200ms | PRD §9.2.1、Design 4.1.2 |
|
||||||
|
| `POST /api/v1/jobs` p95 | 回應時間 95 百分位(含 multipart 上傳到 Converter Bucket,與檔案大小相關)| < 5s(200MB 檔案)| 依 PRD §9.2.1 的頻寬假設調整 |
|
||||||
|
| `POST /api/v1/jobs/:id/promote` p95 | 回應時間 95 百分位 | < 3s | PRD §9.2.1、Design 4.1.3 |
|
||||||
|
| Token 驗證失敗率 | 401 / 總請求 | < 1%(排除正常過期)| PRD §9.2.1 |
|
||||||
|
|
||||||
|
**關於 `POST /api/v1/jobs` p95**:因為改為 multipart 直接上傳,延遲主要受檔案大小與 visionA-backend 到 Converter 的網路頻寬影響。200MB @ 50MB/s ≈ 4s,500MB @ 50MB/s ≈ 10s。若觀測到頻繁超 SLA,Phase 2 可考慮拆為「建 job」+「上傳 chunk」兩個端點。
|
||||||
|
|
||||||
|
### 4.2 容錯設計
|
||||||
|
|
||||||
|
| 失敗情境 | 設計應對 |
|
||||||
|
|---------|---------|
|
||||||
|
| Member Center JWKS 不可達 | JWKS 本地 cache(TTL 10 min,stale-while-revalidate 24h),短時離線仍可驗 token |
|
||||||
|
| Member Center token endpoint 不可達(取 Converter 自己的 token)| Token cache(有效期內不重取);過期後重試 3 次,失敗則 `promote` 回 503 + `auth_service_unavailable`,由 visionA-backend 重試 |
|
||||||
|
| multipart 上傳失敗 / 超過 500MB | `POST /api/v1/jobs` 回 400 `validation_error` 或 413 `file_too_large`;Redis 不建 job record(避免殘留)|
|
||||||
|
| File Access Agent promote 失敗 | Converter Bucket 檔案保留 7 天,使用者可重試 promote;API 回 502 |
|
||||||
|
| Worker Crash | 既有 Crash 即 Reset 機制:Worker 重啟後繼續 consume Redis Stream,in-progress 的 job 若在 Redis 中會被接手(前提:Redis 沒死)|
|
||||||
|
| Redis Crash | 符合「Crash 即 Reset」設計哲學,所有 job 遺失,使用者重送 |
|
||||||
|
| Scheduler Crash | 同上,重啟後繼續服務;進行中的 Worker 下階段 done 事件會找不到 job record 而被忽略(既有行為)|
|
||||||
|
|
||||||
|
### 4.3 災難復原
|
||||||
|
|
||||||
|
維持既有 **Crash 即 Reset**:RPO = 無保證(Redis 重啟即清空)、RTO < 30s(Docker restart)。本次不新增 DR 機制。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 安全架構 (Security)
|
||||||
|
|
||||||
|
### 5.1 威脅模型(STRIDE 摘要)
|
||||||
|
|
||||||
|
| 威脅 | 風險 | 防護 |
|
||||||
|
|------|------|------|
|
||||||
|
| Spoofing(偽造 visionA-backend)| 中 | OAuth2 Bearer JWT(Member Center 簽發),JWKS 驗簽 |
|
||||||
|
| Spoofing(偽造 user_id)| 低(接受)| 信任 visionA-backend 的 user_id;Converter 不做 user ACL(PRD §5.6 明確)|
|
||||||
|
| Tampering(改動 job record)| 低 | Redis 在內部網段,無外部存取;Job record 僅 Scheduler 可寫 |
|
||||||
|
| Repudiation(否認呼叫)| 中 | Log `client_id` + `request_id` + `user_id`,保留 30 天 |
|
||||||
|
| Info Disclosure(跨 client 看別人的 job)| 中 | Job 查詢過濾:只回 `created_by_client_id` 吻合的 job(見 §5.3)|
|
||||||
|
| DoS | 中 | Rate Limit per `client_id`;檔案大小上限 500MB(既有)|
|
||||||
|
| Elevation of Privilege | 低 | scope 檢查嚴格:`converter:job.read` vs `converter:job.write` |
|
||||||
|
|
||||||
|
### 5.2 安全邊界
|
||||||
|
|
||||||
|
#### 5.2.1 端點 auth 要求
|
||||||
|
|
||||||
|
| 端點 | 需要 auth? | 需要的 scope |
|
||||||
|
|------|-----------|-------------|
|
||||||
|
| `GET /health` | ❌ 不需要 | — |
|
||||||
|
| `POST /api/v1/jobs` | ✅ | `converter:job.write` |
|
||||||
|
| `GET /api/v1/jobs` | ✅ | `converter:job.read` |
|
||||||
|
| `GET /api/v1/jobs/:id` | ✅ | `converter:job.read` |
|
||||||
|
| `POST /api/v1/jobs/:id/promote` | ✅ | `converter:job.write` |
|
||||||
|
| 舊 `/jobs/*`(Web UI 走的路徑)| ❌ 不加 OAuth | — |
|
||||||
|
| `/jobs/*/events`(SSE)| ❌ 不加 OAuth | — |
|
||||||
|
|
||||||
|
#### 5.2.2 Token 驗證清單
|
||||||
|
|
||||||
|
每個 `/api/v1/*` 請求的 middleware 必須檢查:
|
||||||
|
|
||||||
|
1. `Authorization: Bearer <JWT>` header 存在
|
||||||
|
2. JWT 格式合法
|
||||||
|
3. JWT 簽章(用 Member Center JWKS 驗)
|
||||||
|
4. `iss` == 設定的 Member Center issuer
|
||||||
|
5. `aud` 包含 `kneron_converter_api`(可能是 string 或 array)
|
||||||
|
6. `exp` 未過期(含 clock skew ±60s)
|
||||||
|
7. `scope` 包含該端點要求的 scope(空白分隔字串)
|
||||||
|
8. `client_id` claim 存在(記錄用)
|
||||||
|
9. (可選)`tenant_id` claim(見 §5.3)
|
||||||
|
|
||||||
|
**失敗處理**:
|
||||||
|
- 1-5 失敗 → 401 `invalid_token`
|
||||||
|
- 6 失敗 → 401 `token_expired`
|
||||||
|
- 7 失敗 → 403 `insufficient_scope` + `details.required_scope`
|
||||||
|
- 8 失敗 → 401 `invalid_token`(缺必要 claim)
|
||||||
|
|
||||||
|
#### 5.2.3 user_id 的邊界(方式 A:multipart 欄位)
|
||||||
|
|
||||||
|
**再次強調:user_id 不是授權邊界。** Converter 的設計如下:
|
||||||
|
|
||||||
|
| 操作 | 處理方式 |
|
||||||
|
|------|---------|
|
||||||
|
| `POST /api/v1/jobs` 建 job | 信任 multipart field 中的 `user_id`,寫入 job record |
|
||||||
|
| `GET /api/v1/jobs/:id` 查 job | 回傳 job record(不比對呼叫者 user_id)|
|
||||||
|
| `GET /api/v1/jobs?user_id=X` 查列表 | 以 query 中的 `user_id` 過濾(**呼叫者可以查任意 user 的 job**)|
|
||||||
|
| `POST /api/v1/jobs/:id/promote` | 不需要 user_id(只檢查 job 狀態)|
|
||||||
|
|
||||||
|
**為什麼這樣設計**:
|
||||||
|
- 服務消費者(例如 visionA-backend)本來就需要看自己所有 user 的 job(例如 admin 監控面板)
|
||||||
|
- 授權邊界在 `client_id` 層:只要 client 有 `converter:job.read` scope 就能查任何 job
|
||||||
|
- 防止跨 client 串連(見 §5.3)
|
||||||
|
|
||||||
|
#### 5.2.4 跨 client 隔離(重要)
|
||||||
|
|
||||||
|
**風險**:若未來有 client A 和 client B 都接 Converter,client B 不應看到 client A 建的 job。
|
||||||
|
|
||||||
|
**設計**:Job record 記錄 `created_by_client_id`,查詢時**預設**只回相同 client_id 的 job。
|
||||||
|
|
||||||
|
| 查詢 | 預設行為 |
|
||||||
|
|------|---------|
|
||||||
|
| `GET /api/v1/jobs` | 只回 `created_by_client_id == token.client_id` 的 job |
|
||||||
|
| `GET /api/v1/jobs/:id` | 若 `created_by_client_id != token.client_id`,回 404(**不是 403**,避免資訊洩露)|
|
||||||
|
| `POST /api/v1/jobs/:id/promote` | 同上 |
|
||||||
|
|
||||||
|
Phase 1 的第一個 client 是 visionA-backend,此規則對它無感。未來加新 client 時,自動獲得隔離。
|
||||||
|
|
||||||
|
**例外**:若需要「管理員 client」能跨 client 查詢(例如監控用),可設定特殊 scope `converter:admin.read`,Phase 1 不實作,但保留擴展空間。
|
||||||
|
|
||||||
|
### 5.3 Tenant 策略(回應 PM 疑問 A.1.3)
|
||||||
|
|
||||||
|
**決定**:Phase 1 採 **single-tenant per Converter deployment**,Converter 不自行管理 tenant 隔離。
|
||||||
|
|
||||||
|
具體作法:
|
||||||
|
- Converter 在設定檔中記錄 `EXPECTED_TENANT_ID`(從環境變數 `CONVERTER_TENANT_ID`)。
|
||||||
|
- 驗 token 時若 JWT 有 `tenant_id` claim,則檢查等於 `EXPECTED_TENANT_ID`,不等則 403 `tenant_mismatch`。
|
||||||
|
- 若 JWT 沒有 `tenant_id` claim,依 Member Center owner 的決定行事(Phase 1 初期可能沒有 tenant claim,可先 warn log,不擋)。
|
||||||
|
- Job record 記錄 `tenant_id`(方便未來 log 與審計)。
|
||||||
|
- 所有 Converter 打 File Access Agent 的請求自然帶著 Converter 自己的 `tenant_id`(來自 token),File Access Agent 的 `INSTANCE_TENANT_ID` 必須與之吻合。
|
||||||
|
|
||||||
|
**未來(多租戶)的擴展路徑**:
|
||||||
|
- 若要 Converter 一份程式碼支援多個 tenant,需要新增「根據 token 的 tenant_id 路由到對應的 File Access Agent instance」邏輯。Phase 1 不做。
|
||||||
|
|
||||||
|
### 5.4 舊 `/jobs/*` 路徑的保護(部署分流)
|
||||||
|
|
||||||
|
Web UI 走的 `/jobs/*` 路徑**不加 OAuth**。若公網可達,會被繞過對外 API 的 scope 檢查直接打 Scheduler。
|
||||||
|
|
||||||
|
**解決**:**部署層級分流**(見 §7)。Nginx 分兩個 vhost:
|
||||||
|
- **public vhost**(443 對公網):只 proxy `/api/v1/*` 到 Scheduler,其他路徑一律 404
|
||||||
|
- **internal vhost**(僅內部網段可達):proxy `/jobs/*`、`/health`、`/queues/stats`、`/jobs/*/events` 到 Scheduler
|
||||||
|
|
||||||
|
這樣 Web UI(部署在內部網段 / 跳板後面)正常運作,對外 API 僅暴露 `/api/v1/*`。
|
||||||
|
|
||||||
|
採納 Design Review §2.3 建議的方案 B。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 效能工程 (Performance)
|
||||||
|
|
||||||
|
### 6.1 延遲預算(`POST /api/v1/jobs`)
|
||||||
|
|
||||||
|
| 階段 | 預算 |
|
||||||
|
|------|------|
|
||||||
|
| Nginx ingress(含 multipart 前置處理)| 10ms |
|
||||||
|
| JWT 驗證(JWKS cache hit)| 5ms |
|
||||||
|
| multipart 接收(multer memory,與檔案大小相關)| 200MB @ 50MB/s ≈ 4s |
|
||||||
|
| multipart validation(欄位、mimetype、副檔名)| 20ms |
|
||||||
|
| Redis 查 active_job | 10ms |
|
||||||
|
| MinIO PutObject(寫入 Converter Bucket,buffer 已在記憶體)| 200MB @ 200MB/s ≈ 1s |
|
||||||
|
| Redis 寫 job record + 索引 | 20ms |
|
||||||
|
| Enqueue 到 Redis Stream | 10ms |
|
||||||
|
| **總預算(200MB 檔案)** | **~5s(p95)** |
|
||||||
|
| **總預算(500MB 檔案)** | **~12s(p95)** |
|
||||||
|
|
||||||
|
**若檔案很大**:p95 會超過 5s SLA。考慮未來:
|
||||||
|
- 改為 chunked upload(前端把檔案切塊,多個 PUT /api/v1/jobs/:id/chunks)— Phase 2 可選
|
||||||
|
- 改為 async 上傳模式(先回 202 + job_id,背景接收剩餘 chunks)— Phase 2 可選
|
||||||
|
|
||||||
|
### 6.2 Token cache 策略
|
||||||
|
|
||||||
|
| Cache | TTL | 退出條件 |
|
||||||
|
|-------|-----|---------|
|
||||||
|
| JWKS(驗 JWT 用)| 10 min(主動 refresh)| 遇到未知 `kid` 時強制 refresh 一次 |
|
||||||
|
| Converter 自己的 access token | `expires_in - 60s`(快到期才 refresh)| 遇到 401 時強制 refresh 一次 |
|
||||||
|
|
||||||
|
### 6.3 Rate Limit 策略
|
||||||
|
|
||||||
|
| 範圍 | 限制 | 動機 |
|
||||||
|
|------|------|------|
|
||||||
|
| 全局(IP)| 200 req / 15min(既有)| 維持既有防護 |
|
||||||
|
| Per `client_id` | 300 req / 5min | 防止單一 client 暴力 polling,但容許正常 2-5s polling(5min 可 60-150 次已足夠)|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 部署架構
|
||||||
|
|
||||||
|
### 7.1 Nginx 雙 vhost 分流
|
||||||
|
|
||||||
|
Phase 1 採**一份 Nginx process、兩個 `server` block** 的設計(方案 B):
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ Nginx(單一 process) │
|
||||||
|
│ │
|
||||||
|
│ ┌────────────────────────┐ ┌────────────────────────────┐ │
|
||||||
|
│ │ server { │ │ server { │ │
|
||||||
|
│ │ listen 443 ssl; │ │ listen 10.0.0.1:80; │ │
|
||||||
|
│ │ server_name │ │ server_name │ │
|
||||||
|
│ │ converter....com; │ │ converter-internal...; │ │
|
||||||
|
│ │ │ │ │ │
|
||||||
|
│ │ location /api/v1/ {} │ │ location /jobs {} │ │
|
||||||
|
│ │ location = /health {} │ │ location /queues/stats {} │ │
|
||||||
|
│ │ location / { │ │ location / { │ │
|
||||||
|
│ │ return 404; │ │ proxy_pass web:3000; │ │
|
||||||
|
│ │ } │ │ } │ │
|
||||||
|
│ │ } │ │ } │ │
|
||||||
|
│ │ (public vhost) │ │ (internal vhost, 內網 IP) │ │
|
||||||
|
│ └───────────┬─────────────┘ └────────────┬────────────────┘ │
|
||||||
|
└──────────────┼──────────────────────────────┼───────────────────┘
|
||||||
|
│ │
|
||||||
|
▼ ▼
|
||||||
|
┌──────────────────────────────────────────────────┐
|
||||||
|
│ Task Scheduler (:4000) │
|
||||||
|
│ - /api/v1/* (OAuth 保護,僅 public vhost 轉入)│
|
||||||
|
│ - /jobs/* (無 auth,僅 internal vhost 轉入) │
|
||||||
|
│ - /jobs/*/events(SSE) │
|
||||||
|
│ - /health, /queues/stats │
|
||||||
|
└──────────────────────────────────────────────────┘
|
||||||
|
▲
|
||||||
|
│ (僅 internal vhost 流入)
|
||||||
|
│
|
||||||
|
Web UI / 內部工具(內網)
|
||||||
|
```
|
||||||
|
|
||||||
|
實務上可以兩種實作方式:
|
||||||
|
- **A. 兩份 Nginx instance**(一個 public、一個 internal,各自獨立 process)
|
||||||
|
- **B. 一份 Nginx process、兩個 `server` block**,根據 `listen` 的 interface(public IP vs internal IP)做分流
|
||||||
|
|
||||||
|
Phase 1 採 **B**(設定簡單、資源省、單一 reload 管理)。上方 ASCII 圖即為方案 B 的實際結構;對應完整 Nginx config 詳見 `TDD.md §7.1`。
|
||||||
|
|
||||||
|
### 7.2 docker-compose 變化
|
||||||
|
|
||||||
|
本次**不**在 docker-compose.yml 新增 File Access Agent / Member Center(由對方團隊部署,Converter 只是 client)。
|
||||||
|
|
||||||
|
新增環境變數(詳見 `TDD.md §9`):
|
||||||
|
- `MC_ISSUER`, `MC_JWKS_URL`, `MC_TOKEN_URL`
|
||||||
|
- `KNERON_CONVERTER_CLIENT_ID`, `KNERON_CONVERTER_CLIENT_SECRET`
|
||||||
|
- `KNERON_CONVERTER_AUDIENCE`(接收端)
|
||||||
|
- `FILE_ACCESS_AGENT_BASE_URL`, `FILE_ACCESS_AGENT_AUDIENCE`
|
||||||
|
- `CONVERTER_TENANT_ID`
|
||||||
|
- `CONVERTER_SCOPES_REQUIRED_WRITE`, `CONVERTER_SCOPES_REQUIRED_READ`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. ADR(架構決策紀錄)
|
||||||
|
|
||||||
|
### ADR-001:對外 API 採 Member Center OAuth2,不自建 API Key
|
||||||
|
|
||||||
|
**狀態**:Accepted
|
||||||
|
**背景**:對外 API 需要身分驗證機制。選項有(a)自建 API Key、(b)採 Innovedus Member Center OAuth2。
|
||||||
|
|
||||||
|
**決定**:採 **Member Center OAuth2**。
|
||||||
|
|
||||||
|
**理由**:
|
||||||
|
1. 使用者已決定 Converter 對齊 Innovedus 生態(progress.md)。
|
||||||
|
2. OAuth2 是業界標準,visionA-backend 本來就要接 Member Center(Phase 1 規劃)。
|
||||||
|
3. 自建 API Key 等於要自己管 secret rotation、scope、audit,重複發明輪子。
|
||||||
|
4. Member Center 已提供 JWKS、token endpoint、client management,Converter 只要實作 resource server + client 兩個 OAuth2 角色即可。
|
||||||
|
|
||||||
|
**代價**:跨團隊依賴(註冊 audience、client、scope),若 Member Center owner 不配合會阻塞。已列入 progress.md 風險。
|
||||||
|
|
||||||
|
**替代方案**:
|
||||||
|
- A. 自建 API Key:簡單但不符合生態標準,未來遷移成本高
|
||||||
|
- B. mTLS:運維成本高,對 Node.js 生態不夠友善
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ADR-002:promote 結果檔採「做法 2」— Converter 自己推到 File Access Agent
|
||||||
|
|
||||||
|
**狀態**:Accepted
|
||||||
|
|
||||||
|
**背景**:轉檔完的結果要搬回 File Access Agent(`promote`)。有三種搬檔做法:
|
||||||
|
|
||||||
|
| 做法 | 說明 | 優劣 |
|
||||||
|
|------|------|------|
|
||||||
|
| 1 | 結果檔流經 visionA-backend(Converter → visionA-backend → File Access Agent)| 浪費頻寬、visionA-backend 要扛大檔 |
|
||||||
|
| 2 | Converter 自己 PUT File Access Agent(`files:upload.write`)| 檔案只在 NAS 側流動,單次寫入 |
|
||||||
|
| 3 | File Access Agent 主動拉 Converter Bucket | File Access Agent 沒這功能,不能為了我們改 |
|
||||||
|
|
||||||
|
**決定**:採做法 2。
|
||||||
|
|
||||||
|
**理由**:
|
||||||
|
1. 省流量:Converter 和 File Access Agent 都在 NAS 側,直連 HTTP。
|
||||||
|
2. visionA-backend 職責單純(只管 orchestration + 原始檔 multipart 轉送,不碰結果檔)。
|
||||||
|
3. 符合 PRD US-13 的明確要求。
|
||||||
|
4. File Access Agent 的 `PUT /files/{key}` 已明確支援 S2S JWT + `files:upload.write` scope(對方現有 API 已實作,無跨團隊阻塞)。
|
||||||
|
|
||||||
|
**範圍澄清(2026-04-25 更新)**:
|
||||||
|
本 ADR **只針對結果檔 promote 的搬檔路徑**。原始模型不會進 File Access Agent(visionA-backend 直接 multipart 上傳 Converter,見 §3.4),Phase 1 Converter 完全不需要 `files:download.read` / `files:metadata.read` scope。
|
||||||
|
|
||||||
|
**代價**:
|
||||||
|
- Converter 需要取自己的 service token(OAuth client 邏輯)。Token 可 cache、失敗可 retry。
|
||||||
|
- Converter 需要設定 `KNERON_CONVERTER_CLIENT_ID` + `CLIENT_SECRET`(secret 管理責任)。
|
||||||
|
|
||||||
|
**替代方案**(已排除):
|
||||||
|
- 備案 A:讓 visionA-backend 自己從 Converter 下載結果、再上傳到 File Access Agent — 浪費頻寬 × 2,不合理
|
||||||
|
- 備案 B:File Access Agent 主動拉 — 對方沒這個介面,不做
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ADR-003:user_id 以 multipart 欄位傳遞(方式 A),不放 token claim、不放自訂 header
|
||||||
|
|
||||||
|
**狀態**:Accepted(已由使用者決策)
|
||||||
|
|
||||||
|
**背景**:需要記錄 job 是誰的(VisionA 使用者 ID)。有三種方式:
|
||||||
|
|
||||||
|
| 方式 | 做法 | 優劣 |
|
||||||
|
|------|------|------|
|
||||||
|
| A | 放 multipart 欄位 `user_id`,Converter 信任 | 與 model_id / version / platform 等其他業務欄位用同一條路徑,和既有 Web UI 的 `POST /jobs` 對齊 |
|
||||||
|
| B | Member Center 簽 token 時把 `user_id` 塞 claim | 看起來像「user 的 token」,但 client_credentials 本質是 S2S,不該綁 user |
|
||||||
|
| C | 自訂 `X-User-Id` header | 跟一般業務欄位(model_id 等)放不同位置,增加心智負擔 |
|
||||||
|
|
||||||
|
**決定**:採方式 A(multipart 欄位)。
|
||||||
|
|
||||||
|
**理由**:
|
||||||
|
1. client_credentials 是服務對服務的 token,沒有「user」概念,不應把 user_id 放 claim
|
||||||
|
2. `POST /api/v1/jobs` 本身就是 multipart,多一個 `user_id` 欄位最自然,和 `model_id`、`version`、`platform` 等業務欄位放一起
|
||||||
|
3. 與既有 Web UI `POST /jobs` 的 multipart 欄位路徑一致,程式碼可共用 validation
|
||||||
|
4. Converter 不做 user 層 ACL(PRD §5.6 明確),user_id 只用於業務邏輯(同使用者限制、查詢過濾)
|
||||||
|
5. 避免跟 Member Center 要客製化 claim,維持標準 OAuth2
|
||||||
|
|
||||||
|
**代價**:Converter 完全信任 visionA-backend 送的 user_id。若 visionA-backend 被入侵或 bug 亂送,可能造成「A 使用者看到 B 的 job」。
|
||||||
|
- **緩解**:授權責任邊界在 visionA-backend,這是合理的 trust boundary。Converter 的日誌會記錄 user_id 變更頻率,可做異常監測(PRD §12.2 已列)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ADR-004:Polling 而非 Webhook
|
||||||
|
|
||||||
|
**狀態**:Accepted(PM 已決策)
|
||||||
|
|
||||||
|
**背景**:轉檔時間長(可能 30s-數分鐘),visionA-backend 需要知道何時完成。
|
||||||
|
|
||||||
|
**決定**:Phase 1 只提供 polling(`GET /api/v1/jobs/:id`),不實作 Webhook。
|
||||||
|
|
||||||
|
**理由**:
|
||||||
|
1. 下游是另一個 backend 服務,polling 對它是標準做法
|
||||||
|
2. Webhook 需要處理:retry、簽章、對方 endpoint 驗證、重放防護,surface area 太大
|
||||||
|
3. Phase 1 先快速上線驗證產品價值,Webhook 可以 Phase 3 再考慮
|
||||||
|
|
||||||
|
**代價**:進度延遲 = polling 間隔。API 文件建議 2-5s 間隔,對 UX 足夠。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ADR-005:Phase 1 使用者下載延至 Phase 2
|
||||||
|
|
||||||
|
**狀態**:Accepted(已由使用者決策)
|
||||||
|
|
||||||
|
**背景**:Member Center 的 `POST /file-access/download-tokens` 尚未實作,該 endpoint 是使用者直連下載的前提。
|
||||||
|
|
||||||
|
**決定**:Phase 1 **完全不做**使用者下載功能,延至 Phase 2。
|
||||||
|
|
||||||
|
**理由**:
|
||||||
|
1. 阻塞於外部依賴(Member Center owner 的時程)
|
||||||
|
2. Phase 1 先讓「上傳 → 轉檔 → 搬進模型庫」閉環可以跑
|
||||||
|
3. UX 缺口由 VisionA 產品團隊用 messaging 策略處理(Design 議題 #1)
|
||||||
|
|
||||||
|
**代價**:VisionA 使用者暫時沒有下載能力。VisionA 產品團隊需有 fallback(Design Review §6 議題 #1)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ADR-006:Phase 1 Web UI 不改,維持既有 multipart 路徑
|
||||||
|
|
||||||
|
**狀態**:Accepted(PM 已決策)
|
||||||
|
|
||||||
|
**背景**:Web UI 目前走 `POST /jobs`(multipart)、`GET /jobs/:id/events`(SSE)、`GET /jobs/:id/download/...`。
|
||||||
|
|
||||||
|
**決定**:**全部保留不動**,Web UI 與對外 API 兩套並存。
|
||||||
|
|
||||||
|
**理由**:
|
||||||
|
1. Web UI 是內部工具,persona 與 API 消費者不同
|
||||||
|
2. 給 Web UI 加 OAuth 等於加登入流程,UX 倒退(Design Review §2.3)
|
||||||
|
3. 降低本次 L 級的範圍,避免同時改兩套
|
||||||
|
|
||||||
|
**代價**:Web UI 的 `/jobs/*` 路徑若曝露公網會繞過 OAuth。
|
||||||
|
- **緩解**:部署層分流(§5.4、§7),public Nginx 只 proxy `/api/v1/*`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 回應 PRD 附錄 A 疑問清單
|
||||||
|
|
||||||
|
| # | PM 疑問 | 架構決策 |
|
||||||
|
|---|---------|---------|
|
||||||
|
| A.1.1 | Web UI 要不要也改走 OAuth | **不改**,見 ADR-006。以 Nginx 分流保護。未來若要改,屬於獨立的 M/L 級任務 |
|
||||||
|
| A.1.2 | 同使用者限制的範圍 | **整個 Converter 服務共用 user_id 空間**。現階段第一個 client 是 visionA-backend,未來加 client 時若真的有 user_id 衝突風險,再考慮用 `(client_id, user_id)` 複合鍵(Phase 1 不做)|
|
||||||
|
| A.1.3 | tenant_id 策略 | **single-tenant per Converter deployment**。見 §5.3。Job record 會記錄 tenant_id 方便 audit |
|
||||||
|
| A.1.4 | Phase 2 fallback | 本架構文件不決定此議題(屬 VisionA 產品團隊決策)。我們的架構不阻擋 VisionA 選任一方案 |
|
||||||
|
| A.1.5 | API 採用度 baseline | 非架構議題。建議 Phase 1 上線後跑 1 個月 beta 再訂 SLA 目標 |
|
||||||
|
| A.1.6 | 既有 `[推測]` 標記清理 | 非本次範圍 |
|
||||||
|
| A.2.1 | Member Center owner 協調 | **阻塞項**,必須在 kickoff 前解決。建議的 naming:`kneron_converter_api` audience、`kneron_converter` client、scopes 見 TDD §8(Phase 1 Converter 只需 `files:upload.write` 一個 scope 打 FAA)|
|
||||||
|
| A.2.2 | File Access Agent deployment / tenant_id | 中度依賴(只 promote 時用)。Converter 在 setup 時需要 FILE_ACCESS_AGENT_BASE_URL + 確認 tenant_id 吻合;FAA 現有 `PUT /files/{key}` 已支援 S2S JWT,無需擴充 |
|
||||||
|
| A.2.3 | VisionA Phase 1 OAuth 整合時程 | 需雙方 kickoff 對齊 |
|
||||||
|
| A.2.4 | Member Center download-tokens 實作時程 | Phase 2 啟動觸發條件 |
|
||||||
|
| A.3.1 | scope 命名 | 建議採 `converter:job.write`、`converter:job.read`,格式對齊 File Access Agent 的 `files:*.*` 慣例 |
|
||||||
|
| A.3.2 | Effort 估算 | 見 TDD §12(按 T1-T8 拆分,預估 4-5 人週)|
|
||||||
|
| A.3.3 | OpenAPI 維護策略 | **手寫 YAML**(Phase 1),手動與實作同步。未來再評估自動生成 |
|
||||||
|
| A.3.4 | user_id 索引的 Redis 策略 | **新增 `user:{user_id}:jobs` Set + `user:{user_id}:active_job` String**。避免 `KEYS *` 全掃(Design 4.1.2 建議已採納)|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. 回應 Design Review 7 條建議
|
||||||
|
|
||||||
|
| # | Design 建議 | 是否採納 | 說明 |
|
||||||
|
|---|-----------|---------|------|
|
||||||
|
| 1 | Response schema(`stage_timings`、`stage_progress`、`expires_at`、結構化 error)| ✅ 全採納 | 見 TDD §1 |
|
||||||
|
| 2 | 錯誤碼結構化(`{error: {code, message, details}}`,409 帶 active_job 詳情)| ✅ 全採納 | 見 TDD §1.5 |
|
||||||
|
| 3 | Polling 效能(p95 < 200ms,ETag 支援)| ✅ 採納 | 見 §6.1、TDD §1.3(ETag)|
|
||||||
|
| 4 | 部署隔離(避免 `/jobs/*` 公網曝光)| ✅ 採納方案 B(Nginx 分流) | 見 §7 |
|
||||||
|
| 5 | 預留擴展(metadata、ETA、DELETE 路徑、progress 顆粒度)| ✅ 大部分採納 | API 留 `metadata: {}`、保留 `DELETE` 路徑可回 501 Not Implemented(Phase 2 再啟用)|
|
||||||
|
| 6 | promote 同步(p95 < 3s)| ✅ 採納 | 見 §6.1。超過 10s timeout 的 async 模式 Phase 1 不做 |
|
||||||
|
| 7 | Rate limit per client_id | ✅ 採納 | 見 §6.3 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. 風險與待確認事項(給使用者決策)
|
||||||
|
|
||||||
|
> 註:2026-04-25 變更後,原 R1(File Access Agent GET S2S 授權問題)已移除(見 §0 變更歷程),現存 R1-R6 為原 R2-R6 的內容沿用原敘述挪上一格後重編;R7 為本次針對 multer memoryStorage 大檔並發 OOM 新增的風險。
|
||||||
|
|
||||||
|
| # | 風險 / 議題 | 影響 | 行動 |
|
||||||
|
|---|-----------|------|------|
|
||||||
|
| R1 | Member Center client / audience / scope 註冊時程 | 高 | Orchestrator 協助排跨團隊會議 |
|
||||||
|
| R2 | Member Center 是否支援 `tenant_id` claim?格式?| 中 | 待確認;Phase 1 可先不擋 tenant(warn log),等 Member Center 定案 |
|
||||||
|
| R3 | File Access Agent 的 `object_key` 命名約定與 VisionA 對齊(僅 promote 需要)| 中 | 見 TDD §6.1 建議,需 VisionA 確認 |
|
||||||
|
| R4 | JWKS cache stale 時 Member Center 更新 key 的同步策略 | 低 | 10 min TTL + 遇到未知 kid 強制 refresh 應足夠 |
|
||||||
|
| R5 | 大檔(> 200MB)multipart 上傳會超過 p95 SLA | 中 | Phase 1 接受(SLA 已調整為 5s @ 200MB、12s @ 500MB);若觀測到頻繁超 SLA,Phase 2 引入 chunked upload |
|
||||||
|
| R6 | docker-compose 本地開發時如何測 OAuth | 中 | 見 TDD §11(建議本地跑 Member Center docker-compose,或以 mock server)|
|
||||||
|
| R7 | Scheduler 同時承接多個 500MB multipart 上傳會吃光記憶體(`multer.memoryStorage()`)| 中 | Phase 1 依賴 `user_has_active_job` 鎖避免同 user 併發;若跨 user 併發成為瓶頸,改 `multer.diskStorage()` 或 streaming 上傳 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. Phase 1 / Phase 2 切分(架構層)
|
||||||
|
|
||||||
|
### Phase 1 必做
|
||||||
|
|
||||||
|
- auth middleware + scope 檢查
|
||||||
|
- OAuth client + token cache
|
||||||
|
- 新路由群 `/api/v1/*`(POST jobs、GET jobs、GET jobs/:id、POST promote)
|
||||||
|
- Redis 資料模型擴充(user_id、tenant_id、索引)
|
||||||
|
- 同使用者一個轉檔限制
|
||||||
|
- Recovery 查詢
|
||||||
|
- 部署分流(Nginx 雙 vhost)
|
||||||
|
- OpenAPI 文件
|
||||||
|
|
||||||
|
### Phase 2 預留(不改契約)
|
||||||
|
|
||||||
|
- `POST /api/v1/jobs/:id/download-tokens`(等 Member Center 補完)
|
||||||
|
- `DELETE /api/v1/jobs/:id`(回 501 Phase 1 → 實作 Phase 2/3)
|
||||||
|
- `POST /api/v1/jobs/:id/webhooks`(預留結構,可回 501)
|
||||||
|
- Async promote 模式(若觀測到 p95 > SLA)
|
||||||
|
|
||||||
|
### 阻塞條件
|
||||||
|
|
||||||
|
- **Phase 1 阻塞**:見 R1(Member Center 註冊),必須在 kickoff 前解除。File Access Agent 側 Phase 1 只需 `PUT /files/{key}` S2S 支援(對方現有 API 已支援,無阻塞)
|
||||||
|
- **Phase 2 阻塞**:Member Center `POST /file-access/download-tokens` 實作
|
||||||
|
|
||||||
|
### 觸發條件
|
||||||
|
|
||||||
|
- Phase 1:三方交叉審閱通過 + 使用者審核 Design Doc + TDD + R1 解除
|
||||||
|
- Phase 2:Member Center endpoint 實作完成並提供測試環境
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 13. 後續步驟
|
||||||
|
|
||||||
|
1. 本 Design Doc 送 PM / Design 交叉審閱
|
||||||
|
2. 使用者審核最終版
|
||||||
|
3. 跨團隊協調 R1(Member Center 註冊 audience / client / scope)
|
||||||
|
4. 工程師依 `TDD.md` 的任務清單(T1-T8)增量式開發
|
||||||
|
5. 第二階段:Reviewer 每個任務把關
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**附註**:本 Design Doc 約 710 行,已超過建議拆分門檻(500 行)。本次更新聚焦內容修正,暫不拆分;下輪更新建議拆分為 `design-doc.md`(索引)+ 子模組。
|
||||||
347
docs/autoflow/04-architecture/security.md
Normal file
347
docs/autoflow/04-architecture/security.md
Normal file
@ -0,0 +1,347 @@
|
|||||||
|
# Security Notes — Phase 1
|
||||||
|
|
||||||
|
> 本文件記錄 Phase 1 已知的安全設計決策、被接受的風險、以及對應的 mitigation 與 Phase 2 改進候補方案。
|
||||||
|
>
|
||||||
|
> **更新時機**:每次安全審查(Reviewer / Security Auditor)發現新風險或變更現有 trust assumption 時,必須更新此檔案。
|
||||||
|
|
||||||
|
## 索引
|
||||||
|
|
||||||
|
| Section | 內容 |
|
||||||
|
|---------|------|
|
||||||
|
| [Trust Boundary](#trust-boundary重要-design-risk) | user_id 來源信任問題(Phase 1 接受風險) |
|
||||||
|
| [Input Validation](#input-validation) | 已落實的輸入驗證機制 |
|
||||||
|
| [Storage Security](#storage-security) | MinIO object key 控制與 cleanup 策略 |
|
||||||
|
| [Auth Security](#auth-security) | JWT / JWKS 配置、algorithm pin |
|
||||||
|
| [Rate Limiting](#rate-limiting) | 雙層 rate limiter 設計 |
|
||||||
|
| [Logging](#logging) | 結構化 log 與敏感資料保護 |
|
||||||
|
| [Phase 2 候補方案清單](#phase-2-候補方案清單) | 已知待補強的設計 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Trust Boundary(重要 design risk)
|
||||||
|
|
||||||
|
### user_id 為 multipart field,受信任 visionA-backend 帶來
|
||||||
|
|
||||||
|
#### 設計
|
||||||
|
|
||||||
|
`POST /api/v1/jobs` 的 `user_id` 從 multipart form field 傳入,**不是**從 JWT claim derive。Converter 完全信任 visionA-backend 端把對的 `user_id` 傳進來。
|
||||||
|
|
||||||
|
```
|
||||||
|
visionA-backend Converter
|
||||||
|
│ │
|
||||||
|
├── client_credentials ──────→│ (取得 access token)
|
||||||
|
│ │
|
||||||
|
├── POST /api/v1/jobs ────────→│ Form-Data:
|
||||||
|
│ Authorization: Bearer … │ user_id: "alice" ← visionA 端決定
|
||||||
|
│ │ model: <file>
|
||||||
|
│ │ ...
|
||||||
|
│ │
|
||||||
|
│ │ Converter 端:
|
||||||
|
│ │ - 用 token 驗 client(OK)
|
||||||
|
│ │ - 信任 user_id 是「真正提交的 user」
|
||||||
|
│ │ - 不再驗證 user_id 與 token 的關係
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Trust assumption(Phase 1)
|
||||||
|
|
||||||
|
visionA-backend 端:
|
||||||
|
|
||||||
|
1. **程式碼安全** — 無 XSS / SSRF / RCE 漏洞,user_id 來源可信
|
||||||
|
2. **infra 安全** — network ACL、IP allow-list、TLS 確保只有 visionA-backend 能呼叫此 API
|
||||||
|
3. **credential 管理** — `client_secret` 不外洩、不放 git、不寫 log
|
||||||
|
4. **audit log 健全** — visionA 端能追溯「哪個真實用戶觸發了哪次轉檔」
|
||||||
|
|
||||||
|
#### Risk(被接受)
|
||||||
|
|
||||||
|
visionA-backend **一旦被 compromise**,attacker 可用同一個合法 `client_credentials`:
|
||||||
|
|
||||||
|
| 攻擊面 | 影響 |
|
||||||
|
|-------|------|
|
||||||
|
| 為任意 `user_id` 建 job | 冒充任何 user(user_id 完全由 attacker 控制)|
|
||||||
|
| 鎖定特定 user 7 天 | active_job conflict 機制被當武器(任意 user_id 一旦被鎖,正常請求也 409)|
|
||||||
|
| 偽造的 job 計入 victim user_id 的 history | `user:{victim}:jobs` Set 被汙染,未來查 history 看到不是自己的紀錄 |
|
||||||
|
| 累計 victim 的 job count(如有 quota / billing) | Phase 2 若引入 per-user quota / billing,會誤計到 victim 上 |
|
||||||
|
|
||||||
|
#### Phase 1 決策(2026-04-25 使用者裁決)
|
||||||
|
|
||||||
|
**接受此風險。** 理由:
|
||||||
|
1. visionA-backend 是內部受控系統(非 Internet-facing),compromise 機率低
|
||||||
|
2. Phase 1 重點是把核心 pipeline 跑通,安全強化排在 Phase 2
|
||||||
|
3. 引入 HMAC / OBO 會增加 visionA 端的整合工作量,目前未取得對方確認
|
||||||
|
|
||||||
|
#### Mitigations(Phase 1 已採用)
|
||||||
|
|
||||||
|
| Mitigation | 說明 |
|
||||||
|
|-----------|------|
|
||||||
|
| **per-client_id rate limiter(300 req / 5min)** | 限制單一 client(即被 compromise 的 visionA-backend)的攻擊速度 |
|
||||||
|
| **input 完全由 server 控制 object_key** | `jobs/{server-生成 uuidv4}/input/{sanitize 後 filename}`,attacker 無法控制 prefix |
|
||||||
|
| **filename / user_id 嚴格 sanitize** | 阻擋 path traversal / Redis key injection / log injection / XSS / glob pattern |
|
||||||
|
| **structured audit log(含 client_id + user_id pair)** | 可從 log 反查「哪個 client 為哪個 user_id 建了 job」,發現 compromise 時加速 forensics |
|
||||||
|
| **active_job 7 天 TTL**(fail-safe)| 即便 worker 異常未清,TTL 也會自動 GC,避免 attacker 鎖死後永久不釋放 |
|
||||||
|
|
||||||
|
#### Phase 2 候補方案
|
||||||
|
|
||||||
|
##### 方案 1:HMAC-signed user_id(推薦短期)
|
||||||
|
|
||||||
|
visionA-backend 用共享 secret HMAC 簽 user_id,Converter 驗簽:
|
||||||
|
|
||||||
|
```
|
||||||
|
visionA-backend Converter
|
||||||
|
|
||||||
|
hmac = HMAC-SHA256( 收到 multipart 後:
|
||||||
|
secret, recv_user_id, recv_hmac
|
||||||
|
user_id || timestamp) ↓
|
||||||
|
if HMAC-SHA256(secret, recv_user_id || ts) != recv_hmac:
|
||||||
|
POST /api/v1/jobs ─────────→ return 401 invalid_hmac
|
||||||
|
Form: if abs(now - ts) > 60s:
|
||||||
|
user_id: "alice" return 401 hmac_expired
|
||||||
|
x_user_id_hmac: "<hex>" else:
|
||||||
|
x_user_id_ts: "<unix>" accept user_id
|
||||||
|
```
|
||||||
|
|
||||||
|
**優點**:實作簡單,雙方只需要共享 secret + 規範 hash。
|
||||||
|
**缺點**:仍是 symmetric secret,有外洩風險;不會解決「visionA 自己被 compromise」的場景(attacker 也能簽)。
|
||||||
|
|
||||||
|
##### 方案 2:OBO Token / Token Exchange(業界標準,推薦中期)
|
||||||
|
|
||||||
|
visionA-backend 為每個 user 取 user-context token(例如 OBO flow / Token Exchange RFC 8693),Converter 從 JWT claims 取 `user_id`:
|
||||||
|
|
||||||
|
```
|
||||||
|
visionA-backend Member Center Converter
|
||||||
|
|
||||||
|
POST /token ──────────────→ grant_type=token-exchange
|
||||||
|
subject_token=<user-token> audience=converter
|
||||||
|
subject_token_type=jwt ↓
|
||||||
|
new token with claims:
|
||||||
|
sub: "alice" ← user 身份
|
||||||
|
actor: { sub: "visionA-client" } ← 委託 client
|
||||||
|
←─── new access token ─────
|
||||||
|
|
||||||
|
POST /api/v1/jobs ─────────────────────────────────────────────────→
|
||||||
|
Authorization: Bearer <new token> ↓
|
||||||
|
Converter 從 token claims
|
||||||
|
取 user_id(不是 multipart)
|
||||||
|
```
|
||||||
|
|
||||||
|
**優點**:
|
||||||
|
- 完全消除 trust boundary 問題(user_id 來自 Member Center 簽過的 JWT)
|
||||||
|
- 業界標準,跨 vendor 適用
|
||||||
|
- 自動 audit chain(actor + subject 雙重身份)
|
||||||
|
|
||||||
|
**缺點**:
|
||||||
|
- visionA / Member Center 都需要實作 Token Exchange 流程
|
||||||
|
- 性能:每次 POST /jobs 多一次 Token Exchange round-trip(可 cache 緩解)
|
||||||
|
|
||||||
|
##### 方案 3:Audit Anomaly Detection(補強)
|
||||||
|
|
||||||
|
偵測同 `client_id` 短期內出現大量不同 `user_id` 的異常 pattern:
|
||||||
|
|
||||||
|
```
|
||||||
|
監控 metric:
|
||||||
|
unique_user_ids_per_client_per_5min{client_id="visionA-backend-client"}
|
||||||
|
|
||||||
|
正常 pattern:
|
||||||
|
- 一個 client_id 5 分鐘內可能服務 5-50 個不同 user_id
|
||||||
|
|
||||||
|
異常 pattern:
|
||||||
|
- 一個 client_id 5 分鐘內出現 500 個不同 user_id
|
||||||
|
- 一個 client_id 連續 1 小時內每 5 分鐘出現 < 1 秒的 burst(自動化攻擊)
|
||||||
|
|
||||||
|
告警 → 人工介入 → 視情況 revoke client_credentials
|
||||||
|
```
|
||||||
|
|
||||||
|
**優點**:不需要改動 protocol,可獨立實作;對已 deployed 系統最容易加上。
|
||||||
|
**缺點**:被動防禦(事後發現),無法即時阻擋。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Input Validation
|
||||||
|
|
||||||
|
### 已落實(Phase 1)
|
||||||
|
|
||||||
|
| 項目 | 實作位置 | 機制 |
|
||||||
|
|------|---------|------|
|
||||||
|
| **filename sanitize** | `src/utils/sanitize.js` `sanitizeFilename` | NUL byte truncation / path.posix.basename / 控制字元 / 白名單字元 / 截長 200 / leading-dot 移除 |
|
||||||
|
| **user_id 嚴格白名單** | `src/utils/sanitize.js` `validateUserId` | `^[A-Za-z0-9._-]+$` regex(Sec M1 強化)+ 額外 `..` 拒絕 |
|
||||||
|
| **version 嚴格白名單** | `src/routes/v1/validators/createJob.js` | `^[A-Za-z0-9._-]+$` regex(Sec M3)|
|
||||||
|
| **model_id 數字範圍** | 同上 | `^\d+$` + 1 ≤ x ≤ 65535 |
|
||||||
|
| **platform enum** | 同上 | `{520, 720, 530, 630, 730}` |
|
||||||
|
| **enable_* boolean** | 同上 | 嚴格 `'true'` / `'false'` 字串 |
|
||||||
|
| **metadata JSON object** | 同上 | JSON.parse + 拒絕 array / null / primitive |
|
||||||
|
| **model 副檔名白名單** | 同上 | `{.onnx, .tflite}`(PRD F-01)|
|
||||||
|
| **model file 大小** | multer + handler | 預設 500MB(multer LIMIT_FILE_SIZE → 413)|
|
||||||
|
| **ref_image per-file 大小** | `validateCreateJobRequest` | 10MB(Sec C2 修正,避免 100 張 × 500MB = 50GB OOM)|
|
||||||
|
| **ref_image 張數** | multer fields config | maxCount=100 |
|
||||||
|
|
||||||
|
### 攻擊向量驗證清單
|
||||||
|
|
||||||
|
| 攻擊向量 | 防禦點 | 測試 |
|
||||||
|
|---------|-------|------|
|
||||||
|
| Path traversal in filename | `sanitizeFilename` `path.posix.basename` + leading-dot strip | `sanitize.test.js` |
|
||||||
|
| NUL byte truncation | `sanitizeFilename` `split('\0')` | 同上 |
|
||||||
|
| Windows path / backslash | `sanitizeFilename` `replace(/\\/g, '/')` | 同上 |
|
||||||
|
| Redis key injection in user_id | `validateUserId` 拒絕 `:` | 同上 |
|
||||||
|
| XSS in user_id | `validateUserId` 嚴格白名單 | 同上(Sec M1)|
|
||||||
|
| XSS in version | `version` 嚴格白名單 | `createJob.validator.test.js`(Sec M3)|
|
||||||
|
| Unicode RTL override | 嚴格白名單拒絕非 ASCII | 同上 |
|
||||||
|
| Glob / shell metachar in user_id / version | 嚴格白名單拒絕 `*?[];&|$` 等 | 同上 |
|
||||||
|
| ref_image OOM (100 × 500MB) | per-file 10MB 上限 | `createJob.integration.test.js`(Sec C2)|
|
||||||
|
| log injection (CRLF) | 嚴格白名單拒絕 `\r\n` | 同上 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Storage Security
|
||||||
|
|
||||||
|
### MinIO object key 完全 server 控制
|
||||||
|
|
||||||
|
```
|
||||||
|
inputObjectKey = `jobs/${jobId}/input/${safeFilename}`
|
||||||
|
^uuidv4 ^server-controlled prefix ^sanitized
|
||||||
|
refImageKey = `jobs/${jobId}/ref_images/${index}_${safeFilename}`
|
||||||
|
```
|
||||||
|
|
||||||
|
attacker 無法控制:
|
||||||
|
- prefix `jobs/` — server hardcode
|
||||||
|
- jobId — server 用 `uuidv4()` 生成
|
||||||
|
- ref_images index — server 用 `idx` 自增
|
||||||
|
|
||||||
|
attacker 部分控制(已 sanitize):
|
||||||
|
- safeFilename — 經 `sanitizeFilename` 處理(最壞情況產生合法的相對檔名)
|
||||||
|
|
||||||
|
### M5 方案 A:先寫 MinIO 後 Lua claim
|
||||||
|
|
||||||
|
避免「拿到 Lua claim 但 MinIO 失敗」需要 rollback Redis 的複雜度:
|
||||||
|
- MinIO 失敗 → 直接回 502,Redis 完全乾淨
|
||||||
|
- Lua conflict / throw → cleanup MinIO(fire-and-forget,靠 7d lifecycle 兜底)
|
||||||
|
- enqueue 失敗 → 補償 release Redis + cleanup MinIO(Sec M2 + Reviewer Major-2 修正)
|
||||||
|
|
||||||
|
### Sec M4:寫入放大 pre-check
|
||||||
|
|
||||||
|
handler 在 `writeInputToMinIO` 之前先廉價 GET `user:{userId}:active_job`,若已存在直接回 409。
|
||||||
|
避免 conflict request 還是上傳完 500MB 才被 Lua reject(節省頻寬與記憶體)。
|
||||||
|
|
||||||
|
> ⚠️ pre-check 與 Lua claim 之間仍有 race(兩個 request 同時通過 pre-check),最終 atomicity 仍由 Lua 保證;pre-check 純粹是「optimization」。
|
||||||
|
|
||||||
|
### Sec M5:mount-time STORAGE_BACKEND 檢查
|
||||||
|
|
||||||
|
`createJobsRouter` 在 mount 時就檢查 `STORAGE_BACKEND === 'minio'`:
|
||||||
|
- 不對 → **不掛 multer**,POST /api/v1/jobs 直接回 500 misconfiguration
|
||||||
|
- 不會吃 multipart body,避免 misconfig 也消耗 500MB 記憶體
|
||||||
|
- GET / DELETE / download-tokens 不依賴 storage backend,仍正常掛
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Auth Security
|
||||||
|
|
||||||
|
### JWT Algorithm Pin(Sec m3)
|
||||||
|
|
||||||
|
`src/auth/jwks.js` 明確 pin 接受的 JWT signing algorithm:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const ALLOWED_JWT_ALGS = ['RS256', 'ES256', 'PS256'];
|
||||||
|
```
|
||||||
|
|
||||||
|
拒絕:
|
||||||
|
- `none`(jose 預設拒絕,但仍明確列出)
|
||||||
|
- `HS256` / `HS384` / `HS512`(HMAC,避免演算法混淆攻擊)
|
||||||
|
|
||||||
|
### JWKS Cache
|
||||||
|
|
||||||
|
- TTL 10 分鐘(`JWKS_CACHE_MAX_AGE_MS` env override)
|
||||||
|
- Cooldown 30 秒(避免 JWKS endpoint 失敗時 thundering herd)
|
||||||
|
- 模組層級 cache(同一個 jwksUrl 共用一個 RemoteJWKSet)
|
||||||
|
|
||||||
|
### Token 驗證
|
||||||
|
|
||||||
|
| 檢查項 | jose 預設 | Converter 加碼 |
|
||||||
|
|-------|----------|----------------|
|
||||||
|
| signature | ✅ | — |
|
||||||
|
| exp | ✅ | clockTolerance 60s |
|
||||||
|
| nbf | ✅ | — |
|
||||||
|
| issuer | — | ✅(`MEMBER_CENTER_ISSUER`)|
|
||||||
|
| audience | — | ✅(`KNERON_CONVERTER_AUDIENCE`)|
|
||||||
|
| algorithm | 拒絕 none | ✅ pin to RS256/ES256/PS256(Sec m3)|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Rate Limiting
|
||||||
|
|
||||||
|
### 雙層設計
|
||||||
|
|
||||||
|
```
|
||||||
|
Request → IP-based limiter (200 req / 15min) ← app.js 全域
|
||||||
|
→ requireAuth (驗 token)
|
||||||
|
→ per-client_id limiter (300 req / 5min) ← v1 jobs router
|
||||||
|
→ multer / handler
|
||||||
|
```
|
||||||
|
|
||||||
|
| 層級 | 目的 | 範圍 |
|
||||||
|
|------|------|------|
|
||||||
|
| IP-based | 防匿名流量 / DDoS | 全 `/api` 前綴 |
|
||||||
|
| per-client_id | 合約上限 / 攻擊速度限制 | `/api/v1/jobs` 寫入端點 |
|
||||||
|
|
||||||
|
### Phase 2 待補
|
||||||
|
|
||||||
|
- Memory store 警告:目前用 process-local memory,**多 instance 部署需改 Redis store**
|
||||||
|
- 目前對「未認證但合法路由」的 quota 計算可能誤殺 — 預設 1 個 visionA-backend IP 帶多 user 共用,需要監控 IP-based 是否誤殺
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Logging
|
||||||
|
|
||||||
|
### 結構化 JSON
|
||||||
|
|
||||||
|
所有 log 使用結構化 JSON 格式,必含:
|
||||||
|
- `timestamp`(ISO 8601)
|
||||||
|
- `level`(INFO / WARN / ERROR)
|
||||||
|
- `service`(`task-scheduler`)
|
||||||
|
- `request_id`(貫穿請求生命週期)
|
||||||
|
- `action`(`domain.action` 格式)
|
||||||
|
|
||||||
|
### 敏感資料保護
|
||||||
|
|
||||||
|
**絕對不寫 log**(已逐條檢查):
|
||||||
|
- token / Authorization header
|
||||||
|
- file body / model 內容
|
||||||
|
- MinIO secret / OAuth client_secret
|
||||||
|
- JWT payload 完整 dump(只記 `client_id` / `tenant_id` / `user_id`)
|
||||||
|
|
||||||
|
**遮罩處理**(如有需要在 Phase 2 加):
|
||||||
|
- 原始 filename(已 sanitize)— 通常不視為敏感
|
||||||
|
- IP(log 仍記,但 GDPR 場景可能需要遮罩)
|
||||||
|
|
||||||
|
### Sec C1 暫緩
|
||||||
|
|
||||||
|
`.env` 一度被 commit 進 git history(健檢時發現),**已加入 `.gitignore` 但 history 仍可追溯**。
|
||||||
|
|
||||||
|
決策:
|
||||||
|
- **Phase 1 暫緩**(2026-04-25 使用者裁決)
|
||||||
|
- **Phase 1 ready 後**會做一次 git history rewrite + 強制 rotate 所有 secret
|
||||||
|
- 在那之前,所有 secret 都被視為「可能已外洩」,**dev 環境用 dummy secret,prod 用全新生成的 secret**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 2 候補方案清單
|
||||||
|
|
||||||
|
### 已知待補強(依優先級)
|
||||||
|
|
||||||
|
| # | 項目 | 優先級 | 預期任務 |
|
||||||
|
|---|------|-------|---------|
|
||||||
|
| 1 | **HMAC-signed user_id 或 OBO token**(解決 Trust Boundary)| HIGH | Phase 2 — auth 強化 |
|
||||||
|
| 2 | **Git history rewrite**(清掉 .env 洩漏)| HIGH | Phase 1 ready 收尾 |
|
||||||
|
| 3 | **MULTIPART_MODEL_MAX_BYTES env 串接**(目前寫死 500MB)| MEDIUM | T10 |
|
||||||
|
| 4 | **MAX_CONCURRENT_UPLOADS semaphore**(防多 user 並發 OOM)| MEDIUM | T10 |
|
||||||
|
| 5 | **Stream storage 評估**(取代 memoryStorage,根本解決 OOM)| MEDIUM | Phase 2 — infra |
|
||||||
|
| 6 | **Rate limiter Redis store**(多 instance 部署前提)| MEDIUM | Phase 2 — infra |
|
||||||
|
| 7 | **Audit anomaly detection**(user_id pattern 異常告警)| LOW | Phase 2 — observability |
|
||||||
|
| 8 | **Filename Unicode normalization**(極端 unicode bypass)| LOW | Phase 2 — security 細修 |
|
||||||
|
| 9 | **Metadata prototype pollution 防護**(白名單 keys)| LOW | Phase 2 — security 細修 |
|
||||||
|
| 10 | **Token revocation list / JWT blacklist**(無此需求現在)| LOW | Phase 2 — auth |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 變更歷史
|
||||||
|
|
||||||
|
| 日期 | 變更 | 觸發 |
|
||||||
|
|------|------|------|
|
||||||
|
| 2026-04-25 | 初版 | T5 Reviewer + Security Audit 修復 |
|
||||||
Loading…
x
Reference in New Issue
Block a user