Spaces:
Sleeping
Sleeping
| package config | |
| import ( | |
| "encoding/json" | |
| "fmt" | |
| "os" | |
| "path/filepath" | |
| "sync" | |
| "github.com/caarlos0/env/v11" | |
| ) | |
| // FlexibleStringSlice is a []string that also accepts JSON numbers, | |
| // so allow_from can contain both "123" and 123. | |
| type FlexibleStringSlice []string | |
| func (f *FlexibleStringSlice) UnmarshalJSON(data []byte) error { | |
| // Try []string first | |
| var ss []string | |
| if err := json.Unmarshal(data, &ss); err == nil { | |
| *f = ss | |
| return nil | |
| } | |
| // Try []interface{} to handle mixed types | |
| var raw []interface{} | |
| if err := json.Unmarshal(data, &raw); err != nil { | |
| return err | |
| } | |
| result := make([]string, 0, len(raw)) | |
| for _, v := range raw { | |
| switch val := v.(type) { | |
| case string: | |
| result = append(result, val) | |
| case float64: | |
| result = append(result, fmt.Sprintf("%.0f", val)) | |
| default: | |
| result = append(result, fmt.Sprintf("%v", val)) | |
| } | |
| } | |
| *f = result | |
| return nil | |
| } | |
| type Config struct { | |
| Agents AgentsConfig `json:"agents"` | |
| Channels ChannelsConfig `json:"channels"` | |
| Providers ProvidersConfig `json:"providers"` | |
| Gateway GatewayConfig `json:"gateway"` | |
| Tools ToolsConfig `json:"tools"` | |
| Heartbeat HeartbeatConfig `json:"heartbeat"` | |
| Devices DevicesConfig `json:"devices"` | |
| mu sync.RWMutex | |
| } | |
| func (c *Config) Update(newCfg *Config) { | |
| c.mu.Lock() | |
| defer c.mu.Unlock() | |
| // Copy fields | |
| c.Agents = newCfg.Agents | |
| c.Channels = newCfg.Channels | |
| c.Providers = newCfg.Providers | |
| c.Gateway = newCfg.Gateway | |
| c.Tools = newCfg.Tools | |
| c.Heartbeat = newCfg.Heartbeat | |
| c.Devices = newCfg.Devices | |
| } | |
| func (c *Config) GetAgentDefaults() AgentDefaults { | |
| c.mu.RLock() | |
| defer c.mu.RUnlock() | |
| return c.Agents.Defaults | |
| } | |
| type AgentsConfig struct { | |
| Defaults AgentDefaults `json:"defaults"` | |
| } | |
| type AgentDefaults struct { | |
| Workspace string `json:"workspace" env:"PICOCLAW_AGENTS_DEFAULTS_WORKSPACE"` | |
| RestrictToWorkspace bool `json:"restrict_to_workspace" env:"PICOCLAW_AGENTS_DEFAULTS_RESTRICT_TO_WORKSPACE"` | |
| Provider string `json:"provider" env:"PICOCLAW_AGENTS_DEFAULTS_PROVIDER"` | |
| Model string `json:"model" env:"PICOCLAW_AGENTS_DEFAULTS_MODEL"` | |
| FallbackModels []string `json:"fallback_models" env:"PICOCLAW_AGENTS_DEFAULTS_FALLBACK_MODELS" envSeparator:","` | |
| 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"` | |
| QQ QQConfig `json:"qq"` | |
| DingTalk DingTalkConfig `json:"dingtalk"` | |
| Slack SlackConfig `json:"slack"` | |
| LINE LINEConfig `json:"line"` | |
| OneBot OneBotConfig `json:"onebot"` | |
| } | |
| type WhatsAppConfig struct { | |
| Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_WHATSAPP_ENABLED"` | |
| BridgeURL string `json:"bridge_url" env:"PICOCLAW_CHANNELS_WHATSAPP_BRIDGE_URL"` | |
| AllowFrom FlexibleStringSlice `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"` | |
| Proxy string `json:"proxy" env:"PICOCLAW_CHANNELS_TELEGRAM_PROXY"` | |
| AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_TELEGRAM_ALLOW_FROM"` | |
| // Webhook configuration for environments with restricted egress (e.g., Hugging Face Spaces) | |
| WebhookEnabled bool `json:"webhook_enabled" env:"PICOCLAW_CHANNELS_TELEGRAM_WEBHOOK_ENABLED"` | |
| WebhookURL string `json:"webhook_url" env:"PICOCLAW_CHANNELS_TELEGRAM_WEBHOOK_URL"` | |
| WebhookSecret string `json:"webhook_secret" env:"PICOCLAW_CHANNELS_TELEGRAM_WEBHOOK_SECRET"` | |
| WebhookPath string `json:"webhook_path" env:"PICOCLAW_CHANNELS_TELEGRAM_WEBHOOK_PATH"` | |
| } | |
| 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 FlexibleStringSlice `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 FlexibleStringSlice `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 FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_MAIXCAM_ALLOW_FROM"` | |
| } | |
| type QQConfig struct { | |
| Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_QQ_ENABLED"` | |
| AppID string `json:"app_id" env:"PICOCLAW_CHANNELS_QQ_APP_ID"` | |
| AppSecret string `json:"app_secret" env:"PICOCLAW_CHANNELS_QQ_APP_SECRET"` | |
| AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_QQ_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 FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_DINGTALK_ALLOW_FROM"` | |
| } | |
| type SlackConfig struct { | |
| Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_SLACK_ENABLED"` | |
| BotToken string `json:"bot_token" env:"PICOCLAW_CHANNELS_SLACK_BOT_TOKEN"` | |
| AppToken string `json:"app_token" env:"PICOCLAW_CHANNELS_SLACK_APP_TOKEN"` | |
| AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_SLACK_ALLOW_FROM"` | |
| } | |
| type LINEConfig struct { | |
| Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_LINE_ENABLED"` | |
| ChannelSecret string `json:"channel_secret" env:"PICOCLAW_CHANNELS_LINE_CHANNEL_SECRET"` | |
| ChannelAccessToken string `json:"channel_access_token" env:"PICOCLAW_CHANNELS_LINE_CHANNEL_ACCESS_TOKEN"` | |
| WebhookHost string `json:"webhook_host" env:"PICOCLAW_CHANNELS_LINE_WEBHOOK_HOST"` | |
| WebhookPort int `json:"webhook_port" env:"PICOCLAW_CHANNELS_LINE_WEBHOOK_PORT"` | |
| WebhookPath string `json:"webhook_path" env:"PICOCLAW_CHANNELS_LINE_WEBHOOK_PATH"` | |
| AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_LINE_ALLOW_FROM"` | |
| } | |
| type OneBotConfig struct { | |
| Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_ONEBOT_ENABLED"` | |
| WSUrl string `json:"ws_url" env:"PICOCLAW_CHANNELS_ONEBOT_WS_URL"` | |
| AccessToken string `json:"access_token" env:"PICOCLAW_CHANNELS_ONEBOT_ACCESS_TOKEN"` | |
| ReconnectInterval int `json:"reconnect_interval" env:"PICOCLAW_CHANNELS_ONEBOT_RECONNECT_INTERVAL"` | |
| GroupTriggerPrefix []string `json:"group_trigger_prefix" env:"PICOCLAW_CHANNELS_ONEBOT_GROUP_TRIGGER_PREFIX"` | |
| AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_ONEBOT_ALLOW_FROM"` | |
| } | |
| type HeartbeatConfig struct { | |
| Enabled bool `json:"enabled" env:"PICOCLAW_HEARTBEAT_ENABLED"` | |
| Interval int `json:"interval" env:"PICOCLAW_HEARTBEAT_INTERVAL"` // minutes, min 5 | |
| } | |
| type DevicesConfig struct { | |
| Enabled bool `json:"enabled" env:"PICOCLAW_DEVICES_ENABLED"` | |
| MonitorUSB bool `json:"monitor_usb" env:"PICOCLAW_DEVICES_MONITOR_USB"` | |
| } | |
| type ProvidersConfig struct { | |
| Anthropic ProviderConfig `json:"anthropic" envPrefix:"PICOCLAW_PROVIDERS_ANTHROPIC_"` | |
| OpenAI ProviderConfig `json:"openai" envPrefix:"PICOCLAW_PROVIDERS_OPENAI_"` | |
| OpenRouter ProviderConfig `json:"openrouter" envPrefix:"PICOCLAW_PROVIDERS_OPENROUTER_"` | |
| Groq ProviderConfig `json:"groq" envPrefix:"PICOCLAW_PROVIDERS_GROQ_"` | |
| Zhipu ProviderConfig `json:"zhipu" envPrefix:"PICOCLAW_PROVIDERS_ZHIPU_"` | |
| VLLM ProviderConfig `json:"vllm" envPrefix:"PICOCLAW_PROVIDERS_VLLM_"` | |
| Gemini ProviderConfig `json:"gemini" envPrefix:"PICOCLAW_PROVIDERS_GEMINI_"` | |
| Nvidia ProviderConfig `json:"nvidia" envPrefix:"PICOCLAW_PROVIDERS_NVIDIA_"` | |
| Ollama ProviderConfig `json:"ollama" envPrefix:"PICOCLAW_PROVIDERS_OLLAMA_"` | |
| Moonshot ProviderConfig `json:"moonshot" envPrefix:"PICOCLAW_PROVIDERS_MOONSHOT_"` | |
| ShengSuanYun ProviderConfig `json:"shengsuanyun" envPrefix:"PICOCLAW_PROVIDERS_SHENGSUANYUN_"` | |
| DeepSeek ProviderConfig `json:"deepseek" envPrefix:"PICOCLAW_PROVIDERS_DEEPSEEK_"` | |
| StepFun ProviderConfig `json:"stepfun" envPrefix:"PICOCLAW_PROVIDERS_STEPFUN_"` | |
| GitHubCopilot ProviderConfig `json:"github_copilot" envPrefix:"PICOCLAW_PROVIDERS_GITHUB_COPILOT_"` | |
| } | |
| type ProviderConfig struct { | |
| APIKey string `json:"api_key" env:"API_KEY"` | |
| APIBase string `json:"api_base" env:"API_BASE"` | |
| Proxy string `json:"proxy,omitempty" env:"PROXY"` | |
| AuthMethod string `json:"auth_method,omitempty" env:"AUTH_METHOD"` | |
| ConnectMode string `json:"connect_mode,omitempty" env:"CONNECT_MODE"` //only for Github Copilot, `stdio` or `grpc` | |
| } | |
| type GatewayConfig struct { | |
| Host string `json:"host" env:"PICOCLAW_GATEWAY_HOST"` | |
| Port int `json:"port" env:"PICOCLAW_GATEWAY_PORT"` | |
| } | |
| type BraveConfig struct { | |
| Enabled bool `json:"enabled" env:"PICOCLAW_TOOLS_WEB_BRAVE_ENABLED"` | |
| APIKey string `json:"api_key" env:"PICOCLAW_TOOLS_WEB_BRAVE_API_KEY"` | |
| MaxResults int `json:"max_results" env:"PICOCLAW_TOOLS_WEB_BRAVE_MAX_RESULTS"` | |
| } | |
| type DuckDuckGoConfig struct { | |
| Enabled bool `json:"enabled" env:"PICOCLAW_TOOLS_WEB_DUCKDUCKGO_ENABLED"` | |
| MaxResults int `json:"max_results" env:"PICOCLAW_TOOLS_WEB_DUCKDUCKGO_MAX_RESULTS"` | |
| } | |
| type WebToolsConfig struct { | |
| Brave BraveConfig `json:"brave"` | |
| DuckDuckGo DuckDuckGoConfig `json:"duckduckgo"` | |
| } | |
| type ResourceLimits struct { | |
| MaxExecutionTime int `json:"max_execution_time" env:"PICOCLAW_TOOLS_MAX_EXECUTION_TIME"` // seconds | |
| MaxMemory string `json:"max_memory" env:"PICOCLAW_TOOLS_MAX_MEMORY"` // e.g., "512MB", "1GB" | |
| MaxOutputSize int `json:"max_output_size" env:"PICOCLAW_TOOLS_MAX_OUTPUT_SIZE"` // bytes | |
| AllowedEnvVars []string `json:"allowed_env_vars" env:"PICOCLAW_TOOLS_ALLOWED_ENV_VARS"` | |
| } | |
| type ToolsConfig struct { | |
| Web WebToolsConfig `json:"web"` | |
| ResourceLimits ResourceLimits `json:"resource_limits"` | |
| } | |
| func DefaultConfig() *Config { | |
| return &Config{ | |
| Agents: AgentsConfig{ | |
| Defaults: AgentDefaults{ | |
| Workspace: "~/.picoclaw/workspace", | |
| RestrictToWorkspace: false, | |
| Provider: "openrouter", | |
| Model: "nvidia/nemotron-3-nano-30b-a3b:free", | |
| FallbackModels: []string{ | |
| "arcee-ai/trinity-large-preview:free", | |
| }, | |
| MaxTokens: 8192, | |
| Temperature: 0.7, | |
| MaxToolIterations: 50, | |
| }, | |
| }, | |
| Channels: ChannelsConfig{ | |
| WhatsApp: WhatsAppConfig{ | |
| Enabled: false, | |
| BridgeURL: "ws://localhost:3001", | |
| AllowFrom: FlexibleStringSlice{}, | |
| }, | |
| Telegram: TelegramConfig{ | |
| Enabled: false, | |
| Token: "", | |
| AllowFrom: FlexibleStringSlice{}, | |
| }, | |
| Feishu: FeishuConfig{ | |
| Enabled: false, | |
| AppID: "", | |
| AppSecret: "", | |
| EncryptKey: "", | |
| VerificationToken: "", | |
| AllowFrom: FlexibleStringSlice{}, | |
| }, | |
| Discord: DiscordConfig{ | |
| Enabled: false, | |
| Token: "", | |
| AllowFrom: FlexibleStringSlice{}, | |
| }, | |
| MaixCam: MaixCamConfig{ | |
| Enabled: false, | |
| Host: "0.0.0.0", | |
| Port: 18790, | |
| AllowFrom: FlexibleStringSlice{}, | |
| }, | |
| QQ: QQConfig{ | |
| Enabled: false, | |
| AppID: "", | |
| AppSecret: "", | |
| AllowFrom: FlexibleStringSlice{}, | |
| }, | |
| DingTalk: DingTalkConfig{ | |
| Enabled: false, | |
| ClientID: "", | |
| ClientSecret: "", | |
| AllowFrom: FlexibleStringSlice{}, | |
| }, | |
| Slack: SlackConfig{ | |
| Enabled: false, | |
| BotToken: "", | |
| AppToken: "", | |
| AllowFrom: FlexibleStringSlice{}, | |
| }, | |
| LINE: LINEConfig{ | |
| Enabled: false, | |
| ChannelSecret: "", | |
| ChannelAccessToken: "", | |
| WebhookHost: "0.0.0.0", | |
| WebhookPort: 18791, | |
| WebhookPath: "/webhook/line", | |
| AllowFrom: FlexibleStringSlice{}, | |
| }, | |
| OneBot: OneBotConfig{ | |
| Enabled: false, | |
| WSUrl: "ws://127.0.0.1:3001", | |
| AccessToken: "", | |
| ReconnectInterval: 5, | |
| GroupTriggerPrefix: []string{}, | |
| AllowFrom: FlexibleStringSlice{}, | |
| }, | |
| }, | |
| Providers: ProvidersConfig{ | |
| Anthropic: ProviderConfig{}, | |
| OpenAI: ProviderConfig{}, | |
| OpenRouter: ProviderConfig{}, | |
| Groq: ProviderConfig{}, | |
| Zhipu: ProviderConfig{}, | |
| VLLM: ProviderConfig{}, | |
| Gemini: ProviderConfig{}, | |
| Nvidia: ProviderConfig{}, | |
| Moonshot: ProviderConfig{}, | |
| ShengSuanYun: ProviderConfig{}, | |
| }, | |
| Gateway: GatewayConfig{ | |
| Host: "0.0.0.0", | |
| Port: 18790, | |
| }, | |
| Tools: ToolsConfig{ | |
| Web: WebToolsConfig{ | |
| Brave: BraveConfig{ | |
| Enabled: false, | |
| APIKey: "", | |
| MaxResults: 5, | |
| }, | |
| DuckDuckGo: DuckDuckGoConfig{ | |
| Enabled: true, | |
| MaxResults: 5, | |
| }, | |
| }, | |
| }, | |
| Heartbeat: HeartbeatConfig{ | |
| Enabled: true, | |
| Interval: 30, // default 30 minutes | |
| }, | |
| Devices: DevicesConfig{ | |
| Enabled: false, | |
| MonitorUSB: true, | |
| }, | |
| } | |
| } | |
| 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, 0600) | |
| } | |
| 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 | |
| } | |
| if c.Providers.ShengSuanYun.APIKey != "" { | |
| return c.Providers.ShengSuanYun.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 | |
| } | |