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>
725 lines
41 KiB
Markdown
725 lines
41 KiB
Markdown
# 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`(索引)+ 子模組。
|