jim800121chen 4d0b870480 feat(visionA-backend): DB 接入 — 6 store 接 PostgreSQL/Redis 持久化(塊 0-5)
把 visionA-backend 6 個 in-memory store 接到資料庫持久化,範圍=完整
(PG 全接 + session 接 Redis + 交易韌性)。interface / handler 不動,
只加 DB 實作 + 換 wiring,config 未設 DB 時保留 in-memory fallback。

- 塊 0 基礎建設:pgx/v5 連線池 + DatabaseConfig/RedisConfig + golang-migrate
  runner(embed)+ cmd/migrate + testcontainers 測試基礎建設
- 塊 1 model → Postgres:array 映射、upsert 保留 CreatedAt、faa_object_key、
  三維 filter(owner/chip/source)、soft-delete partial index
- 塊 2 device → Postgres:partial unique(已刪 serial 可重註冊)、雙狀態欄位
- 塊 3 token → Postgres:pairing_tokens + session_tokens 分表、token_hash 當 PK
- 塊 4 userSession → Redis:idle + absolute 雙 TTL 取代 cleanup goroutine
  (tunnel session 維持 in-memory,yamux handle 不可序列化)
- 塊 5 交易/韌性:WithTx helper + 刪 device cascade 撤銷 token(同 tx 原子)
  + /healthz ping PG/Redis(fail-fast 503)+ pgx error 統一映射(不洩漏 raw error)

降級策略(fail-fast):PG 掉 → 持久資料 API 回 503;Redis 掉 → session 失敗
不自動 fallback in-memory(避免多機 session 不同步)。

DB:PostgreSQL 14.23(gen_random_uuid 內建、無 citext → email 用 lower() unique
index)。每塊經 Reviewer 審查 + 真 PG/Redis testcontainers 全量 dbtest 綠燈,
in-memory fallback 未受影響。

docs: 同步更新 database.md(schema/config/migration 清單)+ api-spec.md
(409/503 錯誤碼、/healthz 新行為、device unpair cascade)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 18:28:04 +08:00

433 lines
18 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.

# 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 middlewarestatic
│ - 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 / LinuxWindows 未測試)
### 方式 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",...}}
```
### 方式 BDocker 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 HTTPcompose 內部,不對外)
---
## 如何用 POC `edge-ai-server` 驗證 tunnel
雛形不包含 local agentQ3 決策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 + middlewareB4 + B5
│ ├── auth/ # AuthService / AuthProvider / PairingStore雛形 Static + InMemory
│ ├── session/ # Store / Handle / Forwarder / ProxyClient
│ ├── device/ # Device domain + InMemoryRepository
│ ├── model/ # Model domain + InMemoryRepository
│ ├── cluster/ # Cluster domainPOC 複製dispatcher 留 TODO
│ ├── relay/ # tunnel server + internal forward APIPOC 改造)
│ ├── wsconn/ # WebSocket ↔ net.Conn adapterPOC 複製)
│ ├── converter/ # StubClientPhase 2 才實作)
│ ├── storage/ # Store interface + LocalFSStoreHMAC presigned URL
│ ├── config/ # Config + Load()12-Factor含 DatabaseConfig / RedisConfig
│ ├── db/ # DB 接入塊 0pgxpool 連線池 + migration runner嵌入式
│ │ └── testsupport/ # testcontainers 整合測試 helper + fixture factory-tags=dbtest
│ └── logger/ # slog JSON logger wrapper
├── migrations/ # golang-migrate SQLNNNN_*.up/down.sql+ embed.go
├── docker/
│ ├── Dockerfile.api-server # multi-stagenon-roothealthcheck
│ ├── 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 tokenvAc_ + 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 2marked 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 runnergolang-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 是塊 13。
```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`B4api-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 接入塊 0pool 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 路線圖
- [ ] 真 authOIDC via Clerk / Auth0
- [ ] PostgreSQL + Redis參考 `.autoflow/04-architecture/database.md`
- [ ] S3 / R2 storage backend替換 LocalFSStore
- [ ] WebSocket proxyhijack + 雙向 io.Copy
- [ ] 多裝置 / 多 cluster 支援
- [ ] Rate limiting + audit log
- [ ] K8s / ECS deployment參考 `.autoflow/04-architecture/build-deploy.md` §7
- [ ] CI/CD pipelineGitHub 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-composemulti-stage + non-root + healthcheck
- [x] **B7** README + `.env.example` + Makefile 補完
完整任務紀錄見 [`.autoflow/progress.md`](../.autoflow/progress.md)。