推論工作區後端塊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>
83 lines
4.2 KiB
Go
83 lines
4.2 KiB
Go
package api
|
||
|
||
import (
|
||
"github.com/gin-gonic/gin"
|
||
)
|
||
|
||
// registerStubRoutes 註冊尚未實作 / Phase 1 才處理的 HTTP endpoint,一律回 501 NOT_IMPLEMENTED。
|
||
//
|
||
// **為什麼只留這些**:Auth / Pairing 補齊 / Devices / Models / GET /clusters /
|
||
// system/deps / /storage 都在 B5 補實作(見 auth.go / devices.go / models.go /
|
||
// clusters.go / storage.go 各檔);Camera / Media 已在 camera.go(registerCameraRoutes)
|
||
// 換成真正的 tunnel proxy 宣告。這裡只剩:
|
||
// - Cloud 裝置記錄(非 tunnel 的 CRUD,Phase 1)
|
||
// - Clusters 寫入類(Phase 1)
|
||
// - Converter(Phase 1)
|
||
//
|
||
// WebSocket 的 501 stub 在 registerWebSocketStubs(下方),不在此函式。
|
||
//
|
||
// 讓前端對錯誤路徑能拿到 501 而非 404,減少除錯成本。
|
||
func registerStubRoutes(g *gin.RouterGroup, _ Deps) {
|
||
stub := func(hint string) gin.HandlerFunc {
|
||
return func(c *gin.Context) {
|
||
WriteNotImplemented(c, hint)
|
||
}
|
||
}
|
||
|
||
// --- Cloud-side device records(非 tunnel) ---
|
||
g.GET("/cloud/devices", stub("cloud.devices.list — pending Phase 1"))
|
||
g.POST("/cloud/devices/:id/rename", stub("cloud.devices.rename — pending Phase 1"))
|
||
|
||
// --- Models 其餘 ---
|
||
// load-to-device 已在 models.go 註冊為 501 stub(為了讓 /api/models/:id/load-to-device 路徑註冊完整)。
|
||
|
||
// --- Clusters 寫入類 ---
|
||
g.POST("/clusters", stub("clusters.create — pending Phase 1"))
|
||
g.GET("/clusters/:id", stub("clusters.get — pending Phase 1"))
|
||
g.DELETE("/clusters/:id", stub("clusters.delete — pending Phase 1"))
|
||
g.POST("/clusters/:id/devices", stub("clusters.add-device — pending Phase 1"))
|
||
g.DELETE("/clusters/:id/devices/:deviceId", stub("clusters.remove-device — pending Phase 1"))
|
||
g.PUT("/clusters/:id/devices/:deviceId/weight", stub("clusters.set-weight — pending Phase 1"))
|
||
g.POST("/clusters/:id/flash", stub("clusters.flash — pending Phase 1"))
|
||
g.POST("/clusters/:id/inference/start", stub("clusters.inference.start — pending Phase 1"))
|
||
g.POST("/clusters/:id/inference/stop", stub("clusters.inference.stop — pending Phase 1"))
|
||
|
||
// --- Camera / Media(走 tunnel proxy) ---
|
||
// 已由 registerCameraRoutes(camera.go)換成真正的 proxy 宣告,不再是 501 stub。
|
||
|
||
// --- Converter(Phase 1) ---
|
||
g.POST("/converter/jobs", stub("converter.submit — pending Phase 1"))
|
||
g.GET("/converter/jobs", stub("converter.list — pending Phase 1"))
|
||
g.GET("/converter/jobs/:id", stub("converter.get — pending Phase 1"))
|
||
g.GET("/converter/jobs/:id/download", stub("converter.download — pending Phase 1"))
|
||
}
|
||
|
||
// registerWebSocketStubs 註冊「尚未實作」的 /ws/* endpoint,一律回 501,讓前端能收到
|
||
// 明確錯誤,由 B7 補齊。
|
||
//
|
||
// WS tunnel proxy 的基礎設施(Forwarder.ForwardWebSocket、api-server 端 Hijack + 雙向
|
||
// io.Copy)已實作(見 forwarder.go §ForwardWebSocket、proxy.go newWebSocketProxyHandler),
|
||
// 且 /ws/devices/:id/inference 已由 registerWebSocketRoutes 換成真正的 WS tunnel proxy。
|
||
// 這裡只剩尚未接上 proxy 的其餘 /ws/* 端點(events / flash-progress / server-logs /
|
||
// system / clusters / pairing)。
|
||
//
|
||
// 注意:ws endpoint 在 /ws 而非 /api/ws,所以由 NewRouter 直接註冊而非 apiGroup。
|
||
func registerWebSocketStubs(r *gin.Engine) {
|
||
stub := func(hint string) gin.HandlerFunc {
|
||
return func(c *gin.Context) {
|
||
WriteNotImplemented(c, hint)
|
||
}
|
||
}
|
||
// 用 GET(WebSocket upgrade 的初始 HTTP request)
|
||
r.GET("/ws/devices/events", stub("ws.devices.events — pending B7"))
|
||
r.GET("/ws/devices/:id/flash-progress", stub("ws.flash-progress — pending B7"))
|
||
// /ws/devices/:id/inference 已由 registerWebSocketRoutes 換成真正的 WS tunnel proxy
|
||
// (掛在 AuthMiddleware group 內,走 same-origin cookie 認證)。不在此註冊 stub,
|
||
// 避免 gin radix tree 同路徑重複註冊 panic。
|
||
r.GET("/ws/server-logs", stub("ws.server-logs — pending B7"))
|
||
r.GET("/ws/system", stub("ws.system — pending B7"))
|
||
r.GET("/ws/clusters/:id/inference", stub("ws.clusters.inference — pending B7"))
|
||
r.GET("/ws/clusters/:id/flash-progress", stub("ws.clusters.flash — pending B7"))
|
||
r.GET("/ws/pairing/status", stub("ws.pairing.status — pending B7"))
|
||
}
|