visionA/docs/autoflow/04-architecture/adr/adr-014-conversion-integration.md
jim800121chen dab13ed984 docs(autoflow): ADR-016 — visionA download 改走 converter GetResult,撤回 FAA delegated token 鏈
致命發現(grep MC + FAA source 確認):
- MC source 沒有 issue delegated download token endpoint
- MC source 沒有 validate delegated download token endpoint
- FAA MemberCenterDelegatedDownloadTokenValidator.cs 假設的 MC introspection endpoint 不存在
- ADR-014 §2 從 5/2 寫完到現在這條鏈一直是斷的、只是因為從未實際 e2e 跑通過所以沒被發現

使用者拍板硬約束:不動 MC + 不動 FAA

新增 ADR-016:
- visionA download 改用 converter GET /api/v1/jobs/{id}/result(新 endpoint)
- visionA backend 用既有 ConverterAPIKey 認證(不需新增 secret)
- 維持 T4 已實作的 stream proxy 結構(io.CopyN + Content-Disposition + size cap)
- promote 仍 PUT FAA(converter 內部用自己的 OAuth、與 visionA 無關)
- 不需動 MC + FAA + warrenchen
- 6 個替代方案逐一說明排除理由

修訂既有文件:
- ADR-014 v1.1 → v1.2:§2 download flow 標註被 ADR-016 部分 supersede
- ADR-015 v2.0 → v2.1:§2 visionA → FAA delegated token 設計(v2.0 從 v1.x 撤回的設計)再次撤回;§9 env 表撤回 v2.0 加回的 OIDC ServiceClient* / TenantID / FAABaseURL;visionA 端 server-to-server 只剩 ConverterAPIKey 一把
- conversion.md v0.5 → v0.6:§1 sequence diagram 重畫(移除 MC node)、§2 模組設計(mc_token_client.go 整檔刪除確認、faa_client.go 改名 converter_result_client.go)、§3.2 visionA → FAA 整段標撤回、§4.1 download handler 改 converter.GetResult、§6 錯誤碼撤回 mc/faa 三個 code 加 result_not_found / result_expired
- api-conversion.md v0.5 → v0.6:檔頭 Auth 段落改寫、§4 download endpoint 改述、error code 表撤回 mc_token_unavailable / download_token_failed
- oidc-tdd.md v0.3 → v0.4:§13.1 環境變數表 OIDC ServiceClient* / TenantID / FAABaseURL 從「重新啟用」改回「再次廢棄」、§13.1.1 stage env 範例移除 service client / tenant_id / FAA URL、§13.1.3 改寫為「v0.4 單線設計」說明

整體影響:
- 不需復活 mc_token_client.go(commit 86b7175 砍除狀態維持)
- 不需復活 OIDCConfig.ServiceClientID/Secret/TenantID(commit 86b7175 移除狀態維持)
- visionA backend faa_client.go 要改名為 converter_result_client.go、改呼叫 converter.GetResult
- visionA backend flow.go DownloadStream / PromoteToModels 改用 converter.GetResult
- jimchen 跨 repo 任務:converter scheduler 加 GET /api/v1/jobs/{id}/result endpoint

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 12:30:46 +08:00

19 KiB
Raw Blame History

ADR-014visionA 端轉檔功能架構Phase 0.8

狀態

Accepted — 2026-04-30 / §2 download flow 部分 supersede — 2026-05-16ADR-016

2026-05-16 更新§2「Download — FAA delegated tokenbrowser 直連 / v1.1 後 server-side proxy」整段被 ADR-016 部分 supersede。原因對 MC source 完整驗證後發現「MC issue + validate delegated download token」endpoint 從未存在——本 §2 從 2026-05-02 寫定起即為 broken design只是因從未 e2e 跑通 visionA → FAA download 而未被發現。

新設計visionA download 改走 converter 新增的 GET /api/v1/jobs/{id}/result + visionA stream 中轉jimchen 可單方控制兩端、不必動 MC / FAA / warrenchen。詳見 ADR-016。

本 ADR 仍有效的段落§1upload streaming proxy、§3半自動分流的「加到模型庫」原則但 server-side pull 的 FAA 部分改走 converter、§4模組劃分、§5service token cache 僅 converter 部分已被 ADR-015 §1 supersedeFAA 部分被 ADR-016 supersede、§6user_id trust boundary核心原則完全不變、§7FAA / MC 相關 row 被 ADR-016 supersedeconverter row 維持、§8active job 衝突處理不變)。

