致命發現(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>
19 KiB
ADR-014:visionA 端轉檔功能架構(Phase 0.8)
狀態
Accepted — 2026-04-30 / §2 download flow 部分 supersede — 2026-05-16(ADR-016)
2026-05-16 更新:§2「Download — FAA delegated token(browser 直連 / 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 仍有效的段落:§1(upload streaming proxy)、§3(半自動分流的「加到模型庫」原則,但 server-side pull 的 FAA 部分改走 converter)、§4(模組劃分)、§5(service token cache 僅 converter 部分已被 ADR-015 §1 supersede;FAA 部分被 ADR-016 supersede)、§6(user_id trust boundary,核心原則完全不變)、§7(FAA / MC 相關 row 被 ADR-016 supersede;converter row 維持)、§8(active job 衝突處理不變)。
上位 / 同層 ADR
- 沿用:ADR-006(in-memory state)、ADR-010(OIDC BFF + confidential client)、ADR-011(OIDC 取代 StaticAuth)、ADR-013(user OIDC client 為 public + PKCE-only;service client 仍為 confidential)
- 並存:本 ADR 規範「visionA-backend 同時當 multipart streaming proxy(upload)+ delegated download token broker(download)」
背景 (Context)
Phase 0.8 要把 kneron_model_converter(以下簡稱 converter)整合進 visionA Cloud。雙方為各自獨立部署的後端:
- converter 仍在公司內網
192.168.0.130:POST /api/v1/jobs(multipart, 500MB cap)/GET /api/v1/jobs/{id}poll /POST /api/v1/jobs/{id}/promote推 NEF 到 File Access Agent (FAA) - visionA-backend 將部署到 AWS(stage 已上
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
整合上必須回答兩個問題:
- Upload(轉檔 input) 怎麼進 converter?browser 直連 vs visionA backend 中轉?
- Download(轉檔結果) 怎麼出 FAA?browser 直連 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 job(409 user_has_active_job) - FAA delegated download token TTL 短(5–15 分鐘),可給 browser 直連
- Member Center service client(
23605e14a2c64660abd97e29963d8d58)已配置,需 4 個 scope:converter:job.write/read、files: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 token(browser 直連) 的非對稱設計,並把 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 在這條路徑做的事:
- 從 cookie session 取
user_id(OIDC sub),灌進 converter request 的user_id表單欄位 - 跟 MC 取 service token(scope
converter:job.write),帶在Authorization: Bearer - 透傳 model file + ref_images[] + 其他 form fields(target_chip / 各 enable_* flag)
- converter response 整形後回 frontend(不直接洩 converter response shape)
- 從 cookie session 取
- converter 零修改 — 沿用既有
POST /api/v1/jobsmultipart endpoint
2. Download — 多次性 → FAA delegated token(server-side 302 redirect → browser 直連 FAA)
⚠️ 2026-05-16:本節整段被 ADR-016 supersede。
致命發現(2026-05-16):
- MC source 沒有
POST /file-access/download-tokensendpoint(無法 issue delegated token)- MC source 沒有
IDelegatedDownloadTokenValidator對應的 introspection endpoint(即使有 token 也無法 validate)- FAA
GET /files/{key}強制只接 delegated token、不接 service token(visionA 即使能拿 service token 也打不進去)本節下方描述的「server-side 302 redirect」/「跟 MC 換 delegated token」/「FAA 跟 MC validate」整條鏈從 2026-05-02 起即為 broken design、從未 e2e 跑通。
新設計(ADR-016):visionA 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 多次下載到不同 device,N 次跨 internet 流量燒不起
- FAA 收到 token 後線上跟 MC validate(FAA 自己跟 MC 對打,visionA-backend 不參與)
- visionA-backend 在這條路徑做的事(單一 GET endpoint 內完成):
- 既有 OIDC AuthMiddleware 驗 cookie session 拿 user_id
- 確認該 user 對該 job 有權(從 visionA 內部記錄查 ownership,禁止讓 client 直接傳 object_key)
- server-to-server 跟 MC 換 delegated token(scope
files:download.delegate) - 組 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
DownloadFileDirectaction(FileAccessAgent.TestSite/Controllers/HomeController.cs:255-282)— 也是 server 端組 URL 後return Redirect(directUrl),token 不過 frontend JS
為什麼 302 redirect 比「frontend 拿 token + navigation」更安全
| 面向 | 方案 X(frontend 拿 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 場景會自動 follow,response body 為 FAA 內容;但 anchor / navigation 場景 JS 完全看不到) |
| 需要 FAA CORS 設定 | ✗ 需要(fetch / XHR 受 CORS 限制) | ✓ 不需要(CORS 只管 JS fetch / XHR;server-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 個 scope:visionA-backend 為了跟 MC 換 delegated token,service token 仍需 files:download.delegate scope(沒變)。302 redirect 是「換到 token 後怎麼把它送進 browser」的差異,不影響 token issuance 路徑。
3. 半自動 — converter 完成後使用者選擇路徑
job completed 後 frontend 詢問 user:
| 動作 | 路徑 | 說明 |
|---|---|---|
| 「加到模型庫」 | visionA backend 跟 FAA pull NEF(server-to-server,scope 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}/promote,promote response 含 target_object_key。
4. 模組劃分 — 新增 internal/conversion/
不擴 model.Model schema(Source / SourceJobID 欄位 ADR-005 / database.md 已預埋)。新增獨立 package:
internal/conversion/
├── conversion.go # 對外 interface (Service)
├── converter_client.go # converter scheduler API client
├── faa_client.go # FAA API client(delegated 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.RWMutex),exp - 15s重取 - token request 失敗:4xx 不重試(log + 5xx response 給 client);5xx 指數退避 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_id(converter 端的 trust boundary 設計詳見 converter openapi.yaml)
- visionA-backend 必須確保:
- 任何呼叫 converter 的 endpoint 一律先過 OIDC AuthMiddleware(既有)
- job_id → user_id 的 mapping 記在 visionA 內部(in-memory 或之後 DB),每次 status / promote / download token 操作前 ownership 檢查
- 絕不接受 client 直接傳 user_id / object_key — 一律從 session 反查
7. 失敗模式 retry 矩陣
| 操作 | 重試策略 | 失敗回 frontend |
|---|---|---|
POST /api/v1/jobs(init) |
4xx 不重試;5xx / network 退避 max 2 次 | 4xx 透傳 converter error code;5xx 一律 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_failed,job 留在 completed 狀態,user 可重試 |
| FAA pull(加到模型庫) | 5xx / network 退避 max 2 次 | 失敗回 frontend 502 faa_unavailable,model record 不寫入 |
| MC token endpoint | 4xx fatal;5xx 退避 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 提示「你已有進行中的轉檔任務」。
考慮過的替代方案
方案 A:Upload 也走 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 成本可接受 |
方案 B:Download 也走 visionA backend 中轉
| 評估 | 內容 |
|---|---|
| 優點 | visionA-backend 看得到所有下載流量、易做 audit |
| 缺點 | (1) 跨 internet 流量 N 倍(同 NEF 多次下載);(2) visionA-backend 變成 streaming bottleneck;(3) FAA delegated token 機制(已實作)白做 |
| 排除原因 | 流量成本;FAA 已具備 delegated token,不用浪費 |
方案 C:Upload + Download 都走 backend 中轉(對稱設計)
| 評估 | 內容 |
|---|---|
| 排除原因 | 同方案 B 的流量成本問題 |
方案 D:擴 model.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 其他 API(user 組織查詢 / push 通知)零成本擴展
- 不破壞既有 model store:沿用
/api/models/init+finalize,conversion 只是「來源不同」
負面影響(接受的取捨)
- 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 共用一個 cache,token 失效會同時影響轉檔與下載(MVP 階段可接受;後續可拆 cache)
- 取消 job 不做:user 一旦 init 就要等到 converter 自己跑完或 timeout(converter 端 expires_at 7 天)
風險
| 風險 | 緩解 |
|---|---|
| visionA-backend 處理 500MB upload 時記憶體爆 | 嚴格 streaming(io.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.130;prod 上線前需 converter 也上 AWS 或開 VPN tunnel — 列入 prod 上線 blocker |
| 同 user 多 tab 各 init 一個 job → converter 409 | frontend 在 init 前先打 visionA backend 查當前 user 有無 active job;backend 直接拒絕第二次 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 CORS:Phase 0.8 採 server-side 302 redirect,不需要 CORS 設定(仿 FAA TestSite
DownloadFileDirectpattern) - MC 待確認:service client
23605e14a2c64660abd97e29963d8d58已授權 4 個 scope
相關文件
- 上位:
prd.md(Phase 0.8 轉檔功能 PRD,PM 領地) - 同層:
adr-006-no-redis-in-prototype.md(in-memory token cache 沿用)、adr-010-oidc-bff.md(OIDC BFF)、adr-011-supersede-adr-005.md、adr-013-public-client.md(service client 仍為 confidential) - 詳細實作:
conversion.md(本 ADR 實作 spec)、api/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 DownloadFileDirect),token 不過 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。 |