osaet/backend/internal/admin/config.go
yarnom f0b50d13ea feat: add admin publishing workflow and yar theme
Add Go/Postgres admin APIs, Angular admin UI, manual build flow, asset uploads, markdown import/export, configurable slug generation, and the Yar reading theme. Exclude local docs and generated development artifacts from version control.
2026-06-01 15:48:04 +08:00

243 lines
5.8 KiB
Go

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
}