上位 / 同層 ADR

  • 沿用:ADR-006in-memory stateADR-010OIDC BFF + confidential clientADR-011OIDC 取代 StaticAuthADR-013user OIDC client 為 public + PKCE-onlyservice client 仍為 confidential
  • 並存:本 ADR 規範「visionA-backend 同時當 multipart streaming proxyupload+ delegated download token brokerdownload

背景 (Context)

Phase 0.8 要把 kneron_model_converter以下簡稱 converter整合進 visionA Cloud。雙方為各自獨立部署的後端

  • converter 仍在公司內網 192.168.0.130POST /api/v1/jobsmultipart, 500MB cap/ GET /api/v1/jobs/{id} poll / POST /api/v1/jobs/{id}/promote 推 NEF 到 File Access Agent (FAA)
  • visionA-backend 將部署到 AWSstage 已上 https://stage-9527.innovedus.com:9527/
  • FAA 是 ASP.NET Core stateless 服務,存放 NEF支援 GET /files/{key}?access_token=<delegated> browser 直連
  • Innovedus Member Center (MC) 是 OAuth/OIDC IdP同時負責簽 service-to-service token 與 delegated download token

整合上必須回答兩個問題:

  1. Upload轉檔 input 怎麼進 converterbrowser 直連 vs visionA backend 中轉?
  2. Download轉檔結果 怎麼出 FAAbrowser 直連 vs visionA backend 中轉?

並存的設計約束:

  • visionA-backend 是 user 身份 / OIDC sub 注入 converter user_id 表單欄位的唯一可信任點converter 完全信任 caller 帶來的 user_id見 converter openapi.yaml ## user_id 與 trust boundary
  • converter 一個 user_id 同時間只能有 1 個 active job409 user_has_active_job
  • FAA delegated download token TTL 短515 分鐘),可給 browser 直連
  • Member Center service client23605e14a2c64660abd97e29963d8d58)已配置,需 4 個 scopeconverter:job.write/readfiles:download.read/delegate
  • internal/config.OIDCConfig.ServiceClientID/Secret 鉤子在 ADR-013 / Phase 0.7 已預埋但未啟用A1 階段)

Phase 0.8 MVP 範圍:上傳 → 轉檔 → 半自動處理user 完成後選「加到模型庫」or「下載」Non-Goals:歷史 / 取消 / SSE 進度推送 / 同 user 多個 active job / 多 chip 同時轉。

決策 (Decision)

Upload 走 visionA backend streaming proxy + Download 走 FAA delegated tokenbrowser 直連) 的非對稱設計,並把 visionA-backend 同時當 multipart streaming proxy + delegated download token broker

1. Upload — 一次性 → visionA backend 中轉

Browser ──multipart──► visionA backend ──multipart streaming──► converter
                       (io.Pipe + multipart.Reader/Writer)
  • 每個檔案只上傳「一次」,跨 internet 一次成本可接受500MB × 1 次 vs 500MB × N 次下載)
  • io.Pipe + goroutine一邊讀 client、一邊寫 converter — 不暫存 disk、不 buffer 全 RAM
  • visionA-backend 在這條路徑做的事:
    1. 從 cookie session 取 user_idOIDC sub灌進 converter request 的 user_id 表單欄位
    2. 跟 MC 取 service tokenscope converter:job.write),帶在 Authorization: Bearer
    3. 透傳 model file + ref_images[] + 其他 form fieldstarget_chip / 各 enable_* flag
    4. converter response 整形後回 frontend不直接洩 converter response shape
  • converter 零修改 — 沿用既有 POST /api/v1/jobs multipart endpoint

2. Download — 多次性 → FAA delegated tokenserver-side 302 redirect → browser 直連 FAA

⚠️ 2026-05-16本節整段被 ADR-016 supersede

致命發現2026-05-16

  1. MC source 沒有 POST /file-access/download-tokens endpoint無法 issue delegated token
  2. MC source 沒有 IDelegatedDownloadTokenValidator 對應的 introspection endpoint即使有 token 也無法 validate
  3. FAA GET /files/{key} 強制只接 delegated token、不接 service tokenvisionA 即使能拿 service token 也打不進去)

本節下方描述的「server-side 302 redirect」/「跟 MC 換 delegated token」/「FAA 跟 MC validate」整條鏈從 2026-05-02 起即為 broken design、從未 e2e 跑通。

新設計ADR-016visionA download 改走 converter 新增的 GET /api/v1/jobs/{id}/result + visionA stream 中轉。不再有 visionA → MC、visionA → FAA 任何 server-to-server 路徑。

