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>
190 lines
7.8 KiB
Plaintext
190 lines
7.8 KiB
Plaintext
# visionA-backend 環境變數範本
|
||
#
|
||
# 使用方式:
|
||
# cp .env.example .env
|
||
# # 視情況修改 .env 內的值(尤其 VISIONA_STORAGE_SIGNING_SECRET 與 VISIONA_PAIRING_TOKEN)
|
||
#
|
||
# ⚠️ 不要把 .env commit 進 git(已在 .gitignore 中排除)
|
||
# 相關文件:
|
||
# - .autoflow/04-architecture/build-deploy.md §9(變數對照表)
|
||
# - internal/config/config.go(每個欄位的定義)
|
||
|
||
# ============================================================
|
||
# 共用
|
||
# ============================================================
|
||
|
||
# 日誌等級:debug / info / warn / error
|
||
VISIONA_LOG_LEVEL=info
|
||
|
||
|
||
# ============================================================
|
||
# api-server
|
||
# ============================================================
|
||
|
||
# 對前端的 REST / WebSocket port(對齊 local-tool 的 base URL 預設)
|
||
VISIONA_API_PORT=3721
|
||
|
||
# api-server 連 remote-proxy 的 internal HTTP base URL
|
||
# 本機 go run 時用 localhost;docker-compose 內部會被 compose 覆寫為 http://remote-proxy:3801
|
||
VISIONA_PROXY_INTERNAL_URL=http://localhost:3801
|
||
|
||
# Static user — Phase 0.7 security audit 後僅供 dev seed(VISIONA_SEED_DEMO_DATA=true)
|
||
# 與 unit test fixture 用;不再注入 api.Deps、stage/prod 留空無影響。
|
||
# 見 .autoflow/05-implementation/review/phase-0.7-security-audit.md C1。
|
||
VISIONA_STATIC_USER_ID=demo-user
|
||
|
||
# 啟動時 seed 示範資料(device + model + pairing token),方便前端 demo
|
||
VISIONA_SEED_DEMO_DATA=true
|
||
|
||
# CORS 白名單(逗號分隔)— 預設允許 frontend dev server(http://localhost:3000)
|
||
VISIONA_CORS_ALLOWED_ORIGINS=http://localhost:3000
|
||
|
||
# ============================================================
|
||
# OIDC(必填 — OB5 起 OIDC 是唯一認證路徑;A1 起支援 public PKCE-only client)
|
||
# ============================================================
|
||
# 必填欄位缺任何一項,main.go 啟動時會 fatal log 退出。
|
||
#
|
||
# 對應 Innovedus Member Center 的 OIDC client 設定:
|
||
# - 在 Member Center 註冊一個 OAuth client(confidential 或 public 皆可)
|
||
# - 取得 client_id(public client 沒有 client_secret)
|
||
# - 將 RedirectURL 加入 Member Center 的白名單
|
||
|
||
# Member Center 的 issuer(不帶結尾斜線;MC 的 issuer 末尾斜線必要時請保留)
|
||
# dev: http://localhost:5050
|
||
# stage: https://stage-9527.innovedus.com:7850/
|
||
# prod: https://members.innovedus.com
|
||
VISIONA_OIDC_ISSUER_URL=http://localhost:5050
|
||
|
||
# 在 Member Center 註冊的 OAuth client_id
|
||
VISIONA_OIDC_CLIENT_ID=visiona-cloud
|
||
|
||
# Client secret(A1:選填 — public PKCE-only client 留空)
|
||
# - 有值 → confidential client mode(client_secret + PKCE 雙保險)
|
||
# - 留空 → public PKCE-only client mode(依靠 PKCE 防 code interception)
|
||
# ⚠️ 不可 commit;prod 用 Secrets Manager。Stage MC 配的 client `b8093fea...` 是 public,留空。
|
||
VISIONA_OIDC_CLIENT_SECRET=
|
||
|
||
# Backend callback URL — 必須與 Member Center 註冊值完全一致
|
||
# dev: http://localhost:3721/api/auth/callback
|
||
# stage: https://stage-9527.innovedus.com:9527/api/auth/callback
|
||
# prod: https://api.visiona.cloud/api/auth/callback
|
||
VISIONA_OIDC_REDIRECT_URL=http://localhost:3721/api/auth/callback
|
||
|
||
# Frontend base URL — callback 完成後 302 redirect 的目的地
|
||
# dev: http://localhost:3000
|
||
# stage: https://stage-9527.innovedus.com:9527
|
||
# prod: https://app.visiona.cloud
|
||
VISIONA_FRONTEND_URL=http://localhost:3000
|
||
|
||
# Service client(client_credentials grant)— A1 預留欄位,**目前不啟用**。
|
||
# 將來 visionA-backend 需以服務身份呼叫 MC API 時(例如查詢使用者組織、推送通知)
|
||
# 才會接這條路。留空代表「不啟用」,main.go 不會 wire。
|
||
# 對應 Stage 的 service client:<see stage .env.stage>
|
||
VISIONA_OIDC_SERVICE_CLIENT_ID=
|
||
VISIONA_OIDC_SERVICE_CLIENT_SECRET=
|
||
|
||
# Cookie HMAC 簽章 secret(⚠️ 至少 32 byte 隨機字串;prod 用 openssl rand -hex 32)
|
||
VISIONA_SESSION_SECRET=CHANGE_ME_TO_RANDOM_64_BYTES_in_production
|
||
|
||
# Cookie 設定(dev 預設 host-only / non-secure;prod 改 .visiona.cloud + Secure=true)
|
||
VISIONA_SESSION_COOKIE_NAME=visiona_session
|
||
VISIONA_SESSION_COOKIE_DOMAIN=
|
||
VISIONA_SESSION_COOKIE_SECURE=false
|
||
|
||
# Session TTL — 預設 7 天 absolute / 24h idle
|
||
VISIONA_SESSION_ABSOLUTE_TTL=168h
|
||
VISIONA_SESSION_IDLE_TTL=24h
|
||
|
||
# Relay 對外可達 URL(agent tunnel 用)— POST /api/pairing/exchange 會回給 agent。
|
||
# 雛形為空時會 fallback 到 wss://relay.visionA.cloud(placeholder)。
|
||
# 實機請設為實際可達的 WSS URL,例:wss://relay.visionA.cloud
|
||
VISIONA_RELAY_PUBLIC_URL=
|
||
|
||
|
||
# ============================================================
|
||
# remote-proxy
|
||
# ============================================================
|
||
|
||
# 對 local agent 的 WebSocket tunnel port
|
||
VISIONA_TUNNEL_PORT=3800
|
||
|
||
# 對 api-server 的 internal HTTP port(不對外暴露)
|
||
VISIONA_PROXY_INTERNAL_PORT=3801
|
||
|
||
|
||
# ============================================================
|
||
# Tunnel 心跳 / 掉線判定(對齊 tunnel.md §4.2)
|
||
# ============================================================
|
||
|
||
VISIONA_TUNNEL_HEARTBEAT_INTERVAL=10s
|
||
VISIONA_TUNNEL_IDLE_TIMEOUT=30s
|
||
|
||
|
||
# ============================================================
|
||
# Storage(LocalFS — Phase 0 雛形;Phase 1 會改 S3)
|
||
# ============================================================
|
||
|
||
# 儲存根目錄(容器內;docker-compose 已 mount 成 volume)
|
||
VISIONA_STORAGE_BACKEND=localfs
|
||
VISIONA_STORAGE_LOCALFS_ROOT=./data/storage
|
||
|
||
# 瀏覽器 / 上傳 client 看到的 presigned URL base
|
||
# 本機開發:http://localhost:3721/storage
|
||
# docker-compose demo:同上(透過 host port mapping)
|
||
VISIONA_STORAGE_BASE_URL=http://localhost:3721/storage
|
||
VISIONA_STORAGE_LOCALFS_BASE_URL=http://localhost:3721/storage
|
||
|
||
# HMAC 簽章 secret — 用於 LocalFS presigned URL 與(Phase 1)pairing token hash
|
||
# ⚠️ 生產環境必改(openssl rand -hex 32 產生 64 字元 hex)
|
||
VISIONA_STORAGE_SIGNING_SECRET=CHANGE_ME_IN_PRODUCTION_use_openssl_rand_hex_32
|
||
|
||
|
||
# ============================================================
|
||
# Model 上傳限制
|
||
# ============================================================
|
||
|
||
# 單檔上限(MB)— Phase 0 規範 100 MB(PRD §8.4)
|
||
VISIONA_MODEL_MAX_SIZE_MB=100
|
||
|
||
|
||
# ============================================================
|
||
# Pairing(local agent ↔ remote-proxy 配對)
|
||
# ============================================================
|
||
|
||
# 格式:vAc_ + 32 hex(見 security.md §1.3)
|
||
# 建議用:vAc_$(openssl rand -hex 16)
|
||
# 留空代表雛形 InMemoryPairingStore 會動態配發(前端呼叫 POST /api/pairing/token)
|
||
VISIONA_PAIRING_TOKEN=
|
||
|
||
|
||
# ============================================================
|
||
# Phase 0.8 — 轉檔功能整合(converter / FAA / Member Center service token)
|
||
# ============================================================
|
||
# 對齊 .autoflow/04-architecture/conversion.md §5.3
|
||
#
|
||
# 啟用判定:當 VISIONA_CONVERTER_BASE_URL 與 VISIONA_FAA_BASE_URL 都非空時,
|
||
# main.go 才會 wire conversion.Service;其中之一留空 → 5 個 /api/conversion/* endpoint 回 501。
|
||
#
|
||
# 啟用時 VISIONA_OIDC_SERVICE_CLIENT_ID/SECRET 必須非空(轉檔依賴 service token 機制)。
|
||
|
||
# kneron_model_converter task-scheduler base URL
|
||
# dev/stage:http://192.168.0.130:9501
|
||
# prod:https://converter.visiona.cloud
|
||
VISIONA_CONVERTER_BASE_URL=
|
||
|
||
# File Access Agent base URL
|
||
# dev/stage:http://192.168.0.130:5081
|
||
# prod:https://faa.innovedus.com
|
||
VISIONA_FAA_BASE_URL=
|
||
|
||
# visionA 在 Member Center 的 tenant id(單一 tenant)
|
||
# 跟 MC 換 delegated download token 時當 tenant_id 欄位用
|
||
VISIONA_OIDC_TENANT_ID=
|
||
|
||
# Delegated download token TTL(秒)— FAA 直連下載用
|
||
# 預設 300(5 分鐘),可調整範圍 60-900
|
||
VISIONA_FAA_DELEGATED_TTL_SECONDS=300
|
||
|
||
# 上傳模型檔大小上限(MB)— 與 converter 端 limit 對齊
|
||
VISIONA_CONVERTER_MAX_MODEL_SIZE_MB=500
|