visionA/docs/autoflow/04-architecture/adr/adr-014-conversion-integration.md
jim800121chen fb7da5d180 chore(autoflow): migrate .autoflow/ 共享層文件至 docs/autoflow/
依 autoflow-agent workspace v2 設計把 PRD / 設計 / 架構 / 交付類
共享文件從個人層 .autoflow/(ignored)搬到 docs/autoflow/(進 git),
讓團隊可共享產品與架構文件,個人層只留 progress / review / testing 等
per-branch 筆記。

- 02-prd/        21 個檔(PRD、features、market-analysis 等)
- 03-design/     18 個檔(design-spec、wireframes、flows 等)
- 04-architecture/ 31 個檔(TDD、design-doc、ADR×14、API 規格等)
- 07-delivery/   3 個檔(project-summary、phase-0.6-handover、stage-deployment-setup)

合計 73 檔。原檔已從 .autoflow/ 移除(migration 工具執行 git mv,
但因 .autoflow/ 在 .gitignore 中、git 將此操作視為新增、無 rename history)。
2026-05-04 16:55:55 +08:00

233 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# ADR-014visionA 端轉檔功能架構Phase 0.8
## 狀態
Accepted — 2026-04-30
## 上位 / 同層 ADR
- 沿用:[ADR-006](./adr-006-no-redis-in-prototype.md)in-memory state、[ADR-010](./adr-010-oidc-bff.md)OIDC BFF + confidential client、[ADR-011](./adr-011-supersede-adr-005.md)OIDC 取代 StaticAuth、[ADR-013](./adr-013-public-client.md)user 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.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** 將部署到 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 job`409 user_has_active_job`
- FAA delegated download token TTL 短515 分鐘),可給 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 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_id`OIDC 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
```
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` action`FileAccessAgent.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 個 scope**visionA-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}/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 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.RWMutex``exp - 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/jobs`init | 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_failed`job 留在 completed 狀態user 可重試 |
| FAA pull加到模型庫| 5xx / network 退避 max 2 次 | 失敗回 frontend `502 faa_unavailable`model 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 的流量成本問題 |
### 方案 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 其他 APIuser 組織查詢 / 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 共用一個 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|
## 合規性
- [x] 與 PM Agent 確認:對齊 PRD Phase 0.8 範圍(半自動 / 模型 ≤500MB / ref_images ≤100×10MB / 同 user 1 active job / 不做歷史/取消/SSE/多 chip
- [x] 與 Architect 確認:模組切分(`internal/conversion/`)、不擴 model schema、沿用 `/api/models/init+finalize`
- [x] 使用者裁決upload 走 backend、download 走 delegated、半自動分流、不擴 schema
- [ ] DevOps 待確認visionA stage → 192.168.0.130 的網路可達性VPN / 直通)
- [x] FAA CORSPhase 0.8 採 server-side 302 redirect**不需要** CORS 設定(仿 FAA TestSite `DownloadFileDirect` pattern
- [ ] MC 待確認service client `23605e14a2c64660abd97e29963d8d58` 已授權 4 個 scope
## 相關文件
- 上位:`prd.md`Phase 0.8 轉檔功能 PRDPM 領地)
- 同層:`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 |