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

12 KiB
Raw Permalink Blame History

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 portmodemock/realpython_modebundled/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 backendarchitecture-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.mdlifecycle 章節§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-portmacOS~/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.ts
  • relay.ts(若存在)
  • update.ts(若存在)
  • flash.ts(若獨立檔案)

保留其他所有 API client。