chore: initial commit
This commit is contained in:
215
internal/config/config.go
Normal file
215
internal/config/config.go
Normal file
@@ -0,0 +1,215 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
MessageChannel string
|
||||
LogLevel string
|
||||
SoulPath string
|
||||
SkillsDir string
|
||||
ReactMaxSteps int
|
||||
|
||||
Telegram TelegramConfig
|
||||
Feishu FeishuConfig
|
||||
LLM LLMConfig
|
||||
Security SecurityConfig
|
||||
|
||||
SQLitePath string
|
||||
}
|
||||
|
||||
type TelegramConfig struct {
|
||||
Token string
|
||||
PollTimeoutSeconds int
|
||||
}
|
||||
|
||||
type FeishuConfig struct {
|
||||
AppID string
|
||||
AppSecret string
|
||||
VerifyToken string
|
||||
ListenAddr string
|
||||
EventPath string
|
||||
}
|
||||
|
||||
type LLMConfig struct {
|
||||
BaseURL string
|
||||
APIKey string
|
||||
Model string
|
||||
}
|
||||
|
||||
type SecurityConfig struct {
|
||||
AllowedDirs []string
|
||||
AllowedCommands []string
|
||||
WorkDir string
|
||||
}
|
||||
|
||||
func Load() (Config, error) {
|
||||
if err := preloadEnvFiles(); err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
|
||||
cfg := Config{
|
||||
MessageChannel: defaultIfEmpty(os.Getenv("MESSAGE_CHANNEL"), "telegram"),
|
||||
LogLevel: defaultIfEmpty(os.Getenv("LOG_LEVEL"), "info"),
|
||||
SoulPath: defaultIfEmpty(os.Getenv("SOUL_PATH"), "./bot_context/soul.md"),
|
||||
SkillsDir: defaultIfEmpty(os.Getenv("SKILLS_DIR"), "./skills"),
|
||||
ReactMaxSteps: intFromEnv("REACT_MAX_STEPS", 4),
|
||||
Telegram: TelegramConfig{
|
||||
Token: strings.TrimSpace(os.Getenv("TELEGRAM_BOT_TOKEN")),
|
||||
PollTimeoutSeconds: intFromEnv("TELEGRAM_POLL_TIMEOUT_SECONDS", 30),
|
||||
},
|
||||
Feishu: FeishuConfig{
|
||||
AppID: strings.TrimSpace(os.Getenv("FEISHU_APP_ID")),
|
||||
AppSecret: strings.TrimSpace(os.Getenv("FEISHU_APP_SECRET")),
|
||||
VerifyToken: strings.TrimSpace(os.Getenv("FEISHU_VERIFY_TOKEN")),
|
||||
ListenAddr: defaultIfEmpty(os.Getenv("FEISHU_LISTEN_ADDR"), ":8080"),
|
||||
EventPath: defaultIfEmpty(os.Getenv("FEISHU_EVENT_PATH"), "/feishu/events"),
|
||||
},
|
||||
LLM: LLMConfig{
|
||||
BaseURL: strings.TrimRight(defaultIfEmpty(os.Getenv("LLM_BASE_URL"), "https://api.openai.com/v1"), "/"),
|
||||
APIKey: strings.TrimSpace(os.Getenv("LLM_API_KEY")),
|
||||
Model: defaultIfEmpty(os.Getenv("LLM_MODEL"), "gpt-4o-mini"),
|
||||
},
|
||||
SQLitePath: defaultIfEmpty(os.Getenv("SQLITE_PATH"), "./data/laodingbot.db"),
|
||||
Security: SecurityConfig{
|
||||
AllowedDirs: splitCSV(defaultIfEmpty(os.Getenv("ALLOWED_DIRS"), "./workspace,./data")),
|
||||
AllowedCommands: splitCSV(defaultIfEmpty(os.Getenv("ALLOWED_COMMANDS"), "pwd,ls,cat,echo,grep,find,head,tail")),
|
||||
WorkDir: defaultIfEmpty(os.Getenv("WORK_DIR"), "./workspace"),
|
||||
},
|
||||
}
|
||||
|
||||
cfg.MessageChannel = strings.ToLower(strings.TrimSpace(cfg.MessageChannel))
|
||||
cfg.LogLevel = strings.ToLower(strings.TrimSpace(cfg.LogLevel))
|
||||
if cfg.MessageChannel != "telegram" && cfg.MessageChannel != "feishu" {
|
||||
return Config{}, fmt.Errorf("MESSAGE_CHANNEL must be telegram or feishu")
|
||||
}
|
||||
if cfg.LogLevel != "debug" && cfg.LogLevel != "info" && cfg.LogLevel != "warn" && cfg.LogLevel != "error" {
|
||||
return Config{}, fmt.Errorf("LOG_LEVEL must be debug, info, warn, or error")
|
||||
}
|
||||
if cfg.ReactMaxSteps < 1 || cfg.ReactMaxSteps > 8 {
|
||||
return Config{}, fmt.Errorf("REACT_MAX_STEPS must be between 1 and 8")
|
||||
}
|
||||
|
||||
if cfg.MessageChannel == "telegram" {
|
||||
if cfg.Telegram.Token == "" {
|
||||
return Config{}, fmt.Errorf("TELEGRAM_BOT_TOKEN is required when MESSAGE_CHANNEL=telegram")
|
||||
}
|
||||
if cfg.Feishu.AppID != "" || cfg.Feishu.AppSecret != "" {
|
||||
return Config{}, fmt.Errorf("feishu config must be empty when MESSAGE_CHANNEL=telegram")
|
||||
}
|
||||
}
|
||||
|
||||
if cfg.MessageChannel == "feishu" {
|
||||
if cfg.Feishu.AppID == "" || cfg.Feishu.AppSecret == "" {
|
||||
return Config{}, fmt.Errorf("FEISHU_APP_ID and FEISHU_APP_SECRET are required when MESSAGE_CHANNEL=feishu")
|
||||
}
|
||||
if cfg.Telegram.Token != "" {
|
||||
return Config{}, fmt.Errorf("TELEGRAM_BOT_TOKEN must be empty when MESSAGE_CHANNEL=feishu")
|
||||
}
|
||||
}
|
||||
|
||||
if cfg.LLM.APIKey == "" {
|
||||
return Config{}, fmt.Errorf("LLM_API_KEY is required")
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func preloadEnvFiles() error {
|
||||
paths := []string{}
|
||||
if explicit := strings.TrimSpace(os.Getenv("CONFIG_ENV_FILE")); explicit != "" {
|
||||
paths = append(paths, explicit)
|
||||
}
|
||||
paths = append(paths, "configs/env", ".env")
|
||||
|
||||
for _, p := range paths {
|
||||
if err := loadEnvFile(p); err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
continue
|
||||
}
|
||||
return fmt.Errorf("load env file %s failed: %w", p, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadEnvFile(path string) error {
|
||||
absPath, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
absPath = path
|
||||
}
|
||||
|
||||
f, err := os.Open(absPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
scanner := bufio.NewScanner(f)
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
line = strings.TrimPrefix(line, "export ")
|
||||
idx := strings.Index(line, "=")
|
||||
if idx <= 0 {
|
||||
continue
|
||||
}
|
||||
key := strings.TrimSpace(line[:idx])
|
||||
if key == "" {
|
||||
continue
|
||||
}
|
||||
val := strings.TrimSpace(line[idx+1:])
|
||||
if len(val) >= 2 {
|
||||
if (val[0] == '"' && val[len(val)-1] == '"') || (val[0] == '\'' && val[len(val)-1] == '\'') {
|
||||
val = val[1 : len(val)-1]
|
||||
}
|
||||
}
|
||||
if _, exists := os.LookupEnv(key); !exists {
|
||||
if err := os.Setenv(key, val); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return scanner.Err()
|
||||
}
|
||||
|
||||
func defaultIfEmpty(v, d string) string {
|
||||
v = strings.TrimSpace(v)
|
||||
if v == "" {
|
||||
return d
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func intFromEnv(name string, d int) int {
|
||||
raw := strings.TrimSpace(os.Getenv(name))
|
||||
if raw == "" {
|
||||
return d
|
||||
}
|
||||
v, err := strconv.Atoi(raw)
|
||||
if err != nil {
|
||||
return d
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func splitCSV(raw string) []string {
|
||||
parts := strings.Split(raw, ",")
|
||||
out := make([]string, 0, len(parts))
|
||||
for _, p := range parts {
|
||||
v := strings.TrimSpace(p)
|
||||
if v != "" {
|
||||
out = append(out, v)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
Reference in New Issue
Block a user