Phase 0.8 把 kneron_model_converter 的轉檔功能整合進 visionA Cloud。
visionA backend 當 streaming proxy(upload)+ delegated download token broker(download)+
ownership trust boundary,converter / FAA / MC 三方零修改。
新增 internal/conversion/ 套件(8 個檔,~10,000 行 prod+test,117+ test cases,race -count=3 全綠):
- conversion.go:Service interface 5 method、Job/PromoteResult/InitJobInput types
- errors.go:13+ sentinel errors + ErrorCode/HTTPStatus mapping,對齊 conversion.md §6
- mc_token_client.go:service-to-service token (client_credentials grant) + DCL cache
(exp - 15s 重取,per-scope cache),IssueDelegatedDownload(MC delegated download token)
錯誤分 idp_misconfigured (4xx) / idp_unavailable (5xx) / download_token_failed / mc_token_unavailable
- converter_client.go:對 converter scheduler 4 method(InitJob multipart streaming /
GetJob / Promote / ListInProgressJobs),InitJob 不 retry 5xx(streaming body 無法 replay)
- faa_client.go:對 FAA GET /files/{key} server-to-server pull,Phase A retry(GET 無 body
可 replay)對齊 §9.1 retry 矩陣,streaming io.ReadCloser 透傳避 OOM
- ownership.go:in-memory job_id → user_id map + per-user mutex 防 thundering herd lazy rebuild
(不同 user 平行 fetch,同 user 100 caller 收斂成 1 次),visionA 重啟靠 converter
ListInProgressJobs(user) 重建
- flow.go:Service interface 整合層(5 method 串接 converter/FAA/MC/ownership)
- InitJob 用 io.Pipe + multipart.Reader/Writer 重組 streaming proxy(黑名單 client user_id
+ 灌入 OIDC sub)
- DownloadRedirectURL 自動觸發 promote(spec §1 Stage 3b),用 ensurePromoted helper
- PromoteToModels 冪等(modelStore.FindBySourceJobID 為 source-of-truth)
- OwnershipMismatch → ErrJobNotFound 不 forbidden(§7.2 防枚舉)
- storage / modelStore 失敗包 ErrStorageUnavailable / ErrModelStoreUnavailable
(視為 visionA 自身 500 而非 502 gateway,SRE alarm 才打對 team)
新增 internal/api/conversion.go(5 endpoint handler + main.go wire):
- POST /api/conversion/init(multipart streaming proxy,不呼叫 c.MultipartForm())
- GET /api/conversion/active(lazy rebuild ownership)
- GET /api/conversion/{job_id}(poll status)
- POST /api/conversion/{job_id}/promote-to-models(FAA pull → models 三段式)
- GET /api/conversion/{job_id}/download(server-side HTTP 302 → FAA,token 不過 frontend
JS,仿 FAA TestSite DownloadFileDirect pattern;Cache-Control: no-store)
5 個 endpoint 全部走 OIDC AuthMiddleware;user_id 從 cookie session 灌(trust boundary),
從不接受 client multipart form / JSON / query 的 user_id。
TestAllAPIEndpointsRequire401WithoutCookie 自動覆蓋新 5 endpoint regression 防呆。
新增 cmd/api-server/conversion_e2e_test.go(4 個 e2e 場景):
- TestConversionE2E_StreamingProxy(10MB body + trust boundary regression)
- TestConversionE2E_LazyRebuildAfterRestart(visionA 重啟仍能 /active)
- TestConversionE2E_Download302Redirect(驗 302 + Location header + token 不在 body)
- TestConversionE2E_ActiveJobConflict(409 + active_job 詳情)
修改 internal/config/{config,load}.go:新增 ConversionConfig 5 欄位
(ConverterBaseURL / FAABaseURL / TenantID / ServiceClientID / ServiceClientSecret)+
Enabled() helper(雙非空判定)。
修改 cmd/api-server/main.go:條件 wire(cfg.Conversion.Enabled() 為 true 才建 client + Service;
否則 Deps.Conversion=nil,handler 自動回 501)。
修改 .env.example:新增 Phase 0.8 區塊註解。
新增 cmd/api-server/conversion_adapters.go:narrow interface adapter(接既有
internal/model.Repository / internal/storage.Store → conversion.ModelStore / Storage,避免 import cycle)。
驗證:go test -race -count=3 ./... 17 packages 全綠 / go vet 0 warning / go build 成功。
對齊文件:
- .autoflow/04-architecture/adr/adr-014-conversion-integration.md
- .autoflow/04-architecture/conversion.md (TDD)
- .autoflow/04-architecture/api/api-conversion.md
- .autoflow/02-prd/features/feature-converter-integration.md
- .autoflow/03-design/wireframes/wireframe-conversion.md
- .autoflow/03-design/flows/flow-conversion.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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(§7 部署).autoflow/04-architecture/TDD.md.autoflow/04-architecture/api/api-spec.md(前端 REST API).autoflow/04-architecture/api/api-internal.md(api-server ↔ remote-proxy)
技術堆疊
| 層級 | 技術 | 備註 |
|---|---|---|
| 語言 | Go 1.26 | go.mod 鎖定 |
| HTTP framework | 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(最快,適合開發)
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(接近生產拓撲)
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。
步驟
# 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。
| 群組 | 端點 | 說明 |
|---|---|---|
| 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 之後補) |
錯誤格式(統一)
{
"success": false,
"error": {
"code": "TUNNEL_DISCONNECTED",
"message": "local agent 未連線或 tunnel 斷開",
"request_id": "req_abc123"
}
}
錯誤碼清單見 internal/api/errors.go。
環境變數
詳見 .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 |
測試
# 所有單元測試 + 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)。
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)
目前實作進度
- B1 專案初始化(go.mod、目錄骨架、Makefile、.gitignore)
- B2 共用
internal/模組(core interface + in-memory 實作 + 單元測試) - B3
cmd/remote-proxy+ relay / tunnel / wsconn / cluster - B4
cmd/api-server+internal/api骨架 + Forwarder + ProxyClient - B5 API handlers 雛形(20+ endpoint 實作、兩階段上傳、tunnel forward)
- B6 Docker image + docker-compose(multi-stage + non-root + healthcheck)
- B7 README +
.env.example+ Makefile 補完
完整任務紀錄見 .autoflow/progress.md。