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>
91 lines
5.2 KiB
Go
91 lines
5.2 KiB
Go
// camera.go — /api/camera/* 與 /api/media/* 的 route 宣告。
|
||
//
|
||
// 這兩組 endpoint 全部走 tunnel proxy:實際的攝影機 / 圖片 / 影片 / 批次推論
|
||
// 都執行在 local agent(USB 插著攝影機、跑著 local-tool 的那台機器)。api-server
|
||
// 只負責「面向瀏覽器 + auth」,把請求原樣中繼給 local agent(見 proxy.go)。
|
||
//
|
||
// 為什麼從 stubs.go 的 501 搬出來獨立成檔:
|
||
// - 對齊 devices.go 的慣例(每個 domain 一個 register 檔),stubs.go 只留真正
|
||
// 還沒 handler 的 endpoint。
|
||
// - camera/media 與 devices 走的是同一套 newProxyHandler,pattern 已在 B5 生產跑通
|
||
// (/api/devices/scan 等)。這裡只是「宣告路徑 + 決定 streaming flag」。
|
||
//
|
||
// streaming flag 的判斷:
|
||
// - GET /camera/stream → streaming(MJPEG 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-228(flusher 逐 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/stream(pipeline.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 / error,raw 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 到
|
||
// URL(security 定案)。
|
||
//
|
||
// 其餘 /ws/* 仍是 registerWebSocketStubs 的 501(events / 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))
|
||
}
|