jim800121chen cff9236699 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>
2026-05-01 10:59:21 +08:00

41 KiB
Raw Blame History

Design Doc — Kneron Model Converter 對外 APIL 級新功能)

作者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-backendGo 服務Converter API 的消費者Persona C
  • Member CenterOAuth2 / OIDC authorization serverC# / OpenIddict
  • File Access Agenttenant 邊界內檔案閘道C# / ASP.NET Core駐守 NAS 側,單一 tenant per instance

1.2 系統定位(新架構)

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. 取 tokenvisionA-backend → Member Center visionA-backend 以 client_credentials 取得 aud=kneron_converter_api 的 access tokenscope=converter:job.write)。
  2. 建 jobvisionA-backend → Convertermultipart/form-data visionA-backend 以 POST /api/v1/jobs 直接把原始模型 multipart 上傳到 ConverterConverter 驗 OAuth token 後把檔案寫入 Converter Bucketjobs/{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 到 NASvisionA-backend → Converter → File Access Agent 使用者在 VisionA 前端按「加進模型庫」時visionA-backend 呼叫 POST /api/v1/jobs/:id/promoteConverter 以自己的 OAuth client 身分取 files:upload.write token把 Converter Bucket 中的結果檔 PUT 到 File Access Agent。

2. 目標與非目標 (Goals and Non-Goals)

GoalsPhase 1 必達)

  • 對外 API 以 OAuth2 Bearer 驗證,對齊 Innovedus Member CenterJWKS 驗簽)
  • Converter 同時具備 Resource Server(驗他人 tokenOAuth Client(取自己 token雙重身分
  • 提供四個對外端點:POST /api/v1/jobsGET /api/v1/jobsGET /api/v1/jobs/:idPOST /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-GoalsPhase 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與本次獨立
  • 使用者層級 ACLConverter 不管,責任邊界在 visionA-backend
  • 跨 tenant 隔離的複雜授權模型(本次設計為 single-tenant per Converter deployment見 §5.3

3. 架構設計 (The Actual Design)

3.1 架構模式選擇

  • 選擇:維持現有 單體 Task SchedulerNode.js Express+ Worker PoolPython 架構。對外 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 關鍵設計決定)
MinIOConverter 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-havePhase 1 API 契約已足夠支撐。

3.3 技術選型Technology Radar

層級 技術選擇 狀態 選型理由 退出成本
Auth 驗 JWT josenpm Adopt 零依賴純 JS支援 JWKS remote + cache主流專案廣泛採用 低(抽 1 個 middleware 即可換 jsonwebtoken + jwks-rsa
Auth 取 token 自寫輕量 HTTP clientnode-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/cliswagger-ui-express 提供 /openapi.json 檢視 Adopt Phase 1 手寫可控,避免 code-first 產出不穩定
Redis 索引 Redis Setuser:{user_id}:jobs+ 原有 job:{id} Adopt Phase 1 量級不足以需要 PostgreSQL維持 stateless 設計一致 中(未來要遷 PG 需雙寫)
觀測工具 結構化 logJSON+ Nginx access log Trial Phase 1 先不引入 Prometheus留待 Phase 2

3.4 關鍵設計決定:原始模型的上傳路徑

背景

  • visionA-backend 需要把使用者上傳的原始模型交給 Converter 轉檔。
  • 原本考慮過「檔案先上傳 File Access AgentConverter 再從 File Access Agent 拉」的方案,但發現:
    1. 原始模型在轉檔成功、使用者按「加進模型庫」前不屬於 NAS 模型庫,沒必要先進 File Access Agent。
    2. File Access Agent 的 GET /files/{objectKey} 只接受 delegated download tokenConverter 以 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/jobsmultipart/form-data進來時Scheduler 驗 OAuth token、以 multer.memoryStorage() 接收檔案(model required ≤500MB、ref_images[] optional maxCount 100
  2. Scheduler 檢查 user_id 是否已有 in-progress job若無則把 buffer 寫入 Converter Bucketjobs/{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 AgentConverter 再拉 — 需要跨團隊擴充 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-Match304 Not Modified 省流量(採納 Design 建議)

詳細 API spec 見 TDD.md §1

3.6 資料架構

核心資料Redis

Key 類型 內容 TTL
job:{job_id} String (JSON) Job 完整 record新增 user_idtenant_idcreated_by_client_idmetadatastage_timingsexpires_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 個 jobRedis 足以承擔。未來若需要歷史任務持久化、跨 Crash recovery再評估 PG。

資料流Phase 1

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與檔案大小相關 < 5s200MB 檔案) 依 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 ≈ 4s500MB @ 50MB/s ≈ 10s。若觀測到頻繁超 SLAPhase 2 可考慮拆為「建 job」+「上傳 chunk」兩個端點。

4.2 容錯設計

失敗情境 設計應對
Member Center JWKS 不可達 JWKS 本地 cacheTTL 10 minstale-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_largeRedis 不建 job record避免殘留
File Access Agent promote 失敗 Converter Bucket 檔案保留 7 天,使用者可重試 promoteAPI 回 502
Worker Crash 既有 Crash 即 Reset 機制Worker 重啟後繼續 consume Redis Streamin-progress 的 job 若在 Redis 中會被接手前提Redis 沒死)
Redis Crash 符合「Crash 即 Reset」設計哲學所有 job 遺失,使用者重送
Scheduler Crash 同上,重啟後繼續服務;進行中的 Worker 下階段 done 事件會找不到 job record 而被忽略(既有行為)

4.3 災難復原

維持既有 Crash 即 ResetRPO = 無保證Redis 重啟即清空、RTO < 30sDocker restart。本次不新增 DR 機制。


5. 安全架構 (Security)

5.1 威脅模型STRIDE 摘要)

威脅 風險 防護
Spoofing偽造 visionA-backend OAuth2 Bearer JWTMember Center 簽發JWKS 驗簽
Spoofing偽造 user_id 低(接受) 信任 visionA-backend 的 user_idConverter 不做 user ACLPRD §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/*/eventsSSE 不加 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 的邊界(方式 Amultipart 欄位)

再次強調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 都接 Converterclient 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.readPhase 1 不實作,但保留擴展空間。

5.3 Tenant 策略(回應 PM 疑問 A.1.3

決定Phase 1 採 single-tenant per Converter deploymentConverter 不自行管理 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(來自 tokenFile 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 vhost443 對公網):只 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 Bucketbuffer 已在記憶體) 200MB @ 200MB/s ≈ 1s
Redis 寫 job record + 索引 20ms
Enqueue 到 Redis Stream 10ms
總預算200MB 檔案) ~5sp95
總預算500MB 檔案) ~12sp95

若檔案很大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 polling5min 可 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/*/eventsSSE                           │
          │   - /health, /queues/stats                         │
          └──────────────────────────────────────────────────┘
                                 ▲
                                 │ (僅 internal vhost 流入)
                                 │
                        Web UI / 內部工具(內網)

實務上可以兩種實作方式:

  • A. 兩份 Nginx instance(一個 public、一個 internal各自獨立 process
  • B. 一份 Nginx process、兩個 server block,根據 listen 的 interfacepublic 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 CenterPhase 1 規劃)。
  3. 自建 API Key 等於要自己管 secret rotation、scope、audit重複發明輪子。
  4. Member Center 已提供 JWKS、token endpoint、client managementConverter 只要實作 resource server + client 兩個 OAuth2 角色即可。

代價:跨團隊依賴(註冊 audience、client、scope若 Member Center owner 不配合會阻塞。已列入 progress.md 風險。

替代方案

  • A. 自建 API Key簡單但不符合生態標準未來遷移成本高
  • B. mTLS運維成本高對 Node.js 生態不夠友善

ADR-002promote 結果檔採「做法 2」— Converter 自己推到 File Access Agent

狀態Accepted

背景:轉檔完的結果要搬回 File Access Agentpromote)。有三種搬檔做法:

做法 說明 優劣
1 結果檔流經 visionA-backendConverter → visionA-backend → File Access Agent 浪費頻寬、visionA-backend 要扛大檔
2 Converter 自己 PUT File Access Agentfiles: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 AgentvisionA-backend 直接 multipart 上傳 Converter見 §3.4Phase 1 Converter 完全不需要 files:download.read / files:metadata.read scope。

代價

  • Converter 需要取自己的 service tokenOAuth client 邏輯。Token 可 cache、失敗可 retry。
  • Converter 需要設定 KNERON_CONVERTER_CLIENT_ID + CLIENT_SECRETsecret 管理責任)。

替代方案(已排除):

  • 備案 A讓 visionA-backend 自己從 Converter 下載結果、再上傳到 File Access Agent — 浪費頻寬 × 2不合理
  • 備案 BFile Access Agent 主動拉 — 對方沒這個介面,不做

ADR-003user_id 以 multipart 欄位傳遞(方式 A不放 token claim、不放自訂 header

狀態Accepted已由使用者決策

背景:需要記錄 job 是誰的VisionA 使用者 ID。有三種方式

方式 做法 優劣
A 放 multipart 欄位 user_idConverter 信任 與 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 等)放不同位置,增加心智負擔

決定:採方式 Amultipart 欄位)。

