HTTP 用 Gin,配置用 Viper,日志用 zap,数据用 sqlx;保留原有接口与会商行为。 顺带补上就绪探活、请求号、滚动日志、systemd 与 Dockerfile。 Co-authored-by: Cursor <cursoragent@cursor.com>
227 lines
7.5 KiB
Go
227 lines
7.5 KiB
Go
// Package config 从环境变量装载运行配置。
|
||
//
|
||
// 为什么用 Viper:Go 服务里读配置的常见入口,以后若要加配置文件不用再改调用方。
|
||
// 约定:当前只认环境变量,键名与 Java / 旧版 Go 完全一致(EBA_*、ANALYSIS_*、LOG_*、SERVER_PORT)。
|
||
// 边界:空字符串当成没配,回落到联调缺省;非法数字同样回落,不在这里把进程打崩(致命项交给 Validate)。
|
||
package config
|
||
|
||
import (
|
||
"fmt"
|
||
"strings"
|
||
|
||
"github.com/spf13/viper"
|
||
)
|
||
|
||
// Config 运行配置(环境变量优先,与 Java EBA_* / analysis.* 对齐)。
|
||
type Config struct {
|
||
Port int
|
||
|
||
// DBDSN MySQL 连接串。优先 EBA_DB_DSN;否则用 HOST/PORT/NAME/USER/PASSWORD 拼,或解析 JDBC URL。
|
||
DBDSN string
|
||
|
||
AnalysisMode string // live | demo。demo 时强制走假进度,不再打 DeerFlow。
|
||
AuthEnabled bool // false 时写操作不强制账号头,仅联调。
|
||
DemoEnabled bool // 真模型与假进度二选一;live 下只有显式打开才为 true。
|
||
LivePollMS int
|
||
DemoTickMS int
|
||
LiveMaxInflight int // 真模型并发上限。小于 1 时 Load 会抬回 2。
|
||
|
||
DeerFlowBaseURL string
|
||
DeerFlowUser string
|
||
DeerFlowPassword string
|
||
DeerFlowCoordinator string
|
||
DeerFlowStreamIdleTimeout int // 毫秒。流式读空闲超过这个值视为卡住。
|
||
|
||
LogLevel string
|
||
LogFile string
|
||
LogMaxMB int
|
||
LogMaxAgeDays int
|
||
LogMaxBackups int
|
||
|
||
// SchemaPath 建表脚本路径;空则尝试 ./schema.sql。
|
||
SchemaPath string
|
||
|
||
// StrictConfig true 时:live 使用联调默认/空 DeerFlow 口令直接拒绝启动。
|
||
StrictConfig bool
|
||
|
||
// Version 构建信息(也可用环境变量 EBA_VERSION;ldflags 可再覆盖二进制)。
|
||
Version string
|
||
}
|
||
|
||
// Load 从环境变量加载;缺省贴近本机联调。
|
||
//
|
||
// ANALYSIS_MODE=demo 时无视 worker 开关,一律假进度。
|
||
// ANALYSIS_MODE=live 且没有显式设置 EBA_WORKER_DEMO_ENABLED 时,强制关掉 demo,
|
||
// 避免壳环境里残留的默认值把真模型服务悄悄变成演示。
|
||
func Load() Config {
|
||
v := newEnv()
|
||
c := Config{
|
||
Port: vInt(v, "SERVER_PORT", 18097),
|
||
AnalysisMode: strings.ToLower(vString(v, "ANALYSIS_MODE", "live")),
|
||
AuthEnabled: vBool(v, "ANALYSIS_AUTH_ENABLED", false),
|
||
DemoEnabled: vBool(v, "EBA_WORKER_DEMO_ENABLED", false),
|
||
LivePollMS: vInt(v, "EBA_WORKER_LIVE_POLL_MS", 1000),
|
||
DemoTickMS: vInt(v, "EBA_WORKER_TICK_MS", 400),
|
||
LiveMaxInflight: vInt(v, "EBA_WORKER_LIVE_MAX", 2),
|
||
DeerFlowBaseURL: strings.TrimRight(vString(v, "EBA_DF_BASE_URL", "http://127.0.0.1:8001"), "/"),
|
||
DeerFlowUser: vString(v, "EBA_DF_USER", "admin"),
|
||
DeerFlowPassword: vString(v, "EBA_DF_PASSWORD", "123ewq"),
|
||
DeerFlowCoordinator: vString(v, "EBA_DF_COORDINATOR", "roundtable-coordinator"),
|
||
DeerFlowStreamIdleTimeout: vInt(v, "EBA_DF_STREAM_IDLE_TIMEOUT_MS", 180000),
|
||
LogLevel: vString(v, "LOG_LEVEL", "info"),
|
||
LogFile: vString(v, "LOG_FILE", ""),
|
||
LogMaxMB: vInt(v, "LOG_MAX_MB", 100),
|
||
LogMaxAgeDays: vInt(v, "LOG_MAX_AGE_DAYS", 14),
|
||
LogMaxBackups: vInt(v, "LOG_MAX_BACKUPS", 14),
|
||
SchemaPath: vString(v, "EBA_SCHEMA_PATH", "schema.sql"),
|
||
StrictConfig: vBool(v, "EBA_STRICT_CONFIG", false),
|
||
Version: vString(v, "EBA_VERSION", "dev"),
|
||
}
|
||
c.DBDSN = resolveDSN(v)
|
||
if c.AnalysisMode == "demo" {
|
||
c.DemoEnabled = true
|
||
}
|
||
if c.AnalysisMode == "live" && !v.IsSet("EBA_WORKER_DEMO_ENABLED") {
|
||
c.DemoEnabled = false
|
||
}
|
||
if c.LiveMaxInflight < 1 {
|
||
c.LiveMaxInflight = 2
|
||
}
|
||
return c
|
||
}
|
||
|
||
// Validate 启动前校验;返回警告列表。strict 时致命错误用 error 返回,调用方应退出进程。
|
||
func (c Config) Validate() (warnings []string, err error) {
|
||
switch c.AnalysisMode {
|
||
case "live", "demo":
|
||
default:
|
||
return nil, fmt.Errorf("ANALYSIS_MODE 只能是 live 或 demo,当前=%q", c.AnalysisMode)
|
||
}
|
||
if c.Port < 1 || c.Port > 65535 {
|
||
return nil, fmt.Errorf("SERVER_PORT 非法: %d", c.Port)
|
||
}
|
||
if c.DBDSN == "" {
|
||
return nil, fmt.Errorf("数据库 DSN 为空,请配置 EBA_DB_DSN 或 EBA_DB_*")
|
||
}
|
||
|
||
weakDF := c.DeerFlowPassword == "123ewq" || c.DeerFlowPassword == ""
|
||
weakHint := "正式环境请改 EBA_DF_PASSWORD,勿用联调默认口令"
|
||
|
||
// live 声明下就要管 DF 口令(即使误开了 demo 开关也不放过 strict)。
|
||
if c.AnalysisMode == "live" {
|
||
if !c.DemoEnabled && c.DeerFlowBaseURL == "" {
|
||
return nil, fmt.Errorf("live 模式必须配置 EBA_DF_BASE_URL")
|
||
}
|
||
if weakDF {
|
||
msg := "DeerFlow 仍使用联调默认/空密码:" + weakHint
|
||
if c.StrictConfig {
|
||
return nil, fmt.Errorf("%s(EBA_STRICT_CONFIG=true)", msg)
|
||
}
|
||
warnings = append(warnings, msg)
|
||
}
|
||
}
|
||
if !c.AuthEnabled {
|
||
warnings = append(warnings, "ANALYSIS_AUTH_ENABLED=false:写操作不强制账号头(仅联调)")
|
||
}
|
||
return warnings, nil
|
||
}
|
||
|
||
// newEnv 只绑定进程环境变量,不读配置文件,避免工作目录里偶然的 yaml 盖掉部署注入的 EBA_*。
|
||
func newEnv() *viper.Viper {
|
||
v := viper.New()
|
||
v.AutomaticEnv()
|
||
return v
|
||
}
|
||
|
||
func resolveDSN(v *viper.Viper) string {
|
||
if d := strings.TrimSpace(v.GetString("EBA_DB_DSN")); v.IsSet("EBA_DB_DSN") && d != "" {
|
||
return ensureMultiStatements(d)
|
||
}
|
||
user := vString(v, "EBA_DB_USER", "eba")
|
||
pass := vString(v, "EBA_DB_PASSWORD", "eba_local_dev")
|
||
jdbc := ""
|
||
if v.IsSet("EBA_DB_URL") {
|
||
jdbc = strings.TrimSpace(v.GetString("EBA_DB_URL"))
|
||
}
|
||
if jdbc != "" {
|
||
if dsn, ok := jdbcToDSN(jdbc, user, pass); ok {
|
||
return dsn
|
||
}
|
||
}
|
||
host := vString(v, "EBA_DB_HOST", "127.0.0.1")
|
||
port := vString(v, "EBA_DB_PORT", "3306")
|
||
name := vString(v, "EBA_DB_NAME", "eightbf_analysis")
|
||
return user + ":" + pass + "@tcp(" + host + ":" + port + ")/" + name +
|
||
"?parseTime=true&loc=Asia%2FShanghai&charset=utf8mb4&multiStatements=true"
|
||
}
|
||
|
||
func jdbcToDSN(jdbc, user, pass string) (string, bool) {
|
||
s := strings.TrimPrefix(jdbc, "jdbc:mysql://")
|
||
if s == jdbc {
|
||
return "", false
|
||
}
|
||
hostPath := s
|
||
if i := strings.Index(s, "?"); i >= 0 {
|
||
hostPath = s[:i]
|
||
}
|
||
host := hostPath
|
||
db := "eightbf_analysis"
|
||
if i := strings.Index(hostPath, "/"); i >= 0 {
|
||
host = hostPath[:i]
|
||
db = hostPath[i+1:]
|
||
}
|
||
params := "parseTime=true&loc=Asia%2FShanghai&charset=utf8mb4&multiStatements=true"
|
||
return user + ":" + pass + "@tcp(" + host + ")/" + db + "?" + params, true
|
||
}
|
||
|
||
func ensureMultiStatements(dsn string) string {
|
||
if strings.Contains(dsn, "multiStatements=") {
|
||
return dsn
|
||
}
|
||
if strings.Contains(dsn, "?") {
|
||
return dsn + "&multiStatements=true"
|
||
}
|
||
return dsn + "?multiStatements=true"
|
||
}
|
||
|
||
// vString 未设置或空白都用缺省。Viper 对空环境变量会返回空串,这里与旧版 os.Getenv 行为对齐。
|
||
func vString(v *viper.Viper, key, def string) string {
|
||
if !v.IsSet(key) {
|
||
return def
|
||
}
|
||
s := strings.TrimSpace(v.GetString(key))
|
||
if s == "" {
|
||
return def
|
||
}
|
||
return s
|
||
}
|
||
|
||
func vBool(v *viper.Viper, key string, def bool) bool {
|
||
if !v.IsSet(key) {
|
||
return def
|
||
}
|
||
switch strings.ToLower(strings.TrimSpace(v.GetString(key))) {
|
||
case "1", "true", "yes", "on":
|
||
return true
|
||
case "0", "false", "no", "off":
|
||
return false
|
||
default:
|
||
return def
|
||
}
|
||
}
|
||
|
||
func vInt(v *viper.Viper, key string, def int) int {
|
||
if !v.IsSet(key) {
|
||
return def
|
||
}
|
||
s := strings.TrimSpace(v.GetString(key))
|
||
if s == "" {
|
||
return def
|
||
}
|
||
var n int
|
||
if _, err := fmt.Sscan(s, &n); err != nil {
|
||
return def
|
||
}
|
||
return n
|
||
}
|