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.
243 lines
5.8 KiB
Go
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
|
|
}
|