package config import ( "crypto/md5" "encoding/hex" "fmt" "github.com/spf13/viper" ) type AppConfig struct { AppEnv string AppPort string LogLevel string RedisAddr string MySQLDSN string PostgresDSN string MongoDSN string MinIoEndpoint string MinioAccessKey string MinioSecretKey string MinioBucketName string AsynqmonPort string TotpViewPassword string TotpViewHost string TotpViewPort string SimCardDingTalkWebhook string SimCardDingTalkSecret string } type FwhApiConfig struct { FwhEnergy string FwhOss string FwhUser string FwhPassword string FwhTotpSecret string } type FwhMqttConfig struct { FwhMqttBroker string FwhMqttCaUrl string FwhMqttCertUrl string FwhMqttKeyUrl string } // FwhEsConfig Elasticsearch(iotcore-stat) 连接配置 type FwhEsConfig struct { // EsHost 形如 "alb-prod.franklinwh.click:5603"(不含 scheme) EsHost string // EsUser / EsPassword basic auth 凭证 EsUser string EsPassword string // EsIndexPrefix 索引前缀,形如 "iotcore-stat", // 实际索引名会拼接日期,如 "iotcore-stat-2026.09.07" EsIndexPrefix string // EsInsecureSkipVerify 是否跳过 TLS 证书校验(对应 curl -k) EsInsecureSkipVerify bool } type PlaneApiConfig struct { Host string ApiKey string WorkspaceSlug string ProjectId string LabelBugId string LabelFeatureId string } type PlaneWebApiConfig struct { Host string Email string Password string WorkspaceSlug string ProjectId string LabelBugId string LabelFeatureId string LabelFeedbackId string } // AgentConfig 新 AI 助手(AgentServer)配置,见 docs/ai-assistant-agentserver/technical-design.md §3.2 type AgentConfig struct { // Enabled 新 AI 助手总开关(AGENT_ENABLED) Enabled bool // ServerURL AgentServer 地址,如 http://127.0.0.1:8787(AGENT_SERVER_URL) ServerURL string // ApiKey AgentServer 签发的平台 API Key,只在后端使用(AGENT_SERVER_API_KEY) ApiKey string // ProfileDefault 默认 Profile ID(AGENT_PROFILE_DEFAULT),为空时使用 API Key 的默认 Profile ProfileDefault string // ProfileByRole 角色到 Profile 的映射,格式 "role:prf_x,role2:prf_y",按顺序取第一个命中的(AGENT_PROFILE_BY_ROLE) ProfileByRole string // AllowedRoles 允许使用新助手的角色,逗号分隔;为空表示所有登录用户(AGENT_ALLOWED_ROLES) AllowedRoles string } type Config struct { App AppConfig FwhApi FwhApiConfig FwhTestApi FwhApiConfig FwhMqtt FwhMqttConfig FwhTestMqtt FwhMqttConfig FwhEs FwhEsConfig PlaneApi PlaneApiConfig PlaneWebApi PlaneWebApiConfig Agent AgentConfig } // 需先执行LoadConfig后才能读取ENV // var APP_ENV string = "dev" // var FWH_ENERGY string = "energy.franklinwh.com" // var FWH_OSS string = "oss.franklinwh.com" // var FWH_USER string = "" // var FWH_PASSWORD string = "" // The production API receives its MD5 digest. // LoadConfig 读取配置文件和环境变量。 // 可选参数 configPaths 指定候选配置文件路径(按顺序尝试,命中第一个存在的即停)。 // 不传时使用默认路径 ["./.env", "./config.env"](后端主服务用)。 // MCP 等独立进程可传入自己的配置文件,例如 LoadConfig("./config.agent-mcp.env")。 func LoadConfig(configPaths ...string) (*Config, error) { v := viper.New() // 默认值(避免缺省时崩溃) // APP v.SetDefault("APP_ENV", "dev") v.SetDefault("APP_PORT", "8080") v.SetDefault("REDIS_ADDR", "localhost:6379") v.SetDefault("MYSQL_DSN", "") v.SetDefault("POSTGRES_DSN", "host=localhost user=fwh_toolbox password=fwh_toolbox dbname=fwh_toolbox_db port=5432 sslmode=disable") v.SetDefault("MONGO_DSN", "mongodb://localhost:27017/FwhToolboxDbDev") v.SetDefault("MINIO_ENDPOINT", "localhost:9198") v.SetDefault("MINIO_ACCESS_KEY", "fwh-toolbox") v.SetDefault("MINIO_SECRET_KEY", "fwh-toolbox") v.SetDefault("MINIO_BUCKET_NAME", "fwh-toolbox") v.SetDefault("ASYNQMON_PORT", "8081") v.SetDefault("FWH_TOTP_VIEW_PORT", "8303") v.SetDefault("FWH_TOTP_VIEW_HOST", "127.0.0.1") // FWH API v.SetDefault("FWH_ENERGY", "energy.franklinwh.com") v.SetDefault("FWH_OSS", "oss.franklinwh.com") // FWH MQTT // FWH ES(iotcore-stat) v.SetDefault("FWH_ES_INDEX_PREFIX", "iotcore-stat") v.SetDefault("FWH_ES_INSECURE_SKIP_VERIFY", true) // 新 AI 助手(AgentServer) v.SetDefault("AGENT_ENABLED", false) v.SetDefault("AGENT_SERVER_URL", "http://127.0.0.1:8787") // 多文件路径支持:优先使用调用方传入的路径,否则回退默认 paths := configPaths if len(paths) == 0 { paths = []string{"./.env", "./config.env"} } var readErr error for _, path := range paths { v.SetConfigFile(path) if err := v.ReadInConfig(); err == nil { fmt.Printf("✅ Using config file: %s\n", path) readErr = nil break } else { readErr = err } } // 环境变量覆盖文件配置 v.AutomaticEnv() // 如果两个文件都没读到,也没关系,直接用默认值 if readErr != nil { fmt.Println("⚠️ No config file found, using default values & environment variables") } // APP_ENV = v.GetString("APP_ENV") // FWH_ENERGY = v.GetString("FWH_ENERGY") // FWH_OSS = v.GetString("FWH_OSS") // FWH_USER = v.GetString("FWH_USER") // FWH_PASSWORD = v.GetString("FWH_PASSWORD") h := md5.New() h.Write([]byte(v.GetString("FWH_PASSWORD"))) fwhApiPassword := hex.EncodeToString(h.Sum(nil)) h = md5.New() h.Write([]byte(v.GetString("FWH_TEST_PASSWORD"))) fwhTestApiPassword := hex.EncodeToString(h.Sum(nil)) return &Config{ App: AppConfig{ AppEnv: v.GetString("APP_ENV"), AppPort: v.GetString("APP_PORT"), LogLevel: v.GetString("LOG_LEVEL"), RedisAddr: v.GetString("REDIS_ADDR"), MySQLDSN: v.GetString("MYSQL_DSN"), PostgresDSN: v.GetString("POSTGRES_DSN"), MongoDSN: v.GetString("MONGO_DSN"), MinIoEndpoint: v.GetString("MINIO_ENDPOINT"), MinioAccessKey: v.GetString("MINIO_ACCESS_KEY"), MinioSecretKey: v.GetString("MINIO_SECRET_KEY"), MinioBucketName: v.GetString("MINIO_BUCKET_NAME"), AsynqmonPort: v.GetString("ASYNQMON_PORT"), TotpViewPassword: v.GetString("FWH_TOTP_VIEW_PASSWORD"), TotpViewHost: v.GetString("FWH_TOTP_VIEW_HOST"), TotpViewPort: v.GetString("FWH_TOTP_VIEW_PORT"), SimCardDingTalkWebhook: v.GetString("SIM_CARD_DINGTALK_WEBHOOK"), SimCardDingTalkSecret: v.GetString("SIM_CARD_DINGTALK_SECRET"), }, FwhApi: FwhApiConfig{ FwhEnergy: v.GetString("FWH_ENERGY"), FwhOss: v.GetString("FWH_OSS"), FwhUser: v.GetString("FWH_USER"), FwhPassword: fwhApiPassword, FwhTotpSecret: v.GetString("FWH_TOTP_SECRET"), }, FwhTestApi: FwhApiConfig{ FwhEnergy: v.GetString("FWH_TEST_ENERGY"), FwhOss: v.GetString("FWH_TEST_OSS"), FwhUser: v.GetString("FWH_TEST_USER"), FwhPassword: fwhTestApiPassword, }, FwhMqtt: FwhMqttConfig{ FwhMqttBroker: v.GetString("FWH_MQTT_BROKER"), FwhMqttCaUrl: v.GetString("FWH_MQTT_CA"), FwhMqttCertUrl: v.GetString("FWH_MQTT_CERT"), FwhMqttKeyUrl: v.GetString("FWH_MQTT_KEY"), }, FwhTestMqtt: FwhMqttConfig{ FwhMqttBroker: v.GetString("FWH_MQTT_TEST_BROKER"), FwhMqttCaUrl: v.GetString("FWH_MQTT_TEST_CA"), FwhMqttCertUrl: v.GetString("FWH_MQTT_TEST_CERT"), FwhMqttKeyUrl: v.GetString("FWH_MQTT_TEST_KEY"), }, FwhEs: FwhEsConfig{ EsHost: v.GetString("FWH_ES_HOST"), EsUser: v.GetString("FWH_ES_USER"), EsPassword: v.GetString("FWH_ES_PASSWORD"), EsIndexPrefix: v.GetString("FWH_ES_INDEX_PREFIX"), EsInsecureSkipVerify: v.GetBool("FWH_ES_INSECURE_SKIP_VERIFY"), }, PlaneApi: PlaneApiConfig{ Host: v.GetString("PLANE_HOST"), ApiKey: v.GetString("PLANE_API_KEY"), WorkspaceSlug: v.GetString("PLANE_WORKSPACE_SLUG"), ProjectId: v.GetString("PLANE_PROJECT_ID"), LabelBugId: v.GetString("PLANE_LABEL_BUG"), LabelFeatureId: v.GetString("PLANE_LABEL_FEATURE"), }, PlaneWebApi: PlaneWebApiConfig{ Host: v.GetString("PLANE_WEB_HOST"), Email: v.GetString("PLANE_WEB_EMAIL"), Password: v.GetString("PLANE_WEB_PASSWORD"), WorkspaceSlug: v.GetString("PLANE_WEB_WORKSPACE_SLUG"), ProjectId: v.GetString("PLANE_WEB_PROJECT_ID"), LabelBugId: v.GetString("PLANE_WEB_LABEL_BUG"), LabelFeatureId: v.GetString("PLANE_WEB_LABEL_FEATURE"), LabelFeedbackId: v.GetString("PLANE_WEB_LABEL_FEEDBACK"), }, Agent: AgentConfig{ Enabled: v.GetBool("AGENT_ENABLED"), ServerURL: v.GetString("AGENT_SERVER_URL"), ApiKey: v.GetString("AGENT_SERVER_API_KEY"), ProfileDefault: v.GetString("AGENT_PROFILE_DEFAULT"), ProfileByRole: v.GetString("AGENT_PROFILE_BY_ROLE"), AllowedRoles: v.GetString("AGENT_ALLOWED_ROLES"), }, }, nil }