本節以下內容僅作歷史保留、實作以 ADR-016 為準。

Browser ──GET /api/conversion/{job_id}/download──► visionA backend
                                                        ↓
                                                   ownership 檢查
                                                        ↓
                                                   MC POST /file-access/download-tokens
                                                        ↓
Browser ◄─── HTTP 302 Found, Location: https://faa/files/{key}?access_token=<delegated>
   ↓
   browser 自動 follow redirect
   ↓
Browser ──直連 FAA──► GET /files/{key}?access_token=<delegated>
  • 同 NEF 可能被同一 user 多次下載到不同 deviceN 次跨 internet 流量燒不起
  • FAA 收到 token 後線上跟 MC validateFAA 自己跟 MC 對打visionA-backend 不參與)
  • visionA-backend 在這條路徑做的事(單一 GET endpoint 內完成):
    1. 既有 OIDC AuthMiddleware 驗 cookie session 拿 user_id
    2. 確認該 user 對該 job 有權(從 visionA 內部記錄查 ownership禁止讓 client 直接傳 object_key
    3. server-to-server 跟 MC 換 delegated tokenscope files:download.delegate
    4. 組 download URL 後直接 c.Redirect(http.StatusFound, downloadURL) — 把 token 放在 Location header
  • visionA-frontend 不需處理 token<a href="/api/conversion/{job_id}/download" download>下載</a>window.location.href = '/api/conversion/{job_id}/download' 即可browser 自動 follow 302
  • Pattern 對齊:仿 FAA TestSite DownloadFileDirect actionFileAccessAgent.TestSite/Controllers/HomeController.cs:255-282)— 也是 server 端組 URL 後 return Redirect(directUrl)token 不過 frontend JS

為什麼 302 redirect 比「frontend 拿 token + navigation」更安全

面向 方案 Xfrontend 拿 token JSON 方案 ✓server 302 redirect
Token 在 fetch response body ✗ 在JS 看得到、可能進 console.log / Sentry / 第三方分析) ✓ 不在(沒有 JSON response
Token 在 URL bar ✗ 在(window.location.href = url 之後 URL bar 會短暫顯示) △ 短暫302 的 final URL 仍會出現,但 browser navigation 完成後通常立即被 FAA download 流程取代;且 navigation 期間 history entry 可被 Cache-Control: no-store + 短 TTL 緩解)
Token 在 localStorage / sessionStorage △ 視 frontend 實作(容易誤存) ✓ 結構性不可能(沒入口)
受 frontend XSS 影響 ✗ XSS 可竊取 token ✓ XSS 看不到302 在 fetch 場景會自動 followresponse body 為 FAA 內容;但 anchor / navigation 場景 JS 完全看不到)
需要 FAA CORS 設定 ✗ 需要fetch / XHR 受 CORS 限制) ✓ 不需要CORS 只管 JS fetch / XHRserver-side 302 + browser navigation 走「navigation request」完全不適用 CORS
跟 visionA OIDC cookie session 整合 △ 額外 endpoint + JSON 流程 ✓ 自然整合GET endpoint 走既有 AuthMiddleware
Frontend 程式碼複雜度 fetch → 取 url → navigation 低(一個 anchor tag / 一行 navigation

Token 仍需 4 個 scopevisionA-backend 為了跟 MC 換 delegated tokenservice token 仍需 files:download.delegate scope沒變。302 redirect 是「換到 token 後怎麼把它送進 browser」的差異不影響 token issuance 路徑。

3. 半自動 — converter 完成後使用者選擇路徑

job completed 後 frontend 詢問 user

動作 路徑 說明
「加到模型庫」 visionA backend 跟 FAA pull NEFserver-to-serverscope files:download.read)→ 走既有 /api/models/init + /api/models/finalize 三段式 upload flow → 寫進 visionA model.Model.Source="converted" + SourceJobID=<converter-job-id> 進 visionA storage 給後續 device load 用,這次走 backend 因為終點是 visionA storage
「下載」 上述 §2 流程 browser 直連 FAA

兩者都先呼叫 converter POST /api/v1/jobs/{id}/promotepromote response 含 target_object_key

4. 模組劃分 — 新增 internal/conversion/

不擴 model.Model schemaSource / SourceJobID 欄位 ADR-005 / database.md 已預埋)。新增獨立 package

