# 模型上傳流程 — visionA Cloud > 雲端版新增流程。使用者上傳 Kneron 編譯後的 `.nef` 模型檔到雲端 object storage,之後才能選模型燒錄到遠端裝置。 > > **技術背景**(給 Design 協作者):模型檔可能達 100MB,若走 tunnel / API server 會拖慢服務。Architect 決策走 **presigned PUT**:前端向後端要一組有期限的 PUT URL,前端直接把檔案 PUT 到 object storage(S3 / R2 / GCS),不經過 API server 也不經過 local agent tunnel。進度 / 失敗偵測靠瀏覽器 `XMLHttpRequest.upload.onprogress`。 > > 對應頁面:`/models`(入口) + `UploadModelDialog`(模態框,或走獨立 `/models/upload` 頁,見 6 節決策) > > 配套文字版 wireframe:`wireframes/wf-model-upload.md`(本次一併建立概念,實作期補圖) --- ## 1. User Story > **作為** 一個 Kneron 開發者, > **我想要** 把本地編好的 `.nef` 模型檔上傳到雲端, > **這樣** 我就能在任何裝置、任何地方把這個模型燒錄到 Kneron 硬體上。 **成功條件:** - 100MB 雛形上限內的檔案可在 3G 以上網速下 60 秒內完成 - 上傳失敗能明確告訴使用者原因(網路、過期、大小、格式)並可**續傳 / 重試** - 上傳中不阻塞其他頁面操作(背景上傳) --- ## 2. 範圍與限制(Phase 0 雛形) | 項目 | Phase 0 | Phase 1+ | |------|---------|---------| | 單檔最大 | **100 MB**(前端硬限) | 2 GB + 分段上傳 | | 副檔名 | **`.nef`** 限定(前端驗證) | `.nef` / `.onnx` / 其他 | | 同時上傳數 | 1(雛形不支援佇列) | N 個並行 | | 中途暫停 | 不支援 | 支援 | | 續傳 | 失敗後全檔重傳 | 分段續傳 | | 取消 | 支援(abort XHR) | 同 | | 背景上傳 | 不支援(離開頁面 = 中斷)| 支援(Service Worker) | | 病毒掃描進度 | 不顯示(後端非同步處理) | 顯示掃描狀態 | --- ## 3. 完整流程 ``` 使用者在 /models 按「上傳模型」 ↓ 開啟 UploadModelDialog ↓ ┌────────────────────────┐ │ 步驟 A · 選檔 + 填 meta │ │ - 檔案 (.nef) │ │ - 名稱、版本、備註 │ └────────────────────────┘ ↓ 按「開始上傳」 ↓ 前端驗證: - 副檔名 .nef - 大小 ≤ 100 MB - 必填欄位 驗證失敗 → 顯示 error,不發 API ↓ POST /api/models/upload-url body: { filename, size, contentType, metadata } → 後端產 presigned PUT URL(TTL 15 分鐘) ↓ 前端 PUT file 直接到 storage URL(不經 API server) - XHR.upload.onprogress → 更新進度條 - 可 abort ↓ 成功 (HTTP 200) 失敗 ↓ ↓ POST /api/models/confirm 顯示錯誤原因 body: { uploadId, etag } → 重試 / 重新取 URL → 後端驗證 checksum、寫入 DB ↓ Toast「✓ 模型 {name} 已上傳」 關閉 Dialog,回到 /models 列表(新模型顯示在頂部) ↓ 後端非同步掃毒 / 解析 metadata → 狀態更新(WebSocket push) ``` --- ## 4. UI 設計 ### 4.1 入口(`/models` 頁面) ``` ┌────────────────────────────────────────────────┐ │ 模型庫 │ │ 管理雲端上的 Kneron 模型 │ │ │ │ [📤 上傳模型] │ ├────────────────────────────────────────────────┤ │ ModelCard · ModelCard · ModelCard ... │ └────────────────────────────────────────────────┘ ``` 「上傳模型」按鈕: - `variant=default size=default` - 右上角(既有版型) - 圖示:`Upload` (Lucide) ### 4.2 UploadModelDialog — 選檔階段 ``` ┌─────────────────────────────────────────────────┐ │ 上傳模型 [✕] │ ├─────────────────────────────────────────────────┤ │ │ │ ┌───────────────────────────────────────────┐ │ │ │ │ │ │ │ 📁 拖曳 .nef 檔到此處 │ │ │ │ │ │ │ │ 或 [選擇檔案] │ │ │ │ │ │ │ │ 支援格式:.nef · 最大 100 MB │ │ │ │ │ │ │ └───────────────────────────────────────────┘ │ │ (bg-muted/50 border-2 border-dashed, h-48) │ │ │ │ 已選檔案: │ │ ┌───────────────────────────────────────────┐ │ │ │ 📄 yolov5s_kl520.nef │ │ │ │ 47.3 MB · 修改於 2026-04-21 │ │ │ │ [✕ 移除] │ │ │ └───────────────────────────────────────────┘ │ │ (選好後才出現;hover 淡 bg-accent) │ │ │ │ 模型名稱 * │ │ ┌───────────────────────────────────────────┐ │ │ │ YOLOv5s KL520 │ │ │ └───────────────────────────────────────────┘ │ │ (預設帶入檔名去副檔名,可編輯) │ │ │ │ 版本 │ │ ┌───────────────────────────────────────────┐ │ │ │ v1.0 │ │ │ └───────────────────────────────────────────┘ │ │ │ │ 備註(選填) │ │ ┌───────────────────────────────────────────┐ │ │ │ 針對戶外停車場場景訓練 │ │ │ └───────────────────────────────────────────┘ │ │ (Textarea, rows=3) │ │ │ │ [取消] [開始上傳] │ └─────────────────────────────────────────────────┘ ``` **規格:** - Dialog `max-w-lg` - Drop zone:`h-48 border-2 border-dashed rounded-lg bg-muted/50`,hover / drag over 時 `border-primary bg-primary/5` - `` - 「開始上傳」:檔案未選 / 必填未填 → disabled ### 4.3 UploadModelDialog — 上傳中 ``` ┌─────────────────────────────────────────────────┐ │ 上傳中 [取消] │ ├─────────────────────────────────────────────────┤ │ │ │ 📄 yolov5s_kl520.nef │ │ │ │ ████████████████████░░░░░░░░░░ 62% │ │ (Progress 元件, h-2, bg-primary) │ │ │ │ 28.5 MB / 47.3 MB · 3.4 MB/s · 剩餘約 6 秒 │ │ (text-sm text-muted-foreground) │ │ │ │ ⓘ 請勿關閉此視窗或導航到其他頁面 │ │ │ └─────────────────────────────────────────────────┘ ``` **規格:** - Progress bar:既有 `Progress` 元件 - 百分比:`XHR.upload.onprogress` 算 `loaded / total` - 速度:滑動視窗(最近 3 秒的 `loaded` 差 / 時間差) - 剩餘時間:`(total - loaded) / speed`;< 5 秒顯示「即將完成」 - Dialog `onOpenChange` 被阻擋:正在上傳時 ESC / 點外面**不**關 dialog(防誤觸);使用者只能按「取消」 - 「取消」:AlertDialog「確定要取消上傳?已傳的資料會作廢。」→ 確認後 `xhr.abort()` ### 4.4 UploadModelDialog — 成功 ``` ┌─────────────────────────────────────────────────┐ │ │ │ ✓ │ │ (CheckCircle2, green, h-16) │ │ │ │ 上傳完成 │ │ │ │ yolov5s_kl520.nef (47.3 MB) │ │ │ │ 模型即將進入安全掃描,完成後可燒錄 │ │ │ │ [完成] │ └─────────────────────────────────────────────────┘ ``` - 按「完成」關 Dialog,toast「✓ 模型 YOLOv5s KL520 已上傳」 - 列表頂部插入新模型,狀態 badge「掃描中」(Phase 1 精緻化) ### 4.5 UploadModelDialog — 錯誤狀態 不同錯誤顯示不同文案與行動: | 錯誤 | 文案 | CTA | |------|------|-----| | 副檔名錯誤(前端擋)| 「只支援 .nef 檔案,你選的是 .onnx」| [重新選檔] | | 超過大小(前端擋)| 「檔案太大({size} MB),最大允許 100 MB」| [重新選檔] | | 取不到 presigned URL | 「伺服器忙碌,請稍後再試」| [重試] [取消] | | Presigned URL 過期(PUT 403)| 「上傳授權已過期,重新取得中...」→ 自動重拿 URL 重傳(僅 1 次) | 自動 | | 網路中斷 | 「網路中斷,上傳已暫停」| [繼續上傳](雛形=從頭重傳) [取消] | | Storage 回 5xx | 「儲存空間暫時無法回應,請稍後再試」| [重試] [取消] | | Confirm API 失敗 | 「上傳完成,但伺服器確認失敗,請重新上傳」| [重新上傳] | | 取消(使用者觸發)| 不顯示錯誤 | 關 Dialog | 錯誤視覺: ``` ┌─────────────────────────────────────────────────┐ │ ⚠ 上傳失敗 │ │ │ │ yolov5s_kl520.nef │ │ │ │ 網路中斷,上傳已暫停 │ │ │ │ 已上傳 28.5 MB / 47.3 MB(62%) │ │ │ │ [取消] [重試] │ └─────────────────────────────────────────────────┘ ``` --- ## 5. 互動細節 ### 5.1 拖曳上傳 - Drop zone 接受 `.nef`,其他格式:drop 後立刻顯示錯誤 - 整個視窗偵測 `dragenter` 顯示 drop zone 放大提示(`ring-2 ring-primary`) - `dragleave` / drop 後移除 highlight ### 5.2 離開頁面的防護 - 上傳中 `window.onbeforeunload` 提示「上傳進行中,確定要離開嗎?」 - 使用者同意 → abort XHR,資料作廢 ### 5.3 重複檔名 - 若同名模型已存在:Dialog 送出時顯示確認「已存在同名模型,要新增版本還是覆蓋?」 - **新增版本**:送出時把版本欄位自動 `+1`(Phase 0 簡化為使用者自己改版本欄) - **覆蓋**:後端支援後再做(Phase 1) ### 5.4 模型卡片上的新狀態 `ModelCard` 新增狀態 badge(Phase 0 簡單版,Phase 1 強化): | 狀態 | Badge | 可操作 | |------|-------|--------| | uploading(僅上傳期間)| 🔵 上傳中 | 不顯示於列表(Dialog 內)| | scanning | 🟡 掃描中 | 不可燒錄 | | ready | 🟢 可用 | 可燒錄 | | rejected | 🔴 檢測失敗 | 可刪除、不可燒錄 | --- ## 6. 流程 vs 頁面 — Dialog vs Page 決策 **採用:Dialog**(在 `/models` 觸發) 理由: - 雛形只支援單檔上傳,Dialog 夠用 - 保留使用者所在頁面上下文,上傳完可以立刻看到列表 - 獨立頁面 `/models/upload` 留到 Phase 1 支援多檔 / 佇列時再做 URL 不變(不用 router);Dialog 的開關用 Zustand store 狀態管理(`uploadStore`),方便未來移到全域 FAB。 --- ## 7. API 契約(給 Backend / Architect) ### 7.1 `POST /api/models/upload-url` ```json Request: { "filename": "yolov5s_kl520.nef", "size": 49573888, "contentType": "application/octet-stream", "metadata": { "name": "YOLOv5s KL520", "version": "v1.0", "notes": "針對戶外停車場場景訓練" } } Response 200: { "uploadId": "upl_01HXXXX", "putUrl": "https://storage.visiona.ai/models/upl_01HXXXX?X-Amz-Signature=...", "expiresAt": "2026-04-21T14:45:00Z", "headers": { "Content-Type": "application/octet-stream", "x-amz-meta-model-name": "YOLOv5s KL520" } } ``` ### 7.2 前端直送 storage ``` PUT {putUrl} Content-Type: application/octet-stream body: (file binary) ``` ### 7.3 `POST /api/models/confirm` ```json Request: { "uploadId": "upl_01HXXXX", "etag": "" } Response 200: { "model": { "id": "mdl_...", "name": "YOLOv5s KL520", "status": "scanning", ... } } ``` --- ## 8. 無障礙 - Dialog:既有 shadcn 已處理焦點陷阱與 ESC(上傳中 ESC 被攔截) - Drop zone:`role="button" tabIndex={0}`,Enter / Space 開啟檔案選擇器 - Progress:`role="progressbar" aria-valuenow aria-valuemin aria-valuemax` - 錯誤訊息:`role="alert"` - 成功:`role="status" aria-live="polite"` - 不只靠顏色:Progress 旁顯示百分比文字,錯誤有圖示 --- ## 9. i18n key ``` models.upload.button → 上傳模型 models.upload.dialog.title → 上傳模型 models.upload.dropzone.label → 拖曳 .nef 檔到此處 models.upload.dropzone.or → 或 models.upload.dropzone.browse → 選擇檔案 models.upload.dropzone.hint → 支援格式:.nef · 最大 100 MB models.upload.selectedFile → 已選檔案 models.upload.field.name → 模型名稱 models.upload.field.version → 版本 models.upload.field.notes → 備註(選填) models.upload.action.remove → 移除 models.upload.action.cancel → 取消 models.upload.action.start → 開始上傳 models.upload.uploading.title → 上傳中 models.upload.uploading.hint → 請勿關閉此視窗或導航到其他頁面 models.upload.uploading.stats → {uploaded} / {total} · {speed} · 剩餘約 {eta} models.upload.uploading.almostDone → 即將完成 models.upload.cancelConfirm.title → 確定要取消上傳? models.upload.cancelConfirm.desc → 已傳的資料會作廢 models.upload.success.title → 上傳完成 models.upload.success.scanHint → 模型即將進入安全掃描,完成後可燒錄 models.upload.error.invalidType → 只支援 .nef 檔案,你選的是 {type} models.upload.error.tooLarge → 檔案太大({size}),最大允許 100 MB models.upload.error.requiredField → {field} 為必填 models.upload.error.urlFailed → 伺服器忙碌,請稍後再試 models.upload.error.urlExpired → 上傳授權已過期,重新取得中... models.upload.error.networkLost → 網路中斷,上傳已暫停 models.upload.error.storage5xx → 儲存空間暫時無法回應,請稍後再試 models.upload.error.confirmFailed → 上傳完成,但伺服器確認失敗,請重新上傳 models.upload.error.retry → 重試 models.upload.error.resume → 繼續上傳 models.upload.toast.uploaded → ✓ 模型 {name} 已上傳 models.upload.leaveWarning → 上傳進行中,確定要離開嗎? ``` --- ## 10. 響應式 | 斷點 | 調整 | |------|------| | Mobile (< 640px) | Dialog 改 `max-w-full h-full rounded-none`(近似全螢幕);Drop zone `h-36` | | Tablet / Desktop | `max-w-lg` 居中 | --- ## 11. TODO(Phase 1+) | 項目 | 時機 | |------|------| | 多檔佇列上傳 | Phase 1 | | 分段上傳(resumable, > 100MB)| Phase 1 | | 背景上傳(Service Worker 持續)| Phase 2 | | 獨立 `/models/upload` 頁 | Phase 1(有佇列時)| | 上傳歷史 / 失敗重試列表 | Phase 2 | | 拖曳排序上傳順序 | Phase 2 | | 病毒掃描詳細進度 | Phase 1 |