visionA/docs/autoflow/03-design/flows/flow-model-upload.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

412 lines
18 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.

# 模型上傳流程 — visionA Cloud
> 雲端版新增流程。使用者上傳 Kneron 編譯後的 `.nef` 模型檔到雲端 object storage之後才能選模型燒錄到遠端裝置。
>
> **技術背景**(給 Design 協作者):模型檔可能達 100MB若走 tunnel / API server 會拖慢服務。Architect 決策走 **presigned PUT**:前端向後端要一組有期限的 PUT URL前端直接把檔案 PUT 到 object storageS3 / 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 URLTTL 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`
- `<input type="file" accept=".nef" hidden>`
- 「開始上傳」:檔案未選 / 必填未填 → 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) │
│ │
│ 模型即將進入安全掃描,完成後可燒錄 │
│ │
│ [完成] │
└─────────────────────────────────────────────────┘
```
- 完成 Dialogtoast「✓ 模型 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 MB62%
│ │
│ [取消] [重試] │
└─────────────────────────────────────────────────┘
```
---
## 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` 新增狀態 badgePhase 0 簡單版Phase 1 強化
| 狀態 | Badge | 可操作 |
|------|-------|--------|
| uploading僅上傳期間| 🔵 上傳中 | 不顯示於列表Dialog |
| scanning | 🟡 掃描中 | 不可燒錄 |
| ready | 🟢 可用 | 可燒錄 |
| rejected | 🔴 檢測失敗 | 可刪除不可燒錄 |
---
## 6. 流程 vs 頁面 — Dialog vs Page 決策
**採用Dialog** `/models` 觸發
理由
- 雛形只支援單檔上傳Dialog 夠用
- 保留使用者所在頁面上下文上傳完可以立刻看到列表
- 獨立頁面 `/models/upload` 留到 Phase 1 支援多檔 / 佇列時再做
URL 不變不用 routerDialog 的開關用 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": "<storage 回的 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. TODOPhase 1+
| 項目 | 時機 |
|------|------|
| 多檔佇列上傳 | Phase 1 |
| 分段上傳resumable, > 100MB| Phase 1 |
| 背景上傳Service Worker 持續)| Phase 2 |
| 獨立 `/models/upload` 頁 | Phase 1有佇列時|
| 上傳歷史 / 失敗重試列表 | Phase 2 |
| 拖曳排序上傳順序 | Phase 2 |
| 病毒掃描詳細進度 | Phase 1 |