jim800121chen 4f50ad7350 feat(ws): flash-progress WS tunnel proxy(後端,載入模型進度回顯)
flash「載入模型到裝置」後端缺口:flash-progress WS 從 501 stub 換真
tunnel proxy,複用 inference WS 的 path-agnostic handler。

- camera.go registerWebSocketRoutes:加 /ws/devices/:id/flash-progress
  (複用 newWebSocketProxyHandler、掛 wsAuthGroup same-origin cookie)
- stubs.go:刪 flash-progress 501 stub(成對防 radix panic)+ 更新註解
- api.go:更新 wsAuthGroup 註解(現含 inference + flash-progress 兩條裝置級 WS)
- camera_ws_test.go:+3 test(NoForwarder/TunnelDisconnected/CoexistsWithInference)
- all_endpoints_require_auth_test.go:收窄白名單,flash-progress 從 skipped→
  covered 納入「無 cookie 應 401」回歸檢查(守得住證明:移除 auth 即 FAIL)

契約:WS /ws/devices/:id/flash-progress、payload raw {percent,stage,message?,error?}
透明轉發。Reviewer 0C/0M/1Mi 通過(Mi 註解已修)。build/vet/全回歸綠。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 06:30:36 +08:00

91 lines
5.2 KiB
Go
Raw Permalink 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.

// camera.go — /api/camera/* 與 /api/media/* 的 route 宣告。
//
// 這兩組 endpoint 全部走 tunnel proxy實際的攝影機 / 圖片 / 影片 / 批次推論
// 都執行在 local agentUSB 插著攝影機、跑著 local-tool 的那台機器。api-server
// 只負責「面向瀏覽器 + auth」把請求原樣中繼給 local agent見 proxy.go
//
// 為什麼從 stubs.go 的 501 搬出來獨立成檔:
// - 對齊 devices.go 的慣例(每個 domain 一個 register 檔stubs.go 只留真正
// 還沒 handler 的 endpoint。
// - camera/media 與 devices 走的是同一套 newProxyHandlerpattern 已在 B5 生產跑通
// /api/devices/scan 等)。這裡只是「宣告路徑 + 決定 streaming flag」。
//
// streaming flag 的判斷:
// - GET /camera/stream → streamingMJPEG multipart/x-mixed-replace 長連線)
// - GET /media/batch-images/:i → **非 streaming**:回單張 jpeg一次讀完
// request-response 即可。
// - 其餘list / start / stop / upload / seek→ 非 streaming request-response。
// upload 類的 request body 由 http.NewRequestWithContext 以 streaming 方式送出
// proxy.go 直接把 c.Request.Body 當 upstream body大 multipart 也不會一次
// 載入記憶體streaming flag 只控制「response 是否逐 chunk flush」與 request
// body 上傳無關,所以 upload 用 proxyOptions{} 即可。
//
// 對齊 .autoflow/04-architecture/camera-e2e-effort-estimate.md §2.1 / §3.1 與
// local-tool/server/internal/api/router.go:108-118路徑與 local agent 完全一致,
// 無需 rewritePath
//
// POC 對照edge-ai-platform relay/server.go:213-228flusher 逐 chunk 送 MJPEG
// 對應到 visionA 的 proxy.go writeProxyResponse streaming 分支 —— 那段已在 api-server
// 實作好camera/media 只需宣告路徑即可享用。
package api
import (
"github.com/gin-gonic/gin"
)
// registerCameraRoutes 註冊 /api/camera/* 與 /api/media/* 的 tunnel proxy routes。
//
// 掛在 apiGroup已套 AuthMiddleware底下所以每條路徑都受 OIDC session 保護。
// MJPEG <img src> 帶不了 Authorization header 的認證問題R-M3/R-C4靠 same-origin
// cookie 解決:瀏覽器對 same-origin 的 <img src> 會自動帶 visiona_session cookie
// AuthMiddleware 驗 cookie 即放行。**刻意不支援 token-in-URL**security 定案:
// long-lived session token 進 URL 會落 access log → 帳號接管風險)。本檔只負責把
// 已認證請求中繼出去。
func registerCameraRoutes(g *gin.RouterGroup, deps Deps) {
// request-response 類body / response 一次讀完即可。
proxy := newProxyHandler(deps, proxyOptions{})
// streaming 類response 是 MJPEG 長連線,需逐 chunk flush。
streamProxy := newProxyHandler(deps, proxyOptions{streaming: true})
// --- Camera即時攝影機 ---
g.GET("/camera/list", proxy) // 列出可用攝影機
g.POST("/camera/start", proxy) // 開 camera + 起推論,回 streamUrl
g.POST("/camera/stop", proxy) // 停止 pipeline
g.GET("/camera/stream", streamProxy) // MJPEG multipart/x-mixed-replace 長連線
// --- Media圖片 / 影片 / 批次推論) ---
// 注意media 的「結果畫面」也走上面的 /camera/streampipeline.go 共用同一個
// MJPEGStreamer所以 media 端點本身都是 request-response回 streamUrl + metadata
g.POST("/media/upload/image", proxy) // multipart 圖片上傳
g.POST("/media/upload/video", proxy) // multipart 影片上傳大檔body streaming 送出)
g.POST("/media/upload/batch-images", proxy) // 多檔(最多 50 張)
g.GET("/media/batch-images/:index", proxy) // 回單張 jpeg非 streaming
g.POST("/media/seek", proxy) // 影片 seek
}
// registerWebSocketRoutes 註冊需要 WS tunnel proxy 的 /ws/* endpoint。
//
// 目前有兩條:
// - /ws/devices/:id/inference — 推論結果即時推播camera overlay + media 結果
// 顯示的共用資料通道)。
// - /ws/devices/:id/flash-progress — flash載入模型到裝置進度回顯percent /
// stage / message / errorraw JSON 透明轉發)。
//
// 兩條共用同一個 newWebSocketProxyHandler該 handler 是 path-agnostic 的
// outPath 取自 c.Request.URL.Path原樣轉發到 local agent不綁定任何 path
// 所以 inference 與 flash-progress 只差路由掛載handler 零改動。local agent 端
// 兩條 path 都已實作router.go /ws/devices/:id/flash-progress → flash_ws.go
//
// 掛在 wsGroup已套 AuthMiddleware走 same-origin cookie 認證,不放 token 到
// URLsecurity 定案)。
//
// 其餘 /ws/* 仍是 registerWebSocketStubs 的 501events / server-logs / system /
// clusters / pairing非本次範圍。
//
// wsGroup 必須是「path 前綴為 /ws 且套了 AuthMiddleware」的 group見 api.go NewRouter
func registerWebSocketRoutes(wsGroup *gin.RouterGroup, deps Deps) {
wsGroup.GET("/devices/:id/inference", newWebSocketProxyHandler(deps))
wsGroup.GET("/devices/:id/flash-progress", newWebSocketProxyHandler(deps))
}