package admin import ( "errors" "fmt" "os" "path/filepath" "strings" "gopkg.in/yaml.v3" ) const defaultAdminAddr = ":8080" const defaultMigrationsDir = "migrations" type Config struct { Addr string DatabaseURL string MigrationsDir string RepoRoot string PostsDir string SiteDir string AssetsDir string AdminDir string DeepSeek DeepSeekConfig LocalLLM LocalLLMConfig SlugProvider string } type DeepSeekConfig struct { APIKey string BaseURL string Model string } type LocalLLMConfig struct { URL string Model string Temperature float64 TopP float64 NumPredict int } type localConfig struct { Database struct { PostgresDSN string `yaml:"postgres_dsn"` } `yaml:"database"` Slug struct { Provider string `yaml:"provider"` } `yaml:"slug"` DeepSeek struct { APIKey string `yaml:"api_key"` APIKeyEnv string `yaml:"api_key_env"` BaseURL string `yaml:"base_url"` Model string `yaml:"model"` } `yaml:"deepseek"` LocalLLM struct { URL string `yaml:"url"` Model string `yaml:"model"` Temperature float64 `yaml:"temperature"` TopP float64 `yaml:"top_p"` NumPredict int `yaml:"num_predict"` } `yaml:"local_llm"` } func LoadConfig() Config { addr := os.Getenv("OSAET_ADMIN_ADDR") if addr == "" { addr = defaultAdminAddr } migrationsDir := os.Getenv("OSAET_MIGRATIONS_DIR") if migrationsDir == "" { migrationsDir = defaultMigrationsDir } repoRoot := os.Getenv("OSAET_REPO_ROOT") if repoRoot == "" { repoRoot = ".." } postsDir := os.Getenv("OSAET_POSTS_DIR") if postsDir == "" { postsDir = filepath.Join(repoRoot, "content", "posts") } siteDir := os.Getenv("OSAET_SITE_DIR") if siteDir == "" { siteDir = filepath.Join(repoRoot, "frontend", "site") } assetsDir := os.Getenv("OSAET_ASSETS_DIR") if assetsDir == "" { assetsDir = filepath.Join(siteDir, "public", "assets") } adminDir := os.Getenv("OSAET_ADMIN_DIR") if adminDir == "" { adminDir = filepath.Join(repoRoot, "frontend", "admin", "dist", "admin", "browser") } local := loadLocalConfig(repoRoot) databaseURL := firstNonEmpty(os.Getenv("DATABASE_URL"), local.Database.PostgresDSN) deepSeekAPIKeyEnv := firstNonEmpty(local.DeepSeek.APIKeyEnv, "DEEPSEEK_API_KEY") deepSeekAPIKey := strings.TrimSpace(os.Getenv(deepSeekAPIKeyEnv)) if deepSeekAPIKey == "" && deepSeekAPIKeyEnv != "DEEPSEEK_API_KEY" { deepSeekAPIKey = strings.TrimSpace(os.Getenv("DEEPSEEK_API_KEY")) } if deepSeekAPIKey == "" { deepSeekAPIKey = strings.TrimSpace(local.DeepSeek.APIKey) } return Config{ Addr: addr, DatabaseURL: databaseURL, MigrationsDir: migrationsDir, RepoRoot: repoRoot, PostsDir: postsDir, SiteDir: siteDir, AssetsDir: assetsDir, AdminDir: adminDir, SlugProvider: firstNonEmpty(os.Getenv("OSAET_SLUG_PROVIDER"), local.Slug.Provider, "deepseek"), DeepSeek: DeepSeekConfig{ APIKey: deepSeekAPIKey, BaseURL: firstNonEmpty(os.Getenv("DEEPSEEK_BASE_URL"), local.DeepSeek.BaseURL), Model: firstNonEmpty(os.Getenv("DEEPSEEK_MODEL"), local.DeepSeek.Model), }, LocalLLM: LocalLLMConfig{ URL: firstNonEmpty(os.Getenv("LOCAL_LLM_URL"), local.LocalLLM.URL, "http://127.0.0.1:11434/api/generate"), Model: firstNonEmpty(os.Getenv("LOCAL_LLM_MODEL"), local.LocalLLM.Model), Temperature: firstNonZeroFloat(envFloat("LOCAL_LLM_TEMPERATURE"), local.LocalLLM.Temperature, 0.1), TopP: firstNonZeroFloat(envFloat("LOCAL_LLM_TOP_P"), local.LocalLLM.TopP, 0.8), NumPredict: firstNonZeroInt(envInt("LOCAL_LLM_NUM_PREDICT"), local.LocalLLM.NumPredict, 32), }, } } func loadLocalConfig(repoRoot string) localConfig { var config localConfig paths := []string{} if path := strings.TrimSpace(os.Getenv("OSAET_LOCAL_CONFIG")); path != "" { paths = append(paths, path) } else { paths = append(paths, filepath.Join(repoRoot, "config", "local.yaml"), filepath.Join("config", "local.yaml"), filepath.Join("..", "config", "local.yaml"), ) } path, data, ok := readFirstExistingFile(paths) if !ok { return config } if err := yaml.Unmarshal(data, &config); err != nil { fmt.Fprintf(os.Stderr, "warning: failed to read %s: %v\n", path, err) } return config } func readFirstExistingFile(paths []string) (string, []byte, bool) { for _, path := range paths { data, err := os.ReadFile(path) if err == nil { return path, data, true } } return "", nil, false } func firstNonEmpty(values ...string) string { for _, value := range values { if strings.TrimSpace(value) != "" { return strings.TrimSpace(value) } } return "" } func firstNonZeroFloat(values ...float64) float64 { for _, value := range values { if value != 0 { return value } } return 0 } func firstNonZeroInt(values ...int) int { for _, value := range values { if value != 0 { return value } } return 0 } func envFloat(key string) float64 { value := strings.TrimSpace(os.Getenv(key)) if value == "" { return 0 } var parsed float64 if _, err := fmt.Sscanf(value, "%f", &parsed); err != nil { return 0 } return parsed } func envInt(key string) int { value := strings.TrimSpace(os.Getenv(key)) if value == "" { return 0 } var parsed int if _, err := fmt.Sscanf(value, "%d", &parsed); err != nil { return 0 } return parsed } func (c Config) Validate() error { if c.Addr == "" { return errors.New("admin addr is required") } if c.DatabaseURL == "" { return errors.New("DATABASE_URL or config/local.yaml database.postgres_dsn is required") } if c.MigrationsDir == "" { return errors.New("migrations dir is required") } if c.PostsDir == "" { return errors.New("posts dir is required") } if c.SiteDir == "" { return errors.New("site dir is required") } if c.AssetsDir == "" { return errors.New("assets dir is required") } return nil }