# visionA-backend > visionA Cloud 的後端服務。由 **`api-server`**(無狀態 REST/WS API)與 **`remote-proxy`**(有狀態 tunnel server)兩個 binary 組成。 --- ## ⚠️ Phase 0 雛形警告 **這是雛形(prototype)版本,不是生產交付物。** 主要限制: - 單一 user(永遠回 `demo-user`,無真正認證) - 所有狀態 in-memory(重啟即消失,無 DB / Redis) - Storage 走 LocalFS(無 S3) - WebSocket proxy 尚未實作(所有 `/ws/*` 皆回 501) - 單一 instance(無水平擴展) 完整限制見下方 [雛形範圍與限制](#雛形範圍與限制)。 --- ## 架構總覽 ``` ┌─────────────────────┐ │ Browser / curl │ └──────────┬──────────┘ │ REST / WS (3721) ▼ ┌──────────────────────────────┐ │ api-server │ │ (cmd/api-server) │ │ │ │ - REST + WS handler │ │ - Auth middleware(static)│ │ - ProxyClientStore │ │ (查詢 session) │ │ - Forwarder │ │ (轉發 HTTP 到 tunnel) │ │ - LocalFS storage (/storage)│ │ 無狀態,可水平擴展 │ └──────────┬───────────────────┘ │ internal HTTP (3801) ▼ ┌──────────────────────────────┐ │ remote-proxy │ │ (cmd/remote-proxy) │ │ │ │ - Tunnel server (ws://3800) │ │ - yamux session store │ │ - /internal/forward/raw │ │ - /internal/session/:token │ │ │ │ ⚠️ 有狀態(單 instance) │ └──────────┬───────────────────┘ │ WebSocket + yamux (3800) ▼ ┌──────────────────────────────┐ │ Local Agent │ │ (在客戶端機器上跑) │ │ │ │ 目前 demo 用 POC 的 │ │ edge-ai-server 當 client │ └──────────────────────────────┘ ``` 詳細設計見: - [`.autoflow/04-architecture/design-doc.md`](../.autoflow/04-architecture/design-doc.md)(§7 部署) - [`.autoflow/04-architecture/TDD.md`](../.autoflow/04-architecture/TDD.md) - [`.autoflow/04-architecture/api/api-spec.md`](../.autoflow/04-architecture/api/api-spec.md)(前端 REST API) - [`.autoflow/04-architecture/api/api-internal.md`](../.autoflow/04-architecture/api/api-internal.md)(api-server ↔ remote-proxy) --- ## 技術堆疊 | 層級 | 技術 | 備註 | |------|------|------| | 語言 | Go 1.26 | `go.mod` 鎖定 | | HTTP framework | [Gin](https://github.com/gin-gonic/gin) + `gin-contrib/cors` | B4 導入 | | Tunnel 傳輸 | `gorilla/websocket` + `hashicorp/yamux` | 沿用 POC `edge-ai-platform` | | Logging | `log/slog`(stdlib) | JSON handler,結構化輸出 | | ID 生成 | `google/uuid` | request-id / demo seed | | 單元測試 | `stretchr/testify` | B2 導入 | | 配置 | 12-Factor App | 全走 env,不寫死 | --- ## 快速啟動(10 分鐘起步) ### 前置 - Go 1.26+(本機 run)或 Docker 27+(容器 run) - macOS / Linux(Windows 未測試) ### 方式 A:本機 `go run`(最快,適合開發) ```bash cd visionA-backend # 1) 一鍵跑 remote-proxy + api-server(任一 Ctrl+C 兩個都會停) make dev # 另開 terminal 驗證: curl http://localhost:3721/healthz # {"status":"ok"} curl -X POST http://localhost:3721/api/auth/login \ -H "Content-Type: application/json" \ -d '{"email":"demo@visionA.local","password":"any"}' # {"success":true,"data":{"user":{"id":"demo-user",...},"access_token":"demo-access-token",...}} ``` ### 方式 B:Docker Compose(接近生產拓撲) ```bash cd visionA-backend # 1) 複製環境變數範本 cp .env.example .env # (視情況編輯 .env — 通常預設就能跑) # 2) 建 image + 啟動 make docker-compose-up # 3) 驗證 curl http://localhost:3721/healthz curl -X POST http://localhost:3721/api/auth/login \ -H "Content-Type: application/json" \ -d '{"email":"demo@visionA.local","password":"any"}' # 4) 跟 logs make docker-compose-logs # 5) 停 make docker-compose-down ``` **Ports:** - `3721`:api-server REST + WS(對前端) - `3800`:remote-proxy tunnel WS(對 local agent) - `3801`:remote-proxy internal HTTP(compose 內部,不對外) --- ## 如何用 POC `edge-ai-server` 驗證 tunnel 雛形不包含 local agent(Q3 決策:local agent 模組 Phase 1 才做)。要驗證 tunnel 整條鏈路,用 POC `edge-ai-platform/edge-ai-server` 當 tunnel client。 ### 步驟 ```bash # Terminal 1 — 起 visionA-backend cd visionA-backend make dev # 或 docker compose up # Terminal 2 — 申請 pairing token(雛形:POST 就會配發一個) curl -X POST http://localhost:3721/api/pairing/token \ -H "Content-Type: application/json" \ -d '{"name":"demo"}' # { "success": true, "data": { "token": "vAc_...", "expires_at": "..." } } # Terminal 3 — POC edge-ai-server 當 tunnel client 接上來 cd /path/to/edge-ai-platform ./dist/edge-ai-server \ --relay-url=ws://localhost:3800/tunnel/connect \ --relay-token=vAc_<貼上上一步的 token> # Terminal 2 — 驗證 tunnel 已連上 curl -H "Authorization: Bearer demo-access-token" \ http://localhost:3721/api/pairing/status # { "success": true, "data": { "connected": true, ... } } # 打到 local agent(會被 forward 過 tunnel) curl -H "Authorization: Bearer demo-access-token" \ http://localhost:3721/api/devices/scan ``` ### 或:用 B3 的 fake tunnel client 寫小 demo 若不想起 POC,`cmd/api-server/b5_integration_test.go` 裡的 `startFakeTunnelClient` 是現成的 60 行範例,直接複製到 `cmd/tunnel-demo/main.go` 就能跑。 --- ## 目錄結構 ``` visionA-backend/ ├── cmd/ │ ├── api-server/ # REST/WS API server(無狀態) │ │ ├── main.go │ │ ├── seed.go # --seed-demo-data 用的示範資料 │ │ ├── integration_test.go # B4 端到端測試 │ │ └── b5_integration_test.go # B5 端到端測試(含 tunnel forward + model upload) │ ├── migrate/ # 獨立 migration 工具(go run ./cmd/migrate up|down|version) │ └── remote-proxy/ # tunnel server(有狀態,持有 session in-memory) │ └── main.go ├── internal/ │ ├── api/ # API handlers + Gin router + middleware(B4 + B5) │ ├── auth/ # AuthService / AuthProvider / PairingStore(雛形 Static + InMemory) │ ├── session/ # Store / Handle / Forwarder / ProxyClient │ ├── device/ # Device domain + InMemoryRepository │ ├── model/ # Model domain + InMemoryRepository │ ├── cluster/ # Cluster domain(POC 複製,dispatcher 留 TODO) │ ├── relay/ # tunnel server + internal forward API(POC 改造) │ ├── wsconn/ # WebSocket ↔ net.Conn adapter(POC 複製) │ ├── converter/ # StubClient(Phase 2 才實作) │ ├── storage/ # Store interface + LocalFSStore(HMAC presigned URL) │ ├── config/ # Config + Load()(12-Factor;含 DatabaseConfig / RedisConfig) │ ├── db/ # DB 接入塊 0:pgxpool 連線池 + migration runner(嵌入式) │ │ └── testsupport/ # testcontainers 整合測試 helper + fixture factory(-tags=dbtest) │ └── logger/ # slog JSON logger wrapper ├── migrations/ # golang-migrate SQL(NNNN_*.up/down.sql)+ embed.go ├── docker/ │ ├── Dockerfile.api-server # multi-stage,non-root,healthcheck │ ├── Dockerfile.remote-proxy │ └── docker-compose.yml ├── .env.example # 環境變數範本(commit) ├── .gitignore # 已排除 .env / bin/ / data/ ├── Makefile # build / dev / test / docker-* 等 targets ├── go.mod / go.sum └── README.md # 本檔 ``` --- ## API 端點摘要 完整規格見 [`.autoflow/04-architecture/api/api-spec.md`](../.autoflow/04-architecture/api/api-spec.md)。 | 群組 | 端點 | 說明 | |------|------|------| | System | `GET /healthz` | liveness/readiness(無需認證) | | System | `GET /api/system/health` | tunnel / agent 連線狀態 | | System | `GET /api/system/info` | 版本 + build 資訊 | | Auth | `POST /api/auth/login` | 雛形永遠回 `demo-user` | | Auth | `POST /api/auth/register` | 雛形 501 | | Auth | `GET /api/auth/me` | 當前 user 資訊 | | Pairing | `POST /api/pairing/token` | 申請 pairing token(vAc_ + 32 hex) | | Pairing | `GET /api/pairing/status` | 目前 user 的 tunnel 狀態 | | Pairing | `GET /api/pairing/tokens` | 列出已簽發的 token | | Pairing | `DELETE /api/pairing/tokens/:id` | revoke token | | Devices | `GET /api/devices` | 列出裝置 | | Devices | `POST /api/devices/scan` | 觸發 local agent 掃 USB(透過 tunnel) | | Devices | `DELETE /api/devices/:id` | unpair(同時關 tunnel) | | Models | `POST /api/models/init` | 兩階段上傳 step 1(拿 presigned PUT URL) | | Models | `PUT /storage/:signed` | 實際上傳檔案(HMAC 驗簽) | | Models | `POST /api/models/:id/finalize` | 兩階段上傳 step 2(marked ready) | | Models | `GET /api/models` | 列出模型 | | Clusters | `GET /api/clusters` | 列出 cluster(骨架) | | Storage | `GET /storage/:signed` | presigned download | | Camera / Inference | `/api/cameras/*`、`/api/inference/*` | proxy 到 local agent | | WebSocket | `/ws/*` | **雛形 501**(B7 之後補) | ### 錯誤格式(統一) ```json { "success": false, "error": { "code": "TUNNEL_DISCONNECTED", "message": "local agent 未連線或 tunnel 斷開", "request_id": "req_abc123" } } ``` 錯誤碼清單見 [`internal/api/errors.go`](internal/api/errors.go)。 --- ## 環境變數 詳見 [`.env.example`](.env.example)。常用: | 變數 | 預設 | 說明 | |------|------|------| | `VISIONA_API_PORT` | `3721` | api-server listen port | | `VISIONA_TUNNEL_PORT` | `3800` | remote-proxy 對 local agent 的 WS port | | `VISIONA_PROXY_INTERNAL_PORT` | `3801` | remote-proxy 對 api-server 的內部 HTTP port | | `VISIONA_PROXY_INTERNAL_URL` | `http://localhost:3801` | api-server 連 remote-proxy 用(docker compose 會覆為 `http://remote-proxy:3801`) | | `VISIONA_SEED_DEMO_DATA` | `false` | 啟動時塞示範資料(device + model + pairing) | | `VISIONA_STORAGE_SIGNING_SECRET` | `dev-signing-secret-...` | presigned URL HMAC secret(**生產必改**) | | `VISIONA_STATIC_USER_ID` | `demo-user` | 雛形 static auth 的 user id | | `VISIONA_MODEL_MAX_SIZE_MB` | `100` | 模型上傳大小上限 | | `VISIONA_CORS_ALLOWED_ORIGINS` | `http://localhost:3000` | CORS 白名單(逗號分隔) | | `VISIONA_LOG_LEVEL` | `info` | debug / info / warn / error | | `VISIONA_DB_HOST` / `_USER` / `_NAME` | (空) | PostgreSQL 連線;三者全非空才啟用(否則維持 in-memory) | | `VISIONA_DB_AUTO_MIGRATE` | `true` | 啟動時自動跑 migrate up | --- ## 資料庫(DB 接入塊 0) > 對齊 [`docs/autoflow/04-architecture/database.md`](../docs/autoflow/04-architecture/database.md) §5、§5.5。 塊 0 = **DB 基礎建設**:PostgreSQL 連線池(pgxpool)+ migration runner(golang-migrate, 嵌入式 SQL)+ testcontainers 整合測試骨架。 **啟用模式**:`VISIONA_DB_HOST` / `VISIONA_DB_USER` / `VISIONA_DB_NAME` 三者全非空 → 建池 + 自動 migrate;任一缺 → **不建池,所有 repository 維持 in-memory**(local dev fallback,雛形行為不變)。 > ⚠️ **塊 0 範圍**:即使 DB 啟用,目前 model / device / token repository **仍是 in-memory**。 > 建池只為驗證基礎建設可用、並讓 schema 先就位。把 repository 切到 Postgres 是塊 1–3。 ```bash # 設好 VISIONA_DB_* 後,手動跑 migration(或交給 api-server 啟動時 auto-migrate) make migrate-up make migrate-version # 顯示目前 schema 版本 # DB 整合測試(testcontainers,需本機 / CI 有 Docker daemon) make test-db # = go test -tags=dbtest ./internal/db/... ``` migration 檔在 [`migrations/`](migrations/)(`NNNN_description.up.sql` / `.down.sql`), 透過 `migrations/embed.go` 的 `//go:embed` 嵌入 binary,部署不需另複製。 第一份 `0001_create_users_models` 建 `users` + `models`(對齊 database.md §5.1)。 --- ## 測試 ```bash # 所有單元測試 + integration test + race detector(預設 tags,不需 Docker) make test-race # DB 整合測試(testcontainers,需 Docker daemon) make test-db # 僅 go vet / gofmt check make lint # 詳細輸出 make test ``` 覆蓋面: - 單元測試:`internal/{auth,session,device,model,config,storage,api,relay,wsconn,logger,converter,db}` - `internal/db`:DSN 組裝 / SafeTarget 遮蔽 / Config.Enabled(純函式,預設 tags 即跑) - Integration: - `cmd/api-server/integration_test.go`(B4:api-server → remote-proxy → fake tunnel) - `cmd/api-server/b5_integration_test.go`(B5:完整端到端 — login / scan / model upload / tunnel disconnect) - `internal/db/db_integration_test.go`(DB 接入塊 0:pool ping / migrate 冪等 / up-down-up / schema 形狀 / fail-fast;`-tags=dbtest`,需 Docker) --- ## 開發者指南 | 我想做的事 | 看哪裡 | |-----------|--------| | 新增一個 REST endpoint | `internal/api/` 找類似的 handler 複製;proxy 類用 `newProxyHandler` | | 改動 API 規格 | 先改 `.autoflow/04-architecture/api/api-spec.md` 再改 code | | 加環境變數 | `internal/config/config.go` + `load.go` + `.env.example` + 本 README | | 改 tunnel 協定 | `.autoflow/04-architecture/tunnel.md` + `internal/relay/` + `internal/session/forwarder.go` | | 追蹤某個 Review 問題 | `.autoflow/05-implementation/review/` | --- ## Known Issues ### Tunnel client 不在本 repo visionA-backend 只實作 tunnel **server** 端(`internal/relay/` + `internal/wsconn/`);tunnel **client** 由 visionA Agent 實作(從 POC `edge-ai-platform/server/internal/tunnel/client.go` 複製)。本 repo 過去曾保留一份 `internal/tunnel/` 副本,但因從未被 import 且會造成「兩處需要同步修補」的維護負擔,已於 2026-04-21 刪除(決策見 [`.autoflow/04-architecture/adr/adr-008-tunnel-client-reuse.md`](../.autoflow/04-architecture/adr/adr-008-tunnel-client-reuse.md))。 POC `client.go` 的 `backoff()` 有單位 mix bug(`attempt >= 1` 時永遠回 30 秒),visionA Agent 建立後需在自己的 repo 修復;本 repo 不再追蹤此 issue。 ### WebSocket proxy 未實作 所有 `/ws/*` endpoint 目前回 `501 Not Implemented`。原因:`Forwarder.ForwardWebSocket` 需要 `http.Hijacker` + 雙向 `io.Copy` 架構,B7 範圍外。 前端呼叫 `/ws/*` 時會收到 JSON body `{ "success": false, "error": { "code": "NOT_IMPLEMENTED", ... } }`,瀏覽器 WebSocket 會 fail upgrade。 --- ## 雛形範圍與限制 ### 是什麼 - 雙 binary 架構驗證(api-server 無狀態 + remote-proxy in-memory session) - REST + Tunnel 完整鏈路:browser → api-server → internal HTTP → remote-proxy → yamux → local agent - 兩階段模型上傳(init → PUT presigned → finalize) - Docker image + docker-compose 可跑 ### 不是什麼 | 項目 | 雛形 | Phase 1+ | |------|------|---------| | Auth | static,一律 `demo-user` | OIDC / Clerk | | 資料庫 | 無(全 in-memory) | PostgreSQL | | Session 存放 | `remote-proxy` 進程內 | Redis(支援水平擴展) | | 檔案儲存 | LocalFS(`./data/storage`) | S3 | | 支援多 user | ❌ | ✅ | | Rate limiting | ❌ | ✅ | | Audit log | ❌ | ✅ | | WebSocket proxy | 501 stub | ✅ | | TLS | ❌(http only) | ✅(ALB / NLB termination) | | 水平擴展 | ❌ | ✅(api-server 可;remote-proxy 需加 shared session store) | ### 重啟即消失 `make docker-compose-down` 或 restart 後: - 所有 pairing token 消失 - 所有 device / model 紀錄消失(除非 `VISIONA_SEED_DEMO_DATA=true`) - Storage 檔案保留(`./data/storage` 被 mount 為 volume) --- ## Phase 1 路線圖 - [ ] 真 auth(OIDC via Clerk / Auth0) - [ ] PostgreSQL + Redis(參考 `.autoflow/04-architecture/database.md`) - [ ] S3 / R2 storage backend(替換 LocalFSStore) - [ ] WebSocket proxy(hijack + 雙向 io.Copy) - [ ] 多裝置 / 多 cluster 支援 - [ ] Rate limiting + audit log - [ ] K8s / ECS deployment(參考 `.autoflow/04-architecture/build-deploy.md` §7) - [ ] CI/CD pipeline(GitHub Actions) - [ ] Local agent 模組(獨立 binary,取代 POC edge-ai-server 當 tunnel client) --- ## 目前實作進度 - [x] **B1** 專案初始化(go.mod、目錄骨架、Makefile、.gitignore) - [x] **B2** 共用 `internal/` 模組(core interface + in-memory 實作 + 單元測試) - [x] **B3** `cmd/remote-proxy` + relay / tunnel / wsconn / cluster - [x] **B4** `cmd/api-server` + `internal/api` 骨架 + Forwarder + ProxyClient - [x] **B5** API handlers 雛形(20+ endpoint 實作、兩階段上傳、tunnel forward) - [x] **B6** Docker image + docker-compose(multi-stage + non-root + healthcheck) - [x] **B7** README + `.env.example` + Makefile 補完 完整任務紀錄見 [`.autoflow/progress.md`](../.autoflow/progress.md)。