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

315 lines
14 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 定義 visionA-backend 的組態結構,對齊 TDD §2.10。
//
// 雛形遵循 12-Factor App所有可變設定皆透過環境變數注入不寫死在程式碼裡。
// `api-server` 與 `remote-proxy` 共享同一份 Config各自只消費自己需要的欄位。
package config
import "time"
// Config 是整個 visionA-backend 的環境設定。
//
// 所有欄位皆由 Load() 從環境變數讀取並套用預設值。
// 欄位命名對齊 TDD §2.10;新增欄位時請同步更新 `.env.example`(待 B6
type Config struct {
Server ServerConfig
Session SessionConfig
Auth AuthConfig
OIDC OIDCConfig
UserSession UserSessionConfig
Storage StorageConfig
Model ModelConfig
Tunnel TunnelConfig
Logger LoggerConfig
CORS CORSConfig
// Conversion 控制 Phase 0.8 轉檔功能整合converter / FAA / MC service token
// 對齊 .autoflow/04-architecture/conversion.md §5.3。
Conversion ConversionConfig
}
// ServerConfig 控制 HTTP listener 的位址與埠號。
//
// api-server 端使用 Port 提供 REST / WebSocket
// remote-proxy 端使用 TunnelPort面向 local agent與 InternalPort面向 api-server
//
// Port 預設為 3721 — 對齊 local-tool 的 base URL這樣 local-tool 前端切到雲端版時
// base URL 可以維持一致,降低前端的 dev 流程切換成本B4 決定)。
type ServerConfig struct {
Host string // VISIONA_HOST預設 "0.0.0.0"
Port int // VISIONA_API_PORT預設 3721對齊 local-tool
TunnelPort int // VISIONA_TUNNEL_PORT預設 3800
InternalPort int // VISIONA_PROXY_INTERNAL_PORT預設 3801
// RelayPublicURL 是 agent 連 tunnel 用的對外可達 URL通常是 wss://.../tunnel/connect
// 的 origin 部分wss://relay.visionA.cloud
// AB11 新增:/api/pairing/exchange 會把這個值回傳給 agent。
// 雛形預設為空 — handler 會 fallback 到 placeholder `wss://relay.visionA.cloud`。
RelayPublicURL string
// SeedDemoData 控制 api-server 啟動時是否塞入示範用 device + model + pairing token。
// 預設 false本機開發或 demo 時可設 VISIONA_SEED_DEMO_DATA=true 開啟,
// 方便前端不必跑完整 pairing 流程就能看到資料。
SeedDemoData bool
}
// SessionConfig 控制 SessionStore 的實作選擇與連線資訊。
//
// Backend:
// - "inmemory" — remote-proxy 端持有 yamux session 的唯一來源
// - "proxy-client" — api-server 端透過 internal HTTP 查詢 remote-proxy
type SessionConfig struct {
Backend string // VISIONA_SESSION_BACKEND預設 "inmemory"
ProxyInternalURL string // VISIONA_PROXY_INTERNAL_URL預設 "http://localhost:3801"
}
// AuthConfig 控制雛形專用的 user fallback 與 pairing token。
//
// OB52026-04-26起認證走 OIDCOIDCConfig
// Phase 0.72026-05-01security audit 移除了 api.Deps.StaticUserID handler fallback
// (見 .autoflow/05-implementation/review/phase-0.7-security-audit.md C1
// 此處的 StaticUserID 欄位**僅供 dev seedVISIONA_SEED_DEMO_DATA=true與 unit test
// fixture 讀取使用**,不再注入 api.Deps、不影響 stage/prod 認證行為。
type AuthConfig struct {
// StaticUserID — Deprecated for routing/auth use. 僅供 dev seed / unit test。
// 見 internal/api/api.go 的 Deps 註解stage/prod 留空無影響。
StaticUserID string // VISIONA_STATIC_USER_ID預設 "demo-user"dev seed only
PairingToken string // VISIONA_PAIRING_TOKEN格式必須為 vAc_ + 32 hex
SigningSecret string // VISIONA_STORAGE_SIGNING_SECRETpresigned URL HMAC secret
}
// OIDCConfig 控制 OpenID Connect 登入流程BFF 模式)。
//
// 對齊 oidc-tdd.md §13.1 + ADR-010 + ADR-011 + ADR-013。
// OB5 起 OIDC 是唯一認證路徑A1 起支援 public PKCE-only client。
type OIDCConfig struct {
// IssuerURL 是 OIDC IdP 的 issuer不帶結尾斜線例如
// dev: http://localhost:5050
// prod: https://members.innovedus.com
// 對齊 VISIONA_OIDC_ISSUER_URL。
IssuerURL string
// ClientID 是 visionA 在 IdP 註冊的 OAuth client idconfidential 或 public 皆可)。
// 對齊 VISIONA_OIDC_CLIENT_ID。
ClientID string
// ClientSecret 為**選填**A1, 2026-05-01
// - 有值 → confidential client modeclient_secret + PKCE 雙保險)
// - 留空 → PKCE-only public client mode純依靠 PKCE 防 code interception
// 兩種 mode 由 IdP 決定visionA-backend 都支援(見 ADR-013
// **禁止 commit 進 repo**;對齊 VISIONA_OIDC_CLIENT_SECRET。
ClientSecret string
// RedirectURL 是 visionA-backend 的 callback URL必須與 IdP 註冊值完全一致。
// dev: http://localhost:3721/api/auth/callback
// prod: https://api.visiona.cloud/api/auth/callback
// 對齊 VISIONA_OIDC_REDIRECT_URL。
RedirectURL string
// PostLoginURL 是 callback 完成後 302 回 frontend 的 base URL。
// dev: http://localhost:3000
// prod: https://app.visiona.cloud
// 對齊 VISIONA_FRONTEND_URL沿用 oidc-tdd.md §13.1 命名)。
PostLoginURL string
// ServiceClientID 是「visionA-backend 以服務身份呼叫 MC API」用的 client id
// 預留給未來 client_credentials grant flow例如查詢使用者組織、推送通知等
//
// **A1 階段不啟用**Validate() 不檢查、main.go 不 wire只先把 config 鉤子留好,
// 之後接時不必再改 OIDCConfig schema。對齊 VISIONA_OIDC_SERVICE_CLIENT_ID。
ServiceClientID string
// ServiceClientSecret 是 service clientclient_credentials grant的 secret。
// 與 ServiceClientID 配對使用;同樣 A1 階段不啟用、Validate() 不檢查。
// **禁止 commit 進 repo**;對齊 VISIONA_OIDC_SERVICE_CLIENT_SECRET。
ServiceClientSecret string
}
// UserSessionConfig 控制 OIDC 登入後在 browser 端建立的 cookie session。
//
// 注意:與既有 SessionConfigtunnel session 用)刻意分開,避免命名混淆。
// 對齊 oidc-tdd.md §5、§13.1。
type UserSessionConfig struct {
// Secret 是 cookie HMAC-SHA256 簽章金鑰;應為至少 32 byte 隨機字串。
// 對齊 VISIONA_SESSION_SECRET。
Secret string
// CookieName 預設 "visiona_session"。
// 對齊 VISIONA_SESSION_COOKIE_NAME。
CookieName string
// CookieDomaindev 留空host-only cookieprod 設 ".visiona.cloud"。
// 對齊 VISIONA_SESSION_COOKIE_DOMAIN。
CookieDomain string
// CookieSecure 控制 Secure flag。dev=falsehttpprod=truehttps
// 對齊 VISIONA_SESSION_COOKIE_SECURE。
CookieSecure bool
// AbsoluteTTL 是 session 的最長存活時間(從 Create 起算)。預設 168h7 天)。
// 對齊 VISIONA_SESSION_ABSOLUTE_TTL。
AbsoluteTTL time.Duration
// IdleTTL 是 session 的閒置存活時間(從 LastSeenAt 起算)。預設 24h。
// 對齊 VISIONA_SESSION_IDLE_TTL。
IdleTTL time.Duration
}
// StorageConfig 控制儲存層實作LocalFS / S3與路徑。
type StorageConfig struct {
Backend string // VISIONA_STORAGE_BACKEND預設 "localfs"
RootDir string // VISIONA_STORAGE_LOCALFS_ROOT預設 "./data/storage"
BaseURL string // VISIONA_STORAGE_LOCALFS_BASE_URL預設 "http://localhost:3721/storage"(對齊 api-server port
}
// ModelConfig 針對模型資源的驗證限制(大小等)。
type ModelConfig struct {
// MaxSizeMB 是允許上傳的單一模型檔案大小上限MB
// PRD §8.4 規範 Phase 0 為 100 MB可由 VISIONA_MODEL_MAX_SIZE_MB 覆寫。
MaxSizeMB int
}
// TunnelConfig 控制 tunnel 心跳與掉線判定閾值,對齊 tunnel.md §4.2。
type TunnelConfig struct {
// HeartbeatInterval 為 yamux KeepAliveInterval 值。預設 10s。
HeartbeatInterval time.Duration
// IdleTimeout 為判定對端失聯的時間。預設 30s= 3 次心跳未回)。
IdleTimeout time.Duration
}
// LoggerConfig 控制結構化 logger 的輸出等級。
type LoggerConfig struct {
Level string // VISIONA_LOG_LEVELdebug / info / warn / error預設 "info"
}
// ConversionConfig 控制 Phase 0.8 / 0.8b 轉檔功能整合。
//
// 對齊 docs/autoflow/04-architecture/conversion.md §3、
// `adr/adr-015-server-to-server-api-key.md`、`adr/adr-016-download-via-converter.md`。
//
// 啟用判定(由 Enabled() 給 main.go 用Phase 0.8b v0.6 起2 個必要欄位
// ConverterBaseURL / ConverterAPIKey**全部非空**才視為啟用;任一缺即視為未啟用,
// 5 個 /api/conversion/* endpoint 不會 wiremain.go 在 wire 階段跳過、log warn
//
// **Phase 0.8b 變更**:服務間認證從 OAuth client_credentials 改為 pre-shared API keyADR-015
//
// **Phase 0.8b T5 完成**(見 conversion.md §3.2 / ADR-015 §5 §7原暫留欄位
// TenantID / DelegatedTTLSeconds 已移除 — MC 認證鏈與 delegated download token 機制
// 都不存在了,兩個欄位連同對應 envVISIONA_OIDC_TENANT_ID /
// VISIONA_FAA_DELEGATED_TTL_SECONDS一併清除。
//
// **Phase 0.8b v0.6 T4 完成**(見 ADR-016 §2 / conversion.md v0.6.1 §3.1):原 FAA 相關
// 欄位 FAABaseURL / FAAAPIKey 已移除 — visionA 端不再直接呼叫 FAAdownload / promote
// 流程改走 converter.GetResultADR-016 撤回 v0.5 設計缺口),兩個欄位連同對應 env
// VISIONA_FAA_BASE_URL / VISIONA_FAA_API_KEY一併清除`Enabled()` 也簡化為只判
// converter 兩欄位。
type ConversionConfig struct {
// ConverterBaseURL 是 kneron_model_converter task-scheduler 服務的 base URL。
// 例http://192.168.0.130:9501dev / stage / https://converter.visiona.cloudprod
// 對齊 VISIONA_CONVERTER_BASE_URL留空 = 不啟用 Phase 0.8 轉檔功能。
ConverterBaseURL string
// ConverterAPIKey 是 visionA → converter 服務間認證的 pre-shared API keyPhase 0.8b 新增)。
// 對齊 VISIONA_CONVERTER_API_KEY以 `Authorization: Bearer <key>` 形式帶上。
// 雙方獨立產生(`openssl rand -hex 32`visionA 端的值必須與 converter 端的
// `CONVERTER_API_KEY` env 對齊;不對齊 → 下游 401visionA 端不重試,回 502 converter_auth_failed
// 對應 ADR-015 §3。
//
// 安全log 永遠不印此值全文(可印 `api_key_set=true/false` 或前 8 字元 prefix
// 部署用 AWS Secrets Manager / Vault嚴格分環境dev / stage / prod 各自獨立 key
ConverterAPIKey string
// MaxModelSizeMB 是 visionA-backend 端對上傳模型檔的大小上限MB
// 與 converter 端 limit 對齊converter 預設 500 MB
// 對齊 VISIONA_CONVERTER_MAX_MODEL_SIZE_MB預設 500。
MaxModelSizeMB int
}
// Enabled 回傳 Phase 0.8 / 0.8b conversion 是否啟用。
//
// **Phase 0.8b v0.6 T4 簡化**ADR-016 §2 / conversion.md v0.6.1 §3.1visionA 端撤回
// 對 FAA 的直接呼叫download / promote 改走 converter.GetResult只剩 ConverterBaseURL
// 與 ConverterAPIKey 兩個欄位需非空。任一缺 → 視為未啟用main.go 不會 wire
// conversion.Service5 個 endpoint 回 501 / 不註冊)。
func (c ConversionConfig) Enabled() bool {
return c.ConverterBaseURL != "" && c.ConverterAPIKey != ""
}
// CORSConfig 控制 api-server 對瀏覽器的 CORS 白名單。
//
// AllowedOrigins 為逗號分隔字串解析後的 slice
// 空時 api.Deps.validate() 會 fallback 到 http://localhost:3000前端 dev server
type CORSConfig struct {
AllowedOrigins []string // VISIONA_CORS_ALLOWED_ORIGINS逗號分隔
}
// Validate 在 Load() 之後檢查交叉依賴與必填欄位。
//
// OB5 起 OIDC 是唯一認證路徑,所有 OIDC 必填欄位永遠都要非空:
// - IssuerURL / ClientID / RedirectURL / PostLoginURL
// - UserSession.Secretcookie HMAC 簽章)
//
// ClientSecret 為**選填**A1, 2026-05-01
// - 有值 → confidential client mode標準 OAuth + PKCE 雙保險)
// - 留空 → PKCE-only public client mode依靠 PKCE 防 code interception
//
// 兩種 mode 由 IdP 決定visionA 都支援(見 ADR-013、oidc-tdd.md §13.1)。
//
// ServiceClientID / ServiceClientSecret 為 client_credentials grant 預留欄位,
// A1 階段不啟用、不檢查;之後若接服務間 API 呼叫再補 Validate。
//
// 缺任何**必填**項 → 回 *MissingEnvErrormain.go 啟動時 fatal log 退出。
// 維持單一 error 而非列表 — caller 只是 fail-fast 紀錄,不需要結構化處理。
func (c *Config) Validate() error {
missing := make([]string, 0, 5)
if c.OIDC.IssuerURL == "" {
missing = append(missing, "VISIONA_OIDC_ISSUER_URL")
}
if c.OIDC.ClientID == "" {
missing = append(missing, "VISIONA_OIDC_CLIENT_ID")
}
// ClientSecret 為選填public PKCE-only client 留空)— 不檢查。
if c.OIDC.RedirectURL == "" {
missing = append(missing, "VISIONA_OIDC_REDIRECT_URL")
}
if c.OIDC.PostLoginURL == "" {
missing = append(missing, "VISIONA_FRONTEND_URL")
}
if c.UserSession.Secret == "" {
missing = append(missing, "VISIONA_SESSION_SECRET")
}
if len(missing) > 0 {
return &MissingEnvError{Vars: missing}
}
return nil
}
// MissingEnvError 表示 OIDC 必填環境變數缺少OB5 起永遠檢查)。
type MissingEnvError struct {
Vars []string
}
func (e *MissingEnvError) Error() string {
return "config: OIDC enabled but required env vars are missing: " + joinStrings(e.Vars, ", ")
}
// joinStrings 是 strings.Join 的本地版本,避免單純為了 join 引入 strings package。
func joinStrings(parts []string, sep string) string {
switch len(parts) {
case 0:
return ""
case 1:
return parts[0]
}
n := len(sep) * (len(parts) - 1)
for _, p := range parts {
n += len(p)
}
out := make([]byte, 0, n)
out = append(out, parts[0]...)
for _, p := range parts[1:] {
out = append(out, sep...)
out = append(out, p...)
}
return string(out)
}