internal/conversion/
├── conversion.go        # 對外 interface (Service)
├── converter_client.go  # converter scheduler API client
├── faa_client.go        # FAA API clientdelegated token + server-to-server pull
├── mc_token_client.go   # MC client_credentials grant + token cache
└── flow.go              # 整體 flow 協調init / poll / promote / pull / persist

internal/conversion/ 依賴 internal/model.Repository(沿用既有 /api/models/init+finalize 邏輯,不繞過)。

5. Service token cache — 仿 converter scheduler 模式

  • visionA backend 啟動時不主動取lazy第一次需要時才打 MC POST {issuer}/oauth/token (grant_type=client_credentials)
  • token cache記憶體 + sync.RWMutexexp - 15s 重取
  • token request 失敗4xx 不重試log + 5xx response 給 client5xx 指數退避 max 2 次
  • visionA-backend 預設 service-to-service token 共用converter:job.write / read / files:download.read / delegate 同一 client + 同一個 cache— MC 端發單一 token 含所有 4 個 scope

6. user_id 注入 + trust boundary

  • visionA backend 是唯一灌 user_id 的點:從 cookie session 拿 OIDC sub → POST /jobs 時帶 user_id
  • converter 信任 visionA backend 帶來的 user_idconverter 端的 trust boundary 設計詳見 converter openapi.yaml
  • visionA-backend 必須確保:
    1. 任何呼叫 converter 的 endpoint 一律先過 OIDC AuthMiddleware既有
    2. job_id → user_id 的 mapping 記在 visionA 內部in-memory 或之後 DB每次 status / promote / download token 操作前 ownership 檢查
    3. 絕不接受 client 直接傳 user_id / object_key — 一律從 session 反查

7. 失敗模式 retry 矩陣

操作 重試策略 失敗回 frontend
POST /api/v1/jobsinit 4xx 不重試5xx / network 退避 max 2 次 4xx 透傳 converter error code5xx 一律 502 converter_unavailable
GET /api/v1/jobs/{id}poll 5xx / network 退避 max 3 次;各次 2s 內 timeout 持續失敗 → frontend 視為 stuck提示重試
POST /promote 5xx / network 退避 max 2 次 失敗回 502 promote_failedjob 留在 completed 狀態user 可重試
FAA pull加到模型庫 5xx / network 退避 max 2 次 失敗回 frontend 502 faa_unavailablemodel record 不寫入
MC token endpoint 4xx fatal5xx 退避 max 2 次 失敗回 frontend 503 idp_unavailable
MC delegated token 4xx 透傳5xx 退避 max 2 次 失敗回 frontend 502 download_token_failed

8. 同 user active job 衝突409

converter 回 409 user_has_active_job → visionA-backend 透傳 409 active_job_exists + 既有 job 詳情給 frontend由 frontend 提示「你已有進行中的轉檔任務」。

考慮過的替代方案

方案 AUpload 也走 browser 直連converter 開放 CORS + 公網)

評估 內容
優點 visionA-backend 不需處理 500MB streaming省記憶體與頻寬
缺點 (1) converter 必須開公網或開 CORS安全表面變大(2) user_id trust boundary 失守browser 自己灌 user_id 等於沒驗);(3) converter 要新增 OIDC delegated upload token 機制converter 團隊額外工作量)
排除原因 user_id 信任邊界守不住converter 端要新增工作量。Upload 一次性,跨 internet 成本可接受

方案 BDownload 也走 visionA backend 中轉

評估 內容
優點 visionA-backend 看得到所有下載流量、易做 audit
缺點 (1) 跨 internet 流量 N 倍(同 NEF 多次下載);(2) visionA-backend 變成 streaming bottleneck(3) FAA delegated token 機制(已實作)白做
排除原因 流量成本FAA 已具備 delegated token不用浪費

方案 CUpload + Download 都走 backend 中轉(對稱設計)

評估 內容
排除原因 同方案 B 的流量成本問題

方案 Dmodel.Model schema 加轉檔狀態

評估 內容
排除原因 (1) 違反 SRP — model 應該只代表「已就緒可載入 device 的模型」;(2) job 狀態屬於 conversion 領域,不該污染 model 領域;(3) model.Model.Source="converted" + SourceJobID 已足夠表達來源關聯

後果 (Consequences)

正面影響

  • converter 零修改:沿用既有 multipart endpoint
  • user_id 信任邊界乾淨visionA-backend 是唯一灌入點,從 OIDC cookie session 拿,不可被偽造
  • 流量成本最佳upload 1× / download N× 的不對稱反映物流現實
  • Service token cache 可重用:之後接 MC 其他 APIuser 組織查詢 / push 通知)零成本擴展
  • 不破壞既有 model store:沿用 /api/models/init+finalizeconversion 只是「來源不同」

