589 lines
22 KiB
Go
589 lines
22 KiB
Go
package config
|
||
|
||
import (
|
||
"agent-desk/internal/pkg/enums"
|
||
"errors"
|
||
"fmt"
|
||
"os"
|
||
"path/filepath"
|
||
"strings"
|
||
"time"
|
||
|
||
"github.com/spf13/viper"
|
||
"github.com/subosito/gotenv"
|
||
)
|
||
|
||
type Config struct {
|
||
Language string `yaml:"language"`
|
||
Server ServerConfig `yaml:"server"`
|
||
DB DBConfig `yaml:"db"`
|
||
Logger LoggerConfig `yaml:"logger"`
|
||
Auth AuthConfig `yaml:"auth"`
|
||
Storage StorageConfig `yaml:"storage"`
|
||
VectorDB VectorDBConfig `yaml:"vectorDB"`
|
||
MCP MCPConfig `yaml:"mcp"`
|
||
OIDC OIDCConfig `yaml:"oidc"`
|
||
CustomerSession CustomerSessionConfig `yaml:"customerSession"`
|
||
Identity IdentityConfig `yaml:"identity"`
|
||
Conversation ConversationConfig `yaml:"conversation"`
|
||
Webhook WebhookConfig `yaml:"webhook"`
|
||
Discord DiscordConfig `yaml:"discord"`
|
||
Email EmailConfig `yaml:"email"`
|
||
Slack SlackConfig `yaml:"slack"`
|
||
}
|
||
|
||
func (c Config) LanguageOrDefault() string {
|
||
switch strings.ToLower(strings.TrimSpace(c.Language)) {
|
||
case "zh", "zh-cn", "zh_cn", "zh-hans":
|
||
return "zh-CN"
|
||
case "en", "en-us", "en_us":
|
||
return "en-US"
|
||
case "ru", "ru-ru", "ru_ru":
|
||
return "ru-RU"
|
||
default:
|
||
return "en-US"
|
||
}
|
||
}
|
||
|
||
type ServerConfig struct {
|
||
Port int `yaml:"port"`
|
||
CompanyName string `yaml:"companyName"`
|
||
CompanyLogoURL string `yaml:"companyLogoUrl"`
|
||
CORS CORSConfig `yaml:"cors"`
|
||
// TrustedProxies are the CIDR blocks of the reverse proxies that sit in front
|
||
// of the application. Gin's own default is 0.0.0.0/0 and ::/0, which trusts
|
||
// every peer and makes ClientIP() return the leftmost X-Forwarded-For value -
|
||
// a header any caller can set.
|
||
TrustedProxies []string `yaml:"trustedProxies"`
|
||
// TrustedPlatform names an edge that overwrites rather than appends the real
|
||
// client address, for example "cloudflare". When set it takes precedence over
|
||
// X-Forwarded-For entirely.
|
||
TrustedPlatform string `yaml:"trustedPlatform"`
|
||
RateLimit RateLimitConfig `yaml:"rateLimit"`
|
||
}
|
||
|
||
// defaultTrustedProxies covers loopback, RFC1918, IPv6 unique-local and
|
||
// link-local ranges. That is the shape of almost every real deployment - a
|
||
// sidecar tunnel, a compose network, a local nginx - and a client on the public
|
||
// internet cannot present one of these addresses as its direct peer, so
|
||
// X-Forwarded-For stays honest. When the app is exposed directly the peer is a
|
||
// public address, is not trusted, and Gin falls back to it.
|
||
var defaultTrustedProxies = []string{
|
||
"127.0.0.0/8",
|
||
"10.0.0.0/8",
|
||
"172.16.0.0/12",
|
||
"192.168.0.0/16",
|
||
"::1/128",
|
||
"fc00::/7",
|
||
"fe80::/10",
|
||
}
|
||
|
||
func (s ServerConfig) TrustedProxiesOrDefault() []string {
|
||
proxies := make([]string, 0, len(s.TrustedProxies))
|
||
for _, proxy := range s.TrustedProxies {
|
||
if proxy = strings.TrimSpace(proxy); proxy != "" {
|
||
proxies = append(proxies, proxy)
|
||
}
|
||
}
|
||
if len(proxies) == 0 {
|
||
return defaultTrustedProxies
|
||
}
|
||
return proxies
|
||
}
|
||
|
||
// TrustedPlatformHeader resolves the configured platform name to the header Gin
|
||
// should read the client address from. Recognised names map to Gin's own
|
||
// constants; any other non-empty value is passed through as a literal header
|
||
// name, which is what Gin's TrustedPlatform field expects.
|
||
func (s ServerConfig) TrustedPlatformHeader() string {
|
||
platform := strings.TrimSpace(s.TrustedPlatform)
|
||
if platform == "" {
|
||
return ""
|
||
}
|
||
switch strings.ToLower(platform) {
|
||
case "cloudflare", "cf":
|
||
return "CF-Connecting-IP"
|
||
case "fly.io", "flyio", "fly-io":
|
||
return "Fly-Client-IP"
|
||
case "google-app-engine", "appengine", "gae":
|
||
return "X-Appengine-Remote-Addr"
|
||
default:
|
||
return platform
|
||
}
|
||
}
|
||
|
||
func (s ServerConfig) Address() string {
|
||
if s.Port <= 0 {
|
||
return ":8080"
|
||
}
|
||
return fmt.Sprintf(":%d", s.Port)
|
||
}
|
||
|
||
type CORSConfig struct {
|
||
// AllowedOrigins 是允许浏览器跨域访问的 Origin 白名单,必须包含协议和域名。
|
||
// 留空表示不允许跨域请求;同源请求通常不会携带 Origin,不受影响。
|
||
AllowedOrigins []string `yaml:"allowedOrigins"`
|
||
}
|
||
|
||
// RateLimitConfig bounds how often one client address may call the public,
|
||
// unauthenticated endpoints. It deliberately does not cover channel webhooks,
|
||
// websockets or authenticated dashboard routes: a platform that receives a 429
|
||
// from a webhook endpoint stops retrying and eventually disables the webhook.
|
||
type RateLimitConfig struct {
|
||
// Enabled defaults to true. Set it to false to switch the limits off without
|
||
// taking them out of the route table.
|
||
Enabled *bool `yaml:"enabled"`
|
||
// WindowSeconds is the length of the counting window. Zero or negative means
|
||
// one minute.
|
||
WindowSeconds int `yaml:"windowSeconds"`
|
||
}
|
||
|
||
func (r RateLimitConfig) IsEnabled() bool {
|
||
if r.Enabled == nil {
|
||
return true
|
||
}
|
||
return *r.Enabled
|
||
}
|
||
|
||
func (r RateLimitConfig) WindowSecondsOrDefault() int {
|
||
if r.WindowSeconds <= 0 {
|
||
return 60
|
||
}
|
||
return r.WindowSeconds
|
||
}
|
||
|
||
type DBConfig struct {
|
||
Type string `yaml:"type"`
|
||
DSN string `yaml:"dsn"`
|
||
MaxIdleConns int `yaml:"maxIdleConns"`
|
||
MaxOpenConns int `yaml:"maxOpenConns"`
|
||
ConnMaxIdleTimeSeconds int `yaml:"connMaxIdleTimeSeconds"`
|
||
ConnMaxLifetimeSeconds int `yaml:"connMaxLifetimeSeconds"`
|
||
}
|
||
|
||
type LoggerConfig struct {
|
||
Level string `yaml:"level"`
|
||
Format string `yaml:"format"`
|
||
AddSource bool `yaml:"addSource"`
|
||
}
|
||
|
||
type AuthConfig struct {
|
||
PasswordLoginEnabled *bool `yaml:"passwordLoginEnabled"`
|
||
TokenTTLHours int `yaml:"tokenTTLHours"`
|
||
MaxFailedAttempts int `yaml:"maxFailedAttempts"`
|
||
// MaxFailedAttemptsPerIP bounds failures from one client address across every
|
||
// username, which is what credential stuffing looks like. Zero or unset
|
||
// derives four times MaxFailedAttempts; it is disabled when MaxFailedAttempts
|
||
// is disabled.
|
||
MaxFailedAttemptsPerIP int `yaml:"maxFailedAttemptsPerIP"`
|
||
CredentialLockMinute int `yaml:"credentialLockMinute"`
|
||
}
|
||
|
||
// MaxFailedAttemptsPerIPOrDefault derives the per-address threshold from the
|
||
// per-account one so that a deployment which only tunes MaxFailedAttempts still
|
||
// gets a coherent pair of limits.
|
||
func (a AuthConfig) MaxFailedAttemptsPerIPOrDefault() int {
|
||
if a.MaxFailedAttemptsPerIP > 0 {
|
||
return a.MaxFailedAttemptsPerIP
|
||
}
|
||
if a.MaxFailedAttempts <= 0 {
|
||
return 0
|
||
}
|
||
return a.MaxFailedAttempts * 4
|
||
}
|
||
|
||
func (a AuthConfig) IsPasswordLoginEnabled() bool {
|
||
if a.PasswordLoginEnabled == nil {
|
||
return true
|
||
}
|
||
return *a.PasswordLoginEnabled
|
||
}
|
||
|
||
type CustomerSessionConfig struct {
|
||
Secret string `yaml:"secret"`
|
||
TTLMinutes int `yaml:"ttlMinutes"`
|
||
RefreshThresholdMinutes int `yaml:"refreshThresholdMinutes"`
|
||
}
|
||
|
||
// IdentityConfig holds the deployment-wide shared secret a host platform uses to
|
||
// sign the external visitor ID it passes to the widget.
|
||
type IdentityConfig struct {
|
||
// Secret is the HMAC-SHA256 key. Leave it empty to disable the feature: a
|
||
// visitor whose ID carries no valid signature is then always treated as an
|
||
// anonymous guest, which is the safe default.
|
||
Secret string `yaml:"secret"`
|
||
// MaxAgeMinutes bounds how old a signed identity may be. It limits how long a
|
||
// captured signature stays replayable. Values <= 0 fall back to 24 hours.
|
||
MaxAgeMinutes int `yaml:"maxAgeMinutes"`
|
||
}
|
||
|
||
func (c IdentityConfig) MaxAge() time.Duration {
|
||
if c.MaxAgeMinutes <= 0 {
|
||
return 24 * time.Hour
|
||
}
|
||
return time.Duration(c.MaxAgeMinutes) * time.Minute
|
||
}
|
||
|
||
// ConversationConfig holds visitor-facing conversation limits.
|
||
type ConversationConfig struct {
|
||
// CustomerMaxOpen caps how many not-yet-closed conversations one visitor may
|
||
// have at a time. It exists to protect agents, not the visitor: every open
|
||
// conversation is real work, and in human-only mode opening one dispatches
|
||
// an agent immediately. Without a cap a single caller could flood the queue,
|
||
// and the visitor cannot undo that by closing a conversation.
|
||
//
|
||
// 0 or unset falls back to the default of 3. Use -1 to remove the cap
|
||
// entirely, which is an explicit opt-out rather than something reached by
|
||
// omission.
|
||
CustomerMaxOpen int `yaml:"customerMaxOpen"`
|
||
// EmailChatLiveMinutes is how long a chat still counts as being watched. An
|
||
// email that arrives while a chat is live is answered in the chat instead of
|
||
// starting an email thread, because the visitor is present there and absent
|
||
// from their inbox. Past this window the chat is treated as abandoned and the
|
||
// email becomes the conversation.
|
||
EmailChatLiveMinutes int `yaml:"emailChatLiveMinutes"`
|
||
}
|
||
|
||
func (c ConversationConfig) EmailChatLiveWindow() time.Duration {
|
||
if c.EmailChatLiveMinutes <= 0 {
|
||
return 30 * time.Minute
|
||
}
|
||
return time.Duration(c.EmailChatLiveMinutes) * time.Minute
|
||
}
|
||
|
||
func (c ConversationConfig) MaxOpen() int {
|
||
if c.CustomerMaxOpen < 0 {
|
||
return 0
|
||
}
|
||
if c.CustomerMaxOpen == 0 {
|
||
return 3
|
||
}
|
||
return c.CustomerMaxOpen
|
||
}
|
||
|
||
func (c CustomerSessionConfig) TTL() int {
|
||
if c.TTLMinutes <= 0 {
|
||
return 120
|
||
}
|
||
return c.TTLMinutes
|
||
}
|
||
|
||
func (c CustomerSessionConfig) RefreshThreshold() int {
|
||
if c.RefreshThresholdMinutes <= 0 {
|
||
return 30
|
||
}
|
||
return c.RefreshThresholdMinutes
|
||
}
|
||
|
||
type StorageConfig struct {
|
||
Default enums.AssetProvider `yaml:"default"`
|
||
MaxUploadSizeMB int64 `yaml:"maxUploadSizeMB"`
|
||
Local LocalStorageConfig `yaml:"local"`
|
||
OSS OSSStorageConfig `yaml:"oss"`
|
||
}
|
||
|
||
func (s StorageConfig) MaxUploadSizeBytes() int64 {
|
||
if s.MaxUploadSizeMB <= 0 {
|
||
return 5 << 20
|
||
}
|
||
return s.MaxUploadSizeMB << 20
|
||
}
|
||
|
||
func (s StorageConfig) MaxRequestBodySizeBytes() int64 {
|
||
limit := s.MaxUploadSizeBytes()
|
||
return limit + (1 << 20)
|
||
}
|
||
|
||
type LocalStorageConfig struct {
|
||
Root string `yaml:"root"`
|
||
BaseURL string `yaml:"baseUrl"`
|
||
}
|
||
|
||
type OSSStorageConfig struct {
|
||
Endpoint string `yaml:"endpoint"`
|
||
Bucket string `yaml:"bucket"`
|
||
AccessKeyID string `yaml:"accessKeyId"`
|
||
AccessKeySecret string `yaml:"accessKeySecret"`
|
||
BaseURL string `yaml:"baseUrl"`
|
||
Private bool `yaml:"private"`
|
||
SignedURLExpire int `yaml:"signedUrlExpireSeconds"`
|
||
}
|
||
|
||
type VectorDBConfig struct {
|
||
Type string `yaml:"type"`
|
||
Qdrant QdrantVectorDBConfig `yaml:"qdrant"`
|
||
LanceDB LanceDBVectorDBConfig `yaml:"lancedb"`
|
||
}
|
||
|
||
type QdrantVectorDBConfig struct {
|
||
Host string `yaml:"host"`
|
||
GrpcPort int `yaml:"grpcPort"`
|
||
APIKey string `yaml:"apiKey"`
|
||
UseTLS bool `yaml:"useTls"`
|
||
}
|
||
|
||
type LanceDBVectorDBConfig struct {
|
||
Path string `yaml:"path"`
|
||
}
|
||
|
||
type MCPConfig struct {
|
||
Enabled bool `yaml:"enabled"`
|
||
Servers map[string]MCPServerConfig `yaml:"servers"`
|
||
}
|
||
|
||
type MCPServerConfig struct {
|
||
Enabled bool `yaml:"enabled"`
|
||
Endpoint string `yaml:"endpoint"`
|
||
TimeoutMS int `yaml:"timeoutMs"`
|
||
Headers map[string]string `yaml:"headers"`
|
||
}
|
||
|
||
type OIDCConfig struct {
|
||
Enabled bool `yaml:"enabled"`
|
||
Issuer string `yaml:"issuer"`
|
||
ClientID string `yaml:"clientId"`
|
||
ClientSecret string `yaml:"clientSecret"`
|
||
RedirectURL string `yaml:"redirectUrl"`
|
||
StateSecret string `yaml:"stateSecret"`
|
||
Scopes []string `yaml:"scopes"`
|
||
}
|
||
|
||
type WebhookConfig struct {
|
||
OrgSyncSecret string `yaml:"orgSyncSecret"`
|
||
DOSOrgSyncSecret string `yaml:"dosOrgSyncSecret"`
|
||
}
|
||
|
||
// DiscordConfig holds deployment-wide Discord bot credentials. A channel may
|
||
// carry its own bot token, which takes precedence; these are the fallback for a
|
||
// single shared bot.
|
||
type DiscordConfig struct {
|
||
ClientID string `yaml:"clientId"`
|
||
ClientSecret string `yaml:"clientSecret"`
|
||
BotToken string `yaml:"botToken"`
|
||
PublicKey string `yaml:"publicKey"`
|
||
}
|
||
|
||
// EmailConfig holds the email delivery provider settings.
|
||
type EmailConfig struct {
|
||
Provider string `yaml:"provider"`
|
||
FromAddress string `yaml:"fromAddress"`
|
||
FromName string `yaml:"fromName"`
|
||
APIKey string `yaml:"apiKey"`
|
||
SMTPHost string `yaml:"smtpHost"`
|
||
SMTPPort int `yaml:"smtpPort"`
|
||
SMTPUser string `yaml:"smtpUser"`
|
||
SMTPPassword string `yaml:"smtpPassword"`
|
||
SMTPUseTLS bool `yaml:"smtpUseTls"`
|
||
// SMTPAllowInsecure accepts a self-signed certificate and skips opportunistic
|
||
// STARTTLS so a deployment can be pointed at a local test SMTP server. It
|
||
// weakens transport security and must stay off for real mailboxes.
|
||
SMTPAllowInsecure bool `yaml:"smtpAllowInsecure"`
|
||
InboundSecret string `yaml:"inboundSecret"`
|
||
}
|
||
|
||
// SlackConfig holds deployment-wide Slack app credentials. A channel may carry
|
||
// its own bot token and signing secret, which take precedence; these are the
|
||
// fallback for a single shared Slack app.
|
||
type SlackConfig struct {
|
||
ClientID string `yaml:"clientId"`
|
||
ClientSecret string `yaml:"clientSecret"`
|
||
BotToken string `yaml:"botToken"`
|
||
SigningSecret string `yaml:"signingSecret"`
|
||
}
|
||
|
||
// SlackApp resolves the Slack app credentials for one channel. A channel value
|
||
// wins over the deployment-wide one, so a single deployment can serve several
|
||
// workspaces while still having a default app.
|
||
func (c Config) SlackApp(channelBotToken, channelSigningSecret string) SlackConfig {
|
||
return SlackConfig{
|
||
ClientID: strings.TrimSpace(c.Slack.ClientID),
|
||
ClientSecret: strings.TrimSpace(c.Slack.ClientSecret),
|
||
BotToken: firstNonBlank(channelBotToken, c.Slack.BotToken),
|
||
SigningSecret: firstNonBlank(channelSigningSecret, c.Slack.SigningSecret),
|
||
}
|
||
}
|
||
|
||
func firstNonBlank(values ...string) string {
|
||
for _, value := range values {
|
||
if trimmed := strings.TrimSpace(value); trimmed != "" {
|
||
return trimmed
|
||
}
|
||
}
|
||
return ""
|
||
}
|
||
|
||
func Load(path string) (*Config, error) {
|
||
loadDotEnv(path)
|
||
|
||
v := viper.New()
|
||
bindConfigDefaults(v)
|
||
|
||
if strings.TrimSpace(path) != "" {
|
||
v.SetConfigFile(path)
|
||
v.SetConfigType("yaml")
|
||
}
|
||
|
||
v.SetEnvPrefix("AGENT_DESK")
|
||
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
|
||
v.AutomaticEnv()
|
||
bindEnvironmentAliases(v)
|
||
|
||
if strings.TrimSpace(path) != "" {
|
||
if err := v.ReadInConfig(); err != nil {
|
||
var configFileNotFoundError viper.ConfigFileNotFoundError
|
||
if !os.IsNotExist(err) && !errors.As(err, &configFileNotFoundError) {
|
||
return nil, err
|
||
}
|
||
}
|
||
}
|
||
|
||
cfg := &Config{}
|
||
if err := v.Unmarshal(cfg); err != nil {
|
||
return nil, err
|
||
}
|
||
normalizeLoadedConfig(cfg)
|
||
return cfg, nil
|
||
}
|
||
|
||
func loadDotEnv(configPath string) {
|
||
if envFile := os.Getenv("AGENT_DESK_ENV_FILE"); envFile != "" {
|
||
_ = gotenv.Load(envFile)
|
||
return
|
||
}
|
||
if envFile := os.Getenv("ENV_FILE"); envFile != "" {
|
||
_ = gotenv.Load(envFile)
|
||
return
|
||
}
|
||
_ = gotenv.Load(".env")
|
||
_ = gotenv.Load("../.env")
|
||
_ = gotenv.Load("../../.env")
|
||
if configPath != "" {
|
||
dir := filepath.Dir(configPath)
|
||
if dir != "." && dir != "" {
|
||
_ = gotenv.Load(filepath.Join(dir, ".env"))
|
||
_ = gotenv.Load(filepath.Join(dir, "..", ".env"))
|
||
}
|
||
}
|
||
}
|
||
|
||
func bindConfigDefaults(v *viper.Viper) {
|
||
v.SetDefault("language", "zh-CN")
|
||
v.SetDefault("server.port", 8083)
|
||
v.SetDefault("server.companyName", "")
|
||
v.SetDefault("server.companyLogoUrl", "")
|
||
v.SetDefault("server.cors.allowedOrigins", []string{})
|
||
v.SetDefault("server.trustedProxies", []string{})
|
||
v.SetDefault("server.trustedPlatform", "")
|
||
v.SetDefault("server.rateLimit.windowSeconds", 60)
|
||
v.SetDefault("db.type", "sqlite")
|
||
v.SetDefault("db.dsn", "file:./data/app.db?_busy_timeout=5000")
|
||
v.SetDefault("db.maxIdleConns", 5)
|
||
v.SetDefault("db.maxOpenConns", 20)
|
||
v.SetDefault("db.connMaxIdleTimeSeconds", 300)
|
||
v.SetDefault("db.connMaxLifetimeSeconds", 1800)
|
||
v.SetDefault("logger.level", "info")
|
||
v.SetDefault("logger.format", "text")
|
||
v.SetDefault("logger.addSource", false)
|
||
v.SetDefault("auth.tokenTTLHours", 12)
|
||
v.SetDefault("auth.maxFailedAttempts", 5)
|
||
v.SetDefault("auth.maxFailedAttemptsPerIP", 0)
|
||
v.SetDefault("auth.credentialLockMinute", 15)
|
||
v.SetDefault("customerSession.ttlMinutes", 120)
|
||
v.SetDefault("customerSession.refreshThresholdMinutes", 30)
|
||
// Intentionally no default: an empty identity secret disables signed visitor
|
||
// identities rather than silently trusting unsigned ones.
|
||
v.SetDefault("identity.secret", "")
|
||
v.SetDefault("identity.maxAgeMinutes", 1440)
|
||
v.SetDefault("storage.default", "local")
|
||
v.SetDefault("storage.maxUploadSizeMB", 20)
|
||
v.SetDefault("storage.local.root", "data/storage")
|
||
v.SetDefault("storage.local.baseUrl", "/storage")
|
||
v.SetDefault("vectorDB.type", "qdrant")
|
||
v.SetDefault("vectorDB.qdrant.host", "127.0.0.1")
|
||
v.SetDefault("vectorDB.qdrant.grpcPort", 6334)
|
||
v.SetDefault("mcp.enabled", true)
|
||
v.SetDefault("discord.clientId", "")
|
||
v.SetDefault("discord.clientSecret", "")
|
||
v.SetDefault("discord.botToken", "")
|
||
v.SetDefault("discord.publicKey", "")
|
||
v.SetDefault("slack.clientId", "")
|
||
v.SetDefault("slack.clientSecret", "")
|
||
v.SetDefault("slack.botToken", "")
|
||
v.SetDefault("slack.signingSecret", "")
|
||
v.SetDefault("email.provider", "smtp")
|
||
v.SetDefault("email.fromAddress", "")
|
||
v.SetDefault("email.fromName", "")
|
||
v.SetDefault("email.apiKey", "")
|
||
v.SetDefault("email.smtpHost", "")
|
||
v.SetDefault("email.smtpPort", 587)
|
||
v.SetDefault("email.smtpUser", "")
|
||
v.SetDefault("email.smtpPassword", "")
|
||
v.SetDefault("email.smtpUseTls", false)
|
||
v.SetDefault("email.inboundSecret", "")
|
||
}
|
||
|
||
func bindEnvironmentAliases(v *viper.Viper) {
|
||
// Prefixed AGENT_DESK_* aliases are listed first so that ambient legacy
|
||
// variables (PORT, DATABASE_URL, ...) cannot silently override the
|
||
// documented configuration.
|
||
_ = v.BindEnv("server.port", "AGENT_DESK_SERVER_PORT", "PORT", "SERVER_PORT")
|
||
_ = v.BindEnv("server.companyName", "AGENT_DESK_SERVER_COMPANYNAME", "COMPANY_NAME", "NEXT_PUBLIC_COMPANY_NAME", "BRAND_NAME", "BRAND_COMPANY_NAME")
|
||
_ = v.BindEnv("server.companyLogoUrl", "AGENT_DESK_SERVER_COMPANYLOGOURL", "COMPANY_LOGO_URL", "NEXT_PUBLIC_COMPANY_LOGO_URL", "BRAND_LOGO_URL")
|
||
_ = v.BindEnv("server.trustedProxies", "AGENT_DESK_SERVER_TRUSTEDPROXIES", "TRUSTED_PROXIES")
|
||
_ = v.BindEnv("server.trustedPlatform", "AGENT_DESK_SERVER_TRUSTEDPLATFORM", "TRUSTED_PLATFORM")
|
||
_ = v.BindEnv("server.rateLimit.enabled", "AGENT_DESK_SERVER_RATELIMIT_ENABLED", "RATE_LIMIT_ENABLED")
|
||
_ = v.BindEnv("server.rateLimit.windowSeconds", "AGENT_DESK_SERVER_RATELIMIT_WINDOWSECONDS", "RATE_LIMIT_WINDOW_SECONDS")
|
||
_ = v.BindEnv("db.type", "AGENT_DESK_DB_TYPE", "DATABASE_TYPE", "DB_TYPE")
|
||
_ = v.BindEnv("db.dsn", "AGENT_DESK_DB_DSN", "DATABASE_URL", "DB_DSN")
|
||
_ = v.BindEnv("auth.passwordLoginEnabled", "AGENT_DESK_AUTH_PASSWORDLOGINENABLED", "PASSWORD_LOGIN_ENABLED")
|
||
_ = v.BindEnv("auth.tokenTTLHours", "AGENT_DESK_AUTH_TOKENTTLHOURS", "AUTH_TOKEN_TTL_HOURS")
|
||
_ = v.BindEnv("customerSession.secret", "AGENT_DESK_CUSTOMERSESSION_SECRET", "CUSTOMER_SESSION_SECRET", "SESSION_SECRET", "JWT_SECRET")
|
||
_ = v.BindEnv("identity.secret", "AGENT_DESK_IDENTITY_SECRET", "AGENT_DESK_IDENTITY_HMAC_SECRET", "IDENTITY_HMAC_SECRET", "WIDGET_IDENTITY_SECRET")
|
||
_ = v.BindEnv("identity.maxAgeMinutes", "AGENT_DESK_IDENTITY_MAXAGEMINUTES", "IDENTITY_SIGNATURE_MAX_AGE_MINUTES")
|
||
_ = v.BindEnv("conversation.customerMaxOpen", "AGENT_DESK_CONVERSATION_CUSTOMERMAXOPEN", "CONVERSATION_CUSTOMER_MAX_OPEN")
|
||
_ = v.BindEnv("conversation.emailChatLiveMinutes", "AGENT_DESK_CONVERSATION_EMAILCHATLIVEMINUTES", "CONVERSATION_EMAIL_CHAT_LIVE_MINUTES")
|
||
_ = v.BindEnv("storage.default", "AGENT_DESK_STORAGE_DEFAULT", "STORAGE_DEFAULT", "STORAGE_TYPE")
|
||
_ = v.BindEnv("storage.local.root", "AGENT_DESK_STORAGE_LOCAL_ROOT", "STORAGE_LOCAL_ROOT")
|
||
_ = v.BindEnv("storage.local.baseUrl", "AGENT_DESK_STORAGE_LOCAL_BASEURL", "STORAGE_LOCAL_BASE_URL")
|
||
_ = v.BindEnv("vectorDB.type", "AGENT_DESK_VECTORDB_TYPE", "VECTOR_DB_TYPE")
|
||
_ = v.BindEnv("vectorDB.qdrant.host", "AGENT_DESK_VECTORDB_QDRANT_HOST", "QDRANT_HOST")
|
||
_ = v.BindEnv("vectorDB.qdrant.grpcPort", "AGENT_DESK_VECTORDB_QDRANT_GRPCPORT", "QDRANT_GRPC_PORT", "QDRANT_PORT")
|
||
_ = v.BindEnv("vectorDB.qdrant.apiKey", "AGENT_DESK_VECTORDB_QDRANT_APIKEY", "QDRANT_API_KEY")
|
||
_ = v.BindEnv("oidc.enabled", "AGENT_DESK_OIDC_ENABLED", "OIDC_ENABLED")
|
||
_ = v.BindEnv("oidc.issuer", "AGENT_DESK_OIDC_ISSUER", "OIDC_ISSUER")
|
||
_ = v.BindEnv("oidc.clientId", "AGENT_DESK_OIDC_CLIENTID", "OIDC_CLIENT_ID", "CUSTOM_OAUTH_CLIENT_ID")
|
||
_ = v.BindEnv("oidc.clientSecret", "AGENT_DESK_OIDC_CLIENTSECRET", "OIDC_CLIENT_SECRET", "CUSTOM_OAUTH_CLIENT_SECRET")
|
||
_ = v.BindEnv("oidc.redirectUrl", "AGENT_DESK_OIDC_REDIRECTURL", "OIDC_REDIRECT_URL", "CUSTOM_OAUTH_REDIRECT_URI")
|
||
_ = v.BindEnv("webhook.orgSyncSecret", "AGENT_DESK_WEBHOOK_ORGSYNCSECRET", "ORG_SYNC_SECRET", "WEBHOOK_SECRET")
|
||
_ = v.BindEnv("discord.clientId", "AGENT_DESK_DISCORD_CLIENTID", "DISCORD_CLIENT_ID")
|
||
_ = v.BindEnv("discord.clientSecret", "AGENT_DESK_DISCORD_CLIENTSECRET", "DISCORD_CLIENT_SECRET")
|
||
_ = v.BindEnv("discord.botToken", "AGENT_DESK_DISCORD_BOTTOKEN", "DISCORD_BOT_TOKEN")
|
||
_ = v.BindEnv("discord.publicKey", "AGENT_DESK_DISCORD_PUBLICKEY", "DISCORD_PUBLIC_KEY")
|
||
_ = v.BindEnv("slack.clientId", "AGENT_DESK_SLACK_CLIENTID", "SLACK_CLIENT_ID")
|
||
_ = v.BindEnv("slack.clientSecret", "AGENT_DESK_SLACK_CLIENTSECRET", "SLACK_CLIENT_SECRET")
|
||
_ = v.BindEnv("slack.botToken", "AGENT_DESK_SLACK_BOTTOKEN", "SLACK_BOT_TOKEN")
|
||
_ = v.BindEnv("slack.signingSecret", "AGENT_DESK_SLACK_SIGNINGSECRET", "SLACK_SIGNING_SECRET")
|
||
_ = v.BindEnv("email.provider", "EMAIL_PROVIDER", "AGENT_DESK_EMAIL_PROVIDER")
|
||
_ = v.BindEnv("email.fromAddress", "EMAIL_FROM", "EMAIL_FROM_ADDRESS", "AGENT_DESK_EMAIL_FROMADDRESS")
|
||
_ = v.BindEnv("email.fromName", "EMAIL_FROM_NAME", "EMAIL_SENDER_NAME", "AGENT_DESK_EMAIL_FROMNAME")
|
||
_ = v.BindEnv("email.apiKey", "EMAIL_API_KEY", "BREVO_API_KEY", "AGENT_DESK_EMAIL_APIKEY")
|
||
_ = v.BindEnv("email.smtpHost", "SMTP_HOST", "EMAIL_SMTP_HOST", "AGENT_DESK_EMAIL_SMTPHOST")
|
||
_ = v.BindEnv("email.smtpPort", "SMTP_PORT", "EMAIL_SMTP_PORT", "AGENT_DESK_EMAIL_SMTPPORT")
|
||
_ = v.BindEnv("email.smtpUser", "SMTP_USER", "EMAIL_SMTP_USER", "AGENT_DESK_EMAIL_SMTPUSER")
|
||
_ = v.BindEnv("email.smtpPassword", "SMTP_PASSWORD", "SMTP_PASS", "AGENT_DESK_EMAIL_SMTPPASSWORD")
|
||
_ = v.BindEnv("email.smtpUseTls", "SMTP_USE_TLS", "SMTP_SSL", "AGENT_DESK_EMAIL_SMTPUSETLS")
|
||
_ = v.BindEnv("email.smtpAllowInsecure", "SMTP_ALLOW_INSECURE", "AGENT_DESK_EMAIL_SMTPALLOWINSECURE")
|
||
_ = v.BindEnv("email.inboundSecret", "EMAIL_INBOUND_SECRET", "EMAIL_WEBHOOK_SECRET", "AGENT_DESK_EMAIL_INBOUNDSECRET")
|
||
}
|
||
|
||
func normalizeLoadedConfig(cfg *Config) {
|
||
if cfg == nil {
|
||
return
|
||
}
|
||
if cfg.DB.Type == "sqlite" && (strings.HasPrefix(cfg.DB.DSN, "postgres://") || strings.HasPrefix(cfg.DB.DSN, "postgresql://")) {
|
||
cfg.DB.Type = "postgres"
|
||
} else if cfg.DB.Type == "sqlite" && strings.Contains(cfg.DB.DSN, "@tcp(") {
|
||
cfg.DB.Type = "mysql"
|
||
}
|
||
}
|