jim800121chen c54f16fca0 Initial commit: visionA monorepo with local-tool subproject
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>
2026-04-11 22:10:38 +08:00

308 lines
12 KiB
Markdown
Raw Permalink 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.

# 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 ↔ Realbody `{"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 portbody `{"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 endpointlocal 無需 |
| 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. IPCvisionA-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 listenerbound 到 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。