從 edge-ai-platform POC 轉為正式產品的雲端後端,含以下整合階段:
- Phase 0:雛形骨架 — `cmd/api-server` (REST :3721) + `cmd/remote-proxy`
(tunnel :3800 / internal :3801) 雙 binary 共用 internal/,沿用 POC 的
WebSocket+yamux tunnel 協定但解耦 relay 與 API
- Phase 0.6:OIDC BFF 接 Innovedus Member Center
- internal/oidc package(coreos/go-oidc + PKCE S256 + state + nonce)
- internal/usersession package(HMAC-SHA256 cookie + RotateSessionID
防 session fixation, OWASP ASVS V3.2.1)
- 4 個 OIDC handler(/api/auth/login|callback|me|logout)+ AuthMiddleware
- 完全拔除 StaticAuthProvider,OIDC 是唯一認證路徑
- 9 個 ADR(含 ADR-010 BFF / ADR-011 取代 static auth /
ADR-012 pending session shared cookie / ADR-013 PKCE-only public client)
- Phase 0.7:A1 改造 + security audit 修復
- OIDC ClientSecret 變選填,支援 stage MC 的 public PKCE-only client
(AuthStyleInParams 強制 token endpoint 不送 client_secret)
- 預留 ServiceClient* 欄位給未來 client_credentials grant
- 移除 13+ 處 resolveUserID(uc, StaticUserID) fallback 改 strict mode
(Audit C1:multi-tenant 隔離破口)
- Pairing exchange MarkUsed 失敗 abort + revoke session token(Audit M3)
- 新增 all_endpoints_require_auth_test 整合測試(51 endpoint × 401)
驗證:go test -race -count=3 ./... 17 packages 全綠 / go vet 0 warning
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
394 lines
16 KiB
Markdown
394 lines
16 KiB
Markdown
# 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)
|
||
│ └── 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)
|
||
│ └── logger/ # slog JSON logger wrapper
|
||
├── 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 |
|
||
|
||
---
|
||
|
||
## 測試
|
||
|
||
```bash
|
||
# 所有單元測試 + integration test + race detector
|
||
make test-race
|
||
|
||
# 僅 go vet / gofmt check
|
||
make lint
|
||
|
||
# 詳細輸出
|
||
make test
|
||
```
|
||
|
||
覆蓋面:
|
||
- 單元測試:`internal/{auth,session,device,model,config,storage,api,relay,wsconn,logger,converter}`
|
||
- 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)
|
||
|
||
---
|
||
|
||
## 開發者指南
|
||
|
||
| 我想做的事 | 看哪裡 |
|
||
|-----------|--------|
|
||
| 新增一個 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)。
|