package config import ( "os" "strconv" "strings" "time" ) // Load 從環境變數讀取並組出一個 Config。 // // 所有欄位皆有預設值(雛形便利),因此 Load 不會回傳 error; // 未來加入必填欄位時(例如 Phase 1 的 DB URL),應改為回傳 error。 func Load() *Config { return &Config{ Server: ServerConfig{ Host: getEnvString("VISIONA_HOST", "0.0.0.0"), Port: getEnvInt("VISIONA_API_PORT", 3721), TunnelPort: getEnvInt("VISIONA_TUNNEL_PORT", 3800), InternalPort: getEnvInt("VISIONA_PROXY_INTERNAL_PORT", 3801), RelayPublicURL: getEnvString("VISIONA_RELAY_PUBLIC_URL", ""), SeedDemoData: getEnvBool("VISIONA_SEED_DEMO_DATA", false), }, Session: SessionConfig{ Backend: getEnvString("VISIONA_SESSION_BACKEND", "inmemory"), ProxyInternalURL: getEnvString("VISIONA_PROXY_INTERNAL_URL", "http://localhost:3801"), }, Auth: AuthConfig{ // Phase 0.7 security fix C1:VISIONA_STATIC_USER_ID 僅供 dev seed / unit test 用, // stage/prod 留空無影響;不再注入 api.Deps(見 internal/api/api.go Deps 註解)。 StaticUserID: getEnvString("VISIONA_STATIC_USER_ID", "demo-user"), PairingToken: getEnvString("VISIONA_PAIRING_TOKEN", ""), SigningSecret: getEnvString("VISIONA_STORAGE_SIGNING_SECRET", "dev-signing-secret-do-not-use-in-prod"), }, OIDC: OIDCConfig{ IssuerURL: getEnvString("VISIONA_OIDC_ISSUER_URL", ""), ClientID: getEnvString("VISIONA_OIDC_CLIENT_ID", ""), ClientSecret: getEnvString("VISIONA_OIDC_CLIENT_SECRET", ""), RedirectURL: getEnvString("VISIONA_OIDC_REDIRECT_URL", ""), PostLoginURL: getEnvString("VISIONA_FRONTEND_URL", ""), // A1:client_credentials grant 預留欄位,留空表「不啟用 service client」。 ServiceClientID: getEnvString("VISIONA_OIDC_SERVICE_CLIENT_ID", ""), ServiceClientSecret: getEnvString("VISIONA_OIDC_SERVICE_CLIENT_SECRET", ""), }, UserSession: UserSessionConfig{ Secret: getEnvString("VISIONA_SESSION_SECRET", ""), CookieName: getEnvString("VISIONA_SESSION_COOKIE_NAME", "visiona_session"), CookieDomain: getEnvString("VISIONA_SESSION_COOKIE_DOMAIN", ""), CookieSecure: getEnvBool("VISIONA_SESSION_COOKIE_SECURE", false), AbsoluteTTL: getEnvDuration("VISIONA_SESSION_ABSOLUTE_TTL", 168*time.Hour), IdleTTL: getEnvDuration("VISIONA_SESSION_IDLE_TTL", 24*time.Hour), }, Storage: StorageConfig{ Backend: getEnvString("VISIONA_STORAGE_BACKEND", "localfs"), RootDir: getEnvString("VISIONA_STORAGE_LOCALFS_ROOT", "./data/storage"), BaseURL: getEnvString("VISIONA_STORAGE_LOCALFS_BASE_URL", "http://localhost:3721/storage"), }, Model: ModelConfig{ MaxSizeMB: getEnvInt("VISIONA_MODEL_MAX_SIZE_MB", 100), }, Tunnel: TunnelConfig{ HeartbeatInterval: getEnvDuration("VISIONA_TUNNEL_HEARTBEAT_INTERVAL", 10*time.Second), IdleTimeout: getEnvDuration("VISIONA_TUNNEL_IDLE_TIMEOUT", 30*time.Second), }, Logger: LoggerConfig{ Level: getEnvString("VISIONA_LOG_LEVEL", "info"), }, CORS: CORSConfig{ AllowedOrigins: getEnvStringSlice("VISIONA_CORS_ALLOWED_ORIGINS", nil), }, // Phase 0.8 / 0.8b conversion (見 docs/autoflow/04-architecture/conversion.md §3、 // ADR-015、ADR-016) // Phase 0.8b T5:原暫留欄位 TenantID / DelegatedTTLSeconds 與對應 env // (VISIONA_OIDC_TENANT_ID / VISIONA_FAA_DELEGATED_TTL_SECONDS)已移除 — // MC 認證鏈與 delegated download token 機制不存在了。 // Phase 0.8b v0.6 T4:原 FAA 相關欄位 FAABaseURL / FAAAPIKey 與對應 env // (VISIONA_FAA_BASE_URL / VISIONA_FAA_API_KEY)已移除 — ADR-016 撤回 v0.5 // 設計缺口,visionA 端不再直接呼叫 FAA、download/promote 改走 converter.GetResult。 Conversion: ConversionConfig{ ConverterBaseURL: getEnvString("VISIONA_CONVERTER_BASE_URL", ""), ConverterAPIKey: getEnvString("VISIONA_CONVERTER_API_KEY", ""), MaxModelSizeMB: getEnvInt("VISIONA_CONVERTER_MAX_MODEL_SIZE_MB", 500), }, // Phase 0.9 模型庫 FAA 直連下載(ADR-017 (a),見 adr-017 §10 stage e2e 藍本)。 // ⚠️ 技術債:ServiceClientID/Secret 第一階段 PoC 共用 FAA 的 service client; // 正式上線前須換 visionA 專屬 usage=file_api client(ADR-017 §7 R1 / Q10)。 FileAccess: FileAccessConfig{ MCBaseURL: getEnvString("VISIONA_FILE_ACCESS_MC_BASE_URL", ""), ServiceClientID: getEnvString("VISIONA_FILE_ACCESS_SERVICE_CLIENT_ID", ""), ServiceClientSecret: getEnvString("VISIONA_FILE_ACCESS_SERVICE_CLIENT_SECRET", ""), TenantID: getEnvString("VISIONA_FILE_ACCESS_TENANT_ID", ""), FAABaseURL: getEnvString("VISIONA_FILE_ACCESS_FAA_BASE_URL", ""), DownloadTokenTTLSeconds: getEnvInt("VISIONA_FILE_ACCESS_DOWNLOAD_TOKEN_TTL_SECONDS", 120), }, // DB 接入塊 0:PostgreSQL 連線(持久業務資料)。 // 對齊 docs/autoflow/04-architecture/database.md §5.5.1。 // 留空(Host/User/DBName 任一缺)→ Enabled()=false → main.go 不建池、維持 in-memory。 Database: DatabaseConfig{ Host: getEnvString("VISIONA_DB_HOST", ""), Port: getEnvInt("VISIONA_DB_PORT", 5432), User: getEnvString("VISIONA_DB_USER", ""), Password: getEnvString("VISIONA_DB_PASSWORD", ""), DBName: getEnvString("VISIONA_DB_NAME", ""), SSLMode: getEnvString("VISIONA_DB_SSLMODE", "require"), MaxConns: getEnvInt("VISIONA_DB_MAX_CONNS", 10), MinConns: getEnvInt("VISIONA_DB_MIN_CONNS", 2), MaxConnLifetime: getEnvDuration("VISIONA_DB_MAX_CONN_LIFETIME", time.Hour), ConnTimeout: getEnvDuration("VISIONA_DB_CONN_TIMEOUT", 5*time.Second), AutoMigrate: getEnvBool("VISIONA_DB_AUTO_MIGRATE", true), }, // DB 接入塊 4(鉤子先留,塊 0 不 wire):Redis 連線(僅 userSession)。 // 對齊 docs/autoflow/04-architecture/database.md §5.5.2。 Redis: RedisConfig{ Host: getEnvString("VISIONA_REDIS_HOST", ""), Port: getEnvInt("VISIONA_REDIS_PORT", 6379), Password: getEnvString("VISIONA_REDIS_PASSWORD", ""), DB: getEnvInt("VISIONA_REDIS_DB", 0), ConnTimeout: getEnvDuration("VISIONA_REDIS_CONN_TIMEOUT", 5*time.Second), }, } } // getEnvStringSlice 從環境變數取逗號分隔字串,拆成 slice。 // 每段都會 TrimSpace;空段會被過濾。若環境變數未設定或為空,回傳 fallback。 func getEnvStringSlice(key string, fallback []string) []string { v, ok := os.LookupEnv(key) if !ok || v == "" { return fallback } parts := strings.Split(v, ",") result := make([]string, 0, len(parts)) for _, p := range parts { if trimmed := strings.TrimSpace(p); trimmed != "" { result = append(result, trimmed) } } if len(result) == 0 { return fallback } return result } // getEnvString 從環境變數取字串,不存在或為空則回傳預設值。 func getEnvString(key, fallback string) string { if v, ok := os.LookupEnv(key); ok && v != "" { return v } return fallback } // getEnvInt 從環境變數取整數,若無法解析則回傳預設值。 func getEnvInt(key string, fallback int) int { if v, ok := os.LookupEnv(key); ok && v != "" { if n, err := strconv.Atoi(v); err == nil { return n } } return fallback } // getEnvDuration 從環境變數取 time.Duration(支援 "10s"、"1m" 等格式)。 func getEnvDuration(key string, fallback time.Duration) time.Duration { if v, ok := os.LookupEnv(key); ok && v != "" { if d, err := time.ParseDuration(v); err == nil { return d } } return fallback } // getEnvBool 從環境變數取布林值(接受 "true"/"false"/"1"/"0",大小寫不敏感)。 // 解析失敗或未設定回傳 fallback。 func getEnvBool(key string, fallback bool) bool { if v, ok := os.LookupEnv(key); ok && v != "" { if b, err := strconv.ParseBool(v); err == nil { return b } } return fallback }