package config import ( "encoding/json" "os" "path/filepath" "sync" "github.com/caarlos0/env/v11" ) type Config struct { Agents AgentsConfig `json:"agents"` Channels ChannelsConfig `json:"channels"` Providers ProvidersConfig `json:"providers"` Gateway GatewayConfig `json:"gateway"` Tools ToolsConfig `json:"tools"` mu sync.RWMutex } type AgentsConfig struct { Defaults AgentDefaults `json:"defaults"` } type AgentDefaults struct { Workspace string `json:"workspace" env:"PICOCLAW_AGENTS_DEFAULTS_WORKSPACE"` Model string `json:"model" env:"PICOCLAW_AGENTS_DEFAULTS_MODEL"` MaxTokens int `json:"max_tokens" env:"PICOCLAW_AGENTS_DEFAULTS_MAX_TOKENS"` Temperature float64 `json:"temperature" env:"PICOCLAW_AGENTS_DEFAULTS_TEMPERATURE"` MaxToolIterations int `json:"max_tool_iterations" env:"PICOCLAW_AGENTS_DEFAULTS_MAX_TOOL_ITERATIONS"` } type ChannelsConfig struct { WhatsApp WhatsAppConfig `json:"whatsapp"` Telegram TelegramConfig `json:"telegram"` Feishu FeishuConfig `json:"feishu"` Discord DiscordConfig `json:"discord"` MaixCam MaixCamConfig `json:"maixcam"` DingTalk DingTalkConfig `json:"dingtalk"` } type WhatsAppConfig struct { Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_WHATSAPP_ENABLED"` BridgeURL string `json:"bridge_url" env:"PICOCLAW_CHANNELS_WHATSAPP_BRIDGE_URL"` AllowFrom []string `json:"allow_from" env:"PICOCLAW_CHANNELS_WHATSAPP_ALLOW_FROM"` } type TelegramConfig struct { Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_TELEGRAM_ENABLED"` Token string `json:"token" env:"PICOCLAW_CHANNELS_TELEGRAM_TOKEN"` AllowFrom []string `json:"allow_from" env:"PICOCLAW_CHANNELS_TELEGRAM_ALLOW_FROM"` } type FeishuConfig struct { Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_FEISHU_ENABLED"` AppID string `json:"app_id" env:"PICOCLAW_CHANNELS_FEISHU_APP_ID"` AppSecret string `json:"app_secret" env:"PICOCLAW_CHANNELS_FEISHU_APP_SECRET"` EncryptKey string `json:"encrypt_key" env:"PICOCLAW_CHANNELS_FEISHU_ENCRYPT_KEY"` VerificationToken string `json:"verification_token" env:"PICOCLAW_CHANNELS_FEISHU_VERIFICATION_TOKEN"` AllowFrom []string `json:"allow_from" env:"PICOCLAW_CHANNELS_FEISHU_ALLOW_FROM"` } type DiscordConfig struct { Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_DISCORD_ENABLED"` Token string `json:"token" env:"PICOCLAW_CHANNELS_DISCORD_TOKEN"` AllowFrom []string `json:"allow_from" env:"PICOCLAW_CHANNELS_DISCORD_ALLOW_FROM"` } type MaixCamConfig struct { Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_MAIXCAM_ENABLED"` Host string `json:"host" env:"PICOCLAW_CHANNELS_MAIXCAM_HOST"` Port int `json:"port" env:"PICOCLAW_CHANNELS_MAIXCAM_PORT"` AllowFrom []string `json:"allow_from" env:"PICOCLAW_CHANNELS_MAIXCAM_ALLOW_FROM"` } type DingTalkConfig struct { Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_DINGTALK_ENABLED"` ClientID string `json:"client_id" env:"PICOCLAW_CHANNELS_DINGTALK_CLIENT_ID"` ClientSecret string `json:"client_secret" env:"PICOCLAW_CHANNELS_DINGTALK_CLIENT_SECRET"` AllowFrom []string `json:"allow_from" env:"PICOCLAW_CHANNELS_DINGTALK_ALLOW_FROM"` } type ProvidersConfig struct { Anthropic ProviderConfig `json:"anthropic"` OpenAI ProviderConfig `json:"openai"` OpenRouter ProviderConfig `json:"openrouter"` Groq ProviderConfig `json:"groq"` Zhipu ProviderConfig `json:"zhipu"` VLLM ProviderConfig `json:"vllm"` Gemini ProviderConfig `json:"gemini"` } type ProviderConfig struct { APIKey string `json:"api_key" env:"PICOCLAW_PROVIDERS_{{.Name}}_API_KEY"` APIBase string `json:"api_base" env:"PICOCLAW_PROVIDERS_{{.Name}}_API_BASE"` } type GatewayConfig struct { Host string `json:"host" env:"PICOCLAW_GATEWAY_HOST"` Port int `json:"port" env:"PICOCLAW_GATEWAY_PORT"` } type WebSearchConfig struct { APIKey string `json:"api_key" env:"PICOCLAW_TOOLS_WEB_SEARCH_API_KEY"` MaxResults int `json:"max_results" env:"PICOCLAW_TOOLS_WEB_SEARCH_MAX_RESULTS"` } type WebToolsConfig struct { Search WebSearchConfig `json:"search"` } type ToolsConfig struct { Web WebToolsConfig `json:"web"` } func DefaultConfig() *Config { return &Config{ Agents: AgentsConfig{ Defaults: AgentDefaults{ Workspace: "~/.picoclaw/workspace", Model: "glm-4.7", MaxTokens: 8192, Temperature: 0.7, MaxToolIterations: 20, }, }, Channels: ChannelsConfig{ WhatsApp: WhatsAppConfig{ Enabled: false, BridgeURL: "ws://localhost:3001", AllowFrom: []string{}, }, Telegram: TelegramConfig{ Enabled: false, Token: "", AllowFrom: []string{}, }, Feishu: FeishuConfig{ Enabled: false, AppID: "", AppSecret: "", EncryptKey: "", VerificationToken: "", AllowFrom: []string{}, }, Discord: DiscordConfig{ Enabled: false, Token: "", AllowFrom: []string{}, }, MaixCam: MaixCamConfig{ Enabled: false, Host: "0.0.0.0", Port: 18790, AllowFrom: []string{}, }, DingTalk: DingTalkConfig{ Enabled: false, ClientID: "", ClientSecret: "", AllowFrom: []string{}, }, }, Providers: ProvidersConfig{ Anthropic: ProviderConfig{}, OpenAI: ProviderConfig{}, OpenRouter: ProviderConfig{}, Groq: ProviderConfig{}, Zhipu: ProviderConfig{}, VLLM: ProviderConfig{}, Gemini: ProviderConfig{}, }, Gateway: GatewayConfig{ Host: "0.0.0.0", Port: 18790, }, Tools: ToolsConfig{ Web: WebToolsConfig{ Search: WebSearchConfig{ APIKey: "", MaxResults: 5, }, }, }, } } func LoadConfig(path string) (*Config, error) { cfg := DefaultConfig() data, err := os.ReadFile(path) if err != nil { if os.IsNotExist(err) { return cfg, nil } return nil, err } if err := json.Unmarshal(data, cfg); err != nil { return nil, err } if err := env.Parse(cfg); err != nil { return nil, err } return cfg, nil } func SaveConfig(path string, cfg *Config) error { cfg.mu.RLock() defer cfg.mu.RUnlock() data, err := json.MarshalIndent(cfg, "", " ") if err != nil { return err } dir := filepath.Dir(path) if err := os.MkdirAll(dir, 0755); err != nil { return err } return os.WriteFile(path, data, 0644) } func (c *Config) WorkspacePath() string { c.mu.RLock() defer c.mu.RUnlock() return expandHome(c.Agents.Defaults.Workspace) } func (c *Config) GetAPIKey() string { c.mu.RLock() defer c.mu.RUnlock() if c.Providers.OpenRouter.APIKey != "" { return c.Providers.OpenRouter.APIKey } if c.Providers.Anthropic.APIKey != "" { return c.Providers.Anthropic.APIKey } if c.Providers.OpenAI.APIKey != "" { return c.Providers.OpenAI.APIKey } if c.Providers.Gemini.APIKey != "" { return c.Providers.Gemini.APIKey } if c.Providers.Zhipu.APIKey != "" { return c.Providers.Zhipu.APIKey } if c.Providers.Groq.APIKey != "" { return c.Providers.Groq.APIKey } if c.Providers.VLLM.APIKey != "" { return c.Providers.VLLM.APIKey } return "" } func (c *Config) GetAPIBase() string { c.mu.RLock() defer c.mu.RUnlock() if c.Providers.OpenRouter.APIKey != "" { if c.Providers.OpenRouter.APIBase != "" { return c.Providers.OpenRouter.APIBase } return "https://openrouter.ai/api/v1" } if c.Providers.Zhipu.APIKey != "" { return c.Providers.Zhipu.APIBase } if c.Providers.VLLM.APIKey != "" && c.Providers.VLLM.APIBase != "" { return c.Providers.VLLM.APIBase } return "" } func expandHome(path string) string { if path == "" { return path } if path[0] == '~' { home, _ := os.UserHomeDir() if len(path) > 1 && path[1] == '/' { return home + path[1:] } return home } return path }