理由

  1. client_credentials 是服務對服務的 token沒有「user」概念不應把 user_id 放 claim
  2. POST /api/v1/jobs 本身就是 multipart多一個 user_id 欄位最自然,和 model_idversionplatform 等業務欄位放一起
  3. 與既有 Web UI POST /jobs 的 multipart 欄位路徑一致,程式碼可共用 validation
  4. Converter 不做 user 層 ACLPRD §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-004Polling 而非 Webhook

狀態AcceptedPM 已決策)

背景:轉檔時間長(可能 30s-數分鐘visionA-backend 需要知道何時完成。

決定Phase 1 只提供 pollingGET /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-005Phase 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 產品團隊需有 fallbackDesign Review §6 議題 #1


ADR-006Phase 1 Web UI 不改,維持既有 multipart 路徑

狀態AcceptedPM 已決策)

背景Web UI 目前走 POST /jobsmultipartGET /jobs/:id/eventsSSEGET /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、§7public 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 前解決。建議的 namingkneron_converter_api audience、kneron_converter client、scopes 見 TDD §8Phase 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.writeconverter:job.read,格式對齊 File Access Agent 的 files:*.* 慣例
A.3.2 Effort 估算 見 TDD §12按 T1-T8 拆分,預估 4-5 人週)
A.3.3 OpenAPI 維護策略 手寫 YAMLPhase 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 schemastage_timingsstage_progressexpires_at、結構化 error 全採納 見 TDD §1
2 錯誤碼結構化({error: {code, message, details}}409 帶 active_job 詳情) 全採納 見 TDD §1.5
3 Polling 效能p95 < 200msETag 支援) 採納 見 §6.1、TDD §1.3ETag
4 部署隔離(避免 /jobs/* 公網曝光) 採納方案 BNginx 分流) 見 §7
5 預留擴展metadata、ETA、DELETE 路徑、progress 顆粒度) 大部分採納 API 留 metadata: {}、保留 DELETE 路徑可回 501 Not ImplementedPhase 2 再啟用)
6 promote 同步p95 < 3s 採納 見 §6.1。超過 10s timeout 的 async 模式 Phase 1 不做
7 Rate limit per client_id 採納 見 §6.3

11. 風險與待確認事項(給使用者決策)

2026-04-25 變更後,原 R1File 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 可先不擋 tenantwarn 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 大檔(> 200MBmultipart 上傳會超過 p95 SLA Phase 1 接受SLA 已調整為 5s @ 200MB、12s @ 500MB若觀測到頻繁超 SLAPhase 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 阻塞:見 R1Member 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 2Member Center endpoint 實作完成並提供測試環境

13. 後續步驟

  1. 本 Design Doc 送 PM / Design 交叉審閱
  2. 使用者審核最終版
  3. 跨團隊協調 R1Member Center 註冊 audience / client / scope
  4. 工程師依 TDD.md 的任務清單T1-T8增量式開發
  5. 第二階段Reviewer 每個任務把關

附註:本 Design Doc 約 710 行已超過建議拆分門檻500 行)。本次更新聚焦內容修正,暫不拆分;下輪更新建議拆分為 design-doc.md(索引)+ 子模組。