# 8. 介面契約(Interface Contracts) — visionA Cloud > 父文件:[PRD.md](PRD.md) > > **本章節是 Phase 0 的核心產出之一**。Phase 0 有很多 TODO,介面契約讓未來能「無痛換實作」。 --- ## 8.1 為什麼介面這麼重要 Phase 0 是雛形階段,很多子系統用 **stub 實作**,但: - **雛形不實作 ≠ 雛形不設計** - 介面(interface / contract)現在就要定義清楚 - 未來只換實作,不動業務邏輯 五大關鍵介面: 1. **AuthProvider** — 會員系統(Phase 0 stub → Phase 1 JWT) 2. **SessionStore** — Tunnel session 狀態(Phase 0 in-memory → Phase 1 Redis) 3. **ObjectStorage** — 模型檔儲存(Phase 0 local fs → Phase 1 S3/MinIO) 4. **ConverterClient** — 轉檔服務(Phase 0 stub → Phase 2 真實 API) 5. **BillingProvider** — 計費(Phase 0 / 1 都不做 → Phase 2+) > 實際 Go interface 定義由 Architect Agent 寫進 TDD。本文件定義的是**需求與合約**。 --- ## 8.2 AuthProvider 介面 ### 目的 讓 visionA-backend 的所有 Auth 相關功能(登入、註冊、驗 token、登出)透過一個 interface 抽象,Phase 0 接 stub,Phase 1 接真實 Auth。 ### 必須提供的能力 | 能力 | 說明 | Phase 0 | Phase 1 | |------|------|---------|---------| | Register | 建立新 user(email + password)| stub(僅接受固定 demo-user)| DB | | Login | 驗證 user,發 session token | **接受任何帳密,一律回 demo-user + stub token** | JWT | | ValidateToken | 驗證 token,回傳 user | 查 in-memory map | 驗簽 + 查 DB | | Logout | 撤銷 token | 刪 in-memory entry | 加 blacklist | | GetUser | 查 user 資訊 | in-memory(固定 demo-user)| DB | | — 以下為 **Phase 1** 才做 — | | | | | RefreshToken | Refresh token rotation | ❌(Phase 1) | ✅ | | RequestPasswordReset | 寄 email 重設 | ❌(Phase 1) | ✅ | | ConfirmPasswordReset | 確認重設 | ❌(Phase 1) | ✅ | | Delete | 刪除帳號 + 所有相關資源 | ❌(Phase 1) | ✅ | ### Phase 0 雛形實作:`StaticAuthProvider` Phase 0 不做真實會員系統,改用 `StaticAuthProvider`: - **Login**:不驗證 email/password,一律接受並回 `demo-user` 的 stub token - **ValidateToken**:只認得 `StaticAuthProvider` 自己發的 stub token,驗過回 `demo-user` - **Logout**:從 in-memory map 刪 token - **GetUser**:固定回 `demo-user`(email: `demo@visiona.cloud`) - **Register**:Phase 0 可回 `ErrNotImplemented` 或直接回 demo-user(Architect 決定) - **Phase 1 方法**(RefreshToken / RequestPasswordReset / ConfirmPasswordReset / Delete):stub,直接回 `ErrNotImplemented` 這個設計讓前端登入流程能跑通(任何帳密都能登入),但不涉及真實會員資料。Phase 1 換成 `JWTAuthProvider`(綁 DB + JWT 簽章)時,所有業務邏輯不用動。 ### 錯誤類型 必須定義:`ErrUserNotFound`、`ErrInvalidCredentials`、`ErrTokenExpired`、`ErrTokenInvalid`、`ErrUserAlreadyExists`、`ErrNotImplemented`(Phase 0 stub 用)。 ### 使用方式 api-server 啟動時透過 DI 注入 `AuthProvider` 實作: ```go // Phase 0 authProvider := stub.NewStaticAuthProvider() // Phase 1 authProvider := jwt.NewJWTAuthProvider(db, jwtSecret) router := api.NewRouter(authProvider, ...) ``` **Middleware 設計**: ```go router.Use(auth.RequireAuth(authProvider)) // 需登入的路由 ``` ### 詳細介面定義 見 [features/feature-auth.md § 技術細節](features/feature-auth.md#技術細節給-architect-參考)。 --- ## 8.3 SessionStore 介面 ### 目的 Tunnel session 狀態管理。api-server 和 remote-proxy 都需要查詢「某 user/pairing token 對應哪個 tunnel session」,Phase 0 兩個 binary 共用記憶體(同一個 process 或 shared memory);Phase 1 用 Redis 跨節點共享。 ### 必須提供的能力 | 能力 | 說明 | |------|------| | CreatePairingToken | 建立一次性 pairing token,綁 user_id,15 min TTL | | ConsumePairingToken | 驗證並消耗 pairing token,回傳對應 user_id | | CreateSession | 建立 tunnel session,發 session token | | GetSession | 從 session token 查 session(回 user_id、device_id、created_at 等)| | RevokeSession | 撤銷 session(登出或使用者要求撤銷)| | ListSessionsByUser | 查使用者所有活躍 session | | UpdateSessionHeartbeat | 更新 session 最後心跳時間 | | CleanupExpired | 清過期 session | ### Phase 0 實作:`MemorySessionStore` - 單一 process 內用 sync.Map / mutex - api-server 和 remote-proxy **必須跑在同一個 process**(用 goroutine 起兩個 server)OR - **共享 memory store 透過 gRPC / HTTP**(remote-proxy 呼叫 api-server 的 internal API 查 session) > **Architect 需決策**:Phase 0 的 api-server 和 remote-proxy 是一個 process 還是兩個? > 兩個 binary 就是兩個 process,不能共享記憶體。 > 方案 A:兩個 process + internal gRPC 同步 session;方案 B:單 binary 啟動時透過參數選擇角色,Phase 0 都跑在一起。 > **建議 Phase 0 用方案 A**(符合最終架構),即便兩個 process 都在本機跑,介面也走 HTTP/gRPC。 ### Phase 1 實作:`RedisSessionStore` - 所有狀態存 Redis - api-server 和 remote-proxy 都連同一個 Redis - 支援多節點部署 ### 資料模型(邏輯) ``` PairingToken: - token (string, primary) - user_id (string) - created_at (timestamp) - expires_at (timestamp) - consumed (bool) - consumed_at (timestamp, nullable) Session: - session_token (string, primary) - user_id (string) - device_id (string) - pairing_token_used (string, FK) - created_at (timestamp) - last_heartbeat_at (timestamp) - revoked (bool) - revoked_at (timestamp, nullable) - proxy_node_id (string, Phase 1 for routing) ``` --- ## 8.4 ObjectStorage 介面 ### 目的 統一模型檔(`.nef`)、未來使用者上傳檔(影片、圖片)、推論結果等二進位資料的儲存。 ### 必須提供的能力 | 能力 | 說明 | |------|------| | Upload | 上傳檔案到指定 key | | Download | 下載檔案 | | Delete | 刪除檔案 | | List | 列出某 prefix 下的所有 key | | Exists | 檢查 key 是否存在 | | GetSize | 取得檔案大小 | | GetDownloadURL | 取得下載 URL(Phase 0 走自己的 API,Phase 1 用 presigned)| | GetUploadURL | (Phase 1)presigned upload URL | ### Key 規範 採統一的 key schema,讓未來換 S3 不需要 migrate: ``` models/{user_id}/{model_id}.nef ← 使用者上傳 models/system/{model_id}.nef ← 預設模型(所有 user 共享) uploads/{user_id}/{upload_id}.{ext} ← 使用者臨時上傳(影片、圖片等) converter-inputs/{job_id}/{filename} ← 轉檔的來源檔(臨時) converter-outputs/{job_id}/model.nef ← 轉檔結果 ``` ### Phase 0 實作:`LocalFSStorage` - 存在 `./data/storage/{key}` - `GetDownloadURL` 回傳 `/api/storage/download?key=...`,api-server 自己 stream 出去 - 需做 key 合法性檢查(不能 path traversal) ### Phase 1 實作:`S3Storage` / `MinIOStorage` - 標準 AWS S3 SDK 或 minio-go - 原生 presigned URL - Bucket 策略: - `visiona-models`:公開讀權限 for `system/` prefix,其他要 presigned - `visiona-uploads`:全私有 ### 檔案大小限制 - Phase 0:100 MB(記憶體限制) - Phase 1:500 MB(model)、5 GB(video upload) --- ## 8.5 ConverterClient 介面 ### 目的 呼叫 kneron_model_converter 服務做格式轉檔。Phase 0 / 1 用 stub,Phase 2 接真實 API。 ### 介面能力 見 [features/feature-converter-integration.md](features/feature-converter-integration.md) 的完整 API 合約。 **核心能力**: - `Submit(ConvertRequest) → ConvertJob` - `GetStatus(jobID) → ConvertJob` - `Download(jobID) → io.ReadCloser` - `Cancel(jobID) → error` ### 現況:converter API 尚未存在 **visionA-backend 先定義介面,對 converter 團隊提出 spec 需求**。流程: 1. Phase 0:visionA-backend 定義 `ConverterClient` interface + `StubConverterClient` 實作 2. Phase 0:PM Agent 把 API spec(`feature-converter-integration.md`)正式交付給 converter 團隊 3. Phase 0-1:converter 團隊依 spec 實作 API 4. Phase 2:visionA-backend 實作 `HTTPConverterClient`,換掉 stub ### Stub 行為 `StubConverterClient`: - Submit 回 fake job_id + status = queued - GetStatus 第一次回 queued,第二次回 processing,第三次之後回 completed(給 fake download URL) - Download 回一個內建的 fake `.nef`(就是某個預設模型) - 讓前端能跑完整流程 UI 開發 --- ## 8.6 BillingProvider 介面(Phase 2) ### 目的 抽象計費與訂閱管理,方便 Phase 2 接 Stripe / Paddle / 其他。 ### 介面能力 | 能力 | 說明 | |------|------| | CreateCustomer | 建立計費客戶 | | CreateSubscription | 建立訂閱 | | CancelSubscription | 取消訂閱 | | UpdateSubscription | 升降級 | | ReportUsage | 上報使用量(usage-based pricing) | | CreateCheckoutSession | 建立結帳 session(hosted checkout)| | HandleWebhook | 處理 provider webhook(付款成功、失敗等)| | GetInvoices | 列出 invoice | ### Phase 0 / 1 都不做 Phase 0 / 1 visionA-backend 不存在任何 billing 相關程式碼。Phase 2 再從頭加。 --- ## 8.7 給外部團隊的 API 合約清單 Phase 0 期間,需要對外溝通的契約: ### 對 kneron_model_converter 團隊 **交付文件**:[features/feature-converter-integration.md](features/feature-converter-integration.md) **對方 Action Items**: - 審閱 API spec - 評估實作時程 - 提供 dev 環境與 test API key - 決定 webhook 簽章格式 - 決定 rate limit 數字 ### 對 local-tool 團隊(同 repo) Phase 0 期間 local-tool **完全不動**。Phase 1 才考慮整合: - local-tool 新增「配對到 visionA Cloud」功能 - 要共享哪些 Go 套件(tunnel client 可能可共用) ### 對 Innovedus IT / DevOps Phase 0 後期,若要部署 staging 環境,需要: - 一個子域名(例如 `cloud.visiona.dev`) - TLS 憑證(Let's Encrypt) - Docker host 或 AWS account - DNS 管理權 --- ## 8.8 API 版本策略 ### URL 版本 所有 visionA Cloud API 採 URL 版本策略: ``` /api/v1/... ← Phase 0 從 v1 開始 /api/v2/... ← 未來 breaking change 時 ``` ### 相容性 - 同一 major version 內必須向後相容 - 遇到必須 breaking 時開新 major version,舊版保留至少 6 個月 - Phase 0 內部使用,允許 v1 小幅變動(不視為 breaking) ### WebSocket URL ``` wss://api.visiona.cloud/api/v1/ws/... wss://relay.visiona.cloud/v1/tunnel/connect ``` --- ## 連結 - 上一章:[非功能性需求](nonfunctional.md) - 下一章:[成功指標](success-metrics.md) - 跳回:[PRD 索引](PRD.md)