2026-04-01 00:58:42 +08:00
|
|
|
package config
|
|
|
|
|
|
|
|
|
|
import (
|
2026-05-03 02:02:39 +08:00
|
|
|
"crypto/tls"
|
2026-04-18 20:56:05 +08:00
|
|
|
"encoding/json"
|
2026-04-01 00:58:42 +08:00
|
|
|
"fmt"
|
2026-05-03 15:04:08 +08:00
|
|
|
"net"
|
|
|
|
|
"net/url"
|
2026-04-01 00:58:42 +08:00
|
|
|
"os"
|
2026-05-05 20:48:14 +08:00
|
|
|
"strconv"
|
2026-04-01 00:58:42 +08:00
|
|
|
"strings"
|
|
|
|
|
"time"
|
|
|
|
|
|
2026-05-01 16:01:23 +08:00
|
|
|
"github.com/mitchellh/mapstructure"
|
|
|
|
|
|
|
|
|
|
configruntime "github.com/geo-platform/tenant-api/internal/shared/config/runtime"
|
|
|
|
|
runtimeenv "github.com/geo-platform/tenant-api/internal/shared/config/runtime/env"
|
|
|
|
|
runtimefile "github.com/geo-platform/tenant-api/internal/shared/config/runtime/file"
|
2026-04-01 00:58:42 +08:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type Config struct {
|
2026-04-22 00:24:21 +08:00
|
|
|
Server ServerConfig `mapstructure:"server"`
|
|
|
|
|
Database DatabaseConfig `mapstructure:"database"`
|
|
|
|
|
MonitoringDatabase DatabaseConfig `mapstructure:"monitoring_database"`
|
|
|
|
|
RabbitMQ RabbitMQConfig `mapstructure:"rabbitmq"`
|
|
|
|
|
Scheduler SchedulerConfig `mapstructure:"scheduler"`
|
|
|
|
|
MonitoringWorkers MonitoringConfig `mapstructure:"monitoring_workers"`
|
|
|
|
|
MonitoringDispatch MonitoringDispatchConfig `mapstructure:"monitoring_dispatch"`
|
|
|
|
|
Membership MembershipConfig `mapstructure:"membership"`
|
|
|
|
|
BrandLibrary BrandLibraryConfig `mapstructure:"brand_library"`
|
2026-05-13 15:59:39 +08:00
|
|
|
IPRegion IPRegionConfig `mapstructure:"ip_region"`
|
2026-04-22 00:24:21 +08:00
|
|
|
Redis RedisConfig `mapstructure:"redis"`
|
|
|
|
|
Qdrant QdrantConfig `mapstructure:"qdrant"`
|
|
|
|
|
ObjectStorage ObjectStorageConfig `mapstructure:"object_storage"`
|
|
|
|
|
Cache CacheConfig `mapstructure:"cache"`
|
|
|
|
|
JWT JWTConfig `mapstructure:"jwt"`
|
2026-05-12 12:32:39 +08:00
|
|
|
Auth AuthConfig `mapstructure:"auth"`
|
2026-04-22 00:24:21 +08:00
|
|
|
Log LogConfig `mapstructure:"log"`
|
|
|
|
|
LLM LLMConfig `mapstructure:"llm"`
|
2026-05-05 20:48:14 +08:00
|
|
|
Compliance ComplianceConfig `mapstructure:"compliance"`
|
2026-04-22 00:24:21 +08:00
|
|
|
Retrieval RetrievalConfig `mapstructure:"retrieval"`
|
|
|
|
|
Generation GenerationConfig `mapstructure:"generation"`
|
2026-05-11 11:11:21 +08:00
|
|
|
BrowserFetch BrowserFetchConfig `mapstructure:"browser_fetch"`
|
2026-05-29 23:17:01 +08:00
|
|
|
MediaSupply MediaSupplyConfig `mapstructure:"media_supply"`
|
2026-04-01 00:58:42 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type ServerConfig struct {
|
2026-05-14 11:05:20 +08:00
|
|
|
Port int `mapstructure:"port"`
|
|
|
|
|
Mode string `mapstructure:"mode"`
|
2026-06-06 13:06:14 +08:00
|
|
|
PublicBaseURL string `mapstructure:"public_base_url"`
|
2026-05-14 11:05:20 +08:00
|
|
|
TrustedProxies []string `mapstructure:"trusted_proxies"`
|
|
|
|
|
AllowedOrigins []string `mapstructure:"allowed_origins"`
|
|
|
|
|
SecurityHeaders SecurityHeaders `mapstructure:"security_headers"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type SecurityHeaders struct {
|
|
|
|
|
Enabled bool `mapstructure:"enabled"`
|
2026-04-01 00:58:42 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type DatabaseConfig struct {
|
|
|
|
|
Host string `mapstructure:"host"`
|
|
|
|
|
Port int `mapstructure:"port"`
|
|
|
|
|
User string `mapstructure:"user"`
|
|
|
|
|
Password string `mapstructure:"password"`
|
|
|
|
|
DBName string `mapstructure:"dbname"`
|
|
|
|
|
SSLMode string `mapstructure:"sslmode"`
|
|
|
|
|
MaxOpenConns int `mapstructure:"max_open_conns"`
|
|
|
|
|
MaxIdleConns int `mapstructure:"max_idle_conns"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (d DatabaseConfig) DSN() string {
|
2026-05-03 15:04:08 +08:00
|
|
|
u := url.URL{
|
|
|
|
|
Scheme: "postgres",
|
|
|
|
|
User: url.UserPassword(d.User, d.Password),
|
|
|
|
|
Host: net.JoinHostPort(d.Host, fmt.Sprintf("%d", d.Port)),
|
|
|
|
|
Path: "/" + d.DBName,
|
|
|
|
|
}
|
|
|
|
|
q := u.Query()
|
|
|
|
|
q.Set("sslmode", d.SSLMode)
|
|
|
|
|
u.RawQuery = q.Encode()
|
|
|
|
|
return u.String()
|
2026-04-01 00:58:42 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type RedisConfig struct {
|
2026-05-03 02:02:39 +08:00
|
|
|
Addr string `mapstructure:"addr"`
|
|
|
|
|
Password string `mapstructure:"password"`
|
|
|
|
|
DB int `mapstructure:"db"`
|
|
|
|
|
DialTimeout time.Duration `mapstructure:"dial_timeout"`
|
|
|
|
|
ReadTimeout time.Duration `mapstructure:"read_timeout"`
|
|
|
|
|
WriteTimeout time.Duration `mapstructure:"write_timeout"`
|
|
|
|
|
PoolSize int `mapstructure:"pool_size"`
|
|
|
|
|
MinIdleConns int `mapstructure:"min_idle_conns"`
|
|
|
|
|
MaxRetries int `mapstructure:"max_retries"`
|
|
|
|
|
PoolTimeout time.Duration `mapstructure:"pool_timeout"`
|
|
|
|
|
TLSEnabled bool `mapstructure:"tls_enabled"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (r RedisConfig) TLSConfig() *tls.Config {
|
|
|
|
|
if !r.TLSEnabled {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
return &tls.Config{MinVersion: tls.VersionTLS12}
|
2026-04-01 00:58:42 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-12 09:56:18 +08:00
|
|
|
type RabbitMQConfig struct {
|
2026-04-27 21:33:36 +08:00
|
|
|
URL string `mapstructure:"url"`
|
|
|
|
|
MonitorResultExchange string `mapstructure:"monitor_result_exchange"`
|
|
|
|
|
MonitorResultRoutingKey string `mapstructure:"monitor_result_routing_key"`
|
|
|
|
|
MonitorResultQueue string `mapstructure:"monitor_result_queue"`
|
|
|
|
|
MonitorResultDLX string `mapstructure:"monitor_result_dlx"`
|
|
|
|
|
MonitorResultDLQ string `mapstructure:"monitor_result_dlq"`
|
|
|
|
|
MonitorResultDLQRouteKey string `mapstructure:"monitor_result_dlq_routing_key"`
|
|
|
|
|
MonitorProjectionExchange string `mapstructure:"monitor_projection_exchange"`
|
|
|
|
|
MonitorProjectionRoutingKey string `mapstructure:"monitor_projection_routing_key"`
|
|
|
|
|
MonitorProjectionQueue string `mapstructure:"monitor_projection_queue"`
|
|
|
|
|
MonitorProjectionDLX string `mapstructure:"monitor_projection_dlx"`
|
|
|
|
|
MonitorProjectionDLQ string `mapstructure:"monitor_projection_dlq"`
|
|
|
|
|
MonitorProjectionDLQRouteKey string `mapstructure:"monitor_projection_dlq_routing_key"`
|
|
|
|
|
GenerationExchange string `mapstructure:"generation_exchange"`
|
|
|
|
|
GenerationRoutingKey string `mapstructure:"generation_routing_key"`
|
|
|
|
|
GenerationQueue string `mapstructure:"generation_queue"`
|
|
|
|
|
GenerationDLX string `mapstructure:"generation_dlx"`
|
|
|
|
|
GenerationDLQ string `mapstructure:"generation_dlq"`
|
|
|
|
|
GenerationDLQRouteKey string `mapstructure:"generation_dlq_routing_key"`
|
|
|
|
|
GenerationEventExchange string `mapstructure:"generation_event_exchange"`
|
|
|
|
|
DesktopTaskEventExchange string `mapstructure:"desktop_task_event_exchange"`
|
|
|
|
|
DesktopDispatchExchange string `mapstructure:"desktop_dispatch_exchange"`
|
|
|
|
|
DesktopDispatchPublishPrefix string `mapstructure:"desktop_dispatch_publish_prefix"`
|
|
|
|
|
DesktopDispatchMonitorPrefix string `mapstructure:"desktop_dispatch_monitor_prefix"`
|
|
|
|
|
DesktopAccountHealthExchange string `mapstructure:"desktop_account_health_exchange"`
|
|
|
|
|
DesktopAccountHealthRoutingKey string `mapstructure:"desktop_account_health_routing_key"`
|
|
|
|
|
DesktopAccountHealthQueue string `mapstructure:"desktop_account_health_queue"`
|
|
|
|
|
DesktopAccountHealthDLX string `mapstructure:"desktop_account_health_dlx"`
|
|
|
|
|
DesktopAccountHealthDLQ string `mapstructure:"desktop_account_health_dlq"`
|
|
|
|
|
DesktopAccountHealthDLQRouteKey string `mapstructure:"desktop_account_health_dlq_routing_key"`
|
|
|
|
|
TemplateAssistExchange string `mapstructure:"template_assist_exchange"`
|
|
|
|
|
TemplateAssistRoutingKey string `mapstructure:"template_assist_routing_key"`
|
|
|
|
|
TemplateAssistQueue string `mapstructure:"template_assist_queue"`
|
|
|
|
|
TemplateAssistDLX string `mapstructure:"template_assist_dlx"`
|
|
|
|
|
TemplateAssistDLQ string `mapstructure:"template_assist_dlq"`
|
|
|
|
|
TemplateAssistDLQRouteKey string `mapstructure:"template_assist_dlq_routing_key"`
|
|
|
|
|
KolAssistExchange string `mapstructure:"kol_assist_exchange"`
|
|
|
|
|
KolAssistRoutingKey string `mapstructure:"kol_assist_routing_key"`
|
|
|
|
|
KolAssistQueue string `mapstructure:"kol_assist_queue"`
|
|
|
|
|
KolAssistDLX string `mapstructure:"kol_assist_dlx"`
|
|
|
|
|
KolAssistDLQ string `mapstructure:"kol_assist_dlq"`
|
|
|
|
|
KolAssistDLQRouteKey string `mapstructure:"kol_assist_dlq_routing_key"`
|
|
|
|
|
ImageAssetExchange string `mapstructure:"image_asset_exchange"`
|
|
|
|
|
ImageAssetRoutingKey string `mapstructure:"image_asset_routing_key"`
|
|
|
|
|
ImageAssetQueue string `mapstructure:"image_asset_queue"`
|
|
|
|
|
ImageAssetDLX string `mapstructure:"image_asset_dlx"`
|
|
|
|
|
ImageAssetDLQ string `mapstructure:"image_asset_dlq"`
|
|
|
|
|
ImageAssetDLQRouteKey string `mapstructure:"image_asset_dlq_routing_key"`
|
2026-05-05 20:48:14 +08:00
|
|
|
ComplianceReviewExchange string `mapstructure:"compliance_review_exchange"`
|
|
|
|
|
ComplianceReviewRoutingKey string `mapstructure:"compliance_review_routing_key"`
|
|
|
|
|
ComplianceReviewQueue string `mapstructure:"compliance_review_queue"`
|
|
|
|
|
ComplianceReviewDLX string `mapstructure:"compliance_review_dlx"`
|
|
|
|
|
ComplianceReviewDLQ string `mapstructure:"compliance_review_dlq"`
|
|
|
|
|
ComplianceReviewDLQRouteKey string `mapstructure:"compliance_review_dlq_routing_key"`
|
2026-04-27 21:33:36 +08:00
|
|
|
ConsumerPrefetch int `mapstructure:"consumer_prefetch"`
|
|
|
|
|
PublishChannelPoolSize int `mapstructure:"publish_channel_pool_size"`
|
2026-04-15 14:18:55 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type SchedulerConfig struct {
|
2026-04-24 22:19:57 +08:00
|
|
|
HTTPHost string `mapstructure:"http_host"`
|
|
|
|
|
HTTPPort int `mapstructure:"http_port"`
|
|
|
|
|
InternalMetricsToken string `mapstructure:"internal_metrics_token"`
|
2026-04-15 14:18:55 +08:00
|
|
|
DispatchInterval time.Duration `mapstructure:"dispatch_interval"`
|
|
|
|
|
DispatchTimeout time.Duration `mapstructure:"dispatch_timeout"`
|
|
|
|
|
DispatchBatchSize int `mapstructure:"dispatch_batch_size"`
|
|
|
|
|
DispatchConcurrency int `mapstructure:"dispatch_concurrency"`
|
|
|
|
|
GenerationQueueBackpressureLimit int `mapstructure:"generation_queue_backpressure_limit"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type MonitoringConfig struct {
|
|
|
|
|
ResultIngestConcurrency int `mapstructure:"result_ingest_concurrency"`
|
|
|
|
|
ProjectionRebuildConcurrency int `mapstructure:"projection_rebuild_concurrency"`
|
2026-04-12 09:56:18 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-22 00:24:21 +08:00
|
|
|
type MonitoringDispatchConfig struct {
|
|
|
|
|
DesktopTasksRolloutPercentage int `mapstructure:"desktop_tasks_rollout_percentage"`
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-18 20:56:05 +08:00
|
|
|
const (
|
|
|
|
|
ArticleQuotaCycleLifetime = "lifetime"
|
|
|
|
|
ArticleQuotaCycleMonthly = "monthly"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type MembershipConfig struct {
|
|
|
|
|
DefaultPlanCode string `mapstructure:"default_plan_code"`
|
|
|
|
|
Plans []MembershipPlanConfig `mapstructure:"plans"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type MembershipPlanConfig struct {
|
|
|
|
|
Code string `mapstructure:"code"`
|
|
|
|
|
Name string `mapstructure:"name"`
|
|
|
|
|
ArticleGeneration int `mapstructure:"article_generation"`
|
|
|
|
|
ArticleQuotaCycle string `mapstructure:"article_quota_cycle"`
|
2026-04-28 11:33:30 +08:00
|
|
|
AIPointsMonthly int `mapstructure:"ai_points_monthly"`
|
|
|
|
|
AIPointBaseChars int `mapstructure:"ai_point_base_chars"`
|
2026-04-18 20:56:05 +08:00
|
|
|
ImageStorageBytes int64 `mapstructure:"image_storage_bytes"`
|
|
|
|
|
CompanyLimit int `mapstructure:"company_limit"`
|
|
|
|
|
SubscriptionDuration time.Duration `mapstructure:"subscription_duration"`
|
|
|
|
|
ContactAdminOnExpiry bool `mapstructure:"contact_admin_on_expiry"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type PlanQuotaPolicy struct {
|
|
|
|
|
ArticleGeneration int `json:"article_generation"`
|
|
|
|
|
ArticleQuotaCycle string `json:"article_quota_cycle"`
|
2026-04-28 11:33:30 +08:00
|
|
|
AIPointsMonthly int `json:"ai_points_monthly"`
|
|
|
|
|
AIPointBaseChars int `json:"ai_point_base_chars"`
|
2026-04-18 20:56:05 +08:00
|
|
|
ImageStorageBytes int64 `json:"image_storage_bytes"`
|
|
|
|
|
BrandLimit int `json:"brand_limit"`
|
|
|
|
|
SubscriptionDurationSecs int64 `json:"subscription_duration_seconds,omitempty"`
|
|
|
|
|
ContactAdminOnExpiry bool `json:"contact_admin_on_expiry,omitempty"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (c MembershipConfig) FindPlan(code string) (MembershipPlanConfig, bool) {
|
|
|
|
|
normalizedCode := strings.ToLower(strings.TrimSpace(code))
|
|
|
|
|
for _, plan := range c.Plans {
|
|
|
|
|
if strings.EqualFold(strings.TrimSpace(plan.Code), normalizedCode) {
|
|
|
|
|
return plan, true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return MembershipPlanConfig{}, false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (p MembershipPlanConfig) QuotaPolicy() PlanQuotaPolicy {
|
|
|
|
|
policy := PlanQuotaPolicy{
|
|
|
|
|
ArticleGeneration: maxInt(p.ArticleGeneration, 0),
|
|
|
|
|
ArticleQuotaCycle: normalizeArticleQuotaCycle(p.ArticleQuotaCycle, p.Code),
|
2026-04-28 11:33:30 +08:00
|
|
|
AIPointsMonthly: maxInt(p.AIPointsMonthly, 0),
|
|
|
|
|
AIPointBaseChars: normalizeAIPointBaseChars(p.AIPointBaseChars),
|
2026-04-18 20:56:05 +08:00
|
|
|
ImageStorageBytes: maxInt64(p.ImageStorageBytes, 0),
|
|
|
|
|
BrandLimit: maxInt(p.CompanyLimit, 0),
|
|
|
|
|
ContactAdminOnExpiry: p.ContactAdminOnExpiry,
|
|
|
|
|
}
|
|
|
|
|
if p.SubscriptionDuration > 0 {
|
|
|
|
|
policy.SubscriptionDurationSecs = int64(p.SubscriptionDuration / time.Second)
|
|
|
|
|
}
|
|
|
|
|
return policy
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (p MembershipPlanConfig) QuotaPolicyJSON() ([]byte, error) {
|
|
|
|
|
return json.Marshal(p.QuotaPolicy())
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-16 21:01:40 +08:00
|
|
|
type BrandLibraryConfig struct {
|
2026-05-12 21:53:36 +08:00
|
|
|
FreeBrandLimit int `mapstructure:"free_brand_limit"`
|
|
|
|
|
PaidBrandLimit int `mapstructure:"paid_brand_limit"`
|
|
|
|
|
MaxKeywords int `mapstructure:"max_keywords"`
|
|
|
|
|
MaxQuestionsPerKeyword int `mapstructure:"max_questions_per_keyword"`
|
|
|
|
|
QuestionLimitsByPlan map[string]int `mapstructure:"question_limits_by_plan"`
|
2026-04-16 21:01:40 +08:00
|
|
|
}
|
|
|
|
|
|
2026-05-13 15:59:39 +08:00
|
|
|
type IPRegionConfig struct {
|
|
|
|
|
V4XDBPath string `mapstructure:"v4_xdb_path"`
|
|
|
|
|
V6XDBPath string `mapstructure:"v6_xdb_path"`
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-16 21:01:40 +08:00
|
|
|
func (c BrandLibraryConfig) BrandLimitForPlan(planCode string) int {
|
|
|
|
|
if strings.EqualFold(strings.TrimSpace(planCode), "free") {
|
|
|
|
|
return c.FreeBrandLimit
|
|
|
|
|
}
|
|
|
|
|
return c.PaidBrandLimit
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-12 21:53:36 +08:00
|
|
|
func (c BrandLibraryConfig) QuestionLimitForPlan(planCode string) int {
|
|
|
|
|
limits := c.QuestionLimitsByPlan
|
|
|
|
|
normalizedPlan := strings.ToLower(strings.TrimSpace(planCode))
|
|
|
|
|
if limits != nil {
|
|
|
|
|
if value := limits[normalizedPlan]; value > 0 {
|
|
|
|
|
return value
|
|
|
|
|
}
|
|
|
|
|
if value := limits["default"]; value > 0 {
|
|
|
|
|
return value
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return 25
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-05 17:14:13 +08:00
|
|
|
type QdrantConfig struct {
|
|
|
|
|
URL string `mapstructure:"url"`
|
|
|
|
|
APIKey string `mapstructure:"api_key"`
|
|
|
|
|
Collection string `mapstructure:"collection"`
|
|
|
|
|
Timeout time.Duration `mapstructure:"timeout"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type ObjectStorageConfig struct {
|
2026-05-26 01:19:01 +08:00
|
|
|
Provider string `mapstructure:"provider"`
|
|
|
|
|
Endpoint string `mapstructure:"endpoint"`
|
|
|
|
|
AccessKey string `mapstructure:"access_key"`
|
|
|
|
|
SecretKey string `mapstructure:"secret_key"`
|
|
|
|
|
Bucket string `mapstructure:"bucket"`
|
|
|
|
|
UseSSL bool `mapstructure:"use_ssl"`
|
|
|
|
|
PublicBaseURL string `mapstructure:"public_base_url"`
|
|
|
|
|
BucketACL string `mapstructure:"bucket_acl"`
|
|
|
|
|
ObjectACL string `mapstructure:"object_acl"`
|
|
|
|
|
SignedURLTTL time.Duration `mapstructure:"signed_url_ttl"`
|
|
|
|
|
Region string `mapstructure:"region"`
|
|
|
|
|
ForcePathStyle bool `mapstructure:"force_path_style"`
|
2026-04-05 17:14:13 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-01 00:58:42 +08:00
|
|
|
type JWTConfig struct {
|
|
|
|
|
Secret string `mapstructure:"secret"`
|
|
|
|
|
AccessTTL time.Duration `mapstructure:"access_ttl"`
|
|
|
|
|
RefreshTTL time.Duration `mapstructure:"refresh_ttl"`
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-12 12:32:39 +08:00
|
|
|
type AuthConfig struct {
|
2026-05-14 11:05:20 +08:00
|
|
|
LoginGuard LoginGuardConfig `mapstructure:"login_guard"`
|
|
|
|
|
PasswordCipher PasswordCipherConfig `mapstructure:"password_cipher"`
|
2026-05-12 12:32:39 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type LoginGuardConfig struct {
|
|
|
|
|
Enabled bool `mapstructure:"enabled"`
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-14 11:05:20 +08:00
|
|
|
type PasswordCipherConfig struct {
|
|
|
|
|
Enabled bool `mapstructure:"enabled"`
|
|
|
|
|
KeyID string `mapstructure:"key_id"`
|
|
|
|
|
PrivateKeyPEM string `mapstructure:"private_key_pem"`
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-01 00:58:42 +08:00
|
|
|
type LogConfig struct {
|
|
|
|
|
Level string `mapstructure:"level"`
|
|
|
|
|
Format string `mapstructure:"format"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type LLMConfig struct {
|
2026-04-05 20:41:42 +08:00
|
|
|
Provider string `mapstructure:"provider"`
|
|
|
|
|
APIKey string `mapstructure:"api_key"`
|
|
|
|
|
BaseURL string `mapstructure:"base_url"`
|
|
|
|
|
Model string `mapstructure:"model"`
|
|
|
|
|
KnowledgeURLModel string `mapstructure:"knowledge_url_model"`
|
|
|
|
|
Timeout time.Duration `mapstructure:"timeout"`
|
|
|
|
|
MaxOutputTokens int64 `mapstructure:"max_output_tokens"`
|
|
|
|
|
Temperature float64 `mapstructure:"temperature"`
|
|
|
|
|
TopP float64 `mapstructure:"top_p"`
|
|
|
|
|
ReasoningEffort string `mapstructure:"reasoning_effort"`
|
|
|
|
|
WebSearchLimit int32 `mapstructure:"web_search_limit"`
|
2026-04-02 00:31:28 +08:00
|
|
|
}
|
|
|
|
|
|
2026-05-05 20:48:14 +08:00
|
|
|
type ComplianceConfig struct {
|
|
|
|
|
Enabled bool `mapstructure:"enabled"`
|
|
|
|
|
LLMJudgeEnabled bool `mapstructure:"llm_judge_enabled"`
|
|
|
|
|
SnapshotReloadInterval time.Duration `mapstructure:"snapshot_reload_interval"`
|
|
|
|
|
PublishGateTimeout time.Duration `mapstructure:"publish_gate_timeout"`
|
|
|
|
|
ReviewWorkerConcurrency int `mapstructure:"review_worker_concurrency"`
|
|
|
|
|
ReviewWorkerTimeout time.Duration `mapstructure:"review_worker_timeout"`
|
|
|
|
|
ReviewMaxAttempts int `mapstructure:"review_max_attempts"`
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-05 17:14:13 +08:00
|
|
|
type RetrievalConfig struct {
|
|
|
|
|
Provider string `mapstructure:"provider"`
|
|
|
|
|
BaseURL string `mapstructure:"base_url"`
|
|
|
|
|
APIKey string `mapstructure:"api_key"`
|
|
|
|
|
EmbeddingModel string `mapstructure:"embedding_model"`
|
|
|
|
|
RerankerModel string `mapstructure:"reranker_model"`
|
|
|
|
|
Timeout time.Duration `mapstructure:"timeout"`
|
|
|
|
|
ChunkSize int `mapstructure:"chunk_size"`
|
|
|
|
|
ChunkOverlap int `mapstructure:"chunk_overlap"`
|
|
|
|
|
EmbeddingBatchSize int `mapstructure:"embedding_batch_size"`
|
|
|
|
|
RecallLimit int `mapstructure:"recall_limit"`
|
|
|
|
|
RerankTopN int `mapstructure:"rerank_top_n"`
|
|
|
|
|
MaxChunksPerDoc int `mapstructure:"max_chunks_per_doc"`
|
|
|
|
|
OverlapTokens int `mapstructure:"overlap_tokens"`
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-02 00:31:28 +08:00
|
|
|
type CacheConfig struct {
|
2026-05-03 02:02:39 +08:00
|
|
|
Driver string `mapstructure:"driver"` // "redis" or "memory"
|
|
|
|
|
Namespace string `mapstructure:"namespace"`
|
|
|
|
|
JitterRatio float64 `mapstructure:"jitter_ratio"`
|
|
|
|
|
MetricsEnabled bool `mapstructure:"metrics_enabled"`
|
|
|
|
|
AsyncFillEnabled bool `mapstructure:"async_fill_enabled"`
|
|
|
|
|
AsyncFillWorkers int `mapstructure:"async_fill_workers"`
|
|
|
|
|
AsyncFillBuffer int `mapstructure:"async_fill_buffer"`
|
|
|
|
|
AsyncFillTimeout time.Duration `mapstructure:"async_fill_timeout"`
|
|
|
|
|
L1Enabled bool `mapstructure:"l1_enabled"`
|
|
|
|
|
L1TTL time.Duration `mapstructure:"l1_ttl"`
|
|
|
|
|
DeleteScanCount int64 `mapstructure:"delete_scan_count"`
|
2026-04-01 00:58:42 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type GenerationConfig struct {
|
2026-05-05 23:43:10 +08:00
|
|
|
QueueSize int `mapstructure:"queue_size"`
|
|
|
|
|
WorkerConcurrency int `mapstructure:"worker_concurrency"`
|
|
|
|
|
StreamEnabled bool `mapstructure:"stream_enabled"`
|
|
|
|
|
ArticleTimeout time.Duration `mapstructure:"article_timeout"`
|
|
|
|
|
TaskLeaseTTL time.Duration `mapstructure:"task_lease_ttl"`
|
|
|
|
|
TaskRecoveryInterval time.Duration `mapstructure:"task_recovery_interval"`
|
|
|
|
|
TaskRecoveryTimeout time.Duration `mapstructure:"task_recovery_timeout"`
|
|
|
|
|
TaskRecoveryBatchSize int `mapstructure:"task_recovery_batch_size"`
|
|
|
|
|
TaskQueuedStaleAfter time.Duration `mapstructure:"task_queued_stale_after"`
|
|
|
|
|
TaskMaxAttempts int `mapstructure:"task_max_attempts"`
|
|
|
|
|
TaskStateCheckInterval time.Duration `mapstructure:"task_state_check_interval"`
|
|
|
|
|
TaskStateCheckTimeout time.Duration `mapstructure:"task_state_check_timeout"`
|
|
|
|
|
TaskStateCheckBatchSize int `mapstructure:"task_state_check_batch_size"`
|
|
|
|
|
TaskStateCheckLookback time.Duration `mapstructure:"task_state_check_lookback"`
|
2026-04-01 00:58:42 +08:00
|
|
|
}
|
|
|
|
|
|
2026-05-11 11:11:21 +08:00
|
|
|
type BrowserFetchConfig struct {
|
|
|
|
|
Enabled bool `mapstructure:"enabled"`
|
|
|
|
|
Provider string `mapstructure:"provider"`
|
|
|
|
|
ServiceURL string `mapstructure:"service_url"`
|
|
|
|
|
Token string `mapstructure:"token"`
|
|
|
|
|
QueueSize int `mapstructure:"queue_size"`
|
|
|
|
|
WorkerConcurrency int `mapstructure:"worker_concurrency"`
|
|
|
|
|
RequestTimeout time.Duration `mapstructure:"request_timeout"`
|
|
|
|
|
CacheTTL time.Duration `mapstructure:"cache_ttl"`
|
|
|
|
|
AllowedDomains []string `mapstructure:"allowed_domains"`
|
|
|
|
|
TriggerStatus []int `mapstructure:"trigger_status"`
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-29 23:17:01 +08:00
|
|
|
type MediaSupplyConfig struct {
|
|
|
|
|
Enabled bool `mapstructure:"enabled"`
|
|
|
|
|
DefaultMarkupPercent float64 `mapstructure:"default_markup_percent"`
|
|
|
|
|
MinimumMarkupCents int64 `mapstructure:"minimum_markup_cents"`
|
|
|
|
|
Worker MediaSupplyWorkerConfig `mapstructure:"worker"`
|
|
|
|
|
Meijiequan MeijiequanConfig `mapstructure:"meijiequan"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type MediaSupplyWorkerConfig struct {
|
|
|
|
|
Enabled bool `mapstructure:"enabled"`
|
|
|
|
|
PollInterval time.Duration `mapstructure:"poll_interval"`
|
|
|
|
|
BatchSize int `mapstructure:"batch_size"`
|
|
|
|
|
OrderMaxAttempts int `mapstructure:"order_max_attempts"`
|
|
|
|
|
SyncMaxAttempts int `mapstructure:"sync_max_attempts"`
|
|
|
|
|
BacklinkSyncInterval time.Duration `mapstructure:"backlink_sync_interval"`
|
|
|
|
|
BacklinkBatchSize int `mapstructure:"backlink_batch_size"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type MeijiequanConfig struct {
|
|
|
|
|
BaseURL string `mapstructure:"base_url"`
|
|
|
|
|
UserID string `mapstructure:"user_id"`
|
|
|
|
|
Username string `mapstructure:"username"`
|
|
|
|
|
Password string `mapstructure:"password"`
|
|
|
|
|
SessionTTL time.Duration `mapstructure:"session_ttl"`
|
|
|
|
|
RequestTimeout time.Duration `mapstructure:"request_timeout"`
|
|
|
|
|
SyncPageDelay time.Duration `mapstructure:"sync_page_delay"`
|
|
|
|
|
SyncPageSize int `mapstructure:"sync_page_size"`
|
|
|
|
|
MaxSyncPages int `mapstructure:"max_sync_pages"`
|
|
|
|
|
PublishedPageSize int `mapstructure:"published_page_size"`
|
|
|
|
|
PublishedMaxPages int `mapstructure:"published_max_pages"`
|
|
|
|
|
SearchOptionsTTL time.Duration `mapstructure:"search_options_ttl"`
|
|
|
|
|
UpstreamLockTTL time.Duration `mapstructure:"upstream_lock_ttl"`
|
|
|
|
|
UpstreamMinInterval time.Duration `mapstructure:"upstream_min_interval"`
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-01 00:58:42 +08:00
|
|
|
func Load(configPath string) (*Config, error) {
|
2026-05-01 16:01:23 +08:00
|
|
|
cfg, _, err := loadWithFiles(configPath)
|
|
|
|
|
return cfg, err
|
|
|
|
|
}
|
2026-04-01 00:58:42 +08:00
|
|
|
|
2026-05-01 16:01:23 +08:00
|
|
|
func loadWithFiles(configPath string) (*Config, []string, error) {
|
|
|
|
|
configFile, err := readConfigWithFallback(configPath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, nil, fmt.Errorf("read config: %w", err)
|
2026-04-01 00:58:42 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Allow local overrides
|
2026-05-01 16:01:23 +08:00
|
|
|
localConfigFile, err := mergeLocalOverrideWithFallback(configPath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, nil, fmt.Errorf("merge local config: %w", err)
|
|
|
|
|
}
|
2026-04-01 00:58:42 +08:00
|
|
|
|
2026-05-01 16:01:23 +08:00
|
|
|
cfg, err := decodeResolvedConfig(configFile, localConfigFile)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, nil, fmt.Errorf("unmarshal config: %w", err)
|
2026-04-01 00:58:42 +08:00
|
|
|
}
|
|
|
|
|
|
2026-05-01 16:01:23 +08:00
|
|
|
applyEnvOverrides(cfg)
|
2026-05-05 23:01:25 +08:00
|
|
|
NormalizeServerConfig(&cfg.Server)
|
2026-05-03 02:02:39 +08:00
|
|
|
NormalizeRedisConfig(&cfg.Redis)
|
|
|
|
|
NormalizeCacheConfig(&cfg.Cache)
|
2026-04-12 09:56:18 +08:00
|
|
|
normalizeRabbitMQConfig(&cfg.RabbitMQ)
|
2026-04-15 14:18:55 +08:00
|
|
|
normalizeSchedulerConfig(&cfg.Scheduler)
|
|
|
|
|
normalizeMonitoringConfig(&cfg.MonitoringWorkers)
|
2026-04-22 00:24:21 +08:00
|
|
|
normalizeMonitoringDispatchConfig(&cfg.MonitoringDispatch)
|
2026-04-18 20:56:05 +08:00
|
|
|
normalizeMembershipConfig(&cfg.Membership)
|
2026-04-16 21:01:40 +08:00
|
|
|
normalizeBrandLibraryConfig(&cfg.BrandLibrary)
|
2026-05-05 20:48:14 +08:00
|
|
|
NormalizeComplianceConfig(&cfg.Compliance)
|
2026-05-03 02:02:39 +08:00
|
|
|
NormalizeGenerationConfig(&cfg.Generation)
|
2026-05-11 11:11:21 +08:00
|
|
|
NormalizeBrowserFetchConfig(&cfg.BrowserFetch)
|
2026-05-29 23:17:01 +08:00
|
|
|
NormalizeMediaSupplyConfig(&cfg.MediaSupply)
|
2026-04-01 00:58:42 +08:00
|
|
|
|
2026-05-01 16:01:23 +08:00
|
|
|
files := []string{configFile}
|
|
|
|
|
if localConfigFile != "" {
|
|
|
|
|
files = append(files, localConfigFile)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return cfg, files, nil
|
2026-04-01 00:58:42 +08:00
|
|
|
}
|
|
|
|
|
|
2026-05-01 16:01:23 +08:00
|
|
|
func readConfigWithFallback(configPath string) (string, error) {
|
2026-04-18 20:56:05 +08:00
|
|
|
var lastErr error
|
|
|
|
|
for _, candidate := range candidateConfigPaths(configPath, false) {
|
2026-05-01 16:01:23 +08:00
|
|
|
if _, err := os.Stat(candidate); err == nil {
|
|
|
|
|
return candidate, nil
|
2026-04-18 20:56:05 +08:00
|
|
|
} else {
|
|
|
|
|
lastErr = err
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-05-01 16:01:23 +08:00
|
|
|
return "", lastErr
|
2026-04-18 20:56:05 +08:00
|
|
|
}
|
|
|
|
|
|
2026-05-01 16:01:23 +08:00
|
|
|
func mergeLocalOverrideWithFallback(configPath string) (string, error) {
|
2026-04-18 20:56:05 +08:00
|
|
|
for _, candidate := range candidateConfigPaths(configPath, true) {
|
2026-05-01 16:01:23 +08:00
|
|
|
if _, err := os.Stat(candidate); err != nil {
|
|
|
|
|
continue
|
2026-04-18 20:56:05 +08:00
|
|
|
}
|
2026-05-01 16:01:23 +08:00
|
|
|
return candidate, nil
|
|
|
|
|
}
|
|
|
|
|
return "", nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func decodeResolvedConfig(configFile, localConfigFile string) (*Config, error) {
|
|
|
|
|
sources := []configruntime.Source{runtimefile.NewSource(configFile)}
|
|
|
|
|
if localConfigFile != "" {
|
|
|
|
|
sources = append(sources, runtimefile.NewSource(localConfigFile))
|
2026-04-18 20:56:05 +08:00
|
|
|
}
|
2026-05-01 16:01:23 +08:00
|
|
|
sources = append(sources, runtimeenv.NewSource())
|
|
|
|
|
|
|
|
|
|
resolved := configruntime.New(configruntime.WithSource(sources...))
|
|
|
|
|
defer resolved.Close()
|
|
|
|
|
|
|
|
|
|
if err := resolved.Load(); err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
settings := make(map[string]any)
|
|
|
|
|
if err := resolved.Scan(&settings); err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
2026-05-03 02:02:39 +08:00
|
|
|
applyConfigDefaults(settings)
|
2026-05-01 16:01:23 +08:00
|
|
|
|
|
|
|
|
var cfg Config
|
|
|
|
|
decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
|
|
|
|
|
Result: &cfg,
|
|
|
|
|
TagName: "mapstructure",
|
|
|
|
|
WeaklyTypedInput: true,
|
|
|
|
|
DecodeHook: mapstructure.ComposeDecodeHookFunc(
|
|
|
|
|
mapstructure.StringToTimeDurationHookFunc(),
|
|
|
|
|
mapstructure.StringToSliceHookFunc(","),
|
|
|
|
|
),
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
if err := decoder.Decode(settings); err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
return &cfg, nil
|
2026-04-18 20:56:05 +08:00
|
|
|
}
|
|
|
|
|
|
2026-05-03 02:02:39 +08:00
|
|
|
func applyConfigDefaults(settings map[string]any) {
|
2026-05-14 11:05:20 +08:00
|
|
|
serverSettings := ensureMapSetting(settings, "server")
|
|
|
|
|
securityHeadersSettings := ensureMapSetting(serverSettings, "security_headers")
|
|
|
|
|
if _, ok := securityHeadersSettings["enabled"]; !ok {
|
|
|
|
|
securityHeadersSettings["enabled"] = true
|
|
|
|
|
}
|
2026-05-03 02:02:39 +08:00
|
|
|
cacheSettings := ensureMapSetting(settings, "cache")
|
|
|
|
|
if _, ok := cacheSettings["metrics_enabled"]; !ok {
|
|
|
|
|
cacheSettings["metrics_enabled"] = true
|
|
|
|
|
}
|
2026-05-05 20:48:14 +08:00
|
|
|
complianceSettings := ensureMapSetting(settings, "compliance")
|
|
|
|
|
if _, ok := complianceSettings["enabled"]; !ok {
|
|
|
|
|
complianceSettings["enabled"] = true
|
|
|
|
|
}
|
2026-05-12 12:32:39 +08:00
|
|
|
authSettings := ensureMapSetting(settings, "auth")
|
|
|
|
|
loginGuardSettings := ensureMapSetting(authSettings, "login_guard")
|
|
|
|
|
if _, ok := loginGuardSettings["enabled"]; !ok {
|
|
|
|
|
loginGuardSettings["enabled"] = true
|
|
|
|
|
}
|
2026-05-03 02:02:39 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func ensureMapSetting(settings map[string]any, key string) map[string]any {
|
|
|
|
|
if settings == nil {
|
|
|
|
|
return map[string]any{}
|
|
|
|
|
}
|
|
|
|
|
if existing, ok := settings[key].(map[string]any); ok {
|
|
|
|
|
return existing
|
|
|
|
|
}
|
|
|
|
|
next := make(map[string]any)
|
|
|
|
|
settings[key] = next
|
|
|
|
|
return next
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-05 23:01:25 +08:00
|
|
|
func DefaultTrustedProxies() []string {
|
|
|
|
|
return []string{
|
|
|
|
|
"127.0.0.1",
|
|
|
|
|
"::1",
|
|
|
|
|
"10.42.0.0/16",
|
|
|
|
|
"10.43.0.0/16",
|
|
|
|
|
"172.16.0.0/12",
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func NormalizeServerConfig(cfg *ServerConfig) {
|
|
|
|
|
if cfg == nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
cfg.Mode = strings.TrimSpace(cfg.Mode)
|
2026-05-14 11:05:20 +08:00
|
|
|
cfg.AllowedOrigins = compactStringSlice(cfg.AllowedOrigins)
|
2026-05-05 23:01:25 +08:00
|
|
|
if cfg.TrustedProxies == nil {
|
|
|
|
|
cfg.TrustedProxies = DefaultTrustedProxies()
|
2026-05-14 11:05:20 +08:00
|
|
|
} else {
|
|
|
|
|
cfg.TrustedProxies = compactStringSlice(cfg.TrustedProxies)
|
2026-05-05 23:01:25 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func compactStringSlice(values []string) []string {
|
|
|
|
|
if values == nil {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
out := make([]string, 0, len(values))
|
|
|
|
|
for _, value := range values {
|
|
|
|
|
value = strings.TrimSpace(value)
|
|
|
|
|
if value != "" {
|
|
|
|
|
out = append(out, value)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return out
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-11 11:11:21 +08:00
|
|
|
func compactIntSlice(values []int) []int {
|
|
|
|
|
if values == nil {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
out := make([]int, 0, len(values))
|
|
|
|
|
seen := make(map[int]struct{}, len(values))
|
|
|
|
|
for _, value := range values {
|
|
|
|
|
if value <= 0 {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if _, exists := seen[value]; exists {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
seen[value] = struct{}{}
|
|
|
|
|
out = append(out, value)
|
|
|
|
|
}
|
|
|
|
|
return out
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func parseIntList(value string) []int {
|
|
|
|
|
parts := strings.Split(value, ",")
|
|
|
|
|
out := make([]int, 0, len(parts))
|
|
|
|
|
for _, part := range parts {
|
|
|
|
|
part = strings.TrimSpace(part)
|
|
|
|
|
if part == "" {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
n, err := strconv.Atoi(part)
|
|
|
|
|
if err != nil {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
out = append(out, n)
|
|
|
|
|
}
|
|
|
|
|
return out
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-22 00:24:21 +08:00
|
|
|
func normalizeMonitoringDispatchConfig(cfg *MonitoringDispatchConfig) {
|
|
|
|
|
if cfg == nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if cfg.DesktopTasksRolloutPercentage < 0 {
|
|
|
|
|
cfg.DesktopTasksRolloutPercentage = 0
|
|
|
|
|
}
|
|
|
|
|
if cfg.DesktopTasksRolloutPercentage > 100 {
|
|
|
|
|
cfg.DesktopTasksRolloutPercentage = 100
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-03 02:02:39 +08:00
|
|
|
func NormalizeRedisConfig(cfg *RedisConfig) {
|
|
|
|
|
if cfg == nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
cfg.Addr = strings.TrimSpace(cfg.Addr)
|
|
|
|
|
if cfg.Addr == "" {
|
|
|
|
|
cfg.Addr = "localhost:6379"
|
|
|
|
|
}
|
|
|
|
|
if cfg.DialTimeout <= 0 {
|
|
|
|
|
cfg.DialTimeout = 5 * time.Second
|
|
|
|
|
}
|
|
|
|
|
if cfg.ReadTimeout <= 0 {
|
|
|
|
|
cfg.ReadTimeout = 3 * time.Second
|
|
|
|
|
}
|
|
|
|
|
if cfg.WriteTimeout <= 0 {
|
|
|
|
|
cfg.WriteTimeout = 3 * time.Second
|
|
|
|
|
}
|
|
|
|
|
if cfg.PoolSize <= 0 {
|
|
|
|
|
cfg.PoolSize = 16
|
|
|
|
|
}
|
|
|
|
|
if cfg.MinIdleConns < 0 {
|
|
|
|
|
cfg.MinIdleConns = 0
|
|
|
|
|
}
|
|
|
|
|
if cfg.MinIdleConns > cfg.PoolSize {
|
|
|
|
|
cfg.MinIdleConns = cfg.PoolSize
|
|
|
|
|
}
|
|
|
|
|
if cfg.MaxRetries < 0 {
|
|
|
|
|
cfg.MaxRetries = 0
|
|
|
|
|
}
|
|
|
|
|
if cfg.PoolTimeout <= 0 {
|
|
|
|
|
cfg.PoolTimeout = 4 * time.Second
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func NormalizeCacheConfig(cfg *CacheConfig) {
|
|
|
|
|
if cfg == nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
cfg.Driver = strings.ToLower(strings.TrimSpace(cfg.Driver))
|
|
|
|
|
if cfg.Driver == "" {
|
|
|
|
|
cfg.Driver = "redis"
|
|
|
|
|
}
|
|
|
|
|
if cfg.Driver != "redis" && cfg.Driver != "memory" {
|
|
|
|
|
cfg.Driver = "memory"
|
|
|
|
|
}
|
|
|
|
|
cfg.Namespace = strings.Trim(strings.TrimSpace(cfg.Namespace), ":")
|
|
|
|
|
if cfg.Namespace == "" {
|
|
|
|
|
cfg.Namespace = "geo"
|
|
|
|
|
}
|
|
|
|
|
if cfg.JitterRatio <= 0 {
|
|
|
|
|
cfg.JitterRatio = 0.1
|
|
|
|
|
}
|
|
|
|
|
if cfg.JitterRatio > 1 {
|
|
|
|
|
cfg.JitterRatio = 1
|
|
|
|
|
}
|
|
|
|
|
if cfg.AsyncFillWorkers <= 0 {
|
|
|
|
|
cfg.AsyncFillWorkers = 2
|
|
|
|
|
}
|
|
|
|
|
if cfg.AsyncFillBuffer <= 0 {
|
|
|
|
|
cfg.AsyncFillBuffer = 1024
|
|
|
|
|
}
|
|
|
|
|
if cfg.AsyncFillTimeout <= 0 {
|
|
|
|
|
cfg.AsyncFillTimeout = 2 * time.Second
|
|
|
|
|
}
|
|
|
|
|
if cfg.L1TTL <= 0 {
|
|
|
|
|
cfg.L1TTL = 30 * time.Second
|
|
|
|
|
}
|
|
|
|
|
if cfg.DeleteScanCount <= 0 {
|
|
|
|
|
cfg.DeleteScanCount = 500
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func NormalizeGenerationConfig(cfg *GenerationConfig) {
|
|
|
|
|
if cfg == nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if cfg.QueueSize <= 0 {
|
|
|
|
|
cfg.QueueSize = 128
|
|
|
|
|
}
|
|
|
|
|
if cfg.WorkerConcurrency <= 0 {
|
|
|
|
|
cfg.WorkerConcurrency = 1
|
|
|
|
|
}
|
|
|
|
|
if cfg.ArticleTimeout <= 0 {
|
|
|
|
|
cfg.ArticleTimeout = 8 * time.Minute
|
|
|
|
|
}
|
|
|
|
|
if cfg.TaskLeaseTTL <= 0 {
|
|
|
|
|
cfg.TaskLeaseTTL = cfg.ArticleTimeout + 2*time.Minute
|
|
|
|
|
}
|
|
|
|
|
if cfg.TaskLeaseTTL < time.Minute {
|
|
|
|
|
cfg.TaskLeaseTTL = time.Minute
|
|
|
|
|
}
|
|
|
|
|
if cfg.TaskRecoveryInterval <= 0 {
|
|
|
|
|
cfg.TaskRecoveryInterval = time.Minute
|
|
|
|
|
}
|
|
|
|
|
if cfg.TaskRecoveryTimeout <= 0 {
|
|
|
|
|
cfg.TaskRecoveryTimeout = 30 * time.Second
|
|
|
|
|
}
|
|
|
|
|
if cfg.TaskRecoveryBatchSize <= 0 {
|
|
|
|
|
cfg.TaskRecoveryBatchSize = 100
|
|
|
|
|
}
|
|
|
|
|
if cfg.TaskQueuedStaleAfter <= 0 {
|
|
|
|
|
cfg.TaskQueuedStaleAfter = 2 * time.Minute
|
|
|
|
|
}
|
|
|
|
|
if cfg.TaskMaxAttempts <= 0 {
|
|
|
|
|
cfg.TaskMaxAttempts = 3
|
|
|
|
|
}
|
2026-05-05 23:43:10 +08:00
|
|
|
if cfg.TaskStateCheckInterval <= 0 {
|
|
|
|
|
cfg.TaskStateCheckInterval = 5 * time.Minute
|
|
|
|
|
}
|
|
|
|
|
if cfg.TaskStateCheckTimeout <= 0 {
|
|
|
|
|
cfg.TaskStateCheckTimeout = 10 * time.Second
|
|
|
|
|
}
|
|
|
|
|
if cfg.TaskStateCheckBatchSize <= 0 {
|
|
|
|
|
cfg.TaskStateCheckBatchSize = 100
|
|
|
|
|
}
|
|
|
|
|
if cfg.TaskStateCheckLookback <= 0 {
|
|
|
|
|
cfg.TaskStateCheckLookback = 24 * time.Hour
|
|
|
|
|
}
|
2026-05-03 02:02:39 +08:00
|
|
|
}
|
|
|
|
|
|
2026-05-11 11:11:21 +08:00
|
|
|
func NormalizeBrowserFetchConfig(cfg *BrowserFetchConfig) {
|
|
|
|
|
if cfg == nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
cfg.Provider = strings.ToLower(strings.TrimSpace(cfg.Provider))
|
|
|
|
|
if cfg.Provider == "" {
|
|
|
|
|
cfg.Provider = "lightpanda"
|
|
|
|
|
}
|
|
|
|
|
cfg.ServiceURL = strings.TrimRight(strings.TrimSpace(cfg.ServiceURL), "/")
|
|
|
|
|
cfg.Token = strings.TrimSpace(cfg.Token)
|
|
|
|
|
if cfg.QueueSize <= 0 {
|
|
|
|
|
cfg.QueueSize = 64
|
|
|
|
|
}
|
|
|
|
|
if cfg.WorkerConcurrency <= 0 {
|
|
|
|
|
cfg.WorkerConcurrency = 1
|
|
|
|
|
}
|
|
|
|
|
if cfg.RequestTimeout <= 0 {
|
|
|
|
|
cfg.RequestTimeout = 45 * time.Second
|
|
|
|
|
}
|
|
|
|
|
if cfg.CacheTTL <= 0 {
|
|
|
|
|
cfg.CacheTTL = 6 * time.Hour
|
|
|
|
|
}
|
|
|
|
|
cfg.AllowedDomains = compactStringSlice(cfg.AllowedDomains)
|
|
|
|
|
cfg.TriggerStatus = compactIntSlice(cfg.TriggerStatus)
|
|
|
|
|
if len(cfg.TriggerStatus) == 0 {
|
|
|
|
|
cfg.TriggerStatus = []int{403, 429, 500, 502, 503, 504}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-29 23:17:01 +08:00
|
|
|
func NormalizeMediaSupplyConfig(cfg *MediaSupplyConfig) {
|
|
|
|
|
if cfg == nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if cfg.DefaultMarkupPercent < 0 {
|
|
|
|
|
cfg.DefaultMarkupPercent = 0
|
|
|
|
|
}
|
|
|
|
|
if cfg.MinimumMarkupCents < 0 {
|
|
|
|
|
cfg.MinimumMarkupCents = 0
|
|
|
|
|
}
|
|
|
|
|
if cfg.Worker.PollInterval <= 0 {
|
|
|
|
|
cfg.Worker.PollInterval = 5 * time.Second
|
|
|
|
|
}
|
|
|
|
|
if cfg.Worker.BatchSize <= 0 {
|
|
|
|
|
cfg.Worker.BatchSize = 1
|
|
|
|
|
}
|
|
|
|
|
if cfg.Worker.BatchSize > 10 {
|
|
|
|
|
cfg.Worker.BatchSize = 10
|
|
|
|
|
}
|
|
|
|
|
if cfg.Worker.OrderMaxAttempts <= 0 {
|
|
|
|
|
cfg.Worker.OrderMaxAttempts = 3
|
|
|
|
|
}
|
|
|
|
|
if cfg.Worker.SyncMaxAttempts <= 0 {
|
|
|
|
|
cfg.Worker.SyncMaxAttempts = 2
|
|
|
|
|
}
|
|
|
|
|
if cfg.Worker.BacklinkSyncInterval <= 0 {
|
2026-06-02 14:50:36 +08:00
|
|
|
cfg.Worker.BacklinkSyncInterval = 30 * time.Minute
|
2026-05-29 23:17:01 +08:00
|
|
|
}
|
|
|
|
|
if cfg.Worker.BacklinkBatchSize <= 0 {
|
|
|
|
|
cfg.Worker.BacklinkBatchSize = 20
|
|
|
|
|
}
|
|
|
|
|
if cfg.Worker.BacklinkBatchSize > 100 {
|
|
|
|
|
cfg.Worker.BacklinkBatchSize = 100
|
|
|
|
|
}
|
|
|
|
|
cfg.Meijiequan.BaseURL = strings.TrimRight(strings.TrimSpace(cfg.Meijiequan.BaseURL), "/")
|
|
|
|
|
if cfg.Meijiequan.BaseURL == "" {
|
|
|
|
|
cfg.Meijiequan.BaseURL = "http://www.meijiequan.com"
|
|
|
|
|
}
|
|
|
|
|
cfg.Meijiequan.UserID = strings.TrimSpace(cfg.Meijiequan.UserID)
|
|
|
|
|
cfg.Meijiequan.Username = strings.TrimSpace(cfg.Meijiequan.Username)
|
|
|
|
|
cfg.Meijiequan.Password = strings.TrimSpace(cfg.Meijiequan.Password)
|
|
|
|
|
if cfg.Meijiequan.SessionTTL <= 0 {
|
|
|
|
|
cfg.Meijiequan.SessionTTL = 4 * time.Hour
|
|
|
|
|
}
|
|
|
|
|
if cfg.Meijiequan.RequestTimeout <= 0 {
|
|
|
|
|
cfg.Meijiequan.RequestTimeout = 30 * time.Second
|
|
|
|
|
}
|
|
|
|
|
if cfg.Meijiequan.SyncPageDelay <= 0 {
|
|
|
|
|
cfg.Meijiequan.SyncPageDelay = 800 * time.Millisecond
|
|
|
|
|
}
|
|
|
|
|
if cfg.Meijiequan.SyncPageSize <= 0 {
|
|
|
|
|
cfg.Meijiequan.SyncPageSize = 100
|
|
|
|
|
}
|
|
|
|
|
if cfg.Meijiequan.SyncPageSize > 500 {
|
|
|
|
|
cfg.Meijiequan.SyncPageSize = 500
|
|
|
|
|
}
|
|
|
|
|
if cfg.Meijiequan.MaxSyncPages <= 0 {
|
|
|
|
|
cfg.Meijiequan.MaxSyncPages = 200
|
|
|
|
|
}
|
|
|
|
|
if cfg.Meijiequan.PublishedPageSize <= 0 {
|
|
|
|
|
cfg.Meijiequan.PublishedPageSize = 20
|
|
|
|
|
}
|
|
|
|
|
if cfg.Meijiequan.PublishedPageSize > 100 {
|
|
|
|
|
cfg.Meijiequan.PublishedPageSize = 100
|
|
|
|
|
}
|
|
|
|
|
if cfg.Meijiequan.PublishedMaxPages <= 0 {
|
|
|
|
|
cfg.Meijiequan.PublishedMaxPages = 3
|
|
|
|
|
}
|
|
|
|
|
if cfg.Meijiequan.PublishedMaxPages > 20 {
|
|
|
|
|
cfg.Meijiequan.PublishedMaxPages = 20
|
|
|
|
|
}
|
|
|
|
|
if cfg.Meijiequan.SearchOptionsTTL <= 0 {
|
|
|
|
|
cfg.Meijiequan.SearchOptionsTTL = 12 * time.Hour
|
|
|
|
|
}
|
|
|
|
|
if cfg.Meijiequan.UpstreamLockTTL <= 0 {
|
|
|
|
|
cfg.Meijiequan.UpstreamLockTTL = 2 * time.Minute
|
|
|
|
|
}
|
|
|
|
|
if cfg.Meijiequan.UpstreamMinInterval <= 0 {
|
|
|
|
|
cfg.Meijiequan.UpstreamMinInterval = cfg.Meijiequan.SyncPageDelay
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-05 20:48:14 +08:00
|
|
|
func NormalizeComplianceConfig(cfg *ComplianceConfig) {
|
|
|
|
|
if cfg == nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if cfg.SnapshotReloadInterval <= 0 {
|
|
|
|
|
cfg.SnapshotReloadInterval = 30 * time.Second
|
|
|
|
|
}
|
|
|
|
|
if cfg.PublishGateTimeout <= 0 {
|
|
|
|
|
cfg.PublishGateTimeout = 800 * time.Millisecond
|
|
|
|
|
}
|
|
|
|
|
if cfg.ReviewWorkerConcurrency <= 0 {
|
|
|
|
|
cfg.ReviewWorkerConcurrency = 1
|
|
|
|
|
}
|
|
|
|
|
if cfg.ReviewWorkerTimeout <= 0 {
|
|
|
|
|
cfg.ReviewWorkerTimeout = 45 * time.Second
|
|
|
|
|
}
|
|
|
|
|
if cfg.ReviewMaxAttempts <= 0 {
|
|
|
|
|
cfg.ReviewMaxAttempts = 3
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-18 20:56:05 +08:00
|
|
|
func candidateConfigPaths(configPath string, local bool) []string {
|
|
|
|
|
trimmed := strings.TrimSpace(configPath)
|
|
|
|
|
if trimmed == "" {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
paths := make([]string, 0, 2)
|
|
|
|
|
switch {
|
|
|
|
|
case strings.HasSuffix(trimmed, ".yaml"):
|
|
|
|
|
if local {
|
|
|
|
|
paths = append(paths, strings.TrimSuffix(trimmed, ".yaml")+".local.yaml")
|
|
|
|
|
} else {
|
|
|
|
|
paths = append(paths, trimmed)
|
|
|
|
|
}
|
|
|
|
|
alt := strings.TrimSuffix(trimmed, ".yaml")
|
|
|
|
|
if local {
|
|
|
|
|
paths = append(paths, alt+".local.yml")
|
|
|
|
|
} else {
|
|
|
|
|
paths = append(paths, alt+".yml")
|
|
|
|
|
}
|
|
|
|
|
case strings.HasSuffix(trimmed, ".yml"):
|
|
|
|
|
if local {
|
|
|
|
|
paths = append(paths, strings.TrimSuffix(trimmed, ".yml")+".local.yml")
|
|
|
|
|
} else {
|
|
|
|
|
paths = append(paths, trimmed)
|
|
|
|
|
}
|
|
|
|
|
alt := strings.TrimSuffix(trimmed, ".yml")
|
|
|
|
|
if local {
|
|
|
|
|
paths = append(paths, alt+".local.yaml")
|
|
|
|
|
} else {
|
|
|
|
|
paths = append(paths, alt+".yaml")
|
|
|
|
|
}
|
|
|
|
|
default:
|
|
|
|
|
if local {
|
|
|
|
|
paths = append(paths, trimmed+".local.yaml", trimmed+".local.yml")
|
|
|
|
|
} else {
|
|
|
|
|
paths = append(paths, trimmed, trimmed+".yaml", trimmed+".yml")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return paths
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-01 00:58:42 +08:00
|
|
|
func applyEnvOverrides(cfg *Config) {
|
|
|
|
|
if cfg == nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-25 18:09:01 +08:00
|
|
|
if secret, ok := lookupNonEmptyEnv("JWT_SECRET"); ok {
|
|
|
|
|
cfg.JWT.Secret = secret
|
|
|
|
|
}
|
|
|
|
|
if ttl, ok := lookupDurationEnv("JWT_ACCESS_TTL"); ok {
|
|
|
|
|
cfg.JWT.AccessTTL = ttl
|
|
|
|
|
}
|
|
|
|
|
if ttl, ok := lookupDurationEnv("JWT_REFRESH_TTL"); ok {
|
|
|
|
|
cfg.JWT.RefreshTTL = ttl
|
|
|
|
|
}
|
2026-05-12 12:32:39 +08:00
|
|
|
if enabled, ok := lookupBoolEnv("AUTH_LOGIN_GUARD_ENABLED"); ok {
|
|
|
|
|
cfg.Auth.LoginGuard.Enabled = enabled
|
|
|
|
|
}
|
2026-04-01 00:58:42 +08:00
|
|
|
if apiKey, ok := lookupNonEmptyEnv("LLM_API_KEY"); ok {
|
|
|
|
|
cfg.LLM.APIKey = apiKey
|
|
|
|
|
}
|
2026-04-05 20:41:42 +08:00
|
|
|
if model, ok := lookupNonEmptyEnv("LLM_KNOWLEDGE_URL_MODEL"); ok {
|
|
|
|
|
cfg.LLM.KnowledgeURLModel = model
|
|
|
|
|
}
|
2026-05-05 20:48:14 +08:00
|
|
|
if enabled, ok := lookupBoolEnv("COMPLIANCE_ENABLED"); ok {
|
|
|
|
|
cfg.Compliance.Enabled = enabled
|
|
|
|
|
}
|
|
|
|
|
if enabled, ok := lookupBoolEnv("COMPLIANCE_LLM_ENABLED"); ok {
|
|
|
|
|
cfg.Compliance.LLMJudgeEnabled = enabled
|
|
|
|
|
}
|
|
|
|
|
if d, ok := lookupDurationEnv("COMPLIANCE_SNAPSHOT_RELOAD_INTERVAL"); ok {
|
|
|
|
|
cfg.Compliance.SnapshotReloadInterval = d
|
|
|
|
|
}
|
|
|
|
|
if d, ok := lookupDurationEnv("COMPLIANCE_PUBLISH_GATE_TIMEOUT"); ok {
|
|
|
|
|
cfg.Compliance.PublishGateTimeout = d
|
|
|
|
|
}
|
|
|
|
|
if n, ok := lookupIntEnv("COMPLIANCE_REVIEW_WORKER_CONCURRENCY"); ok {
|
|
|
|
|
cfg.Compliance.ReviewWorkerConcurrency = n
|
|
|
|
|
}
|
|
|
|
|
if d, ok := lookupDurationEnv("COMPLIANCE_REVIEW_WORKER_TIMEOUT"); ok {
|
|
|
|
|
cfg.Compliance.ReviewWorkerTimeout = d
|
|
|
|
|
}
|
|
|
|
|
if n, ok := lookupIntEnv("COMPLIANCE_REVIEW_MAX_ATTEMPTS"); ok {
|
|
|
|
|
cfg.Compliance.ReviewMaxAttempts = n
|
|
|
|
|
}
|
2026-05-05 23:43:10 +08:00
|
|
|
if d, ok := lookupDurationEnv("GENERATION_TASK_STATE_CHECK_INTERVAL"); ok {
|
|
|
|
|
cfg.Generation.TaskStateCheckInterval = d
|
|
|
|
|
}
|
|
|
|
|
if d, ok := lookupDurationEnv("GENERATION_TASK_STATE_CHECK_TIMEOUT"); ok {
|
|
|
|
|
cfg.Generation.TaskStateCheckTimeout = d
|
|
|
|
|
}
|
|
|
|
|
if n, ok := lookupIntEnv("GENERATION_TASK_STATE_CHECK_BATCH_SIZE"); ok {
|
|
|
|
|
cfg.Generation.TaskStateCheckBatchSize = n
|
|
|
|
|
}
|
|
|
|
|
if d, ok := lookupDurationEnv("GENERATION_TASK_STATE_CHECK_LOOKBACK"); ok {
|
|
|
|
|
cfg.Generation.TaskStateCheckLookback = d
|
|
|
|
|
}
|
2026-05-11 11:11:21 +08:00
|
|
|
if enabled, ok := lookupBoolEnv("BROWSER_FETCH_ENABLED"); ok {
|
|
|
|
|
cfg.BrowserFetch.Enabled = enabled
|
|
|
|
|
}
|
|
|
|
|
if provider, ok := lookupNonEmptyEnv("BROWSER_FETCH_PROVIDER"); ok {
|
|
|
|
|
cfg.BrowserFetch.Provider = provider
|
|
|
|
|
}
|
|
|
|
|
if serviceURL, ok := lookupFirstNonEmptyEnv("BROWSER_FETCH_SERVICE_URL", "LIGHTPANDA_BROWSER_FETCH_URL"); ok {
|
|
|
|
|
cfg.BrowserFetch.ServiceURL = serviceURL
|
|
|
|
|
}
|
|
|
|
|
if token, ok := lookupNonEmptyEnv("BROWSER_FETCH_TOKEN"); ok {
|
|
|
|
|
cfg.BrowserFetch.Token = token
|
|
|
|
|
}
|
|
|
|
|
if n, ok := lookupIntEnv("BROWSER_FETCH_QUEUE_SIZE"); ok {
|
|
|
|
|
cfg.BrowserFetch.QueueSize = n
|
|
|
|
|
}
|
|
|
|
|
if n, ok := lookupIntEnv("BROWSER_FETCH_WORKER_CONCURRENCY"); ok {
|
|
|
|
|
cfg.BrowserFetch.WorkerConcurrency = n
|
|
|
|
|
}
|
|
|
|
|
if d, ok := lookupDurationEnv("BROWSER_FETCH_REQUEST_TIMEOUT"); ok {
|
|
|
|
|
cfg.BrowserFetch.RequestTimeout = d
|
|
|
|
|
}
|
|
|
|
|
if d, ok := lookupDurationEnv("BROWSER_FETCH_CACHE_TTL"); ok {
|
|
|
|
|
cfg.BrowserFetch.CacheTTL = d
|
|
|
|
|
}
|
|
|
|
|
if domains, ok := lookupNonEmptyEnv("BROWSER_FETCH_ALLOWED_DOMAINS"); ok {
|
|
|
|
|
cfg.BrowserFetch.AllowedDomains = strings.Split(domains, ",")
|
|
|
|
|
}
|
|
|
|
|
if statuses, ok := lookupNonEmptyEnv("BROWSER_FETCH_TRIGGER_STATUS"); ok {
|
|
|
|
|
cfg.BrowserFetch.TriggerStatus = parseIntList(statuses)
|
|
|
|
|
}
|
2026-05-29 23:17:01 +08:00
|
|
|
if enabled, ok := lookupBoolEnv("MEDIA_SUPPLY_ENABLED"); ok {
|
|
|
|
|
cfg.MediaSupply.Enabled = enabled
|
|
|
|
|
}
|
|
|
|
|
if markup, ok := lookupFloatEnv("MEDIA_SUPPLY_DEFAULT_MARKUP_PERCENT"); ok {
|
|
|
|
|
cfg.MediaSupply.DefaultMarkupPercent = markup
|
|
|
|
|
}
|
|
|
|
|
if cents, ok := lookupInt64Env("MEDIA_SUPPLY_MINIMUM_MARKUP_CENTS"); ok {
|
|
|
|
|
cfg.MediaSupply.MinimumMarkupCents = cents
|
|
|
|
|
}
|
|
|
|
|
if enabled, ok := lookupBoolEnv("MEDIA_SUPPLY_WORKER_ENABLED"); ok {
|
|
|
|
|
cfg.MediaSupply.Worker.Enabled = enabled
|
|
|
|
|
}
|
|
|
|
|
if interval, ok := lookupDurationEnv("MEDIA_SUPPLY_WORKER_POLL_INTERVAL"); ok {
|
|
|
|
|
cfg.MediaSupply.Worker.PollInterval = interval
|
|
|
|
|
}
|
|
|
|
|
if n, ok := lookupIntEnv("MEDIA_SUPPLY_WORKER_BATCH_SIZE"); ok {
|
|
|
|
|
cfg.MediaSupply.Worker.BatchSize = n
|
|
|
|
|
}
|
|
|
|
|
if n, ok := lookupIntEnv("MEDIA_SUPPLY_ORDER_MAX_ATTEMPTS"); ok {
|
|
|
|
|
cfg.MediaSupply.Worker.OrderMaxAttempts = n
|
|
|
|
|
}
|
|
|
|
|
if n, ok := lookupIntEnv("MEDIA_SUPPLY_SYNC_MAX_ATTEMPTS"); ok {
|
|
|
|
|
cfg.MediaSupply.Worker.SyncMaxAttempts = n
|
|
|
|
|
}
|
|
|
|
|
if interval, ok := lookupDurationEnv("MEDIA_SUPPLY_BACKLINK_SYNC_INTERVAL"); ok {
|
|
|
|
|
cfg.MediaSupply.Worker.BacklinkSyncInterval = interval
|
|
|
|
|
}
|
|
|
|
|
if n, ok := lookupIntEnv("MEDIA_SUPPLY_BACKLINK_BATCH_SIZE"); ok {
|
|
|
|
|
cfg.MediaSupply.Worker.BacklinkBatchSize = n
|
|
|
|
|
}
|
|
|
|
|
if baseURL, ok := lookupNonEmptyEnv("MEIJIEQUAN_BASE_URL"); ok {
|
|
|
|
|
cfg.MediaSupply.Meijiequan.BaseURL = baseURL
|
|
|
|
|
}
|
|
|
|
|
if userID, ok := lookupNonEmptyEnv("MEIJIEQUAN_USER_ID"); ok {
|
|
|
|
|
cfg.MediaSupply.Meijiequan.UserID = userID
|
|
|
|
|
}
|
|
|
|
|
if username, ok := lookupNonEmptyEnv("MEIJIEQUAN_USERNAME"); ok {
|
|
|
|
|
cfg.MediaSupply.Meijiequan.Username = username
|
|
|
|
|
}
|
|
|
|
|
if password, ok := lookupNonEmptyEnv("MEIJIEQUAN_PASSWORD"); ok {
|
|
|
|
|
cfg.MediaSupply.Meijiequan.Password = password
|
|
|
|
|
}
|
|
|
|
|
if ttl, ok := lookupDurationEnv("MEIJIEQUAN_SESSION_TTL"); ok {
|
|
|
|
|
cfg.MediaSupply.Meijiequan.SessionTTL = ttl
|
|
|
|
|
}
|
|
|
|
|
if timeout, ok := lookupDurationEnv("MEIJIEQUAN_REQUEST_TIMEOUT"); ok {
|
|
|
|
|
cfg.MediaSupply.Meijiequan.RequestTimeout = timeout
|
|
|
|
|
}
|
|
|
|
|
if delay, ok := lookupDurationEnv("MEIJIEQUAN_SYNC_PAGE_DELAY"); ok {
|
|
|
|
|
cfg.MediaSupply.Meijiequan.SyncPageDelay = delay
|
|
|
|
|
}
|
|
|
|
|
if n, ok := lookupIntEnv("MEIJIEQUAN_SYNC_PAGE_SIZE"); ok {
|
|
|
|
|
cfg.MediaSupply.Meijiequan.SyncPageSize = n
|
|
|
|
|
}
|
|
|
|
|
if n, ok := lookupIntEnv("MEIJIEQUAN_MAX_SYNC_PAGES"); ok {
|
|
|
|
|
cfg.MediaSupply.Meijiequan.MaxSyncPages = n
|
|
|
|
|
}
|
|
|
|
|
if n, ok := lookupIntEnv("MEIJIEQUAN_PUBLISHED_PAGE_SIZE"); ok {
|
|
|
|
|
cfg.MediaSupply.Meijiequan.PublishedPageSize = n
|
|
|
|
|
}
|
|
|
|
|
if n, ok := lookupIntEnv("MEIJIEQUAN_PUBLISHED_MAX_PAGES"); ok {
|
|
|
|
|
cfg.MediaSupply.Meijiequan.PublishedMaxPages = n
|
|
|
|
|
}
|
|
|
|
|
if ttl, ok := lookupDurationEnv("MEIJIEQUAN_SEARCH_OPTIONS_TTL"); ok {
|
|
|
|
|
cfg.MediaSupply.Meijiequan.SearchOptionsTTL = ttl
|
|
|
|
|
}
|
|
|
|
|
if ttl, ok := lookupDurationEnv("MEIJIEQUAN_UPSTREAM_LOCK_TTL"); ok {
|
|
|
|
|
cfg.MediaSupply.Meijiequan.UpstreamLockTTL = ttl
|
|
|
|
|
}
|
|
|
|
|
if interval, ok := lookupDurationEnv("MEIJIEQUAN_UPSTREAM_MIN_INTERVAL"); ok {
|
|
|
|
|
cfg.MediaSupply.Meijiequan.UpstreamMinInterval = interval
|
|
|
|
|
}
|
2026-04-05 17:14:13 +08:00
|
|
|
if apiKey, ok := lookupNonEmptyEnv("QDRANT_API_KEY"); ok {
|
|
|
|
|
cfg.Qdrant.APIKey = apiKey
|
|
|
|
|
}
|
2026-04-12 09:56:18 +08:00
|
|
|
if rabbitmqURL, ok := lookupNonEmptyEnv("RABBITMQ_URL"); ok {
|
|
|
|
|
cfg.RabbitMQ.URL = rabbitmqURL
|
|
|
|
|
}
|
2026-04-05 17:14:13 +08:00
|
|
|
if url, ok := lookupNonEmptyEnv("QDRANT_URL"); ok {
|
|
|
|
|
cfg.Qdrant.URL = url
|
|
|
|
|
}
|
|
|
|
|
if collection, ok := lookupNonEmptyEnv("QDRANT_COLLECTION"); ok {
|
|
|
|
|
cfg.Qdrant.Collection = collection
|
|
|
|
|
}
|
|
|
|
|
if endpoint, ok := lookupNonEmptyEnv("OBJECT_STORAGE_ENDPOINT"); ok {
|
|
|
|
|
cfg.ObjectStorage.Endpoint = endpoint
|
|
|
|
|
}
|
|
|
|
|
if accessKey, ok := lookupNonEmptyEnv("OBJECT_STORAGE_ACCESS_KEY"); ok {
|
|
|
|
|
cfg.ObjectStorage.AccessKey = accessKey
|
|
|
|
|
}
|
|
|
|
|
if secretKey, ok := lookupNonEmptyEnv("OBJECT_STORAGE_SECRET_KEY"); ok {
|
|
|
|
|
cfg.ObjectStorage.SecretKey = secretKey
|
|
|
|
|
}
|
|
|
|
|
if bucket, ok := lookupNonEmptyEnv("OBJECT_STORAGE_BUCKET"); ok {
|
|
|
|
|
cfg.ObjectStorage.Bucket = bucket
|
|
|
|
|
}
|
|
|
|
|
if provider, ok := lookupNonEmptyEnv("OBJECT_STORAGE_PROVIDER"); ok {
|
|
|
|
|
cfg.ObjectStorage.Provider = provider
|
|
|
|
|
}
|
|
|
|
|
if publicBaseURL, ok := lookupNonEmptyEnv("OBJECT_STORAGE_PUBLIC_BASE_URL"); ok {
|
|
|
|
|
cfg.ObjectStorage.PublicBaseURL = publicBaseURL
|
|
|
|
|
}
|
2026-05-25 19:23:49 +08:00
|
|
|
if bucketACL, ok := lookupNonEmptyEnv("OBJECT_STORAGE_BUCKET_ACL"); ok {
|
|
|
|
|
cfg.ObjectStorage.BucketACL = bucketACL
|
|
|
|
|
}
|
|
|
|
|
if objectACL, ok := lookupNonEmptyEnv("OBJECT_STORAGE_OBJECT_ACL"); ok {
|
|
|
|
|
cfg.ObjectStorage.ObjectACL = objectACL
|
|
|
|
|
}
|
|
|
|
|
if ttl, ok := lookupDurationEnv("OBJECT_STORAGE_SIGNED_URL_TTL"); ok {
|
|
|
|
|
cfg.ObjectStorage.SignedURLTTL = ttl
|
|
|
|
|
}
|
2026-04-05 22:10:05 +08:00
|
|
|
if region, ok := lookupNonEmptyEnv("OBJECT_STORAGE_REGION"); ok {
|
|
|
|
|
cfg.ObjectStorage.Region = region
|
|
|
|
|
}
|
2026-05-26 01:19:01 +08:00
|
|
|
if forcePathStyle, ok := lookupBoolEnv("OBJECT_STORAGE_FORCE_PATH_STYLE"); ok {
|
|
|
|
|
cfg.ObjectStorage.ForcePathStyle = forcePathStyle
|
|
|
|
|
}
|
2026-04-05 22:10:05 +08:00
|
|
|
if useSSL, ok := lookupBoolEnv("OBJECT_STORAGE_USE_SSL"); ok {
|
|
|
|
|
cfg.ObjectStorage.UseSSL = useSSL
|
|
|
|
|
}
|
2026-04-05 17:14:13 +08:00
|
|
|
if apiKey, ok := lookupFirstNonEmptyEnv("SILICONFLOW_API_KEY", "RETRIEVAL_API_KEY", "EMBEDDING_API_KEY", "RERANKER_API_KEY"); ok {
|
|
|
|
|
cfg.Retrieval.APIKey = apiKey
|
|
|
|
|
}
|
|
|
|
|
if baseURL, ok := lookupFirstNonEmptyEnv("SILICONFLOW_BASE_URL", "RETRIEVAL_BASE_URL"); ok {
|
|
|
|
|
cfg.Retrieval.BaseURL = baseURL
|
|
|
|
|
}
|
|
|
|
|
if model, ok := lookupFirstNonEmptyEnv("SILICONFLOW_EMBEDDING_MODEL", "RETRIEVAL_EMBEDDING_MODEL"); ok {
|
|
|
|
|
cfg.Retrieval.EmbeddingModel = model
|
|
|
|
|
}
|
|
|
|
|
if model, ok := lookupFirstNonEmptyEnv("SILICONFLOW_RERANKER_MODEL", "RETRIEVAL_RERANKER_MODEL"); ok {
|
|
|
|
|
cfg.Retrieval.RerankerModel = model
|
|
|
|
|
}
|
2026-04-24 22:19:57 +08:00
|
|
|
if host, ok := lookupNonEmptyEnv("SCHEDULER_HTTP_HOST"); ok {
|
|
|
|
|
cfg.Scheduler.HTTPHost = host
|
|
|
|
|
}
|
|
|
|
|
if token, ok := lookupFirstNonEmptyEnv("SCHEDULER_INTERNAL_METRICS_TOKEN", "SCHEDULER_METRICS_TOKEN"); ok {
|
|
|
|
|
cfg.Scheduler.InternalMetricsToken = token
|
|
|
|
|
}
|
2026-05-05 23:01:25 +08:00
|
|
|
if trustedProxies, ok := lookupNonEmptyEnv("SERVER_TRUSTED_PROXIES"); ok {
|
|
|
|
|
cfg.Server.TrustedProxies = strings.Split(trustedProxies, ",")
|
|
|
|
|
}
|
2026-05-14 11:05:20 +08:00
|
|
|
if origins, ok := lookupNonEmptyEnv("SERVER_ALLOWED_ORIGINS"); ok {
|
|
|
|
|
cfg.Server.AllowedOrigins = strings.Split(origins, ",")
|
|
|
|
|
}
|
2026-06-06 13:06:14 +08:00
|
|
|
if publicBaseURL, ok := lookupNonEmptyEnv("SERVER_PUBLIC_BASE_URL"); ok {
|
|
|
|
|
cfg.Server.PublicBaseURL = publicBaseURL
|
|
|
|
|
}
|
2026-05-14 11:05:20 +08:00
|
|
|
if enabled, ok := lookupBoolEnv("SERVER_SECURITY_HEADERS_ENABLED"); ok {
|
|
|
|
|
cfg.Server.SecurityHeaders.Enabled = enabled
|
|
|
|
|
}
|
|
|
|
|
if enabled, ok := lookupBoolEnv("AUTH_PASSWORD_CIPHER_ENABLED"); ok {
|
|
|
|
|
cfg.Auth.PasswordCipher.Enabled = enabled
|
|
|
|
|
}
|
|
|
|
|
if keyID, ok := lookupNonEmptyEnv("AUTH_PASSWORD_CIPHER_KEY_ID"); ok {
|
|
|
|
|
cfg.Auth.PasswordCipher.KeyID = keyID
|
|
|
|
|
}
|
|
|
|
|
if privateKey, ok := lookupNonEmptyEnv("AUTH_PASSWORD_CIPHER_PRIVATE_KEY_PEM"); ok {
|
|
|
|
|
cfg.Auth.PasswordCipher.PrivateKeyPEM = privateKey
|
|
|
|
|
}
|
2026-05-13 15:59:39 +08:00
|
|
|
if path, ok := lookupNonEmptyEnv("IP_REGION_V4_XDB_PATH"); ok {
|
|
|
|
|
cfg.IPRegion.V4XDBPath = path
|
|
|
|
|
}
|
|
|
|
|
if path, ok := lookupNonEmptyEnv("IP_REGION_V6_XDB_PATH"); ok {
|
|
|
|
|
cfg.IPRegion.V6XDBPath = path
|
|
|
|
|
}
|
2026-04-05 17:14:13 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-12 09:56:18 +08:00
|
|
|
func normalizeRabbitMQConfig(cfg *RabbitMQConfig) {
|
|
|
|
|
if cfg == nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if strings.TrimSpace(cfg.MonitorResultExchange) == "" {
|
|
|
|
|
cfg.MonitorResultExchange = "monitor.result"
|
|
|
|
|
}
|
|
|
|
|
if strings.TrimSpace(cfg.MonitorResultRoutingKey) == "" {
|
|
|
|
|
cfg.MonitorResultRoutingKey = "monitor.result.ingest"
|
|
|
|
|
}
|
|
|
|
|
if strings.TrimSpace(cfg.MonitorResultQueue) == "" {
|
|
|
|
|
cfg.MonitorResultQueue = "monitor.result.ingest"
|
|
|
|
|
}
|
|
|
|
|
if strings.TrimSpace(cfg.MonitorResultDLX) == "" {
|
|
|
|
|
cfg.MonitorResultDLX = "monitor.result.dlx"
|
|
|
|
|
}
|
|
|
|
|
if strings.TrimSpace(cfg.MonitorResultDLQ) == "" {
|
|
|
|
|
cfg.MonitorResultDLQ = "monitor.result.ingest.dlq"
|
|
|
|
|
}
|
|
|
|
|
if strings.TrimSpace(cfg.MonitorResultDLQRouteKey) == "" {
|
|
|
|
|
cfg.MonitorResultDLQRouteKey = "monitor.result.ingest.dlq"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if strings.TrimSpace(cfg.MonitorProjectionExchange) == "" {
|
|
|
|
|
cfg.MonitorProjectionExchange = "monitor.projection"
|
|
|
|
|
}
|
|
|
|
|
if strings.TrimSpace(cfg.MonitorProjectionRoutingKey) == "" {
|
|
|
|
|
cfg.MonitorProjectionRoutingKey = "monitor.projection.rebuild"
|
|
|
|
|
}
|
|
|
|
|
if strings.TrimSpace(cfg.MonitorProjectionQueue) == "" {
|
|
|
|
|
cfg.MonitorProjectionQueue = "monitor.projection.rebuild"
|
|
|
|
|
}
|
|
|
|
|
if strings.TrimSpace(cfg.MonitorProjectionDLX) == "" {
|
|
|
|
|
cfg.MonitorProjectionDLX = "monitor.projection.dlx"
|
|
|
|
|
}
|
|
|
|
|
if strings.TrimSpace(cfg.MonitorProjectionDLQ) == "" {
|
|
|
|
|
cfg.MonitorProjectionDLQ = "monitor.projection.rebuild.dlq"
|
|
|
|
|
}
|
|
|
|
|
if strings.TrimSpace(cfg.MonitorProjectionDLQRouteKey) == "" {
|
|
|
|
|
cfg.MonitorProjectionDLQRouteKey = "monitor.projection.rebuild.dlq"
|
|
|
|
|
}
|
2026-04-15 14:18:55 +08:00
|
|
|
|
|
|
|
|
if strings.TrimSpace(cfg.GenerationExchange) == "" {
|
|
|
|
|
cfg.GenerationExchange = "generation.task"
|
|
|
|
|
}
|
|
|
|
|
if strings.TrimSpace(cfg.GenerationRoutingKey) == "" {
|
|
|
|
|
cfg.GenerationRoutingKey = "generation.task.run"
|
|
|
|
|
}
|
|
|
|
|
if strings.TrimSpace(cfg.GenerationQueue) == "" {
|
|
|
|
|
cfg.GenerationQueue = "generation.task.run"
|
|
|
|
|
}
|
|
|
|
|
if strings.TrimSpace(cfg.GenerationDLX) == "" {
|
|
|
|
|
cfg.GenerationDLX = "generation.task.dlx"
|
|
|
|
|
}
|
|
|
|
|
if strings.TrimSpace(cfg.GenerationDLQ) == "" {
|
|
|
|
|
cfg.GenerationDLQ = "generation.task.run.dlq"
|
|
|
|
|
}
|
|
|
|
|
if strings.TrimSpace(cfg.GenerationDLQRouteKey) == "" {
|
|
|
|
|
cfg.GenerationDLQRouteKey = "generation.task.run.dlq"
|
|
|
|
|
}
|
|
|
|
|
if strings.TrimSpace(cfg.GenerationEventExchange) == "" {
|
|
|
|
|
cfg.GenerationEventExchange = "generation.event"
|
|
|
|
|
}
|
2026-04-19 14:18:20 +08:00
|
|
|
if strings.TrimSpace(cfg.DesktopTaskEventExchange) == "" {
|
|
|
|
|
cfg.DesktopTaskEventExchange = "desktop.task.event"
|
|
|
|
|
}
|
2026-04-20 19:46:59 +08:00
|
|
|
if strings.TrimSpace(cfg.DesktopDispatchExchange) == "" {
|
|
|
|
|
cfg.DesktopDispatchExchange = "desktop.task.dispatch"
|
|
|
|
|
}
|
|
|
|
|
if strings.TrimSpace(cfg.DesktopDispatchPublishPrefix) == "" {
|
|
|
|
|
cfg.DesktopDispatchPublishPrefix = "publish"
|
|
|
|
|
}
|
|
|
|
|
if strings.TrimSpace(cfg.DesktopDispatchMonitorPrefix) == "" {
|
|
|
|
|
cfg.DesktopDispatchMonitorPrefix = "monitor"
|
|
|
|
|
}
|
2026-04-27 21:33:36 +08:00
|
|
|
if strings.TrimSpace(cfg.DesktopAccountHealthExchange) == "" {
|
|
|
|
|
cfg.DesktopAccountHealthExchange = "desktop.account.health"
|
|
|
|
|
}
|
|
|
|
|
if strings.TrimSpace(cfg.DesktopAccountHealthRoutingKey) == "" {
|
|
|
|
|
cfg.DesktopAccountHealthRoutingKey = "desktop.account.health.report"
|
|
|
|
|
}
|
|
|
|
|
if strings.TrimSpace(cfg.DesktopAccountHealthQueue) == "" {
|
|
|
|
|
cfg.DesktopAccountHealthQueue = "desktop.account.health.report"
|
|
|
|
|
}
|
|
|
|
|
if strings.TrimSpace(cfg.DesktopAccountHealthDLX) == "" {
|
|
|
|
|
cfg.DesktopAccountHealthDLX = "desktop.account.health.dlx"
|
|
|
|
|
}
|
|
|
|
|
if strings.TrimSpace(cfg.DesktopAccountHealthDLQ) == "" {
|
|
|
|
|
cfg.DesktopAccountHealthDLQ = "desktop.account.health.report.dlq"
|
|
|
|
|
}
|
|
|
|
|
if strings.TrimSpace(cfg.DesktopAccountHealthDLQRouteKey) == "" {
|
|
|
|
|
cfg.DesktopAccountHealthDLQRouteKey = "desktop.account.health.report.dlq"
|
|
|
|
|
}
|
2026-04-15 14:18:55 +08:00
|
|
|
if strings.TrimSpace(cfg.TemplateAssistExchange) == "" {
|
|
|
|
|
cfg.TemplateAssistExchange = "template.assist"
|
|
|
|
|
}
|
|
|
|
|
if strings.TrimSpace(cfg.TemplateAssistRoutingKey) == "" {
|
|
|
|
|
cfg.TemplateAssistRoutingKey = "template.assist.run"
|
|
|
|
|
}
|
|
|
|
|
if strings.TrimSpace(cfg.TemplateAssistQueue) == "" {
|
|
|
|
|
cfg.TemplateAssistQueue = "template.assist.run"
|
|
|
|
|
}
|
|
|
|
|
if strings.TrimSpace(cfg.TemplateAssistDLX) == "" {
|
|
|
|
|
cfg.TemplateAssistDLX = "template.assist.dlx"
|
|
|
|
|
}
|
|
|
|
|
if strings.TrimSpace(cfg.TemplateAssistDLQ) == "" {
|
|
|
|
|
cfg.TemplateAssistDLQ = "template.assist.run.dlq"
|
|
|
|
|
}
|
|
|
|
|
if strings.TrimSpace(cfg.TemplateAssistDLQRouteKey) == "" {
|
|
|
|
|
cfg.TemplateAssistDLQRouteKey = "template.assist.run.dlq"
|
|
|
|
|
}
|
2026-04-17 13:52:01 +08:00
|
|
|
if strings.TrimSpace(cfg.KolAssistExchange) == "" {
|
|
|
|
|
cfg.KolAssistExchange = "kol.assist"
|
|
|
|
|
}
|
|
|
|
|
if strings.TrimSpace(cfg.KolAssistRoutingKey) == "" {
|
|
|
|
|
cfg.KolAssistRoutingKey = "kol.assist.run"
|
|
|
|
|
}
|
|
|
|
|
if strings.TrimSpace(cfg.KolAssistQueue) == "" {
|
|
|
|
|
cfg.KolAssistQueue = "kol.assist.run"
|
|
|
|
|
}
|
|
|
|
|
if strings.TrimSpace(cfg.KolAssistDLX) == "" {
|
|
|
|
|
cfg.KolAssistDLX = "kol.assist.dlx"
|
|
|
|
|
}
|
|
|
|
|
if strings.TrimSpace(cfg.KolAssistDLQ) == "" {
|
|
|
|
|
cfg.KolAssistDLQ = "kol.assist.run.dlq"
|
|
|
|
|
}
|
|
|
|
|
if strings.TrimSpace(cfg.KolAssistDLQRouteKey) == "" {
|
|
|
|
|
cfg.KolAssistDLQRouteKey = "kol.assist.run.dlq"
|
|
|
|
|
}
|
2026-04-16 20:40:41 +08:00
|
|
|
if strings.TrimSpace(cfg.ImageAssetExchange) == "" {
|
|
|
|
|
cfg.ImageAssetExchange = "image.asset"
|
|
|
|
|
}
|
|
|
|
|
if strings.TrimSpace(cfg.ImageAssetRoutingKey) == "" {
|
|
|
|
|
cfg.ImageAssetRoutingKey = "image.asset.finalize"
|
|
|
|
|
}
|
|
|
|
|
if strings.TrimSpace(cfg.ImageAssetQueue) == "" {
|
|
|
|
|
cfg.ImageAssetQueue = "image.asset.finalize"
|
|
|
|
|
}
|
|
|
|
|
if strings.TrimSpace(cfg.ImageAssetDLX) == "" {
|
|
|
|
|
cfg.ImageAssetDLX = "image.asset.dlx"
|
|
|
|
|
}
|
|
|
|
|
if strings.TrimSpace(cfg.ImageAssetDLQ) == "" {
|
|
|
|
|
cfg.ImageAssetDLQ = "image.asset.finalize.dlq"
|
|
|
|
|
}
|
|
|
|
|
if strings.TrimSpace(cfg.ImageAssetDLQRouteKey) == "" {
|
|
|
|
|
cfg.ImageAssetDLQRouteKey = "image.asset.finalize.dlq"
|
|
|
|
|
}
|
2026-05-05 20:48:14 +08:00
|
|
|
if strings.TrimSpace(cfg.ComplianceReviewExchange) == "" {
|
|
|
|
|
cfg.ComplianceReviewExchange = "compliance.review"
|
|
|
|
|
}
|
|
|
|
|
if strings.TrimSpace(cfg.ComplianceReviewRoutingKey) == "" {
|
|
|
|
|
cfg.ComplianceReviewRoutingKey = "compliance.review.run"
|
|
|
|
|
}
|
|
|
|
|
if strings.TrimSpace(cfg.ComplianceReviewQueue) == "" {
|
|
|
|
|
cfg.ComplianceReviewQueue = "compliance.review.run"
|
|
|
|
|
}
|
|
|
|
|
if strings.TrimSpace(cfg.ComplianceReviewDLX) == "" {
|
|
|
|
|
cfg.ComplianceReviewDLX = "compliance.review.dlx"
|
|
|
|
|
}
|
|
|
|
|
if strings.TrimSpace(cfg.ComplianceReviewDLQ) == "" {
|
|
|
|
|
cfg.ComplianceReviewDLQ = "compliance.review.run.dlq"
|
|
|
|
|
}
|
|
|
|
|
if strings.TrimSpace(cfg.ComplianceReviewDLQRouteKey) == "" {
|
|
|
|
|
cfg.ComplianceReviewDLQRouteKey = "compliance.review.run.dlq"
|
|
|
|
|
}
|
2026-04-15 14:18:55 +08:00
|
|
|
if cfg.PublishChannelPoolSize <= 0 {
|
|
|
|
|
cfg.PublishChannelPoolSize = 16
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func normalizeSchedulerConfig(cfg *SchedulerConfig) {
|
|
|
|
|
if cfg == nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-24 22:19:57 +08:00
|
|
|
if cfg.HTTPPort == 0 {
|
|
|
|
|
cfg.HTTPPort = 8081
|
|
|
|
|
}
|
|
|
|
|
cfg.HTTPHost = strings.TrimSpace(cfg.HTTPHost)
|
|
|
|
|
if cfg.HTTPHost == "" {
|
|
|
|
|
cfg.HTTPHost = "127.0.0.1"
|
|
|
|
|
}
|
|
|
|
|
cfg.InternalMetricsToken = strings.TrimSpace(cfg.InternalMetricsToken)
|
2026-04-15 14:18:55 +08:00
|
|
|
if cfg.DispatchInterval <= 0 {
|
|
|
|
|
cfg.DispatchInterval = 15 * time.Second
|
|
|
|
|
}
|
|
|
|
|
if cfg.DispatchTimeout <= 0 {
|
|
|
|
|
cfg.DispatchTimeout = 30 * time.Second
|
|
|
|
|
}
|
|
|
|
|
if cfg.DispatchBatchSize <= 0 {
|
|
|
|
|
cfg.DispatchBatchSize = 20
|
|
|
|
|
}
|
|
|
|
|
if cfg.DispatchConcurrency <= 0 {
|
|
|
|
|
cfg.DispatchConcurrency = 1
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func normalizeMonitoringConfig(cfg *MonitoringConfig) {
|
|
|
|
|
if cfg == nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if cfg.ResultIngestConcurrency <= 0 {
|
|
|
|
|
cfg.ResultIngestConcurrency = 1
|
|
|
|
|
}
|
|
|
|
|
if cfg.ProjectionRebuildConcurrency <= 0 {
|
|
|
|
|
cfg.ProjectionRebuildConcurrency = 1
|
|
|
|
|
}
|
2026-04-12 09:56:18 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-16 21:01:40 +08:00
|
|
|
func normalizeBrandLibraryConfig(cfg *BrandLibraryConfig) {
|
|
|
|
|
if cfg == nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if cfg.FreeBrandLimit <= 0 {
|
|
|
|
|
cfg.FreeBrandLimit = 1
|
|
|
|
|
}
|
|
|
|
|
if cfg.PaidBrandLimit <= 0 {
|
|
|
|
|
cfg.PaidBrandLimit = 2
|
|
|
|
|
}
|
|
|
|
|
if cfg.MaxKeywords <= 0 {
|
|
|
|
|
cfg.MaxKeywords = 5
|
|
|
|
|
}
|
|
|
|
|
if cfg.MaxQuestionsPerKeyword <= 0 {
|
|
|
|
|
cfg.MaxQuestionsPerKeyword = 5
|
|
|
|
|
}
|
2026-05-12 21:53:36 +08:00
|
|
|
if cfg.QuestionLimitsByPlan == nil {
|
|
|
|
|
cfg.QuestionLimitsByPlan = map[string]int{}
|
|
|
|
|
}
|
|
|
|
|
normalized := make(map[string]int, len(cfg.QuestionLimitsByPlan)+4)
|
|
|
|
|
for key, value := range cfg.QuestionLimitsByPlan {
|
|
|
|
|
trimmed := strings.ToLower(strings.TrimSpace(key))
|
|
|
|
|
if trimmed != "" && value > 0 {
|
|
|
|
|
normalized[trimmed] = value
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if normalized["default"] <= 0 {
|
|
|
|
|
normalized["default"] = 25
|
|
|
|
|
}
|
|
|
|
|
if normalized["free"] <= 0 {
|
|
|
|
|
normalized["free"] = 5
|
|
|
|
|
}
|
|
|
|
|
if normalized["plus"] <= 0 {
|
|
|
|
|
normalized["plus"] = 25
|
|
|
|
|
}
|
|
|
|
|
if normalized["pro"] <= 0 {
|
|
|
|
|
normalized["pro"] = 50
|
|
|
|
|
}
|
|
|
|
|
cfg.QuestionLimitsByPlan = normalized
|
2026-04-16 21:01:40 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-18 20:56:05 +08:00
|
|
|
func normalizeMembershipConfig(cfg *MembershipConfig) {
|
|
|
|
|
if cfg == nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if strings.TrimSpace(cfg.DefaultPlanCode) == "" {
|
|
|
|
|
cfg.DefaultPlanCode = "free"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if len(cfg.Plans) == 0 {
|
|
|
|
|
cfg.Plans = defaultMembershipPlans()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
normalized := make([]MembershipPlanConfig, 0, len(cfg.Plans))
|
|
|
|
|
seen := make(map[string]struct{}, len(cfg.Plans))
|
|
|
|
|
for _, plan := range cfg.Plans {
|
|
|
|
|
plan.Code = strings.ToLower(strings.TrimSpace(plan.Code))
|
|
|
|
|
if plan.Code == "" {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if _, exists := seen[plan.Code]; exists {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if strings.TrimSpace(plan.Name) == "" {
|
|
|
|
|
plan.Name = defaultPlanName(plan.Code)
|
|
|
|
|
}
|
|
|
|
|
plan.ArticleQuotaCycle = normalizeArticleQuotaCycle(plan.ArticleQuotaCycle, plan.Code)
|
|
|
|
|
if plan.ArticleGeneration < 0 {
|
|
|
|
|
plan.ArticleGeneration = 0
|
|
|
|
|
}
|
2026-04-28 11:33:30 +08:00
|
|
|
if plan.AIPointsMonthly < 0 {
|
|
|
|
|
plan.AIPointsMonthly = 0
|
|
|
|
|
}
|
|
|
|
|
plan.AIPointBaseChars = normalizeAIPointBaseChars(plan.AIPointBaseChars)
|
2026-04-18 20:56:05 +08:00
|
|
|
if plan.ImageStorageBytes < 0 {
|
|
|
|
|
plan.ImageStorageBytes = 0
|
|
|
|
|
}
|
|
|
|
|
if plan.CompanyLimit < 0 {
|
|
|
|
|
plan.CompanyLimit = 0
|
|
|
|
|
}
|
|
|
|
|
if plan.SubscriptionDuration < 0 {
|
|
|
|
|
plan.SubscriptionDuration = 0
|
|
|
|
|
}
|
|
|
|
|
normalized = append(normalized, plan)
|
|
|
|
|
seen[plan.Code] = struct{}{}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if len(normalized) == 0 {
|
|
|
|
|
normalized = defaultMembershipPlans()
|
|
|
|
|
}
|
|
|
|
|
cfg.Plans = normalized
|
|
|
|
|
|
|
|
|
|
if _, ok := cfg.FindPlan(cfg.DefaultPlanCode); !ok {
|
|
|
|
|
cfg.DefaultPlanCode = cfg.Plans[0].Code
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func defaultMembershipPlans() []MembershipPlanConfig {
|
|
|
|
|
return []MembershipPlanConfig{
|
|
|
|
|
{
|
|
|
|
|
Code: "free",
|
|
|
|
|
Name: "Free",
|
|
|
|
|
ArticleGeneration: 3,
|
|
|
|
|
ArticleQuotaCycle: ArticleQuotaCycleLifetime,
|
2026-04-28 11:33:30 +08:00
|
|
|
AIPointBaseChars: 1000,
|
2026-04-18 20:56:05 +08:00
|
|
|
ImageStorageBytes: 10 * 1024 * 1024,
|
|
|
|
|
CompanyLimit: 1,
|
|
|
|
|
SubscriptionDuration: 72 * time.Hour,
|
|
|
|
|
ContactAdminOnExpiry: true,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
Code: "plus",
|
|
|
|
|
Name: "Plus",
|
|
|
|
|
ArticleGeneration: 200,
|
|
|
|
|
ArticleQuotaCycle: ArticleQuotaCycleMonthly,
|
2026-04-28 11:33:30 +08:00
|
|
|
AIPointBaseChars: 1000,
|
2026-04-18 20:56:05 +08:00
|
|
|
ImageStorageBytes: 500 * 1024 * 1024,
|
|
|
|
|
CompanyLimit: 1,
|
|
|
|
|
SubscriptionDuration: 720 * time.Hour,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
Code: "pro",
|
|
|
|
|
Name: "Pro",
|
|
|
|
|
ArticleGeneration: 400,
|
|
|
|
|
ArticleQuotaCycle: ArticleQuotaCycleMonthly,
|
2026-04-28 11:33:30 +08:00
|
|
|
AIPointsMonthly: 1500,
|
|
|
|
|
AIPointBaseChars: 1000,
|
2026-04-18 20:56:05 +08:00
|
|
|
ImageStorageBytes: 1024 * 1024 * 1024,
|
|
|
|
|
CompanyLimit: 2,
|
|
|
|
|
SubscriptionDuration: 720 * time.Hour,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-28 11:33:30 +08:00
|
|
|
func normalizeAIPointBaseChars(value int) int {
|
|
|
|
|
if value <= 0 {
|
|
|
|
|
return 1000
|
|
|
|
|
}
|
|
|
|
|
return value
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-18 20:56:05 +08:00
|
|
|
func normalizeArticleQuotaCycle(value, planCode string) string {
|
|
|
|
|
switch strings.ToLower(strings.TrimSpace(value)) {
|
|
|
|
|
case ArticleQuotaCycleLifetime:
|
|
|
|
|
return ArticleQuotaCycleLifetime
|
|
|
|
|
case ArticleQuotaCycleMonthly:
|
|
|
|
|
return ArticleQuotaCycleMonthly
|
|
|
|
|
default:
|
|
|
|
|
if strings.EqualFold(strings.TrimSpace(planCode), "free") {
|
|
|
|
|
return ArticleQuotaCycleLifetime
|
|
|
|
|
}
|
|
|
|
|
return ArticleQuotaCycleMonthly
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func defaultPlanName(code string) string {
|
|
|
|
|
switch strings.ToLower(strings.TrimSpace(code)) {
|
|
|
|
|
case "free":
|
|
|
|
|
return "Free"
|
|
|
|
|
case "plus":
|
|
|
|
|
return "Plus"
|
|
|
|
|
case "pro":
|
|
|
|
|
return "Pro"
|
|
|
|
|
default:
|
|
|
|
|
return strings.ToUpper(strings.TrimSpace(code))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-05 17:14:13 +08:00
|
|
|
func lookupFirstNonEmptyEnv(keys ...string) (string, bool) {
|
|
|
|
|
for _, key := range keys {
|
|
|
|
|
if value, ok := lookupNonEmptyEnv(key); ok {
|
|
|
|
|
return value, true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return "", false
|
2026-04-01 00:58:42 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func lookupNonEmptyEnv(key string) (string, bool) {
|
|
|
|
|
value, ok := os.LookupEnv(key)
|
|
|
|
|
if !ok {
|
|
|
|
|
return "", false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
value = strings.TrimSpace(value)
|
|
|
|
|
if value == "" {
|
|
|
|
|
return "", false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return value, true
|
|
|
|
|
}
|
2026-04-05 22:10:05 +08:00
|
|
|
|
|
|
|
|
func lookupBoolEnv(key string) (bool, bool) {
|
|
|
|
|
value, ok := os.LookupEnv(key)
|
|
|
|
|
if !ok {
|
|
|
|
|
return false, false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
switch strings.ToLower(strings.TrimSpace(value)) {
|
|
|
|
|
case "1", "true", "yes", "on":
|
|
|
|
|
return true, true
|
|
|
|
|
case "0", "false", "no", "off":
|
|
|
|
|
return false, true
|
|
|
|
|
default:
|
|
|
|
|
return false, false
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-18 20:56:05 +08:00
|
|
|
|
2026-04-25 18:09:01 +08:00
|
|
|
func lookupDurationEnv(key string) (time.Duration, bool) {
|
|
|
|
|
value, ok := lookupNonEmptyEnv(key)
|
|
|
|
|
if !ok {
|
|
|
|
|
return 0, false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
duration, err := time.ParseDuration(value)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return 0, false
|
|
|
|
|
}
|
|
|
|
|
return duration, true
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-05 20:48:14 +08:00
|
|
|
func lookupIntEnv(key string) (int, bool) {
|
|
|
|
|
value, ok := lookupNonEmptyEnv(key)
|
|
|
|
|
if !ok {
|
|
|
|
|
return 0, false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
number, err := strconv.Atoi(value)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return 0, false
|
|
|
|
|
}
|
|
|
|
|
return number, true
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-29 23:17:01 +08:00
|
|
|
func lookupInt64Env(key string) (int64, bool) {
|
|
|
|
|
value, ok := lookupNonEmptyEnv(key)
|
|
|
|
|
if !ok {
|
|
|
|
|
return 0, false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
number, err := strconv.ParseInt(value, 10, 64)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return 0, false
|
|
|
|
|
}
|
|
|
|
|
return number, true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func lookupFloatEnv(key string) (float64, bool) {
|
|
|
|
|
value, ok := lookupNonEmptyEnv(key)
|
|
|
|
|
if !ok {
|
|
|
|
|
return 0, false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
number, err := strconv.ParseFloat(value, 64)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return 0, false
|
|
|
|
|
}
|
|
|
|
|
return number, true
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-18 20:56:05 +08:00
|
|
|
func maxInt(value, fallback int) int {
|
|
|
|
|
if value < fallback {
|
|
|
|
|
return fallback
|
|
|
|
|
}
|
|
|
|
|
return value
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func maxInt64(value, fallback int64) int64 {
|
|
|
|
|
if value < fallback {
|
|
|
|
|
return fallback
|
|
|
|
|
}
|
|
|
|
|
return value
|
|
|
|
|
}
|