jim800121chen 9e29ebf767 feat(visionA-backend): Phase 0.8b v0.6 T4 — config FAA 欄位砍 + .env 清 + i18n/godoc polish
對齊 ADR-016 / conversion.md v0.6.1 §3.1:visionA 端不再需要 FAA 設定(v0.5 T1 加的 FAAAPIKey/FAABaseURL 撤回)。

config 砍除:
- internal/config/config.go: ConversionConfig.FAABaseURL + FAAAPIKey 兩欄位
- internal/config/load.go: VISIONA_FAA_BASE_URL + VISIONA_FAA_API_KEY 兩 env 讀取
- Enabled() 簡化為「ConverterBaseURL + ConverterAPIKey 兩個非空」
- internal/config/load_test.go: TestLoad_ConversionEnabled 從 6 case 簡化為 4 case (all_set / missing_converter_url / missing_converter_key / all_empty)

.env*.example 對齊(3 個檔):
- visionA-backend/.env.example: 砍 2 個 FAA env row + 註解;header 改「2 欄位啟用」
- .env.stage.example: 同上;VISIONA_CONVERTER_API_KEY 保留 CHANGE_ME_OPENSSL_RAND_HEX_32 placeholder
- .env.dev.example: 註解區塊統一對齊

T3 review polish:
- m-2 internal/api/conversion.go: i18n message map 砍 4 個 dead case (download_token_failed / mc_token_unavailable / idp_misconfigured / idp_unavailable) — 對應 v0.5 mc_token_client 撤回時砍的 sentinel;落入 default「內部錯誤」、行為不變
- m-3 internal/conversion/util.go: hashObjectKey godoc 補「設計約束(重要)」段 + 3 條「不應做的事」(不出現在 response body/header / 不組 URL / 不寫進 user-facing 錯誤訊息)— 明示用途限定於 slog 欄位內、避免 misuse vector
- cmd/api-server/main.go: godoc 對齊 T4 完成狀態

驗證:
- B 層 verification 主動跑(T3 reviewer 接受暫緩、backend 主動跑避免 reviewer 二次要求):
  * 跨檔 grep: production code 0 functional 命中(殘留全是註解 audit trail / test fixture name)
  * 17 packages race -count=3 全綠
  * 3 個 .env 環境一致性驗證
- go build ./... exit 0
- go test -race -count=3 ./... 17 packages 全綠
- Reviewer 5 軸(v0.6-t4-review) 通過(0 Critical / 0 Major / 2 Minor / 4 Suggestion)

v0.6 對齊改造事實上完工:
- T1 ConverterClient.GetResult method
- T2 flow.go DownloadStream/PromoteToModels 改用 GetResult + e2e endpoint
- T3 faa_client 整檔砍 + ErrFAA* sentinel 清 + s-3/s-4/s-5 必補 + mockFAA regression-only
- T4 config FAA 欄位砍 + .env 清 + i18n/godoc polish

main.go startup log 已是「converter_api_key_set only」、無 FAA 殘留 / 無 tenant_id(T2-T3 已處理)。e2e regression 防護由 mockFAA negative assertion 守住(T3)。

下一步:
- visionA backend 端 ADR-016 對齊完工,等使用者跨 repo 加 converter GET /api/v1/jobs/{id}/result endpoint
- stage redeploy + e2e 完整測試

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 19:16:28 +08:00

145 lines
5.6 KiB
Go
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.

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 C1VISIONA_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", ""),
// A1client_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),
},
}
}
// 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
}