jim800121chen 22f0837ba8 feat(visionA-backend): Phase 0 → 0.7 雲端後端(雙 binary + OIDC BFF + stage 部署)
從 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>
2026-05-01 11:21:20 +08:00

394 lines
16 KiB
Markdown
Raw 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
│ └── 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
│ └── logger/ # slog JSON logger wrapper
├── 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 |
---
## 測試
```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`B4api-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 路線圖
- [ ] 真 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)。