jim800121chen dc6ca211ae feat(ws): tunnel WS forward — 推論結果經 tunnel 推回雲端(後端塊2)
推論工作區後端塊2:實作 WS forward,讓 local agent 的推論結果 WS
(inference:<deviceId>)經 tunnel 推回雲端瀏覽器 canvas overlay。照
edge-ai-platform POC relay/server.go proxyWebSocket 移植(唯讀參考)。

- forwarder.go ForwardWebSocket:OpenStream→寫 upgrade→讀 101→WebSocketConn
  (新增 WebSocketConn/wsUpgradeError/AsWSUpgradeError)
- proxy.go newWebSocketProxyHandler:hijack browser→回寫 101→雙向 io.Copy
  cross-close(無 goroutine leak)+ copyWebSocketUpgradeHeaders(保留
  Sec-WebSocket-*、剝 Authorization/Origin)
- camera.go registerWebSocketRoutes:GET /ws/devices/:id/inference
- api.go wsAuthGroup(/ws + AuthMiddleware):same-origin cookie 認證,
  無 token-in-URL(security 定案)
- stubs.go:移除 WS inference 501 stub、更新過時 doc comment(Mi-1/2)

架構差異:POC 單 binary,visionA api-server(auth)+remote-proxy 雙 binary,
remote-proxy raw byte pipe 透明穿過 WS upgrade bytes。local agent 端未動。

認證/授權(security 定案 + S1/S2):same-origin cookie、剝 Auth/Origin、
WS 不套 300s timeout、走 pickActiveSessionToken(帶別人 deviceId 也只打到
自己 tunnel;多租戶嚴格綁定屬 Phase 1 M2 debt)。

Reviewer 0C/1M/3Mi 通過(修後)。Major-1 已修:收窄 all_endpoints_require_auth_test
的 /ws/ 白名單,讓 authed inference WS 納入「無 cookie 應 401」回歸檢查
(附守得住證明:移除 auth 測試即 FAIL)。+forwarder 5 test + camera_ws
端到端四層真連線雙向 pipe。build/vet/全回歸綠。

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

80 lines
4.6 KiB
Go
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.

// 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 結果
// 顯示的共用資料通道)。掛在 wsGroup已套 AuthMiddleware走 same-origin cookie
// 認證,不放 token 到 URLsecurity 定案)。
//
// 其餘 /ws/* 仍是 registerWebSocketStubs 的 501events / flash-progress / 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))
}