負面影響(接受的取捨)

  • visionA-backend 多一塊 streaming proxy 責任:要寫好 io.Pipe + multipart streaming + context cancellation錯誤處理複雜
  • 跨網路依賴增加visionA-backend 失能 → 轉檔功能整個壞MC 失能 → token 無法簽,轉檔不可用
  • MVP 不做進度推送user upload 完看 converter polling status沒 SSE → UX 較粗PRD Phase 0.8 接受)
  • Service token 集中失敗:所有 4 個 scope 共用一個 cachetoken 失效會同時影響轉檔與下載MVP 階段可接受;後續可拆 cache
  • 取消 job 不做user 一旦 init 就要等到 converter 自己跑完或 timeoutconverter 端 expires_at 7 天)

風險

風險 緩解
visionA-backend 處理 500MB upload 時記憶體爆 嚴格 streamingio.Pipe不暫存上線前壓測 1 個 + 2 個併發 upload若有問題降到 200MB cap
Service token endpoint 被打爆(過度頻繁取 token token cache 確保 exp - 15s 內只取一次log 記每次 cache miss
FAA CORS 還沒加 不再阻擋:採用 server-side 302 redirect 後browser navigation 不適用 CORS。Phase 1+ 若要改 fetch + Blob + a.click() 才需要 CORS例如要顯示下載進度條
MC usage=webhook_outbound 命名不對(同 ADR-010 不影響 visionA 程式碼MC 改 web_app 後只需改 admin 註冊欄位
converter 在 visionA 上 AWS 後不可達(網路) Phase 0.8 範圍visionA stage 仍可走 VPN 到 192.168.0.130prod 上線前需 converter 也上 AWS 或開 VPN tunnel — 列入 prod 上線 blocker
同 user 多 tab 各 init 一個 job → converter 409 frontend 在 init 前先打 visionA backend 查當前 user 有無 active jobbackend 直接拒絕第二次 init不打到 converter

合規性

  • 與 PM Agent 確認:對齊 PRD Phase 0.8 範圍(半自動 / 模型 ≤500MB / ref_images ≤100×10MB / 同 user 1 active job / 不做歷史/取消/SSE/多 chip
  • 與 Architect 確認:模組切分(internal/conversion/)、不擴 model schema、沿用 /api/models/init+finalize
  • 使用者裁決upload 走 backend、download 走 delegated、半自動分流、不擴 schema
  • DevOps 待確認visionA stage → 192.168.0.130 的網路可達性VPN / 直通)
  • FAA CORSPhase 0.8 採 server-side 302 redirect不需要 CORS 設定(仿 FAA TestSite DownloadFileDirect pattern
  • MC 待確認service client 23605e14a2c64660abd97e29963d8d58 已授權 4 個 scope

相關文件

  • 上位:prd.mdPhase 0.8 轉檔功能 PRDPM 領地)
  • 同層:adr-006-no-redis-in-prototype.mdin-memory token cache 沿用)、adr-010-oidc-bff.mdOIDC BFFadr-011-supersede-adr-005.mdadr-013-public-client.mdservice client 仍為 confidential
  • 詳細實作:conversion.md(本 ADR 實作 specapi/api-conversion.md(對 frontend 的 API 規格)
  • 安全:security.md §service-to-service token 流程(本次新增)
  • 跨團隊整合:/Users/jimchen/kneron_model_converter/docs/TODO-visionA-integration.md

版本記錄

日期 版本 變更
2026-04-30 1.0 初版 — Phase 0.8 轉檔整合架構決策
2026-04-30 1.1 Download flow 改為 server-side HTTP 302 redirect仿 FAA TestSite DownloadFileDirecttoken 不過 frontend JS、不需 FAA CORS
2026-05-16 1.2 §2 download flow 整段標 supersede by ADR-016:致命發現 MC source 沒有 issue / validate delegated download token endpoint、§2 從 2026-05-02 起即為 broken design。新設計 visionA download 改走 converter GET /api/v1/jobs/{id}/result + visionA stream 中轉。其他段落§1 upload streaming proxy / §3 半自動分流 / §4 模組劃分 / §6 user_id trust boundary / §8 active job 衝突處理維持有效§5 / §7 中 FAA / MC 相關部分連帶 supersede。