2026-05-01 20:56:25 +08:00
package config
import (
"fmt"
"os"
"strconv"
"strings"
)
type Config struct {
2026-05-06 10:45:51 +08:00
HTTP HTTPConfig
Postgres PostgresConfig
Webhook WebhookConfig
PlatformAdapters PlatformAdaptersConfig
Runtime RuntimeConfig
2026-05-04 07:38:10 +08:00
}
type RuntimeConfig struct {
Env string
2026-05-01 20:56:25 +08:00
}
type HTTPConfig struct {
Addr string
ReadHeaderTimeout int
ReadTimeout int
WriteTimeout int
IdleTimeout int
MaxHeaderBytes int
MaxBodyBytes int64
}
type PostgresConfig struct {
Enabled bool
DSN string
MigrationDir string
MaxOpenConns int
MaxIdleConns int
ConnMaxLifetime int
}
type WebhookConfig struct {
Secret string
TimestampHeader string
SignatureHeader string
MaxSkewSeconds int
}
2026-05-06 10:45:51 +08:00
type PlatformAdaptersConfig struct {
Enabled bool
Sub2API PlatformAdapterProfileConfig
NewAPI PlatformAdapterProfileConfig
}
type PlatformAdapterProfileConfig struct {
2026-05-06 10:54:08 +08:00
Enabled bool
IngressSecret string
CallbackBaseURL string
CallbackSecret string
CallbackTimeoutMS int
CallbackMaxRetries int
CallbackPollIntervalMS int
CallbackBatchSize int
CallbackRetrySchedule [ ] int
2026-05-06 10:45:51 +08:00
}
2026-05-01 20:56:25 +08:00
func Load ( ) ( * Config , error ) {
cfg := & Config {
HTTP : HTTPConfig {
Addr : getEnv ( "AI_CS_ADDR" , ":8080" ) ,
ReadHeaderTimeout : getEnvInt ( "AI_CS_READ_HEADER_TIMEOUT_SEC" , 5 ) ,
ReadTimeout : getEnvInt ( "AI_CS_READ_TIMEOUT_SEC" , 10 ) ,
WriteTimeout : getEnvInt ( "AI_CS_WRITE_TIMEOUT_SEC" , 15 ) ,
IdleTimeout : getEnvInt ( "AI_CS_IDLE_TIMEOUT_SEC" , 60 ) ,
MaxHeaderBytes : getEnvInt ( "AI_CS_MAX_HEADER_BYTES" , 1 << 20 ) ,
MaxBodyBytes : getEnvInt64 ( "AI_CS_MAX_BODY_BYTES" , 1 << 20 ) ,
} ,
Postgres : PostgresConfig {
Enabled : getEnvBool ( "AI_CS_POSTGRES_ENABLED" , false ) ,
DSN : getEnv ( "AI_CS_POSTGRES_DSN" , "" ) ,
MigrationDir : getEnv ( "AI_CS_POSTGRES_MIGRATION_DIR" , "db/migration" ) ,
MaxOpenConns : getEnvInt ( "AI_CS_POSTGRES_MAX_OPEN_CONNS" , 20 ) ,
MaxIdleConns : getEnvInt ( "AI_CS_POSTGRES_MAX_IDLE_CONNS" , 5 ) ,
ConnMaxLifetime : getEnvInt ( "AI_CS_POSTGRES_CONN_MAX_LIFETIME_SEC" , 300 ) ,
} ,
Webhook : WebhookConfig {
Secret : getEnv ( "AI_CS_WEBHOOK_SECRET" , "" ) ,
TimestampHeader : getEnv ( "AI_CS_WEBHOOK_TIMESTAMP_HEADER" , "X-CS-Timestamp" ) ,
SignatureHeader : getEnv ( "AI_CS_WEBHOOK_SIGNATURE_HEADER" , "X-CS-Signature" ) ,
MaxSkewSeconds : getEnvInt ( "AI_CS_WEBHOOK_MAX_SKEW_SECONDS" , 300 ) ,
} ,
2026-05-06 10:45:51 +08:00
PlatformAdapters : PlatformAdaptersConfig {
Enabled : getEnvBool ( "AI_CS_PLATFORM_ADAPTERS_ENABLED" , false ) ,
Sub2API : PlatformAdapterProfileConfig {
2026-05-06 10:54:08 +08:00
Enabled : getEnvBool ( "AI_CS_PLATFORM_SUB2API_ENABLED" , false ) ,
IngressSecret : getEnv ( "AI_CS_PLATFORM_SUB2API_INGRESS_SECRET" , "" ) ,
CallbackBaseURL : getEnv ( "AI_CS_PLATFORM_SUB2API_CALLBACK_BASE_URL" , "" ) ,
CallbackSecret : getEnv ( "AI_CS_PLATFORM_SUB2API_CALLBACK_SECRET" , "" ) ,
CallbackTimeoutMS : getEnvInt ( "AI_CS_PLATFORM_SUB2API_CALLBACK_TIMEOUT_MS" , 3000 ) ,
CallbackMaxRetries : getEnvInt ( "AI_CS_PLATFORM_SUB2API_CALLBACK_MAX_RETRIES" , 5 ) ,
CallbackPollIntervalMS : getEnvInt ( "AI_CS_PLATFORM_SUB2API_CALLBACK_POLL_INTERVAL_MS" , 5000 ) ,
CallbackBatchSize : getEnvInt ( "AI_CS_PLATFORM_SUB2API_CALLBACK_BATCH_SIZE" , 20 ) ,
CallbackRetrySchedule : getEnvIntList ( "AI_CS_PLATFORM_SUB2API_CALLBACK_RETRY_SCHEDULE_SEC" , [ ] int { 10 , 30 , 60 , 300 , 900 } ) ,
2026-05-06 10:45:51 +08:00
} ,
NewAPI : PlatformAdapterProfileConfig {
2026-05-06 10:54:08 +08:00
Enabled : getEnvBool ( "AI_CS_PLATFORM_NEWAPI_ENABLED" , false ) ,
IngressSecret : getEnv ( "AI_CS_PLATFORM_NEWAPI_INGRESS_SECRET" , "" ) ,
CallbackBaseURL : getEnv ( "AI_CS_PLATFORM_NEWAPI_CALLBACK_BASE_URL" , "" ) ,
CallbackSecret : getEnv ( "AI_CS_PLATFORM_NEWAPI_CALLBACK_SECRET" , "" ) ,
CallbackTimeoutMS : getEnvInt ( "AI_CS_PLATFORM_NEWAPI_CALLBACK_TIMEOUT_MS" , 3000 ) ,
CallbackMaxRetries : getEnvInt ( "AI_CS_PLATFORM_NEWAPI_CALLBACK_MAX_RETRIES" , 5 ) ,
CallbackPollIntervalMS : getEnvInt ( "AI_CS_PLATFORM_NEWAPI_CALLBACK_POLL_INTERVAL_MS" , 5000 ) ,
CallbackBatchSize : getEnvInt ( "AI_CS_PLATFORM_NEWAPI_CALLBACK_BATCH_SIZE" , 20 ) ,
CallbackRetrySchedule : getEnvIntList ( "AI_CS_PLATFORM_NEWAPI_CALLBACK_RETRY_SCHEDULE_SEC" , [ ] int { 10 , 30 , 60 , 300 , 900 } ) ,
2026-05-06 10:45:51 +08:00
} ,
} ,
2026-05-04 07:38:10 +08:00
Runtime : RuntimeConfig {
Env : normalizeRuntimeEnv ( getEnv ( "AI_CS_RUNTIME_ENV" , getEnv ( "AI_CS_ENV" , "development" ) ) ) ,
} ,
2026-05-01 20:56:25 +08:00
}
if strings . TrimSpace ( cfg . HTTP . Addr ) == "" {
return nil , fmt . Errorf ( "AI_CS_ADDR must not be empty" )
}
if cfg . HTTP . MaxBodyBytes <= 0 {
return nil , fmt . Errorf ( "AI_CS_MAX_BODY_BYTES must be positive" )
}
if cfg . Postgres . Enabled && strings . TrimSpace ( cfg . Postgres . DSN ) == "" {
return nil , fmt . Errorf ( "AI_CS_POSTGRES_DSN must not be empty when postgres is enabled" )
}
if cfg . Webhook . MaxSkewSeconds <= 0 {
return nil , fmt . Errorf ( "AI_CS_WEBHOOK_MAX_SKEW_SECONDS must be positive" )
}
2026-05-06 10:45:51 +08:00
if err := validatePlatformProfile ( "sub2api" , cfg . PlatformAdapters . Enabled , cfg . PlatformAdapters . Sub2API ) ; err != nil {
return nil , err
}
if err := validatePlatformProfile ( "newapi" , cfg . PlatformAdapters . Enabled , cfg . PlatformAdapters . NewAPI ) ; err != nil {
return nil , err
}
2026-05-04 07:38:10 +08:00
if cfg . Runtime . Env != "production" && cfg . Runtime . Env != "development" && cfg . Runtime . Env != "test" {
return nil , fmt . Errorf ( "AI_CS_RUNTIME_ENV must be one of production/development/test, got: %s" , cfg . Runtime . Env )
}
if cfg . Runtime . Env == "production" && ! cfg . Postgres . Enabled {
return nil , fmt . Errorf ( "AI_CS_RUNTIME_ENV=production requires AI_CS_POSTGRES_ENABLED=true, but it is false (memory fallback is not allowed in production)" )
}
if cfg . Runtime . Env == "production" && strings . TrimSpace ( cfg . Webhook . Secret ) == "" {
return nil , fmt . Errorf ( "AI_CS_WEBHOOK_SECRET must not be empty in production" )
}
2026-05-01 20:56:25 +08:00
return cfg , nil
}
2026-05-06 10:45:51 +08:00
func validatePlatformProfile ( platform string , adaptersEnabled bool , profile PlatformAdapterProfileConfig ) error {
if ! adaptersEnabled || ! profile . Enabled {
return nil
}
upperPlatform := strings . ToUpper ( platform )
if strings . TrimSpace ( profile . IngressSecret ) == "" {
return fmt . Errorf ( "AI_CS_PLATFORM_%s_INGRESS_SECRET must not be empty when platform ingress is enabled" , upperPlatform )
}
if profile . CallbackTimeoutMS <= 0 {
return fmt . Errorf ( "AI_CS_PLATFORM_%s_CALLBACK_TIMEOUT_MS must be positive" , upperPlatform )
}
if profile . CallbackMaxRetries < 0 {
return fmt . Errorf ( "AI_CS_PLATFORM_%s_CALLBACK_MAX_RETRIES must not be negative" , upperPlatform )
}
2026-05-06 10:54:08 +08:00
if profile . CallbackPollIntervalMS <= 0 {
return fmt . Errorf ( "AI_CS_PLATFORM_%s_CALLBACK_POLL_INTERVAL_MS must be positive" , upperPlatform )
}
if profile . CallbackBatchSize <= 0 {
return fmt . Errorf ( "AI_CS_PLATFORM_%s_CALLBACK_BATCH_SIZE must be positive" , upperPlatform )
}
if len ( profile . CallbackRetrySchedule ) == 0 {
return fmt . Errorf ( "AI_CS_PLATFORM_%s_CALLBACK_RETRY_SCHEDULE_SEC must not be empty" , upperPlatform )
}
for _ , seconds := range profile . CallbackRetrySchedule {
if seconds <= 0 {
return fmt . Errorf ( "AI_CS_PLATFORM_%s_CALLBACK_RETRY_SCHEDULE_SEC must contain only positive integers" , upperPlatform )
}
}
2026-05-06 10:45:51 +08:00
return nil
}
2026-05-04 07:38:10 +08:00
func normalizeRuntimeEnv ( value string ) string {
switch strings . TrimSpace ( strings . ToLower ( value ) ) {
case "" , "dev" , "development" :
return "development"
case "prod" , "production" :
return "production"
case "test" :
return "test"
default :
return strings . TrimSpace ( strings . ToLower ( value ) )
}
}
2026-05-01 20:56:25 +08:00
func getEnv ( key , fallback string ) string {
if value := strings . TrimSpace ( os . Getenv ( key ) ) ; value != "" {
return value
}
return fallback
}
func getEnvInt ( key string , fallback int ) int {
value := strings . TrimSpace ( os . Getenv ( key ) )
if value == "" {
return fallback
}
parsed , err := strconv . Atoi ( value )
if err != nil {
return fallback
}
return parsed
}
func getEnvInt64 ( key string , fallback int64 ) int64 {
value := strings . TrimSpace ( os . Getenv ( key ) )
if value == "" {
return fallback
}
parsed , err := strconv . ParseInt ( value , 10 , 64 )
if err != nil {
return fallback
}
return parsed
}
func getEnvBool ( key string , fallback bool ) bool {
value := strings . TrimSpace ( strings . ToLower ( os . Getenv ( key ) ) )
if value == "" {
return fallback
}
switch value {
case "1" , "true" , "yes" , "on" :
return true
case "0" , "false" , "no" , "off" :
return false
default :
return fallback
}
}
2026-05-06 10:54:08 +08:00
func getEnvIntList ( key string , fallback [ ] int ) [ ] int {
value := strings . TrimSpace ( os . Getenv ( key ) )
if value == "" {
return append ( [ ] int ( nil ) , fallback ... )
}
parts := strings . Split ( value , "," )
result := make ( [ ] int , 0 , len ( parts ) )
for _ , part := range parts {
parsed , err := strconv . Atoi ( strings . TrimSpace ( part ) )
if err != nil {
return append ( [ ] int ( nil ) , fallback ... )
}
result = append ( result , parsed )
}
return result
}