From 14e80919a87ffd3abaab4f5f6b7f2ca8fa46c1a6 Mon Sep 17 00:00:00 2001 From: avaksru Date: Mon, 23 Mar 2026 10:37:13 +0300 Subject: [PATCH] add web (launcher) --- pkg/config/config.go | 1883 +++++++++++++---- web/backend/api/channels.go | 1 + web/backend/api/config.go | 13 + .../channels/channel-config-page.tsx | 4 + .../channels/channel-forms/generic-form.tsx | 12 + .../src/hooks/use-sidebar-channels.ts | 1 + web/frontend/src/i18n/locales/en.json | 12 +- web/frontend/src/i18n/locales/zh.json | 12 +- 8 files changed, 1471 insertions(+), 467 deletions(-) diff --git a/pkg/config/config.go b/pkg/config/config.go index dcc0715d3..840c598d3 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -15,759 +15,1257 @@ import ( ) // rrCounter is a global counter for round-robin load balancing across models. + var rrCounter atomic.Uint64 // FlexibleStringSlice is a []string that also accepts JSON numbers, + // so allow_from can contain both "123" and 123. + // It also supports parsing comma-separated strings from environment variables, + // including both English (,) and Chinese (,) commas. + 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 []any + 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 + } // UnmarshalText implements encoding.TextUnmarshaler to support env variable parsing. + // It handles comma-separated values with both English (,) and Chinese (,) commas. + func (f *FlexibleStringSlice) UnmarshalText(text []byte) error { + if len(text) == 0 { + *f = nil + return nil + } s := string(text) + // Replace Chinese comma with English comma, then split + s = strings.ReplaceAll(s, ",", ",") + parts := strings.Split(s, ",") result := make([]string, 0, len(parts)) + for _, part := range parts { + part = strings.TrimSpace(part) + if part != "" { + result = append(result, part) + } + } + *f = result + return nil + } type Config struct { - Agents AgentsConfig `json:"agents"` - Bindings []AgentBinding `json:"bindings,omitempty"` - Session SessionConfig `json:"session,omitempty"` - Channels ChannelsConfig `json:"channels"` + Agents AgentsConfig `json:"agents"` + + Bindings []AgentBinding `json:"bindings,omitempty"` + + Session SessionConfig `json:"session,omitempty"` + + Channels ChannelsConfig `json:"channels"` + Providers ProvidersConfig `json:"providers,omitempty"` - ModelList []ModelConfig `json:"model_list"` // New model-centric provider configuration - Gateway GatewayConfig `json:"gateway"` - Hooks HooksConfig `json:"hooks,omitempty"` - Tools ToolsConfig `json:"tools"` + + ModelList []ModelConfig `json:"model_list"` // New model-centric provider configuration + + Gateway GatewayConfig `json:"gateway"` + + Hooks HooksConfig `json:"hooks,omitempty"` + + Tools ToolsConfig `json:"tools"` + Heartbeat HeartbeatConfig `json:"heartbeat"` - Devices DevicesConfig `json:"devices"` - Voice VoiceConfig `json:"voice"` + + Devices DevicesConfig `json:"devices"` + + Voice VoiceConfig `json:"voice"` + // BuildInfo contains build-time version information + BuildInfo BuildInfo `json:"build_info,omitempty"` } type HooksConfig struct { - Enabled bool `json:"enabled"` - Defaults HookDefaultsConfig `json:"defaults,omitempty"` - Builtins map[string]BuiltinHookConfig `json:"builtins,omitempty"` + Enabled bool `json:"enabled"` + + Defaults HookDefaultsConfig `json:"defaults,omitempty"` + + Builtins map[string]BuiltinHookConfig `json:"builtins,omitempty"` + Processes map[string]ProcessHookConfig `json:"processes,omitempty"` } type HookDefaultsConfig struct { - ObserverTimeoutMS int `json:"observer_timeout_ms,omitempty"` + ObserverTimeoutMS int `json:"observer_timeout_ms,omitempty"` + InterceptorTimeoutMS int `json:"interceptor_timeout_ms,omitempty"` - ApprovalTimeoutMS int `json:"approval_timeout_ms,omitempty"` + + ApprovalTimeoutMS int `json:"approval_timeout_ms,omitempty"` } type BuiltinHookConfig struct { - Enabled bool `json:"enabled"` - Priority int `json:"priority,omitempty"` - Config json.RawMessage `json:"config,omitempty"` + Enabled bool `json:"enabled"` + + Priority int `json:"priority,omitempty"` + + Config json.RawMessage `json:"config,omitempty"` } type ProcessHookConfig struct { - Enabled bool `json:"enabled"` - Priority int `json:"priority,omitempty"` - Transport string `json:"transport,omitempty"` - Command []string `json:"command,omitempty"` - Dir string `json:"dir,omitempty"` - Env map[string]string `json:"env,omitempty"` - Observe []string `json:"observe,omitempty"` - Intercept []string `json:"intercept,omitempty"` + Enabled bool `json:"enabled"` + + Priority int `json:"priority,omitempty"` + + Transport string `json:"transport,omitempty"` + + Command []string `json:"command,omitempty"` + + Dir string `json:"dir,omitempty"` + + Env map[string]string `json:"env,omitempty"` + + Observe []string `json:"observe,omitempty"` + + Intercept []string `json:"intercept,omitempty"` } // BuildInfo contains build-time version information + type BuildInfo struct { - Version string `json:"version"` + Version string `json:"version"` + GitCommit string `json:"git_commit"` + BuildTime string `json:"build_time"` + GoVersion string `json:"go_version"` } // MarshalJSON implements custom JSON marshaling for Config + // to omit providers section when empty and session when empty + func (c Config) MarshalJSON() ([]byte, error) { + type Alias Config + aux := &struct { Providers *ProvidersConfig `json:"providers,omitempty"` - Session *SessionConfig `json:"session,omitempty"` + + Session *SessionConfig `json:"session,omitempty"` + *Alias }{ + Alias: (*Alias)(&c), } // Only include providers if not empty + if !c.Providers.IsEmpty() { + aux.Providers = &c.Providers + } // Only include session if not empty + if c.Session.DMScope != "" || len(c.Session.IdentityLinks) > 0 { + aux.Session = &c.Session + } return json.Marshal(aux) + } type AgentsConfig struct { Defaults AgentDefaults `json:"defaults"` - List []AgentConfig `json:"list,omitempty"` + + List []AgentConfig `json:"list,omitempty"` } // AgentModelConfig supports both string and structured model config. + // String format: "gpt-4" (just primary, no fallbacks) + // Object format: {"primary": "gpt-4", "fallbacks": ["claude-haiku"]} + type AgentModelConfig struct { - Primary string `json:"primary,omitempty"` + Primary string `json:"primary,omitempty"` + Fallbacks []string `json:"fallbacks,omitempty"` } func (m *AgentModelConfig) UnmarshalJSON(data []byte) error { + var s string + if err := json.Unmarshal(data, &s); err == nil { + m.Primary = s + m.Fallbacks = nil + return nil + } + type raw struct { - Primary string `json:"primary"` + Primary string `json:"primary"` + Fallbacks []string `json:"fallbacks"` } + var r raw + if err := json.Unmarshal(data, &r); err != nil { + return err + } + m.Primary = r.Primary + m.Fallbacks = r.Fallbacks + return nil + } func (m AgentModelConfig) MarshalJSON() ([]byte, error) { + if len(m.Fallbacks) == 0 && m.Primary != "" { + return json.Marshal(m.Primary) + } + type raw struct { - Primary string `json:"primary,omitempty"` + Primary string `json:"primary,omitempty"` + Fallbacks []string `json:"fallbacks,omitempty"` } + return json.Marshal(raw{Primary: m.Primary, Fallbacks: m.Fallbacks}) + } type AgentConfig struct { - ID string `json:"id"` - Default bool `json:"default,omitempty"` - Name string `json:"name,omitempty"` - Workspace string `json:"workspace,omitempty"` - Model *AgentModelConfig `json:"model,omitempty"` - Skills []string `json:"skills,omitempty"` - Subagents *SubagentsConfig `json:"subagents,omitempty"` + ID string `json:"id"` + + Default bool `json:"default,omitempty"` + + Name string `json:"name,omitempty"` + + Workspace string `json:"workspace,omitempty"` + + Model *AgentModelConfig `json:"model,omitempty"` + + Skills []string `json:"skills,omitempty"` + + Subagents *SubagentsConfig `json:"subagents,omitempty"` } type SubagentsConfig struct { - AllowAgents []string `json:"allow_agents,omitempty"` - Model *AgentModelConfig `json:"model,omitempty"` + AllowAgents []string `json:"allow_agents,omitempty"` + + Model *AgentModelConfig `json:"model,omitempty"` } type PeerMatch struct { Kind string `json:"kind"` - ID string `json:"id"` + + ID string `json:"id"` } type BindingMatch struct { - Channel string `json:"channel"` - AccountID string `json:"account_id,omitempty"` - Peer *PeerMatch `json:"peer,omitempty"` - GuildID string `json:"guild_id,omitempty"` - TeamID string `json:"team_id,omitempty"` + Channel string `json:"channel"` + + AccountID string `json:"account_id,omitempty"` + + Peer *PeerMatch `json:"peer,omitempty"` + + GuildID string `json:"guild_id,omitempty"` + + TeamID string `json:"team_id,omitempty"` } type AgentBinding struct { - AgentID string `json:"agent_id"` - Match BindingMatch `json:"match"` + AgentID string `json:"agent_id"` + + Match BindingMatch `json:"match"` } type SessionConfig struct { - DMScope string `json:"dm_scope,omitempty"` + DMScope string `json:"dm_scope,omitempty"` + IdentityLinks map[string][]string `json:"identity_links,omitempty"` } // RoutingConfig controls the intelligent model routing feature. + // When enabled, each incoming message is scored against structural features + // (message length, code blocks, tool call history, conversation depth, attachments). + // Messages scoring below Threshold are sent to LightModel; all others use the + // agent's primary model. This reduces cost and latency for simple tasks without + // requiring any keyword matching — all scoring is language-agnostic. + type RoutingConfig struct { - Enabled bool `json:"enabled"` - LightModel string `json:"light_model"` // model_name from model_list to use for simple tasks - Threshold float64 `json:"threshold"` // complexity score in [0,1]; score >= threshold → primary model + Enabled bool `json:"enabled"` + + LightModel string `json:"light_model"` // model_name from model_list to use for simple tasks + + Threshold float64 `json:"threshold"` // complexity score in [0,1]; score >= threshold → primary model + } // SubTurnConfig configures the SubTurn execution system. + type SubTurnConfig struct { - MaxDepth int `json:"max_depth" env:"PICOCLAW_AGENTS_DEFAULTS_SUBTURN_MAX_DEPTH"` - MaxConcurrent int `json:"max_concurrent" env:"PICOCLAW_AGENTS_DEFAULTS_SUBTURN_MAX_CONCURRENT"` + MaxDepth int `json:"max_depth" env:"PICOCLAW_AGENTS_DEFAULTS_SUBTURN_MAX_DEPTH"` + + MaxConcurrent int `json:"max_concurrent" env:"PICOCLAW_AGENTS_DEFAULTS_SUBTURN_MAX_CONCURRENT"` + DefaultTimeoutMinutes int `json:"default_timeout_minutes" env:"PICOCLAW_AGENTS_DEFAULTS_SUBTURN_DEFAULT_TIMEOUT_MINUTES"` - DefaultTokenBudget int `json:"default_token_budget" env:"PICOCLAW_AGENTS_DEFAULTS_SUBTURN_DEFAULT_TOKEN_BUDGET"` + + DefaultTokenBudget int `json:"default_token_budget" env:"PICOCLAW_AGENTS_DEFAULTS_SUBTURN_DEFAULT_TOKEN_BUDGET"` + ConcurrencyTimeoutSec int `json:"concurrency_timeout_sec" env:"PICOCLAW_AGENTS_DEFAULTS_SUBTURN_CONCURRENCY_TIMEOUT_SEC"` } type ToolFeedbackConfig struct { - Enabled bool `json:"enabled" env:"PICOCLAW_AGENTS_DEFAULTS_TOOL_FEEDBACK_ENABLED"` - MaxArgsLength int `json:"max_args_length" env:"PICOCLAW_AGENTS_DEFAULTS_TOOL_FEEDBACK_MAX_ARGS_LENGTH"` + Enabled bool `json:"enabled" env:"PICOCLAW_AGENTS_DEFAULTS_TOOL_FEEDBACK_ENABLED"` + + MaxArgsLength int `json:"max_args_length" env:"PICOCLAW_AGENTS_DEFAULTS_TOOL_FEEDBACK_MAX_ARGS_LENGTH"` } 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"` - AllowReadOutsideWorkspace bool `json:"allow_read_outside_workspace" env:"PICOCLAW_AGENTS_DEFAULTS_ALLOW_READ_OUTSIDE_WORKSPACE"` - Provider string `json:"provider" env:"PICOCLAW_AGENTS_DEFAULTS_PROVIDER"` - ModelName string `json:"model_name" env:"PICOCLAW_AGENTS_DEFAULTS_MODEL_NAME"` - Model string `json:"model,omitempty" env:"PICOCLAW_AGENTS_DEFAULTS_MODEL"` // Deprecated: use model_name instead - ModelFallbacks []string `json:"model_fallbacks,omitempty"` - ImageModel string `json:"image_model,omitempty" env:"PICOCLAW_AGENTS_DEFAULTS_IMAGE_MODEL"` - ImageModelFallbacks []string `json:"image_model_fallbacks,omitempty"` - MaxTokens int `json:"max_tokens" env:"PICOCLAW_AGENTS_DEFAULTS_MAX_TOKENS"` - ContextWindow int `json:"context_window,omitempty" env:"PICOCLAW_AGENTS_DEFAULTS_CONTEXT_WINDOW"` - Temperature *float64 `json:"temperature,omitempty" env:"PICOCLAW_AGENTS_DEFAULTS_TEMPERATURE"` - MaxToolIterations int `json:"max_tool_iterations" env:"PICOCLAW_AGENTS_DEFAULTS_MAX_TOOL_ITERATIONS"` - SummarizeMessageThreshold int `json:"summarize_message_threshold" env:"PICOCLAW_AGENTS_DEFAULTS_SUMMARIZE_MESSAGE_THRESHOLD"` - SummarizeTokenPercent int `json:"summarize_token_percent" env:"PICOCLAW_AGENTS_DEFAULTS_SUMMARIZE_TOKEN_PERCENT"` - MaxMediaSize int `json:"max_media_size,omitempty" env:"PICOCLAW_AGENTS_DEFAULTS_MAX_MEDIA_SIZE"` - Routing *RoutingConfig `json:"routing,omitempty"` - SteeringMode string `json:"steering_mode,omitempty" env:"PICOCLAW_AGENTS_DEFAULTS_STEERING_MODE"` // "one-at-a-time" (default) or "all" - SubTurn SubTurnConfig `json:"subturn" envPrefix:"PICOCLAW_AGENTS_DEFAULTS_SUBTURN_"` - ToolFeedback ToolFeedbackConfig `json:"tool_feedback,omitempty"` + Workspace string `json:"workspace" env:"PICOCLAW_AGENTS_DEFAULTS_WORKSPACE"` + + RestrictToWorkspace bool `json:"restrict_to_workspace" env:"PICOCLAW_AGENTS_DEFAULTS_RESTRICT_TO_WORKSPACE"` + + AllowReadOutsideWorkspace bool `json:"allow_read_outside_workspace" env:"PICOCLAW_AGENTS_DEFAULTS_ALLOW_READ_OUTSIDE_WORKSPACE"` + + Provider string `json:"provider" env:"PICOCLAW_AGENTS_DEFAULTS_PROVIDER"` + + ModelName string `json:"model_name" env:"PICOCLAW_AGENTS_DEFAULTS_MODEL_NAME"` + + Model string `json:"model,omitempty" env:"PICOCLAW_AGENTS_DEFAULTS_MODEL"` // Deprecated: use model_name instead + + ModelFallbacks []string `json:"model_fallbacks,omitempty"` + + ImageModel string `json:"image_model,omitempty" env:"PICOCLAW_AGENTS_DEFAULTS_IMAGE_MODEL"` + + ImageModelFallbacks []string `json:"image_model_fallbacks,omitempty"` + + MaxTokens int `json:"max_tokens" env:"PICOCLAW_AGENTS_DEFAULTS_MAX_TOKENS"` + + ContextWindow int `json:"context_window,omitempty" env:"PICOCLAW_AGENTS_DEFAULTS_CONTEXT_WINDOW"` + + Temperature *float64 `json:"temperature,omitempty" env:"PICOCLAW_AGENTS_DEFAULTS_TEMPERATURE"` + + MaxToolIterations int `json:"max_tool_iterations" env:"PICOCLAW_AGENTS_DEFAULTS_MAX_TOOL_ITERATIONS"` + + SummarizeMessageThreshold int `json:"summarize_message_threshold" env:"PICOCLAW_AGENTS_DEFAULTS_SUMMARIZE_MESSAGE_THRESHOLD"` + + SummarizeTokenPercent int `json:"summarize_token_percent" env:"PICOCLAW_AGENTS_DEFAULTS_SUMMARIZE_TOKEN_PERCENT"` + + MaxMediaSize int `json:"max_media_size,omitempty" env:"PICOCLAW_AGENTS_DEFAULTS_MAX_MEDIA_SIZE"` + + Routing *RoutingConfig `json:"routing,omitempty"` + + SteeringMode string `json:"steering_mode,omitempty" env:"PICOCLAW_AGENTS_DEFAULTS_STEERING_MODE"` // "one-at-a-time" (default) or "all" + + SubTurn SubTurnConfig `json:"subturn" envPrefix:"PICOCLAW_AGENTS_DEFAULTS_SUBTURN_"` + + ToolFeedback ToolFeedbackConfig `json:"tool_feedback,omitempty"` } const ( - DefaultMaxMediaSize = 20 * 1024 * 1024 // 20 MB + DefaultMaxMediaSize = 20 * 1024 * 1024 // 20 MB + DefaultWeComAIBotProcessingMessage = "⏳ Processing, please wait. The results will be sent shortly." ) func (d *AgentDefaults) GetMaxMediaSize() int { + if d.MaxMediaSize > 0 { + return d.MaxMediaSize + } + return DefaultMaxMediaSize + } // GetToolFeedbackMaxArgsLength returns the max args preview length for tool feedback messages. + func (d *AgentDefaults) GetToolFeedbackMaxArgsLength() int { + if d.ToolFeedback.MaxArgsLength > 0 { + return d.ToolFeedback.MaxArgsLength + } + return 300 + } // IsToolFeedbackEnabled returns true when tool feedback messages should be sent to the chat. + func (d *AgentDefaults) IsToolFeedbackEnabled() bool { + return d.ToolFeedback.Enabled + } // GetModelName returns the effective model name for the agent defaults. + // It prefers the new "model_name" field but falls back to "model" for backward compatibility. + func (d *AgentDefaults) GetModelName() string { + if d.ModelName != "" { + return d.ModelName + } + return d.Model + } 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"` - Matrix MatrixConfig `json:"matrix"` - LINE LINEConfig `json:"line"` - OneBot OneBotConfig `json:"onebot"` - WeCom WeComConfig `json:"wecom"` - WeComApp WeComAppConfig `json:"wecom_app"` + 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"` + + Matrix MatrixConfig `json:"matrix"` + + LINE LINEConfig `json:"line"` + + OneBot OneBotConfig `json:"onebot"` + + WeCom WeComConfig `json:"wecom"` + + WeComApp WeComAppConfig `json:"wecom_app"` + WeComAIBot WeComAIBotConfig `json:"wecom_aibot"` - Weixin WeixinConfig `json:"weixin"` - Pico PicoConfig `json:"pico"` + + Weixin WeixinConfig `json:"weixin"` + + Pico PicoConfig `json:"pico"` + PicoClient PicoClientConfig `json:"pico_client"` - IRC IRCConfig `json:"irc"` - MQTT MQTTConfig `json:"mqtt"` + + IRC IRCConfig `json:"irc"` + + MQTT MQTTConfig `json:"mqtt"` } // GroupTriggerConfig controls when the bot responds in group chats. + type GroupTriggerConfig struct { - MentionOnly bool `json:"mention_only,omitempty"` - Prefixes []string `json:"prefixes,omitempty"` + MentionOnly bool `json:"mention_only,omitempty"` + + Prefixes []string `json:"prefixes,omitempty"` } // TypingConfig controls typing indicator behavior (Phase 10). + type TypingConfig struct { Enabled bool `json:"enabled,omitempty"` } // PlaceholderConfig controls placeholder message behavior (Phase 10). + type PlaceholderConfig struct { - Enabled bool `json:"enabled,omitempty"` - Text string `json:"text,omitempty"` + Enabled bool `json:"enabled,omitempty"` + + Text string `json:"text,omitempty"` } type StreamingConfig struct { - Enabled bool `json:"enabled,omitempty" env:"PICOCLAW_CHANNELS_TELEGRAM_STREAMING_ENABLED"` - ThrottleSeconds int `json:"throttle_seconds,omitempty" env:"PICOCLAW_CHANNELS_TELEGRAM_STREAMING_THROTTLE_SECONDS"` - MinGrowthChars int `json:"min_growth_chars,omitempty" env:"PICOCLAW_CHANNELS_TELEGRAM_STREAMING_MIN_GROWTH_CHARS"` + Enabled bool `json:"enabled,omitempty" env:"PICOCLAW_CHANNELS_TELEGRAM_STREAMING_ENABLED"` + + ThrottleSeconds int `json:"throttle_seconds,omitempty" env:"PICOCLAW_CHANNELS_TELEGRAM_STREAMING_THROTTLE_SECONDS"` + + MinGrowthChars int `json:"min_growth_chars,omitempty" env:"PICOCLAW_CHANNELS_TELEGRAM_STREAMING_MIN_GROWTH_CHARS"` } type WhatsAppConfig struct { - Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_WHATSAPP_ENABLED"` - BridgeURL string `json:"bridge_url" env:"PICOCLAW_CHANNELS_WHATSAPP_BRIDGE_URL"` - UseNative bool `json:"use_native" env:"PICOCLAW_CHANNELS_WHATSAPP_USE_NATIVE"` - SessionStorePath string `json:"session_store_path" env:"PICOCLAW_CHANNELS_WHATSAPP_SESSION_STORE_PATH"` - AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_WHATSAPP_ALLOW_FROM"` - ReasoningChannelID string `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_WHATSAPP_REASONING_CHANNEL_ID"` + Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_WHATSAPP_ENABLED"` + + BridgeURL string `json:"bridge_url" env:"PICOCLAW_CHANNELS_WHATSAPP_BRIDGE_URL"` + + UseNative bool `json:"use_native" env:"PICOCLAW_CHANNELS_WHATSAPP_USE_NATIVE"` + + SessionStorePath string `json:"session_store_path" env:"PICOCLAW_CHANNELS_WHATSAPP_SESSION_STORE_PATH"` + + AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_WHATSAPP_ALLOW_FROM"` + + ReasoningChannelID string `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_WHATSAPP_REASONING_CHANNEL_ID"` } type TelegramConfig struct { - Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_TELEGRAM_ENABLED"` - Token string `json:"token" env:"PICOCLAW_CHANNELS_TELEGRAM_TOKEN"` - BaseURL string `json:"base_url" env:"PICOCLAW_CHANNELS_TELEGRAM_BASE_URL"` - Proxy string `json:"proxy" env:"PICOCLAW_CHANNELS_TELEGRAM_PROXY"` - AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_TELEGRAM_ALLOW_FROM"` - GroupTrigger GroupTriggerConfig `json:"group_trigger,omitempty"` - Typing TypingConfig `json:"typing,omitempty"` - Placeholder PlaceholderConfig `json:"placeholder,omitempty"` - Streaming StreamingConfig `json:"streaming,omitempty"` - ReasoningChannelID string `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_TELEGRAM_REASONING_CHANNEL_ID"` - UseMarkdownV2 bool `json:"use_markdown_v2" env:"PICOCLAW_CHANNELS_TELEGRAM_USE_MARKDOWN_V2"` + Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_TELEGRAM_ENABLED"` + + Token string `json:"token" env:"PICOCLAW_CHANNELS_TELEGRAM_TOKEN"` + + BaseURL string `json:"base_url" env:"PICOCLAW_CHANNELS_TELEGRAM_BASE_URL"` + + Proxy string `json:"proxy" env:"PICOCLAW_CHANNELS_TELEGRAM_PROXY"` + + AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_TELEGRAM_ALLOW_FROM"` + + GroupTrigger GroupTriggerConfig `json:"group_trigger,omitempty"` + + Typing TypingConfig `json:"typing,omitempty"` + + Placeholder PlaceholderConfig `json:"placeholder,omitempty"` + + Streaming StreamingConfig `json:"streaming,omitempty"` + + ReasoningChannelID string `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_TELEGRAM_REASONING_CHANNEL_ID"` + + UseMarkdownV2 bool `json:"use_markdown_v2" env:"PICOCLAW_CHANNELS_TELEGRAM_USE_MARKDOWN_V2"` } 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"` - GroupTrigger GroupTriggerConfig `json:"group_trigger,omitempty"` - Placeholder PlaceholderConfig `json:"placeholder,omitempty"` - ReasoningChannelID string `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_FEISHU_REASONING_CHANNEL_ID"` - RandomReactionEmoji FlexibleStringSlice `json:"random_reaction_emoji" env:"PICOCLAW_CHANNELS_FEISHU_RANDOM_REACTION_EMOJI"` - IsLark bool `json:"is_lark" env:"PICOCLAW_CHANNELS_FEISHU_IS_LARK"` + 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"` + + GroupTrigger GroupTriggerConfig `json:"group_trigger,omitempty"` + + Placeholder PlaceholderConfig `json:"placeholder,omitempty"` + + ReasoningChannelID string `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_FEISHU_REASONING_CHANNEL_ID"` + + RandomReactionEmoji FlexibleStringSlice `json:"random_reaction_emoji" env:"PICOCLAW_CHANNELS_FEISHU_RANDOM_REACTION_EMOJI"` + + IsLark bool `json:"is_lark" env:"PICOCLAW_CHANNELS_FEISHU_IS_LARK"` } type DiscordConfig struct { - Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_DISCORD_ENABLED"` - Token string `json:"token" env:"PICOCLAW_CHANNELS_DISCORD_TOKEN"` - Proxy string `json:"proxy" env:"PICOCLAW_CHANNELS_DISCORD_PROXY"` - AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_DISCORD_ALLOW_FROM"` - MentionOnly bool `json:"mention_only" env:"PICOCLAW_CHANNELS_DISCORD_MENTION_ONLY"` - GroupTrigger GroupTriggerConfig `json:"group_trigger,omitempty"` - Typing TypingConfig `json:"typing,omitempty"` - Placeholder PlaceholderConfig `json:"placeholder,omitempty"` - ReasoningChannelID string `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_DISCORD_REASONING_CHANNEL_ID"` + Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_DISCORD_ENABLED"` + + Token string `json:"token" env:"PICOCLAW_CHANNELS_DISCORD_TOKEN"` + + Proxy string `json:"proxy" env:"PICOCLAW_CHANNELS_DISCORD_PROXY"` + + AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_DISCORD_ALLOW_FROM"` + + MentionOnly bool `json:"mention_only" env:"PICOCLAW_CHANNELS_DISCORD_MENTION_ONLY"` + + GroupTrigger GroupTriggerConfig `json:"group_trigger,omitempty"` + + Typing TypingConfig `json:"typing,omitempty"` + + Placeholder PlaceholderConfig `json:"placeholder,omitempty"` + + ReasoningChannelID string `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_DISCORD_REASONING_CHANNEL_ID"` } 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"` - ReasoningChannelID string `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_MAIXCAM_REASONING_CHANNEL_ID"` + 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"` + + ReasoningChannelID string `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_MAIXCAM_REASONING_CHANNEL_ID"` } 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"` - GroupTrigger GroupTriggerConfig `json:"group_trigger,omitempty"` - MaxMessageLength int `json:"max_message_length" env:"PICOCLAW_CHANNELS_QQ_MAX_MESSAGE_LENGTH"` - MaxBase64FileSizeMiB int64 `json:"max_base64_file_size_mib" env:"PICOCLAW_CHANNELS_QQ_MAX_BASE64_FILE_SIZE_MIB"` - SendMarkdown bool `json:"send_markdown" env:"PICOCLAW_CHANNELS_QQ_SEND_MARKDOWN"` - ReasoningChannelID string `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_QQ_REASONING_CHANNEL_ID"` + 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"` + + GroupTrigger GroupTriggerConfig `json:"group_trigger,omitempty"` + + MaxMessageLength int `json:"max_message_length" env:"PICOCLAW_CHANNELS_QQ_MAX_MESSAGE_LENGTH"` + + MaxBase64FileSizeMiB int64 `json:"max_base64_file_size_mib" env:"PICOCLAW_CHANNELS_QQ_MAX_BASE64_FILE_SIZE_MIB"` + + SendMarkdown bool `json:"send_markdown" env:"PICOCLAW_CHANNELS_QQ_SEND_MARKDOWN"` + + ReasoningChannelID string `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_QQ_REASONING_CHANNEL_ID"` } 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"` - GroupTrigger GroupTriggerConfig `json:"group_trigger,omitempty"` - ReasoningChannelID string `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_DINGTALK_REASONING_CHANNEL_ID"` + 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"` + + GroupTrigger GroupTriggerConfig `json:"group_trigger,omitempty"` + + ReasoningChannelID string `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_DINGTALK_REASONING_CHANNEL_ID"` } 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"` - GroupTrigger GroupTriggerConfig `json:"group_trigger,omitempty"` - Typing TypingConfig `json:"typing,omitempty"` - Placeholder PlaceholderConfig `json:"placeholder,omitempty"` - ReasoningChannelID string `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_SLACK_REASONING_CHANNEL_ID"` + 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"` + + GroupTrigger GroupTriggerConfig `json:"group_trigger,omitempty"` + + Typing TypingConfig `json:"typing,omitempty"` + + Placeholder PlaceholderConfig `json:"placeholder,omitempty"` + + ReasoningChannelID string `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_SLACK_REASONING_CHANNEL_ID"` } type MatrixConfig struct { - Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_MATRIX_ENABLED"` - Homeserver string `json:"homeserver" env:"PICOCLAW_CHANNELS_MATRIX_HOMESERVER"` - UserID string `json:"user_id" env:"PICOCLAW_CHANNELS_MATRIX_USER_ID"` - AccessToken string `json:"access_token" env:"PICOCLAW_CHANNELS_MATRIX_ACCESS_TOKEN"` - DeviceID string `json:"device_id,omitempty" env:"PICOCLAW_CHANNELS_MATRIX_DEVICE_ID"` - JoinOnInvite bool `json:"join_on_invite" env:"PICOCLAW_CHANNELS_MATRIX_JOIN_ON_INVITE"` - MessageFormat string `json:"message_format,omitempty" env:"PICOCLAW_CHANNELS_MATRIX_MESSAGE_FORMAT"` - AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_MATRIX_ALLOW_FROM"` - GroupTrigger GroupTriggerConfig `json:"group_trigger,omitempty"` - Placeholder PlaceholderConfig `json:"placeholder,omitempty"` - ReasoningChannelID string `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_MATRIX_REASONING_CHANNEL_ID"` + Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_MATRIX_ENABLED"` + + Homeserver string `json:"homeserver" env:"PICOCLAW_CHANNELS_MATRIX_HOMESERVER"` + + UserID string `json:"user_id" env:"PICOCLAW_CHANNELS_MATRIX_USER_ID"` + + AccessToken string `json:"access_token" env:"PICOCLAW_CHANNELS_MATRIX_ACCESS_TOKEN"` + + DeviceID string `json:"device_id,omitempty" env:"PICOCLAW_CHANNELS_MATRIX_DEVICE_ID"` + + JoinOnInvite bool `json:"join_on_invite" env:"PICOCLAW_CHANNELS_MATRIX_JOIN_ON_INVITE"` + + MessageFormat string `json:"message_format,omitempty" env:"PICOCLAW_CHANNELS_MATRIX_MESSAGE_FORMAT"` + + AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_MATRIX_ALLOW_FROM"` + + GroupTrigger GroupTriggerConfig `json:"group_trigger,omitempty"` + + Placeholder PlaceholderConfig `json:"placeholder,omitempty"` + + ReasoningChannelID string `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_MATRIX_REASONING_CHANNEL_ID"` } 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"` - GroupTrigger GroupTriggerConfig `json:"group_trigger,omitempty"` - Typing TypingConfig `json:"typing,omitempty"` - Placeholder PlaceholderConfig `json:"placeholder,omitempty"` - ReasoningChannelID string `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_LINE_REASONING_CHANNEL_ID"` + 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"` + + GroupTrigger GroupTriggerConfig `json:"group_trigger,omitempty"` + + Typing TypingConfig `json:"typing,omitempty"` + + Placeholder PlaceholderConfig `json:"placeholder,omitempty"` + + ReasoningChannelID string `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_LINE_REASONING_CHANNEL_ID"` } 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"` - GroupTrigger GroupTriggerConfig `json:"group_trigger,omitempty"` - Typing TypingConfig `json:"typing,omitempty"` - Placeholder PlaceholderConfig `json:"placeholder,omitempty"` - ReasoningChannelID string `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_ONEBOT_REASONING_CHANNEL_ID"` + 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"` + + GroupTrigger GroupTriggerConfig `json:"group_trigger,omitempty"` + + Typing TypingConfig `json:"typing,omitempty"` + + Placeholder PlaceholderConfig `json:"placeholder,omitempty"` + + ReasoningChannelID string `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_ONEBOT_REASONING_CHANNEL_ID"` } type WeComConfig struct { - Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_WECOM_ENABLED"` - Token string `json:"token" env:"PICOCLAW_CHANNELS_WECOM_TOKEN"` - EncodingAESKey string `json:"encoding_aes_key" env:"PICOCLAW_CHANNELS_WECOM_ENCODING_AES_KEY"` - WebhookURL string `json:"webhook_url" env:"PICOCLAW_CHANNELS_WECOM_WEBHOOK_URL"` - WebhookHost string `json:"webhook_host" env:"PICOCLAW_CHANNELS_WECOM_WEBHOOK_HOST"` - WebhookPort int `json:"webhook_port" env:"PICOCLAW_CHANNELS_WECOM_WEBHOOK_PORT"` - WebhookPath string `json:"webhook_path" env:"PICOCLAW_CHANNELS_WECOM_WEBHOOK_PATH"` - AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_WECOM_ALLOW_FROM"` - ReplyTimeout int `json:"reply_timeout" env:"PICOCLAW_CHANNELS_WECOM_REPLY_TIMEOUT"` - GroupTrigger GroupTriggerConfig `json:"group_trigger,omitempty"` - ReasoningChannelID string `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_WECOM_REASONING_CHANNEL_ID"` + Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_WECOM_ENABLED"` + + Token string `json:"token" env:"PICOCLAW_CHANNELS_WECOM_TOKEN"` + + EncodingAESKey string `json:"encoding_aes_key" env:"PICOCLAW_CHANNELS_WECOM_ENCODING_AES_KEY"` + + WebhookURL string `json:"webhook_url" env:"PICOCLAW_CHANNELS_WECOM_WEBHOOK_URL"` + + WebhookHost string `json:"webhook_host" env:"PICOCLAW_CHANNELS_WECOM_WEBHOOK_HOST"` + + WebhookPort int `json:"webhook_port" env:"PICOCLAW_CHANNELS_WECOM_WEBHOOK_PORT"` + + WebhookPath string `json:"webhook_path" env:"PICOCLAW_CHANNELS_WECOM_WEBHOOK_PATH"` + + AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_WECOM_ALLOW_FROM"` + + ReplyTimeout int `json:"reply_timeout" env:"PICOCLAW_CHANNELS_WECOM_REPLY_TIMEOUT"` + + GroupTrigger GroupTriggerConfig `json:"group_trigger,omitempty"` + + ReasoningChannelID string `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_WECOM_REASONING_CHANNEL_ID"` } type WeComAppConfig struct { - Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_WECOM_APP_ENABLED"` - CorpID string `json:"corp_id" env:"PICOCLAW_CHANNELS_WECOM_APP_CORP_ID"` - CorpSecret string `json:"corp_secret" env:"PICOCLAW_CHANNELS_WECOM_APP_CORP_SECRET"` - AgentID int64 `json:"agent_id" env:"PICOCLAW_CHANNELS_WECOM_APP_AGENT_ID"` - Token string `json:"token" env:"PICOCLAW_CHANNELS_WECOM_APP_TOKEN"` - EncodingAESKey string `json:"encoding_aes_key" env:"PICOCLAW_CHANNELS_WECOM_APP_ENCODING_AES_KEY"` - WebhookHost string `json:"webhook_host" env:"PICOCLAW_CHANNELS_WECOM_APP_WEBHOOK_HOST"` - WebhookPort int `json:"webhook_port" env:"PICOCLAW_CHANNELS_WECOM_APP_WEBHOOK_PORT"` - WebhookPath string `json:"webhook_path" env:"PICOCLAW_CHANNELS_WECOM_APP_WEBHOOK_PATH"` - AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_WECOM_APP_ALLOW_FROM"` - ReplyTimeout int `json:"reply_timeout" env:"PICOCLAW_CHANNELS_WECOM_APP_REPLY_TIMEOUT"` - GroupTrigger GroupTriggerConfig `json:"group_trigger,omitempty"` - ReasoningChannelID string `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_WECOM_APP_REASONING_CHANNEL_ID"` + Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_WECOM_APP_ENABLED"` + + CorpID string `json:"corp_id" env:"PICOCLAW_CHANNELS_WECOM_APP_CORP_ID"` + + CorpSecret string `json:"corp_secret" env:"PICOCLAW_CHANNELS_WECOM_APP_CORP_SECRET"` + + AgentID int64 `json:"agent_id" env:"PICOCLAW_CHANNELS_WECOM_APP_AGENT_ID"` + + Token string `json:"token" env:"PICOCLAW_CHANNELS_WECOM_APP_TOKEN"` + + EncodingAESKey string `json:"encoding_aes_key" env:"PICOCLAW_CHANNELS_WECOM_APP_ENCODING_AES_KEY"` + + WebhookHost string `json:"webhook_host" env:"PICOCLAW_CHANNELS_WECOM_APP_WEBHOOK_HOST"` + + WebhookPort int `json:"webhook_port" env:"PICOCLAW_CHANNELS_WECOM_APP_WEBHOOK_PORT"` + + WebhookPath string `json:"webhook_path" env:"PICOCLAW_CHANNELS_WECOM_APP_WEBHOOK_PATH"` + + AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_WECOM_APP_ALLOW_FROM"` + + ReplyTimeout int `json:"reply_timeout" env:"PICOCLAW_CHANNELS_WECOM_APP_REPLY_TIMEOUT"` + + GroupTrigger GroupTriggerConfig `json:"group_trigger,omitempty"` + + ReasoningChannelID string `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_WECOM_APP_REASONING_CHANNEL_ID"` } type WeComAIBotConfig struct { - Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_WECOM_AIBOT_ENABLED"` - BotID string `json:"bot_id,omitempty" env:"PICOCLAW_CHANNELS_WECOM_AIBOT_BOT_ID"` - Secret string `json:"secret,omitempty" env:"PICOCLAW_CHANNELS_WECOM_AIBOT_SECRET"` - Token string `json:"token,omitempty" env:"PICOCLAW_CHANNELS_WECOM_AIBOT_TOKEN"` - EncodingAESKey string `json:"encoding_aes_key,omitempty" env:"PICOCLAW_CHANNELS_WECOM_AIBOT_ENCODING_AES_KEY"` - WebhookPath string `json:"webhook_path,omitempty" env:"PICOCLAW_CHANNELS_WECOM_AIBOT_WEBHOOK_PATH"` - AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_WECOM_AIBOT_ALLOW_FROM"` - ReplyTimeout int `json:"reply_timeout" env:"PICOCLAW_CHANNELS_WECOM_AIBOT_REPLY_TIMEOUT"` - MaxSteps int `json:"max_steps" env:"PICOCLAW_CHANNELS_WECOM_AIBOT_MAX_STEPS"` // Maximum streaming steps - WelcomeMessage string `json:"welcome_message" env:"PICOCLAW_CHANNELS_WECOM_AIBOT_WELCOME_MESSAGE"` // Sent on enter_chat event; empty = no welcome - ProcessingMessage string `json:"processing_message,omitempty" env:"PICOCLAW_CHANNELS_WECOM_AIBOT_PROCESSING_MESSAGE"` - ReasoningChannelID string `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_WECOM_AIBOT_REASONING_CHANNEL_ID"` + Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_WECOM_AIBOT_ENABLED"` + + BotID string `json:"bot_id,omitempty" env:"PICOCLAW_CHANNELS_WECOM_AIBOT_BOT_ID"` + + Secret string `json:"secret,omitempty" env:"PICOCLAW_CHANNELS_WECOM_AIBOT_SECRET"` + + Token string `json:"token,omitempty" env:"PICOCLAW_CHANNELS_WECOM_AIBOT_TOKEN"` + + EncodingAESKey string `json:"encoding_aes_key,omitempty" env:"PICOCLAW_CHANNELS_WECOM_AIBOT_ENCODING_AES_KEY"` + + WebhookPath string `json:"webhook_path,omitempty" env:"PICOCLAW_CHANNELS_WECOM_AIBOT_WEBHOOK_PATH"` + + AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_WECOM_AIBOT_ALLOW_FROM"` + + ReplyTimeout int `json:"reply_timeout" env:"PICOCLAW_CHANNELS_WECOM_AIBOT_REPLY_TIMEOUT"` + + MaxSteps int `json:"max_steps" env:"PICOCLAW_CHANNELS_WECOM_AIBOT_MAX_STEPS"` // Maximum streaming steps + + WelcomeMessage string `json:"welcome_message" env:"PICOCLAW_CHANNELS_WECOM_AIBOT_WELCOME_MESSAGE"` // Sent on enter_chat event; empty = no welcome + + ProcessingMessage string `json:"processing_message,omitempty" env:"PICOCLAW_CHANNELS_WECOM_AIBOT_PROCESSING_MESSAGE"` + + ReasoningChannelID string `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_WECOM_AIBOT_REASONING_CHANNEL_ID"` } type WeixinConfig struct { - Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_WEIXIN_ENABLED"` - Token string `json:"token" env:"PICOCLAW_CHANNELS_WEIXIN_TOKEN"` - BaseURL string `json:"base_url" env:"PICOCLAW_CHANNELS_WEIXIN_BASE_URL"` - CDNBaseURL string `json:"cdn_base_url" env:"PICOCLAW_CHANNELS_WEIXIN_CDN_BASE_URL"` - Proxy string `json:"proxy" env:"PICOCLAW_CHANNELS_WEIXIN_PROXY"` - AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_WEIXIN_ALLOW_FROM"` - ReasoningChannelID string `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_WEIXIN_REASONING_CHANNEL_ID"` + Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_WEIXIN_ENABLED"` + + Token string `json:"token" env:"PICOCLAW_CHANNELS_WEIXIN_TOKEN"` + + BaseURL string `json:"base_url" env:"PICOCLAW_CHANNELS_WEIXIN_BASE_URL"` + + CDNBaseURL string `json:"cdn_base_url" env:"PICOCLAW_CHANNELS_WEIXIN_CDN_BASE_URL"` + + Proxy string `json:"proxy" env:"PICOCLAW_CHANNELS_WEIXIN_PROXY"` + + AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_WEIXIN_ALLOW_FROM"` + + ReasoningChannelID string `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_WEIXIN_REASONING_CHANNEL_ID"` } type PicoConfig struct { - Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_PICO_ENABLED"` - Token string `json:"token" env:"PICOCLAW_CHANNELS_PICO_TOKEN"` - AllowTokenQuery bool `json:"allow_token_query,omitempty"` - AllowOrigins []string `json:"allow_origins,omitempty"` - PingInterval int `json:"ping_interval,omitempty"` - ReadTimeout int `json:"read_timeout,omitempty"` - WriteTimeout int `json:"write_timeout,omitempty"` - MaxConnections int `json:"max_connections,omitempty"` - AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_PICO_ALLOW_FROM"` - Placeholder PlaceholderConfig `json:"placeholder,omitempty"` + Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_PICO_ENABLED"` + + Token string `json:"token" env:"PICOCLAW_CHANNELS_PICO_TOKEN"` + + AllowTokenQuery bool `json:"allow_token_query,omitempty"` + + AllowOrigins []string `json:"allow_origins,omitempty"` + + PingInterval int `json:"ping_interval,omitempty"` + + ReadTimeout int `json:"read_timeout,omitempty"` + + WriteTimeout int `json:"write_timeout,omitempty"` + + MaxConnections int `json:"max_connections,omitempty"` + + AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_PICO_ALLOW_FROM"` + + Placeholder PlaceholderConfig `json:"placeholder,omitempty"` } type PicoClientConfig struct { - Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_PICO_CLIENT_ENABLED"` - URL string `json:"url" env:"PICOCLAW_CHANNELS_PICO_CLIENT_URL"` - Token string `json:"token" env:"PICOCLAW_CHANNELS_PICO_CLIENT_TOKEN"` - SessionID string `json:"session_id,omitempty"` - PingInterval int `json:"ping_interval,omitempty"` - ReadTimeout int `json:"read_timeout,omitempty"` - AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_PICO_CLIENT_ALLOW_FROM"` + Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_PICO_CLIENT_ENABLED"` + + URL string `json:"url" env:"PICOCLAW_CHANNELS_PICO_CLIENT_URL"` + + Token string `json:"token" env:"PICOCLAW_CHANNELS_PICO_CLIENT_TOKEN"` + + SessionID string `json:"session_id,omitempty"` + + PingInterval int `json:"ping_interval,omitempty"` + + ReadTimeout int `json:"read_timeout,omitempty"` + + AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_PICO_CLIENT_ALLOW_FROM"` } type IRCConfig struct { - Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_IRC_ENABLED"` - Server string `json:"server" env:"PICOCLAW_CHANNELS_IRC_SERVER"` - TLS bool `json:"tls" env:"PICOCLAW_CHANNELS_IRC_TLS"` - Nick string `json:"nick" env:"PICOCLAW_CHANNELS_IRC_NICK"` - User string `json:"user,omitempty" env:"PICOCLAW_CHANNELS_IRC_USER"` - RealName string `json:"real_name,omitempty" env:"PICOCLAW_CHANNELS_IRC_REAL_NAME"` - Password string `json:"password" env:"PICOCLAW_CHANNELS_IRC_PASSWORD"` - NickServPassword string `json:"nickserv_password" env:"PICOCLAW_CHANNELS_IRC_NICKSERV_PASSWORD"` - SASLUser string `json:"sasl_user" env:"PICOCLAW_CHANNELS_IRC_SASL_USER"` - SASLPassword string `json:"sasl_password" env:"PICOCLAW_CHANNELS_IRC_SASL_PASSWORD"` - Channels FlexibleStringSlice `json:"channels" env:"PICOCLAW_CHANNELS_IRC_CHANNELS"` - RequestCaps FlexibleStringSlice `json:"request_caps,omitempty" env:"PICOCLAW_CHANNELS_IRC_REQUEST_CAPS"` - AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_IRC_ALLOW_FROM"` - GroupTrigger GroupTriggerConfig `json:"group_trigger,omitempty"` - Typing TypingConfig `json:"typing,omitempty"` - ReasoningChannelID string `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_IRC_REASONING_CHANNEL_ID"` + Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_IRC_ENABLED"` + + Server string `json:"server" env:"PICOCLAW_CHANNELS_IRC_SERVER"` + + TLS bool `json:"tls" env:"PICOCLAW_CHANNELS_IRC_TLS"` + + Nick string `json:"nick" env:"PICOCLAW_CHANNELS_IRC_NICK"` + + User string `json:"user,omitempty" env:"PICOCLAW_CHANNELS_IRC_USER"` + + RealName string `json:"real_name,omitempty" env:"PICOCLAW_CHANNELS_IRC_REAL_NAME"` + + Password string `json:"password" env:"PICOCLAW_CHANNELS_IRC_PASSWORD"` + + NickServPassword string `json:"nickserv_password" env:"PICOCLAW_CHANNELS_IRC_NICKSERV_PASSWORD"` + + SASLUser string `json:"sasl_user" env:"PICOCLAW_CHANNELS_IRC_SASL_USER"` + + SASLPassword string `json:"sasl_password" env:"PICOCLAW_CHANNELS_IRC_SASL_PASSWORD"` + + Channels FlexibleStringSlice `json:"channels" env:"PICOCLAW_CHANNELS_IRC_CHANNELS"` + + RequestCaps FlexibleStringSlice `json:"request_caps,omitempty" env:"PICOCLAW_CHANNELS_IRC_REQUEST_CAPS"` + + AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_IRC_ALLOW_FROM"` + + GroupTrigger GroupTriggerConfig `json:"group_trigger,omitempty"` + + Typing TypingConfig `json:"typing,omitempty"` + + ReasoningChannelID string `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_IRC_REASONING_CHANNEL_ID"` } type MQTTConfig struct { - Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_MQTT_ENABLED"` - Broker string `json:"broker" env:"PICOCLAW_CHANNELS_MQTT_BROKER"` - ClientID string `json:"client_id" env:"PICOCLAW_CHANNELS_MQTT_CLIENT_ID"` - Username string `json:"username" env:"PICOCLAW_CHANNELS_MQTT_USERNAME"` - Password string `json:"password" env:"PICOCLAW_CHANNELS_MQTT_PASSWORD"` - SubscribeTopics []string `json:"subscribe_topics" env:"PICOCLAW_CHANNELS_MQTT_SUBSCRIBE_TOPICS"` - SubscribeJSONKey *string `json:"subscribe_json_key,omitempty"` - ReplyTopic string `json:"reply_topic" env:"PICOCLAW_CHANNELS_MQTT_REPLY_TOPIC"` - ReplyJSONKey *string `json:"reply_json_key,omitempty"` - TLS bool `json:"tls" env:"PICOCLAW_CHANNELS_MQTT_TLS"` - TLSCA string `json:"tls_ca" env:"PICOCLAW_CHANNELS_MQTT_TLS_CA"` - TLSCert string `json:"tls_cert" env:"PICOCLAW_CHANNELS_MQTT_TLS_CERT"` - TLSKey string `json:"tls_key" env:"PICOCLAW_CHANNELS_MQTT_TLS_KEY"` - QoS int `json:"qos" env:"PICOCLAW_CHANNELS_MQTT_QOS"` - Retain bool `json:"retain" env:"PICOCLAW_CHANNELS_MQTT_RETAIN"` - Prefix string `json:"prefix" env:"PICOCLAW_CHANNELS_MQTT_PREFIX"` - Instruction string `json:"instruction" env:"PICOCLAW_CHANNELS_MQTT_INSTRUCTION"` - AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_MQTT_ALLOW_FROM"` - GroupTrigger GroupTriggerConfig `json:"group_trigger,omitempty"` - ReasoningChannelID string `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_MQTT_REASONING_CHANNEL_ID"` + Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_MQTT_ENABLED"` + + Broker string `json:"broker" env:"PICOCLAW_CHANNELS_MQTT_BROKER"` + + ClientID string `json:"client_id" env:"PICOCLAW_CHANNELS_MQTT_CLIENT_ID"` + + Username string `json:"username" env:"PICOCLAW_CHANNELS_MQTT_USERNAME"` + + Password string `json:"password" env:"PICOCLAW_CHANNELS_MQTT_PASSWORD"` + + SubscribeTopics []string `json:"subscribe_topics" env:"PICOCLAW_CHANNELS_MQTT_SUBSCRIBE_TOPICS"` + + SubscribeJSONKey *string `json:"subscribe_json_key,omitempty"` + + ReplyTopic string `json:"reply_topic" env:"PICOCLAW_CHANNELS_MQTT_REPLY_TOPIC"` + + ReplyJSONKey *string `json:"reply_json_key,omitempty"` + + TLS bool `json:"tls" env:"PICOCLAW_CHANNELS_MQTT_TLS"` + + TLSCA string `json:"tls_ca" env:"PICOCLAW_CHANNELS_MQTT_TLS_CA"` + + TLSCert string `json:"tls_cert" env:"PICOCLAW_CHANNELS_MQTT_TLS_CERT"` + + TLSKey string `json:"tls_key" env:"PICOCLAW_CHANNELS_MQTT_TLS_KEY"` + + QoS int `json:"qos" env:"PICOCLAW_CHANNELS_MQTT_QOS"` + + Retain bool `json:"retain" env:"PICOCLAW_CHANNELS_MQTT_RETAIN"` + + Prefix string `json:"prefix" env:"PICOCLAW_CHANNELS_MQTT_PREFIX"` + + Instruction string `json:"instruction" env:"PICOCLAW_CHANNELS_MQTT_INSTRUCTION"` + + AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_MQTT_ALLOW_FROM"` + + GroupTrigger GroupTriggerConfig `json:"group_trigger,omitempty"` + + ReasoningChannelID string `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_MQTT_REASONING_CHANNEL_ID"` } type HeartbeatConfig struct { - Enabled bool `json:"enabled" env:"PICOCLAW_HEARTBEAT_ENABLED"` - Interval int `json:"interval" env:"PICOCLAW_HEARTBEAT_INTERVAL"` // minutes, min 5 + 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"` + Enabled bool `json:"enabled" env:"PICOCLAW_DEVICES_ENABLED"` + MonitorUSB bool `json:"monitor_usb" env:"PICOCLAW_DEVICES_MONITOR_USB"` } type VoiceConfig struct { - ModelName string `json:"model_name,omitempty" env:"PICOCLAW_VOICE_MODEL_NAME"` - EchoTranscription bool `json:"echo_transcription" env:"PICOCLAW_VOICE_ECHO_TRANSCRIPTION"` + ModelName string `json:"model_name,omitempty" env:"PICOCLAW_VOICE_MODEL_NAME"` + + EchoTranscription bool `json:"echo_transcription" env:"PICOCLAW_VOICE_ECHO_TRANSCRIPTION"` } type ProvidersConfig struct { - Anthropic ProviderConfig `json:"anthropic"` - OpenAI OpenAIProviderConfig `json:"openai"` - LiteLLM ProviderConfig `json:"litellm"` - OpenRouter ProviderConfig `json:"openrouter"` - Groq ProviderConfig `json:"groq"` - Zhipu ProviderConfig `json:"zhipu"` - VLLM ProviderConfig `json:"vllm"` - Gemini ProviderConfig `json:"gemini"` - Nvidia ProviderConfig `json:"nvidia"` - Ollama ProviderConfig `json:"ollama"` - Moonshot ProviderConfig `json:"moonshot"` - ShengSuanYun ProviderConfig `json:"shengsuanyun"` - DeepSeek ProviderConfig `json:"deepseek"` - Cerebras ProviderConfig `json:"cerebras"` - Vivgrid ProviderConfig `json:"vivgrid"` - VolcEngine ProviderConfig `json:"volcengine"` - GitHubCopilot ProviderConfig `json:"github_copilot"` - Antigravity ProviderConfig `json:"antigravity"` - Qwen ProviderConfig `json:"qwen"` - Mistral ProviderConfig `json:"mistral"` - Avian ProviderConfig `json:"avian"` - Minimax ProviderConfig `json:"minimax"` - LongCat ProviderConfig `json:"longcat"` - ModelScope ProviderConfig `json:"modelscope"` - Novita ProviderConfig `json:"novita"` + Anthropic ProviderConfig `json:"anthropic"` + + OpenAI OpenAIProviderConfig `json:"openai"` + + LiteLLM ProviderConfig `json:"litellm"` + + OpenRouter ProviderConfig `json:"openrouter"` + + Groq ProviderConfig `json:"groq"` + + Zhipu ProviderConfig `json:"zhipu"` + + VLLM ProviderConfig `json:"vllm"` + + Gemini ProviderConfig `json:"gemini"` + + Nvidia ProviderConfig `json:"nvidia"` + + Ollama ProviderConfig `json:"ollama"` + + Moonshot ProviderConfig `json:"moonshot"` + + ShengSuanYun ProviderConfig `json:"shengsuanyun"` + + DeepSeek ProviderConfig `json:"deepseek"` + + Cerebras ProviderConfig `json:"cerebras"` + + Vivgrid ProviderConfig `json:"vivgrid"` + + VolcEngine ProviderConfig `json:"volcengine"` + + GitHubCopilot ProviderConfig `json:"github_copilot"` + + Antigravity ProviderConfig `json:"antigravity"` + + Qwen ProviderConfig `json:"qwen"` + + Mistral ProviderConfig `json:"mistral"` + + Avian ProviderConfig `json:"avian"` + + Minimax ProviderConfig `json:"minimax"` + + LongCat ProviderConfig `json:"longcat"` + + ModelScope ProviderConfig `json:"modelscope"` + + Novita ProviderConfig `json:"novita"` } // IsEmpty checks if all provider configs are empty (no API keys or API bases set) + // Note: WebSearch is an optimization option and doesn't count as "non-empty" + func (p ProvidersConfig) IsEmpty() bool { + return p.Anthropic.APIKey == "" && p.Anthropic.APIBase == "" && + p.OpenAI.APIKey == "" && p.OpenAI.APIBase == "" && + p.LiteLLM.APIKey == "" && p.LiteLLM.APIBase == "" && + p.OpenRouter.APIKey == "" && p.OpenRouter.APIBase == "" && + p.Groq.APIKey == "" && p.Groq.APIBase == "" && + p.Zhipu.APIKey == "" && p.Zhipu.APIBase == "" && + p.VLLM.APIKey == "" && p.VLLM.APIBase == "" && + p.Gemini.APIKey == "" && p.Gemini.APIBase == "" && + p.Nvidia.APIKey == "" && p.Nvidia.APIBase == "" && + p.Ollama.APIKey == "" && p.Ollama.APIBase == "" && + p.Moonshot.APIKey == "" && p.Moonshot.APIBase == "" && + p.ShengSuanYun.APIKey == "" && p.ShengSuanYun.APIBase == "" && + p.DeepSeek.APIKey == "" && p.DeepSeek.APIBase == "" && + p.Cerebras.APIKey == "" && p.Cerebras.APIBase == "" && + p.Vivgrid.APIKey == "" && p.Vivgrid.APIBase == "" && + p.VolcEngine.APIKey == "" && p.VolcEngine.APIBase == "" && + p.GitHubCopilot.APIKey == "" && p.GitHubCopilot.APIBase == "" && + p.Antigravity.APIKey == "" && p.Antigravity.APIBase == "" && + p.Qwen.APIKey == "" && p.Qwen.APIBase == "" && + p.Mistral.APIKey == "" && p.Mistral.APIBase == "" && + p.Avian.APIKey == "" && p.Avian.APIBase == "" && + p.Minimax.APIKey == "" && p.Minimax.APIBase == "" && + p.LongCat.APIKey == "" && p.LongCat.APIBase == "" && + p.ModelScope.APIKey == "" && p.ModelScope.APIBase == "" && + p.Novita.APIKey == "" && p.Novita.APIBase == "" + } // MarshalJSON implements custom JSON marshaling for ProvidersConfig + // to omit the entire section when empty + func (p ProvidersConfig) MarshalJSON() ([]byte, error) { + if p.IsEmpty() { + return []byte("null"), nil + } + type Alias ProvidersConfig + return json.Marshal((*Alias)(&p)) + } 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"` - Proxy string `json:"proxy,omitempty" env:"PICOCLAW_PROVIDERS_{{.Name}}_PROXY"` - RequestTimeout int `json:"request_timeout,omitempty" env:"PICOCLAW_PROVIDERS_{{.Name}}_REQUEST_TIMEOUT"` - AuthMethod string `json:"auth_method,omitempty" env:"PICOCLAW_PROVIDERS_{{.Name}}_AUTH_METHOD"` - ConnectMode string `json:"connect_mode,omitempty" env:"PICOCLAW_PROVIDERS_{{.Name}}_CONNECT_MODE"` // only for Github Copilot, `stdio` or `grpc` + APIKey string `json:"api_key" env:"PICOCLAW_PROVIDERS_{{.Name}}_API_KEY"` + + APIBase string `json:"api_base" env:"PICOCLAW_PROVIDERS_{{.Name}}_API_BASE"` + + Proxy string `json:"proxy,omitempty" env:"PICOCLAW_PROVIDERS_{{.Name}}_PROXY"` + + RequestTimeout int `json:"request_timeout,omitempty" env:"PICOCLAW_PROVIDERS_{{.Name}}_REQUEST_TIMEOUT"` + + AuthMethod string `json:"auth_method,omitempty" env:"PICOCLAW_PROVIDERS_{{.Name}}_AUTH_METHOD"` + + ConnectMode string `json:"connect_mode,omitempty" env:"PICOCLAW_PROVIDERS_{{.Name}}_CONNECT_MODE"` // only for Github Copilot, `stdio` or `grpc` + } type OpenAIProviderConfig struct { ProviderConfig + WebSearch bool `json:"web_search" env:"PICOCLAW_PROVIDERS_OPENAI_WEB_SEARCH"` } // ModelConfig represents a model-centric provider configuration. + // It allows adding new providers (especially OpenAI-compatible ones) via configuration only. + // The model field uses protocol prefix format: [protocol/]model-identifier + // Supported protocols include openai, anthropic, antigravity, claude-cli, + // codex-cli, github-copilot, and named OpenAI-compatible protocols such as + // groq, deepseek, modelscope, and novita. + // Default protocol is "openai" if no prefix is specified. + type ModelConfig struct { + // Required fields + ModelName string `json:"model_name"` // User-facing alias for the model - Model string `json:"model"` // Protocol/model-identifier (e.g., "openai/gpt-4o", "anthropic/claude-sonnet-4.6") + + Model string `json:"model"` // Protocol/model-identifier (e.g., "openai/gpt-4o", "anthropic/claude-sonnet-4.6") // HTTP-based providers - APIBase string `json:"api_base,omitempty"` // API endpoint URL - APIKey string `json:"api_key"` // API authentication key (single key) - APIKeys []string `json:"api_keys,omitempty"` // API authentication keys (multiple keys for failover) - Proxy string `json:"proxy,omitempty"` // HTTP proxy URL + + APIBase string `json:"api_base,omitempty"` // API endpoint URL + + APIKey string `json:"api_key"` // API authentication key (single key) + + APIKeys []string `json:"api_keys,omitempty"` // API authentication keys (multiple keys for failover) + + Proxy string `json:"proxy,omitempty"` // HTTP proxy URL + Fallbacks []string `json:"fallbacks,omitempty"` // Fallback model names for failover // Special providers (CLI-based, OAuth, etc.) - AuthMethod string `json:"auth_method,omitempty"` // Authentication method: oauth, token + + AuthMethod string `json:"auth_method,omitempty"` // Authentication method: oauth, token + ConnectMode string `json:"connect_mode,omitempty"` // Connection mode: stdio, grpc - Workspace string `json:"workspace,omitempty"` // Workspace path for CLI-based providers + + Workspace string `json:"workspace,omitempty"` // Workspace path for CLI-based providers // Optional optimizations - RPM int `json:"rpm,omitempty"` // Requests per minute limit + + RPM int `json:"rpm,omitempty"` // Requests per minute limit + MaxTokensField string `json:"max_tokens_field,omitempty"` // Field name for max tokens (e.g., "max_completion_tokens") - RequestTimeout int `json:"request_timeout,omitempty"` - ThinkingLevel string `json:"thinking_level,omitempty"` // Extended thinking: off|low|medium|high|xhigh|adaptive + + RequestTimeout int `json:"request_timeout,omitempty"` + + ThinkingLevel string `json:"thinking_level,omitempty"` // Extended thinking: off|low|medium|high|xhigh|adaptive + } // Validate checks if the ModelConfig has all required fields. + func (c *ModelConfig) Validate() error { + if c.ModelName == "" { + return fmt.Errorf("model_name is required") + } + if c.Model == "" { + return fmt.Errorf("model is required") + } + return nil + } type GatewayConfig struct { - Host string `json:"host" env:"PICOCLAW_GATEWAY_HOST"` - Port int `json:"port" env:"PICOCLAW_GATEWAY_PORT"` - HotReload bool `json:"hot_reload" env:"PICOCLAW_GATEWAY_HOT_RELOAD"` - LogLevel string `json:"log_level,omitempty" env:"PICOCLAW_LOG_LEVEL"` + Host string `json:"host" env:"PICOCLAW_GATEWAY_HOST"` + + Port int `json:"port" env:"PICOCLAW_GATEWAY_PORT"` + + HotReload bool `json:"hot_reload" env:"PICOCLAW_GATEWAY_HOT_RELOAD"` + + LogLevel string `json:"log_level,omitempty" env:"PICOCLAW_LOG_LEVEL"` } type ToolDiscoveryConfig struct { - Enabled bool `json:"enabled" env:"PICOCLAW_TOOLS_DISCOVERY_ENABLED"` - TTL int `json:"ttl" env:"PICOCLAW_TOOLS_DISCOVERY_TTL"` - MaxSearchResults int `json:"max_search_results" env:"PICOCLAW_MAX_SEARCH_RESULTS"` - UseBM25 bool `json:"use_bm25" env:"PICOCLAW_TOOLS_DISCOVERY_USE_BM25"` - UseRegex bool `json:"use_regex" env:"PICOCLAW_TOOLS_DISCOVERY_USE_REGEX"` + Enabled bool `json:"enabled" env:"PICOCLAW_TOOLS_DISCOVERY_ENABLED"` + + TTL int `json:"ttl" env:"PICOCLAW_TOOLS_DISCOVERY_TTL"` + + MaxSearchResults int `json:"max_search_results" env:"PICOCLAW_MAX_SEARCH_RESULTS"` + + UseBM25 bool `json:"use_bm25" env:"PICOCLAW_TOOLS_DISCOVERY_USE_BM25"` + + UseRegex bool `json:"use_regex" env:"PICOCLAW_TOOLS_DISCOVERY_USE_REGEX"` } type ToolConfig struct { @@ -775,142 +1273,219 @@ type ToolConfig struct { } 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"` - APIKeys []string `json:"api_keys" env:"PICOCLAW_TOOLS_WEB_BRAVE_API_KEYS"` - MaxResults int `json:"max_results" env:"PICOCLAW_TOOLS_WEB_BRAVE_MAX_RESULTS"` + Enabled bool `json:"enabled" env:"PICOCLAW_TOOLS_WEB_BRAVE_ENABLED"` + + APIKey string `json:"api_key" env:"PICOCLAW_TOOLS_WEB_BRAVE_API_KEY"` + + APIKeys []string `json:"api_keys" env:"PICOCLAW_TOOLS_WEB_BRAVE_API_KEYS"` + + MaxResults int `json:"max_results" env:"PICOCLAW_TOOLS_WEB_BRAVE_MAX_RESULTS"` } type TavilyConfig struct { - Enabled bool `json:"enabled" env:"PICOCLAW_TOOLS_WEB_TAVILY_ENABLED"` - APIKey string `json:"api_key" env:"PICOCLAW_TOOLS_WEB_TAVILY_API_KEY"` - APIKeys []string `json:"api_keys" env:"PICOCLAW_TOOLS_WEB_TAVILY_API_KEYS"` - BaseURL string `json:"base_url" env:"PICOCLAW_TOOLS_WEB_TAVILY_BASE_URL"` - MaxResults int `json:"max_results" env:"PICOCLAW_TOOLS_WEB_TAVILY_MAX_RESULTS"` + Enabled bool `json:"enabled" env:"PICOCLAW_TOOLS_WEB_TAVILY_ENABLED"` + + APIKey string `json:"api_key" env:"PICOCLAW_TOOLS_WEB_TAVILY_API_KEY"` + + APIKeys []string `json:"api_keys" env:"PICOCLAW_TOOLS_WEB_TAVILY_API_KEYS"` + + BaseURL string `json:"base_url" env:"PICOCLAW_TOOLS_WEB_TAVILY_BASE_URL"` + + MaxResults int `json:"max_results" env:"PICOCLAW_TOOLS_WEB_TAVILY_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"` + Enabled bool `json:"enabled" env:"PICOCLAW_TOOLS_WEB_DUCKDUCKGO_ENABLED"` + + MaxResults int `json:"max_results" env:"PICOCLAW_TOOLS_WEB_DUCKDUCKGO_MAX_RESULTS"` } type PerplexityConfig struct { - Enabled bool `json:"enabled" env:"PICOCLAW_TOOLS_WEB_PERPLEXITY_ENABLED"` - APIKey string `json:"api_key" env:"PICOCLAW_TOOLS_WEB_PERPLEXITY_API_KEY"` - APIKeys []string `json:"api_keys" env:"PICOCLAW_TOOLS_WEB_PERPLEXITY_API_KEYS"` - MaxResults int `json:"max_results" env:"PICOCLAW_TOOLS_WEB_PERPLEXITY_MAX_RESULTS"` + Enabled bool `json:"enabled" env:"PICOCLAW_TOOLS_WEB_PERPLEXITY_ENABLED"` + + APIKey string `json:"api_key" env:"PICOCLAW_TOOLS_WEB_PERPLEXITY_API_KEY"` + + APIKeys []string `json:"api_keys" env:"PICOCLAW_TOOLS_WEB_PERPLEXITY_API_KEYS"` + + MaxResults int `json:"max_results" env:"PICOCLAW_TOOLS_WEB_PERPLEXITY_MAX_RESULTS"` } type SearXNGConfig struct { - Enabled bool `json:"enabled" env:"PICOCLAW_TOOLS_WEB_SEARXNG_ENABLED"` - BaseURL string `json:"base_url" env:"PICOCLAW_TOOLS_WEB_SEARXNG_BASE_URL"` - MaxResults int `json:"max_results" env:"PICOCLAW_TOOLS_WEB_SEARXNG_MAX_RESULTS"` + Enabled bool `json:"enabled" env:"PICOCLAW_TOOLS_WEB_SEARXNG_ENABLED"` + + BaseURL string `json:"base_url" env:"PICOCLAW_TOOLS_WEB_SEARXNG_BASE_URL"` + + MaxResults int `json:"max_results" env:"PICOCLAW_TOOLS_WEB_SEARXNG_MAX_RESULTS"` } type GLMSearchConfig struct { - Enabled bool `json:"enabled" env:"PICOCLAW_TOOLS_WEB_GLM_ENABLED"` - APIKey string `json:"api_key" env:"PICOCLAW_TOOLS_WEB_GLM_API_KEY"` - BaseURL string `json:"base_url" env:"PICOCLAW_TOOLS_WEB_GLM_BASE_URL"` + Enabled bool `json:"enabled" env:"PICOCLAW_TOOLS_WEB_GLM_ENABLED"` + + APIKey string `json:"api_key" env:"PICOCLAW_TOOLS_WEB_GLM_API_KEY"` + + BaseURL string `json:"base_url" env:"PICOCLAW_TOOLS_WEB_GLM_BASE_URL"` + // SearchEngine specifies the search backend: "search_std" (default), + // "search_pro", "search_pro_sogou", or "search_pro_quark". + SearchEngine string `json:"search_engine" env:"PICOCLAW_TOOLS_WEB_GLM_SEARCH_ENGINE"` - MaxResults int `json:"max_results" env:"PICOCLAW_TOOLS_WEB_GLM_MAX_RESULTS"` + + MaxResults int `json:"max_results" env:"PICOCLAW_TOOLS_WEB_GLM_MAX_RESULTS"` } type BaiduSearchConfig struct { - Enabled bool `json:"enabled" env:"PICOCLAW_TOOLS_WEB_BAIDU_ENABLED"` - APIKey string `json:"api_key" env:"PICOCLAW_TOOLS_WEB_BAIDU_API_KEY"` - BaseURL string `json:"base_url" env:"PICOCLAW_TOOLS_WEB_BAIDU_BASE_URL"` - MaxResults int `json:"max_results" env:"PICOCLAW_TOOLS_WEB_BAIDU_MAX_RESULTS"` + Enabled bool `json:"enabled" env:"PICOCLAW_TOOLS_WEB_BAIDU_ENABLED"` + + APIKey string `json:"api_key" env:"PICOCLAW_TOOLS_WEB_BAIDU_API_KEY"` + + BaseURL string `json:"base_url" env:"PICOCLAW_TOOLS_WEB_BAIDU_BASE_URL"` + + MaxResults int `json:"max_results" env:"PICOCLAW_TOOLS_WEB_BAIDU_MAX_RESULTS"` } type WebToolsConfig struct { + ToolConfig `envPrefix:"PICOCLAW_TOOLS_WEB_"` - ToolConfig ` envPrefix:"PICOCLAW_TOOLS_WEB_"` - Brave BraveConfig ` json:"brave"` - Tavily TavilyConfig ` json:"tavily"` - DuckDuckGo DuckDuckGoConfig ` json:"duckduckgo"` - Perplexity PerplexityConfig ` json:"perplexity"` - SearXNG SearXNGConfig ` json:"searxng"` - GLMSearch GLMSearchConfig ` json:"glm_search"` - BaiduSearch BaiduSearchConfig ` json:"baidu_search"` + Brave BraveConfig `json:"brave"` + + Tavily TavilyConfig `json:"tavily"` + + DuckDuckGo DuckDuckGoConfig `json:"duckduckgo"` + + Perplexity PerplexityConfig `json:"perplexity"` + + SearXNG SearXNGConfig `json:"searxng"` + + GLMSearch GLMSearchConfig `json:"glm_search"` + + BaiduSearch BaiduSearchConfig `json:"baidu_search"` // PreferNative controls whether to use provider-native web search when + // the active LLM supports it (e.g. OpenAI web_search_preview). When true, + // the client-side web_search tool is hidden to avoid duplicate search surfaces, + // and the provider's built-in search is used instead. Falls back to client-side + // search when the provider does not support native search. - PreferNative bool ` json:"prefer_native" env:"PICOCLAW_TOOLS_WEB_PREFER_NATIVE"` + + PreferNative bool `json:"prefer_native" env:"PICOCLAW_TOOLS_WEB_PREFER_NATIVE"` + // Proxy is an optional proxy URL for web tools (http/https/socks5/socks5h). + // For authenticated proxies, prefer HTTP_PROXY/HTTPS_PROXY env vars instead of embedding credentials in config. - Proxy string ` json:"proxy,omitempty" env:"PICOCLAW_TOOLS_WEB_PROXY"` - FetchLimitBytes int64 ` json:"fetch_limit_bytes,omitempty" env:"PICOCLAW_TOOLS_WEB_FETCH_LIMIT_BYTES"` - Format string ` json:"format,omitempty" env:"PICOCLAW_TOOLS_WEB_FORMAT"` - PrivateHostWhitelist FlexibleStringSlice ` json:"private_host_whitelist,omitempty" env:"PICOCLAW_TOOLS_WEB_PRIVATE_HOST_WHITELIST"` + + Proxy string `json:"proxy,omitempty" env:"PICOCLAW_TOOLS_WEB_PROXY"` + + FetchLimitBytes int64 `json:"fetch_limit_bytes,omitempty" env:"PICOCLAW_TOOLS_WEB_FETCH_LIMIT_BYTES"` + + Format string `json:"format,omitempty" env:"PICOCLAW_TOOLS_WEB_FORMAT"` + + PrivateHostWhitelist FlexibleStringSlice `json:"private_host_whitelist,omitempty" env:"PICOCLAW_TOOLS_WEB_PRIVATE_HOST_WHITELIST"` } type CronToolsConfig struct { - ToolConfig ` envPrefix:"PICOCLAW_TOOLS_CRON_"` - ExecTimeoutMinutes int ` env:"PICOCLAW_TOOLS_CRON_EXEC_TIMEOUT_MINUTES" json:"exec_timeout_minutes"` // 0 means no timeout - AllowCommand bool ` env:"PICOCLAW_TOOLS_CRON_ALLOW_COMMAND" json:"allow_command"` + ToolConfig `envPrefix:"PICOCLAW_TOOLS_CRON_"` + + ExecTimeoutMinutes int `env:"PICOCLAW_TOOLS_CRON_EXEC_TIMEOUT_MINUTES" json:"exec_timeout_minutes"` // 0 means no timeout + + AllowCommand bool `env:"PICOCLAW_TOOLS_CRON_ALLOW_COMMAND" json:"allow_command"` } type ExecConfig struct { - ToolConfig ` envPrefix:"PICOCLAW_TOOLS_EXEC_"` - EnableDenyPatterns bool ` env:"PICOCLAW_TOOLS_EXEC_ENABLE_DENY_PATTERNS" json:"enable_deny_patterns"` - AllowRemote bool ` env:"PICOCLAW_TOOLS_EXEC_ALLOW_REMOTE" json:"allow_remote"` - CustomDenyPatterns []string ` env:"PICOCLAW_TOOLS_EXEC_CUSTOM_DENY_PATTERNS" json:"custom_deny_patterns"` - CustomAllowPatterns []string ` env:"PICOCLAW_TOOLS_EXEC_CUSTOM_ALLOW_PATTERNS" json:"custom_allow_patterns"` - TimeoutSeconds int ` env:"PICOCLAW_TOOLS_EXEC_TIMEOUT_SECONDS" json:"timeout_seconds"` // 0 means use default (60s) + ToolConfig `envPrefix:"PICOCLAW_TOOLS_EXEC_"` + + EnableDenyPatterns bool `env:"PICOCLAW_TOOLS_EXEC_ENABLE_DENY_PATTERNS" json:"enable_deny_patterns"` + + AllowRemote bool `env:"PICOCLAW_TOOLS_EXEC_ALLOW_REMOTE" json:"allow_remote"` + + CustomDenyPatterns []string `env:"PICOCLAW_TOOLS_EXEC_CUSTOM_DENY_PATTERNS" json:"custom_deny_patterns"` + + CustomAllowPatterns []string `env:"PICOCLAW_TOOLS_EXEC_CUSTOM_ALLOW_PATTERNS" json:"custom_allow_patterns"` + + TimeoutSeconds int `env:"PICOCLAW_TOOLS_EXEC_TIMEOUT_SECONDS" json:"timeout_seconds"` // 0 means use default (60s) + } type SkillsToolsConfig struct { - ToolConfig ` envPrefix:"PICOCLAW_TOOLS_SKILLS_"` - Registries SkillsRegistriesConfig ` json:"registries"` - Github SkillsGithubConfig ` json:"github"` - MaxConcurrentSearches int ` json:"max_concurrent_searches" env:"PICOCLAW_TOOLS_SKILLS_MAX_CONCURRENT_SEARCHES"` - SearchCache SearchCacheConfig ` json:"search_cache"` + ToolConfig `envPrefix:"PICOCLAW_TOOLS_SKILLS_"` + + Registries SkillsRegistriesConfig `json:"registries"` + + Github SkillsGithubConfig `json:"github"` + + MaxConcurrentSearches int `json:"max_concurrent_searches" env:"PICOCLAW_TOOLS_SKILLS_MAX_CONCURRENT_SEARCHES"` + + SearchCache SearchCacheConfig `json:"search_cache"` } type MediaCleanupConfig struct { - ToolConfig ` envPrefix:"PICOCLAW_MEDIA_CLEANUP_"` - MaxAge int ` env:"PICOCLAW_MEDIA_CLEANUP_MAX_AGE" json:"max_age_minutes"` - Interval int ` env:"PICOCLAW_MEDIA_CLEANUP_INTERVAL" json:"interval_minutes"` + ToolConfig `envPrefix:"PICOCLAW_MEDIA_CLEANUP_"` + + MaxAge int `env:"PICOCLAW_MEDIA_CLEANUP_MAX_AGE" json:"max_age_minutes"` + + Interval int `env:"PICOCLAW_MEDIA_CLEANUP_INTERVAL" json:"interval_minutes"` } type ReadFileToolConfig struct { - Enabled bool `json:"enabled"` - MaxReadFileSize int `json:"max_read_file_size"` + Enabled bool `json:"enabled"` + + MaxReadFileSize int `json:"max_read_file_size"` } type ToolsConfig struct { - AllowReadPaths []string `json:"allow_read_paths" env:"PICOCLAW_TOOLS_ALLOW_READ_PATHS"` - AllowWritePaths []string `json:"allow_write_paths" env:"PICOCLAW_TOOLS_ALLOW_WRITE_PATHS"` - Web WebToolsConfig `json:"web"` - Cron CronToolsConfig `json:"cron"` - Exec ExecConfig `json:"exec"` - Skills SkillsToolsConfig `json:"skills"` - MediaCleanup MediaCleanupConfig `json:"media_cleanup"` - MCP MCPConfig `json:"mcp"` - AppendFile ToolConfig `json:"append_file" envPrefix:"PICOCLAW_TOOLS_APPEND_FILE_"` - EditFile ToolConfig `json:"edit_file" envPrefix:"PICOCLAW_TOOLS_EDIT_FILE_"` - FindSkills ToolConfig `json:"find_skills" envPrefix:"PICOCLAW_TOOLS_FIND_SKILLS_"` - I2C ToolConfig `json:"i2c" envPrefix:"PICOCLAW_TOOLS_I2C_"` - InstallSkill ToolConfig `json:"install_skill" envPrefix:"PICOCLAW_TOOLS_INSTALL_SKILL_"` - ListDir ToolConfig `json:"list_dir" envPrefix:"PICOCLAW_TOOLS_LIST_DIR_"` - Message ToolConfig `json:"message" envPrefix:"PICOCLAW_TOOLS_MESSAGE_"` - ReadFile ReadFileToolConfig `json:"read_file" envPrefix:"PICOCLAW_TOOLS_READ_FILE_"` - SendFile ToolConfig `json:"send_file" envPrefix:"PICOCLAW_TOOLS_SEND_FILE_"` - Spawn ToolConfig `json:"spawn" envPrefix:"PICOCLAW_TOOLS_SPAWN_"` - SpawnStatus ToolConfig `json:"spawn_status" envPrefix:"PICOCLAW_TOOLS_SPAWN_STATUS_"` - SPI ToolConfig `json:"spi" envPrefix:"PICOCLAW_TOOLS_SPI_"` - Subagent ToolConfig `json:"subagent" envPrefix:"PICOCLAW_TOOLS_SUBAGENT_"` - WebFetch ToolConfig `json:"web_fetch" envPrefix:"PICOCLAW_TOOLS_WEB_FETCH_"` - WriteFile ToolConfig `json:"write_file" envPrefix:"PICOCLAW_TOOLS_WRITE_FILE_"` + AllowReadPaths []string `json:"allow_read_paths" env:"PICOCLAW_TOOLS_ALLOW_READ_PATHS"` + + AllowWritePaths []string `json:"allow_write_paths" env:"PICOCLAW_TOOLS_ALLOW_WRITE_PATHS"` + + Web WebToolsConfig `json:"web"` + + Cron CronToolsConfig `json:"cron"` + + Exec ExecConfig `json:"exec"` + + Skills SkillsToolsConfig `json:"skills"` + + MediaCleanup MediaCleanupConfig `json:"media_cleanup"` + + MCP MCPConfig `json:"mcp"` + + AppendFile ToolConfig `json:"append_file" envPrefix:"PICOCLAW_TOOLS_APPEND_FILE_"` + + EditFile ToolConfig `json:"edit_file" envPrefix:"PICOCLAW_TOOLS_EDIT_FILE_"` + + FindSkills ToolConfig `json:"find_skills" envPrefix:"PICOCLAW_TOOLS_FIND_SKILLS_"` + + I2C ToolConfig `json:"i2c" envPrefix:"PICOCLAW_TOOLS_I2C_"` + + InstallSkill ToolConfig `json:"install_skill" envPrefix:"PICOCLAW_TOOLS_INSTALL_SKILL_"` + + ListDir ToolConfig `json:"list_dir" envPrefix:"PICOCLAW_TOOLS_LIST_DIR_"` + + Message ToolConfig `json:"message" envPrefix:"PICOCLAW_TOOLS_MESSAGE_"` + + ReadFile ReadFileToolConfig `json:"read_file" envPrefix:"PICOCLAW_TOOLS_READ_FILE_"` + + SendFile ToolConfig `json:"send_file" envPrefix:"PICOCLAW_TOOLS_SEND_FILE_"` + + Spawn ToolConfig `json:"spawn" envPrefix:"PICOCLAW_TOOLS_SPAWN_"` + + SpawnStatus ToolConfig `json:"spawn_status" envPrefix:"PICOCLAW_TOOLS_SPAWN_STATUS_"` + + SPI ToolConfig `json:"spi" envPrefix:"PICOCLAW_TOOLS_SPI_"` + + Subagent ToolConfig `json:"subagent" envPrefix:"PICOCLAW_TOOLS_SUBAGENT_"` + + WebFetch ToolConfig `json:"web_fetch" envPrefix:"PICOCLAW_TOOLS_WEB_FETCH_"` + + WriteFile ToolConfig `json:"write_file" envPrefix:"PICOCLAW_TOOLS_WRITE_FILE_"` } type SearchCacheConfig struct { - MaxSize int `json:"max_size" env:"PICOCLAW_SKILLS_SEARCH_CACHE_MAX_SIZE"` + MaxSize int `json:"max_size" env:"PICOCLAW_SKILLS_SEARCH_CACHE_MAX_SIZE"` + TTLSeconds int `json:"ttl_seconds" env:"PICOCLAW_SKILLS_SEARCH_CACHE_TTL_SECONDS"` } @@ -920,485 +1495,863 @@ type SkillsRegistriesConfig struct { type SkillsGithubConfig struct { Token string `json:"token,omitempty" env:"PICOCLAW_TOOLS_SKILLS_GITHUB_AUTH_TOKEN"` + Proxy string `json:"proxy,omitempty" env:"PICOCLAW_TOOLS_SKILLS_GITHUB_PROXY"` } type ClawHubRegistryConfig struct { - Enabled bool `json:"enabled" env:"PICOCLAW_SKILLS_REGISTRIES_CLAWHUB_ENABLED"` - BaseURL string `json:"base_url" env:"PICOCLAW_SKILLS_REGISTRIES_CLAWHUB_BASE_URL"` - AuthToken string `json:"auth_token" env:"PICOCLAW_SKILLS_REGISTRIES_CLAWHUB_AUTH_TOKEN"` - SearchPath string `json:"search_path" env:"PICOCLAW_SKILLS_REGISTRIES_CLAWHUB_SEARCH_PATH"` - SkillsPath string `json:"skills_path" env:"PICOCLAW_SKILLS_REGISTRIES_CLAWHUB_SKILLS_PATH"` - DownloadPath string `json:"download_path" env:"PICOCLAW_SKILLS_REGISTRIES_CLAWHUB_DOWNLOAD_PATH"` - Timeout int `json:"timeout" env:"PICOCLAW_SKILLS_REGISTRIES_CLAWHUB_TIMEOUT"` - MaxZipSize int `json:"max_zip_size" env:"PICOCLAW_SKILLS_REGISTRIES_CLAWHUB_MAX_ZIP_SIZE"` - MaxResponseSize int `json:"max_response_size" env:"PICOCLAW_SKILLS_REGISTRIES_CLAWHUB_MAX_RESPONSE_SIZE"` + Enabled bool `json:"enabled" env:"PICOCLAW_SKILLS_REGISTRIES_CLAWHUB_ENABLED"` + + BaseURL string `json:"base_url" env:"PICOCLAW_SKILLS_REGISTRIES_CLAWHUB_BASE_URL"` + + AuthToken string `json:"auth_token" env:"PICOCLAW_SKILLS_REGISTRIES_CLAWHUB_AUTH_TOKEN"` + + SearchPath string `json:"search_path" env:"PICOCLAW_SKILLS_REGISTRIES_CLAWHUB_SEARCH_PATH"` + + SkillsPath string `json:"skills_path" env:"PICOCLAW_SKILLS_REGISTRIES_CLAWHUB_SKILLS_PATH"` + + DownloadPath string `json:"download_path" env:"PICOCLAW_SKILLS_REGISTRIES_CLAWHUB_DOWNLOAD_PATH"` + + Timeout int `json:"timeout" env:"PICOCLAW_SKILLS_REGISTRIES_CLAWHUB_TIMEOUT"` + + MaxZipSize int `json:"max_zip_size" env:"PICOCLAW_SKILLS_REGISTRIES_CLAWHUB_MAX_ZIP_SIZE"` + + MaxResponseSize int `json:"max_response_size" env:"PICOCLAW_SKILLS_REGISTRIES_CLAWHUB_MAX_RESPONSE_SIZE"` } // MCPServerConfig defines configuration for a single MCP server + type MCPServerConfig struct { + // Enabled indicates whether this MCP server is active + Enabled bool `json:"enabled"` + // Deferred controls whether this server's tools are registered as hidden (deferred/discovery mode). + // When nil, the global Discovery.Enabled setting applies. + // When explicitly set to true or false, it overrides the global setting for this server only. + Deferred *bool `json:"deferred,omitempty"` + // Command is the executable to run (e.g., "npx", "python", "/path/to/server") + Command string `json:"command"` + // Args are the arguments to pass to the command + Args []string `json:"args,omitempty"` + // Env are environment variables to set for the server process (stdio only) + Env map[string]string `json:"env,omitempty"` + // EnvFile is the path to a file containing environment variables (stdio only) + EnvFile string `json:"env_file,omitempty"` + // Type is "stdio", "sse", or "http" (default: stdio if command is set, sse if url is set) + Type string `json:"type,omitempty"` + // URL is used for SSE/HTTP transport + URL string `json:"url,omitempty"` + // Headers are HTTP headers to send with requests (sse/http only) + Headers map[string]string `json:"headers,omitempty"` } // MCPConfig defines configuration for all MCP servers + type MCPConfig struct { - ToolConfig ` envPrefix:"PICOCLAW_TOOLS_MCP_"` - Discovery ToolDiscoveryConfig ` json:"discovery"` + ToolConfig `envPrefix:"PICOCLAW_TOOLS_MCP_"` + + Discovery ToolDiscoveryConfig `json:"discovery"` + // Servers is a map of server name to server configuration - Servers map[string]MCPServerConfig ` json:"servers,omitempty"` + + Servers map[string]MCPServerConfig `json:"servers,omitempty"` } 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 + } // Pre-scan the JSON to check how many model_list entries the user provided. + // Go's JSON decoder reuses existing slice backing-array elements rather than + // zero-initializing them, so fields absent from the user's JSON (e.g. api_base) + // would silently inherit values from the DefaultConfig template at the same + // index position. We only reset cfg.ModelList when the user actually provides + // entries; when count is 0 we keep DefaultConfig's built-in list as fallback. + var tmp Config + if err := json.Unmarshal(data, &tmp); err != nil { + return nil, err + } + if len(tmp.ModelList) > 0 { + cfg.ModelList = nil + } if err := json.Unmarshal(data, cfg); err != nil { + return nil, err + } if passphrase := credential.PassphraseProvider(); passphrase != "" { + for _, m := range cfg.ModelList { + if m.APIKey != "" && !strings.HasPrefix(m.APIKey, "enc://") && + !strings.HasPrefix(m.APIKey, "file://") { + fmt.Fprintf( + os.Stderr, + "picoclaw: warning: model %q has a plaintext api_key; call SaveConfig to encrypt it\n", + m.ModelName, ) + } + } + } if err := env.Parse(cfg); err != nil { + return nil, err + } if err := resolveAPIKeys(cfg.ModelList, filepath.Dir(path)); err != nil { + return nil, err + } // Expand multi-key configs into separate entries for key-level failover + cfg.ModelList = ExpandMultiKeyModels(cfg.ModelList) // Migrate legacy channel config fields to new unified structures + cfg.migrateChannelConfigs() // Auto-migrate: if only legacy providers config exists, convert to model_list + if len(cfg.ModelList) == 0 && cfg.HasProvidersConfig() { + cfg.ModelList = ConvertProvidersToModelList(cfg) + } // Inherit credentials from providers to model_list entries (#1635). + // When both providers and model_list are present, model_list entries + // whose api_key/api_base are empty will inherit from the matching + // provider (matched by protocol prefix). Explicit model_list values + // always take precedence. + if cfg.HasProvidersConfig() { + InheritProviderCredentials(cfg.ModelList, cfg.Providers) + } // Validate model_list for uniqueness and required fields + if err := cfg.ValidateModelList(); err != nil { + return nil, err + } return cfg, nil + } // encryptPlaintextAPIKeys returns a copy of models with plaintext api_key values + // encrypted. Returns (nil, nil) when nothing changed (all keys already sealed or + // empty). Returns (nil, error) if any key fails to encrypt — callers must treat + // this as a hard failure to prevent a mixed plaintext/ciphertext state on disk. + // Symmetric counterpart of resolveAPIKeys: both operate purely on []ModelConfig + // and leave JSON marshaling to the caller. + func encryptPlaintextAPIKeys(models []ModelConfig, passphrase string) ([]ModelConfig, error) { + sealed := make([]ModelConfig, len(models)) + copy(sealed, models) + changed := false + for i := range sealed { + m := &sealed[i] + if m.APIKey == "" || strings.HasPrefix(m.APIKey, "enc://") || + strings.HasPrefix(m.APIKey, "file://") { + continue + } + encrypted, err := credential.Encrypt(passphrase, "", m.APIKey) + if err != nil { + return nil, fmt.Errorf("cannot seal api_key for model %q: %w", m.ModelName, err) + } + m.APIKey = encrypted + changed = true + } + if !changed { + return nil, nil + } + return sealed, nil + } // resolveAPIKeys decrypts or dereferences each api_key in models in-place. + // Supports plaintext (no-op), file:// (read from configDir), and enc:// (AES-GCM decrypt). + // Also resolves api_keys array if present. + func resolveAPIKeys(models []ModelConfig, configDir string) error { + cr := credential.NewResolver(configDir) + for i := range models { + // Resolve single APIKey + resolved, err := cr.Resolve(models[i].APIKey) + if err != nil { + return fmt.Errorf("model_list[%d] (%s): %w", i, models[i].ModelName, err) + } + models[i].APIKey = resolved // Resolve APIKeys array + for j, key := range models[i].APIKeys { + resolved, err := cr.Resolve(key) + if err != nil { + return fmt.Errorf( + "model_list[%d] (%s): api_keys[%d]: %w", + i, + models[i].ModelName, + j, + err, ) + } + models[i].APIKeys[j] = resolved + } + } + return nil + } func (c *Config) migrateChannelConfigs() { + // Discord: mention_only -> group_trigger.mention_only + if c.Channels.Discord.MentionOnly && !c.Channels.Discord.GroupTrigger.MentionOnly { + c.Channels.Discord.GroupTrigger.MentionOnly = true + } // OneBot: group_trigger_prefix -> group_trigger.prefixes + if len(c.Channels.OneBot.GroupTriggerPrefix) > 0 && + len(c.Channels.OneBot.GroupTrigger.Prefixes) == 0 { + c.Channels.OneBot.GroupTrigger.Prefixes = c.Channels.OneBot.GroupTriggerPrefix + } + } func SaveConfig(path string, cfg *Config) error { + if passphrase := credential.PassphraseProvider(); passphrase != "" { + sealed, err := encryptPlaintextAPIKeys(cfg.ModelList, passphrase) + if err != nil { + return err + } + if sealed != nil { + tmp := *cfg + tmp.ModelList = sealed + cfg = &tmp + } + } data, err := json.MarshalIndent(cfg, "", " ") + if err != nil { + return err + } + return fileutil.WriteFileAtomic(path, data, 0o600) + } func (c *Config) WorkspacePath() string { + return expandHome(c.Agents.Defaults.Workspace) + } func (c *Config) GetAPIKey() string { + 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 + } + if c.Providers.Cerebras.APIKey != "" { + return c.Providers.Cerebras.APIKey + } + return "" + } func (c *Config) GetAPIBase() string { + 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 + } // GetModelConfig returns the ModelConfig for the given model name. + // If multiple configs exist with the same model_name, it uses round-robin + // selection for load balancing. Returns an error if the model is not found. + func (c *Config) GetModelConfig(modelName string) (*ModelConfig, error) { + matches := c.findMatches(modelName) + if len(matches) == 0 { + return nil, fmt.Errorf("model %q not found in model_list or providers", modelName) + } + if len(matches) == 1 { + return &matches[0], nil + } // Multiple configs - use round-robin for load balancing + idx := (rrCounter.Add(1) - 1) % uint64(len(matches)) + return &matches[idx], nil + } // findMatches finds all ModelConfig entries with the given model_name. + func (c *Config) findMatches(modelName string) []ModelConfig { + var matches []ModelConfig + for i := range c.ModelList { + if c.ModelList[i].ModelName == modelName { + matches = append(matches, c.ModelList[i]) + } + } + return matches + } // HasProvidersConfig checks if any provider in the old providers config has configuration. + func (c *Config) HasProvidersConfig() bool { + return !c.Providers.IsEmpty() + } // ValidateModelList validates all ModelConfig entries in the model_list. + // It checks that each model config is valid. + // Note: Multiple entries with the same model_name are allowed for load balancing. + func (c *Config) ValidateModelList() error { + for i := range c.ModelList { + if err := c.ModelList[i].Validate(); err != nil { + return fmt.Errorf("model_list[%d]: %w", i, err) + } + } + return nil + } func MergeAPIKeys(apiKey string, apiKeys []string) []string { + seen := make(map[string]struct{}) + var all []string if k := strings.TrimSpace(apiKey); k != "" { + if _, exists := seen[k]; !exists { + seen[k] = struct{}{} + all = append(all, k) + } + } for _, k := range apiKeys { + if trimmed := strings.TrimSpace(k); trimmed != "" { + if _, exists := seen[trimmed]; !exists { + seen[trimmed] = struct{}{} + all = append(all, trimmed) + } + } + } return all + } // ExpandMultiKeyModels expands ModelConfig entries with multiple API keys into + // separate entries for key-level failover. Each key gets its own ModelConfig entry, + // and the original entry's fallbacks are set up to chain through the expanded entries. + // + // Example: {"model_name": "gpt-4", "api_keys": ["k1", "k2", "k3"]} + // Becomes: + // - {"model_name": "gpt-4", "api_key": "k1", "fallbacks": ["gpt-4__key_1", "gpt-4__key_2"]} + // - {"model_name": "gpt-4__key_1", "api_key": "k2"} + // - {"model_name": "gpt-4__key_2", "api_key": "k3"} + func ExpandMultiKeyModels(models []ModelConfig) []ModelConfig { + var expanded []ModelConfig for _, m := range models { + keys := MergeAPIKeys(m.APIKey, m.APIKeys) // Single key or no keys: keep as-is + if len(keys) <= 1 { + // Ensure APIKey is set from APIKeys if needed + if m.APIKey == "" && len(keys) == 1 { + m.APIKey = keys[0] + } + m.APIKeys = nil // Clear APIKeys to avoid confusion + expanded = append(expanded, m) + continue + } // Multiple keys: expand + originalName := m.ModelName // Create entries for additional keys (key_1, key_2, ...) + var fallbackNames []string + for i := 1; i < len(keys); i++ { + suffix := fmt.Sprintf("__key_%d", i) + expandedName := originalName + suffix // Create a copy for the additional key + additionalEntry := ModelConfig{ - ModelName: expandedName, - Model: m.Model, - APIBase: m.APIBase, - APIKey: keys[i], - Proxy: m.Proxy, - AuthMethod: m.AuthMethod, - ConnectMode: m.ConnectMode, - Workspace: m.Workspace, - RPM: m.RPM, + + ModelName: expandedName, + + Model: m.Model, + + APIBase: m.APIBase, + + APIKey: keys[i], + + Proxy: m.Proxy, + + AuthMethod: m.AuthMethod, + + ConnectMode: m.ConnectMode, + + Workspace: m.Workspace, + + RPM: m.RPM, + MaxTokensField: m.MaxTokensField, + RequestTimeout: m.RequestTimeout, - ThinkingLevel: m.ThinkingLevel, + + ThinkingLevel: m.ThinkingLevel, } + expanded = append(expanded, additionalEntry) + fallbackNames = append(fallbackNames, expandedName) + } // Create the primary entry with first key and fallbacks + primaryEntry := ModelConfig{ - ModelName: originalName, - Model: m.Model, - APIBase: m.APIBase, - APIKey: keys[0], - Proxy: m.Proxy, - AuthMethod: m.AuthMethod, - ConnectMode: m.ConnectMode, - Workspace: m.Workspace, - RPM: m.RPM, + + ModelName: originalName, + + Model: m.Model, + + APIBase: m.APIBase, + + APIKey: keys[0], + + Proxy: m.Proxy, + + AuthMethod: m.AuthMethod, + + ConnectMode: m.ConnectMode, + + Workspace: m.Workspace, + + RPM: m.RPM, + MaxTokensField: m.MaxTokensField, + RequestTimeout: m.RequestTimeout, - ThinkingLevel: m.ThinkingLevel, + + ThinkingLevel: m.ThinkingLevel, } // Prepend new fallbacks to existing ones + if len(fallbackNames) > 0 { + primaryEntry.Fallbacks = append(fallbackNames, m.Fallbacks...) + } else if len(m.Fallbacks) > 0 { + primaryEntry.Fallbacks = m.Fallbacks + } expanded = append(expanded, primaryEntry) + } return expanded + } func (t *ToolsConfig) IsToolEnabled(name string) bool { + switch name { + case "web": + return t.Web.Enabled + case "cron": + return t.Cron.Enabled + case "exec": + return t.Exec.Enabled + case "skills": + return t.Skills.Enabled + case "media_cleanup": + return t.MediaCleanup.Enabled + case "append_file": + return t.AppendFile.Enabled + case "edit_file": + return t.EditFile.Enabled + case "find_skills": + return t.FindSkills.Enabled + case "i2c": + return t.I2C.Enabled + case "install_skill": + return t.InstallSkill.Enabled + case "list_dir": + return t.ListDir.Enabled + case "message": + return t.Message.Enabled + case "read_file": + return t.ReadFile.Enabled + case "spawn": + return t.Spawn.Enabled + case "spawn_status": + return t.SpawnStatus.Enabled + case "spi": + return t.SPI.Enabled + case "subagent": + return t.Subagent.Enabled + case "web_fetch": + return t.WebFetch.Enabled + case "send_file": + return t.SendFile.Enabled + case "write_file": + return t.WriteFile.Enabled + case "mcp": + return t.MCP.Enabled + default: + return true + } + } diff --git a/web/backend/api/channels.go b/web/backend/api/channels.go index 507882823..56f3da836 100644 --- a/web/backend/api/channels.go +++ b/web/backend/api/channels.go @@ -29,6 +29,7 @@ var channelCatalog = []channelCatalogItem{ {Name: "maixcam", ConfigKey: "maixcam"}, {Name: "matrix", ConfigKey: "matrix"}, {Name: "irc", ConfigKey: "irc"}, + {Name: "mqtt", ConfigKey: "mqtt"}, } // registerChannelRoutes binds read-only channel catalog endpoints to the ServeMux. diff --git a/web/backend/api/config.go b/web/backend/api/config.go index a7d5b3c5d..ab0099e16 100644 --- a/web/backend/api/config.go +++ b/web/backend/api/config.go @@ -189,6 +189,19 @@ func validateConfig(cfg *config.Config) []string { errs = append(errs, "channels.discord.token is required when discord channel is enabled") } + // MQTT: broker and client_id required when enabled + if cfg.Channels.MQTT.Enabled { + if cfg.Channels.MQTT.Broker == "" { + errs = append(errs, "channels.mqtt.broker is required when mqtt channel is enabled") + } + if cfg.Channels.MQTT.ClientID == "" { + errs = append(errs, "channels.mqtt.client_id is required when mqtt channel is enabled") + } + if len(cfg.Channels.MQTT.SubscribeTopics) == 0 { + errs = append(errs, "channels.mqtt.subscribe_topics is required when mqtt channel is enabled") + } + } + if cfg.Tools.Exec.Enabled { if cfg.Tools.Exec.EnableDenyPatterns { errs = append( diff --git a/web/frontend/src/components/channels/channel-config-page.tsx b/web/frontend/src/components/channels/channel-config-page.tsx index b19d11e6a..74c386576 100644 --- a/web/frontend/src/components/channels/channel-config-page.tsx +++ b/web/frontend/src/components/channels/channel-config-page.tsx @@ -166,6 +166,8 @@ function isConfigured( ) case "irc": return asString(config.server) !== "" + case "mqtt": + return asString(config.broker) !== "" && asString(config.client_id) !== "" default: return false } @@ -205,6 +207,8 @@ function getRequiredFieldKeys(channelName: string): string[] { return ["homeserver", "user_id", "access_token"] case "irc": return ["server"] + case "mqtt": + return ["broker", "client_id", "subscribe_topics"] default: return [] } diff --git a/web/frontend/src/components/channels/channel-forms/generic-form.tsx b/web/frontend/src/components/channels/channel-forms/generic-form.tsx index db14fc206..d4567a9ba 100644 --- a/web/frontend/src/components/channels/channel-forms/generic-form.tsx +++ b/web/frontend/src/components/channels/channel-forms/generic-form.tsx @@ -31,6 +31,9 @@ const SECRET_FIELDS = new Set([ "password", "nickserv_password", "sasl_password", + "tls_ca", + "tls_cert", + "tls_key", ]) // Fields to skip in the generic form (handled by enabled toggle or internal). @@ -139,6 +142,15 @@ export function GenericForm({ channels: t("channels.form.desc.channels"), request_caps: t("channels.form.desc.requestCaps"), max_base64_file_size_mib: t("channels.form.desc.maxBase64FileSizeMiB"), + broker: t("channels.form.desc.broker"), + subscribe_topics: t("channels.form.desc.subscribeTopics"), + subscribe_json_key: t("channels.form.desc.subscribeJsonKey"), + reply_topic: t("channels.form.desc.replyTopic"), + reply_json_key: t("channels.form.desc.replyJsonKey"), + qos: t("channels.form.desc.qos"), + retain: t("channels.form.desc.retain"), + prefix: t("channels.form.desc.prefix"), + instruction: t("channels.form.desc.instruction"), } return ( descriptions[key] ?? diff --git a/web/frontend/src/hooks/use-sidebar-channels.ts b/web/frontend/src/hooks/use-sidebar-channels.ts index 5579a955b..22113a740 100644 --- a/web/frontend/src/hooks/use-sidebar-channels.ts +++ b/web/frontend/src/hooks/use-sidebar-channels.ts @@ -44,6 +44,7 @@ const CHANNEL_IMPORTANCE_ORDER = [ "pico", "maixcam", "irc", + "mqtt", "whatsapp", "whatsapp_native", ] diff --git a/web/frontend/src/i18n/locales/en.json b/web/frontend/src/i18n/locales/en.json index 0ff2beb25..f5eae2758 100644 --- a/web/frontend/src/i18n/locales/en.json +++ b/web/frontend/src/i18n/locales/en.json @@ -240,7 +240,8 @@ "pico": "Web", "maixcam": "MaixCam", "matrix": "Matrix", - "irc": "IRC" + "irc": "IRC", + "mqtt": "MQTT" }, "field": { "token": "Bot Token", @@ -328,6 +329,15 @@ "channels": "IRC channels to join.", "requestCaps": "IRC capability list requested on connect.", "maxBase64FileSizeMiB": "Maximum size in MiB for converting local files to base64 before upload. 0 means unlimited. Applies only to local files, not URL uploads.", + "broker": "MQTT broker URL (e.g. tcp://broker.emqx.io:1883).", + "subscribeTopics": "MQTT topics to subscribe to, separated by commas.", + "subscribeJsonKey": "JSON key to extract from incoming messages. Leave empty to use raw payload.", + "replyTopic": "Default topic for publishing replies.", + "replyJsonKey": "JSON key to wrap reply content. Leave empty to send plain text.", + "qos": "Quality of Service level (0, 1, or 2).", + "retain": "Whether to retain published messages on the broker.", + "prefix": "URL prefix for the channel.", + "instruction": "System instruction prepended to incoming messages.", "genericField": "Used to configure {{field}}." } }, diff --git a/web/frontend/src/i18n/locales/zh.json b/web/frontend/src/i18n/locales/zh.json index fc1f007ae..52361621d 100644 --- a/web/frontend/src/i18n/locales/zh.json +++ b/web/frontend/src/i18n/locales/zh.json @@ -240,7 +240,8 @@ "pico": "Web", "maixcam": "MaixCam", "matrix": "Matrix", - "irc": "IRC" + "irc": "IRC", + "mqtt": "MQTT" }, "field": { "token": "Bot Token", @@ -328,6 +329,15 @@ "channels": "要加入的 IRC 频道列表。", "requestCaps": "连接时请求的 IRC 扩展能力列表。", "maxBase64FileSizeMiB": "本地文件转为 base64 上传的最大体积,单位 MiB;0 表示不限制,仅影响本地文件,不影响 URL 直传。", + "broker": "MQTT 代理地址(例如 tcp://broker.emqx.io:1883)。", + "subscribeTopics": "要订阅的 MQTT 主题,多个值用逗号分隔。", + "subscribeJsonKey": "从接收消息中提取的 JSON 键名。留空则使用原始内容。", + "replyTopic": "发布回复的默认主题。", + "replyJsonKey": "用于包装回复内容的 JSON 键名。留空则发送纯文本。", + "qos": "服务质量等级(0、1 或 2)。", + "retain": "是否在代理上保留已发布的消息。", + "prefix": "频道 URL 前缀。", + "instruction": "附加到接收消息前的系统指令。", "genericField": "用于配置{{field}}。" } },