# API Endpoints — visionA-local > REST + WebSocket 端點清單。來源:`edge-ai-platform/server/internal/api/router.go` > 決策:保留所有業務 API,砍掉 cluster、relay、tunnel、update 相關。 --- ## 1. REST API ### 1.1 /api/system/*(保留,部分精簡) | Method | Path | 決定 | 備註 | |--------|------|------|------| | GET | `/api/system/health` | ✅ 保留 | Wails app 用來確認 server 活著 | | GET | `/api/system/info` | ✅ 保留 + **擴充** | 顯示版本、OS、uptime、**`actual_port`**(前端依此顯示實際 listen port)、`mode`(mock/real)、`python_mode`(bundled/system) | | GET | `/api/system/metrics` | ✅ 保留 | CPU / RAM / 裝置數 | | GET | `/api/system/deps` | ✅ 保留 | 依賴檢查(python / ffmpeg / kneron) | | POST | `/api/system/restart` | ✅ 保留 | Settings 使用 | | GET | `/api/system/update-check` | ❌ **刪除** | 使用者決策 Q6 = 不做 auto-update | | GET | `/api/system/mode` | ➕ **新增** | 回傳當前 inference mode:`{"mode":"mock"\|"real"}` | | POST | `/api/system/mode` | ➕ **新增** | 切換 Mock ↔ Real,body `{"mode":"mock"\|"real"}`;**不重啟 server**,只切 inference backend(見 `architecture-overview §8`) | | GET | `/api/system/python-runtime` | ➕ **新增** | 回傳當前 Python 策略:`{"mode":"bundled"\|"system","path":"/.../python3"}` | | POST | `/api/system/python-runtime` | ➕ **新增** | 切換 Python 策略(下次重啟 server 生效),body `{"mode":"auto"\|"bundled"\|"system"}` | | POST | `/api/system/port` | ➕ **新增** | 修改 server listen port:body `{"port":3721}`;flow 見 §6 | ### 1.2 /api/models/*(全保留) | Method | Path | 決定 | |--------|------|------| | GET | `/api/models` | ✅ | | GET | `/api/models/:id` | ✅ | | POST | `/api/models/upload` | ✅ | | DELETE | `/api/models/:id` | ✅ | ### 1.3 /api/devices/*(保留核心,砍掉 flash) | Method | Path | 決定 | 備註 | |--------|------|------|------| | GET | `/api/devices` | ✅ | | | POST | `/api/devices/scan` | ✅ | | | GET | `/api/devices/:id` | ✅ | | | POST | `/api/devices/:id/connect` | ✅ | | | POST | `/api/devices/:id/disconnect` | ✅ | | | POST | `/api/devices/:id/flash` | ❌ **刪除** | 使用者決策 Q9 = 砍掉韌體燒錄 | | POST | `/api/devices/:id/inference/start` | ✅ | | | POST | `/api/devices/:id/inference/stop` | ✅ | | ### 1.4 /api/camera/*(全保留) | Method | Path | 決定 | |--------|------|------| | GET | `/api/camera/list` | ✅ | | POST | `/api/camera/start` | ✅ | | POST | `/api/camera/stop` | ✅ | | GET | `/api/camera/stream` | ✅ | ### 1.5 /api/media/*(全保留,包括 url 與 yt-dlp) | Method | Path | 決定 | 備註 | |--------|------|------|------| | POST | `/api/media/upload/image` | ✅ | | | POST | `/api/media/upload/video` | ✅ | | | POST | `/api/media/upload/batch-images` | ✅ | | | GET | `/api/media/batch-images/:index` | ✅ | | | POST | `/api/media/url` | ✅ | 使用者決策 Q10 = 保留 yt-dlp | | POST | `/api/media/seek` | ✅ | | ### 1.6 /api/clusters/*(全砍) | Method | Path | 決定 | |--------|------|------| | GET | `/api/clusters` | ❌ | | POST | `/api/clusters` | ❌ | | GET | `/api/clusters/:id` | ❌ | | DELETE | `/api/clusters/:id` | ❌ | | POST | `/api/clusters/:id/devices` | ❌ | | DELETE | `/api/clusters/:id/devices/:deviceId` | ❌ | | PUT | `/api/clusters/:id/devices/:deviceId/weight` | ❌ | | POST | `/api/clusters/:id/flash` | ❌ | | POST | `/api/clusters/:id/inference/start` | ❌ | | POST | `/api/clusters/:id/inference/stop` | ❌ | ### 1.7 其他(全砍) | Method | Path | 決定 | 備註 | |--------|------|------|------| | GET | `/auth/token` | ❌ | relay token endpoint,local 無需 | | OPTIONS | `/auth/token` | ❌ | 同上 | ## 2. WebSocket | Path | 決定 | 備註 | |------|------|------| | `/ws/devices/events` | ✅ 保留 | 裝置插拔事件推送 | | `/ws/devices/:id/flash-progress` | ❌ **刪除** | flash 已砍 | | `/ws/devices/:id/inference` | ✅ 保留 | 推論結果 streaming | | `/ws/server-logs` | ✅ 保留 | Settings 頁的即時 log | | `/ws/clusters/:id/inference` | ❌ **刪除** | cluster 已砍 | | `/ws/clusters/:id/flash-progress` | ❌ **刪除** | 同上 | ## 3. IPC(visionA-local 內部) 這是 **新增**的,給 single-instance lock 用,詳見 [`tray-and-lifecycle.md`](./tray-and-lifecycle.md)(lifecycle 章節)§2.3: | Method | Path | 說明 | |--------|------|------| | GET | `/ipc/raise` | 讓已存在的 instance 浮到前景 | | GET | `/ipc/status` | 回傳 server port、版本、pid 等 | 這些 endpoint **不是**由 Go server 提供,而是 Wails app 自己起一個極小的 HTTP listener(bound 到 localhost:random),供同機器的 visionA-local 程序間通訊。Port 號寫在各平台資料目錄下的 `visiona-local.ipc-port`(macOS:`~/Library/Application Support/visiona-local/`;Windows:`%APPDATA%\visiona-local\`;Linux:`~/.local/share/visiona-local/`)。 ## 3.1 順序修正提醒 上面把模式 / runtime / port 切換流程放在 §5、拖放代理放在 §6、前端 client 清理放在 §7,原本的 §4 router.go 結構保持不變。 ## 4. 新版 `router.go` 簡化後結構 ```go // server/internal/api/router.go func NewRouter( modelRepo *model.Repository, modelStore *model.ModelStore, deviceMgr *device.Manager, cameraMgr *camera.Manager, // ❌ clusterMgr 移除 // ❌ flashSvc 移除 inferenceSvc *inference.Service, wsHub *ws.Hub, staticFS http.FileSystem, logBroadcaster *logger.Broadcaster, systemHandler *handlers.SystemHandler, // ❌ relayToken 移除 ) *gin.Engine { r := gin.New() r.Use(gin.Recovery()) r.Use(broadcasterLogger(logBroadcaster)) r.Use(CORSMiddleware()) modelHandler := handlers.NewModelHandler(modelRepo) modelUploadHandler := handlers.NewModelUploadHandler(modelRepo, modelStore) deviceHandler := handlers.NewDeviceHandler(deviceMgr, inferenceSvc, wsHub) // flashSvc 拿掉 cameraHandler := handlers.NewCameraHandler(cameraMgr, deviceMgr, inferenceSvc, wsHub) api := r.Group("/api") { // System api.GET("/system/health", systemHandler.HealthCheck) api.GET("/system/info", systemHandler.Info) api.GET("/system/metrics", systemHandler.Metrics) api.GET("/system/deps", systemHandler.Deps) api.POST("/system/restart", systemHandler.Restart) // ❌ /system/update-check 移除 // Models api.GET("/models", modelHandler.ListModels) api.GET("/models/:id", modelHandler.GetModel) api.POST("/models/upload", modelUploadHandler.UploadModel) api.DELETE("/models/:id", modelUploadHandler.DeleteModel) // Devices api.GET("/devices", deviceHandler.ListDevices) api.POST("/devices/scan", deviceHandler.ScanDevices) api.GET("/devices/:id", deviceHandler.GetDevice) api.POST("/devices/:id/connect", deviceHandler.ConnectDevice) api.POST("/devices/:id/disconnect", deviceHandler.DisconnectDevice) // ❌ /devices/:id/flash 移除 api.POST("/devices/:id/inference/start", deviceHandler.StartInference) api.POST("/devices/:id/inference/stop", deviceHandler.StopInference) // Camera api.GET("/camera/list", cameraHandler.ListCameras) api.POST("/camera/start", cameraHandler.StartPipeline) api.POST("/camera/stop", cameraHandler.StopPipeline) api.GET("/camera/stream", cameraHandler.StreamMJPEG) // Media api.POST("/media/upload/image", cameraHandler.UploadImage) api.POST("/media/upload/video", cameraHandler.UploadVideo) api.POST("/media/upload/batch-images", cameraHandler.UploadBatchImages) api.GET("/media/batch-images/:index", cameraHandler.GetBatchImageFrame) api.POST("/media/url", cameraHandler.StartFromURL) api.POST("/media/seek", cameraHandler.SeekVideo) // ❌ /clusters/* 全部移除 } // ❌ /auth/token 移除 // WebSocket r.GET("/ws/devices/events", ws.DeviceEventsHandler(wsHub, deviceMgr)) // ❌ /ws/devices/:id/flash-progress 移除 r.GET("/ws/devices/:id/inference", ws.InferenceHandler(wsHub, inferenceSvc)) r.GET("/ws/server-logs", ws.ServerLogsHandler(wsHub, logBroadcaster)) // ❌ /ws/clusters/* 全部移除 // Embedded frontend if staticFS != nil { fileServer := http.FileServer(staticFS) r.GET("/_next/*filepath", func(c *gin.Context) { fileServer.ServeHTTP(c.Writer, c.Request) }) r.GET("/favicon.ico", func(c *gin.Context) { fileServer.ServeHTTP(c.Writer, c.Request) }) r.NoRoute(spaFallback(staticFS)) } return r } ``` ## 5. 模式 / Runtime / Port 切換流程 ### 5.1 Mock ↔ Real 模式切換(`POST /api/system/mode`) 不重啟 server,只切 inference backend: ``` 前端 POST /api/system/mode {"mode":"real"} ↓ Go server: device.Manager.SetMode("real") ├─ 若原為 mock → spawn Python sidecar → kneron_bridge scan → 回填 registry └─ 若原為 real → kill Python sidecar → 載入 mock devices ↓ broadcast 200 OK + WebSocket /ws/devices/events push "mode_changed" ↓ 前端重新拉 /api/devices 與 /api/system/info ``` 所有進行中的 inference session 強制終止,使用者收到 toast:「模式已切換,請重新啟動推論」。 ### 5.2 Python Runtime 切換(`POST /api/system/python-runtime`) 切換不即時生效(需重啟 Go server 子行程): ``` 前端 POST /api/system/python-runtime {"mode":"system"} ↓ Go server: 寫入 .installed 的 python.mode 欄位 ↓ 回應 200 + {"needs_restart": true} ↓ 前端顯示「需重啟 server 才會生效,立即重啟?」 → 使用者同意 → 呼叫 POST /api/system/restart → Wails app 接收 → kill 當前 Go server 子行程 → 用新 python mode 重 spawn ``` ### 5.3 Port 變更(`POST /api/system/port`) **Port 修改走「持久化 → 重啟 server 子行程 → Wails WebView 重連」流程**: ``` 前端 POST /api/system/port {"port":3722} ↓ Go server 驗證 port 可用(若不可用回 409) ↓ Go server 寫入 config.json 的 port 欄位(持久化到資料目錄) ↓ Go server 回應 200 + {"new_port":3722,"needs_restart":true} ↓ 前端通知 Wails app 透過 bind API: runtime.RequestServerRestart(3722) ↓ Wails app: 1. 儲存 pending_new_port=3722 到記憶體 2. kill 當前 Go server 子行程(SIGTERM → 3s timeout → SIGKILL) 3. 用新 port spawn 新的 Go server 子行程 4. waitHealthy(3722, 10s) 5. WebView.Reload("http://127.0.0.1:3722/") ↓ 前端重新載入並顯示新 port ``` **失敗回退**:若新 port spawn 失敗,Wails app 用舊 port 重啟,並顯示錯誤 toast。config.json 不回滾(使用者可自行修正)。 ### 5.4 Port picking 後顯示實際 port 使用者未指定時走 `pickPort(3721)`,結果可能落在 3722/3723... 前端取得方式: ``` 前端啟動 → GET /api/system/info → 取 data.actual_port 前端 Sidebar/Settings → 顯示「Server listening on 127.0.0.1:{actual_port}」 ``` ## 6. 拖放檔案代理上傳 Wails v2 `OnFileDrop` 回傳絕對路徑,不是 File 物件。前端不能直接 FormData 上傳,必須走 Wails 代理: ``` 使用者拖 .nef → Wails WebView 捕獲 ↓ Wails Go 端 runtime.OnFileDrop([]string{abs_path}) ↓ Wails 透過 Bind API 把路徑陣列 emit 給前端 ↓ 前端呼叫 window.runtime.UploadFileByPath(path, "/api/models/upload") ↓ Wails Go 端讀檔 → 包成 multipart/form-data → POST http://127.0.0.1:{port}/api/models/upload ↓ Go server 走原本的 upload handler,無需修改 ``` 詳見 `architecture-overview.md §9`。 ## 7. 對應的前端 API client 清理 `frontend/src/lib/api/` 底下需要刪除: - `clusters.ts` - `relay.ts`(若存在) - `update.ts`(若存在) - `flash.ts`(若獨立檔案) 保留其他所有 API client。