local-tool/: visionA-local desktop app
- M1: Wails shell + Go server + Next.js UI + Mock mode (macOS dmg ready)
- M2: i18n (zh-TW/en) + Settings 4-tab refactor
- M3: Embedded Python 3.12 runtime (python-build-standalone) + KneronPLUS wheels
- M4: Windows Inno Setup script (build on Windows runner)
- M5: Linux AppImage script + udev rule (build on Linux runner)
- M6: ffmpeg (GPL, pending legal review) + yt-dlp bundled
- Lifecycle: watchServer health check, fatal native dialog,
Wails IPC raise endpoint, stale process cleanup
.autoflow/: full PRD / Design Spec / Architecture / Testing docs
(4 rounds tri-party discussion + cross review)
.github/workflows/: macOS / Windows / Linux build CI
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
12 KiB
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(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 簡化後結構
// 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.tsrelay.ts(若存在)update.ts(若存在)flash.ts(若獨立檔案)
保留其他所有 API client。