{"repo": "bbgiangpp-cpu/goclaw-daugiatem", "files": {"ui/web/src/constants/providers.ts": {"34d5625c2a00cb343824577f39c03b4cdc414182": {"t": 1785746050.5044146, "n": 4020, "h": "f2890de2bce26f02", "c": "import { slugify } from \"@/lib/slug\";\n\nexport interface ProviderTypeInfo {\n value: string;\n label: string;\n apiBase: string;\n placeholder: string;\n}\n\ntype ProviderAliasSource = string | { name?: string | null };\n\nexport const DEFAULT_CODEX_OAUTH_ALIAS = \"openai-codex\";\n\nexport const PROVIDER_TYPES: ProviderTypeInfo[] = [\n { value: \"chatgpt_oauth\", label: \"ChatGPT Subscription (OAuth)\", apiBase: \"\", placeholder: \"\" },\n { value: \"anthropic_native\", label: \"Anthropic (Native)\", apiBase: \"\", placeholder: \"https://api.anthropic.com\" },\n { value: \"openai_compat\", label: \"OpenAI Compatible\", apiBase: \"\", placeholder: \"https://api.openai.com/v1\" },\n { value: \"gemini_native\", label: \"Google Gemini\", apiBase: \"https://generativelanguage.googleapis.com/v1beta/openai\", placeholder: \"\" },\n { value: \"vertex\", label: \"Google Vertex AI\", apiBase: \"\", placeholder: \"Auto-computed from project_id + region (settings)\" },\n { value: \"openrouter\", label: \"OpenRouter\", apiBase: \"https://openrouter.ai/api/v1\", placeholder: \"\" },\n { value: \"groq\", label: \"Groq\", apiBase: \"https://api.groq.com/openai/v1\", placeholder: \"\" },\n { value: \"deepseek\", label: \"DeepSeek\", apiBase: \"https://api.deepseek.com/v1\", placeholder: \"\" },\n { value: \"mistral\", label: \"Mistral AI\", apiBase: \"https://api.mistral.ai/v1\", placeholder: \"\" },\n { value: \"xai\", label: \"xAI (Grok)\", apiBase: \"https://api.x.ai/v1\", placeholder: \"\" },\n { value: \"minimax_native\", label: \"MiniMax (Native)\", apiBase: \"https://api.minimax.io/v1\", placeholder: \"\" },\n { value: \"novita\", label: \"Novita AI\", apiBase: \"https://api.novita.ai/openai\", placeholder: \"\" },\n { value: \"cohere\", label: \"Cohere\", apiBase: \"https://api.cohere.ai/compatibility/v1\", placeholder: \"\" },\n { value: \"perplexity\", label: \"Perplexity\", apiBase: \"https://api.perplexity.ai\", placeholder: \"\" },\n { value: \"dashscope\", label: \"DashScope (Qwen)\", apiBase: \"https://dashscope-intl.aliyuncs.com/compatible-mode/v1\", placeholder: \"\" },\n { value: \"bailian\", label: \"Bailian Coding\", apiBase: \"https://coding-intl.dashscope.aliyuncs.com/v1\", placeholder: \"\" },\n { value: \"yescale\", label: \"YesScale\", apiBase: \"https://api.yescale.one/v1\", placeholder: \"\" },\n { value: \"zai\", label: \"Z.ai API\", apiBase: \"https://api.z.ai/api/paas/v4\", placeholder: \"\" },\n { value: \"zai_coding\", label: \"Z.ai Coding Plan\", apiBase: \"https://api.z.ai/api/coding/paas/v4\", placeholder: \"\" },\n { value: \"byteplus\", label: \"BytePlus ModelArk\", apiBase: \"https://ark.ap-southeast.bytepluses.com/api/v3\", placeholder: \"\" },\n { value: \"byteplus_coding\", label: \"BytePlus Coding Plan\", apiBase: \"https://ark.ap-southeast.bytepluses.com/api/coding/v3\", placeholder: \"\" },\n { value: \"kimi_coding\", label: \"Kimi Coding (Moonshot)\", apiBase: \"https://api.kimi.com/coding/v1\", placeholder: \"\" },\n { value: \"ollama\", label: \"Ollama (Local)\", apiBase: \"http://localhost:11434/v1\", placeholder: \"\" },\n { value: \"ollama_cloud\", label: \"Ollama Cloud\", apiBase: \"https://ollama.com/v1\", placeholder: \"\" },\n { value: \"claude_cli\", label: \"Claude CLI (Local)\", apiBase: \"\", placeholder: \"\" },\n { value: \"acp\", label: \"ACP Agent (Subprocess)\", apiBase: \"\", placeholder: \"claude\" },\n];\n\nfunction providerAliasName(value: ProviderAliasSource): string {\n if (typeof value === \"string\") return value.trim().toLowerCase();\n return value.name?.trim().toLowerCase() ?? \"\";\n}\n\nexport function suggestUniqueProviderAlias(\n existingProviders: ProviderAliasSource[],\n options?: { baseAlias?: string; excludeName?: string },\n): string {\n const baseAlias = slugify(options?.baseAlias ?? DEFAULT_CODEX_OAUTH_ALIAS) || DEFAULT_CODEX_OAUTH_ALIAS;\n const taken = new Set(existingProviders.map(providerAliasName).filter(Boolean));\n const excludeName = providerAliasName(options?.excludeName ?? \"\");\n\n if (excludeName) taken.delete(excludeName);\n if (!taken.has(baseAlias)) return baseAlias;\n\n let suffix = 2;\n while (taken.has(`${baseAlias}-${suffix}`)) {\n suffix += 1;\n }\n return `${baseAlias}-${suffix}`;\n}\n"}, "b844af8d25afc7ad0c4b3bafd9b95b11fe54126b": {"t": 1785759220.893552, "n": 4020, "h": "f2890de2bce26f02", "c": "import { slugify } from \"@/lib/slug\";\n\nexport interface ProviderTypeInfo {\n value: string;\n label: string;\n apiBase: string;\n placeholder: string;\n}\n\ntype ProviderAliasSource = string | { name?: string | null };\n\nexport const DEFAULT_CODEX_OAUTH_ALIAS = \"openai-codex\";\n\nexport const PROVIDER_TYPES: ProviderTypeInfo[] = [\n { value: \"chatgpt_oauth\", label: \"ChatGPT Subscription (OAuth)\", apiBase: \"\", placeholder: \"\" },\n { value: \"anthropic_native\", label: \"Anthropic (Native)\", apiBase: \"\", placeholder: \"https://api.anthropic.com\" },\n { value: \"openai_compat\", label: \"OpenAI Compatible\", apiBase: \"\", placeholder: \"https://api.openai.com/v1\" },\n { value: \"gemini_native\", label: \"Google Gemini\", apiBase: \"https://generativelanguage.googleapis.com/v1beta/openai\", placeholder: \"\" },\n { value: \"vertex\", label: \"Google Vertex AI\", apiBase: \"\", placeholder: \"Auto-computed from project_id + region (settings)\" },\n { value: \"openrouter\", label: \"OpenRouter\", apiBase: \"https://openrouter.ai/api/v1\", placeholder: \"\" },\n { value: \"groq\", label: \"Groq\", apiBase: \"https://api.groq.com/openai/v1\", placeholder: \"\" },\n { value: \"deepseek\", label: \"DeepSeek\", apiBase: \"https://api.deepseek.com/v1\", placeholder: \"\" },\n { value: \"mistral\", label: \"Mistral AI\", apiBase: \"https://api.mistral.ai/v1\", placeholder: \"\" },\n { value: \"xai\", label: \"xAI (Grok)\", apiBase: \"https://api.x.ai/v1\", placeholder: \"\" },\n { value: \"minimax_native\", label: \"MiniMax (Native)\", apiBase: \"https://api.minimax.io/v1\", placeholder: \"\" },\n { value: \"novita\", label: \"Novita AI\", apiBase: \"https://api.novita.ai/openai\", placeholder: \"\" },\n { value: \"cohere\", label: \"Cohere\", apiBase: \"https://api.cohere.ai/compatibility/v1\", placeholder: \"\" },\n { value: \"perplexity\", label: \"Perplexity\", apiBase: \"https://api.perplexity.ai\", placeholder: \"\" },\n { value: \"dashscope\", label: \"DashScope (Qwen)\", apiBase: \"https://dashscope-intl.aliyuncs.com/compatible-mode/v1\", placeholder: \"\" },\n { value: \"bailian\", label: \"Bailian Coding\", apiBase: \"https://coding-intl.dashscope.aliyuncs.com/v1\", placeholder: \"\" },\n { value: \"yescale\", label: \"YesScale\", apiBase: \"https://api.yescale.one/v1\", placeholder: \"\" },\n { value: \"zai\", label: \"Z.ai API\", apiBase: \"https://api.z.ai/api/paas/v4\", placeholder: \"\" },\n { value: \"zai_coding\", label: \"Z.ai Coding Plan\", apiBase: \"https://api.z.ai/api/coding/paas/v4\", placeholder: \"\" },\n { value: \"byteplus\", label: \"BytePlus ModelArk\", apiBase: \"https://ark.ap-southeast.bytepluses.com/api/v3\", placeholder: \"\" },\n { value: \"byteplus_coding\", label: \"BytePlus Coding Plan\", apiBase: \"https://ark.ap-southeast.bytepluses.com/api/coding/v3\", placeholder: \"\" },\n { value: \"kimi_coding\", label: \"Kimi Coding (Moonshot)\", apiBase: \"https://api.kimi.com/coding/v1\", placeholder: \"\" },\n { value: \"ollama\", label: \"Ollama (Local)\", apiBase: \"http://localhost:11434/v1\", placeholder: \"\" },\n { value: \"ollama_cloud\", label: \"Ollama Cloud\", apiBase: \"https://ollama.com/v1\", placeholder: \"\" },\n { value: \"claude_cli\", label: \"Claude CLI (Local)\", apiBase: \"\", placeholder: \"\" },\n { value: \"acp\", label: \"ACP Agent (Subprocess)\", apiBase: \"\", placeholder: \"claude\" },\n];\n\nfunction providerAliasName(value: ProviderAliasSource): string {\n if (typeof value === \"string\") return value.trim().toLowerCase();\n return value.name?.trim().toLowerCase() ?? \"\";\n}\n\nexport function suggestUniqueProviderAlias(\n existingProviders: ProviderAliasSource[],\n options?: { baseAlias?: string; excludeName?: string },\n): string {\n const baseAlias = slugify(options?.baseAlias ?? DEFAULT_CODEX_OAUTH_ALIAS) || DEFAULT_CODEX_OAUTH_ALIAS;\n const taken = new Set(existingProviders.map(providerAliasName).filter(Boolean));\n const excludeName = providerAliasName(options?.excludeName ?? \"\");\n\n if (excludeName) taken.delete(excludeName);\n if (!taken.has(baseAlias)) return baseAlias;\n\n let suffix = 2;\n while (taken.has(`${baseAlias}-${suffix}`)) {\n suffix += 1;\n }\n return `${baseAlias}-${suffix}`;\n}\n"}, "2da52cfaee222c0dcc956f3bf500c2812edd6bbd": {"t": 1785759223.1240215, "n": 3899, "h": "41bdab05e536a17e", "c": "import { slugify } from \"@/lib/slug\";\n\nexport interface ProviderTypeInfo {\n value: string;\n label: string;\n apiBase: string;\n placeholder: string;\n}\n\ntype ProviderAliasSource = string | { name?: string | null };\n\nexport const DEFAULT_CODEX_OAUTH_ALIAS = \"openai-codex\";\n\nexport const PROVIDER_TYPES: ProviderTypeInfo[] = [\n { value: \"chatgpt_oauth\", label: \"ChatGPT Subscription (OAuth)\", apiBase: \"\", placeholder: \"\" },\n { value: \"anthropic_native\", label: \"Anthropic (Native)\", apiBase: \"\", placeholder: \"https://api.anthropic.com\" },\n { value: \"openai_compat\", label: \"OpenAI Compatible\", apiBase: \"\", placeholder: \"https://api.openai.com/v1\" },\n { value: \"gemini_native\", label: \"Google Gemini\", apiBase: \"https://generativelanguage.googleapis.com/v1beta/openai\", placeholder: \"\" },\n { value: \"vertex\", label: \"Google Vertex AI\", apiBase: \"\", placeholder: \"Auto-computed from project_id + region (settings)\" },\n { value: \"openrouter\", label: \"OpenRouter\", apiBase: \"https://openrouter.ai/api/v1\", placeholder: \"\" },\n { value: \"groq\", label: \"Groq\", apiBase: \"https://api.groq.com/openai/v1\", placeholder: \"\" },\n { value: \"deepseek\", label: \"DeepSeek\", apiBase: \"https://api.deepseek.com/v1\", placeholder: \"\" },\n { value: \"mistral\", label: \"Mistral AI\", apiBase: \"https://api.mistral.ai/v1\", placeholder: \"\" },\n { value: \"xai\", label: \"xAI (Grok)\", apiBase: \"https://api.x.ai/v1\", placeholder: \"\" },\n { value: \"minimax_native\", label: \"MiniMax (Native)\", apiBase: \"https://api.minimax.io/v1\", placeholder: \"\" },\n { value: \"novita\", label: \"Novita AI\", apiBase: \"https://api.novita.ai/openai\", placeholder: \"\" },\n { value: \"cohere\", label: \"Cohere\", apiBase: \"https://api.cohere.ai/compatibility/v1\", placeholder: \"\" },\n { value: \"perplexity\", label: \"Perplexity\", apiBase: \"https://api.perplexity.ai\", placeholder: \"\" },\n { value: \"dashscope\", label: \"DashScope (Qwen)\", apiBase: \"https://dashscope-intl.aliyuncs.com/compatible-mode/v1\", placeholder: \"\" },\n { value: \"bailian\", label: \"Bailian Coding\", apiBase: \"https://coding-intl.dashscope.aliyuncs.com/v1\", placeholder: \"\" },\n { value: \"yescale\", label: \"YesScale\", apiBase: \"https://api.yescale.one/v1\", placeholder: \"\" },\n { value: \"zai\", label: \"Z.ai API\", apiBase: \"https://api.z.ai/api/paas/v4\", placeholder: \"\" },\n { value: \"zai_coding\", label: \"Z.ai Coding Plan\", apiBase: \"https://api.z.ai/api/coding/paas/v4\", placeholder: \"\" },\n { value: \"byteplus\", label: \"BytePlus ModelArk\", apiBase: \"https://ark.ap-southeast.bytepluses.com/api/v3\", placeholder: \"\" },\n { value: \"byteplus_coding\", label: \"BytePlus Coding Plan\", apiBase: \"https://ark.ap-southeast.bytepluses.com/api/coding/v3\", placeholder: \"\" },\n { value: \"ollama\", label: \"Ollama (Local)\", apiBase: \"http://localhost:11434/v1\", placeholder: \"\" },\n { value: \"ollama_cloud\", label: \"Ollama Cloud\", apiBase: \"https://ollama.com/v1\", placeholder: \"\" },\n { value: \"claude_cli\", label: \"Claude CLI (Local)\", apiBase: \"\", placeholder: \"\" },\n { value: \"acp\", label: \"ACP Agent (Subprocess)\", apiBase: \"\", placeholder: \"claude\" },\n];\n\nfunction providerAliasName(value: ProviderAliasSource): string {\n if (typeof value === \"string\") return value.trim().toLowerCase();\n return value.name?.trim().toLowerCase() ?? \"\";\n}\n\nexport function suggestUniqueProviderAlias(\n existingProviders: ProviderAliasSource[],\n options?: { baseAlias?: string; excludeName?: string },\n): string {\n const baseAlias = slugify(options?.baseAlias ?? DEFAULT_CODEX_OAUTH_ALIAS) || DEFAULT_CODEX_OAUTH_ALIAS;\n const taken = new Set(existingProviders.map(providerAliasName).filter(Boolean));\n const excludeName = providerAliasName(options?.excludeName ?? \"\");\n\n if (excludeName) taken.delete(excludeName);\n if (!taken.has(baseAlias)) return baseAlias;\n\n let suffix = 2;\n while (taken.has(`${baseAlias}-${suffix}`)) {\n suffix += 1;\n }\n return `${baseAlias}-${suffix}`;\n}\n"}, "aa5158d4a26422d01a2107ed67387734b7395362": {"t": 1785759227.9553716, "n": 3770, "h": "9106d4947ba2edb4", "c": "import { slugify } from \"@/lib/slug\";\n\nexport interface ProviderTypeInfo {\n value: string;\n label: string;\n apiBase: string;\n placeholder: string;\n}\n\ntype ProviderAliasSource = string | { name?: string | null };\n\nexport const DEFAULT_CODEX_OAUTH_ALIAS = \"openai-codex\";\n\nexport const PROVIDER_TYPES: ProviderTypeInfo[] = [\n { value: \"chatgpt_oauth\", label: \"ChatGPT Subscription (OAuth)\", apiBase: \"\", placeholder: \"\" },\n { value: \"anthropic_native\", label: \"Anthropic (Native)\", apiBase: \"\", placeholder: \"https://api.anthropic.com\" },\n { value: \"openai_compat\", label: \"OpenAI Compatible\", apiBase: \"\", placeholder: \"https://api.openai.com/v1\" },\n { value: \"gemini_native\", label: \"Google Gemini\", apiBase: \"https://generativelanguage.googleapis.com/v1beta/openai\", placeholder: \"\" },\n { value: \"openrouter\", label: \"OpenRouter\", apiBase: \"https://openrouter.ai/api/v1\", placeholder: \"\" },\n { value: \"groq\", label: \"Groq\", apiBase: \"https://api.groq.com/openai/v1\", placeholder: \"\" },\n { value: \"deepseek\", label: \"DeepSeek\", apiBase: \"https://api.deepseek.com/v1\", placeholder: \"\" },\n { value: \"mistral\", label: \"Mistral AI\", apiBase: \"https://api.mistral.ai/v1\", placeholder: \"\" },\n { value: \"xai\", label: \"xAI (Grok)\", apiBase: \"https://api.x.ai/v1\", placeholder: \"\" },\n { value: \"minimax_native\", label: \"MiniMax (Native)\", apiBase: \"https://api.minimax.io/v1\", placeholder: \"\" },\n { value: \"novita\", label: \"Novita AI\", apiBase: \"https://api.novita.ai/openai\", placeholder: \"\" },\n { value: \"cohere\", label: \"Cohere\", apiBase: \"https://api.cohere.ai/compatibility/v1\", placeholder: \"\" },\n { value: \"perplexity\", label: \"Perplexity\", apiBase: \"https://api.perplexity.ai\", placeholder: \"\" },\n { value: \"dashscope\", label: \"DashScope (Qwen)\", apiBase: \"https://dashscope-intl.aliyuncs.com/compatible-mode/v1\", placeholder: \"\" },\n { value: \"bailian\", label: \"Bailian Coding\", apiBase: \"https://coding-intl.dashscope.aliyuncs.com/v1\", placeholder: \"\" },\n { value: \"yescale\", label: \"YesScale\", apiBase: \"https://api.yescale.one/v1\", placeholder: \"\" },\n { value: \"zai\", label: \"Z.ai API\", apiBase: \"https://api.z.ai/api/paas/v4\", placeholder: \"\" },\n { value: \"zai_coding\", label: \"Z.ai Coding Plan\", apiBase: \"https://api.z.ai/api/coding/paas/v4\", placeholder: \"\" },\n { value: \"byteplus\", label: \"BytePlus ModelArk\", apiBase: \"https://ark.ap-southeast.bytepluses.com/api/v3\", placeholder: \"\" },\n { value: \"byteplus_coding\", label: \"BytePlus Coding Plan\", apiBase: \"https://ark.ap-southeast.bytepluses.com/api/coding/v3\", placeholder: \"\" },\n { value: \"ollama\", label: \"Ollama (Local)\", apiBase: \"http://localhost:11434/v1\", placeholder: \"\" },\n { value: \"ollama_cloud\", label: \"Ollama Cloud\", apiBase: \"https://ollama.com/v1\", placeholder: \"\" },\n { value: \"claude_cli\", label: \"Claude CLI (Local)\", apiBase: \"\", placeholder: \"\" },\n { value: \"acp\", label: \"ACP Agent (Subprocess)\", apiBase: \"\", placeholder: \"claude\" },\n];\n\nfunction providerAliasName(value: ProviderAliasSource): string {\n if (typeof value === \"string\") return value.trim().toLowerCase();\n return value.name?.trim().toLowerCase() ?? \"\";\n}\n\nexport function suggestUniqueProviderAlias(\n existingProviders: ProviderAliasSource[],\n options?: { baseAlias?: string; excludeName?: string },\n): string {\n const baseAlias = slugify(options?.baseAlias ?? DEFAULT_CODEX_OAUTH_ALIAS) || DEFAULT_CODEX_OAUTH_ALIAS;\n const taken = new Set(existingProviders.map(providerAliasName).filter(Boolean));\n const excludeName = providerAliasName(options?.excludeName ?? \"\");\n\n if (excludeName) taken.delete(excludeName);\n if (!taken.has(baseAlias)) return baseAlias;\n\n let suffix = 2;\n while (taken.has(`${baseAlias}-${suffix}`)) {\n suffix += 1;\n }\n return `${baseAlias}-${suffix}`;\n}\n"}, "bd1411028b32b9b0351393e09cee7c0f3b5e1c78": {"t": 1785759229.591864, "n": 3495, "h": "f7cebfc69b9fd89a", "c": "import { slugify } from \"@/lib/slug\";\n\nexport interface ProviderTypeInfo {\n value: string;\n label: string;\n apiBase: string;\n placeholder: string;\n}\n\ntype ProviderAliasSource = string | { name?: string | null };\n\nexport const DEFAULT_CODEX_OAUTH_ALIAS = \"openai-codex\";\n\nexport const PROVIDER_TYPES: ProviderTypeInfo[] = [\n { value: \"chatgpt_oauth\", label: \"ChatGPT Subscription (OAuth)\", apiBase: \"\", placeholder: \"\" },\n { value: \"anthropic_native\", label: \"Anthropic (Native)\", apiBase: \"\", placeholder: \"https://api.anthropic.com\" },\n { value: \"openai_compat\", label: \"OpenAI Compatible\", apiBase: \"\", placeholder: \"https://api.openai.com/v1\" },\n { value: \"gemini_native\", label: \"Google Gemini\", apiBase: \"https://generativelanguage.googleapis.com/v1beta/openai\", placeholder: \"\" },\n { value: \"openrouter\", label: \"OpenRouter\", apiBase: \"https://openrouter.ai/api/v1\", placeholder: \"\" },\n { value: \"groq\", label: \"Groq\", apiBase: \"https://api.groq.com/openai/v1\", placeholder: \"\" },\n { value: \"deepseek\", label: \"DeepSeek\", apiBase: \"https://api.deepseek.com/v1\", placeholder: \"\" },\n { value: \"mistral\", label: \"Mistral AI\", apiBase: \"https://api.mistral.ai/v1\", placeholder: \"\" },\n { value: \"xai\", label: \"xAI (Grok)\", apiBase: \"https://api.x.ai/v1\", placeholder: \"\" },\n { value: \"minimax_native\", label: \"MiniMax (Native)\", apiBase: \"https://api.minimax.io/v1\", placeholder: \"\" },\n { value: \"novita\", label: \"Novita AI\", apiBase: \"https://api.novita.ai/openai\", placeholder: \"\" },\n { value: \"cohere\", label: \"Cohere\", apiBase: \"https://api.cohere.ai/compatibility/v1\", placeholder: \"\" },\n { value: \"perplexity\", label: \"Perplexity\", apiBase: \"https://api.perplexity.ai\", placeholder: \"\" },\n { value: \"dashscope\", label: \"DashScope (Qwen)\", apiBase: \"https://dashscope-intl.aliyuncs.com/compatible-mode/v1\", placeholder: \"\" },\n { value: \"bailian\", label: \"Bailian Coding\", apiBase: \"https://coding-intl.dashscope.aliyuncs.com/v1\", placeholder: \"\" },\n { value: \"yescale\", label: \"YesScale\", apiBase: \"https://api.yescale.one/v1\", placeholder: \"\" },\n { value: \"zai\", label: \"Z.ai API\", apiBase: \"https://api.z.ai/api/paas/v4\", placeholder: \"\" },\n { value: \"zai_coding\", label: \"Z.ai Coding Plan\", apiBase: \"https://api.z.ai/api/coding/paas/v4\", placeholder: \"\" },\n { value: \"ollama\", label: \"Ollama (Local)\", apiBase: \"http://localhost:11434/v1\", placeholder: \"\" },\n { value: \"ollama_cloud\", label: \"Ollama Cloud\", apiBase: \"https://ollama.com/v1\", placeholder: \"\" },\n { value: \"claude_cli\", label: \"Claude CLI (Local)\", apiBase: \"\", placeholder: \"\" },\n { value: \"acp\", label: \"ACP Agent (Subprocess)\", apiBase: \"\", placeholder: \"claude\" },\n];\n\nfunction providerAliasName(value: ProviderAliasSource): string {\n if (typeof value === \"string\") return value.trim().toLowerCase();\n return value.name?.trim().toLowerCase() ?? \"\";\n}\n\nexport function suggestUniqueProviderAlias(\n existingProviders: ProviderAliasSource[],\n options?: { baseAlias?: string; excludeName?: string },\n): string {\n const baseAlias = slugify(options?.baseAlias ?? DEFAULT_CODEX_OAUTH_ALIAS) || DEFAULT_CODEX_OAUTH_ALIAS;\n const taken = new Set(existingProviders.map(providerAliasName).filter(Boolean));\n const excludeName = providerAliasName(options?.excludeName ?? \"\");\n\n if (excludeName) taken.delete(excludeName);\n if (!taken.has(baseAlias)) return baseAlias;\n\n let suffix = 2;\n while (taken.has(`${baseAlias}-${suffix}`)) {\n suffix += 1;\n }\n return `${baseAlias}-${suffix}`;\n}\n"}, "30708ae79da930e13627931ba2d3c1f72f2f169c": {"t": 1785759230.857519, "n": 3394, "h": "83a4f97f70422d13", "c": "import { slugify } from \"@/lib/slug\";\n\nexport interface ProviderTypeInfo {\n value: string;\n label: string;\n apiBase: string;\n placeholder: string;\n}\n\ntype ProviderAliasSource = string | { name?: string | null };\n\nexport const DEFAULT_CODEX_OAUTH_ALIAS = \"openai-codex\";\n\nexport const PROVIDER_TYPES: ProviderTypeInfo[] = [\n { value: \"chatgpt_oauth\", label: \"ChatGPT Subscription (OAuth)\", apiBase: \"\", placeholder: \"\" },\n { value: \"anthropic_native\", label: \"Anthropic (Native)\", apiBase: \"\", placeholder: \"https://api.anthropic.com\" },\n { value: \"openai_compat\", label: \"OpenAI Compatible\", apiBase: \"\", placeholder: \"https://api.openai.com/v1\" },\n { value: \"gemini_native\", label: \"Google Gemini\", apiBase: \"https://generativelanguage.googleapis.com/v1beta/openai\", placeholder: \"\" },\n { value: \"openrouter\", label: \"OpenRouter\", apiBase: \"https://openrouter.ai/api/v1\", placeholder: \"\" },\n { value: \"groq\", label: \"Groq\", apiBase: \"https://api.groq.com/openai/v1\", placeholder: \"\" },\n { value: \"deepseek\", label: \"DeepSeek\", apiBase: \"https://api.deepseek.com/v1\", placeholder: \"\" },\n { value: \"mistral\", label: \"Mistral AI\", apiBase: \"https://api.mistral.ai/v1\", placeholder: \"\" },\n { value: \"xai\", label: \"xAI (Grok)\", apiBase: \"https://api.x.ai/v1\", placeholder: \"\" },\n { value: \"minimax_native\", label: \"MiniMax (Native)\", apiBase: \"https://api.minimax.io/v1\", placeholder: \"\" },\n { value: \"cohere\", label: \"Cohere\", apiBase: \"https://api.cohere.ai/compatibility/v1\", placeholder: \"\" },\n { value: \"perplexity\", label: \"Perplexity\", apiBase: \"https://api.perplexity.ai\", placeholder: \"\" },\n { value: \"dashscope\", label: \"DashScope (Qwen)\", apiBase: \"https://dashscope-intl.aliyuncs.com/compatible-mode/v1\", placeholder: \"\" },\n { value: \"bailian\", label: \"Bailian Coding\", apiBase: \"https://coding-intl.dashscope.aliyuncs.com/v1\", placeholder: \"\" },\n { value: \"yescale\", label: \"YesScale\", apiBase: \"https://api.yescale.one/v1\", placeholder: \"\" },\n { value: \"zai\", label: \"Z.ai API\", apiBase: \"https://api.z.ai/api/paas/v4\", placeholder: \"\" },\n { value: \"zai_coding\", label: \"Z.ai Coding Plan\", apiBase: \"https://api.z.ai/api/coding/paas/v4\", placeholder: \"\" },\n { value: \"ollama\", label: \"Ollama (Local)\", apiBase: \"http://localhost:11434/v1\", placeholder: \"\" },\n { value: \"ollama_cloud\", label: \"Ollama Cloud\", apiBase: \"https://ollama.com/v1\", placeholder: \"\" },\n { value: \"claude_cli\", label: \"Claude CLI (Local)\", apiBase: \"\", placeholder: \"\" },\n { value: \"acp\", label: \"ACP Agent (Subprocess)\", apiBase: \"\", placeholder: \"claude\" },\n];\n\nfunction providerAliasName(value: ProviderAliasSource): string {\n if (typeof value === \"string\") return value.trim().toLowerCase();\n return value.name?.trim().toLowerCase() ?? \"\";\n}\n\nexport function suggestUniqueProviderAlias(\n existingProviders: ProviderAliasSource[],\n options?: { baseAlias?: string; excludeName?: string },\n): string {\n const baseAlias = slugify(options?.baseAlias ?? DEFAULT_CODEX_OAUTH_ALIAS) || DEFAULT_CODEX_OAUTH_ALIAS;\n const taken = new Set(existingProviders.map(providerAliasName).filter(Boolean));\n const excludeName = providerAliasName(options?.excludeName ?? \"\");\n\n if (excludeName) taken.delete(excludeName);\n if (!taken.has(baseAlias)) return baseAlias;\n\n let suffix = 2;\n while (taken.has(`${baseAlias}-${suffix}`)) {\n suffix += 1;\n }\n return `${baseAlias}-${suffix}`;\n}\n"}, "5e2fa395c7d6e5b77539d43a0ce00ef17f3cfff8": {"t": 1785759232.2120225, "n": 2412, "h": "49ad13521bf57255", "c": "export interface ProviderTypeInfo {\n value: string;\n label: string;\n apiBase: string;\n placeholder: string;\n}\n\nexport const PROVIDER_TYPES: ProviderTypeInfo[] = [\n { value: \"chatgpt_oauth\", label: \"ChatGPT Subscription (OAuth)\", apiBase: \"\", placeholder: \"\" },\n { value: \"anthropic_native\", label: \"Anthropic (Native)\", apiBase: \"\", placeholder: \"https://api.anthropic.com\" },\n { value: \"openai_compat\", label: \"OpenAI Compatible\", apiBase: \"\", placeholder: \"https://api.openai.com/v1\" },\n { value: \"gemini_native\", label: \"Google Gemini\", apiBase: \"https://generativelanguage.googleapis.com/v1beta/openai\", placeholder: \"\" },\n { value: \"openrouter\", label: \"OpenRouter\", apiBase: \"https://openrouter.ai/api/v1\", placeholder: \"\" },\n { value: \"groq\", label: \"Groq\", apiBase: \"https://api.groq.com/openai/v1\", placeholder: \"\" },\n { value: \"deepseek\", label: \"DeepSeek\", apiBase: \"https://api.deepseek.com/v1\", placeholder: \"\" },\n { value: \"mistral\", label: \"Mistral AI\", apiBase: \"https://api.mistral.ai/v1\", placeholder: \"\" },\n { value: \"xai\", label: \"xAI (Grok)\", apiBase: \"https://api.x.ai/v1\", placeholder: \"\" },\n { value: \"minimax_native\", label: \"MiniMax (Native)\", apiBase: \"https://api.minimax.io/v1\", placeholder: \"\" },\n { value: \"cohere\", label: \"Cohere\", apiBase: \"https://api.cohere.ai/compatibility/v1\", placeholder: \"\" },\n { value: \"perplexity\", label: \"Perplexity\", apiBase: \"https://api.perplexity.ai\", placeholder: \"\" },\n { value: \"dashscope\", label: \"DashScope (Qwen)\", apiBase: \"https://dashscope-intl.aliyuncs.com/compatible-mode/v1\", placeholder: \"\" },\n { value: \"bailian\", label: \"Bailian Coding\", apiBase: \"https://coding-intl.dashscope.aliyuncs.com/v1\", placeholder: \"\" },\n { value: \"yescale\", label: \"YesScale\", apiBase: \"https://api.yescale.one/v1\", placeholder: \"\" },\n { value: \"zai\", label: \"Z.ai API\", apiBase: \"https://api.z.ai/api/paas/v4\", placeholder: \"\" },\n { value: \"zai_coding\", label: \"Z.ai Coding Plan\", apiBase: \"https://api.z.ai/api/coding/paas/v4\", placeholder: \"\" },\n { value: \"ollama\", label: \"Ollama (Local)\", apiBase: \"http://localhost:11434/v1\", placeholder: \"\" },\n { value: \"ollama_cloud\", label: \"Ollama Cloud\", apiBase: \"https://ollama.com/v1\", placeholder: \"\" },\n { value: \"claude_cli\", label: \"Claude CLI (Local)\", apiBase: \"\", placeholder: \"\" },\n { value: \"acp\", label: \"ACP Agent (Subprocess)\", apiBase: \"\", placeholder: \"claude\" },\n];\n"}, "d874266e87b7147d1dada2f49c15cf29f77a4514": {"t": 1785759234.025297, "n": 2323, "h": "b8a0f8827bbbb7f9", "c": "export interface ProviderTypeInfo {\n value: string;\n label: string;\n apiBase: string;\n placeholder: string;\n}\n\nexport const PROVIDER_TYPES: ProviderTypeInfo[] = [\n { value: \"chatgpt_oauth\", label: \"ChatGPT Subscription (OAuth)\", apiBase: \"\", placeholder: \"\" },\n { value: \"anthropic_native\", label: \"Anthropic (Native)\", apiBase: \"\", placeholder: \"https://api.anthropic.com\" },\n { value: \"openai_compat\", label: \"OpenAI Compatible\", apiBase: \"\", placeholder: \"https://api.openai.com/v1\" },\n { value: \"gemini_native\", label: \"Google Gemini\", apiBase: \"https://generativelanguage.googleapis.com/v1beta/openai\", placeholder: \"\" },\n { value: \"openrouter\", label: \"OpenRouter\", apiBase: \"https://openrouter.ai/api/v1\", placeholder: \"\" },\n { value: \"groq\", label: \"Groq\", apiBase: \"https://api.groq.com/openai/v1\", placeholder: \"\" },\n { value: \"deepseek\", label: \"DeepSeek\", apiBase: \"https://api.deepseek.com/v1\", placeholder: \"\" },\n { value: \"mistral\", label: \"Mistral AI\", apiBase: \"https://api.mistral.ai/v1\", placeholder: \"\" },\n { value: \"xai\", label: \"xAI (Grok)\", apiBase: \"https://api.x.ai/v1\", placeholder: \"\" },\n { value: \"minimax_native\", label: \"MiniMax (Native)\", apiBase: \"https://api.minimax.io/v1\", placeholder: \"\" },\n { value: \"cohere\", label: \"Cohere\", apiBase: \"https://api.cohere.ai/compatibility/v1\", placeholder: \"\" },\n { value: \"perplexity\", label: \"Perplexity\", apiBase: \"https://api.perplexity.ai\", placeholder: \"\" },\n { value: \"dashscope\", label: \"DashScope (Qwen)\", apiBase: \"https://dashscope-intl.aliyuncs.com/compatible-mode/v1\", placeholder: \"\" },\n { value: \"bailian\", label: \"Bailian Coding\", apiBase: \"https://coding-intl.dashscope.aliyuncs.com/v1\", placeholder: \"\" },\n { value: \"yescale\", label: \"YesScale\", apiBase: \"https://api.yescale.one/v1\", placeholder: \"\" },\n { value: \"zai\", label: \"Z.ai API\", apiBase: \"https://api.z.ai/api/paas/v4\", placeholder: \"\" },\n { value: \"zai_coding\", label: \"Z.ai Coding Plan\", apiBase: \"https://api.z.ai/api/coding/paas/v4\", placeholder: \"\" },\n { value: \"ollama\", label: \"Ollama (Local)\", apiBase: \"http://localhost:11434/v1\", placeholder: \"\" },\n { value: \"ollama_cloud\", label: \"Ollama Cloud\", apiBase: \"https://ollama.com/v1\", placeholder: \"\" },\n { value: \"claude_cli\", label: \"Claude CLI (Local)\", apiBase: \"\", placeholder: \"\" },\n];\n"}, "e63ff014cbaa5d1fa2db1022ccf53017d4fd1906": {"t": 1785759235.642726, "n": 2117, "h": "7d4a48cc2b9c162b", "c": "export interface ProviderTypeInfo {\n value: string;\n label: string;\n apiBase: string;\n placeholder: string;\n}\n\nexport const PROVIDER_TYPES: ProviderTypeInfo[] = [\n { value: \"chatgpt_oauth\", label: \"ChatGPT Subscription (OAuth)\", apiBase: \"\", placeholder: \"\" },\n { value: \"anthropic_native\", label: \"Anthropic (Native)\", apiBase: \"\", placeholder: \"https://api.anthropic.com\" },\n { value: \"openai_compat\", label: \"OpenAI Compatible\", apiBase: \"\", placeholder: \"https://api.openai.com/v1\" },\n { value: \"gemini_native\", label: \"Google Gemini\", apiBase: \"https://generativelanguage.googleapis.com/v1beta/openai\", placeholder: \"\" },\n { value: \"openrouter\", label: \"OpenRouter\", apiBase: \"https://openrouter.ai/api/v1\", placeholder: \"\" },\n { value: \"groq\", label: \"Groq\", apiBase: \"https://api.groq.com/openai/v1\", placeholder: \"\" },\n { value: \"deepseek\", label: \"DeepSeek\", apiBase: \"https://api.deepseek.com/v1\", placeholder: \"\" },\n { value: \"mistral\", label: \"Mistral AI\", apiBase: \"https://api.mistral.ai/v1\", placeholder: \"\" },\n { value: \"xai\", label: \"xAI (Grok)\", apiBase: \"https://api.x.ai/v1\", placeholder: \"\" },\n { value: \"minimax_native\", label: \"MiniMax (Native)\", apiBase: \"https://api.minimax.io/v1\", placeholder: \"\" },\n { value: \"cohere\", label: \"Cohere\", apiBase: \"https://api.cohere.ai/compatibility/v1\", placeholder: \"\" },\n { value: \"perplexity\", label: \"Perplexity\", apiBase: \"https://api.perplexity.ai\", placeholder: \"\" },\n { value: \"dashscope\", label: \"DashScope (Qwen)\", apiBase: \"https://dashscope-intl.aliyuncs.com/compatible-mode/v1\", placeholder: \"\" },\n { value: \"bailian\", label: \"Bailian Coding\", apiBase: \"https://coding-intl.dashscope.aliyuncs.com/v1\", placeholder: \"\" },\n { value: \"yescale\", label: \"YesScale\", apiBase: \"https://api.yescale.one/v1\", placeholder: \"\" },\n { value: \"zai\", label: \"Z.ai API\", apiBase: \"https://api.z.ai/api/paas/v4\", placeholder: \"\" },\n { value: \"zai_coding\", label: \"Z.ai Coding Plan\", apiBase: \"https://api.z.ai/api/coding/paas/v4\", placeholder: \"\" },\n { value: \"claude_cli\", label: \"Claude CLI (Local)\", apiBase: \"\", placeholder: \"\" },\n];\n"}, "d839e034af7f40efd5f496f100dd84c2326c38d0": {"t": 1785759237.173401, "n": 1901, "h": "4806c64c97c0c622", "c": "export interface ProviderTypeInfo {\n value: string;\n label: string;\n apiBase: string;\n placeholder: string;\n}\n\nexport const PROVIDER_TYPES: ProviderTypeInfo[] = [\n { value: \"chatgpt_oauth\", label: \"ChatGPT Subscription (OAuth)\", apiBase: \"\", placeholder: \"\" },\n { value: \"anthropic_native\", label: \"Anthropic (Native)\", apiBase: \"\", placeholder: \"https://api.anthropic.com\" },\n { value: \"openai_compat\", label: \"OpenAI Compatible\", apiBase: \"\", placeholder: \"https://api.openai.com/v1\" },\n { value: \"gemini_native\", label: \"Google Gemini\", apiBase: \"https://generativelanguage.googleapis.com/v1beta/openai\", placeholder: \"\" },\n { value: \"openrouter\", label: \"OpenRouter\", apiBase: \"https://openrouter.ai/api/v1\", placeholder: \"\" },\n { value: \"groq\", label: \"Groq\", apiBase: \"https://api.groq.com/openai/v1\", placeholder: \"\" },\n { value: \"deepseek\", label: \"DeepSeek\", apiBase: \"https://api.deepseek.com/v1\", placeholder: \"\" },\n { value: \"mistral\", label: \"Mistral AI\", apiBase: \"https://api.mistral.ai/v1\", placeholder: \"\" },\n { value: \"xai\", label: \"xAI (Grok)\", apiBase: \"https://api.x.ai/v1\", placeholder: \"\" },\n { value: \"minimax_native\", label: \"MiniMax (Native)\", apiBase: \"https://api.minimax.io/v1\", placeholder: \"\" },\n { value: \"cohere\", label: \"Cohere\", apiBase: \"https://api.cohere.ai/compatibility/v1\", placeholder: \"\" },\n { value: \"perplexity\", label: \"Perplexity\", apiBase: \"https://api.perplexity.ai\", placeholder: \"\" },\n { value: \"dashscope\", label: \"DashScope (Qwen)\", apiBase: \"https://dashscope-intl.aliyuncs.com/compatible-mode/v1\", placeholder: \"\" },\n { value: \"bailian\", label: \"Bailian Coding\", apiBase: \"https://coding-intl.dashscope.aliyuncs.com/v1\", placeholder: \"\" },\n { value: \"yescale\", label: \"YesScale\", apiBase: \"https://api.yescale.one/v1\", placeholder: \"\" },\n { value: \"claude_cli\", label: \"Claude CLI (Local)\", apiBase: \"\", placeholder: \"\" },\n];\n"}, "b2c4d543aab10b4d40df669fd9bf94ee3e723049": {"t": 1785759238.565978, "n": 1802, "h": "9a4687f18aab3d44", "c": "export interface ProviderTypeInfo {\n value: string;\n label: string;\n apiBase: string;\n placeholder: string;\n}\n\nexport const PROVIDER_TYPES: ProviderTypeInfo[] = [\n { value: \"chatgpt_oauth\", label: \"ChatGPT Subscription (OAuth)\", apiBase: \"\", placeholder: \"\" },\n { value: \"anthropic_native\", label: \"Anthropic (Native)\", apiBase: \"\", placeholder: \"https://api.anthropic.com\" },\n { value: \"openai_compat\", label: \"OpenAI Compatible\", apiBase: \"\", placeholder: \"https://api.openai.com/v1\" },\n { value: \"gemini_native\", label: \"Google Gemini\", apiBase: \"https://generativelanguage.googleapis.com/v1beta/openai\", placeholder: \"\" },\n { value: \"openrouter\", label: \"OpenRouter\", apiBase: \"https://openrouter.ai/api/v1\", placeholder: \"\" },\n { value: \"groq\", label: \"Groq\", apiBase: \"https://api.groq.com/openai/v1\", placeholder: \"\" },\n { value: \"deepseek\", label: \"DeepSeek\", apiBase: \"https://api.deepseek.com/v1\", placeholder: \"\" },\n { value: \"mistral\", label: \"Mistral AI\", apiBase: \"https://api.mistral.ai/v1\", placeholder: \"\" },\n { value: \"xai\", label: \"xAI (Grok)\", apiBase: \"https://api.x.ai/v1\", placeholder: \"\" },\n { value: \"minimax_native\", label: \"MiniMax (Native)\", apiBase: \"https://api.minimax.io/v1\", placeholder: \"\" },\n { value: \"cohere\", label: \"Cohere\", apiBase: \"https://api.cohere.ai/compatibility/v1\", placeholder: \"\" },\n { value: \"perplexity\", label: \"Perplexity\", apiBase: \"https://api.perplexity.ai\", placeholder: \"\" },\n { value: \"dashscope\", label: \"DashScope (Qwen)\", apiBase: \"https://dashscope-intl.aliyuncs.com/compatible-mode/v1\", placeholder: \"\" },\n { value: \"bailian\", label: \"Bailian Coding\", apiBase: \"https://coding-intl.dashscope.aliyuncs.com/v1\", placeholder: \"\" },\n { value: \"claude_cli\", label: \"Claude CLI (Local)\", apiBase: \"\", placeholder: \"\" },\n];\n"}, "7d744eb4f29173dd1b333b8ae525f5db669a0d5a": {"t": 1785759239.9369738, "n": 1716, "h": "80068a293bc2daeb", "c": "export interface ProviderTypeInfo {\n value: string;\n label: string;\n apiBase: string;\n placeholder: string;\n}\n\nexport const PROVIDER_TYPES: ProviderTypeInfo[] = [\n { value: \"chatgpt_oauth\", label: \"ChatGPT Subscription (OAuth)\", apiBase: \"\", placeholder: \"\" },\n { value: \"anthropic_native\", label: \"Anthropic (Native)\", apiBase: \"\", placeholder: \"https://api.anthropic.com\" },\n { value: \"openai_compat\", label: \"OpenAI Compatible\", apiBase: \"\", placeholder: \"https://api.openai.com/v1\" },\n { value: \"gemini_native\", label: \"Google Gemini\", apiBase: \"https://generativelanguage.googleapis.com/v1beta/openai\", placeholder: \"\" },\n { value: \"openrouter\", label: \"OpenRouter\", apiBase: \"https://openrouter.ai/api/v1\", placeholder: \"\" },\n { value: \"groq\", label: \"Groq\", apiBase: \"https://api.groq.com/openai/v1\", placeholder: \"\" },\n { value: \"deepseek\", label: \"DeepSeek\", apiBase: \"https://api.deepseek.com/v1\", placeholder: \"\" },\n { value: \"mistral\", label: \"Mistral AI\", apiBase: \"https://api.mistral.ai/v1\", placeholder: \"\" },\n { value: \"xai\", label: \"xAI (Grok)\", apiBase: \"https://api.x.ai/v1\", placeholder: \"\" },\n { value: \"minimax_native\", label: \"MiniMax (Native)\", apiBase: \"https://api.minimax.io/v1\", placeholder: \"\" },\n { value: \"cohere\", label: \"Cohere\", apiBase: \"https://api.cohere.ai/compatibility/v1\", placeholder: \"\" },\n { value: \"perplexity\", label: \"Perplexity\", apiBase: \"https://api.perplexity.ai\", placeholder: \"\" },\n { value: \"dashscope\", label: \"DashScope (Qwen)\", apiBase: \"https://dashscope-intl.aliyuncs.com/compatible-mode/v1\", placeholder: \"\" },\n { value: \"bailian\", label: \"Bailian Coding\", apiBase: \"https://coding-intl.dashscope.aliyuncs.com/v1\", placeholder: \"\" },\n];\n"}, "6895e369f6e1df4a186dd7e0016149963e4116a9": {"t": 1785759241.4755626, "n": 1617, "h": "b11bd46bbb9da1a4", "c": "export interface ProviderTypeInfo {\n value: string;\n label: string;\n apiBase: string;\n placeholder: string;\n}\n\nexport const PROVIDER_TYPES: ProviderTypeInfo[] = [\n { value: \"anthropic_native\", label: \"Anthropic (Native)\", apiBase: \"\", placeholder: \"https://api.anthropic.com\" },\n { value: \"openai_compat\", label: \"OpenAI Compatible\", apiBase: \"\", placeholder: \"https://api.openai.com/v1\" },\n { value: \"gemini_native\", label: \"Google Gemini\", apiBase: \"https://generativelanguage.googleapis.com/v1beta/openai\", placeholder: \"\" },\n { value: \"openrouter\", label: \"OpenRouter\", apiBase: \"https://openrouter.ai/api/v1\", placeholder: \"\" },\n { value: \"groq\", label: \"Groq\", apiBase: \"https://api.groq.com/openai/v1\", placeholder: \"\" },\n { value: \"deepseek\", label: \"DeepSeek\", apiBase: \"https://api.deepseek.com/v1\", placeholder: \"\" },\n { value: \"mistral\", label: \"Mistral AI\", apiBase: \"https://api.mistral.ai/v1\", placeholder: \"\" },\n { value: \"xai\", label: \"xAI (Grok)\", apiBase: \"https://api.x.ai/v1\", placeholder: \"\" },\n { value: \"minimax_native\", label: \"MiniMax (Native)\", apiBase: \"https://api.minimax.io/v1\", placeholder: \"\" },\n { value: \"cohere\", label: \"Cohere\", apiBase: \"https://api.cohere.ai/compatibility/v1\", placeholder: \"\" },\n { value: \"perplexity\", label: \"Perplexity\", apiBase: \"https://api.perplexity.ai\", placeholder: \"\" },\n { value: \"dashscope\", label: \"DashScope (Qwen)\", apiBase: \"https://dashscope-intl.aliyuncs.com/compatible-mode/v1\", placeholder: \"\" },\n { value: \"bailian\", label: \"Bailian Coding\", apiBase: \"https://coding-intl.dashscope.aliyuncs.com/v1\", placeholder: \"\" },\n];\n"}}, "internal/config/config_channels.go": {"34d5625c2a00cb343824577f39c03b4cdc414182": {"t": 1785747445.9969964, "n": 39871, "h": "b5977e40c606434c", "c": "package config\n\n// ChatBehaviorConfig controls optional human-like channel delivery behavior.\n// Pointer fields allow per-channel overrides to inherit gateway defaults.\ntype ChatBehaviorConfig struct {\n\tEnabled *bool `json:\"enabled,omitempty\"`\n\tIntermediateReplies *IntermediateRepliesConfig `json:\"intermediate_replies,omitempty\"`\n\tQuickAck *QuickAckConfig `json:\"quick_ack,omitempty\"`\n\tFinalSplit *FinalSplitConfig `json:\"final_split,omitempty\"`\n}\n\n// QuickAckConfig controls one short acknowledgement before longer non-streaming runs.\ntype QuickAckConfig struct {\n\tEnabled *bool `json:\"enabled,omitempty\"`\n\tMode *string `json:\"mode,omitempty\"`\n\tMinDelayMs *int `json:\"min_delay_ms,omitempty\"`\n\tProvider string `json:\"provider,omitempty\"`\n\tModel string `json:\"model,omitempty\"`\n\tTimeoutMs *int `json:\"timeout_ms,omitempty\"`\n\tMaxTokens *int `json:\"max_tokens,omitempty\"`\n\tMaxChars *int `json:\"max_chars,omitempty\"`\n\tTemplates []string `json:\"templates,omitempty\"`\n}\n\n// IntermediateRepliesConfig controls delivery-only progress messages during tool phases.\ntype IntermediateRepliesConfig struct {\n\tEnabled *bool `json:\"enabled,omitempty\"`\n\tMode *string `json:\"mode,omitempty\"`\n\tProvider string `json:\"provider,omitempty\"`\n\tModel string `json:\"model,omitempty\"`\n\tTimeoutMs *int `json:\"timeout_ms,omitempty\"`\n\tMaxTokens *int `json:\"max_tokens,omitempty\"`\n\tMaxChars *int `json:\"max_chars,omitempty\"`\n}\n\n// FinalSplitConfig controls semantic splitting of final channel replies.\ntype FinalSplitConfig struct {\n\tEnabled *bool `json:\"enabled,omitempty\"`\n\tMinChars *int `json:\"min_chars,omitempty\"`\n\tMaxMessages *int `json:\"max_messages,omitempty\"`\n\tDelayMs *int `json:\"delay_ms,omitempty\"`\n}\n\n// PendingCompactionConfig configures LLM-based compaction of pending group messages.\n// When a group accumulates more than Threshold pending messages, older messages are\n// summarized by an LLM and replaced with a compact summary, keeping KeepRecent raw messages.\ntype PendingCompactionConfig struct {\n\tThreshold int `json:\"threshold,omitempty\"` // trigger compaction when entries exceed this (default 200)\n\tKeepRecent int `json:\"keep_recent,omitempty\"` // keep this many recent raw messages after compaction (default 40)\n\tMaxTokens int `json:\"max_tokens,omitempty\"` // max output tokens for LLM summarization (default 4096)\n\tProvider string `json:\"provider,omitempty\"` // LLM provider name (e.g. \"openai\"); empty = use agent's provider\n\tModel string `json:\"model,omitempty\"` // model for summarization; empty = use agent's model\n}\n\n// ChannelsConfig contains per-channel configuration.\ntype ChannelsConfig struct {\n\tTelegram TelegramConfig `json:\"telegram\"`\n\tDiscord DiscordConfig `json:\"discord\"`\n\tSlack SlackConfig `json:\"slack\"`\n\tWhatsApp WhatsAppConfig `json:\"whatsapp\"`\n\tZalo ZaloConfig `json:\"zalo\"`\n\tZaloPersonal ZaloPersonalConfig `json:\"zalo_personal\"`\n\tFeishu FeishuConfig `json:\"feishu\"`\n\tPendingCompaction *PendingCompactionConfig `json:\"pending_compaction,omitempty\"` // global pending message compaction settings\n}\n\ntype TelegramConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tToken string `json:\"token\"`\n\tProxy string `json:\"proxy,omitempty\"`\n\tAPIServer string `json:\"api_server,omitempty\"` // custom Telegram Bot API server URL (e.g. \"http://localhost:8081\")\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in groups (default true)\n\tMentionMode string `json:\"mention_mode,omitempty\"` // \"strict\" (default) = only respond when mentioned; \"yield\" = respond unless another bot is mentioned\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tDMStream *bool `json:\"dm_stream,omitempty\"` // enable streaming for DMs (default false) — edits placeholder progressively\n\tGroupStream *bool `json:\"group_stream,omitempty\"` // enable streaming for groups (default false) — sends new message, edits progressively\n\tDraftTransport *bool `json:\"draft_transport,omitempty\"` // use sendMessageDraft for DM streaming (default true) — stealth preview, no notifications per edit\n\tReasoningDelivery string `json:\"reasoning_delivery,omitempty\"` // off, streaming_only, always_bubbles\n\tReasoningStream *bool `json:\"reasoning_stream,omitempty\"` // show reasoning as separate message when provider emits thinking events (default true)\n\tReactionLevel string `json:\"reaction_level,omitempty\"` // \"off\" (default), \"minimal\", \"full\" — status emoji reactions\n\tMediaMaxBytes int64 `json:\"media_max_bytes,omitempty\"` // max media download size in bytes (default 20MB)\n\tLinkPreview *bool `json:\"link_preview,omitempty\"` // enable URL previews in messages (default true)\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tChatBehavior *ChatBehaviorConfig `json:\"chat_behavior,omitempty\"` // override gateway chat behavior (nil = inherit)\n\tForceIPv4 bool `json:\"force_ipv4,omitempty\"` // force IPv4 for all Telegram API requests (use when IPv6 routing is broken)\n\n\t// Optional STT (Speech-to-Text) pipeline for voice/audio inbound messages.\n\t// When stt_proxy_url is set, audio/voice messages are transcribed before being forwarded to the agent.\n\tSTTProxyURL string `json:\"stt_proxy_url,omitempty\"` // base URL of the STT proxy service (e.g. \"https://stt.example.com\")\n\tSTTAPIKey string `json:\"stt_api_key,omitempty\"` // Bearer token for the STT proxy\n\tSTTTenantID string `json:\"stt_tenant_id,omitempty\"` // optional tenant/org identifier forwarded to the STT proxy\n\tSTTTimeoutSeconds int `json:\"stt_timeout_seconds,omitempty\"` // per-request timeout for STT calls (default 30s)\n\n\t// Optional audio-aware routing: when set, voice/audio inbound messages are routed to this\n\t// agent instead of the default channel agent. Requires the named agent to exist in the config.\n\tVoiceAgentID string `json:\"voice_agent_id,omitempty\"` // agent ID to route voice inbound to (e.g. \"speaking-agent\")\n\n\t// Audio guard: intercept technical errors in voice agent replies and replace with friendly fallbacks.\n\t// Only active when VoiceAgentID is set. Custom error markers replace built-in defaults when provided.\n\tAudioGuardFallbackTranscript string `json:\"audio_guard_fallback_transcript,omitempty\"` // fallback with %s for transcript (e.g. \"I heard: \\\"%s\\\". Try again!\")\n\tAudioGuardFallbackNoTranscript string `json:\"audio_guard_fallback_no_transcript,omitempty\"` // fallback when no transcript available\n\tAudioGuardErrorMarkers []string `json:\"audio_guard_error_markers,omitempty\"` // custom error detection markers (replaces defaults)\n\n\t// Per-group (and per-topic) overrides. Key is chat ID string (e.g. \"-100123456\") or \"*\" for wildcard.\n\t// TS ref: channels.telegram.groups in src/config/types.telegram.ts.\n\tGroups map[string]*TelegramGroupConfig `json:\"groups,omitempty\"`\n}\n\n// TelegramGroupConfig defines per-group overrides for a Telegram channel.\n// Matching TS TelegramGroupConfig in src/config/types.telegram.ts.\ntype TelegramGroupConfig struct {\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // override group policy for this group\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // override require_mention for this group\n\tMentionMode string `json:\"mention_mode,omitempty\"` // override mention_mode for this group\n\tAllowFrom FlexibleStringSlice `json:\"allow_from,omitempty\"` // override allow_from for this group\n\tEnabled *bool `json:\"enabled,omitempty\"` // disable bot for this group (default: true)\n\tSkills []string `json:\"skills,omitempty\"` // skill whitelist (nil = all, [] = none)\n\tTools []string `json:\"tools,omitempty\"` // tool allow list (nil = all, supports \"group:xxx\")\n\tSystemPrompt string `json:\"system_prompt,omitempty\"` // extra system prompt for this group\n\tTopics map[string]*TelegramTopicConfig `json:\"topics,omitempty\"` // per-topic overrides (key: thread ID string)\n\tQuota *QuotaWindow `json:\"quota,omitempty\"` // per-group quota override\n}\n\n// TelegramTopicConfig defines per-topic overrides within a Telegram group.\n// Matching TS TelegramTopicConfig in src/config/types.telegram.ts.\ntype TelegramTopicConfig struct {\n\tRequireMention *bool `json:\"require_mention,omitempty\"`\n\tMentionMode string `json:\"mention_mode,omitempty\"`\n\tGroupPolicy string `json:\"group_policy,omitempty\"`\n\tSkills []string `json:\"skills,omitempty\"`\n\tTools []string `json:\"tools,omitempty\"` // tool allow list (nil = inherit, supports \"group:xxx\")\n\tEnabled *bool `json:\"enabled,omitempty\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from,omitempty\"`\n\tSystemPrompt string `json:\"system_prompt,omitempty\"`\n}\n\ntype DiscordConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tToken string `json:\"token\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in groups (default true)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tChatBehavior *ChatBehaviorConfig `json:\"chat_behavior,omitempty\"` // override gateway chat behavior (nil = inherit)\n\tMediaMaxBytes int64 `json:\"media_max_bytes,omitempty\"` // max media download size (default 25MB)\n\tSTTProxyURL string `json:\"stt_proxy_url,omitempty\"`\n\tSTTAPIKey string `json:\"stt_api_key,omitempty\"`\n\tSTTTenantID string `json:\"stt_tenant_id,omitempty\"`\n\tSTTTimeoutSeconds int `json:\"stt_timeout_seconds,omitempty\"`\n\tVoiceAgentID string `json:\"voice_agent_id,omitempty\"`\n}\n\ntype SlackConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tBotToken string `json:\"bot_token\"` // xoxb-... (Bot User OAuth Token)\n\tAppToken string `json:\"app_token\"` // xapp-... (App-Level Token for Socket Mode)\n\tUserToken string `json:\"user_token,omitempty\"` // xoxp-... (Optional: custom bot identity)\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"pairing\", \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in channels (default true)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tDMStream *bool `json:\"dm_stream,omitempty\"` // enable streaming for DMs (default false)\n\tGroupStream *bool `json:\"group_stream,omitempty\"` // enable streaming for groups (default false)\n\tNativeStream *bool `json:\"native_stream,omitempty\"` // use Slack ChatStreamer API if available (default false)\n\tReactionLevel string `json:\"reaction_level,omitempty\"` // \"off\" (default), \"minimal\", \"full\"\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tChatBehavior *ChatBehaviorConfig `json:\"chat_behavior,omitempty\"` // override gateway chat behavior (nil = inherit)\n\tDebounceDelay *int `json:\"debounce_delay,omitempty\"` // ms delay before dispatching rapid messages (default 300, 0=disabled)\n\tThreadTTL *int `json:\"thread_ttl,omitempty\"` // hours before thread participation expires (default 24, 0=disabled — always require @mention)\n\tMediaMaxBytes int64 `json:\"media_max_bytes,omitempty\"` // max file download size in bytes (default 20MB)\n}\n\ntype WhatsAppConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tAuthDir string `json:\"auth_dir,omitempty\"` // optional: SQLite auth dir override (desktop)\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default for DB instances), \"open\", \"allowlist\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"pairing\" (default for DB instances), \"open\" (default for config), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // only respond in groups when bot is @mentioned (default false)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 200, 0=disabled)\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tChatBehavior *ChatBehaviorConfig `json:\"chat_behavior,omitempty\"` // override gateway chat behavior (nil = inherit)\n}\n\ntype ZaloConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tToken string `json:\"token\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tWebhookURL string `json:\"webhook_url,omitempty\"`\n\tWebhookSecret string `json:\"webhook_secret,omitempty\"`\n\tMediaMaxMB int `json:\"media_max_mb,omitempty\"` // default 5\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tChatBehavior *ChatBehaviorConfig `json:\"chat_behavior,omitempty\"` // override gateway chat behavior (nil = inherit)\n}\n\ntype ZaloPersonalConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in groups (default true)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tCredentialsPath string `json:\"credentials_path,omitempty\"` // path to saved cookies JSON\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tChatBehavior *ChatBehaviorConfig `json:\"chat_behavior,omitempty\"` // override gateway chat behavior (nil = inherit)\n}\n\ntype FeishuConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tAppID string `json:\"app_id\"`\n\tAppSecret string `json:\"app_secret\"`\n\tEncryptKey string `json:\"encrypt_key,omitempty\"`\n\tVerificationToken string `json:\"verification_token,omitempty\"`\n\tDomain string `json:\"domain,omitempty\"` // \"lark\" (default/global), \"feishu\" (China), or custom URL\n\tConnectionMode string `json:\"connection_mode,omitempty\"` // \"websocket\" (default), \"webhook\"\n\tWebhookPort int `json:\"webhook_port,omitempty\"` // default 3000\n\tWebhookPath string `json:\"webhook_path,omitempty\"` // default \"/feishu/events\"\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default)\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default)\n\tGroupAllowFrom FlexibleStringSlice `json:\"group_allow_from,omitempty\"`\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // default true (groups)\n\tTopicSessionMode string `json:\"topic_session_mode,omitempty\"` // \"disabled\" (default)\n\tTextChunkLimit int `json:\"text_chunk_limit,omitempty\"` // default 4000\n\tMediaMaxMB int `json:\"media_max_mb,omitempty\"` // default 30\n\tRenderMode string `json:\"render_mode,omitempty\"` // \"auto\", \"raw\", \"card\"\n\tStreaming *bool `json:\"streaming,omitempty\"` // default true\n\tReactionLevel string `json:\"reaction_level,omitempty\"` // \"off\" (default), \"minimal\", \"full\" — typing emoji reactions\n\tHistoryLimit int `json:\"history_limit,omitempty\"`\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tChatBehavior *ChatBehaviorConfig `json:\"chat_behavior,omitempty\"` // override gateway chat behavior (nil = inherit)\n\tSTTProxyURL string `json:\"stt_proxy_url,omitempty\"`\n\tSTTAPIKey string `json:\"stt_api_key,omitempty\"`\n\tSTTTenantID string `json:\"stt_tenant_id,omitempty\"`\n\tSTTTimeoutSeconds int `json:\"stt_timeout_seconds,omitempty\"`\n\tVoiceAgentID string `json:\"voice_agent_id,omitempty\"`\n}\n\n// ProvidersConfig maps provider name to its config.\ntype ProvidersConfig struct {\n\tAnthropic ProviderConfig `json:\"anthropic\"`\n\tOpenAI ProviderConfig `json:\"openai\"`\n\tOpenRouter ProviderConfig `json:\"openrouter\"`\n\tGroq ProviderConfig `json:\"groq\"`\n\tGemini ProviderConfig `json:\"gemini\"`\n\tDeepSeek ProviderConfig `json:\"deepseek\"`\n\tMistral ProviderConfig `json:\"mistral\"`\n\tXAI ProviderConfig `json:\"xai\"`\n\tMiniMax ProviderConfig `json:\"minimax\"`\n\tCohere ProviderConfig `json:\"cohere\"`\n\tPerplexity ProviderConfig `json:\"perplexity\"`\n\tDashScope ProviderConfig `json:\"dashscope\"`\n\tBailian ProviderConfig `json:\"bailian\"`\n\tZai ProviderConfig `json:\"zai\"`\n\tZaiCoding ProviderConfig `json:\"zai_coding\"`\n\tOllama OllamaConfig `json:\"ollama\"` // local Ollama instance (no API key needed)\n\tOllamaCloud ProviderConfig `json:\"ollama_cloud\"` // Ollama Cloud (API key required)\n\tClaudeCLI ClaudeCLIConfig `json:\"claude_cli\"`\n\tACP ACPConfig `json:\"acp\"`\n\tNovita ProviderConfig `json:\"novita\"` // Novita AI (OpenAI-compatible endpoint)\n\tBytePlus ProviderConfig `json:\"byteplus\"` // BytePlus ModelArk (Seed 2.0)\n\tBytePlusCoding ProviderConfig `json:\"byteplus_coding\"` // BytePlus ModelArk Coding Plan\n\tVertex VertexConfig `json:\"vertex\"` // Google Cloud Vertex AI (OAuth2 service account + ADC)\n}\n\n// VertexConfig configures Google Cloud Vertex AI.\n// Credentials precedence: APIKey (inline JSON) > CredentialsFile (path) > ADC (both empty).\n// ProjectID and Region are required; Model optional (defaults to google/gemini-2.0-flash-001).\ntype VertexConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"` // service account JSON inline (secret — never persist in config.json)\n\tCredentialsFile string `json:\"credentials_file,omitempty\"` // path to service account JSON file\n\tProjectID string `json:\"project_id,omitempty\"`\n\tRegion string `json:\"region,omitempty\"`\n\tModel string `json:\"model,omitempty\"`\n}\n\n// OllamaConfig configures a local (or self-hosted) Ollama instance.\n// No API key is required — Ollama accepts any Bearer token value.\ntype OllamaConfig struct {\n\tHost string `json:\"host\"` // Ollama server base URL, e.g. http://localhost:11434\n}\n\n// ClaudeCLIConfig configures the Claude CLI provider (uses subscription, not API key).\ntype ClaudeCLIConfig struct {\n\tCLIPath string `json:\"cli_path\" yaml:\"cli_path\"` // path to claude binary (default: \"claude\")\n\tModel string `json:\"model\" yaml:\"model\"` // default model alias (default: \"sonnet\")\n\tBaseWorkDir string `json:\"base_work_dir\" yaml:\"base_work_dir\"` // base dir for agent workspaces\n\tPermMode string `json:\"perm_mode\" yaml:\"perm_mode\"` // permission mode (default: \"bypassPermissions\")\n}\n\n// ACPConfig configures the ACP (Agent Client Protocol) provider.\n// Orchestrates any ACP-compatible coding agent (Claude Code, Codex CLI, Gemini CLI) as a subprocess.\ntype ACPConfig struct {\n\tBinary string `json:\"binary\"` // agent binary name or path (e.g. \"claude\", \"codex\")\n\tArgs []string `json:\"args\"` // extra spawn args\n\tModel string `json:\"model\"` // default model/agent name\n\tWorkDir string `json:\"work_dir\"` // base workspace dir\n\tIdleTTL string `json:\"idle_ttl\"` // process idle TTL (e.g. \"5m\")\n\tPermMode string `json:\"perm_mode\"` // \"approve-all\" (default), \"approve-reads\", \"deny-all\"\n}\n\ntype ProviderConfig struct {\n\tAPIKey string `json:\"api_key\"`\n\tAPIBase string `json:\"api_base,omitempty\"`\n}\n\n// APIBaseForType returns the config-level api_base for a given provider type.\n// Used as a fallback when DB providers have no api_base set.\nfunc (p *ProvidersConfig) APIBaseForType(providerType string) string {\n\tswitch providerType {\n\tcase \"anthropic_native\":\n\t\treturn p.Anthropic.APIBase\n\tcase \"openai\", \"openai_compat\":\n\t\treturn p.OpenAI.APIBase\n\tcase \"openrouter\":\n\t\treturn p.OpenRouter.APIBase\n\tcase \"groq\":\n\t\treturn p.Groq.APIBase\n\tcase \"deepseek\":\n\t\treturn p.DeepSeek.APIBase\n\tcase \"gemini_native\":\n\t\treturn p.Gemini.APIBase\n\tcase \"mistral\":\n\t\treturn p.Mistral.APIBase\n\tcase \"xai\":\n\t\treturn p.XAI.APIBase\n\tcase \"minimax_native\":\n\t\treturn p.MiniMax.APIBase\n\tcase \"cohere\":\n\t\treturn p.Cohere.APIBase\n\tcase \"perplexity\":\n\t\treturn p.Perplexity.APIBase\n\tcase \"dashscope\":\n\t\treturn p.DashScope.APIBase\n\tcase \"bailian\":\n\t\treturn p.Bailian.APIBase\n\tcase \"zai\":\n\t\treturn p.Zai.APIBase\n\tcase \"zai_coding\":\n\t\treturn p.ZaiCoding.APIBase\n\tcase \"ollama_cloud\":\n\t\treturn p.OllamaCloud.APIBase\n\tcase \"novita\":\n\t\treturn p.Novita.APIBase\n\tcase \"byteplus\":\n\t\treturn p.BytePlus.APIBase\n\tcase \"byteplus_coding\":\n\t\treturn p.BytePlusCoding.APIBase\n\tcase \"vertex\":\n\t\t// Computed from project+region at registration time; no config-level static base.\n\t\treturn \"\"\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\n// HasAnyProvider returns true if at least one provider has an API key or CLI configured.\nfunc (c *Config) HasAnyProvider() bool {\n\tp := c.Providers\n\treturn p.Anthropic.APIKey != \"\" ||\n\t\tp.OpenAI.APIKey != \"\" ||\n\t\tp.OpenRouter.APIKey != \"\" ||\n\t\tp.Groq.APIKey != \"\" ||\n\t\tp.Gemini.APIKey != \"\" ||\n\t\tp.DeepSeek.APIKey != \"\" ||\n\t\tp.Mistral.APIKey != \"\" ||\n\t\tp.XAI.APIKey != \"\" ||\n\t\tp.MiniMax.APIKey != \"\" ||\n\t\tp.Cohere.APIKey != \"\" ||\n\t\tp.Perplexity.APIKey != \"\" ||\n\t\tp.DashScope.APIKey != \"\" ||\n\t\tp.Bailian.APIKey != \"\" ||\n\t\tp.Zai.APIKey != \"\" ||\n\t\tp.ZaiCoding.APIKey != \"\" ||\n\t\tp.Ollama.Host != \"\" ||\n\t\tp.OllamaCloud.APIKey != \"\" ||\n\t\tp.ClaudeCLI.CLIPath != \"\" ||\n\t\tp.ACP.Binary != \"\" ||\n\t\tp.Novita.APIKey != \"\" ||\n\t\tp.BytePlus.APIKey != \"\" ||\n\t\tp.BytePlusCoding.APIKey != \"\" ||\n\t\t(p.Vertex.ProjectID != \"\" && p.Vertex.Region != \"\")\n}\n\n// QuotaWindow defines request limits per time window. Zero means unlimited.\ntype QuotaWindow struct {\n\tHour int `json:\"hour,omitempty\"` // max requests per hour (0 = unlimited)\n\tDay int `json:\"day,omitempty\"` // max requests per day (0 = unlimited)\n\tWeek int `json:\"week,omitempty\"` // max requests per week (0 = unlimited)\n}\n\n// IsZero returns true if no limits are set.\nfunc (w QuotaWindow) IsZero() bool { return w.Hour == 0 && w.Day == 0 && w.Week == 0 }\n\n// QuotaConfig configures per-user/group request quotas.\n// Config merge priority: Groups > Channels > Providers > Default.\ntype QuotaConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tDefault QuotaWindow `json:\"default\"`\n\tProviders map[string]QuotaWindow `json:\"providers,omitempty\"` // key = provider name (e.g. \"anthropic\")\n\tChannels map[string]QuotaWindow `json:\"channels,omitempty\"` // key = channel name (e.g. \"telegram\")\n\tGroups map[string]QuotaWindow `json:\"groups,omitempty\"` // key = userID (e.g. \"group:telegram:-100123\")\n}\n\n// GatewayConfig controls the gateway server.\ntype GatewayConfig struct {\n\tHost string `json:\"host\"`\n\tPort int `json:\"port\"`\n\tToken string `json:\"token,omitempty\"` // bearer token for WS/HTTP auth\n\tOwnerIDs []string `json:\"owner_ids,omitempty\"` // sender IDs considered \"owner\"\n\tAllowedOrigins []string `json:\"allowed_origins,omitempty\"` // WebSocket CORS whitelist (empty = allow all)\n\tMaxMessageChars int `json:\"max_message_chars,omitempty\"` // max user message characters (default 32000)\n\tRateLimitRPM int `json:\"rate_limit_rpm,omitempty\"` // rate limit: requests per minute per user (default 20, 0 = disabled)\n\tInjectionAction string `json:\"injection_action,omitempty\"` // prompt injection action: \"log\", \"warn\" (default), \"block\", \"off\"\n\tInboundDebounceMs int `json:\"inbound_debounce_ms,omitempty\"` // silence-window in ms that merges rapid channel/Web Chat messages from the same sender/session; 0 disables for text but media-bearing messages still honor a built-in media floor so multi-attachment bursts (#63) coalesce into a single agent run. Agents may override via per-agent agent_config.inbound_debounce_ms.\n\tQuota *QuotaConfig `json:\"quota,omitempty\"` // per-user/group request quotas\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // deliver intermediate text during tool iterations (default false)\n\tChatBehavior *ChatBehaviorConfig `json:\"chat_behavior,omitempty\"` // human-like channel delivery behavior (default disabled)\n\tToolStatus *bool `json:\"tool_status,omitempty\"` // show tool name in streaming preview during tool execution (default true)\n\tTaskRecoveryIntervalSec int `json:\"task_recovery_interval_sec,omitempty\"` // team task recovery ticker interval in seconds (default 300 = 5min)\n\tBackgroundProvider string `json:\"background_provider,omitempty\"` // LLM provider for background workers (vault enrichment, consolidation)\n\tBackgroundModel string `json:\"background_model,omitempty\"` // LLM model for background workers\n}\n\n// ToolsConfig controls tool availability, policy, and web search.\ntype ToolsConfig struct {\n\tProfile string `json:\"profile,omitempty\"` // global profile: \"minimal\", \"coding\", \"messaging\", \"full\"\n\tAllow []string `json:\"allow,omitempty\"` // global allow list (tool names or \"group:xxx\")\n\tDeny []string `json:\"deny,omitempty\"` // global deny list\n\tAlsoAllow []string `json:\"alsoAllow,omitempty\"` // additive: adds without removing existing\n\tByProvider map[string]*ToolPolicySpec `json:\"byProvider,omitempty\"` // per-provider overrides\n\tShellDenyGroups map[string]bool `json:\"shellDenyGroups,omitempty\"` // global shell deny-group toggles (group name -> denied); per-agent overrides win per-key\n\tCommandKeywordAllowlist []CommandKeywordAllowlistRule `json:\"commandKeywordAllowlist,omitempty\"` // scoped bypass for credentialed CLI content keywords\n\tExecApproval ExecApprovalCfg `json:\"execApproval\"` // exec command approval settings\n\tWebFetch WebFetchPolicyConfig `json:\"web_fetch\"` // domain policy for URL fetching\n\tBrowser BrowserToolConfig `json:\"browser\"`\n\tRateLimitPerHour int `json:\"rate_limit_per_hour,omitempty\"` // max tool executions per hour per session (0 = disabled)\n\tScrubCredentials *bool `json:\"scrub_credentials,omitempty\"` // auto-redact API keys/tokens in tool output (default true)\n\tMcpServers map[string]*MCPServerConfig `json:\"mcp_servers,omitempty\"` // external MCP server connections\n\tDocumentParser DocumentParserConfig `json:\"document_parser\"` // local-first document text extraction\n}\n\n// DocumentParserConfig controls local-first document text extraction in the\n// read_document tool. Local-first is OPT-IN (disabled by default): plain\n// pdftotext/pandoc output mis-orders tables/multi-column layouts and ignores\n// the analysis prompt, so the cloud vision path remains the default. When\n// enabled, PDF/DOCX are extracted via local binaries before any cloud LLM\n// call; missing binaries or empty output fall back to the provider chain.\n//\n// Changing these values requires a gateway restart — they are captured at tool\n// construction and not picked up by config hot-reload. (Binary availability,\n// however, is re-checked per call, so runtime installs are seen without a\n// restart.)\ntype DocumentParserConfig struct {\n\tLocalFirst *bool `json:\"local_first,omitempty\"` // nil => false (default OFF)\n\tMaxPages int `json:\"max_pages,omitempty\"` // 0 => 200\n\tTimeoutSec int `json:\"timeout_sec,omitempty\"` // 0 => 30s\n\tMinTextLen int `json:\"min_text_len,omitempty\"` // 0 => 16\n}\n\n// LocalFirstEnabled reports whether local-first extraction is turned on.\nfunc (c DocumentParserConfig) LocalFirstEnabled() bool {\n\treturn c.LocalFirst != nil && *c.LocalFirst\n}\n\n// CommandKeywordAllowlistRule scopes product/security vocabulary that may appear\n// in credentialed CLI content args without disabling command-path deny patterns.\ntype CommandKeywordAllowlistRule struct {\n\tID string `json:\"id,omitempty\"`\n\tCommand string `json:\"command\"`\n\tSubcommands []string `json:\"subcommands,omitempty\"`\n\tArgs []string `json:\"args,omitempty\"`\n\tArgPositions []int `json:\"argPositions,omitempty\"` // 0-based after matched subcommand\n\tKeywords []string `json:\"keywords,omitempty\"`\n\tReason string `json:\"reason,omitempty\"`\n\tEnabled *bool `json:\"enabled,omitempty\"` // nil means enabled\n}\n\n// MCPServerConfig configures a single external MCP server connection.\ntype MCPServerConfig struct {\n\tTransport string `json:\"transport\"` // \"stdio\", \"sse\", \"streamable-http\"\n\tCommand string `json:\"command,omitempty\"` // stdio: command to spawn\n\tArgs []string `json:\"args,omitempty\"` // stdio: command arguments\n\tEnv map[string]string `json:\"env,omitempty\"` // stdio: extra environment variables\n\tURL string `json:\"url,omitempty\"` // sse/http: server URL\n\tHeaders map[string]string `json:\"headers,omitempty\"` // sse/http: extra HTTP headers\n\tEnabled *bool `json:\"enabled,omitempty\"` // default true\n\tToolPrefix string `json:\"tool_prefix,omitempty\"` // prefix for tool names (avoids collisions)\n\tTimeoutSec int `json:\"timeout_sec,omitempty\"` // per-tool-call timeout in seconds (default 60)\n}\n\n// IsEnabled returns whether this MCP server is enabled (default true).\nfunc (c *MCPServerConfig) IsEnabled() bool {\n\treturn c.Enabled == nil || *c.Enabled\n}\n\n// ExecApprovalCfg configures command execution approval (matching TS exec-approval.ts).\ntype ExecApprovalCfg struct {\n\tSecurity string `json:\"security,omitempty\"` // \"deny\", \"allowlist\", \"full\" (default \"full\")\n\tAsk string `json:\"ask,omitempty\"` // \"off\", \"on-miss\", \"always\" (default \"off\")\n\tAllowlist []string `json:\"allowlist,omitempty\"` // glob patterns for allowed commands\n}\n\n// WebFetchPolicyConfig controls domain filtering for the web_fetch tool.\ntype WebFetchPolicyConfig struct {\n\tPolicy string `json:\"policy,omitempty\"` // \"allow_all\" (default), \"allowlist\"\n\tAllowedDomains []string `json:\"allowed_domains,omitempty\"` // e.g. [\"github.com\", \"*.example.com\"]\n\tBlockedDomains []string `json:\"blocked_domains,omitempty\"` // always checked regardless of policy\n}\n\n// BrowserToolConfig controls the browser automation tool.\ntype BrowserToolConfig struct {\n\tEnabled bool `json:\"enabled\"` // enable the browser tool (default false)\n\tHeadless bool `json:\"headless,omitempty\"` // run Chrome in headless mode (ignored when RemoteURL is set)\n\tRemoteURL string `json:\"remote_url,omitempty\"` // CDP endpoint for remote Chrome sidecar, e.g. \"ws://chrome:9222\"\n\tActionTimeoutMs int `json:\"action_timeout_ms,omitempty\"` // per-action timeout in ms (default 30000)\n\tIdleTimeoutMs int `json:\"idle_timeout_ms,omitempty\"` // idle page auto-close in ms (default 600000, 0=disabled)\n\tMaxPages int `json:\"max_pages,omitempty\"` // max open pages per tenant (default 5)\n\tCookieSyncEnabled bool `json:\"cookie_sync_enabled\"` // apply selected synced cookies to scoped browser sessions\n}\n\n// ToolPolicySpec defines a tool policy at any level (global, per-agent, per-provider).\ntype ToolPolicySpec struct {\n\tProfile string `json:\"profile,omitempty\"`\n\tAllow []string `json:\"allow,omitempty\"`\n\tDeny []string `json:\"deny,omitempty\"`\n\tAlsoAllow []string `json:\"alsoAllow,omitempty\"`\n\tByProvider map[string]*ToolPolicySpec `json:\"byProvider,omitempty\"`\n\tWait *WaitToolPolicy `json:\"wait,omitempty\"`\n\tToolCallPrefix string `json:\"toolCallPrefix,omitempty\"` // prefix to strip from model's tool call names before registry lookup\n}\n\n// WaitToolPolicy configures per-agent safety bounds for the wait tool.\ntype WaitToolPolicy struct {\n\tMinMs int `json:\"min_ms,omitempty\"`\n\tMaxMs int `json:\"max_ms,omitempty\"`\n}\n\n// SessionsConfig controls session behavior.\n// Matching TS src/config/sessions/types.ts + src/config/types.base.ts.\ntype SessionsConfig struct {\n\tScope string `json:\"scope,omitempty\"` // \"per-sender\" (default), \"global\"\n\tDmScope string `json:\"dm_scope,omitempty\"` // \"main\", \"per-peer\", \"per-channel-peer\" (default), \"per-account-channel-peer\"\n\tMainKey string `json:\"main_key,omitempty\"` // main session key suffix (default \"main\", used when dm_scope=\"main\")\n}\n\n// TtsConfig configures text-to-speech.\n// Matching TS src/config/types.tts.ts.\ntype TtsConfig struct {\n\tProvider string `json:\"provider,omitempty\"` // \"openai\", \"elevenlabs\", \"edge\", \"minimax\", \"gemini\"\n\tAuto string `json:\"auto,omitempty\"` // \"off\" (default), \"always\", \"inbound\", \"tagged\"\n\tMode string `json:\"mode,omitempty\"` // \"final\" (default), \"all\"\n\tMaxLength int `json:\"max_length,omitempty\"` // max text length before truncation (default 1500)\n\tTimeoutMs int `json:\"timeout_ms,omitempty\"` // API timeout in ms (default 30000)\n\tOpenAI TtsOpenAIConfig `json:\"openai\"`\n\tElevenLabs TtsElevenLabsConfig `json:\"elevenlabs\"`\n\tEdge TtsEdgeConfig `json:\"edge\"`\n\tMiniMax TtsMiniMaxConfig `json:\"minimax\"`\n\tGemini TtsGeminiConfig `json:\"gemini\"`\n}\n\n// TtsGeminiConfig configures the Google Gemini TTS provider.\ntype TtsGeminiConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"` // required; encrypted at rest\n\tAPIBase string `json:\"api_base,omitempty\"` // custom endpoint (optional; SSRF-gated)\n\tVoice string `json:\"voice,omitempty\"` // default \"Kore\"\n\tModel string `json:\"model,omitempty\"` // default \"gemini-2.5-flash-preview-tts\"\n\tSpeakers string `json:\"speakers,omitempty\"` // JSON-encoded []SpeakerVoice for multi-speaker mode\n}\n\n// TtsOpenAIConfig configures the OpenAI TTS provider.\ntype TtsOpenAIConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"`\n\tAPIBase string `json:\"api_base,omitempty\"` // custom endpoint URL\n\tModel string `json:\"model,omitempty\"` // default \"gpt-4o-mini-tts\"\n\tVoice string `json:\"voice,omitempty\"` // default \"alloy\"\n}\n\n// TtsElevenLabsConfig configures the ElevenLabs TTS provider.\ntype TtsElevenLabsConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"`\n\tBaseURL string `json:\"base_url,omitempty\"`\n\tVoiceID string `json:\"voice_id,omitempty\"` // default \"pMsXgVXv3BLzUgSXRplE\"\n\tModelID string `json:\"model_id,omitempty\"` // default \"eleven_multilingual_v2\"\n}\n\n// TtsEdgeConfig configures the Microsoft Edge TTS provider (free, no API key).\ntype TtsEdgeConfig struct {\n\tEnabled bool `json:\"enabled,omitempty\"`\n\tVoice string `json:\"voice,omitempty\"` // default \"en-US-MichelleNeural\"\n\tRate string `json:\"rate,omitempty\"` // speech rate, e.g. \"+0%\"\n}\n\n// TtsMiniMaxConfig configures the MiniMax TTS provider.\ntype TtsMiniMaxConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"`\n\tGroupID string `json:\"group_id,omitempty\"` // MiniMax GroupId (required)\n\tAPIBase string `json:\"api_base,omitempty\"` // default \"https://api.minimax.io/v1\"\n\tModel string `json:\"model,omitempty\"` // default \"speech-02-hd\"\n\tVoiceID string `json:\"voice_id,omitempty\"` // default \"Wise_Woman\"\n}\n\n// MergeChannelGroupQuotas merges per-group quota overrides from channel configs\n// (e.g., channels.telegram.groups[chatID].quota) into gateway.quota.groups.\n// This allows per-group quotas to be set at the channel level and picked up\n// by the QuotaChecker at runtime.\nfunc MergeChannelGroupQuotas(cfg *Config) {\n\tif cfg.Gateway.Quota == nil {\n\t\treturn\n\t}\n\tif cfg.Gateway.Quota.Groups == nil {\n\t\tcfg.Gateway.Quota.Groups = make(map[string]QuotaWindow)\n\t}\n\n\t// Telegram per-group quotas\n\tfor chatID, groupCfg := range cfg.Channels.Telegram.Groups {\n\t\tif groupCfg != nil && groupCfg.Quota != nil && !groupCfg.Quota.IsZero() {\n\t\t\tkey := \"group:telegram:\" + chatID\n\t\t\tcfg.Gateway.Quota.Groups[key] = *groupCfg.Quota\n\t\t}\n\t}\n}\n"}, "fdba1c2138a8289bf376ec2702a5fd2b5e6ed979": {"t": 1785759221.122923, "n": 39871, "h": "b5977e40c606434c", "c": "package config\n\n// ChatBehaviorConfig controls optional human-like channel delivery behavior.\n// Pointer fields allow per-channel overrides to inherit gateway defaults.\ntype ChatBehaviorConfig struct {\n\tEnabled *bool `json:\"enabled,omitempty\"`\n\tIntermediateReplies *IntermediateRepliesConfig `json:\"intermediate_replies,omitempty\"`\n\tQuickAck *QuickAckConfig `json:\"quick_ack,omitempty\"`\n\tFinalSplit *FinalSplitConfig `json:\"final_split,omitempty\"`\n}\n\n// QuickAckConfig controls one short acknowledgement before longer non-streaming runs.\ntype QuickAckConfig struct {\n\tEnabled *bool `json:\"enabled,omitempty\"`\n\tMode *string `json:\"mode,omitempty\"`\n\tMinDelayMs *int `json:\"min_delay_ms,omitempty\"`\n\tProvider string `json:\"provider,omitempty\"`\n\tModel string `json:\"model,omitempty\"`\n\tTimeoutMs *int `json:\"timeout_ms,omitempty\"`\n\tMaxTokens *int `json:\"max_tokens,omitempty\"`\n\tMaxChars *int `json:\"max_chars,omitempty\"`\n\tTemplates []string `json:\"templates,omitempty\"`\n}\n\n// IntermediateRepliesConfig controls delivery-only progress messages during tool phases.\ntype IntermediateRepliesConfig struct {\n\tEnabled *bool `json:\"enabled,omitempty\"`\n\tMode *string `json:\"mode,omitempty\"`\n\tProvider string `json:\"provider,omitempty\"`\n\tModel string `json:\"model,omitempty\"`\n\tTimeoutMs *int `json:\"timeout_ms,omitempty\"`\n\tMaxTokens *int `json:\"max_tokens,omitempty\"`\n\tMaxChars *int `json:\"max_chars,omitempty\"`\n}\n\n// FinalSplitConfig controls semantic splitting of final channel replies.\ntype FinalSplitConfig struct {\n\tEnabled *bool `json:\"enabled,omitempty\"`\n\tMinChars *int `json:\"min_chars,omitempty\"`\n\tMaxMessages *int `json:\"max_messages,omitempty\"`\n\tDelayMs *int `json:\"delay_ms,omitempty\"`\n}\n\n// PendingCompactionConfig configures LLM-based compaction of pending group messages.\n// When a group accumulates more than Threshold pending messages, older messages are\n// summarized by an LLM and replaced with a compact summary, keeping KeepRecent raw messages.\ntype PendingCompactionConfig struct {\n\tThreshold int `json:\"threshold,omitempty\"` // trigger compaction when entries exceed this (default 200)\n\tKeepRecent int `json:\"keep_recent,omitempty\"` // keep this many recent raw messages after compaction (default 40)\n\tMaxTokens int `json:\"max_tokens,omitempty\"` // max output tokens for LLM summarization (default 4096)\n\tProvider string `json:\"provider,omitempty\"` // LLM provider name (e.g. \"openai\"); empty = use agent's provider\n\tModel string `json:\"model,omitempty\"` // model for summarization; empty = use agent's model\n}\n\n// ChannelsConfig contains per-channel configuration.\ntype ChannelsConfig struct {\n\tTelegram TelegramConfig `json:\"telegram\"`\n\tDiscord DiscordConfig `json:\"discord\"`\n\tSlack SlackConfig `json:\"slack\"`\n\tWhatsApp WhatsAppConfig `json:\"whatsapp\"`\n\tZalo ZaloConfig `json:\"zalo\"`\n\tZaloPersonal ZaloPersonalConfig `json:\"zalo_personal\"`\n\tFeishu FeishuConfig `json:\"feishu\"`\n\tPendingCompaction *PendingCompactionConfig `json:\"pending_compaction,omitempty\"` // global pending message compaction settings\n}\n\ntype TelegramConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tToken string `json:\"token\"`\n\tProxy string `json:\"proxy,omitempty\"`\n\tAPIServer string `json:\"api_server,omitempty\"` // custom Telegram Bot API server URL (e.g. \"http://localhost:8081\")\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in groups (default true)\n\tMentionMode string `json:\"mention_mode,omitempty\"` // \"strict\" (default) = only respond when mentioned; \"yield\" = respond unless another bot is mentioned\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tDMStream *bool `json:\"dm_stream,omitempty\"` // enable streaming for DMs (default false) — edits placeholder progressively\n\tGroupStream *bool `json:\"group_stream,omitempty\"` // enable streaming for groups (default false) — sends new message, edits progressively\n\tDraftTransport *bool `json:\"draft_transport,omitempty\"` // use sendMessageDraft for DM streaming (default true) — stealth preview, no notifications per edit\n\tReasoningDelivery string `json:\"reasoning_delivery,omitempty\"` // off, streaming_only, always_bubbles\n\tReasoningStream *bool `json:\"reasoning_stream,omitempty\"` // show reasoning as separate message when provider emits thinking events (default true)\n\tReactionLevel string `json:\"reaction_level,omitempty\"` // \"off\" (default), \"minimal\", \"full\" — status emoji reactions\n\tMediaMaxBytes int64 `json:\"media_max_bytes,omitempty\"` // max media download size in bytes (default 20MB)\n\tLinkPreview *bool `json:\"link_preview,omitempty\"` // enable URL previews in messages (default true)\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tChatBehavior *ChatBehaviorConfig `json:\"chat_behavior,omitempty\"` // override gateway chat behavior (nil = inherit)\n\tForceIPv4 bool `json:\"force_ipv4,omitempty\"` // force IPv4 for all Telegram API requests (use when IPv6 routing is broken)\n\n\t// Optional STT (Speech-to-Text) pipeline for voice/audio inbound messages.\n\t// When stt_proxy_url is set, audio/voice messages are transcribed before being forwarded to the agent.\n\tSTTProxyURL string `json:\"stt_proxy_url,omitempty\"` // base URL of the STT proxy service (e.g. \"https://stt.example.com\")\n\tSTTAPIKey string `json:\"stt_api_key,omitempty\"` // Bearer token for the STT proxy\n\tSTTTenantID string `json:\"stt_tenant_id,omitempty\"` // optional tenant/org identifier forwarded to the STT proxy\n\tSTTTimeoutSeconds int `json:\"stt_timeout_seconds,omitempty\"` // per-request timeout for STT calls (default 30s)\n\n\t// Optional audio-aware routing: when set, voice/audio inbound messages are routed to this\n\t// agent instead of the default channel agent. Requires the named agent to exist in the config.\n\tVoiceAgentID string `json:\"voice_agent_id,omitempty\"` // agent ID to route voice inbound to (e.g. \"speaking-agent\")\n\n\t// Audio guard: intercept technical errors in voice agent replies and replace with friendly fallbacks.\n\t// Only active when VoiceAgentID is set. Custom error markers replace built-in defaults when provided.\n\tAudioGuardFallbackTranscript string `json:\"audio_guard_fallback_transcript,omitempty\"` // fallback with %s for transcript (e.g. \"I heard: \\\"%s\\\". Try again!\")\n\tAudioGuardFallbackNoTranscript string `json:\"audio_guard_fallback_no_transcript,omitempty\"` // fallback when no transcript available\n\tAudioGuardErrorMarkers []string `json:\"audio_guard_error_markers,omitempty\"` // custom error detection markers (replaces defaults)\n\n\t// Per-group (and per-topic) overrides. Key is chat ID string (e.g. \"-100123456\") or \"*\" for wildcard.\n\t// TS ref: channels.telegram.groups in src/config/types.telegram.ts.\n\tGroups map[string]*TelegramGroupConfig `json:\"groups,omitempty\"`\n}\n\n// TelegramGroupConfig defines per-group overrides for a Telegram channel.\n// Matching TS TelegramGroupConfig in src/config/types.telegram.ts.\ntype TelegramGroupConfig struct {\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // override group policy for this group\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // override require_mention for this group\n\tMentionMode string `json:\"mention_mode,omitempty\"` // override mention_mode for this group\n\tAllowFrom FlexibleStringSlice `json:\"allow_from,omitempty\"` // override allow_from for this group\n\tEnabled *bool `json:\"enabled,omitempty\"` // disable bot for this group (default: true)\n\tSkills []string `json:\"skills,omitempty\"` // skill whitelist (nil = all, [] = none)\n\tTools []string `json:\"tools,omitempty\"` // tool allow list (nil = all, supports \"group:xxx\")\n\tSystemPrompt string `json:\"system_prompt,omitempty\"` // extra system prompt for this group\n\tTopics map[string]*TelegramTopicConfig `json:\"topics,omitempty\"` // per-topic overrides (key: thread ID string)\n\tQuota *QuotaWindow `json:\"quota,omitempty\"` // per-group quota override\n}\n\n// TelegramTopicConfig defines per-topic overrides within a Telegram group.\n// Matching TS TelegramTopicConfig in src/config/types.telegram.ts.\ntype TelegramTopicConfig struct {\n\tRequireMention *bool `json:\"require_mention,omitempty\"`\n\tMentionMode string `json:\"mention_mode,omitempty\"`\n\tGroupPolicy string `json:\"group_policy,omitempty\"`\n\tSkills []string `json:\"skills,omitempty\"`\n\tTools []string `json:\"tools,omitempty\"` // tool allow list (nil = inherit, supports \"group:xxx\")\n\tEnabled *bool `json:\"enabled,omitempty\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from,omitempty\"`\n\tSystemPrompt string `json:\"system_prompt,omitempty\"`\n}\n\ntype DiscordConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tToken string `json:\"token\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in groups (default true)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tChatBehavior *ChatBehaviorConfig `json:\"chat_behavior,omitempty\"` // override gateway chat behavior (nil = inherit)\n\tMediaMaxBytes int64 `json:\"media_max_bytes,omitempty\"` // max media download size (default 25MB)\n\tSTTProxyURL string `json:\"stt_proxy_url,omitempty\"`\n\tSTTAPIKey string `json:\"stt_api_key,omitempty\"`\n\tSTTTenantID string `json:\"stt_tenant_id,omitempty\"`\n\tSTTTimeoutSeconds int `json:\"stt_timeout_seconds,omitempty\"`\n\tVoiceAgentID string `json:\"voice_agent_id,omitempty\"`\n}\n\ntype SlackConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tBotToken string `json:\"bot_token\"` // xoxb-... (Bot User OAuth Token)\n\tAppToken string `json:\"app_token\"` // xapp-... (App-Level Token for Socket Mode)\n\tUserToken string `json:\"user_token,omitempty\"` // xoxp-... (Optional: custom bot identity)\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"pairing\", \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in channels (default true)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tDMStream *bool `json:\"dm_stream,omitempty\"` // enable streaming for DMs (default false)\n\tGroupStream *bool `json:\"group_stream,omitempty\"` // enable streaming for groups (default false)\n\tNativeStream *bool `json:\"native_stream,omitempty\"` // use Slack ChatStreamer API if available (default false)\n\tReactionLevel string `json:\"reaction_level,omitempty\"` // \"off\" (default), \"minimal\", \"full\"\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tChatBehavior *ChatBehaviorConfig `json:\"chat_behavior,omitempty\"` // override gateway chat behavior (nil = inherit)\n\tDebounceDelay *int `json:\"debounce_delay,omitempty\"` // ms delay before dispatching rapid messages (default 300, 0=disabled)\n\tThreadTTL *int `json:\"thread_ttl,omitempty\"` // hours before thread participation expires (default 24, 0=disabled — always require @mention)\n\tMediaMaxBytes int64 `json:\"media_max_bytes,omitempty\"` // max file download size in bytes (default 20MB)\n}\n\ntype WhatsAppConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tAuthDir string `json:\"auth_dir,omitempty\"` // optional: SQLite auth dir override (desktop)\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default for DB instances), \"open\", \"allowlist\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"pairing\" (default for DB instances), \"open\" (default for config), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // only respond in groups when bot is @mentioned (default false)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 200, 0=disabled)\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tChatBehavior *ChatBehaviorConfig `json:\"chat_behavior,omitempty\"` // override gateway chat behavior (nil = inherit)\n}\n\ntype ZaloConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tToken string `json:\"token\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tWebhookURL string `json:\"webhook_url,omitempty\"`\n\tWebhookSecret string `json:\"webhook_secret,omitempty\"`\n\tMediaMaxMB int `json:\"media_max_mb,omitempty\"` // default 5\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tChatBehavior *ChatBehaviorConfig `json:\"chat_behavior,omitempty\"` // override gateway chat behavior (nil = inherit)\n}\n\ntype ZaloPersonalConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in groups (default true)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tCredentialsPath string `json:\"credentials_path,omitempty\"` // path to saved cookies JSON\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tChatBehavior *ChatBehaviorConfig `json:\"chat_behavior,omitempty\"` // override gateway chat behavior (nil = inherit)\n}\n\ntype FeishuConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tAppID string `json:\"app_id\"`\n\tAppSecret string `json:\"app_secret\"`\n\tEncryptKey string `json:\"encrypt_key,omitempty\"`\n\tVerificationToken string `json:\"verification_token,omitempty\"`\n\tDomain string `json:\"domain,omitempty\"` // \"lark\" (default/global), \"feishu\" (China), or custom URL\n\tConnectionMode string `json:\"connection_mode,omitempty\"` // \"websocket\" (default), \"webhook\"\n\tWebhookPort int `json:\"webhook_port,omitempty\"` // default 3000\n\tWebhookPath string `json:\"webhook_path,omitempty\"` // default \"/feishu/events\"\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default)\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default)\n\tGroupAllowFrom FlexibleStringSlice `json:\"group_allow_from,omitempty\"`\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // default true (groups)\n\tTopicSessionMode string `json:\"topic_session_mode,omitempty\"` // \"disabled\" (default)\n\tTextChunkLimit int `json:\"text_chunk_limit,omitempty\"` // default 4000\n\tMediaMaxMB int `json:\"media_max_mb,omitempty\"` // default 30\n\tRenderMode string `json:\"render_mode,omitempty\"` // \"auto\", \"raw\", \"card\"\n\tStreaming *bool `json:\"streaming,omitempty\"` // default true\n\tReactionLevel string `json:\"reaction_level,omitempty\"` // \"off\" (default), \"minimal\", \"full\" — typing emoji reactions\n\tHistoryLimit int `json:\"history_limit,omitempty\"`\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tChatBehavior *ChatBehaviorConfig `json:\"chat_behavior,omitempty\"` // override gateway chat behavior (nil = inherit)\n\tSTTProxyURL string `json:\"stt_proxy_url,omitempty\"`\n\tSTTAPIKey string `json:\"stt_api_key,omitempty\"`\n\tSTTTenantID string `json:\"stt_tenant_id,omitempty\"`\n\tSTTTimeoutSeconds int `json:\"stt_timeout_seconds,omitempty\"`\n\tVoiceAgentID string `json:\"voice_agent_id,omitempty\"`\n}\n\n// ProvidersConfig maps provider name to its config.\ntype ProvidersConfig struct {\n\tAnthropic ProviderConfig `json:\"anthropic\"`\n\tOpenAI ProviderConfig `json:\"openai\"`\n\tOpenRouter ProviderConfig `json:\"openrouter\"`\n\tGroq ProviderConfig `json:\"groq\"`\n\tGemini ProviderConfig `json:\"gemini\"`\n\tDeepSeek ProviderConfig `json:\"deepseek\"`\n\tMistral ProviderConfig `json:\"mistral\"`\n\tXAI ProviderConfig `json:\"xai\"`\n\tMiniMax ProviderConfig `json:\"minimax\"`\n\tCohere ProviderConfig `json:\"cohere\"`\n\tPerplexity ProviderConfig `json:\"perplexity\"`\n\tDashScope ProviderConfig `json:\"dashscope\"`\n\tBailian ProviderConfig `json:\"bailian\"`\n\tZai ProviderConfig `json:\"zai\"`\n\tZaiCoding ProviderConfig `json:\"zai_coding\"`\n\tOllama OllamaConfig `json:\"ollama\"` // local Ollama instance (no API key needed)\n\tOllamaCloud ProviderConfig `json:\"ollama_cloud\"` // Ollama Cloud (API key required)\n\tClaudeCLI ClaudeCLIConfig `json:\"claude_cli\"`\n\tACP ACPConfig `json:\"acp\"`\n\tNovita ProviderConfig `json:\"novita\"` // Novita AI (OpenAI-compatible endpoint)\n\tBytePlus ProviderConfig `json:\"byteplus\"` // BytePlus ModelArk (Seed 2.0)\n\tBytePlusCoding ProviderConfig `json:\"byteplus_coding\"` // BytePlus ModelArk Coding Plan\n\tVertex VertexConfig `json:\"vertex\"` // Google Cloud Vertex AI (OAuth2 service account + ADC)\n}\n\n// VertexConfig configures Google Cloud Vertex AI.\n// Credentials precedence: APIKey (inline JSON) > CredentialsFile (path) > ADC (both empty).\n// ProjectID and Region are required; Model optional (defaults to google/gemini-2.0-flash-001).\ntype VertexConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"` // service account JSON inline (secret — never persist in config.json)\n\tCredentialsFile string `json:\"credentials_file,omitempty\"` // path to service account JSON file\n\tProjectID string `json:\"project_id,omitempty\"`\n\tRegion string `json:\"region,omitempty\"`\n\tModel string `json:\"model,omitempty\"`\n}\n\n// OllamaConfig configures a local (or self-hosted) Ollama instance.\n// No API key is required — Ollama accepts any Bearer token value.\ntype OllamaConfig struct {\n\tHost string `json:\"host\"` // Ollama server base URL, e.g. http://localhost:11434\n}\n\n// ClaudeCLIConfig configures the Claude CLI provider (uses subscription, not API key).\ntype ClaudeCLIConfig struct {\n\tCLIPath string `json:\"cli_path\" yaml:\"cli_path\"` // path to claude binary (default: \"claude\")\n\tModel string `json:\"model\" yaml:\"model\"` // default model alias (default: \"sonnet\")\n\tBaseWorkDir string `json:\"base_work_dir\" yaml:\"base_work_dir\"` // base dir for agent workspaces\n\tPermMode string `json:\"perm_mode\" yaml:\"perm_mode\"` // permission mode (default: \"bypassPermissions\")\n}\n\n// ACPConfig configures the ACP (Agent Client Protocol) provider.\n// Orchestrates any ACP-compatible coding agent (Claude Code, Codex CLI, Gemini CLI) as a subprocess.\ntype ACPConfig struct {\n\tBinary string `json:\"binary\"` // agent binary name or path (e.g. \"claude\", \"codex\")\n\tArgs []string `json:\"args\"` // extra spawn args\n\tModel string `json:\"model\"` // default model/agent name\n\tWorkDir string `json:\"work_dir\"` // base workspace dir\n\tIdleTTL string `json:\"idle_ttl\"` // process idle TTL (e.g. \"5m\")\n\tPermMode string `json:\"perm_mode\"` // \"approve-all\" (default), \"approve-reads\", \"deny-all\"\n}\n\ntype ProviderConfig struct {\n\tAPIKey string `json:\"api_key\"`\n\tAPIBase string `json:\"api_base,omitempty\"`\n}\n\n// APIBaseForType returns the config-level api_base for a given provider type.\n// Used as a fallback when DB providers have no api_base set.\nfunc (p *ProvidersConfig) APIBaseForType(providerType string) string {\n\tswitch providerType {\n\tcase \"anthropic_native\":\n\t\treturn p.Anthropic.APIBase\n\tcase \"openai\", \"openai_compat\":\n\t\treturn p.OpenAI.APIBase\n\tcase \"openrouter\":\n\t\treturn p.OpenRouter.APIBase\n\tcase \"groq\":\n\t\treturn p.Groq.APIBase\n\tcase \"deepseek\":\n\t\treturn p.DeepSeek.APIBase\n\tcase \"gemini_native\":\n\t\treturn p.Gemini.APIBase\n\tcase \"mistral\":\n\t\treturn p.Mistral.APIBase\n\tcase \"xai\":\n\t\treturn p.XAI.APIBase\n\tcase \"minimax_native\":\n\t\treturn p.MiniMax.APIBase\n\tcase \"cohere\":\n\t\treturn p.Cohere.APIBase\n\tcase \"perplexity\":\n\t\treturn p.Perplexity.APIBase\n\tcase \"dashscope\":\n\t\treturn p.DashScope.APIBase\n\tcase \"bailian\":\n\t\treturn p.Bailian.APIBase\n\tcase \"zai\":\n\t\treturn p.Zai.APIBase\n\tcase \"zai_coding\":\n\t\treturn p.ZaiCoding.APIBase\n\tcase \"ollama_cloud\":\n\t\treturn p.OllamaCloud.APIBase\n\tcase \"novita\":\n\t\treturn p.Novita.APIBase\n\tcase \"byteplus\":\n\t\treturn p.BytePlus.APIBase\n\tcase \"byteplus_coding\":\n\t\treturn p.BytePlusCoding.APIBase\n\tcase \"vertex\":\n\t\t// Computed from project+region at registration time; no config-level static base.\n\t\treturn \"\"\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\n// HasAnyProvider returns true if at least one provider has an API key or CLI configured.\nfunc (c *Config) HasAnyProvider() bool {\n\tp := c.Providers\n\treturn p.Anthropic.APIKey != \"\" ||\n\t\tp.OpenAI.APIKey != \"\" ||\n\t\tp.OpenRouter.APIKey != \"\" ||\n\t\tp.Groq.APIKey != \"\" ||\n\t\tp.Gemini.APIKey != \"\" ||\n\t\tp.DeepSeek.APIKey != \"\" ||\n\t\tp.Mistral.APIKey != \"\" ||\n\t\tp.XAI.APIKey != \"\" ||\n\t\tp.MiniMax.APIKey != \"\" ||\n\t\tp.Cohere.APIKey != \"\" ||\n\t\tp.Perplexity.APIKey != \"\" ||\n\t\tp.DashScope.APIKey != \"\" ||\n\t\tp.Bailian.APIKey != \"\" ||\n\t\tp.Zai.APIKey != \"\" ||\n\t\tp.ZaiCoding.APIKey != \"\" ||\n\t\tp.Ollama.Host != \"\" ||\n\t\tp.OllamaCloud.APIKey != \"\" ||\n\t\tp.ClaudeCLI.CLIPath != \"\" ||\n\t\tp.ACP.Binary != \"\" ||\n\t\tp.Novita.APIKey != \"\" ||\n\t\tp.BytePlus.APIKey != \"\" ||\n\t\tp.BytePlusCoding.APIKey != \"\" ||\n\t\t(p.Vertex.ProjectID != \"\" && p.Vertex.Region != \"\")\n}\n\n// QuotaWindow defines request limits per time window. Zero means unlimited.\ntype QuotaWindow struct {\n\tHour int `json:\"hour,omitempty\"` // max requests per hour (0 = unlimited)\n\tDay int `json:\"day,omitempty\"` // max requests per day (0 = unlimited)\n\tWeek int `json:\"week,omitempty\"` // max requests per week (0 = unlimited)\n}\n\n// IsZero returns true if no limits are set.\nfunc (w QuotaWindow) IsZero() bool { return w.Hour == 0 && w.Day == 0 && w.Week == 0 }\n\n// QuotaConfig configures per-user/group request quotas.\n// Config merge priority: Groups > Channels > Providers > Default.\ntype QuotaConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tDefault QuotaWindow `json:\"default\"`\n\tProviders map[string]QuotaWindow `json:\"providers,omitempty\"` // key = provider name (e.g. \"anthropic\")\n\tChannels map[string]QuotaWindow `json:\"channels,omitempty\"` // key = channel name (e.g. \"telegram\")\n\tGroups map[string]QuotaWindow `json:\"groups,omitempty\"` // key = userID (e.g. \"group:telegram:-100123\")\n}\n\n// GatewayConfig controls the gateway server.\ntype GatewayConfig struct {\n\tHost string `json:\"host\"`\n\tPort int `json:\"port\"`\n\tToken string `json:\"token,omitempty\"` // bearer token for WS/HTTP auth\n\tOwnerIDs []string `json:\"owner_ids,omitempty\"` // sender IDs considered \"owner\"\n\tAllowedOrigins []string `json:\"allowed_origins,omitempty\"` // WebSocket CORS whitelist (empty = allow all)\n\tMaxMessageChars int `json:\"max_message_chars,omitempty\"` // max user message characters (default 32000)\n\tRateLimitRPM int `json:\"rate_limit_rpm,omitempty\"` // rate limit: requests per minute per user (default 20, 0 = disabled)\n\tInjectionAction string `json:\"injection_action,omitempty\"` // prompt injection action: \"log\", \"warn\" (default), \"block\", \"off\"\n\tInboundDebounceMs int `json:\"inbound_debounce_ms,omitempty\"` // silence-window in ms that merges rapid channel/Web Chat messages from the same sender/session; 0 disables for text but media-bearing messages still honor a built-in media floor so multi-attachment bursts (#63) coalesce into a single agent run. Agents may override via per-agent agent_config.inbound_debounce_ms.\n\tQuota *QuotaConfig `json:\"quota,omitempty\"` // per-user/group request quotas\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // deliver intermediate text during tool iterations (default false)\n\tChatBehavior *ChatBehaviorConfig `json:\"chat_behavior,omitempty\"` // human-like channel delivery behavior (default disabled)\n\tToolStatus *bool `json:\"tool_status,omitempty\"` // show tool name in streaming preview during tool execution (default true)\n\tTaskRecoveryIntervalSec int `json:\"task_recovery_interval_sec,omitempty\"` // team task recovery ticker interval in seconds (default 300 = 5min)\n\tBackgroundProvider string `json:\"background_provider,omitempty\"` // LLM provider for background workers (vault enrichment, consolidation)\n\tBackgroundModel string `json:\"background_model,omitempty\"` // LLM model for background workers\n}\n\n// ToolsConfig controls tool availability, policy, and web search.\ntype ToolsConfig struct {\n\tProfile string `json:\"profile,omitempty\"` // global profile: \"minimal\", \"coding\", \"messaging\", \"full\"\n\tAllow []string `json:\"allow,omitempty\"` // global allow list (tool names or \"group:xxx\")\n\tDeny []string `json:\"deny,omitempty\"` // global deny list\n\tAlsoAllow []string `json:\"alsoAllow,omitempty\"` // additive: adds without removing existing\n\tByProvider map[string]*ToolPolicySpec `json:\"byProvider,omitempty\"` // per-provider overrides\n\tShellDenyGroups map[string]bool `json:\"shellDenyGroups,omitempty\"` // global shell deny-group toggles (group name -> denied); per-agent overrides win per-key\n\tCommandKeywordAllowlist []CommandKeywordAllowlistRule `json:\"commandKeywordAllowlist,omitempty\"` // scoped bypass for credentialed CLI content keywords\n\tExecApproval ExecApprovalCfg `json:\"execApproval\"` // exec command approval settings\n\tWebFetch WebFetchPolicyConfig `json:\"web_fetch\"` // domain policy for URL fetching\n\tBrowser BrowserToolConfig `json:\"browser\"`\n\tRateLimitPerHour int `json:\"rate_limit_per_hour,omitempty\"` // max tool executions per hour per session (0 = disabled)\n\tScrubCredentials *bool `json:\"scrub_credentials,omitempty\"` // auto-redact API keys/tokens in tool output (default true)\n\tMcpServers map[string]*MCPServerConfig `json:\"mcp_servers,omitempty\"` // external MCP server connections\n\tDocumentParser DocumentParserConfig `json:\"document_parser\"` // local-first document text extraction\n}\n\n// DocumentParserConfig controls local-first document text extraction in the\n// read_document tool. Local-first is OPT-IN (disabled by default): plain\n// pdftotext/pandoc output mis-orders tables/multi-column layouts and ignores\n// the analysis prompt, so the cloud vision path remains the default. When\n// enabled, PDF/DOCX are extracted via local binaries before any cloud LLM\n// call; missing binaries or empty output fall back to the provider chain.\n//\n// Changing these values requires a gateway restart — they are captured at tool\n// construction and not picked up by config hot-reload. (Binary availability,\n// however, is re-checked per call, so runtime installs are seen without a\n// restart.)\ntype DocumentParserConfig struct {\n\tLocalFirst *bool `json:\"local_first,omitempty\"` // nil => false (default OFF)\n\tMaxPages int `json:\"max_pages,omitempty\"` // 0 => 200\n\tTimeoutSec int `json:\"timeout_sec,omitempty\"` // 0 => 30s\n\tMinTextLen int `json:\"min_text_len,omitempty\"` // 0 => 16\n}\n\n// LocalFirstEnabled reports whether local-first extraction is turned on.\nfunc (c DocumentParserConfig) LocalFirstEnabled() bool {\n\treturn c.LocalFirst != nil && *c.LocalFirst\n}\n\n// CommandKeywordAllowlistRule scopes product/security vocabulary that may appear\n// in credentialed CLI content args without disabling command-path deny patterns.\ntype CommandKeywordAllowlistRule struct {\n\tID string `json:\"id,omitempty\"`\n\tCommand string `json:\"command\"`\n\tSubcommands []string `json:\"subcommands,omitempty\"`\n\tArgs []string `json:\"args,omitempty\"`\n\tArgPositions []int `json:\"argPositions,omitempty\"` // 0-based after matched subcommand\n\tKeywords []string `json:\"keywords,omitempty\"`\n\tReason string `json:\"reason,omitempty\"`\n\tEnabled *bool `json:\"enabled,omitempty\"` // nil means enabled\n}\n\n// MCPServerConfig configures a single external MCP server connection.\ntype MCPServerConfig struct {\n\tTransport string `json:\"transport\"` // \"stdio\", \"sse\", \"streamable-http\"\n\tCommand string `json:\"command,omitempty\"` // stdio: command to spawn\n\tArgs []string `json:\"args,omitempty\"` // stdio: command arguments\n\tEnv map[string]string `json:\"env,omitempty\"` // stdio: extra environment variables\n\tURL string `json:\"url,omitempty\"` // sse/http: server URL\n\tHeaders map[string]string `json:\"headers,omitempty\"` // sse/http: extra HTTP headers\n\tEnabled *bool `json:\"enabled,omitempty\"` // default true\n\tToolPrefix string `json:\"tool_prefix,omitempty\"` // prefix for tool names (avoids collisions)\n\tTimeoutSec int `json:\"timeout_sec,omitempty\"` // per-tool-call timeout in seconds (default 60)\n}\n\n// IsEnabled returns whether this MCP server is enabled (default true).\nfunc (c *MCPServerConfig) IsEnabled() bool {\n\treturn c.Enabled == nil || *c.Enabled\n}\n\n// ExecApprovalCfg configures command execution approval (matching TS exec-approval.ts).\ntype ExecApprovalCfg struct {\n\tSecurity string `json:\"security,omitempty\"` // \"deny\", \"allowlist\", \"full\" (default \"full\")\n\tAsk string `json:\"ask,omitempty\"` // \"off\", \"on-miss\", \"always\" (default \"off\")\n\tAllowlist []string `json:\"allowlist,omitempty\"` // glob patterns for allowed commands\n}\n\n// WebFetchPolicyConfig controls domain filtering for the web_fetch tool.\ntype WebFetchPolicyConfig struct {\n\tPolicy string `json:\"policy,omitempty\"` // \"allow_all\" (default), \"allowlist\"\n\tAllowedDomains []string `json:\"allowed_domains,omitempty\"` // e.g. [\"github.com\", \"*.example.com\"]\n\tBlockedDomains []string `json:\"blocked_domains,omitempty\"` // always checked regardless of policy\n}\n\n// BrowserToolConfig controls the browser automation tool.\ntype BrowserToolConfig struct {\n\tEnabled bool `json:\"enabled\"` // enable the browser tool (default false)\n\tHeadless bool `json:\"headless,omitempty\"` // run Chrome in headless mode (ignored when RemoteURL is set)\n\tRemoteURL string `json:\"remote_url,omitempty\"` // CDP endpoint for remote Chrome sidecar, e.g. \"ws://chrome:9222\"\n\tActionTimeoutMs int `json:\"action_timeout_ms,omitempty\"` // per-action timeout in ms (default 30000)\n\tIdleTimeoutMs int `json:\"idle_timeout_ms,omitempty\"` // idle page auto-close in ms (default 600000, 0=disabled)\n\tMaxPages int `json:\"max_pages,omitempty\"` // max open pages per tenant (default 5)\n\tCookieSyncEnabled bool `json:\"cookie_sync_enabled\"` // apply selected synced cookies to scoped browser sessions\n}\n\n// ToolPolicySpec defines a tool policy at any level (global, per-agent, per-provider).\ntype ToolPolicySpec struct {\n\tProfile string `json:\"profile,omitempty\"`\n\tAllow []string `json:\"allow,omitempty\"`\n\tDeny []string `json:\"deny,omitempty\"`\n\tAlsoAllow []string `json:\"alsoAllow,omitempty\"`\n\tByProvider map[string]*ToolPolicySpec `json:\"byProvider,omitempty\"`\n\tWait *WaitToolPolicy `json:\"wait,omitempty\"`\n\tToolCallPrefix string `json:\"toolCallPrefix,omitempty\"` // prefix to strip from model's tool call names before registry lookup\n}\n\n// WaitToolPolicy configures per-agent safety bounds for the wait tool.\ntype WaitToolPolicy struct {\n\tMinMs int `json:\"min_ms,omitempty\"`\n\tMaxMs int `json:\"max_ms,omitempty\"`\n}\n\n// SessionsConfig controls session behavior.\n// Matching TS src/config/sessions/types.ts + src/config/types.base.ts.\ntype SessionsConfig struct {\n\tScope string `json:\"scope,omitempty\"` // \"per-sender\" (default), \"global\"\n\tDmScope string `json:\"dm_scope,omitempty\"` // \"main\", \"per-peer\", \"per-channel-peer\" (default), \"per-account-channel-peer\"\n\tMainKey string `json:\"main_key,omitempty\"` // main session key suffix (default \"main\", used when dm_scope=\"main\")\n}\n\n// TtsConfig configures text-to-speech.\n// Matching TS src/config/types.tts.ts.\ntype TtsConfig struct {\n\tProvider string `json:\"provider,omitempty\"` // \"openai\", \"elevenlabs\", \"edge\", \"minimax\", \"gemini\"\n\tAuto string `json:\"auto,omitempty\"` // \"off\" (default), \"always\", \"inbound\", \"tagged\"\n\tMode string `json:\"mode,omitempty\"` // \"final\" (default), \"all\"\n\tMaxLength int `json:\"max_length,omitempty\"` // max text length before truncation (default 1500)\n\tTimeoutMs int `json:\"timeout_ms,omitempty\"` // API timeout in ms (default 30000)\n\tOpenAI TtsOpenAIConfig `json:\"openai\"`\n\tElevenLabs TtsElevenLabsConfig `json:\"elevenlabs\"`\n\tEdge TtsEdgeConfig `json:\"edge\"`\n\tMiniMax TtsMiniMaxConfig `json:\"minimax\"`\n\tGemini TtsGeminiConfig `json:\"gemini\"`\n}\n\n// TtsGeminiConfig configures the Google Gemini TTS provider.\ntype TtsGeminiConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"` // required; encrypted at rest\n\tAPIBase string `json:\"api_base,omitempty\"` // custom endpoint (optional; SSRF-gated)\n\tVoice string `json:\"voice,omitempty\"` // default \"Kore\"\n\tModel string `json:\"model,omitempty\"` // default \"gemini-2.5-flash-preview-tts\"\n\tSpeakers string `json:\"speakers,omitempty\"` // JSON-encoded []SpeakerVoice for multi-speaker mode\n}\n\n// TtsOpenAIConfig configures the OpenAI TTS provider.\ntype TtsOpenAIConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"`\n\tAPIBase string `json:\"api_base,omitempty\"` // custom endpoint URL\n\tModel string `json:\"model,omitempty\"` // default \"gpt-4o-mini-tts\"\n\tVoice string `json:\"voice,omitempty\"` // default \"alloy\"\n}\n\n// TtsElevenLabsConfig configures the ElevenLabs TTS provider.\ntype TtsElevenLabsConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"`\n\tBaseURL string `json:\"base_url,omitempty\"`\n\tVoiceID string `json:\"voice_id,omitempty\"` // default \"pMsXgVXv3BLzUgSXRplE\"\n\tModelID string `json:\"model_id,omitempty\"` // default \"eleven_multilingual_v2\"\n}\n\n// TtsEdgeConfig configures the Microsoft Edge TTS provider (free, no API key).\ntype TtsEdgeConfig struct {\n\tEnabled bool `json:\"enabled,omitempty\"`\n\tVoice string `json:\"voice,omitempty\"` // default \"en-US-MichelleNeural\"\n\tRate string `json:\"rate,omitempty\"` // speech rate, e.g. \"+0%\"\n}\n\n// TtsMiniMaxConfig configures the MiniMax TTS provider.\ntype TtsMiniMaxConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"`\n\tGroupID string `json:\"group_id,omitempty\"` // MiniMax GroupId (required)\n\tAPIBase string `json:\"api_base,omitempty\"` // default \"https://api.minimax.io/v1\"\n\tModel string `json:\"model,omitempty\"` // default \"speech-02-hd\"\n\tVoiceID string `json:\"voice_id,omitempty\"` // default \"Wise_Woman\"\n}\n\n// MergeChannelGroupQuotas merges per-group quota overrides from channel configs\n// (e.g., channels.telegram.groups[chatID].quota) into gateway.quota.groups.\n// This allows per-group quotas to be set at the channel level and picked up\n// by the QuotaChecker at runtime.\nfunc MergeChannelGroupQuotas(cfg *Config) {\n\tif cfg.Gateway.Quota == nil {\n\t\treturn\n\t}\n\tif cfg.Gateway.Quota.Groups == nil {\n\t\tcfg.Gateway.Quota.Groups = make(map[string]QuotaWindow)\n\t}\n\n\t// Telegram per-group quotas\n\tfor chatID, groupCfg := range cfg.Channels.Telegram.Groups {\n\t\tif groupCfg != nil && groupCfg.Quota != nil && !groupCfg.Quota.IsZero() {\n\t\t\tkey := \"group:telegram:\" + chatID\n\t\t\tcfg.Gateway.Quota.Groups[key] = *groupCfg.Quota\n\t\t}\n\t}\n}\n"}, "53ef912441103ce464fb0e5362ed60e5d6a96cfc": {"t": 1785759223.5058873, "n": 39023, "h": "40b43be41612767b", "c": "package config\n\n// ChatBehaviorConfig controls optional human-like channel delivery behavior.\n// Pointer fields allow per-channel overrides to inherit gateway defaults.\ntype ChatBehaviorConfig struct {\n\tEnabled *bool `json:\"enabled,omitempty\"`\n\tQuickAck *QuickAckConfig `json:\"quick_ack,omitempty\"`\n\tFinalSplit *FinalSplitConfig `json:\"final_split,omitempty\"`\n}\n\n// QuickAckConfig controls one short acknowledgement before longer non-streaming runs.\ntype QuickAckConfig struct {\n\tEnabled *bool `json:\"enabled,omitempty\"`\n\tMode *string `json:\"mode,omitempty\"`\n\tMinDelayMs *int `json:\"min_delay_ms,omitempty\"`\n\tTemplates []string `json:\"templates,omitempty\"`\n}\n\n// FinalSplitConfig controls semantic splitting of final channel replies.\ntype FinalSplitConfig struct {\n\tEnabled *bool `json:\"enabled,omitempty\"`\n\tMinChars *int `json:\"min_chars,omitempty\"`\n\tMaxMessages *int `json:\"max_messages,omitempty\"`\n\tDelayMs *int `json:\"delay_ms,omitempty\"`\n}\n\n// PendingCompactionConfig configures LLM-based compaction of pending group messages.\n// When a group accumulates more than Threshold pending messages, older messages are\n// summarized by an LLM and replaced with a compact summary, keeping KeepRecent raw messages.\ntype PendingCompactionConfig struct {\n\tThreshold int `json:\"threshold,omitempty\"` // trigger compaction when entries exceed this (default 200)\n\tKeepRecent int `json:\"keep_recent,omitempty\"` // keep this many recent raw messages after compaction (default 40)\n\tMaxTokens int `json:\"max_tokens,omitempty\"` // max output tokens for LLM summarization (default 4096)\n\tProvider string `json:\"provider,omitempty\"` // LLM provider name (e.g. \"openai\"); empty = use agent's provider\n\tModel string `json:\"model,omitempty\"` // model for summarization; empty = use agent's model\n}\n\n// ChannelsConfig contains per-channel configuration.\ntype ChannelsConfig struct {\n\tTelegram TelegramConfig `json:\"telegram\"`\n\tDiscord DiscordConfig `json:\"discord\"`\n\tSlack SlackConfig `json:\"slack\"`\n\tWhatsApp WhatsAppConfig `json:\"whatsapp\"`\n\tZalo ZaloConfig `json:\"zalo\"`\n\tZaloPersonal ZaloPersonalConfig `json:\"zalo_personal\"`\n\tFeishu FeishuConfig `json:\"feishu\"`\n\tPendingCompaction *PendingCompactionConfig `json:\"pending_compaction,omitempty\"` // global pending message compaction settings\n}\n\ntype TelegramConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tToken string `json:\"token\"`\n\tProxy string `json:\"proxy,omitempty\"`\n\tAPIServer string `json:\"api_server,omitempty\"` // custom Telegram Bot API server URL (e.g. \"http://localhost:8081\")\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in groups (default true)\n\tMentionMode string `json:\"mention_mode,omitempty\"` // \"strict\" (default) = only respond when mentioned; \"yield\" = respond unless another bot is mentioned\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tDMStream *bool `json:\"dm_stream,omitempty\"` // enable streaming for DMs (default false) — edits placeholder progressively\n\tGroupStream *bool `json:\"group_stream,omitempty\"` // enable streaming for groups (default false) — sends new message, edits progressively\n\tDraftTransport *bool `json:\"draft_transport,omitempty\"` // use sendMessageDraft for DM streaming (default true) — stealth preview, no notifications per edit\n\tReasoningDelivery string `json:\"reasoning_delivery,omitempty\"` // off, streaming_only, always_bubbles\n\tReasoningStream *bool `json:\"reasoning_stream,omitempty\"` // show reasoning as separate message when provider emits thinking events (default true)\n\tReactionLevel string `json:\"reaction_level,omitempty\"` // \"off\" (default), \"minimal\", \"full\" — status emoji reactions\n\tMediaMaxBytes int64 `json:\"media_max_bytes,omitempty\"` // max media download size in bytes (default 20MB)\n\tLinkPreview *bool `json:\"link_preview,omitempty\"` // enable URL previews in messages (default true)\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tChatBehavior *ChatBehaviorConfig `json:\"chat_behavior,omitempty\"` // override gateway chat behavior (nil = inherit)\n\tForceIPv4 bool `json:\"force_ipv4,omitempty\"` // force IPv4 for all Telegram API requests (use when IPv6 routing is broken)\n\n\t// Optional STT (Speech-to-Text) pipeline for voice/audio inbound messages.\n\t// When stt_proxy_url is set, audio/voice messages are transcribed before being forwarded to the agent.\n\tSTTProxyURL string `json:\"stt_proxy_url,omitempty\"` // base URL of the STT proxy service (e.g. \"https://stt.example.com\")\n\tSTTAPIKey string `json:\"stt_api_key,omitempty\"` // Bearer token for the STT proxy\n\tSTTTenantID string `json:\"stt_tenant_id,omitempty\"` // optional tenant/org identifier forwarded to the STT proxy\n\tSTTTimeoutSeconds int `json:\"stt_timeout_seconds,omitempty\"` // per-request timeout for STT calls (default 30s)\n\n\t// Optional audio-aware routing: when set, voice/audio inbound messages are routed to this\n\t// agent instead of the default channel agent. Requires the named agent to exist in the config.\n\tVoiceAgentID string `json:\"voice_agent_id,omitempty\"` // agent ID to route voice inbound to (e.g. \"speaking-agent\")\n\n\t// Audio guard: intercept technical errors in voice agent replies and replace with friendly fallbacks.\n\t// Only active when VoiceAgentID is set. Custom error markers replace built-in defaults when provided.\n\tAudioGuardFallbackTranscript string `json:\"audio_guard_fallback_transcript,omitempty\"` // fallback with %s for transcript (e.g. \"I heard: \\\"%s\\\". Try again!\")\n\tAudioGuardFallbackNoTranscript string `json:\"audio_guard_fallback_no_transcript,omitempty\"` // fallback when no transcript available\n\tAudioGuardErrorMarkers []string `json:\"audio_guard_error_markers,omitempty\"` // custom error detection markers (replaces defaults)\n\n\t// Per-group (and per-topic) overrides. Key is chat ID string (e.g. \"-100123456\") or \"*\" for wildcard.\n\t// TS ref: channels.telegram.groups in src/config/types.telegram.ts.\n\tGroups map[string]*TelegramGroupConfig `json:\"groups,omitempty\"`\n}\n\n// TelegramGroupConfig defines per-group overrides for a Telegram channel.\n// Matching TS TelegramGroupConfig in src/config/types.telegram.ts.\ntype TelegramGroupConfig struct {\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // override group policy for this group\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // override require_mention for this group\n\tMentionMode string `json:\"mention_mode,omitempty\"` // override mention_mode for this group\n\tAllowFrom FlexibleStringSlice `json:\"allow_from,omitempty\"` // override allow_from for this group\n\tEnabled *bool `json:\"enabled,omitempty\"` // disable bot for this group (default: true)\n\tSkills []string `json:\"skills,omitempty\"` // skill whitelist (nil = all, [] = none)\n\tTools []string `json:\"tools,omitempty\"` // tool allow list (nil = all, supports \"group:xxx\")\n\tSystemPrompt string `json:\"system_prompt,omitempty\"` // extra system prompt for this group\n\tTopics map[string]*TelegramTopicConfig `json:\"topics,omitempty\"` // per-topic overrides (key: thread ID string)\n\tQuota *QuotaWindow `json:\"quota,omitempty\"` // per-group quota override\n}\n\n// TelegramTopicConfig defines per-topic overrides within a Telegram group.\n// Matching TS TelegramTopicConfig in src/config/types.telegram.ts.\ntype TelegramTopicConfig struct {\n\tRequireMention *bool `json:\"require_mention,omitempty\"`\n\tMentionMode string `json:\"mention_mode,omitempty\"`\n\tGroupPolicy string `json:\"group_policy,omitempty\"`\n\tSkills []string `json:\"skills,omitempty\"`\n\tTools []string `json:\"tools,omitempty\"` // tool allow list (nil = inherit, supports \"group:xxx\")\n\tEnabled *bool `json:\"enabled,omitempty\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from,omitempty\"`\n\tSystemPrompt string `json:\"system_prompt,omitempty\"`\n}\n\ntype DiscordConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tToken string `json:\"token\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in groups (default true)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tChatBehavior *ChatBehaviorConfig `json:\"chat_behavior,omitempty\"` // override gateway chat behavior (nil = inherit)\n\tMediaMaxBytes int64 `json:\"media_max_bytes,omitempty\"` // max media download size (default 25MB)\n\tSTTProxyURL string `json:\"stt_proxy_url,omitempty\"`\n\tSTTAPIKey string `json:\"stt_api_key,omitempty\"`\n\tSTTTenantID string `json:\"stt_tenant_id,omitempty\"`\n\tSTTTimeoutSeconds int `json:\"stt_timeout_seconds,omitempty\"`\n\tVoiceAgentID string `json:\"voice_agent_id,omitempty\"`\n}\n\ntype SlackConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tBotToken string `json:\"bot_token\"` // xoxb-... (Bot User OAuth Token)\n\tAppToken string `json:\"app_token\"` // xapp-... (App-Level Token for Socket Mode)\n\tUserToken string `json:\"user_token,omitempty\"` // xoxp-... (Optional: custom bot identity)\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"pairing\", \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in channels (default true)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tDMStream *bool `json:\"dm_stream,omitempty\"` // enable streaming for DMs (default false)\n\tGroupStream *bool `json:\"group_stream,omitempty\"` // enable streaming for groups (default false)\n\tNativeStream *bool `json:\"native_stream,omitempty\"` // use Slack ChatStreamer API if available (default false)\n\tReactionLevel string `json:\"reaction_level,omitempty\"` // \"off\" (default), \"minimal\", \"full\"\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tChatBehavior *ChatBehaviorConfig `json:\"chat_behavior,omitempty\"` // override gateway chat behavior (nil = inherit)\n\tDebounceDelay *int `json:\"debounce_delay,omitempty\"` // ms delay before dispatching rapid messages (default 300, 0=disabled)\n\tThreadTTL *int `json:\"thread_ttl,omitempty\"` // hours before thread participation expires (default 24, 0=disabled — always require @mention)\n\tMediaMaxBytes int64 `json:\"media_max_bytes,omitempty\"` // max file download size in bytes (default 20MB)\n}\n\ntype WhatsAppConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tAuthDir string `json:\"auth_dir,omitempty\"` // optional: SQLite auth dir override (desktop)\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default for DB instances), \"open\", \"allowlist\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"pairing\" (default for DB instances), \"open\" (default for config), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // only respond in groups when bot is @mentioned (default false)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 200, 0=disabled)\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tChatBehavior *ChatBehaviorConfig `json:\"chat_behavior,omitempty\"` // override gateway chat behavior (nil = inherit)\n}\n\ntype ZaloConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tToken string `json:\"token\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tWebhookURL string `json:\"webhook_url,omitempty\"`\n\tWebhookSecret string `json:\"webhook_secret,omitempty\"`\n\tMediaMaxMB int `json:\"media_max_mb,omitempty\"` // default 5\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tChatBehavior *ChatBehaviorConfig `json:\"chat_behavior,omitempty\"` // override gateway chat behavior (nil = inherit)\n}\n\ntype ZaloPersonalConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in groups (default true)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tCredentialsPath string `json:\"credentials_path,omitempty\"` // path to saved cookies JSON\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tChatBehavior *ChatBehaviorConfig `json:\"chat_behavior,omitempty\"` // override gateway chat behavior (nil = inherit)\n}\n\ntype FeishuConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tAppID string `json:\"app_id\"`\n\tAppSecret string `json:\"app_secret\"`\n\tEncryptKey string `json:\"encrypt_key,omitempty\"`\n\tVerificationToken string `json:\"verification_token,omitempty\"`\n\tDomain string `json:\"domain,omitempty\"` // \"lark\" (default/global), \"feishu\" (China), or custom URL\n\tConnectionMode string `json:\"connection_mode,omitempty\"` // \"websocket\" (default), \"webhook\"\n\tWebhookPort int `json:\"webhook_port,omitempty\"` // default 3000\n\tWebhookPath string `json:\"webhook_path,omitempty\"` // default \"/feishu/events\"\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default)\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default)\n\tGroupAllowFrom FlexibleStringSlice `json:\"group_allow_from,omitempty\"`\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // default true (groups)\n\tTopicSessionMode string `json:\"topic_session_mode,omitempty\"` // \"disabled\" (default)\n\tTextChunkLimit int `json:\"text_chunk_limit,omitempty\"` // default 4000\n\tMediaMaxMB int `json:\"media_max_mb,omitempty\"` // default 30\n\tRenderMode string `json:\"render_mode,omitempty\"` // \"auto\", \"raw\", \"card\"\n\tStreaming *bool `json:\"streaming,omitempty\"` // default true\n\tReactionLevel string `json:\"reaction_level,omitempty\"` // \"off\" (default), \"minimal\", \"full\" — typing emoji reactions\n\tHistoryLimit int `json:\"history_limit,omitempty\"`\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tChatBehavior *ChatBehaviorConfig `json:\"chat_behavior,omitempty\"` // override gateway chat behavior (nil = inherit)\n\tSTTProxyURL string `json:\"stt_proxy_url,omitempty\"`\n\tSTTAPIKey string `json:\"stt_api_key,omitempty\"`\n\tSTTTenantID string `json:\"stt_tenant_id,omitempty\"`\n\tSTTTimeoutSeconds int `json:\"stt_timeout_seconds,omitempty\"`\n\tVoiceAgentID string `json:\"voice_agent_id,omitempty\"`\n}\n\n// ProvidersConfig maps provider name to its config.\ntype ProvidersConfig struct {\n\tAnthropic ProviderConfig `json:\"anthropic\"`\n\tOpenAI ProviderConfig `json:\"openai\"`\n\tOpenRouter ProviderConfig `json:\"openrouter\"`\n\tGroq ProviderConfig `json:\"groq\"`\n\tGemini ProviderConfig `json:\"gemini\"`\n\tDeepSeek ProviderConfig `json:\"deepseek\"`\n\tMistral ProviderConfig `json:\"mistral\"`\n\tXAI ProviderConfig `json:\"xai\"`\n\tMiniMax ProviderConfig `json:\"minimax\"`\n\tCohere ProviderConfig `json:\"cohere\"`\n\tPerplexity ProviderConfig `json:\"perplexity\"`\n\tDashScope ProviderConfig `json:\"dashscope\"`\n\tBailian ProviderConfig `json:\"bailian\"`\n\tZai ProviderConfig `json:\"zai\"`\n\tZaiCoding ProviderConfig `json:\"zai_coding\"`\n\tOllama OllamaConfig `json:\"ollama\"` // local Ollama instance (no API key needed)\n\tOllamaCloud ProviderConfig `json:\"ollama_cloud\"` // Ollama Cloud (API key required)\n\tClaudeCLI ClaudeCLIConfig `json:\"claude_cli\"`\n\tACP ACPConfig `json:\"acp\"`\n\tNovita ProviderConfig `json:\"novita\"` // Novita AI (OpenAI-compatible endpoint)\n\tBytePlus ProviderConfig `json:\"byteplus\"` // BytePlus ModelArk (Seed 2.0)\n\tBytePlusCoding ProviderConfig `json:\"byteplus_coding\"` // BytePlus ModelArk Coding Plan\n\tVertex VertexConfig `json:\"vertex\"` // Google Cloud Vertex AI (OAuth2 service account + ADC)\n}\n\n// VertexConfig configures Google Cloud Vertex AI.\n// Credentials precedence: APIKey (inline JSON) > CredentialsFile (path) > ADC (both empty).\n// ProjectID and Region are required; Model optional (defaults to google/gemini-2.0-flash-001).\ntype VertexConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"` // service account JSON inline (secret — never persist in config.json)\n\tCredentialsFile string `json:\"credentials_file,omitempty\"` // path to service account JSON file\n\tProjectID string `json:\"project_id,omitempty\"`\n\tRegion string `json:\"region,omitempty\"`\n\tModel string `json:\"model,omitempty\"`\n}\n\n// OllamaConfig configures a local (or self-hosted) Ollama instance.\n// No API key is required — Ollama accepts any Bearer token value.\ntype OllamaConfig struct {\n\tHost string `json:\"host\"` // Ollama server base URL, e.g. http://localhost:11434\n}\n\n// ClaudeCLIConfig configures the Claude CLI provider (uses subscription, not API key).\ntype ClaudeCLIConfig struct {\n\tCLIPath string `json:\"cli_path\" yaml:\"cli_path\"` // path to claude binary (default: \"claude\")\n\tModel string `json:\"model\" yaml:\"model\"` // default model alias (default: \"sonnet\")\n\tBaseWorkDir string `json:\"base_work_dir\" yaml:\"base_work_dir\"` // base dir for agent workspaces\n\tPermMode string `json:\"perm_mode\" yaml:\"perm_mode\"` // permission mode (default: \"bypassPermissions\")\n}\n\n// ACPConfig configures the ACP (Agent Client Protocol) provider.\n// Orchestrates any ACP-compatible coding agent (Claude Code, Codex CLI, Gemini CLI) as a subprocess.\ntype ACPConfig struct {\n\tBinary string `json:\"binary\"` // agent binary name or path (e.g. \"claude\", \"codex\")\n\tArgs []string `json:\"args\"` // extra spawn args\n\tModel string `json:\"model\"` // default model/agent name\n\tWorkDir string `json:\"work_dir\"` // base workspace dir\n\tIdleTTL string `json:\"idle_ttl\"` // process idle TTL (e.g. \"5m\")\n\tPermMode string `json:\"perm_mode\"` // \"approve-all\" (default), \"approve-reads\", \"deny-all\"\n}\n\ntype ProviderConfig struct {\n\tAPIKey string `json:\"api_key\"`\n\tAPIBase string `json:\"api_base,omitempty\"`\n}\n\n// APIBaseForType returns the config-level api_base for a given provider type.\n// Used as a fallback when DB providers have no api_base set.\nfunc (p *ProvidersConfig) APIBaseForType(providerType string) string {\n\tswitch providerType {\n\tcase \"anthropic_native\":\n\t\treturn p.Anthropic.APIBase\n\tcase \"openai\", \"openai_compat\":\n\t\treturn p.OpenAI.APIBase\n\tcase \"openrouter\":\n\t\treturn p.OpenRouter.APIBase\n\tcase \"groq\":\n\t\treturn p.Groq.APIBase\n\tcase \"deepseek\":\n\t\treturn p.DeepSeek.APIBase\n\tcase \"gemini_native\":\n\t\treturn p.Gemini.APIBase\n\tcase \"mistral\":\n\t\treturn p.Mistral.APIBase\n\tcase \"xai\":\n\t\treturn p.XAI.APIBase\n\tcase \"minimax_native\":\n\t\treturn p.MiniMax.APIBase\n\tcase \"cohere\":\n\t\treturn p.Cohere.APIBase\n\tcase \"perplexity\":\n\t\treturn p.Perplexity.APIBase\n\tcase \"dashscope\":\n\t\treturn p.DashScope.APIBase\n\tcase \"bailian\":\n\t\treturn p.Bailian.APIBase\n\tcase \"zai\":\n\t\treturn p.Zai.APIBase\n\tcase \"zai_coding\":\n\t\treturn p.ZaiCoding.APIBase\n\tcase \"ollama_cloud\":\n\t\treturn p.OllamaCloud.APIBase\n\tcase \"novita\":\n\t\treturn p.Novita.APIBase\n\tcase \"byteplus\":\n\t\treturn p.BytePlus.APIBase\n\tcase \"byteplus_coding\":\n\t\treturn p.BytePlusCoding.APIBase\n\tcase \"vertex\":\n\t\t// Computed from project+region at registration time; no config-level static base.\n\t\treturn \"\"\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\n// HasAnyProvider returns true if at least one provider has an API key or CLI configured.\nfunc (c *Config) HasAnyProvider() bool {\n\tp := c.Providers\n\treturn p.Anthropic.APIKey != \"\" ||\n\t\tp.OpenAI.APIKey != \"\" ||\n\t\tp.OpenRouter.APIKey != \"\" ||\n\t\tp.Groq.APIKey != \"\" ||\n\t\tp.Gemini.APIKey != \"\" ||\n\t\tp.DeepSeek.APIKey != \"\" ||\n\t\tp.Mistral.APIKey != \"\" ||\n\t\tp.XAI.APIKey != \"\" ||\n\t\tp.MiniMax.APIKey != \"\" ||\n\t\tp.Cohere.APIKey != \"\" ||\n\t\tp.Perplexity.APIKey != \"\" ||\n\t\tp.DashScope.APIKey != \"\" ||\n\t\tp.Bailian.APIKey != \"\" ||\n\t\tp.Zai.APIKey != \"\" ||\n\t\tp.ZaiCoding.APIKey != \"\" ||\n\t\tp.Ollama.Host != \"\" ||\n\t\tp.OllamaCloud.APIKey != \"\" ||\n\t\tp.ClaudeCLI.CLIPath != \"\" ||\n\t\tp.ACP.Binary != \"\" ||\n\t\tp.Novita.APIKey != \"\" ||\n\t\tp.BytePlus.APIKey != \"\" ||\n\t\tp.BytePlusCoding.APIKey != \"\" ||\n\t\t(p.Vertex.ProjectID != \"\" && p.Vertex.Region != \"\")\n}\n\n// QuotaWindow defines request limits per time window. Zero means unlimited.\ntype QuotaWindow struct {\n\tHour int `json:\"hour,omitempty\"` // max requests per hour (0 = unlimited)\n\tDay int `json:\"day,omitempty\"` // max requests per day (0 = unlimited)\n\tWeek int `json:\"week,omitempty\"` // max requests per week (0 = unlimited)\n}\n\n// IsZero returns true if no limits are set.\nfunc (w QuotaWindow) IsZero() bool { return w.Hour == 0 && w.Day == 0 && w.Week == 0 }\n\n// QuotaConfig configures per-user/group request quotas.\n// Config merge priority: Groups > Channels > Providers > Default.\ntype QuotaConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tDefault QuotaWindow `json:\"default\"`\n\tProviders map[string]QuotaWindow `json:\"providers,omitempty\"` // key = provider name (e.g. \"anthropic\")\n\tChannels map[string]QuotaWindow `json:\"channels,omitempty\"` // key = channel name (e.g. \"telegram\")\n\tGroups map[string]QuotaWindow `json:\"groups,omitempty\"` // key = userID (e.g. \"group:telegram:-100123\")\n}\n\n// GatewayConfig controls the gateway server.\ntype GatewayConfig struct {\n\tHost string `json:\"host\"`\n\tPort int `json:\"port\"`\n\tToken string `json:\"token,omitempty\"` // bearer token for WS/HTTP auth\n\tOwnerIDs []string `json:\"owner_ids,omitempty\"` // sender IDs considered \"owner\"\n\tAllowedOrigins []string `json:\"allowed_origins,omitempty\"` // WebSocket CORS whitelist (empty = allow all)\n\tMaxMessageChars int `json:\"max_message_chars,omitempty\"` // max user message characters (default 32000)\n\tRateLimitRPM int `json:\"rate_limit_rpm,omitempty\"` // rate limit: requests per minute per user (default 20, 0 = disabled)\n\tInjectionAction string `json:\"injection_action,omitempty\"` // prompt injection action: \"log\", \"warn\" (default), \"block\", \"off\"\n\tInboundDebounceMs int `json:\"inbound_debounce_ms,omitempty\"` // silence-window in ms that merges rapid channel/Web Chat messages from the same sender/session; 0 disables for text but media-bearing messages still honor a built-in media floor so multi-attachment bursts (#63) coalesce into a single agent run. Agents may override via per-agent agent_config.inbound_debounce_ms.\n\tQuota *QuotaConfig `json:\"quota,omitempty\"` // per-user/group request quotas\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // deliver intermediate text during tool iterations (default false)\n\tChatBehavior *ChatBehaviorConfig `json:\"chat_behavior,omitempty\"` // human-like channel delivery behavior (default disabled)\n\tToolStatus *bool `json:\"tool_status,omitempty\"` // show tool name in streaming preview during tool execution (default true)\n\tTaskRecoveryIntervalSec int `json:\"task_recovery_interval_sec,omitempty\"` // team task recovery ticker interval in seconds (default 300 = 5min)\n\tBackgroundProvider string `json:\"background_provider,omitempty\"` // LLM provider for background workers (vault enrichment, consolidation)\n\tBackgroundModel string `json:\"background_model,omitempty\"` // LLM model for background workers\n}\n\n// ToolsConfig controls tool availability, policy, and web search.\ntype ToolsConfig struct {\n\tProfile string `json:\"profile,omitempty\"` // global profile: \"minimal\", \"coding\", \"messaging\", \"full\"\n\tAllow []string `json:\"allow,omitempty\"` // global allow list (tool names or \"group:xxx\")\n\tDeny []string `json:\"deny,omitempty\"` // global deny list\n\tAlsoAllow []string `json:\"alsoAllow,omitempty\"` // additive: adds without removing existing\n\tByProvider map[string]*ToolPolicySpec `json:\"byProvider,omitempty\"` // per-provider overrides\n\tShellDenyGroups map[string]bool `json:\"shellDenyGroups,omitempty\"` // global shell deny-group toggles (group name -> denied); per-agent overrides win per-key\n\tCommandKeywordAllowlist []CommandKeywordAllowlistRule `json:\"commandKeywordAllowlist,omitempty\"` // scoped bypass for credentialed CLI content keywords\n\tExecApproval ExecApprovalCfg `json:\"execApproval\"` // exec command approval settings\n\tWebFetch WebFetchPolicyConfig `json:\"web_fetch\"` // domain policy for URL fetching\n\tBrowser BrowserToolConfig `json:\"browser\"`\n\tRateLimitPerHour int `json:\"rate_limit_per_hour,omitempty\"` // max tool executions per hour per session (0 = disabled)\n\tScrubCredentials *bool `json:\"scrub_credentials,omitempty\"` // auto-redact API keys/tokens in tool output (default true)\n\tMcpServers map[string]*MCPServerConfig `json:\"mcp_servers,omitempty\"` // external MCP server connections\n\tDocumentParser DocumentParserConfig `json:\"document_parser\"` // local-first document text extraction\n}\n\n// DocumentParserConfig controls local-first document text extraction in the\n// read_document tool. Local-first is OPT-IN (disabled by default): plain\n// pdftotext/pandoc output mis-orders tables/multi-column layouts and ignores\n// the analysis prompt, so the cloud vision path remains the default. When\n// enabled, PDF/DOCX are extracted via local binaries before any cloud LLM\n// call; missing binaries or empty output fall back to the provider chain.\n//\n// Changing these values requires a gateway restart — they are captured at tool\n// construction and not picked up by config hot-reload. (Binary availability,\n// however, is re-checked per call, so runtime installs are seen without a\n// restart.)\ntype DocumentParserConfig struct {\n\tLocalFirst *bool `json:\"local_first,omitempty\"` // nil => false (default OFF)\n\tMaxPages int `json:\"max_pages,omitempty\"` // 0 => 200\n\tTimeoutSec int `json:\"timeout_sec,omitempty\"` // 0 => 30s\n\tMinTextLen int `json:\"min_text_len,omitempty\"` // 0 => 16\n}\n\n// LocalFirstEnabled reports whether local-first extraction is turned on.\nfunc (c DocumentParserConfig) LocalFirstEnabled() bool {\n\treturn c.LocalFirst != nil && *c.LocalFirst\n}\n\n// CommandKeywordAllowlistRule scopes product/security vocabulary that may appear\n// in credentialed CLI content args without disabling command-path deny patterns.\ntype CommandKeywordAllowlistRule struct {\n\tID string `json:\"id,omitempty\"`\n\tCommand string `json:\"command\"`\n\tSubcommands []string `json:\"subcommands,omitempty\"`\n\tArgs []string `json:\"args,omitempty\"`\n\tArgPositions []int `json:\"argPositions,omitempty\"` // 0-based after matched subcommand\n\tKeywords []string `json:\"keywords,omitempty\"`\n\tReason string `json:\"reason,omitempty\"`\n\tEnabled *bool `json:\"enabled,omitempty\"` // nil means enabled\n}\n\n// MCPServerConfig configures a single external MCP server connection.\ntype MCPServerConfig struct {\n\tTransport string `json:\"transport\"` // \"stdio\", \"sse\", \"streamable-http\"\n\tCommand string `json:\"command,omitempty\"` // stdio: command to spawn\n\tArgs []string `json:\"args,omitempty\"` // stdio: command arguments\n\tEnv map[string]string `json:\"env,omitempty\"` // stdio: extra environment variables\n\tURL string `json:\"url,omitempty\"` // sse/http: server URL\n\tHeaders map[string]string `json:\"headers,omitempty\"` // sse/http: extra HTTP headers\n\tEnabled *bool `json:\"enabled,omitempty\"` // default true\n\tToolPrefix string `json:\"tool_prefix,omitempty\"` // prefix for tool names (avoids collisions)\n\tTimeoutSec int `json:\"timeout_sec,omitempty\"` // per-tool-call timeout in seconds (default 60)\n}\n\n// IsEnabled returns whether this MCP server is enabled (default true).\nfunc (c *MCPServerConfig) IsEnabled() bool {\n\treturn c.Enabled == nil || *c.Enabled\n}\n\n// ExecApprovalCfg configures command execution approval (matching TS exec-approval.ts).\ntype ExecApprovalCfg struct {\n\tSecurity string `json:\"security,omitempty\"` // \"deny\", \"allowlist\", \"full\" (default \"full\")\n\tAsk string `json:\"ask,omitempty\"` // \"off\", \"on-miss\", \"always\" (default \"off\")\n\tAllowlist []string `json:\"allowlist,omitempty\"` // glob patterns for allowed commands\n}\n\n// WebFetchPolicyConfig controls domain filtering for the web_fetch tool.\ntype WebFetchPolicyConfig struct {\n\tPolicy string `json:\"policy,omitempty\"` // \"allow_all\" (default), \"allowlist\"\n\tAllowedDomains []string `json:\"allowed_domains,omitempty\"` // e.g. [\"github.com\", \"*.example.com\"]\n\tBlockedDomains []string `json:\"blocked_domains,omitempty\"` // always checked regardless of policy\n}\n\n// BrowserToolConfig controls the browser automation tool.\ntype BrowserToolConfig struct {\n\tEnabled bool `json:\"enabled\"` // enable the browser tool (default false)\n\tHeadless bool `json:\"headless,omitempty\"` // run Chrome in headless mode (ignored when RemoteURL is set)\n\tRemoteURL string `json:\"remote_url,omitempty\"` // CDP endpoint for remote Chrome sidecar, e.g. \"ws://chrome:9222\"\n\tActionTimeoutMs int `json:\"action_timeout_ms,omitempty\"` // per-action timeout in ms (default 30000)\n\tIdleTimeoutMs int `json:\"idle_timeout_ms,omitempty\"` // idle page auto-close in ms (default 600000, 0=disabled)\n\tMaxPages int `json:\"max_pages,omitempty\"` // max open pages per tenant (default 5)\n\tCookieSyncEnabled bool `json:\"cookie_sync_enabled\"` // apply selected synced cookies to scoped browser sessions\n}\n\n// ToolPolicySpec defines a tool policy at any level (global, per-agent, per-provider).\ntype ToolPolicySpec struct {\n\tProfile string `json:\"profile,omitempty\"`\n\tAllow []string `json:\"allow,omitempty\"`\n\tDeny []string `json:\"deny,omitempty\"`\n\tAlsoAllow []string `json:\"alsoAllow,omitempty\"`\n\tByProvider map[string]*ToolPolicySpec `json:\"byProvider,omitempty\"`\n\tWait *WaitToolPolicy `json:\"wait,omitempty\"`\n\tToolCallPrefix string `json:\"toolCallPrefix,omitempty\"` // prefix to strip from model's tool call names before registry lookup\n}\n\n// WaitToolPolicy configures per-agent safety bounds for the wait tool.\ntype WaitToolPolicy struct {\n\tMinMs int `json:\"min_ms,omitempty\"`\n\tMaxMs int `json:\"max_ms,omitempty\"`\n}\n\n// SessionsConfig controls session behavior.\n// Matching TS src/config/sessions/types.ts + src/config/types.base.ts.\ntype SessionsConfig struct {\n\tScope string `json:\"scope,omitempty\"` // \"per-sender\" (default), \"global\"\n\tDmScope string `json:\"dm_scope,omitempty\"` // \"main\", \"per-peer\", \"per-channel-peer\" (default), \"per-account-channel-peer\"\n\tMainKey string `json:\"main_key,omitempty\"` // main session key suffix (default \"main\", used when dm_scope=\"main\")\n}\n\n// TtsConfig configures text-to-speech.\n// Matching TS src/config/types.tts.ts.\ntype TtsConfig struct {\n\tProvider string `json:\"provider,omitempty\"` // \"openai\", \"elevenlabs\", \"edge\", \"minimax\", \"gemini\"\n\tAuto string `json:\"auto,omitempty\"` // \"off\" (default), \"always\", \"inbound\", \"tagged\"\n\tMode string `json:\"mode,omitempty\"` // \"final\" (default), \"all\"\n\tMaxLength int `json:\"max_length,omitempty\"` // max text length before truncation (default 1500)\n\tTimeoutMs int `json:\"timeout_ms,omitempty\"` // API timeout in ms (default 30000)\n\tOpenAI TtsOpenAIConfig `json:\"openai\"`\n\tElevenLabs TtsElevenLabsConfig `json:\"elevenlabs\"`\n\tEdge TtsEdgeConfig `json:\"edge\"`\n\tMiniMax TtsMiniMaxConfig `json:\"minimax\"`\n\tGemini TtsGeminiConfig `json:\"gemini\"`\n}\n\n// TtsGeminiConfig configures the Google Gemini TTS provider.\ntype TtsGeminiConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"` // required; encrypted at rest\n\tAPIBase string `json:\"api_base,omitempty\"` // custom endpoint (optional; SSRF-gated)\n\tVoice string `json:\"voice,omitempty\"` // default \"Kore\"\n\tModel string `json:\"model,omitempty\"` // default \"gemini-2.5-flash-preview-tts\"\n\tSpeakers string `json:\"speakers,omitempty\"` // JSON-encoded []SpeakerVoice for multi-speaker mode\n}\n\n// TtsOpenAIConfig configures the OpenAI TTS provider.\ntype TtsOpenAIConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"`\n\tAPIBase string `json:\"api_base,omitempty\"` // custom endpoint URL\n\tModel string `json:\"model,omitempty\"` // default \"gpt-4o-mini-tts\"\n\tVoice string `json:\"voice,omitempty\"` // default \"alloy\"\n}\n\n// TtsElevenLabsConfig configures the ElevenLabs TTS provider.\ntype TtsElevenLabsConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"`\n\tBaseURL string `json:\"base_url,omitempty\"`\n\tVoiceID string `json:\"voice_id,omitempty\"` // default \"pMsXgVXv3BLzUgSXRplE\"\n\tModelID string `json:\"model_id,omitempty\"` // default \"eleven_multilingual_v2\"\n}\n\n// TtsEdgeConfig configures the Microsoft Edge TTS provider (free, no API key).\ntype TtsEdgeConfig struct {\n\tEnabled bool `json:\"enabled,omitempty\"`\n\tVoice string `json:\"voice,omitempty\"` // default \"en-US-MichelleNeural\"\n\tRate string `json:\"rate,omitempty\"` // speech rate, e.g. \"+0%\"\n}\n\n// TtsMiniMaxConfig configures the MiniMax TTS provider.\ntype TtsMiniMaxConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"`\n\tGroupID string `json:\"group_id,omitempty\"` // MiniMax GroupId (required)\n\tAPIBase string `json:\"api_base,omitempty\"` // default \"https://api.minimax.io/v1\"\n\tModel string `json:\"model,omitempty\"` // default \"speech-02-hd\"\n\tVoiceID string `json:\"voice_id,omitempty\"` // default \"Wise_Woman\"\n}\n\n// MergeChannelGroupQuotas merges per-group quota overrides from channel configs\n// (e.g., channels.telegram.groups[chatID].quota) into gateway.quota.groups.\n// This allows per-group quotas to be set at the channel level and picked up\n// by the QuotaChecker at runtime.\nfunc MergeChannelGroupQuotas(cfg *Config) {\n\tif cfg.Gateway.Quota == nil {\n\t\treturn\n\t}\n\tif cfg.Gateway.Quota.Groups == nil {\n\t\tcfg.Gateway.Quota.Groups = make(map[string]QuotaWindow)\n\t}\n\n\t// Telegram per-group quotas\n\tfor chatID, groupCfg := range cfg.Channels.Telegram.Groups {\n\t\tif groupCfg != nil && groupCfg.Quota != nil && !groupCfg.Quota.IsZero() {\n\t\t\tkey := \"group:telegram:\" + chatID\n\t\t\tcfg.Gateway.Quota.Groups[key] = *groupCfg.Quota\n\t\t}\n\t}\n}\n"}, "32273f04d3959158be7a47f08bdfdfc8331d2558": {"t": 1785759225.8111048, "n": 38837, "h": "59482c9c0c52c8f4", "c": "package config\n\n// ChatBehaviorConfig controls optional human-like channel delivery behavior.\n// Pointer fields allow per-channel overrides to inherit gateway defaults.\ntype ChatBehaviorConfig struct {\n\tEnabled *bool `json:\"enabled,omitempty\"`\n\tQuickAck *QuickAckConfig `json:\"quick_ack,omitempty\"`\n\tFinalSplit *FinalSplitConfig `json:\"final_split,omitempty\"`\n}\n\n// QuickAckConfig controls one short acknowledgement before longer non-streaming runs.\ntype QuickAckConfig struct {\n\tEnabled *bool `json:\"enabled,omitempty\"`\n\tMode *string `json:\"mode,omitempty\"`\n\tMinDelayMs *int `json:\"min_delay_ms,omitempty\"`\n\tTemplates []string `json:\"templates,omitempty\"`\n}\n\n// FinalSplitConfig controls semantic splitting of final channel replies.\ntype FinalSplitConfig struct {\n\tEnabled *bool `json:\"enabled,omitempty\"`\n\tMinChars *int `json:\"min_chars,omitempty\"`\n\tMaxMessages *int `json:\"max_messages,omitempty\"`\n\tDelayMs *int `json:\"delay_ms,omitempty\"`\n}\n\n// PendingCompactionConfig configures LLM-based compaction of pending group messages.\n// When a group accumulates more than Threshold pending messages, older messages are\n// summarized by an LLM and replaced with a compact summary, keeping KeepRecent raw messages.\ntype PendingCompactionConfig struct {\n\tThreshold int `json:\"threshold,omitempty\"` // trigger compaction when entries exceed this (default 200)\n\tKeepRecent int `json:\"keep_recent,omitempty\"` // keep this many recent raw messages after compaction (default 40)\n\tMaxTokens int `json:\"max_tokens,omitempty\"` // max output tokens for LLM summarization (default 4096)\n\tProvider string `json:\"provider,omitempty\"` // LLM provider name (e.g. \"openai\"); empty = use agent's provider\n\tModel string `json:\"model,omitempty\"` // model for summarization; empty = use agent's model\n}\n\n// ChannelsConfig contains per-channel configuration.\ntype ChannelsConfig struct {\n\tTelegram TelegramConfig `json:\"telegram\"`\n\tDiscord DiscordConfig `json:\"discord\"`\n\tSlack SlackConfig `json:\"slack\"`\n\tWhatsApp WhatsAppConfig `json:\"whatsapp\"`\n\tZalo ZaloConfig `json:\"zalo\"`\n\tZaloPersonal ZaloPersonalConfig `json:\"zalo_personal\"`\n\tFeishu FeishuConfig `json:\"feishu\"`\n\tPendingCompaction *PendingCompactionConfig `json:\"pending_compaction,omitempty\"` // global pending message compaction settings\n}\n\ntype TelegramConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tToken string `json:\"token\"`\n\tProxy string `json:\"proxy,omitempty\"`\n\tAPIServer string `json:\"api_server,omitempty\"` // custom Telegram Bot API server URL (e.g. \"http://localhost:8081\")\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in groups (default true)\n\tMentionMode string `json:\"mention_mode,omitempty\"` // \"strict\" (default) = only respond when mentioned; \"yield\" = respond unless another bot is mentioned\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tDMStream *bool `json:\"dm_stream,omitempty\"` // enable streaming for DMs (default false) — edits placeholder progressively\n\tGroupStream *bool `json:\"group_stream,omitempty\"` // enable streaming for groups (default false) — sends new message, edits progressively\n\tDraftTransport *bool `json:\"draft_transport,omitempty\"` // use sendMessageDraft for DM streaming (default true) — stealth preview, no notifications per edit\n\tReasoningStream *bool `json:\"reasoning_stream,omitempty\"` // show reasoning as separate message when provider emits thinking events (default true)\n\tReactionLevel string `json:\"reaction_level,omitempty\"` // \"off\" (default), \"minimal\", \"full\" — status emoji reactions\n\tMediaMaxBytes int64 `json:\"media_max_bytes,omitempty\"` // max media download size in bytes (default 20MB)\n\tLinkPreview *bool `json:\"link_preview,omitempty\"` // enable URL previews in messages (default true)\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tChatBehavior *ChatBehaviorConfig `json:\"chat_behavior,omitempty\"` // override gateway chat behavior (nil = inherit)\n\tForceIPv4 bool `json:\"force_ipv4,omitempty\"` // force IPv4 for all Telegram API requests (use when IPv6 routing is broken)\n\n\t// Optional STT (Speech-to-Text) pipeline for voice/audio inbound messages.\n\t// When stt_proxy_url is set, audio/voice messages are transcribed before being forwarded to the agent.\n\tSTTProxyURL string `json:\"stt_proxy_url,omitempty\"` // base URL of the STT proxy service (e.g. \"https://stt.example.com\")\n\tSTTAPIKey string `json:\"stt_api_key,omitempty\"` // Bearer token for the STT proxy\n\tSTTTenantID string `json:\"stt_tenant_id,omitempty\"` // optional tenant/org identifier forwarded to the STT proxy\n\tSTTTimeoutSeconds int `json:\"stt_timeout_seconds,omitempty\"` // per-request timeout for STT calls (default 30s)\n\n\t// Optional audio-aware routing: when set, voice/audio inbound messages are routed to this\n\t// agent instead of the default channel agent. Requires the named agent to exist in the config.\n\tVoiceAgentID string `json:\"voice_agent_id,omitempty\"` // agent ID to route voice inbound to (e.g. \"speaking-agent\")\n\n\t// Audio guard: intercept technical errors in voice agent replies and replace with friendly fallbacks.\n\t// Only active when VoiceAgentID is set. Custom error markers replace built-in defaults when provided.\n\tAudioGuardFallbackTranscript string `json:\"audio_guard_fallback_transcript,omitempty\"` // fallback with %s for transcript (e.g. \"I heard: \\\"%s\\\". Try again!\")\n\tAudioGuardFallbackNoTranscript string `json:\"audio_guard_fallback_no_transcript,omitempty\"` // fallback when no transcript available\n\tAudioGuardErrorMarkers []string `json:\"audio_guard_error_markers,omitempty\"` // custom error detection markers (replaces defaults)\n\n\t// Per-group (and per-topic) overrides. Key is chat ID string (e.g. \"-100123456\") or \"*\" for wildcard.\n\t// TS ref: channels.telegram.groups in src/config/types.telegram.ts.\n\tGroups map[string]*TelegramGroupConfig `json:\"groups,omitempty\"`\n}\n\n// TelegramGroupConfig defines per-group overrides for a Telegram channel.\n// Matching TS TelegramGroupConfig in src/config/types.telegram.ts.\ntype TelegramGroupConfig struct {\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // override group policy for this group\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // override require_mention for this group\n\tMentionMode string `json:\"mention_mode,omitempty\"` // override mention_mode for this group\n\tAllowFrom FlexibleStringSlice `json:\"allow_from,omitempty\"` // override allow_from for this group\n\tEnabled *bool `json:\"enabled,omitempty\"` // disable bot for this group (default: true)\n\tSkills []string `json:\"skills,omitempty\"` // skill whitelist (nil = all, [] = none)\n\tTools []string `json:\"tools,omitempty\"` // tool allow list (nil = all, supports \"group:xxx\")\n\tSystemPrompt string `json:\"system_prompt,omitempty\"` // extra system prompt for this group\n\tTopics map[string]*TelegramTopicConfig `json:\"topics,omitempty\"` // per-topic overrides (key: thread ID string)\n\tQuota *QuotaWindow `json:\"quota,omitempty\"` // per-group quota override\n}\n\n// TelegramTopicConfig defines per-topic overrides within a Telegram group.\n// Matching TS TelegramTopicConfig in src/config/types.telegram.ts.\ntype TelegramTopicConfig struct {\n\tRequireMention *bool `json:\"require_mention,omitempty\"`\n\tMentionMode string `json:\"mention_mode,omitempty\"`\n\tGroupPolicy string `json:\"group_policy,omitempty\"`\n\tSkills []string `json:\"skills,omitempty\"`\n\tTools []string `json:\"tools,omitempty\"` // tool allow list (nil = inherit, supports \"group:xxx\")\n\tEnabled *bool `json:\"enabled,omitempty\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from,omitempty\"`\n\tSystemPrompt string `json:\"system_prompt,omitempty\"`\n}\n\ntype DiscordConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tToken string `json:\"token\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in groups (default true)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tChatBehavior *ChatBehaviorConfig `json:\"chat_behavior,omitempty\"` // override gateway chat behavior (nil = inherit)\n\tMediaMaxBytes int64 `json:\"media_max_bytes,omitempty\"` // max media download size (default 25MB)\n\tSTTProxyURL string `json:\"stt_proxy_url,omitempty\"`\n\tSTTAPIKey string `json:\"stt_api_key,omitempty\"`\n\tSTTTenantID string `json:\"stt_tenant_id,omitempty\"`\n\tSTTTimeoutSeconds int `json:\"stt_timeout_seconds,omitempty\"`\n\tVoiceAgentID string `json:\"voice_agent_id,omitempty\"`\n}\n\ntype SlackConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tBotToken string `json:\"bot_token\"` // xoxb-... (Bot User OAuth Token)\n\tAppToken string `json:\"app_token\"` // xapp-... (App-Level Token for Socket Mode)\n\tUserToken string `json:\"user_token,omitempty\"` // xoxp-... (Optional: custom bot identity)\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"pairing\", \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in channels (default true)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tDMStream *bool `json:\"dm_stream,omitempty\"` // enable streaming for DMs (default false)\n\tGroupStream *bool `json:\"group_stream,omitempty\"` // enable streaming for groups (default false)\n\tNativeStream *bool `json:\"native_stream,omitempty\"` // use Slack ChatStreamer API if available (default false)\n\tReactionLevel string `json:\"reaction_level,omitempty\"` // \"off\" (default), \"minimal\", \"full\"\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tChatBehavior *ChatBehaviorConfig `json:\"chat_behavior,omitempty\"` // override gateway chat behavior (nil = inherit)\n\tDebounceDelay *int `json:\"debounce_delay,omitempty\"` // ms delay before dispatching rapid messages (default 300, 0=disabled)\n\tThreadTTL *int `json:\"thread_ttl,omitempty\"` // hours before thread participation expires (default 24, 0=disabled — always require @mention)\n\tMediaMaxBytes int64 `json:\"media_max_bytes,omitempty\"` // max file download size in bytes (default 20MB)\n}\n\ntype WhatsAppConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tAuthDir string `json:\"auth_dir,omitempty\"` // optional: SQLite auth dir override (desktop)\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default for DB instances), \"open\", \"allowlist\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"pairing\" (default for DB instances), \"open\" (default for config), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // only respond in groups when bot is @mentioned (default false)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 200, 0=disabled)\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tChatBehavior *ChatBehaviorConfig `json:\"chat_behavior,omitempty\"` // override gateway chat behavior (nil = inherit)\n}\n\ntype ZaloConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tToken string `json:\"token\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tWebhookURL string `json:\"webhook_url,omitempty\"`\n\tWebhookSecret string `json:\"webhook_secret,omitempty\"`\n\tMediaMaxMB int `json:\"media_max_mb,omitempty\"` // default 5\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tChatBehavior *ChatBehaviorConfig `json:\"chat_behavior,omitempty\"` // override gateway chat behavior (nil = inherit)\n}\n\ntype ZaloPersonalConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in groups (default true)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tCredentialsPath string `json:\"credentials_path,omitempty\"` // path to saved cookies JSON\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tChatBehavior *ChatBehaviorConfig `json:\"chat_behavior,omitempty\"` // override gateway chat behavior (nil = inherit)\n}\n\ntype FeishuConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tAppID string `json:\"app_id\"`\n\tAppSecret string `json:\"app_secret\"`\n\tEncryptKey string `json:\"encrypt_key,omitempty\"`\n\tVerificationToken string `json:\"verification_token,omitempty\"`\n\tDomain string `json:\"domain,omitempty\"` // \"lark\" (default/global), \"feishu\" (China), or custom URL\n\tConnectionMode string `json:\"connection_mode,omitempty\"` // \"websocket\" (default), \"webhook\"\n\tWebhookPort int `json:\"webhook_port,omitempty\"` // default 3000\n\tWebhookPath string `json:\"webhook_path,omitempty\"` // default \"/feishu/events\"\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default)\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default)\n\tGroupAllowFrom FlexibleStringSlice `json:\"group_allow_from,omitempty\"`\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // default true (groups)\n\tTopicSessionMode string `json:\"topic_session_mode,omitempty\"` // \"disabled\" (default)\n\tTextChunkLimit int `json:\"text_chunk_limit,omitempty\"` // default 4000\n\tMediaMaxMB int `json:\"media_max_mb,omitempty\"` // default 30\n\tRenderMode string `json:\"render_mode,omitempty\"` // \"auto\", \"raw\", \"card\"\n\tStreaming *bool `json:\"streaming,omitempty\"` // default true\n\tReactionLevel string `json:\"reaction_level,omitempty\"` // \"off\" (default), \"minimal\", \"full\" — typing emoji reactions\n\tHistoryLimit int `json:\"history_limit,omitempty\"`\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tChatBehavior *ChatBehaviorConfig `json:\"chat_behavior,omitempty\"` // override gateway chat behavior (nil = inherit)\n\tSTTProxyURL string `json:\"stt_proxy_url,omitempty\"`\n\tSTTAPIKey string `json:\"stt_api_key,omitempty\"`\n\tSTTTenantID string `json:\"stt_tenant_id,omitempty\"`\n\tSTTTimeoutSeconds int `json:\"stt_timeout_seconds,omitempty\"`\n\tVoiceAgentID string `json:\"voice_agent_id,omitempty\"`\n}\n\n// ProvidersConfig maps provider name to its config.\ntype ProvidersConfig struct {\n\tAnthropic ProviderConfig `json:\"anthropic\"`\n\tOpenAI ProviderConfig `json:\"openai\"`\n\tOpenRouter ProviderConfig `json:\"openrouter\"`\n\tGroq ProviderConfig `json:\"groq\"`\n\tGemini ProviderConfig `json:\"gemini\"`\n\tDeepSeek ProviderConfig `json:\"deepseek\"`\n\tMistral ProviderConfig `json:\"mistral\"`\n\tXAI ProviderConfig `json:\"xai\"`\n\tMiniMax ProviderConfig `json:\"minimax\"`\n\tCohere ProviderConfig `json:\"cohere\"`\n\tPerplexity ProviderConfig `json:\"perplexity\"`\n\tDashScope ProviderConfig `json:\"dashscope\"`\n\tBailian ProviderConfig `json:\"bailian\"`\n\tZai ProviderConfig `json:\"zai\"`\n\tZaiCoding ProviderConfig `json:\"zai_coding\"`\n\tOllama OllamaConfig `json:\"ollama\"` // local Ollama instance (no API key needed)\n\tOllamaCloud ProviderConfig `json:\"ollama_cloud\"` // Ollama Cloud (API key required)\n\tClaudeCLI ClaudeCLIConfig `json:\"claude_cli\"`\n\tACP ACPConfig `json:\"acp\"`\n\tNovita ProviderConfig `json:\"novita\"` // Novita AI (OpenAI-compatible endpoint)\n\tBytePlus ProviderConfig `json:\"byteplus\"` // BytePlus ModelArk (Seed 2.0)\n\tBytePlusCoding ProviderConfig `json:\"byteplus_coding\"` // BytePlus ModelArk Coding Plan\n\tVertex VertexConfig `json:\"vertex\"` // Google Cloud Vertex AI (OAuth2 service account + ADC)\n}\n\n// VertexConfig configures Google Cloud Vertex AI.\n// Credentials precedence: APIKey (inline JSON) > CredentialsFile (path) > ADC (both empty).\n// ProjectID and Region are required; Model optional (defaults to google/gemini-2.0-flash-001).\ntype VertexConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"` // service account JSON inline (secret — never persist in config.json)\n\tCredentialsFile string `json:\"credentials_file,omitempty\"` // path to service account JSON file\n\tProjectID string `json:\"project_id,omitempty\"`\n\tRegion string `json:\"region,omitempty\"`\n\tModel string `json:\"model,omitempty\"`\n}\n\n// OllamaConfig configures a local (or self-hosted) Ollama instance.\n// No API key is required — Ollama accepts any Bearer token value.\ntype OllamaConfig struct {\n\tHost string `json:\"host\"` // Ollama server base URL, e.g. http://localhost:11434\n}\n\n// ClaudeCLIConfig configures the Claude CLI provider (uses subscription, not API key).\ntype ClaudeCLIConfig struct {\n\tCLIPath string `json:\"cli_path\" yaml:\"cli_path\"` // path to claude binary (default: \"claude\")\n\tModel string `json:\"model\" yaml:\"model\"` // default model alias (default: \"sonnet\")\n\tBaseWorkDir string `json:\"base_work_dir\" yaml:\"base_work_dir\"` // base dir for agent workspaces\n\tPermMode string `json:\"perm_mode\" yaml:\"perm_mode\"` // permission mode (default: \"bypassPermissions\")\n}\n\n// ACPConfig configures the ACP (Agent Client Protocol) provider.\n// Orchestrates any ACP-compatible coding agent (Claude Code, Codex CLI, Gemini CLI) as a subprocess.\ntype ACPConfig struct {\n\tBinary string `json:\"binary\"` // agent binary name or path (e.g. \"claude\", \"codex\")\n\tArgs []string `json:\"args\"` // extra spawn args\n\tModel string `json:\"model\"` // default model/agent name\n\tWorkDir string `json:\"work_dir\"` // base workspace dir\n\tIdleTTL string `json:\"idle_ttl\"` // process idle TTL (e.g. \"5m\")\n\tPermMode string `json:\"perm_mode\"` // \"approve-all\" (default), \"approve-reads\", \"deny-all\"\n}\n\ntype ProviderConfig struct {\n\tAPIKey string `json:\"api_key\"`\n\tAPIBase string `json:\"api_base,omitempty\"`\n}\n\n// APIBaseForType returns the config-level api_base for a given provider type.\n// Used as a fallback when DB providers have no api_base set.\nfunc (p *ProvidersConfig) APIBaseForType(providerType string) string {\n\tswitch providerType {\n\tcase \"anthropic_native\":\n\t\treturn p.Anthropic.APIBase\n\tcase \"openai\", \"openai_compat\":\n\t\treturn p.OpenAI.APIBase\n\tcase \"openrouter\":\n\t\treturn p.OpenRouter.APIBase\n\tcase \"groq\":\n\t\treturn p.Groq.APIBase\n\tcase \"deepseek\":\n\t\treturn p.DeepSeek.APIBase\n\tcase \"gemini_native\":\n\t\treturn p.Gemini.APIBase\n\tcase \"mistral\":\n\t\treturn p.Mistral.APIBase\n\tcase \"xai\":\n\t\treturn p.XAI.APIBase\n\tcase \"minimax_native\":\n\t\treturn p.MiniMax.APIBase\n\tcase \"cohere\":\n\t\treturn p.Cohere.APIBase\n\tcase \"perplexity\":\n\t\treturn p.Perplexity.APIBase\n\tcase \"dashscope\":\n\t\treturn p.DashScope.APIBase\n\tcase \"bailian\":\n\t\treturn p.Bailian.APIBase\n\tcase \"zai\":\n\t\treturn p.Zai.APIBase\n\tcase \"zai_coding\":\n\t\treturn p.ZaiCoding.APIBase\n\tcase \"ollama_cloud\":\n\t\treturn p.OllamaCloud.APIBase\n\tcase \"novita\":\n\t\treturn p.Novita.APIBase\n\tcase \"byteplus\":\n\t\treturn p.BytePlus.APIBase\n\tcase \"byteplus_coding\":\n\t\treturn p.BytePlusCoding.APIBase\n\tcase \"vertex\":\n\t\t// Computed from project+region at registration time; no config-level static base.\n\t\treturn \"\"\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\n// HasAnyProvider returns true if at least one provider has an API key or CLI configured.\nfunc (c *Config) HasAnyProvider() bool {\n\tp := c.Providers\n\treturn p.Anthropic.APIKey != \"\" ||\n\t\tp.OpenAI.APIKey != \"\" ||\n\t\tp.OpenRouter.APIKey != \"\" ||\n\t\tp.Groq.APIKey != \"\" ||\n\t\tp.Gemini.APIKey != \"\" ||\n\t\tp.DeepSeek.APIKey != \"\" ||\n\t\tp.Mistral.APIKey != \"\" ||\n\t\tp.XAI.APIKey != \"\" ||\n\t\tp.MiniMax.APIKey != \"\" ||\n\t\tp.Cohere.APIKey != \"\" ||\n\t\tp.Perplexity.APIKey != \"\" ||\n\t\tp.DashScope.APIKey != \"\" ||\n\t\tp.Bailian.APIKey != \"\" ||\n\t\tp.Zai.APIKey != \"\" ||\n\t\tp.ZaiCoding.APIKey != \"\" ||\n\t\tp.Ollama.Host != \"\" ||\n\t\tp.OllamaCloud.APIKey != \"\" ||\n\t\tp.ClaudeCLI.CLIPath != \"\" ||\n\t\tp.ACP.Binary != \"\" ||\n\t\tp.Novita.APIKey != \"\" ||\n\t\tp.BytePlus.APIKey != \"\" ||\n\t\tp.BytePlusCoding.APIKey != \"\" ||\n\t\t(p.Vertex.ProjectID != \"\" && p.Vertex.Region != \"\")\n}\n\n// QuotaWindow defines request limits per time window. Zero means unlimited.\ntype QuotaWindow struct {\n\tHour int `json:\"hour,omitempty\"` // max requests per hour (0 = unlimited)\n\tDay int `json:\"day,omitempty\"` // max requests per day (0 = unlimited)\n\tWeek int `json:\"week,omitempty\"` // max requests per week (0 = unlimited)\n}\n\n// IsZero returns true if no limits are set.\nfunc (w QuotaWindow) IsZero() bool { return w.Hour == 0 && w.Day == 0 && w.Week == 0 }\n\n// QuotaConfig configures per-user/group request quotas.\n// Config merge priority: Groups > Channels > Providers > Default.\ntype QuotaConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tDefault QuotaWindow `json:\"default\"`\n\tProviders map[string]QuotaWindow `json:\"providers,omitempty\"` // key = provider name (e.g. \"anthropic\")\n\tChannels map[string]QuotaWindow `json:\"channels,omitempty\"` // key = channel name (e.g. \"telegram\")\n\tGroups map[string]QuotaWindow `json:\"groups,omitempty\"` // key = userID (e.g. \"group:telegram:-100123\")\n}\n\n// GatewayConfig controls the gateway server.\ntype GatewayConfig struct {\n\tHost string `json:\"host\"`\n\tPort int `json:\"port\"`\n\tToken string `json:\"token,omitempty\"` // bearer token for WS/HTTP auth\n\tOwnerIDs []string `json:\"owner_ids,omitempty\"` // sender IDs considered \"owner\"\n\tAllowedOrigins []string `json:\"allowed_origins,omitempty\"` // WebSocket CORS whitelist (empty = allow all)\n\tMaxMessageChars int `json:\"max_message_chars,omitempty\"` // max user message characters (default 32000)\n\tRateLimitRPM int `json:\"rate_limit_rpm,omitempty\"` // rate limit: requests per minute per user (default 20, 0 = disabled)\n\tInjectionAction string `json:\"injection_action,omitempty\"` // prompt injection action: \"log\", \"warn\" (default), \"block\", \"off\"\n\tInboundDebounceMs int `json:\"inbound_debounce_ms,omitempty\"` // silence-window in ms that merges rapid channel/Web Chat messages from the same sender/session; 0 disables for text but media-bearing messages still honor a built-in media floor so multi-attachment bursts (#63) coalesce into a single agent run. Agents may override via per-agent agent_config.inbound_debounce_ms.\n\tQuota *QuotaConfig `json:\"quota,omitempty\"` // per-user/group request quotas\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // deliver intermediate text during tool iterations (default false)\n\tChatBehavior *ChatBehaviorConfig `json:\"chat_behavior,omitempty\"` // human-like channel delivery behavior (default disabled)\n\tToolStatus *bool `json:\"tool_status,omitempty\"` // show tool name in streaming preview during tool execution (default true)\n\tTaskRecoveryIntervalSec int `json:\"task_recovery_interval_sec,omitempty\"` // team task recovery ticker interval in seconds (default 300 = 5min)\n\tBackgroundProvider string `json:\"background_provider,omitempty\"` // LLM provider for background workers (vault enrichment, consolidation)\n\tBackgroundModel string `json:\"background_model,omitempty\"` // LLM model for background workers\n}\n\n// ToolsConfig controls tool availability, policy, and web search.\ntype ToolsConfig struct {\n\tProfile string `json:\"profile,omitempty\"` // global profile: \"minimal\", \"coding\", \"messaging\", \"full\"\n\tAllow []string `json:\"allow,omitempty\"` // global allow list (tool names or \"group:xxx\")\n\tDeny []string `json:\"deny,omitempty\"` // global deny list\n\tAlsoAllow []string `json:\"alsoAllow,omitempty\"` // additive: adds without removing existing\n\tByProvider map[string]*ToolPolicySpec `json:\"byProvider,omitempty\"` // per-provider overrides\n\tShellDenyGroups map[string]bool `json:\"shellDenyGroups,omitempty\"` // global shell deny-group toggles (group name -> denied); per-agent overrides win per-key\n\tCommandKeywordAllowlist []CommandKeywordAllowlistRule `json:\"commandKeywordAllowlist,omitempty\"` // scoped bypass for credentialed CLI content keywords\n\tExecApproval ExecApprovalCfg `json:\"execApproval\"` // exec command approval settings\n\tWebFetch WebFetchPolicyConfig `json:\"web_fetch\"` // domain policy for URL fetching\n\tBrowser BrowserToolConfig `json:\"browser\"`\n\tRateLimitPerHour int `json:\"rate_limit_per_hour,omitempty\"` // max tool executions per hour per session (0 = disabled)\n\tScrubCredentials *bool `json:\"scrub_credentials,omitempty\"` // auto-redact API keys/tokens in tool output (default true)\n\tMcpServers map[string]*MCPServerConfig `json:\"mcp_servers,omitempty\"` // external MCP server connections\n\tDocumentParser DocumentParserConfig `json:\"document_parser\"` // local-first document text extraction\n}\n\n// DocumentParserConfig controls local-first document text extraction in the\n// read_document tool. Local-first is OPT-IN (disabled by default): plain\n// pdftotext/pandoc output mis-orders tables/multi-column layouts and ignores\n// the analysis prompt, so the cloud vision path remains the default. When\n// enabled, PDF/DOCX are extracted via local binaries before any cloud LLM\n// call; missing binaries or empty output fall back to the provider chain.\n//\n// Changing these values requires a gateway restart — they are captured at tool\n// construction and not picked up by config hot-reload. (Binary availability,\n// however, is re-checked per call, so runtime installs are seen without a\n// restart.)\ntype DocumentParserConfig struct {\n\tLocalFirst *bool `json:\"local_first,omitempty\"` // nil => false (default OFF)\n\tMaxPages int `json:\"max_pages,omitempty\"` // 0 => 200\n\tTimeoutSec int `json:\"timeout_sec,omitempty\"` // 0 => 30s\n\tMinTextLen int `json:\"min_text_len,omitempty\"` // 0 => 16\n}\n\n// LocalFirstEnabled reports whether local-first extraction is turned on.\nfunc (c DocumentParserConfig) LocalFirstEnabled() bool {\n\treturn c.LocalFirst != nil && *c.LocalFirst\n}\n\n// CommandKeywordAllowlistRule scopes product/security vocabulary that may appear\n// in credentialed CLI content args without disabling command-path deny patterns.\ntype CommandKeywordAllowlistRule struct {\n\tID string `json:\"id,omitempty\"`\n\tCommand string `json:\"command\"`\n\tSubcommands []string `json:\"subcommands,omitempty\"`\n\tArgs []string `json:\"args,omitempty\"`\n\tArgPositions []int `json:\"argPositions,omitempty\"` // 0-based after matched subcommand\n\tKeywords []string `json:\"keywords,omitempty\"`\n\tReason string `json:\"reason,omitempty\"`\n\tEnabled *bool `json:\"enabled,omitempty\"` // nil means enabled\n}\n\n// MCPServerConfig configures a single external MCP server connection.\ntype MCPServerConfig struct {\n\tTransport string `json:\"transport\"` // \"stdio\", \"sse\", \"streamable-http\"\n\tCommand string `json:\"command,omitempty\"` // stdio: command to spawn\n\tArgs []string `json:\"args,omitempty\"` // stdio: command arguments\n\tEnv map[string]string `json:\"env,omitempty\"` // stdio: extra environment variables\n\tURL string `json:\"url,omitempty\"` // sse/http: server URL\n\tHeaders map[string]string `json:\"headers,omitempty\"` // sse/http: extra HTTP headers\n\tEnabled *bool `json:\"enabled,omitempty\"` // default true\n\tToolPrefix string `json:\"tool_prefix,omitempty\"` // prefix for tool names (avoids collisions)\n\tTimeoutSec int `json:\"timeout_sec,omitempty\"` // per-tool-call timeout in seconds (default 60)\n}\n\n// IsEnabled returns whether this MCP server is enabled (default true).\nfunc (c *MCPServerConfig) IsEnabled() bool {\n\treturn c.Enabled == nil || *c.Enabled\n}\n\n// ExecApprovalCfg configures command execution approval (matching TS exec-approval.ts).\ntype ExecApprovalCfg struct {\n\tSecurity string `json:\"security,omitempty\"` // \"deny\", \"allowlist\", \"full\" (default \"full\")\n\tAsk string `json:\"ask,omitempty\"` // \"off\", \"on-miss\", \"always\" (default \"off\")\n\tAllowlist []string `json:\"allowlist,omitempty\"` // glob patterns for allowed commands\n}\n\n// WebFetchPolicyConfig controls domain filtering for the web_fetch tool.\ntype WebFetchPolicyConfig struct {\n\tPolicy string `json:\"policy,omitempty\"` // \"allow_all\" (default), \"allowlist\"\n\tAllowedDomains []string `json:\"allowed_domains,omitempty\"` // e.g. [\"github.com\", \"*.example.com\"]\n\tBlockedDomains []string `json:\"blocked_domains,omitempty\"` // always checked regardless of policy\n}\n\n// BrowserToolConfig controls the browser automation tool.\ntype BrowserToolConfig struct {\n\tEnabled bool `json:\"enabled\"` // enable the browser tool (default false)\n\tHeadless bool `json:\"headless,omitempty\"` // run Chrome in headless mode (ignored when RemoteURL is set)\n\tRemoteURL string `json:\"remote_url,omitempty\"` // CDP endpoint for remote Chrome sidecar, e.g. \"ws://chrome:9222\"\n\tActionTimeoutMs int `json:\"action_timeout_ms,omitempty\"` // per-action timeout in ms (default 30000)\n\tIdleTimeoutMs int `json:\"idle_timeout_ms,omitempty\"` // idle page auto-close in ms (default 600000, 0=disabled)\n\tMaxPages int `json:\"max_pages,omitempty\"` // max open pages per tenant (default 5)\n\tCookieSyncEnabled bool `json:\"cookie_sync_enabled\"` // apply selected synced cookies to scoped browser sessions\n}\n\n// ToolPolicySpec defines a tool policy at any level (global, per-agent, per-provider).\ntype ToolPolicySpec struct {\n\tProfile string `json:\"profile,omitempty\"`\n\tAllow []string `json:\"allow,omitempty\"`\n\tDeny []string `json:\"deny,omitempty\"`\n\tAlsoAllow []string `json:\"alsoAllow,omitempty\"`\n\tByProvider map[string]*ToolPolicySpec `json:\"byProvider,omitempty\"`\n\tWait *WaitToolPolicy `json:\"wait,omitempty\"`\n\tToolCallPrefix string `json:\"toolCallPrefix,omitempty\"` // prefix to strip from model's tool call names before registry lookup\n}\n\n// WaitToolPolicy configures per-agent safety bounds for the wait tool.\ntype WaitToolPolicy struct {\n\tMinMs int `json:\"min_ms,omitempty\"`\n\tMaxMs int `json:\"max_ms,omitempty\"`\n}\n\n// SessionsConfig controls session behavior.\n// Matching TS src/config/sessions/types.ts + src/config/types.base.ts.\ntype SessionsConfig struct {\n\tScope string `json:\"scope,omitempty\"` // \"per-sender\" (default), \"global\"\n\tDmScope string `json:\"dm_scope,omitempty\"` // \"main\", \"per-peer\", \"per-channel-peer\" (default), \"per-account-channel-peer\"\n\tMainKey string `json:\"main_key,omitempty\"` // main session key suffix (default \"main\", used when dm_scope=\"main\")\n}\n\n// TtsConfig configures text-to-speech.\n// Matching TS src/config/types.tts.ts.\ntype TtsConfig struct {\n\tProvider string `json:\"provider,omitempty\"` // \"openai\", \"elevenlabs\", \"edge\", \"minimax\", \"gemini\"\n\tAuto string `json:\"auto,omitempty\"` // \"off\" (default), \"always\", \"inbound\", \"tagged\"\n\tMode string `json:\"mode,omitempty\"` // \"final\" (default), \"all\"\n\tMaxLength int `json:\"max_length,omitempty\"` // max text length before truncation (default 1500)\n\tTimeoutMs int `json:\"timeout_ms,omitempty\"` // API timeout in ms (default 30000)\n\tOpenAI TtsOpenAIConfig `json:\"openai\"`\n\tElevenLabs TtsElevenLabsConfig `json:\"elevenlabs\"`\n\tEdge TtsEdgeConfig `json:\"edge\"`\n\tMiniMax TtsMiniMaxConfig `json:\"minimax\"`\n\tGemini TtsGeminiConfig `json:\"gemini\"`\n}\n\n// TtsGeminiConfig configures the Google Gemini TTS provider.\ntype TtsGeminiConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"` // required; encrypted at rest\n\tAPIBase string `json:\"api_base,omitempty\"` // custom endpoint (optional; SSRF-gated)\n\tVoice string `json:\"voice,omitempty\"` // default \"Kore\"\n\tModel string `json:\"model,omitempty\"` // default \"gemini-2.5-flash-preview-tts\"\n\tSpeakers string `json:\"speakers,omitempty\"` // JSON-encoded []SpeakerVoice for multi-speaker mode\n}\n\n// TtsOpenAIConfig configures the OpenAI TTS provider.\ntype TtsOpenAIConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"`\n\tAPIBase string `json:\"api_base,omitempty\"` // custom endpoint URL\n\tModel string `json:\"model,omitempty\"` // default \"gpt-4o-mini-tts\"\n\tVoice string `json:\"voice,omitempty\"` // default \"alloy\"\n}\n\n// TtsElevenLabsConfig configures the ElevenLabs TTS provider.\ntype TtsElevenLabsConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"`\n\tBaseURL string `json:\"base_url,omitempty\"`\n\tVoiceID string `json:\"voice_id,omitempty\"` // default \"pMsXgVXv3BLzUgSXRplE\"\n\tModelID string `json:\"model_id,omitempty\"` // default \"eleven_multilingual_v2\"\n}\n\n// TtsEdgeConfig configures the Microsoft Edge TTS provider (free, no API key).\ntype TtsEdgeConfig struct {\n\tEnabled bool `json:\"enabled,omitempty\"`\n\tVoice string `json:\"voice,omitempty\"` // default \"en-US-MichelleNeural\"\n\tRate string `json:\"rate,omitempty\"` // speech rate, e.g. \"+0%\"\n}\n\n// TtsMiniMaxConfig configures the MiniMax TTS provider.\ntype TtsMiniMaxConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"`\n\tGroupID string `json:\"group_id,omitempty\"` // MiniMax GroupId (required)\n\tAPIBase string `json:\"api_base,omitempty\"` // default \"https://api.minimax.io/v1\"\n\tModel string `json:\"model,omitempty\"` // default \"speech-02-hd\"\n\tVoiceID string `json:\"voice_id,omitempty\"` // default \"Wise_Woman\"\n}\n\n// MergeChannelGroupQuotas merges per-group quota overrides from channel configs\n// (e.g., channels.telegram.groups[chatID].quota) into gateway.quota.groups.\n// This allows per-group quotas to be set at the channel level and picked up\n// by the QuotaChecker at runtime.\nfunc MergeChannelGroupQuotas(cfg *Config) {\n\tif cfg.Gateway.Quota == nil {\n\t\treturn\n\t}\n\tif cfg.Gateway.Quota.Groups == nil {\n\t\tcfg.Gateway.Quota.Groups = make(map[string]QuotaWindow)\n\t}\n\n\t// Telegram per-group quotas\n\tfor chatID, groupCfg := range cfg.Channels.Telegram.Groups {\n\t\tif groupCfg != nil && groupCfg.Quota != nil && !groupCfg.Quota.IsZero() {\n\t\t\tkey := \"group:telegram:\" + chatID\n\t\t\tcfg.Gateway.Quota.Groups[key] = *groupCfg.Quota\n\t\t}\n\t}\n}\n"}, "9ab3d6dfe58b2c1a3e4fc36b76ff306e1d3b4c2d": {"t": 1785759227.501285, "n": 38792, "h": "74d32bebfcba5c17", "c": "package config\n\n// ChatBehaviorConfig controls optional human-like channel delivery behavior.\n// Pointer fields allow per-channel overrides to inherit gateway defaults.\ntype ChatBehaviorConfig struct {\n\tEnabled *bool `json:\"enabled,omitempty\"`\n\tQuickAck *QuickAckConfig `json:\"quick_ack,omitempty\"`\n\tFinalSplit *FinalSplitConfig `json:\"final_split,omitempty\"`\n}\n\n// QuickAckConfig controls one short acknowledgement before longer non-streaming runs.\ntype QuickAckConfig struct {\n\tEnabled *bool `json:\"enabled,omitempty\"`\n\tMinDelayMs *int `json:\"min_delay_ms,omitempty\"`\n\tTemplates []string `json:\"templates,omitempty\"`\n}\n\n// FinalSplitConfig controls semantic splitting of final channel replies.\ntype FinalSplitConfig struct {\n\tEnabled *bool `json:\"enabled,omitempty\"`\n\tMinChars *int `json:\"min_chars,omitempty\"`\n\tMaxMessages *int `json:\"max_messages,omitempty\"`\n\tDelayMs *int `json:\"delay_ms,omitempty\"`\n}\n\n// PendingCompactionConfig configures LLM-based compaction of pending group messages.\n// When a group accumulates more than Threshold pending messages, older messages are\n// summarized by an LLM and replaced with a compact summary, keeping KeepRecent raw messages.\ntype PendingCompactionConfig struct {\n\tThreshold int `json:\"threshold,omitempty\"` // trigger compaction when entries exceed this (default 200)\n\tKeepRecent int `json:\"keep_recent,omitempty\"` // keep this many recent raw messages after compaction (default 40)\n\tMaxTokens int `json:\"max_tokens,omitempty\"` // max output tokens for LLM summarization (default 4096)\n\tProvider string `json:\"provider,omitempty\"` // LLM provider name (e.g. \"openai\"); empty = use agent's provider\n\tModel string `json:\"model,omitempty\"` // model for summarization; empty = use agent's model\n}\n\n// ChannelsConfig contains per-channel configuration.\ntype ChannelsConfig struct {\n\tTelegram TelegramConfig `json:\"telegram\"`\n\tDiscord DiscordConfig `json:\"discord\"`\n\tSlack SlackConfig `json:\"slack\"`\n\tWhatsApp WhatsAppConfig `json:\"whatsapp\"`\n\tZalo ZaloConfig `json:\"zalo\"`\n\tZaloPersonal ZaloPersonalConfig `json:\"zalo_personal\"`\n\tFeishu FeishuConfig `json:\"feishu\"`\n\tPendingCompaction *PendingCompactionConfig `json:\"pending_compaction,omitempty\"` // global pending message compaction settings\n}\n\ntype TelegramConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tToken string `json:\"token\"`\n\tProxy string `json:\"proxy,omitempty\"`\n\tAPIServer string `json:\"api_server,omitempty\"` // custom Telegram Bot API server URL (e.g. \"http://localhost:8081\")\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in groups (default true)\n\tMentionMode string `json:\"mention_mode,omitempty\"` // \"strict\" (default) = only respond when mentioned; \"yield\" = respond unless another bot is mentioned\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tDMStream *bool `json:\"dm_stream,omitempty\"` // enable streaming for DMs (default false) — edits placeholder progressively\n\tGroupStream *bool `json:\"group_stream,omitempty\"` // enable streaming for groups (default false) — sends new message, edits progressively\n\tDraftTransport *bool `json:\"draft_transport,omitempty\"` // use sendMessageDraft for DM streaming (default true) — stealth preview, no notifications per edit\n\tReasoningStream *bool `json:\"reasoning_stream,omitempty\"` // show reasoning as separate message when provider emits thinking events (default true)\n\tReactionLevel string `json:\"reaction_level,omitempty\"` // \"off\" (default), \"minimal\", \"full\" — status emoji reactions\n\tMediaMaxBytes int64 `json:\"media_max_bytes,omitempty\"` // max media download size in bytes (default 20MB)\n\tLinkPreview *bool `json:\"link_preview,omitempty\"` // enable URL previews in messages (default true)\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tChatBehavior *ChatBehaviorConfig `json:\"chat_behavior,omitempty\"` // override gateway chat behavior (nil = inherit)\n\tForceIPv4 bool `json:\"force_ipv4,omitempty\"` // force IPv4 for all Telegram API requests (use when IPv6 routing is broken)\n\n\t// Optional STT (Speech-to-Text) pipeline for voice/audio inbound messages.\n\t// When stt_proxy_url is set, audio/voice messages are transcribed before being forwarded to the agent.\n\tSTTProxyURL string `json:\"stt_proxy_url,omitempty\"` // base URL of the STT proxy service (e.g. \"https://stt.example.com\")\n\tSTTAPIKey string `json:\"stt_api_key,omitempty\"` // Bearer token for the STT proxy\n\tSTTTenantID string `json:\"stt_tenant_id,omitempty\"` // optional tenant/org identifier forwarded to the STT proxy\n\tSTTTimeoutSeconds int `json:\"stt_timeout_seconds,omitempty\"` // per-request timeout for STT calls (default 30s)\n\n\t// Optional audio-aware routing: when set, voice/audio inbound messages are routed to this\n\t// agent instead of the default channel agent. Requires the named agent to exist in the config.\n\tVoiceAgentID string `json:\"voice_agent_id,omitempty\"` // agent ID to route voice inbound to (e.g. \"speaking-agent\")\n\n\t// Audio guard: intercept technical errors in voice agent replies and replace with friendly fallbacks.\n\t// Only active when VoiceAgentID is set. Custom error markers replace built-in defaults when provided.\n\tAudioGuardFallbackTranscript string `json:\"audio_guard_fallback_transcript,omitempty\"` // fallback with %s for transcript (e.g. \"I heard: \\\"%s\\\". Try again!\")\n\tAudioGuardFallbackNoTranscript string `json:\"audio_guard_fallback_no_transcript,omitempty\"` // fallback when no transcript available\n\tAudioGuardErrorMarkers []string `json:\"audio_guard_error_markers,omitempty\"` // custom error detection markers (replaces defaults)\n\n\t// Per-group (and per-topic) overrides. Key is chat ID string (e.g. \"-100123456\") or \"*\" for wildcard.\n\t// TS ref: channels.telegram.groups in src/config/types.telegram.ts.\n\tGroups map[string]*TelegramGroupConfig `json:\"groups,omitempty\"`\n}\n\n// TelegramGroupConfig defines per-group overrides for a Telegram channel.\n// Matching TS TelegramGroupConfig in src/config/types.telegram.ts.\ntype TelegramGroupConfig struct {\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // override group policy for this group\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // override require_mention for this group\n\tMentionMode string `json:\"mention_mode,omitempty\"` // override mention_mode for this group\n\tAllowFrom FlexibleStringSlice `json:\"allow_from,omitempty\"` // override allow_from for this group\n\tEnabled *bool `json:\"enabled,omitempty\"` // disable bot for this group (default: true)\n\tSkills []string `json:\"skills,omitempty\"` // skill whitelist (nil = all, [] = none)\n\tTools []string `json:\"tools,omitempty\"` // tool allow list (nil = all, supports \"group:xxx\")\n\tSystemPrompt string `json:\"system_prompt,omitempty\"` // extra system prompt for this group\n\tTopics map[string]*TelegramTopicConfig `json:\"topics,omitempty\"` // per-topic overrides (key: thread ID string)\n\tQuota *QuotaWindow `json:\"quota,omitempty\"` // per-group quota override\n}\n\n// TelegramTopicConfig defines per-topic overrides within a Telegram group.\n// Matching TS TelegramTopicConfig in src/config/types.telegram.ts.\ntype TelegramTopicConfig struct {\n\tRequireMention *bool `json:\"require_mention,omitempty\"`\n\tMentionMode string `json:\"mention_mode,omitempty\"`\n\tGroupPolicy string `json:\"group_policy,omitempty\"`\n\tSkills []string `json:\"skills,omitempty\"`\n\tTools []string `json:\"tools,omitempty\"` // tool allow list (nil = inherit, supports \"group:xxx\")\n\tEnabled *bool `json:\"enabled,omitempty\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from,omitempty\"`\n\tSystemPrompt string `json:\"system_prompt,omitempty\"`\n}\n\ntype DiscordConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tToken string `json:\"token\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in groups (default true)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tChatBehavior *ChatBehaviorConfig `json:\"chat_behavior,omitempty\"` // override gateway chat behavior (nil = inherit)\n\tMediaMaxBytes int64 `json:\"media_max_bytes,omitempty\"` // max media download size (default 25MB)\n\tSTTProxyURL string `json:\"stt_proxy_url,omitempty\"`\n\tSTTAPIKey string `json:\"stt_api_key,omitempty\"`\n\tSTTTenantID string `json:\"stt_tenant_id,omitempty\"`\n\tSTTTimeoutSeconds int `json:\"stt_timeout_seconds,omitempty\"`\n\tVoiceAgentID string `json:\"voice_agent_id,omitempty\"`\n}\n\ntype SlackConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tBotToken string `json:\"bot_token\"` // xoxb-... (Bot User OAuth Token)\n\tAppToken string `json:\"app_token\"` // xapp-... (App-Level Token for Socket Mode)\n\tUserToken string `json:\"user_token,omitempty\"` // xoxp-... (Optional: custom bot identity)\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"pairing\", \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in channels (default true)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tDMStream *bool `json:\"dm_stream,omitempty\"` // enable streaming for DMs (default false)\n\tGroupStream *bool `json:\"group_stream,omitempty\"` // enable streaming for groups (default false)\n\tNativeStream *bool `json:\"native_stream,omitempty\"` // use Slack ChatStreamer API if available (default false)\n\tReactionLevel string `json:\"reaction_level,omitempty\"` // \"off\" (default), \"minimal\", \"full\"\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tChatBehavior *ChatBehaviorConfig `json:\"chat_behavior,omitempty\"` // override gateway chat behavior (nil = inherit)\n\tDebounceDelay *int `json:\"debounce_delay,omitempty\"` // ms delay before dispatching rapid messages (default 300, 0=disabled)\n\tThreadTTL *int `json:\"thread_ttl,omitempty\"` // hours before thread participation expires (default 24, 0=disabled — always require @mention)\n\tMediaMaxBytes int64 `json:\"media_max_bytes,omitempty\"` // max file download size in bytes (default 20MB)\n}\n\ntype WhatsAppConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tAuthDir string `json:\"auth_dir,omitempty\"` // optional: SQLite auth dir override (desktop)\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default for DB instances), \"open\", \"allowlist\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"pairing\" (default for DB instances), \"open\" (default for config), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // only respond in groups when bot is @mentioned (default false)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 200, 0=disabled)\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tChatBehavior *ChatBehaviorConfig `json:\"chat_behavior,omitempty\"` // override gateway chat behavior (nil = inherit)\n}\n\ntype ZaloConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tToken string `json:\"token\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tWebhookURL string `json:\"webhook_url,omitempty\"`\n\tWebhookSecret string `json:\"webhook_secret,omitempty\"`\n\tMediaMaxMB int `json:\"media_max_mb,omitempty\"` // default 5\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tChatBehavior *ChatBehaviorConfig `json:\"chat_behavior,omitempty\"` // override gateway chat behavior (nil = inherit)\n}\n\ntype ZaloPersonalConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in groups (default true)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tCredentialsPath string `json:\"credentials_path,omitempty\"` // path to saved cookies JSON\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tChatBehavior *ChatBehaviorConfig `json:\"chat_behavior,omitempty\"` // override gateway chat behavior (nil = inherit)\n}\n\ntype FeishuConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tAppID string `json:\"app_id\"`\n\tAppSecret string `json:\"app_secret\"`\n\tEncryptKey string `json:\"encrypt_key,omitempty\"`\n\tVerificationToken string `json:\"verification_token,omitempty\"`\n\tDomain string `json:\"domain,omitempty\"` // \"lark\" (default/global), \"feishu\" (China), or custom URL\n\tConnectionMode string `json:\"connection_mode,omitempty\"` // \"websocket\" (default), \"webhook\"\n\tWebhookPort int `json:\"webhook_port,omitempty\"` // default 3000\n\tWebhookPath string `json:\"webhook_path,omitempty\"` // default \"/feishu/events\"\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default)\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default)\n\tGroupAllowFrom FlexibleStringSlice `json:\"group_allow_from,omitempty\"`\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // default true (groups)\n\tTopicSessionMode string `json:\"topic_session_mode,omitempty\"` // \"disabled\" (default)\n\tTextChunkLimit int `json:\"text_chunk_limit,omitempty\"` // default 4000\n\tMediaMaxMB int `json:\"media_max_mb,omitempty\"` // default 30\n\tRenderMode string `json:\"render_mode,omitempty\"` // \"auto\", \"raw\", \"card\"\n\tStreaming *bool `json:\"streaming,omitempty\"` // default true\n\tReactionLevel string `json:\"reaction_level,omitempty\"` // \"off\" (default), \"minimal\", \"full\" — typing emoji reactions\n\tHistoryLimit int `json:\"history_limit,omitempty\"`\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tChatBehavior *ChatBehaviorConfig `json:\"chat_behavior,omitempty\"` // override gateway chat behavior (nil = inherit)\n\tSTTProxyURL string `json:\"stt_proxy_url,omitempty\"`\n\tSTTAPIKey string `json:\"stt_api_key,omitempty\"`\n\tSTTTenantID string `json:\"stt_tenant_id,omitempty\"`\n\tSTTTimeoutSeconds int `json:\"stt_timeout_seconds,omitempty\"`\n\tVoiceAgentID string `json:\"voice_agent_id,omitempty\"`\n}\n\n// ProvidersConfig maps provider name to its config.\ntype ProvidersConfig struct {\n\tAnthropic ProviderConfig `json:\"anthropic\"`\n\tOpenAI ProviderConfig `json:\"openai\"`\n\tOpenRouter ProviderConfig `json:\"openrouter\"`\n\tGroq ProviderConfig `json:\"groq\"`\n\tGemini ProviderConfig `json:\"gemini\"`\n\tDeepSeek ProviderConfig `json:\"deepseek\"`\n\tMistral ProviderConfig `json:\"mistral\"`\n\tXAI ProviderConfig `json:\"xai\"`\n\tMiniMax ProviderConfig `json:\"minimax\"`\n\tCohere ProviderConfig `json:\"cohere\"`\n\tPerplexity ProviderConfig `json:\"perplexity\"`\n\tDashScope ProviderConfig `json:\"dashscope\"`\n\tBailian ProviderConfig `json:\"bailian\"`\n\tZai ProviderConfig `json:\"zai\"`\n\tZaiCoding ProviderConfig `json:\"zai_coding\"`\n\tOllama OllamaConfig `json:\"ollama\"` // local Ollama instance (no API key needed)\n\tOllamaCloud ProviderConfig `json:\"ollama_cloud\"` // Ollama Cloud (API key required)\n\tClaudeCLI ClaudeCLIConfig `json:\"claude_cli\"`\n\tACP ACPConfig `json:\"acp\"`\n\tNovita ProviderConfig `json:\"novita\"` // Novita AI (OpenAI-compatible endpoint)\n\tBytePlus ProviderConfig `json:\"byteplus\"` // BytePlus ModelArk (Seed 2.0)\n\tBytePlusCoding ProviderConfig `json:\"byteplus_coding\"` // BytePlus ModelArk Coding Plan\n\tVertex VertexConfig `json:\"vertex\"` // Google Cloud Vertex AI (OAuth2 service account + ADC)\n}\n\n// VertexConfig configures Google Cloud Vertex AI.\n// Credentials precedence: APIKey (inline JSON) > CredentialsFile (path) > ADC (both empty).\n// ProjectID and Region are required; Model optional (defaults to google/gemini-2.0-flash-001).\ntype VertexConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"` // service account JSON inline (secret — never persist in config.json)\n\tCredentialsFile string `json:\"credentials_file,omitempty\"` // path to service account JSON file\n\tProjectID string `json:\"project_id,omitempty\"`\n\tRegion string `json:\"region,omitempty\"`\n\tModel string `json:\"model,omitempty\"`\n}\n\n// OllamaConfig configures a local (or self-hosted) Ollama instance.\n// No API key is required — Ollama accepts any Bearer token value.\ntype OllamaConfig struct {\n\tHost string `json:\"host\"` // Ollama server base URL, e.g. http://localhost:11434\n}\n\n// ClaudeCLIConfig configures the Claude CLI provider (uses subscription, not API key).\ntype ClaudeCLIConfig struct {\n\tCLIPath string `json:\"cli_path\" yaml:\"cli_path\"` // path to claude binary (default: \"claude\")\n\tModel string `json:\"model\" yaml:\"model\"` // default model alias (default: \"sonnet\")\n\tBaseWorkDir string `json:\"base_work_dir\" yaml:\"base_work_dir\"` // base dir for agent workspaces\n\tPermMode string `json:\"perm_mode\" yaml:\"perm_mode\"` // permission mode (default: \"bypassPermissions\")\n}\n\n// ACPConfig configures the ACP (Agent Client Protocol) provider.\n// Orchestrates any ACP-compatible coding agent (Claude Code, Codex CLI, Gemini CLI) as a subprocess.\ntype ACPConfig struct {\n\tBinary string `json:\"binary\"` // agent binary name or path (e.g. \"claude\", \"codex\")\n\tArgs []string `json:\"args\"` // extra spawn args\n\tModel string `json:\"model\"` // default model/agent name\n\tWorkDir string `json:\"work_dir\"` // base workspace dir\n\tIdleTTL string `json:\"idle_ttl\"` // process idle TTL (e.g. \"5m\")\n\tPermMode string `json:\"perm_mode\"` // \"approve-all\" (default), \"approve-reads\", \"deny-all\"\n}\n\ntype ProviderConfig struct {\n\tAPIKey string `json:\"api_key\"`\n\tAPIBase string `json:\"api_base,omitempty\"`\n}\n\n// APIBaseForType returns the config-level api_base for a given provider type.\n// Used as a fallback when DB providers have no api_base set.\nfunc (p *ProvidersConfig) APIBaseForType(providerType string) string {\n\tswitch providerType {\n\tcase \"anthropic_native\":\n\t\treturn p.Anthropic.APIBase\n\tcase \"openai\", \"openai_compat\":\n\t\treturn p.OpenAI.APIBase\n\tcase \"openrouter\":\n\t\treturn p.OpenRouter.APIBase\n\tcase \"groq\":\n\t\treturn p.Groq.APIBase\n\tcase \"deepseek\":\n\t\treturn p.DeepSeek.APIBase\n\tcase \"gemini_native\":\n\t\treturn p.Gemini.APIBase\n\tcase \"mistral\":\n\t\treturn p.Mistral.APIBase\n\tcase \"xai\":\n\t\treturn p.XAI.APIBase\n\tcase \"minimax_native\":\n\t\treturn p.MiniMax.APIBase\n\tcase \"cohere\":\n\t\treturn p.Cohere.APIBase\n\tcase \"perplexity\":\n\t\treturn p.Perplexity.APIBase\n\tcase \"dashscope\":\n\t\treturn p.DashScope.APIBase\n\tcase \"bailian\":\n\t\treturn p.Bailian.APIBase\n\tcase \"zai\":\n\t\treturn p.Zai.APIBase\n\tcase \"zai_coding\":\n\t\treturn p.ZaiCoding.APIBase\n\tcase \"ollama_cloud\":\n\t\treturn p.OllamaCloud.APIBase\n\tcase \"novita\":\n\t\treturn p.Novita.APIBase\n\tcase \"byteplus\":\n\t\treturn p.BytePlus.APIBase\n\tcase \"byteplus_coding\":\n\t\treturn p.BytePlusCoding.APIBase\n\tcase \"vertex\":\n\t\t// Computed from project+region at registration time; no config-level static base.\n\t\treturn \"\"\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\n// HasAnyProvider returns true if at least one provider has an API key or CLI configured.\nfunc (c *Config) HasAnyProvider() bool {\n\tp := c.Providers\n\treturn p.Anthropic.APIKey != \"\" ||\n\t\tp.OpenAI.APIKey != \"\" ||\n\t\tp.OpenRouter.APIKey != \"\" ||\n\t\tp.Groq.APIKey != \"\" ||\n\t\tp.Gemini.APIKey != \"\" ||\n\t\tp.DeepSeek.APIKey != \"\" ||\n\t\tp.Mistral.APIKey != \"\" ||\n\t\tp.XAI.APIKey != \"\" ||\n\t\tp.MiniMax.APIKey != \"\" ||\n\t\tp.Cohere.APIKey != \"\" ||\n\t\tp.Perplexity.APIKey != \"\" ||\n\t\tp.DashScope.APIKey != \"\" ||\n\t\tp.Bailian.APIKey != \"\" ||\n\t\tp.Zai.APIKey != \"\" ||\n\t\tp.ZaiCoding.APIKey != \"\" ||\n\t\tp.Ollama.Host != \"\" ||\n\t\tp.OllamaCloud.APIKey != \"\" ||\n\t\tp.ClaudeCLI.CLIPath != \"\" ||\n\t\tp.ACP.Binary != \"\" ||\n\t\tp.Novita.APIKey != \"\" ||\n\t\tp.BytePlus.APIKey != \"\" ||\n\t\tp.BytePlusCoding.APIKey != \"\" ||\n\t\t(p.Vertex.ProjectID != \"\" && p.Vertex.Region != \"\")\n}\n\n// QuotaWindow defines request limits per time window. Zero means unlimited.\ntype QuotaWindow struct {\n\tHour int `json:\"hour,omitempty\"` // max requests per hour (0 = unlimited)\n\tDay int `json:\"day,omitempty\"` // max requests per day (0 = unlimited)\n\tWeek int `json:\"week,omitempty\"` // max requests per week (0 = unlimited)\n}\n\n// IsZero returns true if no limits are set.\nfunc (w QuotaWindow) IsZero() bool { return w.Hour == 0 && w.Day == 0 && w.Week == 0 }\n\n// QuotaConfig configures per-user/group request quotas.\n// Config merge priority: Groups > Channels > Providers > Default.\ntype QuotaConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tDefault QuotaWindow `json:\"default\"`\n\tProviders map[string]QuotaWindow `json:\"providers,omitempty\"` // key = provider name (e.g. \"anthropic\")\n\tChannels map[string]QuotaWindow `json:\"channels,omitempty\"` // key = channel name (e.g. \"telegram\")\n\tGroups map[string]QuotaWindow `json:\"groups,omitempty\"` // key = userID (e.g. \"group:telegram:-100123\")\n}\n\n// GatewayConfig controls the gateway server.\ntype GatewayConfig struct {\n\tHost string `json:\"host\"`\n\tPort int `json:\"port\"`\n\tToken string `json:\"token,omitempty\"` // bearer token for WS/HTTP auth\n\tOwnerIDs []string `json:\"owner_ids,omitempty\"` // sender IDs considered \"owner\"\n\tAllowedOrigins []string `json:\"allowed_origins,omitempty\"` // WebSocket CORS whitelist (empty = allow all)\n\tMaxMessageChars int `json:\"max_message_chars,omitempty\"` // max user message characters (default 32000)\n\tRateLimitRPM int `json:\"rate_limit_rpm,omitempty\"` // rate limit: requests per minute per user (default 20, 0 = disabled)\n\tInjectionAction string `json:\"injection_action,omitempty\"` // prompt injection action: \"log\", \"warn\" (default), \"block\", \"off\"\n\tInboundDebounceMs int `json:\"inbound_debounce_ms,omitempty\"` // silence-window in ms that merges rapid channel/Web Chat messages from the same sender/session; 0 disables for text but media-bearing messages still honor a built-in media floor so multi-attachment bursts (#63) coalesce into a single agent run. Agents may override via per-agent agent_config.inbound_debounce_ms.\n\tQuota *QuotaConfig `json:\"quota,omitempty\"` // per-user/group request quotas\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // deliver intermediate text during tool iterations (default false)\n\tChatBehavior *ChatBehaviorConfig `json:\"chat_behavior,omitempty\"` // human-like channel delivery behavior (default disabled)\n\tToolStatus *bool `json:\"tool_status,omitempty\"` // show tool name in streaming preview during tool execution (default true)\n\tTaskRecoveryIntervalSec int `json:\"task_recovery_interval_sec,omitempty\"` // team task recovery ticker interval in seconds (default 300 = 5min)\n\tBackgroundProvider string `json:\"background_provider,omitempty\"` // LLM provider for background workers (vault enrichment, consolidation)\n\tBackgroundModel string `json:\"background_model,omitempty\"` // LLM model for background workers\n}\n\n// ToolsConfig controls tool availability, policy, and web search.\ntype ToolsConfig struct {\n\tProfile string `json:\"profile,omitempty\"` // global profile: \"minimal\", \"coding\", \"messaging\", \"full\"\n\tAllow []string `json:\"allow,omitempty\"` // global allow list (tool names or \"group:xxx\")\n\tDeny []string `json:\"deny,omitempty\"` // global deny list\n\tAlsoAllow []string `json:\"alsoAllow,omitempty\"` // additive: adds without removing existing\n\tByProvider map[string]*ToolPolicySpec `json:\"byProvider,omitempty\"` // per-provider overrides\n\tShellDenyGroups map[string]bool `json:\"shellDenyGroups,omitempty\"` // global shell deny-group toggles (group name -> denied); per-agent overrides win per-key\n\tCommandKeywordAllowlist []CommandKeywordAllowlistRule `json:\"commandKeywordAllowlist,omitempty\"` // scoped bypass for credentialed CLI content keywords\n\tExecApproval ExecApprovalCfg `json:\"execApproval\"` // exec command approval settings\n\tWebFetch WebFetchPolicyConfig `json:\"web_fetch\"` // domain policy for URL fetching\n\tBrowser BrowserToolConfig `json:\"browser\"`\n\tRateLimitPerHour int `json:\"rate_limit_per_hour,omitempty\"` // max tool executions per hour per session (0 = disabled)\n\tScrubCredentials *bool `json:\"scrub_credentials,omitempty\"` // auto-redact API keys/tokens in tool output (default true)\n\tMcpServers map[string]*MCPServerConfig `json:\"mcp_servers,omitempty\"` // external MCP server connections\n\tDocumentParser DocumentParserConfig `json:\"document_parser\"` // local-first document text extraction\n}\n\n// DocumentParserConfig controls local-first document text extraction in the\n// read_document tool. Local-first is OPT-IN (disabled by default): plain\n// pdftotext/pandoc output mis-orders tables/multi-column layouts and ignores\n// the analysis prompt, so the cloud vision path remains the default. When\n// enabled, PDF/DOCX are extracted via local binaries before any cloud LLM\n// call; missing binaries or empty output fall back to the provider chain.\n//\n// Changing these values requires a gateway restart — they are captured at tool\n// construction and not picked up by config hot-reload. (Binary availability,\n// however, is re-checked per call, so runtime installs are seen without a\n// restart.)\ntype DocumentParserConfig struct {\n\tLocalFirst *bool `json:\"local_first,omitempty\"` // nil => false (default OFF)\n\tMaxPages int `json:\"max_pages,omitempty\"` // 0 => 200\n\tTimeoutSec int `json:\"timeout_sec,omitempty\"` // 0 => 30s\n\tMinTextLen int `json:\"min_text_len,omitempty\"` // 0 => 16\n}\n\n// LocalFirstEnabled reports whether local-first extraction is turned on.\nfunc (c DocumentParserConfig) LocalFirstEnabled() bool {\n\treturn c.LocalFirst != nil && *c.LocalFirst\n}\n\n// CommandKeywordAllowlistRule scopes product/security vocabulary that may appear\n// in credentialed CLI content args without disabling command-path deny patterns.\ntype CommandKeywordAllowlistRule struct {\n\tID string `json:\"id,omitempty\"`\n\tCommand string `json:\"command\"`\n\tSubcommands []string `json:\"subcommands,omitempty\"`\n\tArgs []string `json:\"args,omitempty\"`\n\tArgPositions []int `json:\"argPositions,omitempty\"` // 0-based after matched subcommand\n\tKeywords []string `json:\"keywords,omitempty\"`\n\tReason string `json:\"reason,omitempty\"`\n\tEnabled *bool `json:\"enabled,omitempty\"` // nil means enabled\n}\n\n// MCPServerConfig configures a single external MCP server connection.\ntype MCPServerConfig struct {\n\tTransport string `json:\"transport\"` // \"stdio\", \"sse\", \"streamable-http\"\n\tCommand string `json:\"command,omitempty\"` // stdio: command to spawn\n\tArgs []string `json:\"args,omitempty\"` // stdio: command arguments\n\tEnv map[string]string `json:\"env,omitempty\"` // stdio: extra environment variables\n\tURL string `json:\"url,omitempty\"` // sse/http: server URL\n\tHeaders map[string]string `json:\"headers,omitempty\"` // sse/http: extra HTTP headers\n\tEnabled *bool `json:\"enabled,omitempty\"` // default true\n\tToolPrefix string `json:\"tool_prefix,omitempty\"` // prefix for tool names (avoids collisions)\n\tTimeoutSec int `json:\"timeout_sec,omitempty\"` // per-tool-call timeout in seconds (default 60)\n}\n\n// IsEnabled returns whether this MCP server is enabled (default true).\nfunc (c *MCPServerConfig) IsEnabled() bool {\n\treturn c.Enabled == nil || *c.Enabled\n}\n\n// ExecApprovalCfg configures command execution approval (matching TS exec-approval.ts).\ntype ExecApprovalCfg struct {\n\tSecurity string `json:\"security,omitempty\"` // \"deny\", \"allowlist\", \"full\" (default \"full\")\n\tAsk string `json:\"ask,omitempty\"` // \"off\", \"on-miss\", \"always\" (default \"off\")\n\tAllowlist []string `json:\"allowlist,omitempty\"` // glob patterns for allowed commands\n}\n\n// WebFetchPolicyConfig controls domain filtering for the web_fetch tool.\ntype WebFetchPolicyConfig struct {\n\tPolicy string `json:\"policy,omitempty\"` // \"allow_all\" (default), \"allowlist\"\n\tAllowedDomains []string `json:\"allowed_domains,omitempty\"` // e.g. [\"github.com\", \"*.example.com\"]\n\tBlockedDomains []string `json:\"blocked_domains,omitempty\"` // always checked regardless of policy\n}\n\n// BrowserToolConfig controls the browser automation tool.\ntype BrowserToolConfig struct {\n\tEnabled bool `json:\"enabled\"` // enable the browser tool (default false)\n\tHeadless bool `json:\"headless,omitempty\"` // run Chrome in headless mode (ignored when RemoteURL is set)\n\tRemoteURL string `json:\"remote_url,omitempty\"` // CDP endpoint for remote Chrome sidecar, e.g. \"ws://chrome:9222\"\n\tActionTimeoutMs int `json:\"action_timeout_ms,omitempty\"` // per-action timeout in ms (default 30000)\n\tIdleTimeoutMs int `json:\"idle_timeout_ms,omitempty\"` // idle page auto-close in ms (default 600000, 0=disabled)\n\tMaxPages int `json:\"max_pages,omitempty\"` // max open pages per tenant (default 5)\n\tCookieSyncEnabled bool `json:\"cookie_sync_enabled\"` // apply selected synced cookies to scoped browser sessions\n}\n\n// ToolPolicySpec defines a tool policy at any level (global, per-agent, per-provider).\ntype ToolPolicySpec struct {\n\tProfile string `json:\"profile,omitempty\"`\n\tAllow []string `json:\"allow,omitempty\"`\n\tDeny []string `json:\"deny,omitempty\"`\n\tAlsoAllow []string `json:\"alsoAllow,omitempty\"`\n\tByProvider map[string]*ToolPolicySpec `json:\"byProvider,omitempty\"`\n\tWait *WaitToolPolicy `json:\"wait,omitempty\"`\n\tToolCallPrefix string `json:\"toolCallPrefix,omitempty\"` // prefix to strip from model's tool call names before registry lookup\n}\n\n// WaitToolPolicy configures per-agent safety bounds for the wait tool.\ntype WaitToolPolicy struct {\n\tMinMs int `json:\"min_ms,omitempty\"`\n\tMaxMs int `json:\"max_ms,omitempty\"`\n}\n\n// SessionsConfig controls session behavior.\n// Matching TS src/config/sessions/types.ts + src/config/types.base.ts.\ntype SessionsConfig struct {\n\tScope string `json:\"scope,omitempty\"` // \"per-sender\" (default), \"global\"\n\tDmScope string `json:\"dm_scope,omitempty\"` // \"main\", \"per-peer\", \"per-channel-peer\" (default), \"per-account-channel-peer\"\n\tMainKey string `json:\"main_key,omitempty\"` // main session key suffix (default \"main\", used when dm_scope=\"main\")\n}\n\n// TtsConfig configures text-to-speech.\n// Matching TS src/config/types.tts.ts.\ntype TtsConfig struct {\n\tProvider string `json:\"provider,omitempty\"` // \"openai\", \"elevenlabs\", \"edge\", \"minimax\", \"gemini\"\n\tAuto string `json:\"auto,omitempty\"` // \"off\" (default), \"always\", \"inbound\", \"tagged\"\n\tMode string `json:\"mode,omitempty\"` // \"final\" (default), \"all\"\n\tMaxLength int `json:\"max_length,omitempty\"` // max text length before truncation (default 1500)\n\tTimeoutMs int `json:\"timeout_ms,omitempty\"` // API timeout in ms (default 30000)\n\tOpenAI TtsOpenAIConfig `json:\"openai\"`\n\tElevenLabs TtsElevenLabsConfig `json:\"elevenlabs\"`\n\tEdge TtsEdgeConfig `json:\"edge\"`\n\tMiniMax TtsMiniMaxConfig `json:\"minimax\"`\n\tGemini TtsGeminiConfig `json:\"gemini\"`\n}\n\n// TtsGeminiConfig configures the Google Gemini TTS provider.\ntype TtsGeminiConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"` // required; encrypted at rest\n\tAPIBase string `json:\"api_base,omitempty\"` // custom endpoint (optional; SSRF-gated)\n\tVoice string `json:\"voice,omitempty\"` // default \"Kore\"\n\tModel string `json:\"model,omitempty\"` // default \"gemini-2.5-flash-preview-tts\"\n\tSpeakers string `json:\"speakers,omitempty\"` // JSON-encoded []SpeakerVoice for multi-speaker mode\n}\n\n// TtsOpenAIConfig configures the OpenAI TTS provider.\ntype TtsOpenAIConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"`\n\tAPIBase string `json:\"api_base,omitempty\"` // custom endpoint URL\n\tModel string `json:\"model,omitempty\"` // default \"gpt-4o-mini-tts\"\n\tVoice string `json:\"voice,omitempty\"` // default \"alloy\"\n}\n\n// TtsElevenLabsConfig configures the ElevenLabs TTS provider.\ntype TtsElevenLabsConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"`\n\tBaseURL string `json:\"base_url,omitempty\"`\n\tVoiceID string `json:\"voice_id,omitempty\"` // default \"pMsXgVXv3BLzUgSXRplE\"\n\tModelID string `json:\"model_id,omitempty\"` // default \"eleven_multilingual_v2\"\n}\n\n// TtsEdgeConfig configures the Microsoft Edge TTS provider (free, no API key).\ntype TtsEdgeConfig struct {\n\tEnabled bool `json:\"enabled,omitempty\"`\n\tVoice string `json:\"voice,omitempty\"` // default \"en-US-MichelleNeural\"\n\tRate string `json:\"rate,omitempty\"` // speech rate, e.g. \"+0%\"\n}\n\n// TtsMiniMaxConfig configures the MiniMax TTS provider.\ntype TtsMiniMaxConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"`\n\tGroupID string `json:\"group_id,omitempty\"` // MiniMax GroupId (required)\n\tAPIBase string `json:\"api_base,omitempty\"` // default \"https://api.minimax.io/v1\"\n\tModel string `json:\"model,omitempty\"` // default \"speech-02-hd\"\n\tVoiceID string `json:\"voice_id,omitempty\"` // default \"Wise_Woman\"\n}\n\n// MergeChannelGroupQuotas merges per-group quota overrides from channel configs\n// (e.g., channels.telegram.groups[chatID].quota) into gateway.quota.groups.\n// This allows per-group quotas to be set at the channel level and picked up\n// by the QuotaChecker at runtime.\nfunc MergeChannelGroupQuotas(cfg *Config) {\n\tif cfg.Gateway.Quota == nil {\n\t\treturn\n\t}\n\tif cfg.Gateway.Quota.Groups == nil {\n\t\tcfg.Gateway.Quota.Groups = make(map[string]QuotaWindow)\n\t}\n\n\t// Telegram per-group quotas\n\tfor chatID, groupCfg := range cfg.Channels.Telegram.Groups {\n\t\tif groupCfg != nil && groupCfg.Quota != nil && !groupCfg.Quota.IsZero() {\n\t\t\tkey := \"group:telegram:\" + chatID\n\t\t\tcfg.Gateway.Quota.Groups[key] = *groupCfg.Quota\n\t\t}\n\t}\n}\n"}, "f0f39ce31f19189bd00a1119b46e105d321f66fd": {"t": 1785759229.0952728, "n": 37473, "h": "966cfaf520f062b5", "c": "package config\n\n// ChatBehaviorConfig controls optional human-like channel delivery behavior.\n// Pointer fields allow per-channel overrides to inherit gateway defaults.\ntype ChatBehaviorConfig struct {\n\tEnabled *bool `json:\"enabled,omitempty\"`\n\tQuickAck *QuickAckConfig `json:\"quick_ack,omitempty\"`\n\tFinalSplit *FinalSplitConfig `json:\"final_split,omitempty\"`\n}\n\n// QuickAckConfig controls one short acknowledgement before longer non-streaming runs.\ntype QuickAckConfig struct {\n\tEnabled *bool `json:\"enabled,omitempty\"`\n\tMinDelayMs *int `json:\"min_delay_ms,omitempty\"`\n\tTemplates []string `json:\"templates,omitempty\"`\n}\n\n// FinalSplitConfig controls semantic splitting of final channel replies.\ntype FinalSplitConfig struct {\n\tEnabled *bool `json:\"enabled,omitempty\"`\n\tMinChars *int `json:\"min_chars,omitempty\"`\n\tMaxMessages *int `json:\"max_messages,omitempty\"`\n\tDelayMs *int `json:\"delay_ms,omitempty\"`\n}\n\n// PendingCompactionConfig configures LLM-based compaction of pending group messages.\n// When a group accumulates more than Threshold pending messages, older messages are\n// summarized by an LLM and replaced with a compact summary, keeping KeepRecent raw messages.\ntype PendingCompactionConfig struct {\n\tThreshold int `json:\"threshold,omitempty\"` // trigger compaction when entries exceed this (default 200)\n\tKeepRecent int `json:\"keep_recent,omitempty\"` // keep this many recent raw messages after compaction (default 40)\n\tMaxTokens int `json:\"max_tokens,omitempty\"` // max output tokens for LLM summarization (default 4096)\n\tProvider string `json:\"provider,omitempty\"` // LLM provider name (e.g. \"openai\"); empty = use agent's provider\n\tModel string `json:\"model,omitempty\"` // model for summarization; empty = use agent's model\n}\n\n// ChannelsConfig contains per-channel configuration.\ntype ChannelsConfig struct {\n\tTelegram TelegramConfig `json:\"telegram\"`\n\tDiscord DiscordConfig `json:\"discord\"`\n\tSlack SlackConfig `json:\"slack\"`\n\tWhatsApp WhatsAppConfig `json:\"whatsapp\"`\n\tZalo ZaloConfig `json:\"zalo\"`\n\tZaloPersonal ZaloPersonalConfig `json:\"zalo_personal\"`\n\tFeishu FeishuConfig `json:\"feishu\"`\n\tPendingCompaction *PendingCompactionConfig `json:\"pending_compaction,omitempty\"` // global pending message compaction settings\n}\n\ntype TelegramConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tToken string `json:\"token\"`\n\tProxy string `json:\"proxy,omitempty\"`\n\tAPIServer string `json:\"api_server,omitempty\"` // custom Telegram Bot API server URL (e.g. \"http://localhost:8081\")\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in groups (default true)\n\tMentionMode string `json:\"mention_mode,omitempty\"` // \"strict\" (default) = only respond when mentioned; \"yield\" = respond unless another bot is mentioned\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tDMStream *bool `json:\"dm_stream,omitempty\"` // enable streaming for DMs (default false) — edits placeholder progressively\n\tGroupStream *bool `json:\"group_stream,omitempty\"` // enable streaming for groups (default false) — sends new message, edits progressively\n\tDraftTransport *bool `json:\"draft_transport,omitempty\"` // use sendMessageDraft for DM streaming (default true) — stealth preview, no notifications per edit\n\tReasoningStream *bool `json:\"reasoning_stream,omitempty\"` // show reasoning as separate message when provider emits thinking events (default true)\n\tReactionLevel string `json:\"reaction_level,omitempty\"` // \"off\" (default), \"minimal\", \"full\" — status emoji reactions\n\tMediaMaxBytes int64 `json:\"media_max_bytes,omitempty\"` // max media download size in bytes (default 20MB)\n\tLinkPreview *bool `json:\"link_preview,omitempty\"` // enable URL previews in messages (default true)\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tChatBehavior *ChatBehaviorConfig `json:\"chat_behavior,omitempty\"` // override gateway chat behavior (nil = inherit)\n\tForceIPv4 bool `json:\"force_ipv4,omitempty\"` // force IPv4 for all Telegram API requests (use when IPv6 routing is broken)\n\n\t// Optional STT (Speech-to-Text) pipeline for voice/audio inbound messages.\n\t// When stt_proxy_url is set, audio/voice messages are transcribed before being forwarded to the agent.\n\tSTTProxyURL string `json:\"stt_proxy_url,omitempty\"` // base URL of the STT proxy service (e.g. \"https://stt.example.com\")\n\tSTTAPIKey string `json:\"stt_api_key,omitempty\"` // Bearer token for the STT proxy\n\tSTTTenantID string `json:\"stt_tenant_id,omitempty\"` // optional tenant/org identifier forwarded to the STT proxy\n\tSTTTimeoutSeconds int `json:\"stt_timeout_seconds,omitempty\"` // per-request timeout for STT calls (default 30s)\n\n\t// Optional audio-aware routing: when set, voice/audio inbound messages are routed to this\n\t// agent instead of the default channel agent. Requires the named agent to exist in the config.\n\tVoiceAgentID string `json:\"voice_agent_id,omitempty\"` // agent ID to route voice inbound to (e.g. \"speaking-agent\")\n\n\t// Audio guard: intercept technical errors in voice agent replies and replace with friendly fallbacks.\n\t// Only active when VoiceAgentID is set. Custom error markers replace built-in defaults when provided.\n\tAudioGuardFallbackTranscript string `json:\"audio_guard_fallback_transcript,omitempty\"` // fallback with %s for transcript (e.g. \"I heard: \\\"%s\\\". Try again!\")\n\tAudioGuardFallbackNoTranscript string `json:\"audio_guard_fallback_no_transcript,omitempty\"` // fallback when no transcript available\n\tAudioGuardErrorMarkers []string `json:\"audio_guard_error_markers,omitempty\"` // custom error detection markers (replaces defaults)\n\n\t// Per-group (and per-topic) overrides. Key is chat ID string (e.g. \"-100123456\") or \"*\" for wildcard.\n\t// TS ref: channels.telegram.groups in src/config/types.telegram.ts.\n\tGroups map[string]*TelegramGroupConfig `json:\"groups,omitempty\"`\n}\n\n// TelegramGroupConfig defines per-group overrides for a Telegram channel.\n// Matching TS TelegramGroupConfig in src/config/types.telegram.ts.\ntype TelegramGroupConfig struct {\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // override group policy for this group\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // override require_mention for this group\n\tMentionMode string `json:\"mention_mode,omitempty\"` // override mention_mode for this group\n\tAllowFrom FlexibleStringSlice `json:\"allow_from,omitempty\"` // override allow_from for this group\n\tEnabled *bool `json:\"enabled,omitempty\"` // disable bot for this group (default: true)\n\tSkills []string `json:\"skills,omitempty\"` // skill whitelist (nil = all, [] = none)\n\tTools []string `json:\"tools,omitempty\"` // tool allow list (nil = all, supports \"group:xxx\")\n\tSystemPrompt string `json:\"system_prompt,omitempty\"` // extra system prompt for this group\n\tTopics map[string]*TelegramTopicConfig `json:\"topics,omitempty\"` // per-topic overrides (key: thread ID string)\n\tQuota *QuotaWindow `json:\"quota,omitempty\"` // per-group quota override\n}\n\n// TelegramTopicConfig defines per-topic overrides within a Telegram group.\n// Matching TS TelegramTopicConfig in src/config/types.telegram.ts.\ntype TelegramTopicConfig struct {\n\tRequireMention *bool `json:\"require_mention,omitempty\"`\n\tMentionMode string `json:\"mention_mode,omitempty\"`\n\tGroupPolicy string `json:\"group_policy,omitempty\"`\n\tSkills []string `json:\"skills,omitempty\"`\n\tTools []string `json:\"tools,omitempty\"` // tool allow list (nil = inherit, supports \"group:xxx\")\n\tEnabled *bool `json:\"enabled,omitempty\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from,omitempty\"`\n\tSystemPrompt string `json:\"system_prompt,omitempty\"`\n}\n\ntype DiscordConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tToken string `json:\"token\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in groups (default true)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tChatBehavior *ChatBehaviorConfig `json:\"chat_behavior,omitempty\"` // override gateway chat behavior (nil = inherit)\n\tMediaMaxBytes int64 `json:\"media_max_bytes,omitempty\"` // max media download size (default 25MB)\n\tSTTProxyURL string `json:\"stt_proxy_url,omitempty\"`\n\tSTTAPIKey string `json:\"stt_api_key,omitempty\"`\n\tSTTTenantID string `json:\"stt_tenant_id,omitempty\"`\n\tSTTTimeoutSeconds int `json:\"stt_timeout_seconds,omitempty\"`\n\tVoiceAgentID string `json:\"voice_agent_id,omitempty\"`\n}\n\ntype SlackConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tBotToken string `json:\"bot_token\"` // xoxb-... (Bot User OAuth Token)\n\tAppToken string `json:\"app_token\"` // xapp-... (App-Level Token for Socket Mode)\n\tUserToken string `json:\"user_token,omitempty\"` // xoxp-... (Optional: custom bot identity)\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"pairing\", \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in channels (default true)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tDMStream *bool `json:\"dm_stream,omitempty\"` // enable streaming for DMs (default false)\n\tGroupStream *bool `json:\"group_stream,omitempty\"` // enable streaming for groups (default false)\n\tNativeStream *bool `json:\"native_stream,omitempty\"` // use Slack ChatStreamer API if available (default false)\n\tReactionLevel string `json:\"reaction_level,omitempty\"` // \"off\" (default), \"minimal\", \"full\"\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tChatBehavior *ChatBehaviorConfig `json:\"chat_behavior,omitempty\"` // override gateway chat behavior (nil = inherit)\n\tDebounceDelay *int `json:\"debounce_delay,omitempty\"` // ms delay before dispatching rapid messages (default 300, 0=disabled)\n\tThreadTTL *int `json:\"thread_ttl,omitempty\"` // hours before thread participation expires (default 24, 0=disabled — always require @mention)\n\tMediaMaxBytes int64 `json:\"media_max_bytes,omitempty\"` // max file download size in bytes (default 20MB)\n}\n\ntype WhatsAppConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tAuthDir string `json:\"auth_dir,omitempty\"` // optional: SQLite auth dir override (desktop)\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default for DB instances), \"open\", \"allowlist\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"pairing\" (default for DB instances), \"open\" (default for config), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // only respond in groups when bot is @mentioned (default false)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 200, 0=disabled)\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tChatBehavior *ChatBehaviorConfig `json:\"chat_behavior,omitempty\"` // override gateway chat behavior (nil = inherit)\n}\n\ntype ZaloConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tToken string `json:\"token\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tWebhookURL string `json:\"webhook_url,omitempty\"`\n\tWebhookSecret string `json:\"webhook_secret,omitempty\"`\n\tMediaMaxMB int `json:\"media_max_mb,omitempty\"` // default 5\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tChatBehavior *ChatBehaviorConfig `json:\"chat_behavior,omitempty\"` // override gateway chat behavior (nil = inherit)\n}\n\ntype ZaloPersonalConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in groups (default true)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tCredentialsPath string `json:\"credentials_path,omitempty\"` // path to saved cookies JSON\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tChatBehavior *ChatBehaviorConfig `json:\"chat_behavior,omitempty\"` // override gateway chat behavior (nil = inherit)\n}\n\ntype FeishuConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tAppID string `json:\"app_id\"`\n\tAppSecret string `json:\"app_secret\"`\n\tEncryptKey string `json:\"encrypt_key,omitempty\"`\n\tVerificationToken string `json:\"verification_token,omitempty\"`\n\tDomain string `json:\"domain,omitempty\"` // \"lark\" (default/global), \"feishu\" (China), or custom URL\n\tConnectionMode string `json:\"connection_mode,omitempty\"` // \"websocket\" (default), \"webhook\"\n\tWebhookPort int `json:\"webhook_port,omitempty\"` // default 3000\n\tWebhookPath string `json:\"webhook_path,omitempty\"` // default \"/feishu/events\"\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default)\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default)\n\tGroupAllowFrom FlexibleStringSlice `json:\"group_allow_from,omitempty\"`\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // default true (groups)\n\tTopicSessionMode string `json:\"topic_session_mode,omitempty\"` // \"disabled\" (default)\n\tTextChunkLimit int `json:\"text_chunk_limit,omitempty\"` // default 4000\n\tMediaMaxMB int `json:\"media_max_mb,omitempty\"` // default 30\n\tRenderMode string `json:\"render_mode,omitempty\"` // \"auto\", \"raw\", \"card\"\n\tStreaming *bool `json:\"streaming,omitempty\"` // default true\n\tReactionLevel string `json:\"reaction_level,omitempty\"` // \"off\" (default), \"minimal\", \"full\" — typing emoji reactions\n\tHistoryLimit int `json:\"history_limit,omitempty\"`\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tChatBehavior *ChatBehaviorConfig `json:\"chat_behavior,omitempty\"` // override gateway chat behavior (nil = inherit)\n\tSTTProxyURL string `json:\"stt_proxy_url,omitempty\"`\n\tSTTAPIKey string `json:\"stt_api_key,omitempty\"`\n\tSTTTenantID string `json:\"stt_tenant_id,omitempty\"`\n\tSTTTimeoutSeconds int `json:\"stt_timeout_seconds,omitempty\"`\n\tVoiceAgentID string `json:\"voice_agent_id,omitempty\"`\n}\n\n// ProvidersConfig maps provider name to its config.\ntype ProvidersConfig struct {\n\tAnthropic ProviderConfig `json:\"anthropic\"`\n\tOpenAI ProviderConfig `json:\"openai\"`\n\tOpenRouter ProviderConfig `json:\"openrouter\"`\n\tGroq ProviderConfig `json:\"groq\"`\n\tGemini ProviderConfig `json:\"gemini\"`\n\tDeepSeek ProviderConfig `json:\"deepseek\"`\n\tMistral ProviderConfig `json:\"mistral\"`\n\tXAI ProviderConfig `json:\"xai\"`\n\tMiniMax ProviderConfig `json:\"minimax\"`\n\tCohere ProviderConfig `json:\"cohere\"`\n\tPerplexity ProviderConfig `json:\"perplexity\"`\n\tDashScope ProviderConfig `json:\"dashscope\"`\n\tBailian ProviderConfig `json:\"bailian\"`\n\tZai ProviderConfig `json:\"zai\"`\n\tZaiCoding ProviderConfig `json:\"zai_coding\"`\n\tOllama OllamaConfig `json:\"ollama\"` // local Ollama instance (no API key needed)\n\tOllamaCloud ProviderConfig `json:\"ollama_cloud\"` // Ollama Cloud (API key required)\n\tClaudeCLI ClaudeCLIConfig `json:\"claude_cli\"`\n\tACP ACPConfig `json:\"acp\"`\n\tNovita ProviderConfig `json:\"novita\"` // Novita AI (OpenAI-compatible endpoint)\n\tBytePlus ProviderConfig `json:\"byteplus\"` // BytePlus ModelArk (Seed 2.0)\n\tBytePlusCoding ProviderConfig `json:\"byteplus_coding\"` // BytePlus ModelArk Coding Plan\n\tVertex VertexConfig `json:\"vertex\"` // Google Cloud Vertex AI (OAuth2 service account + ADC)\n}\n\n// VertexConfig configures Google Cloud Vertex AI.\n// Credentials precedence: APIKey (inline JSON) > CredentialsFile (path) > ADC (both empty).\n// ProjectID and Region are required; Model optional (defaults to google/gemini-2.0-flash-001).\ntype VertexConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"` // service account JSON inline (secret — never persist in config.json)\n\tCredentialsFile string `json:\"credentials_file,omitempty\"` // path to service account JSON file\n\tProjectID string `json:\"project_id,omitempty\"`\n\tRegion string `json:\"region,omitempty\"`\n\tModel string `json:\"model,omitempty\"`\n}\n\n// OllamaConfig configures a local (or self-hosted) Ollama instance.\n// No API key is required — Ollama accepts any Bearer token value.\ntype OllamaConfig struct {\n\tHost string `json:\"host\"` // Ollama server base URL, e.g. http://localhost:11434\n}\n\n// ClaudeCLIConfig configures the Claude CLI provider (uses subscription, not API key).\ntype ClaudeCLIConfig struct {\n\tCLIPath string `json:\"cli_path\" yaml:\"cli_path\"` // path to claude binary (default: \"claude\")\n\tModel string `json:\"model\" yaml:\"model\"` // default model alias (default: \"sonnet\")\n\tBaseWorkDir string `json:\"base_work_dir\" yaml:\"base_work_dir\"` // base dir for agent workspaces\n\tPermMode string `json:\"perm_mode\" yaml:\"perm_mode\"` // permission mode (default: \"bypassPermissions\")\n}\n\n// ACPConfig configures the ACP (Agent Client Protocol) provider.\n// Orchestrates any ACP-compatible coding agent (Claude Code, Codex CLI, Gemini CLI) as a subprocess.\ntype ACPConfig struct {\n\tBinary string `json:\"binary\"` // agent binary name or path (e.g. \"claude\", \"codex\")\n\tArgs []string `json:\"args\"` // extra spawn args\n\tModel string `json:\"model\"` // default model/agent name\n\tWorkDir string `json:\"work_dir\"` // base workspace dir\n\tIdleTTL string `json:\"idle_ttl\"` // process idle TTL (e.g. \"5m\")\n\tPermMode string `json:\"perm_mode\"` // \"approve-all\" (default), \"approve-reads\", \"deny-all\"\n}\n\ntype ProviderConfig struct {\n\tAPIKey string `json:\"api_key\"`\n\tAPIBase string `json:\"api_base,omitempty\"`\n}\n\n// APIBaseForType returns the config-level api_base for a given provider type.\n// Used as a fallback when DB providers have no api_base set.\nfunc (p *ProvidersConfig) APIBaseForType(providerType string) string {\n\tswitch providerType {\n\tcase \"anthropic_native\":\n\t\treturn p.Anthropic.APIBase\n\tcase \"openai\", \"openai_compat\":\n\t\treturn p.OpenAI.APIBase\n\tcase \"openrouter\":\n\t\treturn p.OpenRouter.APIBase\n\tcase \"groq\":\n\t\treturn p.Groq.APIBase\n\tcase \"deepseek\":\n\t\treturn p.DeepSeek.APIBase\n\tcase \"gemini_native\":\n\t\treturn p.Gemini.APIBase\n\tcase \"mistral\":\n\t\treturn p.Mistral.APIBase\n\tcase \"xai\":\n\t\treturn p.XAI.APIBase\n\tcase \"minimax_native\":\n\t\treturn p.MiniMax.APIBase\n\tcase \"cohere\":\n\t\treturn p.Cohere.APIBase\n\tcase \"perplexity\":\n\t\treturn p.Perplexity.APIBase\n\tcase \"dashscope\":\n\t\treturn p.DashScope.APIBase\n\tcase \"bailian\":\n\t\treturn p.Bailian.APIBase\n\tcase \"zai\":\n\t\treturn p.Zai.APIBase\n\tcase \"zai_coding\":\n\t\treturn p.ZaiCoding.APIBase\n\tcase \"ollama_cloud\":\n\t\treturn p.OllamaCloud.APIBase\n\tcase \"novita\":\n\t\treturn p.Novita.APIBase\n\tcase \"byteplus\":\n\t\treturn p.BytePlus.APIBase\n\tcase \"byteplus_coding\":\n\t\treturn p.BytePlusCoding.APIBase\n\tcase \"vertex\":\n\t\t// Computed from project+region at registration time; no config-level static base.\n\t\treturn \"\"\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\n// HasAnyProvider returns true if at least one provider has an API key or CLI configured.\nfunc (c *Config) HasAnyProvider() bool {\n\tp := c.Providers\n\treturn p.Anthropic.APIKey != \"\" ||\n\t\tp.OpenAI.APIKey != \"\" ||\n\t\tp.OpenRouter.APIKey != \"\" ||\n\t\tp.Groq.APIKey != \"\" ||\n\t\tp.Gemini.APIKey != \"\" ||\n\t\tp.DeepSeek.APIKey != \"\" ||\n\t\tp.Mistral.APIKey != \"\" ||\n\t\tp.XAI.APIKey != \"\" ||\n\t\tp.MiniMax.APIKey != \"\" ||\n\t\tp.Cohere.APIKey != \"\" ||\n\t\tp.Perplexity.APIKey != \"\" ||\n\t\tp.DashScope.APIKey != \"\" ||\n\t\tp.Bailian.APIKey != \"\" ||\n\t\tp.Zai.APIKey != \"\" ||\n\t\tp.ZaiCoding.APIKey != \"\" ||\n\t\tp.Ollama.Host != \"\" ||\n\t\tp.OllamaCloud.APIKey != \"\" ||\n\t\tp.ClaudeCLI.CLIPath != \"\" ||\n\t\tp.ACP.Binary != \"\" ||\n\t\tp.Novita.APIKey != \"\" ||\n\t\tp.BytePlus.APIKey != \"\" ||\n\t\tp.BytePlusCoding.APIKey != \"\" ||\n\t\t(p.Vertex.ProjectID != \"\" && p.Vertex.Region != \"\")\n}\n\n// QuotaWindow defines request limits per time window. Zero means unlimited.\ntype QuotaWindow struct {\n\tHour int `json:\"hour,omitempty\"` // max requests per hour (0 = unlimited)\n\tDay int `json:\"day,omitempty\"` // max requests per day (0 = unlimited)\n\tWeek int `json:\"week,omitempty\"` // max requests per week (0 = unlimited)\n}\n\n// IsZero returns true if no limits are set.\nfunc (w QuotaWindow) IsZero() bool { return w.Hour == 0 && w.Day == 0 && w.Week == 0 }\n\n// QuotaConfig configures per-user/group request quotas.\n// Config merge priority: Groups > Channels > Providers > Default.\ntype QuotaConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tDefault QuotaWindow `json:\"default\"`\n\tProviders map[string]QuotaWindow `json:\"providers,omitempty\"` // key = provider name (e.g. \"anthropic\")\n\tChannels map[string]QuotaWindow `json:\"channels,omitempty\"` // key = channel name (e.g. \"telegram\")\n\tGroups map[string]QuotaWindow `json:\"groups,omitempty\"` // key = userID (e.g. \"group:telegram:-100123\")\n}\n\n// GatewayConfig controls the gateway server.\ntype GatewayConfig struct {\n\tHost string `json:\"host\"`\n\tPort int `json:\"port\"`\n\tToken string `json:\"token,omitempty\"` // bearer token for WS/HTTP auth\n\tOwnerIDs []string `json:\"owner_ids,omitempty\"` // sender IDs considered \"owner\"\n\tAllowedOrigins []string `json:\"allowed_origins,omitempty\"` // WebSocket CORS whitelist (empty = allow all)\n\tMaxMessageChars int `json:\"max_message_chars,omitempty\"` // max user message characters (default 32000)\n\tRateLimitRPM int `json:\"rate_limit_rpm,omitempty\"` // rate limit: requests per minute per user (default 20, 0 = disabled)\n\tInjectionAction string `json:\"injection_action,omitempty\"` // prompt injection action: \"log\", \"warn\" (default), \"block\", \"off\"\n\tInboundDebounceMs int `json:\"inbound_debounce_ms,omitempty\"` // silence-window in ms that merges rapid channel/Web Chat messages from the same sender/session; 0 disables for text but media-bearing messages still honor a built-in media floor so multi-attachment bursts (#63) coalesce into a single agent run. Agents may override via per-agent agent_config.inbound_debounce_ms.\n\tQuota *QuotaConfig `json:\"quota,omitempty\"` // per-user/group request quotas\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // deliver intermediate text during tool iterations (default false)\n\tChatBehavior *ChatBehaviorConfig `json:\"chat_behavior,omitempty\"` // human-like channel delivery behavior (default disabled)\n\tToolStatus *bool `json:\"tool_status,omitempty\"` // show tool name in streaming preview during tool execution (default true)\n\tTaskRecoveryIntervalSec int `json:\"task_recovery_interval_sec,omitempty\"` // team task recovery ticker interval in seconds (default 300 = 5min)\n\tBackgroundProvider string `json:\"background_provider,omitempty\"` // LLM provider for background workers (vault enrichment, consolidation)\n\tBackgroundModel string `json:\"background_model,omitempty\"` // LLM model for background workers\n}\n\n// ToolsConfig controls tool availability, policy, and web search.\ntype ToolsConfig struct {\n\tProfile string `json:\"profile,omitempty\"` // global profile: \"minimal\", \"coding\", \"messaging\", \"full\"\n\tAllow []string `json:\"allow,omitempty\"` // global allow list (tool names or \"group:xxx\")\n\tDeny []string `json:\"deny,omitempty\"` // global deny list\n\tAlsoAllow []string `json:\"alsoAllow,omitempty\"` // additive: adds without removing existing\n\tByProvider map[string]*ToolPolicySpec `json:\"byProvider,omitempty\"` // per-provider overrides\n\tShellDenyGroups map[string]bool `json:\"shellDenyGroups,omitempty\"` // global shell deny-group toggles (group name -> denied); per-agent overrides win per-key\n\tCommandKeywordAllowlist []CommandKeywordAllowlistRule `json:\"commandKeywordAllowlist,omitempty\"` // scoped bypass for credentialed CLI content keywords\n\tExecApproval ExecApprovalCfg `json:\"execApproval\"` // exec command approval settings\n\tWebFetch WebFetchPolicyConfig `json:\"web_fetch\"` // domain policy for URL fetching\n\tBrowser BrowserToolConfig `json:\"browser\"`\n\tRateLimitPerHour int `json:\"rate_limit_per_hour,omitempty\"` // max tool executions per hour per session (0 = disabled)\n\tScrubCredentials *bool `json:\"scrub_credentials,omitempty\"` // auto-redact API keys/tokens in tool output (default true)\n\tMcpServers map[string]*MCPServerConfig `json:\"mcp_servers,omitempty\"` // external MCP server connections\n}\n\n// CommandKeywordAllowlistRule scopes product/security vocabulary that may appear\n// in credentialed CLI content args without disabling command-path deny patterns.\ntype CommandKeywordAllowlistRule struct {\n\tID string `json:\"id,omitempty\"`\n\tCommand string `json:\"command\"`\n\tSubcommands []string `json:\"subcommands,omitempty\"`\n\tArgs []string `json:\"args,omitempty\"`\n\tArgPositions []int `json:\"argPositions,omitempty\"` // 0-based after matched subcommand\n\tKeywords []string `json:\"keywords,omitempty\"`\n\tReason string `json:\"reason,omitempty\"`\n\tEnabled *bool `json:\"enabled,omitempty\"` // nil means enabled\n}\n\n// MCPServerConfig configures a single external MCP server connection.\ntype MCPServerConfig struct {\n\tTransport string `json:\"transport\"` // \"stdio\", \"sse\", \"streamable-http\"\n\tCommand string `json:\"command,omitempty\"` // stdio: command to spawn\n\tArgs []string `json:\"args,omitempty\"` // stdio: command arguments\n\tEnv map[string]string `json:\"env,omitempty\"` // stdio: extra environment variables\n\tURL string `json:\"url,omitempty\"` // sse/http: server URL\n\tHeaders map[string]string `json:\"headers,omitempty\"` // sse/http: extra HTTP headers\n\tEnabled *bool `json:\"enabled,omitempty\"` // default true\n\tToolPrefix string `json:\"tool_prefix,omitempty\"` // prefix for tool names (avoids collisions)\n\tTimeoutSec int `json:\"timeout_sec,omitempty\"` // per-tool-call timeout in seconds (default 60)\n}\n\n// IsEnabled returns whether this MCP server is enabled (default true).\nfunc (c *MCPServerConfig) IsEnabled() bool {\n\treturn c.Enabled == nil || *c.Enabled\n}\n\n// ExecApprovalCfg configures command execution approval (matching TS exec-approval.ts).\ntype ExecApprovalCfg struct {\n\tSecurity string `json:\"security,omitempty\"` // \"deny\", \"allowlist\", \"full\" (default \"full\")\n\tAsk string `json:\"ask,omitempty\"` // \"off\", \"on-miss\", \"always\" (default \"off\")\n\tAllowlist []string `json:\"allowlist,omitempty\"` // glob patterns for allowed commands\n}\n\n// WebFetchPolicyConfig controls domain filtering for the web_fetch tool.\ntype WebFetchPolicyConfig struct {\n\tPolicy string `json:\"policy,omitempty\"` // \"allow_all\" (default), \"allowlist\"\n\tAllowedDomains []string `json:\"allowed_domains,omitempty\"` // e.g. [\"github.com\", \"*.example.com\"]\n\tBlockedDomains []string `json:\"blocked_domains,omitempty\"` // always checked regardless of policy\n}\n\n// BrowserToolConfig controls the browser automation tool.\ntype BrowserToolConfig struct {\n\tEnabled bool `json:\"enabled\"` // enable the browser tool (default false)\n\tHeadless bool `json:\"headless,omitempty\"` // run Chrome in headless mode (ignored when RemoteURL is set)\n\tRemoteURL string `json:\"remote_url,omitempty\"` // CDP endpoint for remote Chrome sidecar, e.g. \"ws://chrome:9222\"\n\tActionTimeoutMs int `json:\"action_timeout_ms,omitempty\"` // per-action timeout in ms (default 30000)\n\tIdleTimeoutMs int `json:\"idle_timeout_ms,omitempty\"` // idle page auto-close in ms (default 600000, 0=disabled)\n\tMaxPages int `json:\"max_pages,omitempty\"` // max open pages per tenant (default 5)\n\tCookieSyncEnabled bool `json:\"cookie_sync_enabled\"` // apply selected synced cookies to scoped browser sessions\n}\n\n// ToolPolicySpec defines a tool policy at any level (global, per-agent, per-provider).\ntype ToolPolicySpec struct {\n\tProfile string `json:\"profile,omitempty\"`\n\tAllow []string `json:\"allow,omitempty\"`\n\tDeny []string `json:\"deny,omitempty\"`\n\tAlsoAllow []string `json:\"alsoAllow,omitempty\"`\n\tByProvider map[string]*ToolPolicySpec `json:\"byProvider,omitempty\"`\n\tWait *WaitToolPolicy `json:\"wait,omitempty\"`\n\tToolCallPrefix string `json:\"toolCallPrefix,omitempty\"` // prefix to strip from model's tool call names before registry lookup\n}\n\n// WaitToolPolicy configures per-agent safety bounds for the wait tool.\ntype WaitToolPolicy struct {\n\tMinMs int `json:\"min_ms,omitempty\"`\n\tMaxMs int `json:\"max_ms,omitempty\"`\n}\n\n// SessionsConfig controls session behavior.\n// Matching TS src/config/sessions/types.ts + src/config/types.base.ts.\ntype SessionsConfig struct {\n\tScope string `json:\"scope,omitempty\"` // \"per-sender\" (default), \"global\"\n\tDmScope string `json:\"dm_scope,omitempty\"` // \"main\", \"per-peer\", \"per-channel-peer\" (default), \"per-account-channel-peer\"\n\tMainKey string `json:\"main_key,omitempty\"` // main session key suffix (default \"main\", used when dm_scope=\"main\")\n}\n\n// TtsConfig configures text-to-speech.\n// Matching TS src/config/types.tts.ts.\ntype TtsConfig struct {\n\tProvider string `json:\"provider,omitempty\"` // \"openai\", \"elevenlabs\", \"edge\", \"minimax\", \"gemini\"\n\tAuto string `json:\"auto,omitempty\"` // \"off\" (default), \"always\", \"inbound\", \"tagged\"\n\tMode string `json:\"mode,omitempty\"` // \"final\" (default), \"all\"\n\tMaxLength int `json:\"max_length,omitempty\"` // max text length before truncation (default 1500)\n\tTimeoutMs int `json:\"timeout_ms,omitempty\"` // API timeout in ms (default 30000)\n\tOpenAI TtsOpenAIConfig `json:\"openai\"`\n\tElevenLabs TtsElevenLabsConfig `json:\"elevenlabs\"`\n\tEdge TtsEdgeConfig `json:\"edge\"`\n\tMiniMax TtsMiniMaxConfig `json:\"minimax\"`\n\tGemini TtsGeminiConfig `json:\"gemini\"`\n}\n\n// TtsGeminiConfig configures the Google Gemini TTS provider.\ntype TtsGeminiConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"` // required; encrypted at rest\n\tAPIBase string `json:\"api_base,omitempty\"` // custom endpoint (optional; SSRF-gated)\n\tVoice string `json:\"voice,omitempty\"` // default \"Kore\"\n\tModel string `json:\"model,omitempty\"` // default \"gemini-2.5-flash-preview-tts\"\n\tSpeakers string `json:\"speakers,omitempty\"` // JSON-encoded []SpeakerVoice for multi-speaker mode\n}\n\n// TtsOpenAIConfig configures the OpenAI TTS provider.\ntype TtsOpenAIConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"`\n\tAPIBase string `json:\"api_base,omitempty\"` // custom endpoint URL\n\tModel string `json:\"model,omitempty\"` // default \"gpt-4o-mini-tts\"\n\tVoice string `json:\"voice,omitempty\"` // default \"alloy\"\n}\n\n// TtsElevenLabsConfig configures the ElevenLabs TTS provider.\ntype TtsElevenLabsConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"`\n\tBaseURL string `json:\"base_url,omitempty\"`\n\tVoiceID string `json:\"voice_id,omitempty\"` // default \"pMsXgVXv3BLzUgSXRplE\"\n\tModelID string `json:\"model_id,omitempty\"` // default \"eleven_multilingual_v2\"\n}\n\n// TtsEdgeConfig configures the Microsoft Edge TTS provider (free, no API key).\ntype TtsEdgeConfig struct {\n\tEnabled bool `json:\"enabled,omitempty\"`\n\tVoice string `json:\"voice,omitempty\"` // default \"en-US-MichelleNeural\"\n\tRate string `json:\"rate,omitempty\"` // speech rate, e.g. \"+0%\"\n}\n\n// TtsMiniMaxConfig configures the MiniMax TTS provider.\ntype TtsMiniMaxConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"`\n\tGroupID string `json:\"group_id,omitempty\"` // MiniMax GroupId (required)\n\tAPIBase string `json:\"api_base,omitempty\"` // default \"https://api.minimax.io/v1\"\n\tModel string `json:\"model,omitempty\"` // default \"speech-02-hd\"\n\tVoiceID string `json:\"voice_id,omitempty\"` // default \"Wise_Woman\"\n}\n\n// MergeChannelGroupQuotas merges per-group quota overrides from channel configs\n// (e.g., channels.telegram.groups[chatID].quota) into gateway.quota.groups.\n// This allows per-group quotas to be set at the channel level and picked up\n// by the QuotaChecker at runtime.\nfunc MergeChannelGroupQuotas(cfg *Config) {\n\tif cfg.Gateway.Quota == nil {\n\t\treturn\n\t}\n\tif cfg.Gateway.Quota.Groups == nil {\n\t\tcfg.Gateway.Quota.Groups = make(map[string]QuotaWindow)\n\t}\n\n\t// Telegram per-group quotas\n\tfor chatID, groupCfg := range cfg.Channels.Telegram.Groups {\n\t\tif groupCfg != nil && groupCfg.Quota != nil && !groupCfg.Quota.IsZero() {\n\t\t\tkey := \"group:telegram:\" + chatID\n\t\t\tcfg.Gateway.Quota.Groups[key] = *groupCfg.Quota\n\t\t}\n\t}\n}\n"}, "f771cff77c8ed9b6f27d8450e99921dad412fc59": {"t": 1785759231.5761716, "n": 35426, "h": "09f5f5f24efa71a9", "c": "package config\n\n// PendingCompactionConfig configures LLM-based compaction of pending group messages.\n// When a group accumulates more than Threshold pending messages, older messages are\n// summarized by an LLM and replaced with a compact summary, keeping KeepRecent raw messages.\ntype PendingCompactionConfig struct {\n\tThreshold int `json:\"threshold,omitempty\"` // trigger compaction when entries exceed this (default 200)\n\tKeepRecent int `json:\"keep_recent,omitempty\"` // keep this many recent raw messages after compaction (default 40)\n\tMaxTokens int `json:\"max_tokens,omitempty\"` // max output tokens for LLM summarization (default 4096)\n\tProvider string `json:\"provider,omitempty\"` // LLM provider name (e.g. \"openai\"); empty = use agent's provider\n\tModel string `json:\"model,omitempty\"` // model for summarization; empty = use agent's model\n}\n\n// ChannelsConfig contains per-channel configuration.\ntype ChannelsConfig struct {\n\tTelegram TelegramConfig `json:\"telegram\"`\n\tDiscord DiscordConfig `json:\"discord\"`\n\tSlack SlackConfig `json:\"slack\"`\n\tWhatsApp WhatsAppConfig `json:\"whatsapp\"`\n\tZalo ZaloConfig `json:\"zalo\"`\n\tZaloPersonal ZaloPersonalConfig `json:\"zalo_personal\"`\n\tFeishu FeishuConfig `json:\"feishu\"`\n\tPendingCompaction *PendingCompactionConfig `json:\"pending_compaction,omitempty\"` // global pending message compaction settings\n}\n\ntype TelegramConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tToken string `json:\"token\"`\n\tProxy string `json:\"proxy,omitempty\"`\n\tAPIServer string `json:\"api_server,omitempty\"` // custom Telegram Bot API server URL (e.g. \"http://localhost:8081\")\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in groups (default true)\n\tMentionMode string `json:\"mention_mode,omitempty\"` // \"strict\" (default) = only respond when mentioned; \"yield\" = respond unless another bot is mentioned\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tDMStream *bool `json:\"dm_stream,omitempty\"` // enable streaming for DMs (default false) — edits placeholder progressively\n\tGroupStream *bool `json:\"group_stream,omitempty\"` // enable streaming for groups (default false) — sends new message, edits progressively\n\tDraftTransport *bool `json:\"draft_transport,omitempty\"` // use sendMessageDraft for DM streaming (default true) — stealth preview, no notifications per edit\n\tReasoningStream *bool `json:\"reasoning_stream,omitempty\"` // show reasoning as separate message when provider emits thinking events (default true)\n\tReactionLevel string `json:\"reaction_level,omitempty\"` // \"off\" (default), \"minimal\", \"full\" — status emoji reactions\n\tMediaMaxBytes int64 `json:\"media_max_bytes,omitempty\"` // max media download size in bytes (default 20MB)\n\tLinkPreview *bool `json:\"link_preview,omitempty\"` // enable URL previews in messages (default true)\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tForceIPv4 bool `json:\"force_ipv4,omitempty\"` // force IPv4 for all Telegram API requests (use when IPv6 routing is broken)\n\n\t// Optional STT (Speech-to-Text) pipeline for voice/audio inbound messages.\n\t// When stt_proxy_url is set, audio/voice messages are transcribed before being forwarded to the agent.\n\tSTTProxyURL string `json:\"stt_proxy_url,omitempty\"` // base URL of the STT proxy service (e.g. \"https://stt.example.com\")\n\tSTTAPIKey string `json:\"stt_api_key,omitempty\"` // Bearer token for the STT proxy\n\tSTTTenantID string `json:\"stt_tenant_id,omitempty\"` // optional tenant/org identifier forwarded to the STT proxy\n\tSTTTimeoutSeconds int `json:\"stt_timeout_seconds,omitempty\"` // per-request timeout for STT calls (default 30s)\n\n\t// Optional audio-aware routing: when set, voice/audio inbound messages are routed to this\n\t// agent instead of the default channel agent. Requires the named agent to exist in the config.\n\tVoiceAgentID string `json:\"voice_agent_id,omitempty\"` // agent ID to route voice inbound to (e.g. \"speaking-agent\")\n\n\t// Audio guard: intercept technical errors in voice agent replies and replace with friendly fallbacks.\n\t// Only active when VoiceAgentID is set. Custom error markers replace built-in defaults when provided.\n\tAudioGuardFallbackTranscript string `json:\"audio_guard_fallback_transcript,omitempty\"` // fallback with %s for transcript (e.g. \"I heard: \\\"%s\\\". Try again!\")\n\tAudioGuardFallbackNoTranscript string `json:\"audio_guard_fallback_no_transcript,omitempty\"` // fallback when no transcript available\n\tAudioGuardErrorMarkers []string `json:\"audio_guard_error_markers,omitempty\"` // custom error detection markers (replaces defaults)\n\n\t// Per-group (and per-topic) overrides. Key is chat ID string (e.g. \"-100123456\") or \"*\" for wildcard.\n\t// TS ref: channels.telegram.groups in src/config/types.telegram.ts.\n\tGroups map[string]*TelegramGroupConfig `json:\"groups,omitempty\"`\n}\n\n// TelegramGroupConfig defines per-group overrides for a Telegram channel.\n// Matching TS TelegramGroupConfig in src/config/types.telegram.ts.\ntype TelegramGroupConfig struct {\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // override group policy for this group\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // override require_mention for this group\n\tMentionMode string `json:\"mention_mode,omitempty\"` // override mention_mode for this group\n\tAllowFrom FlexibleStringSlice `json:\"allow_from,omitempty\"` // override allow_from for this group\n\tEnabled *bool `json:\"enabled,omitempty\"` // disable bot for this group (default: true)\n\tSkills []string `json:\"skills,omitempty\"` // skill whitelist (nil = all, [] = none)\n\tTools []string `json:\"tools,omitempty\"` // tool allow list (nil = all, supports \"group:xxx\")\n\tSystemPrompt string `json:\"system_prompt,omitempty\"` // extra system prompt for this group\n\tTopics map[string]*TelegramTopicConfig `json:\"topics,omitempty\"` // per-topic overrides (key: thread ID string)\n\tQuota *QuotaWindow `json:\"quota,omitempty\"` // per-group quota override\n}\n\n// TelegramTopicConfig defines per-topic overrides within a Telegram group.\n// Matching TS TelegramTopicConfig in src/config/types.telegram.ts.\ntype TelegramTopicConfig struct {\n\tRequireMention *bool `json:\"require_mention,omitempty\"`\n\tMentionMode string `json:\"mention_mode,omitempty\"`\n\tGroupPolicy string `json:\"group_policy,omitempty\"`\n\tSkills []string `json:\"skills,omitempty\"`\n\tTools []string `json:\"tools,omitempty\"` // tool allow list (nil = inherit, supports \"group:xxx\")\n\tEnabled *bool `json:\"enabled,omitempty\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from,omitempty\"`\n\tSystemPrompt string `json:\"system_prompt,omitempty\"`\n}\n\ntype DiscordConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tToken string `json:\"token\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in groups (default true)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tMediaMaxBytes int64 `json:\"media_max_bytes,omitempty\"` // max media download size (default 25MB)\n\tSTTProxyURL string `json:\"stt_proxy_url,omitempty\"`\n\tSTTAPIKey string `json:\"stt_api_key,omitempty\"`\n\tSTTTenantID string `json:\"stt_tenant_id,omitempty\"`\n\tSTTTimeoutSeconds int `json:\"stt_timeout_seconds,omitempty\"`\n\tVoiceAgentID string `json:\"voice_agent_id,omitempty\"`\n}\n\ntype SlackConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tBotToken string `json:\"bot_token\"` // xoxb-... (Bot User OAuth Token)\n\tAppToken string `json:\"app_token\"` // xapp-... (App-Level Token for Socket Mode)\n\tUserToken string `json:\"user_token,omitempty\"` // xoxp-... (Optional: custom bot identity)\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"pairing\", \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in channels (default true)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tDMStream *bool `json:\"dm_stream,omitempty\"` // enable streaming for DMs (default false)\n\tGroupStream *bool `json:\"group_stream,omitempty\"` // enable streaming for groups (default false)\n\tNativeStream *bool `json:\"native_stream,omitempty\"` // use Slack ChatStreamer API if available (default false)\n\tReactionLevel string `json:\"reaction_level,omitempty\"` // \"off\" (default), \"minimal\", \"full\"\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tDebounceDelay *int `json:\"debounce_delay,omitempty\"` // ms delay before dispatching rapid messages (default 300, 0=disabled)\n\tThreadTTL *int `json:\"thread_ttl,omitempty\"` // hours before thread participation expires (default 24, 0=disabled — always require @mention)\n\tMediaMaxBytes int64 `json:\"media_max_bytes,omitempty\"` // max file download size in bytes (default 20MB)\n}\n\ntype WhatsAppConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tAuthDir string `json:\"auth_dir,omitempty\"` // optional: SQLite auth dir override (desktop)\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default for DB instances), \"open\", \"allowlist\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"pairing\" (default for DB instances), \"open\" (default for config), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // only respond in groups when bot is @mentioned (default false)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 200, 0=disabled)\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n}\n\ntype ZaloConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tToken string `json:\"token\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tWebhookURL string `json:\"webhook_url,omitempty\"`\n\tWebhookSecret string `json:\"webhook_secret,omitempty\"`\n\tMediaMaxMB int `json:\"media_max_mb,omitempty\"` // default 5\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n}\n\ntype ZaloPersonalConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in groups (default true)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tCredentialsPath string `json:\"credentials_path,omitempty\"` // path to saved cookies JSON\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n}\n\ntype FeishuConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tAppID string `json:\"app_id\"`\n\tAppSecret string `json:\"app_secret\"`\n\tEncryptKey string `json:\"encrypt_key,omitempty\"`\n\tVerificationToken string `json:\"verification_token,omitempty\"`\n\tDomain string `json:\"domain,omitempty\"` // \"lark\" (default/global), \"feishu\" (China), or custom URL\n\tConnectionMode string `json:\"connection_mode,omitempty\"` // \"websocket\" (default), \"webhook\"\n\tWebhookPort int `json:\"webhook_port,omitempty\"` // default 3000\n\tWebhookPath string `json:\"webhook_path,omitempty\"` // default \"/feishu/events\"\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default)\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default)\n\tGroupAllowFrom FlexibleStringSlice `json:\"group_allow_from,omitempty\"`\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // default true (groups)\n\tTopicSessionMode string `json:\"topic_session_mode,omitempty\"` // \"disabled\" (default)\n\tTextChunkLimit int `json:\"text_chunk_limit,omitempty\"` // default 4000\n\tMediaMaxMB int `json:\"media_max_mb,omitempty\"` // default 30\n\tRenderMode string `json:\"render_mode,omitempty\"` // \"auto\", \"raw\", \"card\"\n\tStreaming *bool `json:\"streaming,omitempty\"` // default true\n\tReactionLevel string `json:\"reaction_level,omitempty\"` // \"off\" (default), \"minimal\", \"full\" — typing emoji reactions\n\tHistoryLimit int `json:\"history_limit,omitempty\"`\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tSTTProxyURL string `json:\"stt_proxy_url,omitempty\"`\n\tSTTAPIKey string `json:\"stt_api_key,omitempty\"`\n\tSTTTenantID string `json:\"stt_tenant_id,omitempty\"`\n\tSTTTimeoutSeconds int `json:\"stt_timeout_seconds,omitempty\"`\n\tVoiceAgentID string `json:\"voice_agent_id,omitempty\"`\n}\n\n// ProvidersConfig maps provider name to its config.\ntype ProvidersConfig struct {\n\tAnthropic ProviderConfig `json:\"anthropic\"`\n\tOpenAI ProviderConfig `json:\"openai\"`\n\tOpenRouter ProviderConfig `json:\"openrouter\"`\n\tGroq ProviderConfig `json:\"groq\"`\n\tGemini ProviderConfig `json:\"gemini\"`\n\tDeepSeek ProviderConfig `json:\"deepseek\"`\n\tMistral ProviderConfig `json:\"mistral\"`\n\tXAI ProviderConfig `json:\"xai\"`\n\tMiniMax ProviderConfig `json:\"minimax\"`\n\tCohere ProviderConfig `json:\"cohere\"`\n\tPerplexity ProviderConfig `json:\"perplexity\"`\n\tDashScope ProviderConfig `json:\"dashscope\"`\n\tBailian ProviderConfig `json:\"bailian\"`\n\tZai ProviderConfig `json:\"zai\"`\n\tZaiCoding ProviderConfig `json:\"zai_coding\"`\n\tOllama OllamaConfig `json:\"ollama\"` // local Ollama instance (no API key needed)\n\tOllamaCloud ProviderConfig `json:\"ollama_cloud\"` // Ollama Cloud (API key required)\n\tClaudeCLI ClaudeCLIConfig `json:\"claude_cli\"`\n\tACP ACPConfig `json:\"acp\"`\n\tNovita ProviderConfig `json:\"novita\"` // Novita AI (OpenAI-compatible endpoint)\n\tBytePlus ProviderConfig `json:\"byteplus\"` // BytePlus ModelArk (Seed 2.0)\n\tBytePlusCoding ProviderConfig `json:\"byteplus_coding\"` // BytePlus ModelArk Coding Plan\n\tVertex VertexConfig `json:\"vertex\"` // Google Cloud Vertex AI (OAuth2 service account + ADC)\n}\n\n// VertexConfig configures Google Cloud Vertex AI.\n// Credentials precedence: APIKey (inline JSON) > CredentialsFile (path) > ADC (both empty).\n// ProjectID and Region are required; Model optional (defaults to google/gemini-2.0-flash-001).\ntype VertexConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"` // service account JSON inline (secret — never persist in config.json)\n\tCredentialsFile string `json:\"credentials_file,omitempty\"` // path to service account JSON file\n\tProjectID string `json:\"project_id,omitempty\"`\n\tRegion string `json:\"region,omitempty\"`\n\tModel string `json:\"model,omitempty\"`\n}\n\n// OllamaConfig configures a local (or self-hosted) Ollama instance.\n// No API key is required — Ollama accepts any Bearer token value.\ntype OllamaConfig struct {\n\tHost string `json:\"host\"` // Ollama server base URL, e.g. http://localhost:11434\n}\n\n// ClaudeCLIConfig configures the Claude CLI provider (uses subscription, not API key).\ntype ClaudeCLIConfig struct {\n\tCLIPath string `json:\"cli_path\" yaml:\"cli_path\"` // path to claude binary (default: \"claude\")\n\tModel string `json:\"model\" yaml:\"model\"` // default model alias (default: \"sonnet\")\n\tBaseWorkDir string `json:\"base_work_dir\" yaml:\"base_work_dir\"` // base dir for agent workspaces\n\tPermMode string `json:\"perm_mode\" yaml:\"perm_mode\"` // permission mode (default: \"bypassPermissions\")\n}\n\n// ACPConfig configures the ACP (Agent Client Protocol) provider.\n// Orchestrates any ACP-compatible coding agent (Claude Code, Codex CLI, Gemini CLI) as a subprocess.\ntype ACPConfig struct {\n\tBinary string `json:\"binary\"` // agent binary name or path (e.g. \"claude\", \"codex\")\n\tArgs []string `json:\"args\"` // extra spawn args\n\tModel string `json:\"model\"` // default model/agent name\n\tWorkDir string `json:\"work_dir\"` // base workspace dir\n\tIdleTTL string `json:\"idle_ttl\"` // process idle TTL (e.g. \"5m\")\n\tPermMode string `json:\"perm_mode\"` // \"approve-all\" (default), \"approve-reads\", \"deny-all\"\n}\n\ntype ProviderConfig struct {\n\tAPIKey string `json:\"api_key\"`\n\tAPIBase string `json:\"api_base,omitempty\"`\n}\n\n// APIBaseForType returns the config-level api_base for a given provider type.\n// Used as a fallback when DB providers have no api_base set.\nfunc (p *ProvidersConfig) APIBaseForType(providerType string) string {\n\tswitch providerType {\n\tcase \"anthropic_native\":\n\t\treturn p.Anthropic.APIBase\n\tcase \"openai\", \"openai_compat\":\n\t\treturn p.OpenAI.APIBase\n\tcase \"openrouter\":\n\t\treturn p.OpenRouter.APIBase\n\tcase \"groq\":\n\t\treturn p.Groq.APIBase\n\tcase \"deepseek\":\n\t\treturn p.DeepSeek.APIBase\n\tcase \"gemini_native\":\n\t\treturn p.Gemini.APIBase\n\tcase \"mistral\":\n\t\treturn p.Mistral.APIBase\n\tcase \"xai\":\n\t\treturn p.XAI.APIBase\n\tcase \"minimax_native\":\n\t\treturn p.MiniMax.APIBase\n\tcase \"cohere\":\n\t\treturn p.Cohere.APIBase\n\tcase \"perplexity\":\n\t\treturn p.Perplexity.APIBase\n\tcase \"dashscope\":\n\t\treturn p.DashScope.APIBase\n\tcase \"bailian\":\n\t\treturn p.Bailian.APIBase\n\tcase \"zai\":\n\t\treturn p.Zai.APIBase\n\tcase \"zai_coding\":\n\t\treturn p.ZaiCoding.APIBase\n\tcase \"ollama_cloud\":\n\t\treturn p.OllamaCloud.APIBase\n\tcase \"novita\":\n\t\treturn p.Novita.APIBase\n\tcase \"byteplus\":\n\t\treturn p.BytePlus.APIBase\n\tcase \"byteplus_coding\":\n\t\treturn p.BytePlusCoding.APIBase\n\tcase \"vertex\":\n\t\t// Computed from project+region at registration time; no config-level static base.\n\t\treturn \"\"\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\n// HasAnyProvider returns true if at least one provider has an API key or CLI configured.\nfunc (c *Config) HasAnyProvider() bool {\n\tp := c.Providers\n\treturn p.Anthropic.APIKey != \"\" ||\n\t\tp.OpenAI.APIKey != \"\" ||\n\t\tp.OpenRouter.APIKey != \"\" ||\n\t\tp.Groq.APIKey != \"\" ||\n\t\tp.Gemini.APIKey != \"\" ||\n\t\tp.DeepSeek.APIKey != \"\" ||\n\t\tp.Mistral.APIKey != \"\" ||\n\t\tp.XAI.APIKey != \"\" ||\n\t\tp.MiniMax.APIKey != \"\" ||\n\t\tp.Cohere.APIKey != \"\" ||\n\t\tp.Perplexity.APIKey != \"\" ||\n\t\tp.DashScope.APIKey != \"\" ||\n\t\tp.Bailian.APIKey != \"\" ||\n\t\tp.Zai.APIKey != \"\" ||\n\t\tp.ZaiCoding.APIKey != \"\" ||\n\t\tp.Ollama.Host != \"\" ||\n\t\tp.OllamaCloud.APIKey != \"\" ||\n\t\tp.ClaudeCLI.CLIPath != \"\" ||\n\t\tp.ACP.Binary != \"\" ||\n\t\tp.Novita.APIKey != \"\" ||\n\t\tp.BytePlus.APIKey != \"\" ||\n\t\tp.BytePlusCoding.APIKey != \"\" ||\n\t\t(p.Vertex.ProjectID != \"\" && p.Vertex.Region != \"\")\n}\n\n// QuotaWindow defines request limits per time window. Zero means unlimited.\ntype QuotaWindow struct {\n\tHour int `json:\"hour,omitempty\"` // max requests per hour (0 = unlimited)\n\tDay int `json:\"day,omitempty\"` // max requests per day (0 = unlimited)\n\tWeek int `json:\"week,omitempty\"` // max requests per week (0 = unlimited)\n}\n\n// IsZero returns true if no limits are set.\nfunc (w QuotaWindow) IsZero() bool { return w.Hour == 0 && w.Day == 0 && w.Week == 0 }\n\n// QuotaConfig configures per-user/group request quotas.\n// Config merge priority: Groups > Channels > Providers > Default.\ntype QuotaConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tDefault QuotaWindow `json:\"default\"`\n\tProviders map[string]QuotaWindow `json:\"providers,omitempty\"` // key = provider name (e.g. \"anthropic\")\n\tChannels map[string]QuotaWindow `json:\"channels,omitempty\"` // key = channel name (e.g. \"telegram\")\n\tGroups map[string]QuotaWindow `json:\"groups,omitempty\"` // key = userID (e.g. \"group:telegram:-100123\")\n}\n\n// GatewayConfig controls the gateway server.\ntype GatewayConfig struct {\n\tHost string `json:\"host\"`\n\tPort int `json:\"port\"`\n\tToken string `json:\"token,omitempty\"` // bearer token for WS/HTTP auth\n\tOwnerIDs []string `json:\"owner_ids,omitempty\"` // sender IDs considered \"owner\"\n\tAllowedOrigins []string `json:\"allowed_origins,omitempty\"` // WebSocket CORS whitelist (empty = allow all)\n\tMaxMessageChars int `json:\"max_message_chars,omitempty\"` // max user message characters (default 32000)\n\tRateLimitRPM int `json:\"rate_limit_rpm,omitempty\"` // rate limit: requests per minute per user (default 20, 0 = disabled)\n\tInjectionAction string `json:\"injection_action,omitempty\"` // prompt injection action: \"log\", \"warn\" (default), \"block\", \"off\"\n\tInboundDebounceMs int `json:\"inbound_debounce_ms,omitempty\"` // silence-window in ms that merges rapid channel/Web Chat messages from the same sender/session; 0 disables for text but media-bearing messages still honor a built-in media floor so multi-attachment bursts (#63) coalesce into a single agent run. Agents may override via per-agent agent_config.inbound_debounce_ms.\n\tQuota *QuotaConfig `json:\"quota,omitempty\"` // per-user/group request quotas\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // deliver intermediate text during tool iterations (default false)\n\tToolStatus *bool `json:\"tool_status,omitempty\"` // show tool name in streaming preview during tool execution (default true)\n\tTaskRecoveryIntervalSec int `json:\"task_recovery_interval_sec,omitempty\"` // team task recovery ticker interval in seconds (default 300 = 5min)\n\tBackgroundProvider string `json:\"background_provider,omitempty\"` // LLM provider for background workers (vault enrichment, consolidation)\n\tBackgroundModel string `json:\"background_model,omitempty\"` // LLM model for background workers\n}\n\n// ToolsConfig controls tool availability, policy, and web search.\ntype ToolsConfig struct {\n\tProfile string `json:\"profile,omitempty\"` // global profile: \"minimal\", \"coding\", \"messaging\", \"full\"\n\tAllow []string `json:\"allow,omitempty\"` // global allow list (tool names or \"group:xxx\")\n\tDeny []string `json:\"deny,omitempty\"` // global deny list\n\tAlsoAllow []string `json:\"alsoAllow,omitempty\"` // additive: adds without removing existing\n\tByProvider map[string]*ToolPolicySpec `json:\"byProvider,omitempty\"` // per-provider overrides\n\tShellDenyGroups map[string]bool `json:\"shellDenyGroups,omitempty\"` // global shell deny-group toggles (group name -> denied); per-agent overrides win per-key\n\tCommandKeywordAllowlist []CommandKeywordAllowlistRule `json:\"commandKeywordAllowlist,omitempty\"` // scoped bypass for credentialed CLI content keywords\n\tExecApproval ExecApprovalCfg `json:\"execApproval\"` // exec command approval settings\n\tWebFetch WebFetchPolicyConfig `json:\"web_fetch\"` // domain policy for URL fetching\n\tBrowser BrowserToolConfig `json:\"browser\"`\n\tRateLimitPerHour int `json:\"rate_limit_per_hour,omitempty\"` // max tool executions per hour per session (0 = disabled)\n\tScrubCredentials *bool `json:\"scrub_credentials,omitempty\"` // auto-redact API keys/tokens in tool output (default true)\n\tMcpServers map[string]*MCPServerConfig `json:\"mcp_servers,omitempty\"` // external MCP server connections\n}\n\n// CommandKeywordAllowlistRule scopes product/security vocabulary that may appear\n// in credentialed CLI content args without disabling command-path deny patterns.\ntype CommandKeywordAllowlistRule struct {\n\tID string `json:\"id,omitempty\"`\n\tCommand string `json:\"command\"`\n\tSubcommands []string `json:\"subcommands,omitempty\"`\n\tArgs []string `json:\"args,omitempty\"`\n\tArgPositions []int `json:\"argPositions,omitempty\"` // 0-based after matched subcommand\n\tKeywords []string `json:\"keywords,omitempty\"`\n\tReason string `json:\"reason,omitempty\"`\n\tEnabled *bool `json:\"enabled,omitempty\"` // nil means enabled\n}\n\n// MCPServerConfig configures a single external MCP server connection.\ntype MCPServerConfig struct {\n\tTransport string `json:\"transport\"` // \"stdio\", \"sse\", \"streamable-http\"\n\tCommand string `json:\"command,omitempty\"` // stdio: command to spawn\n\tArgs []string `json:\"args,omitempty\"` // stdio: command arguments\n\tEnv map[string]string `json:\"env,omitempty\"` // stdio: extra environment variables\n\tURL string `json:\"url,omitempty\"` // sse/http: server URL\n\tHeaders map[string]string `json:\"headers,omitempty\"` // sse/http: extra HTTP headers\n\tEnabled *bool `json:\"enabled,omitempty\"` // default true\n\tToolPrefix string `json:\"tool_prefix,omitempty\"` // prefix for tool names (avoids collisions)\n\tTimeoutSec int `json:\"timeout_sec,omitempty\"` // per-tool-call timeout in seconds (default 60)\n}\n\n// IsEnabled returns whether this MCP server is enabled (default true).\nfunc (c *MCPServerConfig) IsEnabled() bool {\n\treturn c.Enabled == nil || *c.Enabled\n}\n\n// ExecApprovalCfg configures command execution approval (matching TS exec-approval.ts).\ntype ExecApprovalCfg struct {\n\tSecurity string `json:\"security,omitempty\"` // \"deny\", \"allowlist\", \"full\" (default \"full\")\n\tAsk string `json:\"ask,omitempty\"` // \"off\", \"on-miss\", \"always\" (default \"off\")\n\tAllowlist []string `json:\"allowlist,omitempty\"` // glob patterns for allowed commands\n}\n\n// WebFetchPolicyConfig controls domain filtering for the web_fetch tool.\ntype WebFetchPolicyConfig struct {\n\tPolicy string `json:\"policy,omitempty\"` // \"allow_all\" (default), \"allowlist\"\n\tAllowedDomains []string `json:\"allowed_domains,omitempty\"` // e.g. [\"github.com\", \"*.example.com\"]\n\tBlockedDomains []string `json:\"blocked_domains,omitempty\"` // always checked regardless of policy\n}\n\n// BrowserToolConfig controls the browser automation tool.\ntype BrowserToolConfig struct {\n\tEnabled bool `json:\"enabled\"` // enable the browser tool (default false)\n\tHeadless bool `json:\"headless,omitempty\"` // run Chrome in headless mode (ignored when RemoteURL is set)\n\tRemoteURL string `json:\"remote_url,omitempty\"` // CDP endpoint for remote Chrome sidecar, e.g. \"ws://chrome:9222\"\n\tActionTimeoutMs int `json:\"action_timeout_ms,omitempty\"` // per-action timeout in ms (default 30000)\n\tIdleTimeoutMs int `json:\"idle_timeout_ms,omitempty\"` // idle page auto-close in ms (default 600000, 0=disabled)\n\tMaxPages int `json:\"max_pages,omitempty\"` // max open pages per tenant (default 5)\n\tCookieSyncEnabled bool `json:\"cookie_sync_enabled\"` // apply selected synced cookies to scoped browser sessions\n}\n\n// ToolPolicySpec defines a tool policy at any level (global, per-agent, per-provider).\ntype ToolPolicySpec struct {\n\tProfile string `json:\"profile,omitempty\"`\n\tAllow []string `json:\"allow,omitempty\"`\n\tDeny []string `json:\"deny,omitempty\"`\n\tAlsoAllow []string `json:\"alsoAllow,omitempty\"`\n\tByProvider map[string]*ToolPolicySpec `json:\"byProvider,omitempty\"`\n\tWait *WaitToolPolicy `json:\"wait,omitempty\"`\n\tToolCallPrefix string `json:\"toolCallPrefix,omitempty\"` // prefix to strip from model's tool call names before registry lookup\n}\n\n// WaitToolPolicy configures per-agent safety bounds for the wait tool.\ntype WaitToolPolicy struct {\n\tMinMs int `json:\"min_ms,omitempty\"`\n\tMaxMs int `json:\"max_ms,omitempty\"`\n}\n\n// SessionsConfig controls session behavior.\n// Matching TS src/config/sessions/types.ts + src/config/types.base.ts.\ntype SessionsConfig struct {\n\tScope string `json:\"scope,omitempty\"` // \"per-sender\" (default), \"global\"\n\tDmScope string `json:\"dm_scope,omitempty\"` // \"main\", \"per-peer\", \"per-channel-peer\" (default), \"per-account-channel-peer\"\n\tMainKey string `json:\"main_key,omitempty\"` // main session key suffix (default \"main\", used when dm_scope=\"main\")\n}\n\n// TtsConfig configures text-to-speech.\n// Matching TS src/config/types.tts.ts.\ntype TtsConfig struct {\n\tProvider string `json:\"provider,omitempty\"` // \"openai\", \"elevenlabs\", \"edge\", \"minimax\", \"gemini\"\n\tAuto string `json:\"auto,omitempty\"` // \"off\" (default), \"always\", \"inbound\", \"tagged\"\n\tMode string `json:\"mode,omitempty\"` // \"final\" (default), \"all\"\n\tMaxLength int `json:\"max_length,omitempty\"` // max text length before truncation (default 1500)\n\tTimeoutMs int `json:\"timeout_ms,omitempty\"` // API timeout in ms (default 30000)\n\tOpenAI TtsOpenAIConfig `json:\"openai\"`\n\tElevenLabs TtsElevenLabsConfig `json:\"elevenlabs\"`\n\tEdge TtsEdgeConfig `json:\"edge\"`\n\tMiniMax TtsMiniMaxConfig `json:\"minimax\"`\n\tGemini TtsGeminiConfig `json:\"gemini\"`\n}\n\n// TtsGeminiConfig configures the Google Gemini TTS provider.\ntype TtsGeminiConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"` // required; encrypted at rest\n\tAPIBase string `json:\"api_base,omitempty\"` // custom endpoint (optional; SSRF-gated)\n\tVoice string `json:\"voice,omitempty\"` // default \"Kore\"\n\tModel string `json:\"model,omitempty\"` // default \"gemini-2.5-flash-preview-tts\"\n\tSpeakers string `json:\"speakers,omitempty\"` // JSON-encoded []SpeakerVoice for multi-speaker mode\n}\n\n// TtsOpenAIConfig configures the OpenAI TTS provider.\ntype TtsOpenAIConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"`\n\tAPIBase string `json:\"api_base,omitempty\"` // custom endpoint URL\n\tModel string `json:\"model,omitempty\"` // default \"gpt-4o-mini-tts\"\n\tVoice string `json:\"voice,omitempty\"` // default \"alloy\"\n}\n\n// TtsElevenLabsConfig configures the ElevenLabs TTS provider.\ntype TtsElevenLabsConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"`\n\tBaseURL string `json:\"base_url,omitempty\"`\n\tVoiceID string `json:\"voice_id,omitempty\"` // default \"pMsXgVXv3BLzUgSXRplE\"\n\tModelID string `json:\"model_id,omitempty\"` // default \"eleven_multilingual_v2\"\n}\n\n// TtsEdgeConfig configures the Microsoft Edge TTS provider (free, no API key).\ntype TtsEdgeConfig struct {\n\tEnabled bool `json:\"enabled,omitempty\"`\n\tVoice string `json:\"voice,omitempty\"` // default \"en-US-MichelleNeural\"\n\tRate string `json:\"rate,omitempty\"` // speech rate, e.g. \"+0%\"\n}\n\n// TtsMiniMaxConfig configures the MiniMax TTS provider.\ntype TtsMiniMaxConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"`\n\tGroupID string `json:\"group_id,omitempty\"` // MiniMax GroupId (required)\n\tAPIBase string `json:\"api_base,omitempty\"` // default \"https://api.minimax.io/v1\"\n\tModel string `json:\"model,omitempty\"` // default \"speech-02-hd\"\n\tVoiceID string `json:\"voice_id,omitempty\"` // default \"Wise_Woman\"\n}\n\n// MergeChannelGroupQuotas merges per-group quota overrides from channel configs\n// (e.g., channels.telegram.groups[chatID].quota) into gateway.quota.groups.\n// This allows per-group quotas to be set at the channel level and picked up\n// by the QuotaChecker at runtime.\nfunc MergeChannelGroupQuotas(cfg *Config) {\n\tif cfg.Gateway.Quota == nil {\n\t\treturn\n\t}\n\tif cfg.Gateway.Quota.Groups == nil {\n\t\tcfg.Gateway.Quota.Groups = make(map[string]QuotaWindow)\n\t}\n\n\t// Telegram per-group quotas\n\tfor chatID, groupCfg := range cfg.Channels.Telegram.Groups {\n\t\tif groupCfg != nil && groupCfg.Quota != nil && !groupCfg.Quota.IsZero() {\n\t\t\tkey := \"group:telegram:\" + chatID\n\t\t\tcfg.Gateway.Quota.Groups[key] = *groupCfg.Quota\n\t\t}\n\t}\n}\n"}, "61fdcdcb8122a5a1b894d0da8533a8b7f8b7ed66": {"t": 1785759233.2049685, "n": 35191, "h": "d89e5ee5cf482cdd", "c": "package config\n\n// PendingCompactionConfig configures LLM-based compaction of pending group messages.\n// When a group accumulates more than Threshold pending messages, older messages are\n// summarized by an LLM and replaced with a compact summary, keeping KeepRecent raw messages.\ntype PendingCompactionConfig struct {\n\tThreshold int `json:\"threshold,omitempty\"` // trigger compaction when entries exceed this (default 200)\n\tKeepRecent int `json:\"keep_recent,omitempty\"` // keep this many recent raw messages after compaction (default 40)\n\tMaxTokens int `json:\"max_tokens,omitempty\"` // max output tokens for LLM summarization (default 4096)\n\tProvider string `json:\"provider,omitempty\"` // LLM provider name (e.g. \"openai\"); empty = use agent's provider\n\tModel string `json:\"model,omitempty\"` // model for summarization; empty = use agent's model\n}\n\n// ChannelsConfig contains per-channel configuration.\ntype ChannelsConfig struct {\n\tTelegram TelegramConfig `json:\"telegram\"`\n\tDiscord DiscordConfig `json:\"discord\"`\n\tSlack SlackConfig `json:\"slack\"`\n\tWhatsApp WhatsAppConfig `json:\"whatsapp\"`\n\tZalo ZaloConfig `json:\"zalo\"`\n\tZaloPersonal ZaloPersonalConfig `json:\"zalo_personal\"`\n\tFeishu FeishuConfig `json:\"feishu\"`\n\tPendingCompaction *PendingCompactionConfig `json:\"pending_compaction,omitempty\"` // global pending message compaction settings\n}\n\ntype TelegramConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tToken string `json:\"token\"`\n\tProxy string `json:\"proxy,omitempty\"`\n\tAPIServer string `json:\"api_server,omitempty\"` // custom Telegram Bot API server URL (e.g. \"http://localhost:8081\")\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in groups (default true)\n\tMentionMode string `json:\"mention_mode,omitempty\"` // \"strict\" (default) = only respond when mentioned; \"yield\" = respond unless another bot is mentioned\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tDMStream *bool `json:\"dm_stream,omitempty\"` // enable streaming for DMs (default false) — edits placeholder progressively\n\tGroupStream *bool `json:\"group_stream,omitempty\"` // enable streaming for groups (default false) — sends new message, edits progressively\n\tDraftTransport *bool `json:\"draft_transport,omitempty\"` // use sendMessageDraft for DM streaming (default true) — stealth preview, no notifications per edit\n\tReasoningStream *bool `json:\"reasoning_stream,omitempty\"` // show reasoning as separate message when provider emits thinking events (default true)\n\tReactionLevel string `json:\"reaction_level,omitempty\"` // \"off\" (default), \"minimal\", \"full\" — status emoji reactions\n\tMediaMaxBytes int64 `json:\"media_max_bytes,omitempty\"` // max media download size in bytes (default 20MB)\n\tLinkPreview *bool `json:\"link_preview,omitempty\"` // enable URL previews in messages (default true)\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tForceIPv4 bool `json:\"force_ipv4,omitempty\"` // force IPv4 for all Telegram API requests (use when IPv6 routing is broken)\n\n\t// Optional STT (Speech-to-Text) pipeline for voice/audio inbound messages.\n\t// When stt_proxy_url is set, audio/voice messages are transcribed before being forwarded to the agent.\n\tSTTProxyURL string `json:\"stt_proxy_url,omitempty\"` // base URL of the STT proxy service (e.g. \"https://stt.example.com\")\n\tSTTAPIKey string `json:\"stt_api_key,omitempty\"` // Bearer token for the STT proxy\n\tSTTTenantID string `json:\"stt_tenant_id,omitempty\"` // optional tenant/org identifier forwarded to the STT proxy\n\tSTTTimeoutSeconds int `json:\"stt_timeout_seconds,omitempty\"` // per-request timeout for STT calls (default 30s)\n\n\t// Optional audio-aware routing: when set, voice/audio inbound messages are routed to this\n\t// agent instead of the default channel agent. Requires the named agent to exist in the config.\n\tVoiceAgentID string `json:\"voice_agent_id,omitempty\"` // agent ID to route voice inbound to (e.g. \"speaking-agent\")\n\n\t// Audio guard: intercept technical errors in voice agent replies and replace with friendly fallbacks.\n\t// Only active when VoiceAgentID is set. Custom error markers replace built-in defaults when provided.\n\tAudioGuardFallbackTranscript string `json:\"audio_guard_fallback_transcript,omitempty\"` // fallback with %s for transcript (e.g. \"I heard: \\\"%s\\\". Try again!\")\n\tAudioGuardFallbackNoTranscript string `json:\"audio_guard_fallback_no_transcript,omitempty\"` // fallback when no transcript available\n\tAudioGuardErrorMarkers []string `json:\"audio_guard_error_markers,omitempty\"` // custom error detection markers (replaces defaults)\n\n\t// Per-group (and per-topic) overrides. Key is chat ID string (e.g. \"-100123456\") or \"*\" for wildcard.\n\t// TS ref: channels.telegram.groups in src/config/types.telegram.ts.\n\tGroups map[string]*TelegramGroupConfig `json:\"groups,omitempty\"`\n}\n\n// TelegramGroupConfig defines per-group overrides for a Telegram channel.\n// Matching TS TelegramGroupConfig in src/config/types.telegram.ts.\ntype TelegramGroupConfig struct {\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // override group policy for this group\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // override require_mention for this group\n\tMentionMode string `json:\"mention_mode,omitempty\"` // override mention_mode for this group\n\tAllowFrom FlexibleStringSlice `json:\"allow_from,omitempty\"` // override allow_from for this group\n\tEnabled *bool `json:\"enabled,omitempty\"` // disable bot for this group (default: true)\n\tSkills []string `json:\"skills,omitempty\"` // skill whitelist (nil = all, [] = none)\n\tTools []string `json:\"tools,omitempty\"` // tool allow list (nil = all, supports \"group:xxx\")\n\tSystemPrompt string `json:\"system_prompt,omitempty\"` // extra system prompt for this group\n\tTopics map[string]*TelegramTopicConfig `json:\"topics,omitempty\"` // per-topic overrides (key: thread ID string)\n\tQuota *QuotaWindow `json:\"quota,omitempty\"` // per-group quota override\n}\n\n// TelegramTopicConfig defines per-topic overrides within a Telegram group.\n// Matching TS TelegramTopicConfig in src/config/types.telegram.ts.\ntype TelegramTopicConfig struct {\n\tRequireMention *bool `json:\"require_mention,omitempty\"`\n\tMentionMode string `json:\"mention_mode,omitempty\"`\n\tGroupPolicy string `json:\"group_policy,omitempty\"`\n\tSkills []string `json:\"skills,omitempty\"`\n\tTools []string `json:\"tools,omitempty\"` // tool allow list (nil = inherit, supports \"group:xxx\")\n\tEnabled *bool `json:\"enabled,omitempty\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from,omitempty\"`\n\tSystemPrompt string `json:\"system_prompt,omitempty\"`\n}\n\ntype DiscordConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tToken string `json:\"token\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in groups (default true)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tMediaMaxBytes int64 `json:\"media_max_bytes,omitempty\"` // max media download size (default 25MB)\n\tSTTProxyURL string `json:\"stt_proxy_url,omitempty\"`\n\tSTTAPIKey string `json:\"stt_api_key,omitempty\"`\n\tSTTTenantID string `json:\"stt_tenant_id,omitempty\"`\n\tSTTTimeoutSeconds int `json:\"stt_timeout_seconds,omitempty\"`\n\tVoiceAgentID string `json:\"voice_agent_id,omitempty\"`\n}\n\ntype SlackConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tBotToken string `json:\"bot_token\"` // xoxb-... (Bot User OAuth Token)\n\tAppToken string `json:\"app_token\"` // xapp-... (App-Level Token for Socket Mode)\n\tUserToken string `json:\"user_token,omitempty\"` // xoxp-... (Optional: custom bot identity)\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"pairing\", \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in channels (default true)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tDMStream *bool `json:\"dm_stream,omitempty\"` // enable streaming for DMs (default false)\n\tGroupStream *bool `json:\"group_stream,omitempty\"` // enable streaming for groups (default false)\n\tNativeStream *bool `json:\"native_stream,omitempty\"` // use Slack ChatStreamer API if available (default false)\n\tReactionLevel string `json:\"reaction_level,omitempty\"` // \"off\" (default), \"minimal\", \"full\"\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tDebounceDelay *int `json:\"debounce_delay,omitempty\"` // ms delay before dispatching rapid messages (default 300, 0=disabled)\n\tThreadTTL *int `json:\"thread_ttl,omitempty\"` // hours before thread participation expires (default 24, 0=disabled — always require @mention)\n\tMediaMaxBytes int64 `json:\"media_max_bytes,omitempty\"` // max file download size in bytes (default 20MB)\n}\n\ntype WhatsAppConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tAuthDir string `json:\"auth_dir,omitempty\"` // optional: SQLite auth dir override (desktop)\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default for DB instances), \"open\", \"allowlist\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"pairing\" (default for DB instances), \"open\" (default for config), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // only respond in groups when bot is @mentioned (default false)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 200, 0=disabled)\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n}\n\ntype ZaloConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tToken string `json:\"token\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tWebhookURL string `json:\"webhook_url,omitempty\"`\n\tWebhookSecret string `json:\"webhook_secret,omitempty\"`\n\tMediaMaxMB int `json:\"media_max_mb,omitempty\"` // default 5\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n}\n\ntype ZaloPersonalConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in groups (default true)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tCredentialsPath string `json:\"credentials_path,omitempty\"` // path to saved cookies JSON\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n}\n\ntype FeishuConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tAppID string `json:\"app_id\"`\n\tAppSecret string `json:\"app_secret\"`\n\tEncryptKey string `json:\"encrypt_key,omitempty\"`\n\tVerificationToken string `json:\"verification_token,omitempty\"`\n\tDomain string `json:\"domain,omitempty\"` // \"lark\" (default/global), \"feishu\" (China), or custom URL\n\tConnectionMode string `json:\"connection_mode,omitempty\"` // \"websocket\" (default), \"webhook\"\n\tWebhookPort int `json:\"webhook_port,omitempty\"` // default 3000\n\tWebhookPath string `json:\"webhook_path,omitempty\"` // default \"/feishu/events\"\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default)\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default)\n\tGroupAllowFrom FlexibleStringSlice `json:\"group_allow_from,omitempty\"`\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // default true (groups)\n\tTopicSessionMode string `json:\"topic_session_mode,omitempty\"` // \"disabled\" (default)\n\tTextChunkLimit int `json:\"text_chunk_limit,omitempty\"` // default 4000\n\tMediaMaxMB int `json:\"media_max_mb,omitempty\"` // default 30\n\tRenderMode string `json:\"render_mode,omitempty\"` // \"auto\", \"raw\", \"card\"\n\tStreaming *bool `json:\"streaming,omitempty\"` // default true\n\tReactionLevel string `json:\"reaction_level,omitempty\"` // \"off\" (default), \"minimal\", \"full\" — typing emoji reactions\n\tHistoryLimit int `json:\"history_limit,omitempty\"`\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tSTTProxyURL string `json:\"stt_proxy_url,omitempty\"`\n\tSTTAPIKey string `json:\"stt_api_key,omitempty\"`\n\tSTTTenantID string `json:\"stt_tenant_id,omitempty\"`\n\tSTTTimeoutSeconds int `json:\"stt_timeout_seconds,omitempty\"`\n\tVoiceAgentID string `json:\"voice_agent_id,omitempty\"`\n}\n\n// ProvidersConfig maps provider name to its config.\ntype ProvidersConfig struct {\n\tAnthropic ProviderConfig `json:\"anthropic\"`\n\tOpenAI ProviderConfig `json:\"openai\"`\n\tOpenRouter ProviderConfig `json:\"openrouter\"`\n\tGroq ProviderConfig `json:\"groq\"`\n\tGemini ProviderConfig `json:\"gemini\"`\n\tDeepSeek ProviderConfig `json:\"deepseek\"`\n\tMistral ProviderConfig `json:\"mistral\"`\n\tXAI ProviderConfig `json:\"xai\"`\n\tMiniMax ProviderConfig `json:\"minimax\"`\n\tCohere ProviderConfig `json:\"cohere\"`\n\tPerplexity ProviderConfig `json:\"perplexity\"`\n\tDashScope ProviderConfig `json:\"dashscope\"`\n\tBailian ProviderConfig `json:\"bailian\"`\n\tZai ProviderConfig `json:\"zai\"`\n\tZaiCoding ProviderConfig `json:\"zai_coding\"`\n\tOllama OllamaConfig `json:\"ollama\"` // local Ollama instance (no API key needed)\n\tOllamaCloud ProviderConfig `json:\"ollama_cloud\"` // Ollama Cloud (API key required)\n\tClaudeCLI ClaudeCLIConfig `json:\"claude_cli\"`\n\tACP ACPConfig `json:\"acp\"`\n\tNovita ProviderConfig `json:\"novita\"` // Novita AI (OpenAI-compatible endpoint)\n\tBytePlus ProviderConfig `json:\"byteplus\"` // BytePlus ModelArk (Seed 2.0)\n\tBytePlusCoding ProviderConfig `json:\"byteplus_coding\"` // BytePlus ModelArk Coding Plan\n\tVertex VertexConfig `json:\"vertex\"` // Google Cloud Vertex AI (OAuth2 service account + ADC)\n}\n\n// VertexConfig configures Google Cloud Vertex AI.\n// Credentials precedence: APIKey (inline JSON) > CredentialsFile (path) > ADC (both empty).\n// ProjectID and Region are required; Model optional (defaults to google/gemini-2.0-flash-001).\ntype VertexConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"` // service account JSON inline (secret — never persist in config.json)\n\tCredentialsFile string `json:\"credentials_file,omitempty\"` // path to service account JSON file\n\tProjectID string `json:\"project_id,omitempty\"`\n\tRegion string `json:\"region,omitempty\"`\n\tModel string `json:\"model,omitempty\"`\n}\n\n// OllamaConfig configures a local (or self-hosted) Ollama instance.\n// No API key is required — Ollama accepts any Bearer token value.\ntype OllamaConfig struct {\n\tHost string `json:\"host\"` // Ollama server base URL, e.g. http://localhost:11434\n}\n\n// ClaudeCLIConfig configures the Claude CLI provider (uses subscription, not API key).\ntype ClaudeCLIConfig struct {\n\tCLIPath string `json:\"cli_path\" yaml:\"cli_path\"` // path to claude binary (default: \"claude\")\n\tModel string `json:\"model\" yaml:\"model\"` // default model alias (default: \"sonnet\")\n\tBaseWorkDir string `json:\"base_work_dir\" yaml:\"base_work_dir\"` // base dir for agent workspaces\n\tPermMode string `json:\"perm_mode\" yaml:\"perm_mode\"` // permission mode (default: \"bypassPermissions\")\n}\n\n// ACPConfig configures the ACP (Agent Client Protocol) provider.\n// Orchestrates any ACP-compatible coding agent (Claude Code, Codex CLI, Gemini CLI) as a subprocess.\ntype ACPConfig struct {\n\tBinary string `json:\"binary\"` // agent binary name or path (e.g. \"claude\", \"codex\")\n\tArgs []string `json:\"args\"` // extra spawn args\n\tModel string `json:\"model\"` // default model/agent name\n\tWorkDir string `json:\"work_dir\"` // base workspace dir\n\tIdleTTL string `json:\"idle_ttl\"` // process idle TTL (e.g. \"5m\")\n\tPermMode string `json:\"perm_mode\"` // \"approve-all\" (default), \"approve-reads\", \"deny-all\"\n}\n\ntype ProviderConfig struct {\n\tAPIKey string `json:\"api_key\"`\n\tAPIBase string `json:\"api_base,omitempty\"`\n}\n\n// APIBaseForType returns the config-level api_base for a given provider type.\n// Used as a fallback when DB providers have no api_base set.\nfunc (p *ProvidersConfig) APIBaseForType(providerType string) string {\n\tswitch providerType {\n\tcase \"anthropic_native\":\n\t\treturn p.Anthropic.APIBase\n\tcase \"openai\", \"openai_compat\":\n\t\treturn p.OpenAI.APIBase\n\tcase \"openrouter\":\n\t\treturn p.OpenRouter.APIBase\n\tcase \"groq\":\n\t\treturn p.Groq.APIBase\n\tcase \"deepseek\":\n\t\treturn p.DeepSeek.APIBase\n\tcase \"gemini_native\":\n\t\treturn p.Gemini.APIBase\n\tcase \"mistral\":\n\t\treturn p.Mistral.APIBase\n\tcase \"xai\":\n\t\treturn p.XAI.APIBase\n\tcase \"minimax_native\":\n\t\treturn p.MiniMax.APIBase\n\tcase \"cohere\":\n\t\treturn p.Cohere.APIBase\n\tcase \"perplexity\":\n\t\treturn p.Perplexity.APIBase\n\tcase \"dashscope\":\n\t\treturn p.DashScope.APIBase\n\tcase \"bailian\":\n\t\treturn p.Bailian.APIBase\n\tcase \"zai\":\n\t\treturn p.Zai.APIBase\n\tcase \"zai_coding\":\n\t\treturn p.ZaiCoding.APIBase\n\tcase \"ollama_cloud\":\n\t\treturn p.OllamaCloud.APIBase\n\tcase \"novita\":\n\t\treturn p.Novita.APIBase\n\tcase \"byteplus\":\n\t\treturn p.BytePlus.APIBase\n\tcase \"byteplus_coding\":\n\t\treturn p.BytePlusCoding.APIBase\n\tcase \"vertex\":\n\t\t// Computed from project+region at registration time; no config-level static base.\n\t\treturn \"\"\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\n// HasAnyProvider returns true if at least one provider has an API key or CLI configured.\nfunc (c *Config) HasAnyProvider() bool {\n\tp := c.Providers\n\treturn p.Anthropic.APIKey != \"\" ||\n\t\tp.OpenAI.APIKey != \"\" ||\n\t\tp.OpenRouter.APIKey != \"\" ||\n\t\tp.Groq.APIKey != \"\" ||\n\t\tp.Gemini.APIKey != \"\" ||\n\t\tp.DeepSeek.APIKey != \"\" ||\n\t\tp.Mistral.APIKey != \"\" ||\n\t\tp.XAI.APIKey != \"\" ||\n\t\tp.MiniMax.APIKey != \"\" ||\n\t\tp.Cohere.APIKey != \"\" ||\n\t\tp.Perplexity.APIKey != \"\" ||\n\t\tp.DashScope.APIKey != \"\" ||\n\t\tp.Bailian.APIKey != \"\" ||\n\t\tp.Zai.APIKey != \"\" ||\n\t\tp.ZaiCoding.APIKey != \"\" ||\n\t\tp.Ollama.Host != \"\" ||\n\t\tp.OllamaCloud.APIKey != \"\" ||\n\t\tp.ClaudeCLI.CLIPath != \"\" ||\n\t\tp.ACP.Binary != \"\" ||\n\t\tp.Novita.APIKey != \"\" ||\n\t\tp.BytePlus.APIKey != \"\" ||\n\t\tp.BytePlusCoding.APIKey != \"\" ||\n\t\t(p.Vertex.ProjectID != \"\" && p.Vertex.Region != \"\")\n}\n\n// QuotaWindow defines request limits per time window. Zero means unlimited.\ntype QuotaWindow struct {\n\tHour int `json:\"hour,omitempty\"` // max requests per hour (0 = unlimited)\n\tDay int `json:\"day,omitempty\"` // max requests per day (0 = unlimited)\n\tWeek int `json:\"week,omitempty\"` // max requests per week (0 = unlimited)\n}\n\n// IsZero returns true if no limits are set.\nfunc (w QuotaWindow) IsZero() bool { return w.Hour == 0 && w.Day == 0 && w.Week == 0 }\n\n// QuotaConfig configures per-user/group request quotas.\n// Config merge priority: Groups > Channels > Providers > Default.\ntype QuotaConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tDefault QuotaWindow `json:\"default\"`\n\tProviders map[string]QuotaWindow `json:\"providers,omitempty\"` // key = provider name (e.g. \"anthropic\")\n\tChannels map[string]QuotaWindow `json:\"channels,omitempty\"` // key = channel name (e.g. \"telegram\")\n\tGroups map[string]QuotaWindow `json:\"groups,omitempty\"` // key = userID (e.g. \"group:telegram:-100123\")\n}\n\n// GatewayConfig controls the gateway server.\ntype GatewayConfig struct {\n\tHost string `json:\"host\"`\n\tPort int `json:\"port\"`\n\tToken string `json:\"token,omitempty\"` // bearer token for WS/HTTP auth\n\tOwnerIDs []string `json:\"owner_ids,omitempty\"` // sender IDs considered \"owner\"\n\tAllowedOrigins []string `json:\"allowed_origins,omitempty\"` // WebSocket CORS whitelist (empty = allow all)\n\tMaxMessageChars int `json:\"max_message_chars,omitempty\"` // max user message characters (default 32000)\n\tRateLimitRPM int `json:\"rate_limit_rpm,omitempty\"` // rate limit: requests per minute per user (default 20, 0 = disabled)\n\tInjectionAction string `json:\"injection_action,omitempty\"` // prompt injection action: \"log\", \"warn\" (default), \"block\", \"off\"\n\tInboundDebounceMs int `json:\"inbound_debounce_ms,omitempty\"` // merge rapid channel/Web Chat messages from same sender/session (0 = no wait)\n\tQuota *QuotaConfig `json:\"quota,omitempty\"` // per-user/group request quotas\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // deliver intermediate text during tool iterations (default false)\n\tToolStatus *bool `json:\"tool_status,omitempty\"` // show tool name in streaming preview during tool execution (default true)\n\tTaskRecoveryIntervalSec int `json:\"task_recovery_interval_sec,omitempty\"` // team task recovery ticker interval in seconds (default 300 = 5min)\n\tBackgroundProvider string `json:\"background_provider,omitempty\"` // LLM provider for background workers (vault enrichment, consolidation)\n\tBackgroundModel string `json:\"background_model,omitempty\"` // LLM model for background workers\n}\n\n// ToolsConfig controls tool availability, policy, and web search.\ntype ToolsConfig struct {\n\tProfile string `json:\"profile,omitempty\"` // global profile: \"minimal\", \"coding\", \"messaging\", \"full\"\n\tAllow []string `json:\"allow,omitempty\"` // global allow list (tool names or \"group:xxx\")\n\tDeny []string `json:\"deny,omitempty\"` // global deny list\n\tAlsoAllow []string `json:\"alsoAllow,omitempty\"` // additive: adds without removing existing\n\tByProvider map[string]*ToolPolicySpec `json:\"byProvider,omitempty\"` // per-provider overrides\n\tShellDenyGroups map[string]bool `json:\"shellDenyGroups,omitempty\"` // global shell deny-group toggles (group name -> denied); per-agent overrides win per-key\n\tCommandKeywordAllowlist []CommandKeywordAllowlistRule `json:\"commandKeywordAllowlist,omitempty\"` // scoped bypass for credentialed CLI content keywords\n\tExecApproval ExecApprovalCfg `json:\"execApproval\"` // exec command approval settings\n\tWebFetch WebFetchPolicyConfig `json:\"web_fetch\"` // domain policy for URL fetching\n\tBrowser BrowserToolConfig `json:\"browser\"`\n\tRateLimitPerHour int `json:\"rate_limit_per_hour,omitempty\"` // max tool executions per hour per session (0 = disabled)\n\tScrubCredentials *bool `json:\"scrub_credentials,omitempty\"` // auto-redact API keys/tokens in tool output (default true)\n\tMcpServers map[string]*MCPServerConfig `json:\"mcp_servers,omitempty\"` // external MCP server connections\n}\n\n// CommandKeywordAllowlistRule scopes product/security vocabulary that may appear\n// in credentialed CLI content args without disabling command-path deny patterns.\ntype CommandKeywordAllowlistRule struct {\n\tID string `json:\"id,omitempty\"`\n\tCommand string `json:\"command\"`\n\tSubcommands []string `json:\"subcommands,omitempty\"`\n\tArgs []string `json:\"args,omitempty\"`\n\tArgPositions []int `json:\"argPositions,omitempty\"` // 0-based after matched subcommand\n\tKeywords []string `json:\"keywords,omitempty\"`\n\tReason string `json:\"reason,omitempty\"`\n\tEnabled *bool `json:\"enabled,omitempty\"` // nil means enabled\n}\n\n// MCPServerConfig configures a single external MCP server connection.\ntype MCPServerConfig struct {\n\tTransport string `json:\"transport\"` // \"stdio\", \"sse\", \"streamable-http\"\n\tCommand string `json:\"command,omitempty\"` // stdio: command to spawn\n\tArgs []string `json:\"args,omitempty\"` // stdio: command arguments\n\tEnv map[string]string `json:\"env,omitempty\"` // stdio: extra environment variables\n\tURL string `json:\"url,omitempty\"` // sse/http: server URL\n\tHeaders map[string]string `json:\"headers,omitempty\"` // sse/http: extra HTTP headers\n\tEnabled *bool `json:\"enabled,omitempty\"` // default true\n\tToolPrefix string `json:\"tool_prefix,omitempty\"` // prefix for tool names (avoids collisions)\n\tTimeoutSec int `json:\"timeout_sec,omitempty\"` // per-tool-call timeout in seconds (default 60)\n}\n\n// IsEnabled returns whether this MCP server is enabled (default true).\nfunc (c *MCPServerConfig) IsEnabled() bool {\n\treturn c.Enabled == nil || *c.Enabled\n}\n\n// ExecApprovalCfg configures command execution approval (matching TS exec-approval.ts).\ntype ExecApprovalCfg struct {\n\tSecurity string `json:\"security,omitempty\"` // \"deny\", \"allowlist\", \"full\" (default \"full\")\n\tAsk string `json:\"ask,omitempty\"` // \"off\", \"on-miss\", \"always\" (default \"off\")\n\tAllowlist []string `json:\"allowlist,omitempty\"` // glob patterns for allowed commands\n}\n\n// WebFetchPolicyConfig controls domain filtering for the web_fetch tool.\ntype WebFetchPolicyConfig struct {\n\tPolicy string `json:\"policy,omitempty\"` // \"allow_all\" (default), \"allowlist\"\n\tAllowedDomains []string `json:\"allowed_domains,omitempty\"` // e.g. [\"github.com\", \"*.example.com\"]\n\tBlockedDomains []string `json:\"blocked_domains,omitempty\"` // always checked regardless of policy\n}\n\n// BrowserToolConfig controls the browser automation tool.\ntype BrowserToolConfig struct {\n\tEnabled bool `json:\"enabled\"` // enable the browser tool (default false)\n\tHeadless bool `json:\"headless,omitempty\"` // run Chrome in headless mode (ignored when RemoteURL is set)\n\tRemoteURL string `json:\"remote_url,omitempty\"` // CDP endpoint for remote Chrome sidecar, e.g. \"ws://chrome:9222\"\n\tActionTimeoutMs int `json:\"action_timeout_ms,omitempty\"` // per-action timeout in ms (default 30000)\n\tIdleTimeoutMs int `json:\"idle_timeout_ms,omitempty\"` // idle page auto-close in ms (default 600000, 0=disabled)\n\tMaxPages int `json:\"max_pages,omitempty\"` // max open pages per tenant (default 5)\n\tCookieSyncEnabled bool `json:\"cookie_sync_enabled\"` // apply selected synced cookies to scoped browser sessions\n}\n\n// ToolPolicySpec defines a tool policy at any level (global, per-agent, per-provider).\ntype ToolPolicySpec struct {\n\tProfile string `json:\"profile,omitempty\"`\n\tAllow []string `json:\"allow,omitempty\"`\n\tDeny []string `json:\"deny,omitempty\"`\n\tAlsoAllow []string `json:\"alsoAllow,omitempty\"`\n\tByProvider map[string]*ToolPolicySpec `json:\"byProvider,omitempty\"`\n\tWait *WaitToolPolicy `json:\"wait,omitempty\"`\n\tToolCallPrefix string `json:\"toolCallPrefix,omitempty\"` // prefix to strip from model's tool call names before registry lookup\n}\n\n// WaitToolPolicy configures per-agent safety bounds for the wait tool.\ntype WaitToolPolicy struct {\n\tMinMs int `json:\"min_ms,omitempty\"`\n\tMaxMs int `json:\"max_ms,omitempty\"`\n}\n\n// SessionsConfig controls session behavior.\n// Matching TS src/config/sessions/types.ts + src/config/types.base.ts.\ntype SessionsConfig struct {\n\tScope string `json:\"scope,omitempty\"` // \"per-sender\" (default), \"global\"\n\tDmScope string `json:\"dm_scope,omitempty\"` // \"main\", \"per-peer\", \"per-channel-peer\" (default), \"per-account-channel-peer\"\n\tMainKey string `json:\"main_key,omitempty\"` // main session key suffix (default \"main\", used when dm_scope=\"main\")\n}\n\n// TtsConfig configures text-to-speech.\n// Matching TS src/config/types.tts.ts.\ntype TtsConfig struct {\n\tProvider string `json:\"provider,omitempty\"` // \"openai\", \"elevenlabs\", \"edge\", \"minimax\", \"gemini\"\n\tAuto string `json:\"auto,omitempty\"` // \"off\" (default), \"always\", \"inbound\", \"tagged\"\n\tMode string `json:\"mode,omitempty\"` // \"final\" (default), \"all\"\n\tMaxLength int `json:\"max_length,omitempty\"` // max text length before truncation (default 1500)\n\tTimeoutMs int `json:\"timeout_ms,omitempty\"` // API timeout in ms (default 30000)\n\tOpenAI TtsOpenAIConfig `json:\"openai\"`\n\tElevenLabs TtsElevenLabsConfig `json:\"elevenlabs\"`\n\tEdge TtsEdgeConfig `json:\"edge\"`\n\tMiniMax TtsMiniMaxConfig `json:\"minimax\"`\n\tGemini TtsGeminiConfig `json:\"gemini\"`\n}\n\n// TtsGeminiConfig configures the Google Gemini TTS provider.\ntype TtsGeminiConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"` // required; encrypted at rest\n\tAPIBase string `json:\"api_base,omitempty\"` // custom endpoint (optional; SSRF-gated)\n\tVoice string `json:\"voice,omitempty\"` // default \"Kore\"\n\tModel string `json:\"model,omitempty\"` // default \"gemini-2.5-flash-preview-tts\"\n\tSpeakers string `json:\"speakers,omitempty\"` // JSON-encoded []SpeakerVoice for multi-speaker mode\n}\n\n// TtsOpenAIConfig configures the OpenAI TTS provider.\ntype TtsOpenAIConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"`\n\tAPIBase string `json:\"api_base,omitempty\"` // custom endpoint URL\n\tModel string `json:\"model,omitempty\"` // default \"gpt-4o-mini-tts\"\n\tVoice string `json:\"voice,omitempty\"` // default \"alloy\"\n}\n\n// TtsElevenLabsConfig configures the ElevenLabs TTS provider.\ntype TtsElevenLabsConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"`\n\tBaseURL string `json:\"base_url,omitempty\"`\n\tVoiceID string `json:\"voice_id,omitempty\"` // default \"pMsXgVXv3BLzUgSXRplE\"\n\tModelID string `json:\"model_id,omitempty\"` // default \"eleven_multilingual_v2\"\n}\n\n// TtsEdgeConfig configures the Microsoft Edge TTS provider (free, no API key).\ntype TtsEdgeConfig struct {\n\tEnabled bool `json:\"enabled,omitempty\"`\n\tVoice string `json:\"voice,omitempty\"` // default \"en-US-MichelleNeural\"\n\tRate string `json:\"rate,omitempty\"` // speech rate, e.g. \"+0%\"\n}\n\n// TtsMiniMaxConfig configures the MiniMax TTS provider.\ntype TtsMiniMaxConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"`\n\tGroupID string `json:\"group_id,omitempty\"` // MiniMax GroupId (required)\n\tAPIBase string `json:\"api_base,omitempty\"` // default \"https://api.minimax.io/v1\"\n\tModel string `json:\"model,omitempty\"` // default \"speech-02-hd\"\n\tVoiceID string `json:\"voice_id,omitempty\"` // default \"Wise_Woman\"\n}\n\n// MergeChannelGroupQuotas merges per-group quota overrides from channel configs\n// (e.g., channels.telegram.groups[chatID].quota) into gateway.quota.groups.\n// This allows per-group quotas to be set at the channel level and picked up\n// by the QuotaChecker at runtime.\nfunc MergeChannelGroupQuotas(cfg *Config) {\n\tif cfg.Gateway.Quota == nil {\n\t\treturn\n\t}\n\tif cfg.Gateway.Quota.Groups == nil {\n\t\tcfg.Gateway.Quota.Groups = make(map[string]QuotaWindow)\n\t}\n\n\t// Telegram per-group quotas\n\tfor chatID, groupCfg := range cfg.Channels.Telegram.Groups {\n\t\tif groupCfg != nil && groupCfg.Quota != nil && !groupCfg.Quota.IsZero() {\n\t\t\tkey := \"group:telegram:\" + chatID\n\t\t\tcfg.Gateway.Quota.Groups[key] = *groupCfg.Quota\n\t\t}\n\t}\n}\n"}, "68684e48590b5d5662b157819ff5e36d3a640a0b": {"t": 1785759234.7455616, "n": 33924, "h": "4f611ad6974d5daa", "c": "package config\n\n// PendingCompactionConfig configures LLM-based compaction of pending group messages.\n// When a group accumulates more than Threshold pending messages, older messages are\n// summarized by an LLM and replaced with a compact summary, keeping KeepRecent raw messages.\ntype PendingCompactionConfig struct {\n\tThreshold int `json:\"threshold,omitempty\"` // trigger compaction when entries exceed this (default 200)\n\tKeepRecent int `json:\"keep_recent,omitempty\"` // keep this many recent raw messages after compaction (default 40)\n\tMaxTokens int `json:\"max_tokens,omitempty\"` // max output tokens for LLM summarization (default 4096)\n\tProvider string `json:\"provider,omitempty\"` // LLM provider name (e.g. \"openai\"); empty = use agent's provider\n\tModel string `json:\"model,omitempty\"` // model for summarization; empty = use agent's model\n}\n\n// ChannelsConfig contains per-channel configuration.\ntype ChannelsConfig struct {\n\tTelegram TelegramConfig `json:\"telegram\"`\n\tDiscord DiscordConfig `json:\"discord\"`\n\tSlack SlackConfig `json:\"slack\"`\n\tWhatsApp WhatsAppConfig `json:\"whatsapp\"`\n\tZalo ZaloConfig `json:\"zalo\"`\n\tZaloPersonal ZaloPersonalConfig `json:\"zalo_personal\"`\n\tFeishu FeishuConfig `json:\"feishu\"`\n\tPendingCompaction *PendingCompactionConfig `json:\"pending_compaction,omitempty\"` // global pending message compaction settings\n}\n\ntype TelegramConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tToken string `json:\"token\"`\n\tProxy string `json:\"proxy,omitempty\"`\n\tAPIServer string `json:\"api_server,omitempty\"` // custom Telegram Bot API server URL (e.g. \"http://localhost:8081\")\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in groups (default true)\n\tMentionMode string `json:\"mention_mode,omitempty\"` // \"strict\" (default) = only respond when mentioned; \"yield\" = respond unless another bot is mentioned\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tDMStream *bool `json:\"dm_stream,omitempty\"` // enable streaming for DMs (default false) — edits placeholder progressively\n\tGroupStream *bool `json:\"group_stream,omitempty\"` // enable streaming for groups (default false) — sends new message, edits progressively\n\tDraftTransport *bool `json:\"draft_transport,omitempty\"` // use sendMessageDraft for DM streaming (default true) — stealth preview, no notifications per edit\n\tReasoningStream *bool `json:\"reasoning_stream,omitempty\"` // show reasoning as separate message when provider emits thinking events (default true)\n\tReactionLevel string `json:\"reaction_level,omitempty\"` // \"off\" (default), \"minimal\", \"full\" — status emoji reactions\n\tMediaMaxBytes int64 `json:\"media_max_bytes,omitempty\"` // max media download size in bytes (default 20MB)\n\tLinkPreview *bool `json:\"link_preview,omitempty\"` // enable URL previews in messages (default true)\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tForceIPv4 bool `json:\"force_ipv4,omitempty\"` // force IPv4 for all Telegram API requests (use when IPv6 routing is broken)\n\n\t// Optional STT (Speech-to-Text) pipeline for voice/audio inbound messages.\n\t// When stt_proxy_url is set, audio/voice messages are transcribed before being forwarded to the agent.\n\tSTTProxyURL string `json:\"stt_proxy_url,omitempty\"` // base URL of the STT proxy service (e.g. \"https://stt.example.com\")\n\tSTTAPIKey string `json:\"stt_api_key,omitempty\"` // Bearer token for the STT proxy\n\tSTTTenantID string `json:\"stt_tenant_id,omitempty\"` // optional tenant/org identifier forwarded to the STT proxy\n\tSTTTimeoutSeconds int `json:\"stt_timeout_seconds,omitempty\"` // per-request timeout for STT calls (default 30s)\n\n\t// Optional audio-aware routing: when set, voice/audio inbound messages are routed to this\n\t// agent instead of the default channel agent. Requires the named agent to exist in the config.\n\tVoiceAgentID string `json:\"voice_agent_id,omitempty\"` // agent ID to route voice inbound to (e.g. \"speaking-agent\")\n\n\t// Audio guard: intercept technical errors in voice agent replies and replace with friendly fallbacks.\n\t// Only active when VoiceAgentID is set. Custom error markers replace built-in defaults when provided.\n\tAudioGuardFallbackTranscript string `json:\"audio_guard_fallback_transcript,omitempty\"` // fallback with %s for transcript (e.g. \"I heard: \\\"%s\\\". Try again!\")\n\tAudioGuardFallbackNoTranscript string `json:\"audio_guard_fallback_no_transcript,omitempty\"` // fallback when no transcript available\n\tAudioGuardErrorMarkers []string `json:\"audio_guard_error_markers,omitempty\"` // custom error detection markers (replaces defaults)\n\n\t// Per-group (and per-topic) overrides. Key is chat ID string (e.g. \"-100123456\") or \"*\" for wildcard.\n\t// TS ref: channels.telegram.groups in src/config/types.telegram.ts.\n\tGroups map[string]*TelegramGroupConfig `json:\"groups,omitempty\"`\n}\n\n// TelegramGroupConfig defines per-group overrides for a Telegram channel.\n// Matching TS TelegramGroupConfig in src/config/types.telegram.ts.\ntype TelegramGroupConfig struct {\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // override group policy for this group\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // override require_mention for this group\n\tMentionMode string `json:\"mention_mode,omitempty\"` // override mention_mode for this group\n\tAllowFrom FlexibleStringSlice `json:\"allow_from,omitempty\"` // override allow_from for this group\n\tEnabled *bool `json:\"enabled,omitempty\"` // disable bot for this group (default: true)\n\tSkills []string `json:\"skills,omitempty\"` // skill whitelist (nil = all, [] = none)\n\tTools []string `json:\"tools,omitempty\"` // tool allow list (nil = all, supports \"group:xxx\")\n\tSystemPrompt string `json:\"system_prompt,omitempty\"` // extra system prompt for this group\n\tTopics map[string]*TelegramTopicConfig `json:\"topics,omitempty\"` // per-topic overrides (key: thread ID string)\n\tQuota *QuotaWindow `json:\"quota,omitempty\"` // per-group quota override\n}\n\n// TelegramTopicConfig defines per-topic overrides within a Telegram group.\n// Matching TS TelegramTopicConfig in src/config/types.telegram.ts.\ntype TelegramTopicConfig struct {\n\tRequireMention *bool `json:\"require_mention,omitempty\"`\n\tMentionMode string `json:\"mention_mode,omitempty\"`\n\tGroupPolicy string `json:\"group_policy,omitempty\"`\n\tSkills []string `json:\"skills,omitempty\"`\n\tTools []string `json:\"tools,omitempty\"` // tool allow list (nil = inherit, supports \"group:xxx\")\n\tEnabled *bool `json:\"enabled,omitempty\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from,omitempty\"`\n\tSystemPrompt string `json:\"system_prompt,omitempty\"`\n}\n\ntype DiscordConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tToken string `json:\"token\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in groups (default true)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tMediaMaxBytes int64 `json:\"media_max_bytes,omitempty\"` // max media download size (default 25MB)\n\tSTTProxyURL string `json:\"stt_proxy_url,omitempty\"`\n\tSTTAPIKey string `json:\"stt_api_key,omitempty\"`\n\tSTTTenantID string `json:\"stt_tenant_id,omitempty\"`\n\tSTTTimeoutSeconds int `json:\"stt_timeout_seconds,omitempty\"`\n\tVoiceAgentID string `json:\"voice_agent_id,omitempty\"`\n}\n\ntype SlackConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tBotToken string `json:\"bot_token\"` // xoxb-... (Bot User OAuth Token)\n\tAppToken string `json:\"app_token\"` // xapp-... (App-Level Token for Socket Mode)\n\tUserToken string `json:\"user_token,omitempty\"` // xoxp-... (Optional: custom bot identity)\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"pairing\", \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in channels (default true)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tDMStream *bool `json:\"dm_stream,omitempty\"` // enable streaming for DMs (default false)\n\tGroupStream *bool `json:\"group_stream,omitempty\"` // enable streaming for groups (default false)\n\tNativeStream *bool `json:\"native_stream,omitempty\"` // use Slack ChatStreamer API if available (default false)\n\tReactionLevel string `json:\"reaction_level,omitempty\"` // \"off\" (default), \"minimal\", \"full\"\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tDebounceDelay *int `json:\"debounce_delay,omitempty\"` // ms delay before dispatching rapid messages (default 300, 0=disabled)\n\tThreadTTL *int `json:\"thread_ttl,omitempty\"` // hours before thread participation expires (default 24, 0=disabled — always require @mention)\n\tMediaMaxBytes int64 `json:\"media_max_bytes,omitempty\"` // max file download size in bytes (default 20MB)\n}\n\ntype WhatsAppConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tAuthDir string `json:\"auth_dir,omitempty\"` // optional: SQLite auth dir override (desktop)\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default for DB instances), \"open\", \"allowlist\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"pairing\" (default for DB instances), \"open\" (default for config), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // only respond in groups when bot is @mentioned (default false)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 200, 0=disabled)\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n}\n\ntype ZaloConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tToken string `json:\"token\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tWebhookURL string `json:\"webhook_url,omitempty\"`\n\tWebhookSecret string `json:\"webhook_secret,omitempty\"`\n\tMediaMaxMB int `json:\"media_max_mb,omitempty\"` // default 5\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n}\n\ntype ZaloPersonalConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in groups (default true)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tCredentialsPath string `json:\"credentials_path,omitempty\"` // path to saved cookies JSON\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n}\n\ntype FeishuConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tAppID string `json:\"app_id\"`\n\tAppSecret string `json:\"app_secret\"`\n\tEncryptKey string `json:\"encrypt_key,omitempty\"`\n\tVerificationToken string `json:\"verification_token,omitempty\"`\n\tDomain string `json:\"domain,omitempty\"` // \"lark\" (default/global), \"feishu\" (China), or custom URL\n\tConnectionMode string `json:\"connection_mode,omitempty\"` // \"websocket\" (default), \"webhook\"\n\tWebhookPort int `json:\"webhook_port,omitempty\"` // default 3000\n\tWebhookPath string `json:\"webhook_path,omitempty\"` // default \"/feishu/events\"\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default)\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default)\n\tGroupAllowFrom FlexibleStringSlice `json:\"group_allow_from,omitempty\"`\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // default true (groups)\n\tTopicSessionMode string `json:\"topic_session_mode,omitempty\"` // \"disabled\" (default)\n\tTextChunkLimit int `json:\"text_chunk_limit,omitempty\"` // default 4000\n\tMediaMaxMB int `json:\"media_max_mb,omitempty\"` // default 30\n\tRenderMode string `json:\"render_mode,omitempty\"` // \"auto\", \"raw\", \"card\"\n\tStreaming *bool `json:\"streaming,omitempty\"` // default true\n\tReactionLevel string `json:\"reaction_level,omitempty\"` // \"off\" (default), \"minimal\", \"full\" — typing emoji reactions\n\tHistoryLimit int `json:\"history_limit,omitempty\"`\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tSTTProxyURL string `json:\"stt_proxy_url,omitempty\"`\n\tSTTAPIKey string `json:\"stt_api_key,omitempty\"`\n\tSTTTenantID string `json:\"stt_tenant_id,omitempty\"`\n\tSTTTimeoutSeconds int `json:\"stt_timeout_seconds,omitempty\"`\n\tVoiceAgentID string `json:\"voice_agent_id,omitempty\"`\n}\n\n// ProvidersConfig maps provider name to its config.\ntype ProvidersConfig struct {\n\tAnthropic ProviderConfig `json:\"anthropic\"`\n\tOpenAI ProviderConfig `json:\"openai\"`\n\tOpenRouter ProviderConfig `json:\"openrouter\"`\n\tGroq ProviderConfig `json:\"groq\"`\n\tGemini ProviderConfig `json:\"gemini\"`\n\tDeepSeek ProviderConfig `json:\"deepseek\"`\n\tMistral ProviderConfig `json:\"mistral\"`\n\tXAI ProviderConfig `json:\"xai\"`\n\tMiniMax ProviderConfig `json:\"minimax\"`\n\tCohere ProviderConfig `json:\"cohere\"`\n\tPerplexity ProviderConfig `json:\"perplexity\"`\n\tDashScope ProviderConfig `json:\"dashscope\"`\n\tBailian ProviderConfig `json:\"bailian\"`\n\tZai ProviderConfig `json:\"zai\"`\n\tZaiCoding ProviderConfig `json:\"zai_coding\"`\n\tOllama OllamaConfig `json:\"ollama\"` // local Ollama instance (no API key needed)\n\tOllamaCloud ProviderConfig `json:\"ollama_cloud\"` // Ollama Cloud (API key required)\n\tClaudeCLI ClaudeCLIConfig `json:\"claude_cli\"`\n\tACP ACPConfig `json:\"acp\"`\n\tNovita ProviderConfig `json:\"novita\"` // Novita AI (OpenAI-compatible endpoint)\n\tBytePlus ProviderConfig `json:\"byteplus\"` // BytePlus ModelArk (Seed 2.0)\n\tBytePlusCoding ProviderConfig `json:\"byteplus_coding\"` // BytePlus ModelArk Coding Plan\n\tVertex VertexConfig `json:\"vertex\"` // Google Cloud Vertex AI (OAuth2 service account + ADC)\n}\n\n// VertexConfig configures Google Cloud Vertex AI.\n// Credentials precedence: APIKey (inline JSON) > CredentialsFile (path) > ADC (both empty).\n// ProjectID and Region are required; Model optional (defaults to google/gemini-2.0-flash-001).\ntype VertexConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"` // service account JSON inline (secret — never persist in config.json)\n\tCredentialsFile string `json:\"credentials_file,omitempty\"` // path to service account JSON file\n\tProjectID string `json:\"project_id,omitempty\"`\n\tRegion string `json:\"region,omitempty\"`\n\tModel string `json:\"model,omitempty\"`\n}\n\n// OllamaConfig configures a local (or self-hosted) Ollama instance.\n// No API key is required — Ollama accepts any Bearer token value.\ntype OllamaConfig struct {\n\tHost string `json:\"host\"` // Ollama server base URL, e.g. http://localhost:11434\n}\n\n// ClaudeCLIConfig configures the Claude CLI provider (uses subscription, not API key).\ntype ClaudeCLIConfig struct {\n\tCLIPath string `json:\"cli_path\" yaml:\"cli_path\"` // path to claude binary (default: \"claude\")\n\tModel string `json:\"model\" yaml:\"model\"` // default model alias (default: \"sonnet\")\n\tBaseWorkDir string `json:\"base_work_dir\" yaml:\"base_work_dir\"` // base dir for agent workspaces\n\tPermMode string `json:\"perm_mode\" yaml:\"perm_mode\"` // permission mode (default: \"bypassPermissions\")\n}\n\n// ACPConfig configures the ACP (Agent Client Protocol) provider.\n// Orchestrates any ACP-compatible coding agent (Claude Code, Codex CLI, Gemini CLI) as a subprocess.\ntype ACPConfig struct {\n\tBinary string `json:\"binary\"` // agent binary name or path (e.g. \"claude\", \"codex\")\n\tArgs []string `json:\"args\"` // extra spawn args\n\tModel string `json:\"model\"` // default model/agent name\n\tWorkDir string `json:\"work_dir\"` // base workspace dir\n\tIdleTTL string `json:\"idle_ttl\"` // process idle TTL (e.g. \"5m\")\n\tPermMode string `json:\"perm_mode\"` // \"approve-all\" (default), \"approve-reads\", \"deny-all\"\n}\n\ntype ProviderConfig struct {\n\tAPIKey string `json:\"api_key\"`\n\tAPIBase string `json:\"api_base,omitempty\"`\n}\n\n// APIBaseForType returns the config-level api_base for a given provider type.\n// Used as a fallback when DB providers have no api_base set.\nfunc (p *ProvidersConfig) APIBaseForType(providerType string) string {\n\tswitch providerType {\n\tcase \"anthropic_native\":\n\t\treturn p.Anthropic.APIBase\n\tcase \"openai\", \"openai_compat\":\n\t\treturn p.OpenAI.APIBase\n\tcase \"openrouter\":\n\t\treturn p.OpenRouter.APIBase\n\tcase \"groq\":\n\t\treturn p.Groq.APIBase\n\tcase \"deepseek\":\n\t\treturn p.DeepSeek.APIBase\n\tcase \"gemini_native\":\n\t\treturn p.Gemini.APIBase\n\tcase \"mistral\":\n\t\treturn p.Mistral.APIBase\n\tcase \"xai\":\n\t\treturn p.XAI.APIBase\n\tcase \"minimax_native\":\n\t\treturn p.MiniMax.APIBase\n\tcase \"cohere\":\n\t\treturn p.Cohere.APIBase\n\tcase \"perplexity\":\n\t\treturn p.Perplexity.APIBase\n\tcase \"dashscope\":\n\t\treturn p.DashScope.APIBase\n\tcase \"bailian\":\n\t\treturn p.Bailian.APIBase\n\tcase \"zai\":\n\t\treturn p.Zai.APIBase\n\tcase \"zai_coding\":\n\t\treturn p.ZaiCoding.APIBase\n\tcase \"ollama_cloud\":\n\t\treturn p.OllamaCloud.APIBase\n\tcase \"novita\":\n\t\treturn p.Novita.APIBase\n\tcase \"byteplus\":\n\t\treturn p.BytePlus.APIBase\n\tcase \"byteplus_coding\":\n\t\treturn p.BytePlusCoding.APIBase\n\tcase \"vertex\":\n\t\t// Computed from project+region at registration time; no config-level static base.\n\t\treturn \"\"\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\n// HasAnyProvider returns true if at least one provider has an API key or CLI configured.\nfunc (c *Config) HasAnyProvider() bool {\n\tp := c.Providers\n\treturn p.Anthropic.APIKey != \"\" ||\n\t\tp.OpenAI.APIKey != \"\" ||\n\t\tp.OpenRouter.APIKey != \"\" ||\n\t\tp.Groq.APIKey != \"\" ||\n\t\tp.Gemini.APIKey != \"\" ||\n\t\tp.DeepSeek.APIKey != \"\" ||\n\t\tp.Mistral.APIKey != \"\" ||\n\t\tp.XAI.APIKey != \"\" ||\n\t\tp.MiniMax.APIKey != \"\" ||\n\t\tp.Cohere.APIKey != \"\" ||\n\t\tp.Perplexity.APIKey != \"\" ||\n\t\tp.DashScope.APIKey != \"\" ||\n\t\tp.Bailian.APIKey != \"\" ||\n\t\tp.Zai.APIKey != \"\" ||\n\t\tp.ZaiCoding.APIKey != \"\" ||\n\t\tp.Ollama.Host != \"\" ||\n\t\tp.OllamaCloud.APIKey != \"\" ||\n\t\tp.ClaudeCLI.CLIPath != \"\" ||\n\t\tp.ACP.Binary != \"\" ||\n\t\tp.Novita.APIKey != \"\" ||\n\t\tp.BytePlus.APIKey != \"\" ||\n\t\tp.BytePlusCoding.APIKey != \"\" ||\n\t\t(p.Vertex.ProjectID != \"\" && p.Vertex.Region != \"\")\n}\n\n// QuotaWindow defines request limits per time window. Zero means unlimited.\ntype QuotaWindow struct {\n\tHour int `json:\"hour,omitempty\"` // max requests per hour (0 = unlimited)\n\tDay int `json:\"day,omitempty\"` // max requests per day (0 = unlimited)\n\tWeek int `json:\"week,omitempty\"` // max requests per week (0 = unlimited)\n}\n\n// IsZero returns true if no limits are set.\nfunc (w QuotaWindow) IsZero() bool { return w.Hour == 0 && w.Day == 0 && w.Week == 0 }\n\n// QuotaConfig configures per-user/group request quotas.\n// Config merge priority: Groups > Channels > Providers > Default.\ntype QuotaConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tDefault QuotaWindow `json:\"default\"`\n\tProviders map[string]QuotaWindow `json:\"providers,omitempty\"` // key = provider name (e.g. \"anthropic\")\n\tChannels map[string]QuotaWindow `json:\"channels,omitempty\"` // key = channel name (e.g. \"telegram\")\n\tGroups map[string]QuotaWindow `json:\"groups,omitempty\"` // key = userID (e.g. \"group:telegram:-100123\")\n}\n\n// GatewayConfig controls the gateway server.\ntype GatewayConfig struct {\n\tHost string `json:\"host\"`\n\tPort int `json:\"port\"`\n\tToken string `json:\"token,omitempty\"` // bearer token for WS/HTTP auth\n\tOwnerIDs []string `json:\"owner_ids,omitempty\"` // sender IDs considered \"owner\"\n\tAllowedOrigins []string `json:\"allowed_origins,omitempty\"` // WebSocket CORS whitelist (empty = allow all)\n\tMaxMessageChars int `json:\"max_message_chars,omitempty\"` // max user message characters (default 32000)\n\tRateLimitRPM int `json:\"rate_limit_rpm,omitempty\"` // rate limit: requests per minute per user (default 20, 0 = disabled)\n\tInjectionAction string `json:\"injection_action,omitempty\"` // prompt injection action: \"log\", \"warn\" (default), \"block\", \"off\"\n\tInboundDebounceMs int `json:\"inbound_debounce_ms,omitempty\"` // merge rapid channel/Web Chat messages from same sender/session (0 = no wait)\n\tQuota *QuotaConfig `json:\"quota,omitempty\"` // per-user/group request quotas\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // deliver intermediate text during tool iterations (default false)\n\tToolStatus *bool `json:\"tool_status,omitempty\"` // show tool name in streaming preview during tool execution (default true)\n\tTaskRecoveryIntervalSec int `json:\"task_recovery_interval_sec,omitempty\"` // team task recovery ticker interval in seconds (default 300 = 5min)\n\tBackgroundProvider string `json:\"background_provider,omitempty\"` // LLM provider for background workers (vault enrichment, consolidation)\n\tBackgroundModel string `json:\"background_model,omitempty\"` // LLM model for background workers\n}\n\n// ToolsConfig controls tool availability, policy, and web search.\ntype ToolsConfig struct {\n\tProfile string `json:\"profile,omitempty\"` // global profile: \"minimal\", \"coding\", \"messaging\", \"full\"\n\tAllow []string `json:\"allow,omitempty\"` // global allow list (tool names or \"group:xxx\")\n\tDeny []string `json:\"deny,omitempty\"` // global deny list\n\tAlsoAllow []string `json:\"alsoAllow,omitempty\"` // additive: adds without removing existing\n\tByProvider map[string]*ToolPolicySpec `json:\"byProvider,omitempty\"` // per-provider overrides\n\tShellDenyGroups map[string]bool `json:\"shellDenyGroups,omitempty\"` // global shell deny-group toggles (group name -> denied); per-agent overrides win per-key\n\tExecApproval ExecApprovalCfg `json:\"execApproval\"` // exec command approval settings\n\tWebFetch WebFetchPolicyConfig `json:\"web_fetch\"` // domain policy for URL fetching\n\tBrowser BrowserToolConfig `json:\"browser\"`\n\tRateLimitPerHour int `json:\"rate_limit_per_hour,omitempty\"` // max tool executions per hour per session (0 = disabled)\n\tScrubCredentials *bool `json:\"scrub_credentials,omitempty\"` // auto-redact API keys/tokens in tool output (default true)\n\tMcpServers map[string]*MCPServerConfig `json:\"mcp_servers,omitempty\"` // external MCP server connections\n}\n\n// MCPServerConfig configures a single external MCP server connection.\ntype MCPServerConfig struct {\n\tTransport string `json:\"transport\"` // \"stdio\", \"sse\", \"streamable-http\"\n\tCommand string `json:\"command,omitempty\"` // stdio: command to spawn\n\tArgs []string `json:\"args,omitempty\"` // stdio: command arguments\n\tEnv map[string]string `json:\"env,omitempty\"` // stdio: extra environment variables\n\tURL string `json:\"url,omitempty\"` // sse/http: server URL\n\tHeaders map[string]string `json:\"headers,omitempty\"` // sse/http: extra HTTP headers\n\tEnabled *bool `json:\"enabled,omitempty\"` // default true\n\tToolPrefix string `json:\"tool_prefix,omitempty\"` // prefix for tool names (avoids collisions)\n\tTimeoutSec int `json:\"timeout_sec,omitempty\"` // per-tool-call timeout in seconds (default 60)\n}\n\n// IsEnabled returns whether this MCP server is enabled (default true).\nfunc (c *MCPServerConfig) IsEnabled() bool {\n\treturn c.Enabled == nil || *c.Enabled\n}\n\n// ExecApprovalCfg configures command execution approval (matching TS exec-approval.ts).\ntype ExecApprovalCfg struct {\n\tSecurity string `json:\"security,omitempty\"` // \"deny\", \"allowlist\", \"full\" (default \"full\")\n\tAsk string `json:\"ask,omitempty\"` // \"off\", \"on-miss\", \"always\" (default \"off\")\n\tAllowlist []string `json:\"allowlist,omitempty\"` // glob patterns for allowed commands\n}\n\n// WebFetchPolicyConfig controls domain filtering for the web_fetch tool.\ntype WebFetchPolicyConfig struct {\n\tPolicy string `json:\"policy,omitempty\"` // \"allow_all\" (default), \"allowlist\"\n\tAllowedDomains []string `json:\"allowed_domains,omitempty\"` // e.g. [\"github.com\", \"*.example.com\"]\n\tBlockedDomains []string `json:\"blocked_domains,omitempty\"` // always checked regardless of policy\n}\n\n// BrowserToolConfig controls the browser automation tool.\ntype BrowserToolConfig struct {\n\tEnabled bool `json:\"enabled\"` // enable the browser tool (default false)\n\tHeadless bool `json:\"headless,omitempty\"` // run Chrome in headless mode (ignored when RemoteURL is set)\n\tRemoteURL string `json:\"remote_url,omitempty\"` // CDP endpoint for remote Chrome sidecar, e.g. \"ws://chrome:9222\"\n\tActionTimeoutMs int `json:\"action_timeout_ms,omitempty\"` // per-action timeout in ms (default 30000)\n\tIdleTimeoutMs int `json:\"idle_timeout_ms,omitempty\"` // idle page auto-close in ms (default 600000, 0=disabled)\n\tMaxPages int `json:\"max_pages,omitempty\"` // max open pages per tenant (default 5)\n\tCookieSyncEnabled bool `json:\"cookie_sync_enabled\"` // apply selected synced cookies to scoped browser sessions\n}\n\n// ToolPolicySpec defines a tool policy at any level (global, per-agent, per-provider).\ntype ToolPolicySpec struct {\n\tProfile string `json:\"profile,omitempty\"`\n\tAllow []string `json:\"allow,omitempty\"`\n\tDeny []string `json:\"deny,omitempty\"`\n\tAlsoAllow []string `json:\"alsoAllow,omitempty\"`\n\tByProvider map[string]*ToolPolicySpec `json:\"byProvider,omitempty\"`\n\tWait *WaitToolPolicy `json:\"wait,omitempty\"`\n\tToolCallPrefix string `json:\"toolCallPrefix,omitempty\"` // prefix to strip from model's tool call names before registry lookup\n}\n\n// WaitToolPolicy configures per-agent safety bounds for the wait tool.\ntype WaitToolPolicy struct {\n\tMinMs int `json:\"min_ms,omitempty\"`\n\tMaxMs int `json:\"max_ms,omitempty\"`\n}\n\n// SessionsConfig controls session behavior.\n// Matching TS src/config/sessions/types.ts + src/config/types.base.ts.\ntype SessionsConfig struct {\n\tScope string `json:\"scope,omitempty\"` // \"per-sender\" (default), \"global\"\n\tDmScope string `json:\"dm_scope,omitempty\"` // \"main\", \"per-peer\", \"per-channel-peer\" (default), \"per-account-channel-peer\"\n\tMainKey string `json:\"main_key,omitempty\"` // main session key suffix (default \"main\", used when dm_scope=\"main\")\n}\n\n// TtsConfig configures text-to-speech.\n// Matching TS src/config/types.tts.ts.\ntype TtsConfig struct {\n\tProvider string `json:\"provider,omitempty\"` // \"openai\", \"elevenlabs\", \"edge\", \"minimax\", \"gemini\"\n\tAuto string `json:\"auto,omitempty\"` // \"off\" (default), \"always\", \"inbound\", \"tagged\"\n\tMode string `json:\"mode,omitempty\"` // \"final\" (default), \"all\"\n\tMaxLength int `json:\"max_length,omitempty\"` // max text length before truncation (default 1500)\n\tTimeoutMs int `json:\"timeout_ms,omitempty\"` // API timeout in ms (default 30000)\n\tOpenAI TtsOpenAIConfig `json:\"openai\"`\n\tElevenLabs TtsElevenLabsConfig `json:\"elevenlabs\"`\n\tEdge TtsEdgeConfig `json:\"edge\"`\n\tMiniMax TtsMiniMaxConfig `json:\"minimax\"`\n\tGemini TtsGeminiConfig `json:\"gemini\"`\n}\n\n// TtsGeminiConfig configures the Google Gemini TTS provider.\ntype TtsGeminiConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"` // required; encrypted at rest\n\tAPIBase string `json:\"api_base,omitempty\"` // custom endpoint (optional; SSRF-gated)\n\tVoice string `json:\"voice,omitempty\"` // default \"Kore\"\n\tModel string `json:\"model,omitempty\"` // default \"gemini-2.5-flash-preview-tts\"\n\tSpeakers string `json:\"speakers,omitempty\"` // JSON-encoded []SpeakerVoice for multi-speaker mode\n}\n\n// TtsOpenAIConfig configures the OpenAI TTS provider.\ntype TtsOpenAIConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"`\n\tAPIBase string `json:\"api_base,omitempty\"` // custom endpoint URL\n\tModel string `json:\"model,omitempty\"` // default \"gpt-4o-mini-tts\"\n\tVoice string `json:\"voice,omitempty\"` // default \"alloy\"\n}\n\n// TtsElevenLabsConfig configures the ElevenLabs TTS provider.\ntype TtsElevenLabsConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"`\n\tBaseURL string `json:\"base_url,omitempty\"`\n\tVoiceID string `json:\"voice_id,omitempty\"` // default \"pMsXgVXv3BLzUgSXRplE\"\n\tModelID string `json:\"model_id,omitempty\"` // default \"eleven_multilingual_v2\"\n}\n\n// TtsEdgeConfig configures the Microsoft Edge TTS provider (free, no API key).\ntype TtsEdgeConfig struct {\n\tEnabled bool `json:\"enabled,omitempty\"`\n\tVoice string `json:\"voice,omitempty\"` // default \"en-US-MichelleNeural\"\n\tRate string `json:\"rate,omitempty\"` // speech rate, e.g. \"+0%\"\n}\n\n// TtsMiniMaxConfig configures the MiniMax TTS provider.\ntype TtsMiniMaxConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"`\n\tGroupID string `json:\"group_id,omitempty\"` // MiniMax GroupId (required)\n\tAPIBase string `json:\"api_base,omitempty\"` // default \"https://api.minimax.io/v1\"\n\tModel string `json:\"model,omitempty\"` // default \"speech-02-hd\"\n\tVoiceID string `json:\"voice_id,omitempty\"` // default \"Wise_Woman\"\n}\n\n// MergeChannelGroupQuotas merges per-group quota overrides from channel configs\n// (e.g., channels.telegram.groups[chatID].quota) into gateway.quota.groups.\n// This allows per-group quotas to be set at the channel level and picked up\n// by the QuotaChecker at runtime.\nfunc MergeChannelGroupQuotas(cfg *Config) {\n\tif cfg.Gateway.Quota == nil {\n\t\treturn\n\t}\n\tif cfg.Gateway.Quota.Groups == nil {\n\t\tcfg.Gateway.Quota.Groups = make(map[string]QuotaWindow)\n\t}\n\n\t// Telegram per-group quotas\n\tfor chatID, groupCfg := range cfg.Channels.Telegram.Groups {\n\t\tif groupCfg != nil && groupCfg.Quota != nil && !groupCfg.Quota.IsZero() {\n\t\t\tkey := \"group:telegram:\" + chatID\n\t\t\tcfg.Gateway.Quota.Groups[key] = *groupCfg.Quota\n\t\t}\n\t}\n}\n"}, "2c699f31dead16153de7e34a3a9d86353f4802ca": {"t": 1785759236.2141404, "n": 33811, "h": "0500f1cca8b9b73a", "c": "package config\n\n// PendingCompactionConfig configures LLM-based compaction of pending group messages.\n// When a group accumulates more than Threshold pending messages, older messages are\n// summarized by an LLM and replaced with a compact summary, keeping KeepRecent raw messages.\ntype PendingCompactionConfig struct {\n\tThreshold int `json:\"threshold,omitempty\"` // trigger compaction when entries exceed this (default 200)\n\tKeepRecent int `json:\"keep_recent,omitempty\"` // keep this many recent raw messages after compaction (default 40)\n\tMaxTokens int `json:\"max_tokens,omitempty\"` // max output tokens for LLM summarization (default 4096)\n\tProvider string `json:\"provider,omitempty\"` // LLM provider name (e.g. \"openai\"); empty = use agent's provider\n\tModel string `json:\"model,omitempty\"` // model for summarization; empty = use agent's model\n}\n\n// ChannelsConfig contains per-channel configuration.\ntype ChannelsConfig struct {\n\tTelegram TelegramConfig `json:\"telegram\"`\n\tDiscord DiscordConfig `json:\"discord\"`\n\tSlack SlackConfig `json:\"slack\"`\n\tWhatsApp WhatsAppConfig `json:\"whatsapp\"`\n\tZalo ZaloConfig `json:\"zalo\"`\n\tZaloPersonal ZaloPersonalConfig `json:\"zalo_personal\"`\n\tFeishu FeishuConfig `json:\"feishu\"`\n\tPendingCompaction *PendingCompactionConfig `json:\"pending_compaction,omitempty\"` // global pending message compaction settings\n}\n\ntype TelegramConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tToken string `json:\"token\"`\n\tProxy string `json:\"proxy,omitempty\"`\n\tAPIServer string `json:\"api_server,omitempty\"` // custom Telegram Bot API server URL (e.g. \"http://localhost:8081\")\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in groups (default true)\n\tMentionMode string `json:\"mention_mode,omitempty\"` // \"strict\" (default) = only respond when mentioned; \"yield\" = respond unless another bot is mentioned\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tDMStream *bool `json:\"dm_stream,omitempty\"` // enable streaming for DMs (default false) — edits placeholder progressively\n\tGroupStream *bool `json:\"group_stream,omitempty\"` // enable streaming for groups (default false) — sends new message, edits progressively\n\tDraftTransport *bool `json:\"draft_transport,omitempty\"` // use sendMessageDraft for DM streaming (default true) — stealth preview, no notifications per edit\n\tReasoningStream *bool `json:\"reasoning_stream,omitempty\"` // show reasoning as separate message when provider emits thinking events (default true)\n\tReactionLevel string `json:\"reaction_level,omitempty\"` // \"off\" (default), \"minimal\", \"full\" — status emoji reactions\n\tMediaMaxBytes int64 `json:\"media_max_bytes,omitempty\"` // max media download size in bytes (default 20MB)\n\tLinkPreview *bool `json:\"link_preview,omitempty\"` // enable URL previews in messages (default true)\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tForceIPv4 bool `json:\"force_ipv4,omitempty\"` // force IPv4 for all Telegram API requests (use when IPv6 routing is broken)\n\n\t// Optional STT (Speech-to-Text) pipeline for voice/audio inbound messages.\n\t// When stt_proxy_url is set, audio/voice messages are transcribed before being forwarded to the agent.\n\tSTTProxyURL string `json:\"stt_proxy_url,omitempty\"` // base URL of the STT proxy service (e.g. \"https://stt.example.com\")\n\tSTTAPIKey string `json:\"stt_api_key,omitempty\"` // Bearer token for the STT proxy\n\tSTTTenantID string `json:\"stt_tenant_id,omitempty\"` // optional tenant/org identifier forwarded to the STT proxy\n\tSTTTimeoutSeconds int `json:\"stt_timeout_seconds,omitempty\"` // per-request timeout for STT calls (default 30s)\n\n\t// Optional audio-aware routing: when set, voice/audio inbound messages are routed to this\n\t// agent instead of the default channel agent. Requires the named agent to exist in the config.\n\tVoiceAgentID string `json:\"voice_agent_id,omitempty\"` // agent ID to route voice inbound to (e.g. \"speaking-agent\")\n\n\t// Audio guard: intercept technical errors in voice agent replies and replace with friendly fallbacks.\n\t// Only active when VoiceAgentID is set. Custom error markers replace built-in defaults when provided.\n\tAudioGuardFallbackTranscript string `json:\"audio_guard_fallback_transcript,omitempty\"` // fallback with %s for transcript (e.g. \"I heard: \\\"%s\\\". Try again!\")\n\tAudioGuardFallbackNoTranscript string `json:\"audio_guard_fallback_no_transcript,omitempty\"` // fallback when no transcript available\n\tAudioGuardErrorMarkers []string `json:\"audio_guard_error_markers,omitempty\"` // custom error detection markers (replaces defaults)\n\n\t// Per-group (and per-topic) overrides. Key is chat ID string (e.g. \"-100123456\") or \"*\" for wildcard.\n\t// TS ref: channels.telegram.groups in src/config/types.telegram.ts.\n\tGroups map[string]*TelegramGroupConfig `json:\"groups,omitempty\"`\n}\n\n// TelegramGroupConfig defines per-group overrides for a Telegram channel.\n// Matching TS TelegramGroupConfig in src/config/types.telegram.ts.\ntype TelegramGroupConfig struct {\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // override group policy for this group\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // override require_mention for this group\n\tMentionMode string `json:\"mention_mode,omitempty\"` // override mention_mode for this group\n\tAllowFrom FlexibleStringSlice `json:\"allow_from,omitempty\"` // override allow_from for this group\n\tEnabled *bool `json:\"enabled,omitempty\"` // disable bot for this group (default: true)\n\tSkills []string `json:\"skills,omitempty\"` // skill whitelist (nil = all, [] = none)\n\tTools []string `json:\"tools,omitempty\"` // tool allow list (nil = all, supports \"group:xxx\")\n\tSystemPrompt string `json:\"system_prompt,omitempty\"` // extra system prompt for this group\n\tTopics map[string]*TelegramTopicConfig `json:\"topics,omitempty\"` // per-topic overrides (key: thread ID string)\n\tQuota *QuotaWindow `json:\"quota,omitempty\"` // per-group quota override\n}\n\n// TelegramTopicConfig defines per-topic overrides within a Telegram group.\n// Matching TS TelegramTopicConfig in src/config/types.telegram.ts.\ntype TelegramTopicConfig struct {\n\tRequireMention *bool `json:\"require_mention,omitempty\"`\n\tMentionMode string `json:\"mention_mode,omitempty\"`\n\tGroupPolicy string `json:\"group_policy,omitempty\"`\n\tSkills []string `json:\"skills,omitempty\"`\n\tTools []string `json:\"tools,omitempty\"` // tool allow list (nil = inherit, supports \"group:xxx\")\n\tEnabled *bool `json:\"enabled,omitempty\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from,omitempty\"`\n\tSystemPrompt string `json:\"system_prompt,omitempty\"`\n}\n\ntype DiscordConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tToken string `json:\"token\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in groups (default true)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tMediaMaxBytes int64 `json:\"media_max_bytes,omitempty\"` // max media download size (default 25MB)\n\tSTTProxyURL string `json:\"stt_proxy_url,omitempty\"`\n\tSTTAPIKey string `json:\"stt_api_key,omitempty\"`\n\tSTTTenantID string `json:\"stt_tenant_id,omitempty\"`\n\tSTTTimeoutSeconds int `json:\"stt_timeout_seconds,omitempty\"`\n\tVoiceAgentID string `json:\"voice_agent_id,omitempty\"`\n}\n\ntype SlackConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tBotToken string `json:\"bot_token\"` // xoxb-... (Bot User OAuth Token)\n\tAppToken string `json:\"app_token\"` // xapp-... (App-Level Token for Socket Mode)\n\tUserToken string `json:\"user_token,omitempty\"` // xoxp-... (Optional: custom bot identity)\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"pairing\", \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in channels (default true)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tDMStream *bool `json:\"dm_stream,omitempty\"` // enable streaming for DMs (default false)\n\tGroupStream *bool `json:\"group_stream,omitempty\"` // enable streaming for groups (default false)\n\tNativeStream *bool `json:\"native_stream,omitempty\"` // use Slack ChatStreamer API if available (default false)\n\tReactionLevel string `json:\"reaction_level,omitempty\"` // \"off\" (default), \"minimal\", \"full\"\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tDebounceDelay *int `json:\"debounce_delay,omitempty\"` // ms delay before dispatching rapid messages (default 300, 0=disabled)\n\tThreadTTL *int `json:\"thread_ttl,omitempty\"` // hours before thread participation expires (default 24, 0=disabled — always require @mention)\n\tMediaMaxBytes int64 `json:\"media_max_bytes,omitempty\"` // max file download size in bytes (default 20MB)\n}\n\ntype WhatsAppConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tAuthDir string `json:\"auth_dir,omitempty\"` // optional: SQLite auth dir override (desktop)\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default for DB instances), \"open\", \"allowlist\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"pairing\" (default for DB instances), \"open\" (default for config), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // only respond in groups when bot is @mentioned (default false)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 200, 0=disabled)\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n}\n\ntype ZaloConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tToken string `json:\"token\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tWebhookURL string `json:\"webhook_url,omitempty\"`\n\tWebhookSecret string `json:\"webhook_secret,omitempty\"`\n\tMediaMaxMB int `json:\"media_max_mb,omitempty\"` // default 5\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n}\n\ntype ZaloPersonalConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in groups (default true)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tCredentialsPath string `json:\"credentials_path,omitempty\"` // path to saved cookies JSON\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n}\n\ntype FeishuConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tAppID string `json:\"app_id\"`\n\tAppSecret string `json:\"app_secret\"`\n\tEncryptKey string `json:\"encrypt_key,omitempty\"`\n\tVerificationToken string `json:\"verification_token,omitempty\"`\n\tDomain string `json:\"domain,omitempty\"` // \"lark\" (default/global), \"feishu\" (China), or custom URL\n\tConnectionMode string `json:\"connection_mode,omitempty\"` // \"websocket\" (default), \"webhook\"\n\tWebhookPort int `json:\"webhook_port,omitempty\"` // default 3000\n\tWebhookPath string `json:\"webhook_path,omitempty\"` // default \"/feishu/events\"\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default)\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default)\n\tGroupAllowFrom FlexibleStringSlice `json:\"group_allow_from,omitempty\"`\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // default true (groups)\n\tTopicSessionMode string `json:\"topic_session_mode,omitempty\"` // \"disabled\" (default)\n\tTextChunkLimit int `json:\"text_chunk_limit,omitempty\"` // default 4000\n\tMediaMaxMB int `json:\"media_max_mb,omitempty\"` // default 30\n\tRenderMode string `json:\"render_mode,omitempty\"` // \"auto\", \"raw\", \"card\"\n\tStreaming *bool `json:\"streaming,omitempty\"` // default true\n\tReactionLevel string `json:\"reaction_level,omitempty\"` // \"off\" (default), \"minimal\", \"full\" — typing emoji reactions\n\tHistoryLimit int `json:\"history_limit,omitempty\"`\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tSTTProxyURL string `json:\"stt_proxy_url,omitempty\"`\n\tSTTAPIKey string `json:\"stt_api_key,omitempty\"`\n\tSTTTenantID string `json:\"stt_tenant_id,omitempty\"`\n\tSTTTimeoutSeconds int `json:\"stt_timeout_seconds,omitempty\"`\n\tVoiceAgentID string `json:\"voice_agent_id,omitempty\"`\n}\n\n// ProvidersConfig maps provider name to its config.\ntype ProvidersConfig struct {\n\tAnthropic ProviderConfig `json:\"anthropic\"`\n\tOpenAI ProviderConfig `json:\"openai\"`\n\tOpenRouter ProviderConfig `json:\"openrouter\"`\n\tGroq ProviderConfig `json:\"groq\"`\n\tGemini ProviderConfig `json:\"gemini\"`\n\tDeepSeek ProviderConfig `json:\"deepseek\"`\n\tMistral ProviderConfig `json:\"mistral\"`\n\tXAI ProviderConfig `json:\"xai\"`\n\tMiniMax ProviderConfig `json:\"minimax\"`\n\tCohere ProviderConfig `json:\"cohere\"`\n\tPerplexity ProviderConfig `json:\"perplexity\"`\n\tDashScope ProviderConfig `json:\"dashscope\"`\n\tBailian ProviderConfig `json:\"bailian\"`\n\tZai ProviderConfig `json:\"zai\"`\n\tZaiCoding ProviderConfig `json:\"zai_coding\"`\n\tOllama OllamaConfig `json:\"ollama\"` // local Ollama instance (no API key needed)\n\tOllamaCloud ProviderConfig `json:\"ollama_cloud\"` // Ollama Cloud (API key required)\n\tClaudeCLI ClaudeCLIConfig `json:\"claude_cli\"`\n\tACP ACPConfig `json:\"acp\"`\n\tNovita ProviderConfig `json:\"novita\"` // Novita AI (OpenAI-compatible endpoint)\n\tBytePlus ProviderConfig `json:\"byteplus\"` // BytePlus ModelArk (Seed 2.0)\n\tBytePlusCoding ProviderConfig `json:\"byteplus_coding\"` // BytePlus ModelArk Coding Plan\n\tVertex VertexConfig `json:\"vertex\"` // Google Cloud Vertex AI (OAuth2 service account + ADC)\n}\n\n// VertexConfig configures Google Cloud Vertex AI.\n// Credentials precedence: APIKey (inline JSON) > CredentialsFile (path) > ADC (both empty).\n// ProjectID and Region are required; Model optional (defaults to google/gemini-2.0-flash-001).\ntype VertexConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"` // service account JSON inline (secret — never persist in config.json)\n\tCredentialsFile string `json:\"credentials_file,omitempty\"` // path to service account JSON file\n\tProjectID string `json:\"project_id,omitempty\"`\n\tRegion string `json:\"region,omitempty\"`\n\tModel string `json:\"model,omitempty\"`\n}\n\n// OllamaConfig configures a local (or self-hosted) Ollama instance.\n// No API key is required — Ollama accepts any Bearer token value.\ntype OllamaConfig struct {\n\tHost string `json:\"host\"` // Ollama server base URL, e.g. http://localhost:11434\n}\n\n// ClaudeCLIConfig configures the Claude CLI provider (uses subscription, not API key).\ntype ClaudeCLIConfig struct {\n\tCLIPath string `json:\"cli_path\" yaml:\"cli_path\"` // path to claude binary (default: \"claude\")\n\tModel string `json:\"model\" yaml:\"model\"` // default model alias (default: \"sonnet\")\n\tBaseWorkDir string `json:\"base_work_dir\" yaml:\"base_work_dir\"` // base dir for agent workspaces\n\tPermMode string `json:\"perm_mode\" yaml:\"perm_mode\"` // permission mode (default: \"bypassPermissions\")\n}\n\n// ACPConfig configures the ACP (Agent Client Protocol) provider.\n// Orchestrates any ACP-compatible coding agent (Claude Code, Codex CLI, Gemini CLI) as a subprocess.\ntype ACPConfig struct {\n\tBinary string `json:\"binary\"` // agent binary name or path (e.g. \"claude\", \"codex\")\n\tArgs []string `json:\"args\"` // extra spawn args\n\tModel string `json:\"model\"` // default model/agent name\n\tWorkDir string `json:\"work_dir\"` // base workspace dir\n\tIdleTTL string `json:\"idle_ttl\"` // process idle TTL (e.g. \"5m\")\n\tPermMode string `json:\"perm_mode\"` // \"approve-all\" (default), \"approve-reads\", \"deny-all\"\n}\n\ntype ProviderConfig struct {\n\tAPIKey string `json:\"api_key\"`\n\tAPIBase string `json:\"api_base,omitempty\"`\n}\n\n// APIBaseForType returns the config-level api_base for a given provider type.\n// Used as a fallback when DB providers have no api_base set.\nfunc (p *ProvidersConfig) APIBaseForType(providerType string) string {\n\tswitch providerType {\n\tcase \"anthropic_native\":\n\t\treturn p.Anthropic.APIBase\n\tcase \"openai\", \"openai_compat\":\n\t\treturn p.OpenAI.APIBase\n\tcase \"openrouter\":\n\t\treturn p.OpenRouter.APIBase\n\tcase \"groq\":\n\t\treturn p.Groq.APIBase\n\tcase \"deepseek\":\n\t\treturn p.DeepSeek.APIBase\n\tcase \"gemini_native\":\n\t\treturn p.Gemini.APIBase\n\tcase \"mistral\":\n\t\treturn p.Mistral.APIBase\n\tcase \"xai\":\n\t\treturn p.XAI.APIBase\n\tcase \"minimax_native\":\n\t\treturn p.MiniMax.APIBase\n\tcase \"cohere\":\n\t\treturn p.Cohere.APIBase\n\tcase \"perplexity\":\n\t\treturn p.Perplexity.APIBase\n\tcase \"dashscope\":\n\t\treturn p.DashScope.APIBase\n\tcase \"bailian\":\n\t\treturn p.Bailian.APIBase\n\tcase \"zai\":\n\t\treturn p.Zai.APIBase\n\tcase \"zai_coding\":\n\t\treturn p.ZaiCoding.APIBase\n\tcase \"ollama_cloud\":\n\t\treturn p.OllamaCloud.APIBase\n\tcase \"novita\":\n\t\treturn p.Novita.APIBase\n\tcase \"byteplus\":\n\t\treturn p.BytePlus.APIBase\n\tcase \"byteplus_coding\":\n\t\treturn p.BytePlusCoding.APIBase\n\tcase \"vertex\":\n\t\t// Computed from project+region at registration time; no config-level static base.\n\t\treturn \"\"\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\n// HasAnyProvider returns true if at least one provider has an API key or CLI configured.\nfunc (c *Config) HasAnyProvider() bool {\n\tp := c.Providers\n\treturn p.Anthropic.APIKey != \"\" ||\n\t\tp.OpenAI.APIKey != \"\" ||\n\t\tp.OpenRouter.APIKey != \"\" ||\n\t\tp.Groq.APIKey != \"\" ||\n\t\tp.Gemini.APIKey != \"\" ||\n\t\tp.DeepSeek.APIKey != \"\" ||\n\t\tp.Mistral.APIKey != \"\" ||\n\t\tp.XAI.APIKey != \"\" ||\n\t\tp.MiniMax.APIKey != \"\" ||\n\t\tp.Cohere.APIKey != \"\" ||\n\t\tp.Perplexity.APIKey != \"\" ||\n\t\tp.DashScope.APIKey != \"\" ||\n\t\tp.Bailian.APIKey != \"\" ||\n\t\tp.Zai.APIKey != \"\" ||\n\t\tp.ZaiCoding.APIKey != \"\" ||\n\t\tp.Ollama.Host != \"\" ||\n\t\tp.OllamaCloud.APIKey != \"\" ||\n\t\tp.ClaudeCLI.CLIPath != \"\" ||\n\t\tp.ACP.Binary != \"\" ||\n\t\tp.Novita.APIKey != \"\" ||\n\t\tp.BytePlus.APIKey != \"\" ||\n\t\tp.BytePlusCoding.APIKey != \"\" ||\n\t\t(p.Vertex.ProjectID != \"\" && p.Vertex.Region != \"\")\n}\n\n// QuotaWindow defines request limits per time window. Zero means unlimited.\ntype QuotaWindow struct {\n\tHour int `json:\"hour,omitempty\"` // max requests per hour (0 = unlimited)\n\tDay int `json:\"day,omitempty\"` // max requests per day (0 = unlimited)\n\tWeek int `json:\"week,omitempty\"` // max requests per week (0 = unlimited)\n}\n\n// IsZero returns true if no limits are set.\nfunc (w QuotaWindow) IsZero() bool { return w.Hour == 0 && w.Day == 0 && w.Week == 0 }\n\n// QuotaConfig configures per-user/group request quotas.\n// Config merge priority: Groups > Channels > Providers > Default.\ntype QuotaConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tDefault QuotaWindow `json:\"default\"`\n\tProviders map[string]QuotaWindow `json:\"providers,omitempty\"` // key = provider name (e.g. \"anthropic\")\n\tChannels map[string]QuotaWindow `json:\"channels,omitempty\"` // key = channel name (e.g. \"telegram\")\n\tGroups map[string]QuotaWindow `json:\"groups,omitempty\"` // key = userID (e.g. \"group:telegram:-100123\")\n}\n\n// GatewayConfig controls the gateway server.\ntype GatewayConfig struct {\n\tHost string `json:\"host\"`\n\tPort int `json:\"port\"`\n\tToken string `json:\"token,omitempty\"` // bearer token for WS/HTTP auth\n\tOwnerIDs []string `json:\"owner_ids,omitempty\"` // sender IDs considered \"owner\"\n\tAllowedOrigins []string `json:\"allowed_origins,omitempty\"` // WebSocket CORS whitelist (empty = allow all)\n\tMaxMessageChars int `json:\"max_message_chars,omitempty\"` // max user message characters (default 32000)\n\tRateLimitRPM int `json:\"rate_limit_rpm,omitempty\"` // rate limit: requests per minute per user (default 20, 0 = disabled)\n\tInjectionAction string `json:\"injection_action,omitempty\"` // prompt injection action: \"log\", \"warn\" (default), \"block\", \"off\"\n\tInboundDebounceMs int `json:\"inbound_debounce_ms,omitempty\"` // merge rapid channel/Web Chat messages from same sender/session (0 = no wait)\n\tQuota *QuotaConfig `json:\"quota,omitempty\"` // per-user/group request quotas\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // deliver intermediate text during tool iterations (default false)\n\tToolStatus *bool `json:\"tool_status,omitempty\"` // show tool name in streaming preview during tool execution (default true)\n\tTaskRecoveryIntervalSec int `json:\"task_recovery_interval_sec,omitempty\"` // team task recovery ticker interval in seconds (default 300 = 5min)\n\tBackgroundProvider string `json:\"background_provider,omitempty\"` // LLM provider for background workers (vault enrichment, consolidation)\n\tBackgroundModel string `json:\"background_model,omitempty\"` // LLM model for background workers\n}\n\n// ToolsConfig controls tool availability, policy, and web search.\ntype ToolsConfig struct {\n\tProfile string `json:\"profile,omitempty\"` // global profile: \"minimal\", \"coding\", \"messaging\", \"full\"\n\tAllow []string `json:\"allow,omitempty\"` // global allow list (tool names or \"group:xxx\")\n\tDeny []string `json:\"deny,omitempty\"` // global deny list\n\tAlsoAllow []string `json:\"alsoAllow,omitempty\"` // additive: adds without removing existing\n\tByProvider map[string]*ToolPolicySpec `json:\"byProvider,omitempty\"` // per-provider overrides\n\tShellDenyGroups map[string]bool `json:\"shellDenyGroups,omitempty\"` // global shell deny-group toggles (group name -> denied); per-agent overrides win per-key\n\tExecApproval ExecApprovalCfg `json:\"execApproval\"` // exec command approval settings\n\tWebFetch WebFetchPolicyConfig `json:\"web_fetch\"` // domain policy for URL fetching\n\tBrowser BrowserToolConfig `json:\"browser\"`\n\tRateLimitPerHour int `json:\"rate_limit_per_hour,omitempty\"` // max tool executions per hour per session (0 = disabled)\n\tScrubCredentials *bool `json:\"scrub_credentials,omitempty\"` // auto-redact API keys/tokens in tool output (default true)\n\tMcpServers map[string]*MCPServerConfig `json:\"mcp_servers,omitempty\"` // external MCP server connections\n}\n\n// MCPServerConfig configures a single external MCP server connection.\ntype MCPServerConfig struct {\n\tTransport string `json:\"transport\"` // \"stdio\", \"sse\", \"streamable-http\"\n\tCommand string `json:\"command,omitempty\"` // stdio: command to spawn\n\tArgs []string `json:\"args,omitempty\"` // stdio: command arguments\n\tEnv map[string]string `json:\"env,omitempty\"` // stdio: extra environment variables\n\tURL string `json:\"url,omitempty\"` // sse/http: server URL\n\tHeaders map[string]string `json:\"headers,omitempty\"` // sse/http: extra HTTP headers\n\tEnabled *bool `json:\"enabled,omitempty\"` // default true\n\tToolPrefix string `json:\"tool_prefix,omitempty\"` // prefix for tool names (avoids collisions)\n\tTimeoutSec int `json:\"timeout_sec,omitempty\"` // per-tool-call timeout in seconds (default 60)\n}\n\n// IsEnabled returns whether this MCP server is enabled (default true).\nfunc (c *MCPServerConfig) IsEnabled() bool {\n\treturn c.Enabled == nil || *c.Enabled\n}\n\n// ExecApprovalCfg configures command execution approval (matching TS exec-approval.ts).\ntype ExecApprovalCfg struct {\n\tSecurity string `json:\"security,omitempty\"` // \"deny\", \"allowlist\", \"full\" (default \"full\")\n\tAsk string `json:\"ask,omitempty\"` // \"off\", \"on-miss\", \"always\" (default \"off\")\n\tAllowlist []string `json:\"allowlist,omitempty\"` // glob patterns for allowed commands\n}\n\n// WebFetchPolicyConfig controls domain filtering for the web_fetch tool.\ntype WebFetchPolicyConfig struct {\n\tPolicy string `json:\"policy,omitempty\"` // \"allow_all\" (default), \"allowlist\"\n\tAllowedDomains []string `json:\"allowed_domains,omitempty\"` // e.g. [\"github.com\", \"*.example.com\"]\n\tBlockedDomains []string `json:\"blocked_domains,omitempty\"` // always checked regardless of policy\n}\n\n// BrowserToolConfig controls the browser automation tool.\ntype BrowserToolConfig struct {\n\tEnabled bool `json:\"enabled\"` // enable the browser tool (default false)\n\tHeadless bool `json:\"headless,omitempty\"` // run Chrome in headless mode (ignored when RemoteURL is set)\n\tRemoteURL string `json:\"remote_url,omitempty\"` // CDP endpoint for remote Chrome sidecar, e.g. \"ws://chrome:9222\"\n\tActionTimeoutMs int `json:\"action_timeout_ms,omitempty\"` // per-action timeout in ms (default 30000)\n\tIdleTimeoutMs int `json:\"idle_timeout_ms,omitempty\"` // idle page auto-close in ms (default 600000, 0=disabled)\n\tMaxPages int `json:\"max_pages,omitempty\"` // max open pages per tenant (default 5)\n}\n\n// ToolPolicySpec defines a tool policy at any level (global, per-agent, per-provider).\ntype ToolPolicySpec struct {\n\tProfile string `json:\"profile,omitempty\"`\n\tAllow []string `json:\"allow,omitempty\"`\n\tDeny []string `json:\"deny,omitempty\"`\n\tAlsoAllow []string `json:\"alsoAllow,omitempty\"`\n\tByProvider map[string]*ToolPolicySpec `json:\"byProvider,omitempty\"`\n\tWait *WaitToolPolicy `json:\"wait,omitempty\"`\n\tToolCallPrefix string `json:\"toolCallPrefix,omitempty\"` // prefix to strip from model's tool call names before registry lookup\n}\n\n// WaitToolPolicy configures per-agent safety bounds for the wait tool.\ntype WaitToolPolicy struct {\n\tMinMs int `json:\"min_ms,omitempty\"`\n\tMaxMs int `json:\"max_ms,omitempty\"`\n}\n\n// SessionsConfig controls session behavior.\n// Matching TS src/config/sessions/types.ts + src/config/types.base.ts.\ntype SessionsConfig struct {\n\tScope string `json:\"scope,omitempty\"` // \"per-sender\" (default), \"global\"\n\tDmScope string `json:\"dm_scope,omitempty\"` // \"main\", \"per-peer\", \"per-channel-peer\" (default), \"per-account-channel-peer\"\n\tMainKey string `json:\"main_key,omitempty\"` // main session key suffix (default \"main\", used when dm_scope=\"main\")\n}\n\n// TtsConfig configures text-to-speech.\n// Matching TS src/config/types.tts.ts.\ntype TtsConfig struct {\n\tProvider string `json:\"provider,omitempty\"` // \"openai\", \"elevenlabs\", \"edge\", \"minimax\", \"gemini\"\n\tAuto string `json:\"auto,omitempty\"` // \"off\" (default), \"always\", \"inbound\", \"tagged\"\n\tMode string `json:\"mode,omitempty\"` // \"final\" (default), \"all\"\n\tMaxLength int `json:\"max_length,omitempty\"` // max text length before truncation (default 1500)\n\tTimeoutMs int `json:\"timeout_ms,omitempty\"` // API timeout in ms (default 30000)\n\tOpenAI TtsOpenAIConfig `json:\"openai\"`\n\tElevenLabs TtsElevenLabsConfig `json:\"elevenlabs\"`\n\tEdge TtsEdgeConfig `json:\"edge\"`\n\tMiniMax TtsMiniMaxConfig `json:\"minimax\"`\n\tGemini TtsGeminiConfig `json:\"gemini\"`\n}\n\n// TtsGeminiConfig configures the Google Gemini TTS provider.\ntype TtsGeminiConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"` // required; encrypted at rest\n\tAPIBase string `json:\"api_base,omitempty\"` // custom endpoint (optional; SSRF-gated)\n\tVoice string `json:\"voice,omitempty\"` // default \"Kore\"\n\tModel string `json:\"model,omitempty\"` // default \"gemini-2.5-flash-preview-tts\"\n\tSpeakers string `json:\"speakers,omitempty\"` // JSON-encoded []SpeakerVoice for multi-speaker mode\n}\n\n// TtsOpenAIConfig configures the OpenAI TTS provider.\ntype TtsOpenAIConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"`\n\tAPIBase string `json:\"api_base,omitempty\"` // custom endpoint URL\n\tModel string `json:\"model,omitempty\"` // default \"gpt-4o-mini-tts\"\n\tVoice string `json:\"voice,omitempty\"` // default \"alloy\"\n}\n\n// TtsElevenLabsConfig configures the ElevenLabs TTS provider.\ntype TtsElevenLabsConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"`\n\tBaseURL string `json:\"base_url,omitempty\"`\n\tVoiceID string `json:\"voice_id,omitempty\"` // default \"pMsXgVXv3BLzUgSXRplE\"\n\tModelID string `json:\"model_id,omitempty\"` // default \"eleven_multilingual_v2\"\n}\n\n// TtsEdgeConfig configures the Microsoft Edge TTS provider (free, no API key).\ntype TtsEdgeConfig struct {\n\tEnabled bool `json:\"enabled,omitempty\"`\n\tVoice string `json:\"voice,omitempty\"` // default \"en-US-MichelleNeural\"\n\tRate string `json:\"rate,omitempty\"` // speech rate, e.g. \"+0%\"\n}\n\n// TtsMiniMaxConfig configures the MiniMax TTS provider.\ntype TtsMiniMaxConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"`\n\tGroupID string `json:\"group_id,omitempty\"` // MiniMax GroupId (required)\n\tAPIBase string `json:\"api_base,omitempty\"` // default \"https://api.minimax.io/v1\"\n\tModel string `json:\"model,omitempty\"` // default \"speech-02-hd\"\n\tVoiceID string `json:\"voice_id,omitempty\"` // default \"Wise_Woman\"\n}\n\n// MergeChannelGroupQuotas merges per-group quota overrides from channel configs\n// (e.g., channels.telegram.groups[chatID].quota) into gateway.quota.groups.\n// This allows per-group quotas to be set at the channel level and picked up\n// by the QuotaChecker at runtime.\nfunc MergeChannelGroupQuotas(cfg *Config) {\n\tif cfg.Gateway.Quota == nil {\n\t\treturn\n\t}\n\tif cfg.Gateway.Quota.Groups == nil {\n\t\tcfg.Gateway.Quota.Groups = make(map[string]QuotaWindow)\n\t}\n\n\t// Telegram per-group quotas\n\tfor chatID, groupCfg := range cfg.Channels.Telegram.Groups {\n\t\tif groupCfg != nil && groupCfg.Quota != nil && !groupCfg.Quota.IsZero() {\n\t\t\tkey := \"group:telegram:\" + chatID\n\t\t\tcfg.Gateway.Quota.Groups[key] = *groupCfg.Quota\n\t\t}\n\t}\n}\n"}, "8af0e4ae09f6931b02aa93ef1bfe35c1ed2360bd": {"t": 1785759237.81471, "n": 33829, "h": "6f82c69d9f4e5b1f", "c": "package config\n\n// PendingCompactionConfig configures LLM-based compaction of pending group messages.\n// When a group accumulates more than Threshold pending messages, older messages are\n// summarized by an LLM and replaced with a compact summary, keeping KeepRecent raw messages.\ntype PendingCompactionConfig struct {\n\tThreshold int `json:\"threshold,omitempty\"` // trigger compaction when entries exceed this (default 200)\n\tKeepRecent int `json:\"keep_recent,omitempty\"` // keep this many recent raw messages after compaction (default 40)\n\tMaxTokens int `json:\"max_tokens,omitempty\"` // max output tokens for LLM summarization (default 4096)\n\tProvider string `json:\"provider,omitempty\"` // LLM provider name (e.g. \"openai\"); empty = use agent's provider\n\tModel string `json:\"model,omitempty\"` // model for summarization; empty = use agent's model\n}\n\n// ChannelsConfig contains per-channel configuration.\ntype ChannelsConfig struct {\n\tTelegram TelegramConfig `json:\"telegram\"`\n\tDiscord DiscordConfig `json:\"discord\"`\n\tSlack SlackConfig `json:\"slack\"`\n\tWhatsApp WhatsAppConfig `json:\"whatsapp\"`\n\tZalo ZaloConfig `json:\"zalo\"`\n\tZaloPersonal ZaloPersonalConfig `json:\"zalo_personal\"`\n\tFeishu FeishuConfig `json:\"feishu\"`\n\tPendingCompaction *PendingCompactionConfig `json:\"pending_compaction,omitempty\"` // global pending message compaction settings\n}\n\ntype TelegramConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tToken string `json:\"token\"`\n\tProxy string `json:\"proxy,omitempty\"`\n\tAPIServer string `json:\"api_server,omitempty\"` // custom Telegram Bot API server URL (e.g. \"http://localhost:8081\")\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in groups (default true)\n\tMentionMode string `json:\"mention_mode,omitempty\"` // \"strict\" (default) = only respond when mentioned; \"yield\" = respond unless another bot is mentioned\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tDMStream *bool `json:\"dm_stream,omitempty\"` // enable streaming for DMs (default false) — edits placeholder progressively\n\tGroupStream *bool `json:\"group_stream,omitempty\"` // enable streaming for groups (default false) — sends new message, edits progressively\n\tDraftTransport *bool `json:\"draft_transport,omitempty\"` // use sendMessageDraft for DM streaming (default true) — stealth preview, no notifications per edit\n\tReasoningStream *bool `json:\"reasoning_stream,omitempty\"` // show reasoning as separate message when provider emits thinking events (default true)\n\tReactionLevel string `json:\"reaction_level,omitempty\"` // \"off\" (default), \"minimal\", \"full\" — status emoji reactions\n\tMediaMaxBytes int64 `json:\"media_max_bytes,omitempty\"` // max media download size in bytes (default 20MB)\n\tLinkPreview *bool `json:\"link_preview,omitempty\"` // enable URL previews in messages (default true)\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tForceIPv4 bool `json:\"force_ipv4,omitempty\"` // force IPv4 for all Telegram API requests (use when IPv6 routing is broken)\n\n\t// Optional STT (Speech-to-Text) pipeline for voice/audio inbound messages.\n\t// When stt_proxy_url is set, audio/voice messages are transcribed before being forwarded to the agent.\n\tSTTProxyURL string `json:\"stt_proxy_url,omitempty\"` // base URL of the STT proxy service (e.g. \"https://stt.example.com\")\n\tSTTAPIKey string `json:\"stt_api_key,omitempty\"` // Bearer token for the STT proxy\n\tSTTTenantID string `json:\"stt_tenant_id,omitempty\"` // optional tenant/org identifier forwarded to the STT proxy\n\tSTTTimeoutSeconds int `json:\"stt_timeout_seconds,omitempty\"` // per-request timeout for STT calls (default 30s)\n\n\t// Optional audio-aware routing: when set, voice/audio inbound messages are routed to this\n\t// agent instead of the default channel agent. Requires the named agent to exist in the config.\n\tVoiceAgentID string `json:\"voice_agent_id,omitempty\"` // agent ID to route voice inbound to (e.g. \"speaking-agent\")\n\n\t// Audio guard: intercept technical errors in voice agent replies and replace with friendly fallbacks.\n\t// Only active when VoiceAgentID is set. Custom error markers replace built-in defaults when provided.\n\tAudioGuardFallbackTranscript string `json:\"audio_guard_fallback_transcript,omitempty\"` // fallback with %s for transcript (e.g. \"I heard: \\\"%s\\\". Try again!\")\n\tAudioGuardFallbackNoTranscript string `json:\"audio_guard_fallback_no_transcript,omitempty\"` // fallback when no transcript available\n\tAudioGuardErrorMarkers []string `json:\"audio_guard_error_markers,omitempty\"` // custom error detection markers (replaces defaults)\n\n\t// Per-group (and per-topic) overrides. Key is chat ID string (e.g. \"-100123456\") or \"*\" for wildcard.\n\t// TS ref: channels.telegram.groups in src/config/types.telegram.ts.\n\tGroups map[string]*TelegramGroupConfig `json:\"groups,omitempty\"`\n}\n\n// TelegramGroupConfig defines per-group overrides for a Telegram channel.\n// Matching TS TelegramGroupConfig in src/config/types.telegram.ts.\ntype TelegramGroupConfig struct {\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // override group policy for this group\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // override require_mention for this group\n\tMentionMode string `json:\"mention_mode,omitempty\"` // override mention_mode for this group\n\tAllowFrom FlexibleStringSlice `json:\"allow_from,omitempty\"` // override allow_from for this group\n\tEnabled *bool `json:\"enabled,omitempty\"` // disable bot for this group (default: true)\n\tSkills []string `json:\"skills,omitempty\"` // skill whitelist (nil = all, [] = none)\n\tTools []string `json:\"tools,omitempty\"` // tool allow list (nil = all, supports \"group:xxx\")\n\tSystemPrompt string `json:\"system_prompt,omitempty\"` // extra system prompt for this group\n\tTopics map[string]*TelegramTopicConfig `json:\"topics,omitempty\"` // per-topic overrides (key: thread ID string)\n\tQuota *QuotaWindow `json:\"quota,omitempty\"` // per-group quota override\n}\n\n// TelegramTopicConfig defines per-topic overrides within a Telegram group.\n// Matching TS TelegramTopicConfig in src/config/types.telegram.ts.\ntype TelegramTopicConfig struct {\n\tRequireMention *bool `json:\"require_mention,omitempty\"`\n\tMentionMode string `json:\"mention_mode,omitempty\"`\n\tGroupPolicy string `json:\"group_policy,omitempty\"`\n\tSkills []string `json:\"skills,omitempty\"`\n\tTools []string `json:\"tools,omitempty\"` // tool allow list (nil = inherit, supports \"group:xxx\")\n\tEnabled *bool `json:\"enabled,omitempty\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from,omitempty\"`\n\tSystemPrompt string `json:\"system_prompt,omitempty\"`\n}\n\ntype DiscordConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tToken string `json:\"token\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in groups (default true)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tMediaMaxBytes int64 `json:\"media_max_bytes,omitempty\"` // max media download size (default 25MB)\n\tSTTProxyURL string `json:\"stt_proxy_url,omitempty\"`\n\tSTTAPIKey string `json:\"stt_api_key,omitempty\"`\n\tSTTTenantID string `json:\"stt_tenant_id,omitempty\"`\n\tSTTTimeoutSeconds int `json:\"stt_timeout_seconds,omitempty\"`\n\tVoiceAgentID string `json:\"voice_agent_id,omitempty\"`\n}\n\ntype SlackConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tBotToken string `json:\"bot_token\"` // xoxb-... (Bot User OAuth Token)\n\tAppToken string `json:\"app_token\"` // xapp-... (App-Level Token for Socket Mode)\n\tUserToken string `json:\"user_token,omitempty\"` // xoxp-... (Optional: custom bot identity)\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"pairing\", \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in channels (default true)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tDMStream *bool `json:\"dm_stream,omitempty\"` // enable streaming for DMs (default false)\n\tGroupStream *bool `json:\"group_stream,omitempty\"` // enable streaming for groups (default false)\n\tNativeStream *bool `json:\"native_stream,omitempty\"` // use Slack ChatStreamer API if available (default false)\n\tReactionLevel string `json:\"reaction_level,omitempty\"` // \"off\" (default), \"minimal\", \"full\"\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tDebounceDelay *int `json:\"debounce_delay,omitempty\"` // ms delay before dispatching rapid messages (default 300, 0=disabled)\n\tThreadTTL *int `json:\"thread_ttl,omitempty\"` // hours before thread participation expires (default 24, 0=disabled — always require @mention)\n\tMediaMaxBytes int64 `json:\"media_max_bytes,omitempty\"` // max file download size in bytes (default 20MB)\n}\n\ntype WhatsAppConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tAuthDir string `json:\"auth_dir,omitempty\"` // optional: SQLite auth dir override (desktop)\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default for DB instances), \"open\", \"allowlist\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"pairing\" (default for DB instances), \"open\" (default for config), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // only respond in groups when bot is @mentioned (default false)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 200, 0=disabled)\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n}\n\ntype ZaloConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tToken string `json:\"token\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tWebhookURL string `json:\"webhook_url,omitempty\"`\n\tWebhookSecret string `json:\"webhook_secret,omitempty\"`\n\tMediaMaxMB int `json:\"media_max_mb,omitempty\"` // default 5\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n}\n\ntype ZaloPersonalConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in groups (default true)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tCredentialsPath string `json:\"credentials_path,omitempty\"` // path to saved cookies JSON\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n}\n\ntype FeishuConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tAppID string `json:\"app_id\"`\n\tAppSecret string `json:\"app_secret\"`\n\tEncryptKey string `json:\"encrypt_key,omitempty\"`\n\tVerificationToken string `json:\"verification_token,omitempty\"`\n\tDomain string `json:\"domain,omitempty\"` // \"lark\" (default/global), \"feishu\" (China), or custom URL\n\tConnectionMode string `json:\"connection_mode,omitempty\"` // \"websocket\" (default), \"webhook\"\n\tWebhookPort int `json:\"webhook_port,omitempty\"` // default 3000\n\tWebhookPath string `json:\"webhook_path,omitempty\"` // default \"/feishu/events\"\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default)\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default)\n\tGroupAllowFrom FlexibleStringSlice `json:\"group_allow_from,omitempty\"`\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // default true (groups)\n\tTopicSessionMode string `json:\"topic_session_mode,omitempty\"` // \"disabled\" (default)\n\tTextChunkLimit int `json:\"text_chunk_limit,omitempty\"` // default 4000\n\tMediaMaxMB int `json:\"media_max_mb,omitempty\"` // default 30\n\tRenderMode string `json:\"render_mode,omitempty\"` // \"auto\", \"raw\", \"card\"\n\tStreaming *bool `json:\"streaming,omitempty\"` // default true\n\tReactionLevel string `json:\"reaction_level,omitempty\"` // \"off\" (default), \"minimal\", \"full\" — typing emoji reactions\n\tHistoryLimit int `json:\"history_limit,omitempty\"`\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tSTTProxyURL string `json:\"stt_proxy_url,omitempty\"`\n\tSTTAPIKey string `json:\"stt_api_key,omitempty\"`\n\tSTTTenantID string `json:\"stt_tenant_id,omitempty\"`\n\tSTTTimeoutSeconds int `json:\"stt_timeout_seconds,omitempty\"`\n\tVoiceAgentID string `json:\"voice_agent_id,omitempty\"`\n}\n\n// ProvidersConfig maps provider name to its config.\ntype ProvidersConfig struct {\n\tAnthropic ProviderConfig `json:\"anthropic\"`\n\tOpenAI ProviderConfig `json:\"openai\"`\n\tOpenRouter ProviderConfig `json:\"openrouter\"`\n\tGroq ProviderConfig `json:\"groq\"`\n\tGemini ProviderConfig `json:\"gemini\"`\n\tDeepSeek ProviderConfig `json:\"deepseek\"`\n\tMistral ProviderConfig `json:\"mistral\"`\n\tXAI ProviderConfig `json:\"xai\"`\n\tMiniMax ProviderConfig `json:\"minimax\"`\n\tCohere ProviderConfig `json:\"cohere\"`\n\tPerplexity ProviderConfig `json:\"perplexity\"`\n\tDashScope ProviderConfig `json:\"dashscope\"`\n\tBailian ProviderConfig `json:\"bailian\"`\n\tZai ProviderConfig `json:\"zai\"`\n\tZaiCoding ProviderConfig `json:\"zai_coding\"`\n\tOllama OllamaConfig `json:\"ollama\"` // local Ollama instance (no API key needed)\n\tOllamaCloud ProviderConfig `json:\"ollama_cloud\"` // Ollama Cloud (API key required)\n\tClaudeCLI ClaudeCLIConfig `json:\"claude_cli\"`\n\tACP ACPConfig `json:\"acp\"`\n\tNovita ProviderConfig `json:\"novita\"` // Novita AI (OpenAI-compatible endpoint)\n\tBytePlus ProviderConfig `json:\"byteplus\"` // BytePlus ModelArk (Seed 2.0)\n\tBytePlusCoding ProviderConfig `json:\"byteplus_coding\"` // BytePlus ModelArk Coding Plan\n\tVertex VertexConfig `json:\"vertex\"` // Google Cloud Vertex AI (OAuth2 service account + ADC)\n}\n\n// VertexConfig configures Google Cloud Vertex AI.\n// Credentials precedence: APIKey (inline JSON) > CredentialsFile (path) > ADC (both empty).\n// ProjectID and Region are required; Model optional (defaults to google/gemini-2.0-flash-001).\ntype VertexConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"` // service account JSON inline (secret — never persist in config.json)\n\tCredentialsFile string `json:\"credentials_file,omitempty\"` // path to service account JSON file\n\tProjectID string `json:\"project_id,omitempty\"`\n\tRegion string `json:\"region,omitempty\"`\n\tModel string `json:\"model,omitempty\"`\n}\n\n// OllamaConfig configures a local (or self-hosted) Ollama instance.\n// No API key is required — Ollama accepts any Bearer token value.\ntype OllamaConfig struct {\n\tHost string `json:\"host\"` // Ollama server base URL, e.g. http://localhost:11434\n}\n\n// ClaudeCLIConfig configures the Claude CLI provider (uses subscription, not API key).\ntype ClaudeCLIConfig struct {\n\tCLIPath string `json:\"cli_path\" yaml:\"cli_path\"` // path to claude binary (default: \"claude\")\n\tModel string `json:\"model\" yaml:\"model\"` // default model alias (default: \"sonnet\")\n\tBaseWorkDir string `json:\"base_work_dir\" yaml:\"base_work_dir\"` // base dir for agent workspaces\n\tPermMode string `json:\"perm_mode\" yaml:\"perm_mode\"` // permission mode (default: \"bypassPermissions\")\n}\n\n// ACPConfig configures the ACP (Agent Client Protocol) provider.\n// Orchestrates any ACP-compatible coding agent (Claude Code, Codex CLI, Gemini CLI) as a subprocess.\ntype ACPConfig struct {\n\tBinary string `json:\"binary\"` // agent binary name or path (e.g. \"claude\", \"codex\")\n\tArgs []string `json:\"args\"` // extra spawn args\n\tModel string `json:\"model\"` // default model/agent name\n\tWorkDir string `json:\"work_dir\"` // base workspace dir\n\tIdleTTL string `json:\"idle_ttl\"` // process idle TTL (e.g. \"5m\")\n\tPermMode string `json:\"perm_mode\"` // \"approve-all\" (default), \"approve-reads\", \"deny-all\"\n}\n\ntype ProviderConfig struct {\n\tAPIKey string `json:\"api_key\"`\n\tAPIBase string `json:\"api_base,omitempty\"`\n}\n\n// APIBaseForType returns the config-level api_base for a given provider type.\n// Used as a fallback when DB providers have no api_base set.\nfunc (p *ProvidersConfig) APIBaseForType(providerType string) string {\n\tswitch providerType {\n\tcase \"anthropic_native\":\n\t\treturn p.Anthropic.APIBase\n\tcase \"openai\", \"openai_compat\":\n\t\treturn p.OpenAI.APIBase\n\tcase \"openrouter\":\n\t\treturn p.OpenRouter.APIBase\n\tcase \"groq\":\n\t\treturn p.Groq.APIBase\n\tcase \"deepseek\":\n\t\treturn p.DeepSeek.APIBase\n\tcase \"gemini_native\":\n\t\treturn p.Gemini.APIBase\n\tcase \"mistral\":\n\t\treturn p.Mistral.APIBase\n\tcase \"xai\":\n\t\treturn p.XAI.APIBase\n\tcase \"minimax_native\":\n\t\treturn p.MiniMax.APIBase\n\tcase \"cohere\":\n\t\treturn p.Cohere.APIBase\n\tcase \"perplexity\":\n\t\treturn p.Perplexity.APIBase\n\tcase \"dashscope\":\n\t\treturn p.DashScope.APIBase\n\tcase \"bailian\":\n\t\treturn p.Bailian.APIBase\n\tcase \"zai\":\n\t\treturn p.Zai.APIBase\n\tcase \"zai_coding\":\n\t\treturn p.ZaiCoding.APIBase\n\tcase \"ollama_cloud\":\n\t\treturn p.OllamaCloud.APIBase\n\tcase \"novita\":\n\t\treturn p.Novita.APIBase\n\tcase \"byteplus\":\n\t\treturn p.BytePlus.APIBase\n\tcase \"byteplus_coding\":\n\t\treturn p.BytePlusCoding.APIBase\n\tcase \"vertex\":\n\t\t// Computed from project+region at registration time; no config-level static base.\n\t\treturn \"\"\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\n// HasAnyProvider returns true if at least one provider has an API key or CLI configured.\nfunc (c *Config) HasAnyProvider() bool {\n\tp := c.Providers\n\treturn p.Anthropic.APIKey != \"\" ||\n\t\tp.OpenAI.APIKey != \"\" ||\n\t\tp.OpenRouter.APIKey != \"\" ||\n\t\tp.Groq.APIKey != \"\" ||\n\t\tp.Gemini.APIKey != \"\" ||\n\t\tp.DeepSeek.APIKey != \"\" ||\n\t\tp.Mistral.APIKey != \"\" ||\n\t\tp.XAI.APIKey != \"\" ||\n\t\tp.MiniMax.APIKey != \"\" ||\n\t\tp.Cohere.APIKey != \"\" ||\n\t\tp.Perplexity.APIKey != \"\" ||\n\t\tp.DashScope.APIKey != \"\" ||\n\t\tp.Bailian.APIKey != \"\" ||\n\t\tp.Zai.APIKey != \"\" ||\n\t\tp.ZaiCoding.APIKey != \"\" ||\n\t\tp.Ollama.Host != \"\" ||\n\t\tp.OllamaCloud.APIKey != \"\" ||\n\t\tp.ClaudeCLI.CLIPath != \"\" ||\n\t\tp.ACP.Binary != \"\" ||\n\t\tp.Novita.APIKey != \"\" ||\n\t\tp.BytePlus.APIKey != \"\" ||\n\t\tp.BytePlusCoding.APIKey != \"\" ||\n\t\t(p.Vertex.ProjectID != \"\" && p.Vertex.Region != \"\")\n}\n\n// QuotaWindow defines request limits per time window. Zero means unlimited.\ntype QuotaWindow struct {\n\tHour int `json:\"hour,omitempty\"` // max requests per hour (0 = unlimited)\n\tDay int `json:\"day,omitempty\"` // max requests per day (0 = unlimited)\n\tWeek int `json:\"week,omitempty\"` // max requests per week (0 = unlimited)\n}\n\n// IsZero returns true if no limits are set.\nfunc (w QuotaWindow) IsZero() bool { return w.Hour == 0 && w.Day == 0 && w.Week == 0 }\n\n// QuotaConfig configures per-user/group request quotas.\n// Config merge priority: Groups > Channels > Providers > Default.\ntype QuotaConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tDefault QuotaWindow `json:\"default\"`\n\tProviders map[string]QuotaWindow `json:\"providers,omitempty\"` // key = provider name (e.g. \"anthropic\")\n\tChannels map[string]QuotaWindow `json:\"channels,omitempty\"` // key = channel name (e.g. \"telegram\")\n\tGroups map[string]QuotaWindow `json:\"groups,omitempty\"` // key = userID (e.g. \"group:telegram:-100123\")\n}\n\n// GatewayConfig controls the gateway server.\ntype GatewayConfig struct {\n\tHost string `json:\"host\"`\n\tPort int `json:\"port\"`\n\tToken string `json:\"token,omitempty\"` // bearer token for WS/HTTP auth\n\tOwnerIDs []string `json:\"owner_ids,omitempty\"` // sender IDs considered \"owner\"\n\tAllowedOrigins []string `json:\"allowed_origins,omitempty\"` // WebSocket CORS whitelist (empty = allow all)\n\tMaxMessageChars int `json:\"max_message_chars,omitempty\"` // max user message characters (default 32000)\n\tRateLimitRPM int `json:\"rate_limit_rpm,omitempty\"` // rate limit: requests per minute per user (default 20, 0 = disabled)\n\tInjectionAction string `json:\"injection_action,omitempty\"` // prompt injection action: \"log\", \"warn\" (default), \"block\", \"off\"\n\tInboundDebounceMs int `json:\"inbound_debounce_ms,omitempty\"` // merge rapid channel/Web Chat messages from same sender/session (default 1000ms, -1 = disabled)\n\tQuota *QuotaConfig `json:\"quota,omitempty\"` // per-user/group request quotas\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // deliver intermediate text during tool iterations (default false)\n\tToolStatus *bool `json:\"tool_status,omitempty\"` // show tool name in streaming preview during tool execution (default true)\n\tTaskRecoveryIntervalSec int `json:\"task_recovery_interval_sec,omitempty\"` // team task recovery ticker interval in seconds (default 300 = 5min)\n\tBackgroundProvider string `json:\"background_provider,omitempty\"` // LLM provider for background workers (vault enrichment, consolidation)\n\tBackgroundModel string `json:\"background_model,omitempty\"` // LLM model for background workers\n}\n\n// ToolsConfig controls tool availability, policy, and web search.\ntype ToolsConfig struct {\n\tProfile string `json:\"profile,omitempty\"` // global profile: \"minimal\", \"coding\", \"messaging\", \"full\"\n\tAllow []string `json:\"allow,omitempty\"` // global allow list (tool names or \"group:xxx\")\n\tDeny []string `json:\"deny,omitempty\"` // global deny list\n\tAlsoAllow []string `json:\"alsoAllow,omitempty\"` // additive: adds without removing existing\n\tByProvider map[string]*ToolPolicySpec `json:\"byProvider,omitempty\"` // per-provider overrides\n\tShellDenyGroups map[string]bool `json:\"shellDenyGroups,omitempty\"` // global shell deny-group toggles (group name -> denied); per-agent overrides win per-key\n\tExecApproval ExecApprovalCfg `json:\"execApproval\"` // exec command approval settings\n\tWebFetch WebFetchPolicyConfig `json:\"web_fetch\"` // domain policy for URL fetching\n\tBrowser BrowserToolConfig `json:\"browser\"`\n\tRateLimitPerHour int `json:\"rate_limit_per_hour,omitempty\"` // max tool executions per hour per session (0 = disabled)\n\tScrubCredentials *bool `json:\"scrub_credentials,omitempty\"` // auto-redact API keys/tokens in tool output (default true)\n\tMcpServers map[string]*MCPServerConfig `json:\"mcp_servers,omitempty\"` // external MCP server connections\n}\n\n// MCPServerConfig configures a single external MCP server connection.\ntype MCPServerConfig struct {\n\tTransport string `json:\"transport\"` // \"stdio\", \"sse\", \"streamable-http\"\n\tCommand string `json:\"command,omitempty\"` // stdio: command to spawn\n\tArgs []string `json:\"args,omitempty\"` // stdio: command arguments\n\tEnv map[string]string `json:\"env,omitempty\"` // stdio: extra environment variables\n\tURL string `json:\"url,omitempty\"` // sse/http: server URL\n\tHeaders map[string]string `json:\"headers,omitempty\"` // sse/http: extra HTTP headers\n\tEnabled *bool `json:\"enabled,omitempty\"` // default true\n\tToolPrefix string `json:\"tool_prefix,omitempty\"` // prefix for tool names (avoids collisions)\n\tTimeoutSec int `json:\"timeout_sec,omitempty\"` // per-tool-call timeout in seconds (default 60)\n}\n\n// IsEnabled returns whether this MCP server is enabled (default true).\nfunc (c *MCPServerConfig) IsEnabled() bool {\n\treturn c.Enabled == nil || *c.Enabled\n}\n\n// ExecApprovalCfg configures command execution approval (matching TS exec-approval.ts).\ntype ExecApprovalCfg struct {\n\tSecurity string `json:\"security,omitempty\"` // \"deny\", \"allowlist\", \"full\" (default \"full\")\n\tAsk string `json:\"ask,omitempty\"` // \"off\", \"on-miss\", \"always\" (default \"off\")\n\tAllowlist []string `json:\"allowlist,omitempty\"` // glob patterns for allowed commands\n}\n\n// WebFetchPolicyConfig controls domain filtering for the web_fetch tool.\ntype WebFetchPolicyConfig struct {\n\tPolicy string `json:\"policy,omitempty\"` // \"allow_all\" (default), \"allowlist\"\n\tAllowedDomains []string `json:\"allowed_domains,omitempty\"` // e.g. [\"github.com\", \"*.example.com\"]\n\tBlockedDomains []string `json:\"blocked_domains,omitempty\"` // always checked regardless of policy\n}\n\n// BrowserToolConfig controls the browser automation tool.\ntype BrowserToolConfig struct {\n\tEnabled bool `json:\"enabled\"` // enable the browser tool (default false)\n\tHeadless bool `json:\"headless,omitempty\"` // run Chrome in headless mode (ignored when RemoteURL is set)\n\tRemoteURL string `json:\"remote_url,omitempty\"` // CDP endpoint for remote Chrome sidecar, e.g. \"ws://chrome:9222\"\n\tActionTimeoutMs int `json:\"action_timeout_ms,omitempty\"` // per-action timeout in ms (default 30000)\n\tIdleTimeoutMs int `json:\"idle_timeout_ms,omitempty\"` // idle page auto-close in ms (default 600000, 0=disabled)\n\tMaxPages int `json:\"max_pages,omitempty\"` // max open pages per tenant (default 5)\n}\n\n// ToolPolicySpec defines a tool policy at any level (global, per-agent, per-provider).\ntype ToolPolicySpec struct {\n\tProfile string `json:\"profile,omitempty\"`\n\tAllow []string `json:\"allow,omitempty\"`\n\tDeny []string `json:\"deny,omitempty\"`\n\tAlsoAllow []string `json:\"alsoAllow,omitempty\"`\n\tByProvider map[string]*ToolPolicySpec `json:\"byProvider,omitempty\"`\n\tWait *WaitToolPolicy `json:\"wait,omitempty\"`\n\tToolCallPrefix string `json:\"toolCallPrefix,omitempty\"` // prefix to strip from model's tool call names before registry lookup\n}\n\n// WaitToolPolicy configures per-agent safety bounds for the wait tool.\ntype WaitToolPolicy struct {\n\tMinMs int `json:\"min_ms,omitempty\"`\n\tMaxMs int `json:\"max_ms,omitempty\"`\n}\n\n// SessionsConfig controls session behavior.\n// Matching TS src/config/sessions/types.ts + src/config/types.base.ts.\ntype SessionsConfig struct {\n\tScope string `json:\"scope,omitempty\"` // \"per-sender\" (default), \"global\"\n\tDmScope string `json:\"dm_scope,omitempty\"` // \"main\", \"per-peer\", \"per-channel-peer\" (default), \"per-account-channel-peer\"\n\tMainKey string `json:\"main_key,omitempty\"` // main session key suffix (default \"main\", used when dm_scope=\"main\")\n}\n\n// TtsConfig configures text-to-speech.\n// Matching TS src/config/types.tts.ts.\ntype TtsConfig struct {\n\tProvider string `json:\"provider,omitempty\"` // \"openai\", \"elevenlabs\", \"edge\", \"minimax\", \"gemini\"\n\tAuto string `json:\"auto,omitempty\"` // \"off\" (default), \"always\", \"inbound\", \"tagged\"\n\tMode string `json:\"mode,omitempty\"` // \"final\" (default), \"all\"\n\tMaxLength int `json:\"max_length,omitempty\"` // max text length before truncation (default 1500)\n\tTimeoutMs int `json:\"timeout_ms,omitempty\"` // API timeout in ms (default 30000)\n\tOpenAI TtsOpenAIConfig `json:\"openai\"`\n\tElevenLabs TtsElevenLabsConfig `json:\"elevenlabs\"`\n\tEdge TtsEdgeConfig `json:\"edge\"`\n\tMiniMax TtsMiniMaxConfig `json:\"minimax\"`\n\tGemini TtsGeminiConfig `json:\"gemini\"`\n}\n\n// TtsGeminiConfig configures the Google Gemini TTS provider.\ntype TtsGeminiConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"` // required; encrypted at rest\n\tAPIBase string `json:\"api_base,omitempty\"` // custom endpoint (optional; SSRF-gated)\n\tVoice string `json:\"voice,omitempty\"` // default \"Kore\"\n\tModel string `json:\"model,omitempty\"` // default \"gemini-2.5-flash-preview-tts\"\n\tSpeakers string `json:\"speakers,omitempty\"` // JSON-encoded []SpeakerVoice for multi-speaker mode\n}\n\n// TtsOpenAIConfig configures the OpenAI TTS provider.\ntype TtsOpenAIConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"`\n\tAPIBase string `json:\"api_base,omitempty\"` // custom endpoint URL\n\tModel string `json:\"model,omitempty\"` // default \"gpt-4o-mini-tts\"\n\tVoice string `json:\"voice,omitempty\"` // default \"alloy\"\n}\n\n// TtsElevenLabsConfig configures the ElevenLabs TTS provider.\ntype TtsElevenLabsConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"`\n\tBaseURL string `json:\"base_url,omitempty\"`\n\tVoiceID string `json:\"voice_id,omitempty\"` // default \"pMsXgVXv3BLzUgSXRplE\"\n\tModelID string `json:\"model_id,omitempty\"` // default \"eleven_multilingual_v2\"\n}\n\n// TtsEdgeConfig configures the Microsoft Edge TTS provider (free, no API key).\ntype TtsEdgeConfig struct {\n\tEnabled bool `json:\"enabled,omitempty\"`\n\tVoice string `json:\"voice,omitempty\"` // default \"en-US-MichelleNeural\"\n\tRate string `json:\"rate,omitempty\"` // speech rate, e.g. \"+0%\"\n}\n\n// TtsMiniMaxConfig configures the MiniMax TTS provider.\ntype TtsMiniMaxConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"`\n\tGroupID string `json:\"group_id,omitempty\"` // MiniMax GroupId (required)\n\tAPIBase string `json:\"api_base,omitempty\"` // default \"https://api.minimax.io/v1\"\n\tModel string `json:\"model,omitempty\"` // default \"speech-02-hd\"\n\tVoiceID string `json:\"voice_id,omitempty\"` // default \"Wise_Woman\"\n}\n\n// MergeChannelGroupQuotas merges per-group quota overrides from channel configs\n// (e.g., channels.telegram.groups[chatID].quota) into gateway.quota.groups.\n// This allows per-group quotas to be set at the channel level and picked up\n// by the QuotaChecker at runtime.\nfunc MergeChannelGroupQuotas(cfg *Config) {\n\tif cfg.Gateway.Quota == nil {\n\t\treturn\n\t}\n\tif cfg.Gateway.Quota.Groups == nil {\n\t\tcfg.Gateway.Quota.Groups = make(map[string]QuotaWindow)\n\t}\n\n\t// Telegram per-group quotas\n\tfor chatID, groupCfg := range cfg.Channels.Telegram.Groups {\n\t\tif groupCfg != nil && groupCfg.Quota != nil && !groupCfg.Quota.IsZero() {\n\t\t\tkey := \"group:telegram:\" + chatID\n\t\t\tcfg.Gateway.Quota.Groups[key] = *groupCfg.Quota\n\t\t}\n\t}\n}\n"}, "703c0b2df07f70ae3d7593635fc8268ae331cf45": {"t": 1785759239.2882638, "n": 33804, "h": "32c0a0e79d25137e", "c": "package config\n\n// PendingCompactionConfig configures LLM-based compaction of pending group messages.\n// When a group accumulates more than Threshold pending messages, older messages are\n// summarized by an LLM and replaced with a compact summary, keeping KeepRecent raw messages.\ntype PendingCompactionConfig struct {\n\tThreshold int `json:\"threshold,omitempty\"` // trigger compaction when entries exceed this (default 200)\n\tKeepRecent int `json:\"keep_recent,omitempty\"` // keep this many recent raw messages after compaction (default 40)\n\tMaxTokens int `json:\"max_tokens,omitempty\"` // max output tokens for LLM summarization (default 4096)\n\tProvider string `json:\"provider,omitempty\"` // LLM provider name (e.g. \"openai\"); empty = use agent's provider\n\tModel string `json:\"model,omitempty\"` // model for summarization; empty = use agent's model\n}\n\n// ChannelsConfig contains per-channel configuration.\ntype ChannelsConfig struct {\n\tTelegram TelegramConfig `json:\"telegram\"`\n\tDiscord DiscordConfig `json:\"discord\"`\n\tSlack SlackConfig `json:\"slack\"`\n\tWhatsApp WhatsAppConfig `json:\"whatsapp\"`\n\tZalo ZaloConfig `json:\"zalo\"`\n\tZaloPersonal ZaloPersonalConfig `json:\"zalo_personal\"`\n\tFeishu FeishuConfig `json:\"feishu\"`\n\tPendingCompaction *PendingCompactionConfig `json:\"pending_compaction,omitempty\"` // global pending message compaction settings\n}\n\ntype TelegramConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tToken string `json:\"token\"`\n\tProxy string `json:\"proxy,omitempty\"`\n\tAPIServer string `json:\"api_server,omitempty\"` // custom Telegram Bot API server URL (e.g. \"http://localhost:8081\")\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in groups (default true)\n\tMentionMode string `json:\"mention_mode,omitempty\"` // \"strict\" (default) = only respond when mentioned; \"yield\" = respond unless another bot is mentioned\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tDMStream *bool `json:\"dm_stream,omitempty\"` // enable streaming for DMs (default false) — edits placeholder progressively\n\tGroupStream *bool `json:\"group_stream,omitempty\"` // enable streaming for groups (default false) — sends new message, edits progressively\n\tDraftTransport *bool `json:\"draft_transport,omitempty\"` // use sendMessageDraft for DM streaming (default true) — stealth preview, no notifications per edit\n\tReasoningStream *bool `json:\"reasoning_stream,omitempty\"` // show reasoning as separate message when provider emits thinking events (default true)\n\tReactionLevel string `json:\"reaction_level,omitempty\"` // \"off\" (default), \"minimal\", \"full\" — status emoji reactions\n\tMediaMaxBytes int64 `json:\"media_max_bytes,omitempty\"` // max media download size in bytes (default 20MB)\n\tLinkPreview *bool `json:\"link_preview,omitempty\"` // enable URL previews in messages (default true)\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tForceIPv4 bool `json:\"force_ipv4,omitempty\"` // force IPv4 for all Telegram API requests (use when IPv6 routing is broken)\n\n\t// Optional STT (Speech-to-Text) pipeline for voice/audio inbound messages.\n\t// When stt_proxy_url is set, audio/voice messages are transcribed before being forwarded to the agent.\n\tSTTProxyURL string `json:\"stt_proxy_url,omitempty\"` // base URL of the STT proxy service (e.g. \"https://stt.example.com\")\n\tSTTAPIKey string `json:\"stt_api_key,omitempty\"` // Bearer token for the STT proxy\n\tSTTTenantID string `json:\"stt_tenant_id,omitempty\"` // optional tenant/org identifier forwarded to the STT proxy\n\tSTTTimeoutSeconds int `json:\"stt_timeout_seconds,omitempty\"` // per-request timeout for STT calls (default 30s)\n\n\t// Optional audio-aware routing: when set, voice/audio inbound messages are routed to this\n\t// agent instead of the default channel agent. Requires the named agent to exist in the config.\n\tVoiceAgentID string `json:\"voice_agent_id,omitempty\"` // agent ID to route voice inbound to (e.g. \"speaking-agent\")\n\n\t// Audio guard: intercept technical errors in voice agent replies and replace with friendly fallbacks.\n\t// Only active when VoiceAgentID is set. Custom error markers replace built-in defaults when provided.\n\tAudioGuardFallbackTranscript string `json:\"audio_guard_fallback_transcript,omitempty\"` // fallback with %s for transcript (e.g. \"I heard: \\\"%s\\\". Try again!\")\n\tAudioGuardFallbackNoTranscript string `json:\"audio_guard_fallback_no_transcript,omitempty\"` // fallback when no transcript available\n\tAudioGuardErrorMarkers []string `json:\"audio_guard_error_markers,omitempty\"` // custom error detection markers (replaces defaults)\n\n\t// Per-group (and per-topic) overrides. Key is chat ID string (e.g. \"-100123456\") or \"*\" for wildcard.\n\t// TS ref: channels.telegram.groups in src/config/types.telegram.ts.\n\tGroups map[string]*TelegramGroupConfig `json:\"groups,omitempty\"`\n}\n\n// TelegramGroupConfig defines per-group overrides for a Telegram channel.\n// Matching TS TelegramGroupConfig in src/config/types.telegram.ts.\ntype TelegramGroupConfig struct {\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // override group policy for this group\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // override require_mention for this group\n\tMentionMode string `json:\"mention_mode,omitempty\"` // override mention_mode for this group\n\tAllowFrom FlexibleStringSlice `json:\"allow_from,omitempty\"` // override allow_from for this group\n\tEnabled *bool `json:\"enabled,omitempty\"` // disable bot for this group (default: true)\n\tSkills []string `json:\"skills,omitempty\"` // skill whitelist (nil = all, [] = none)\n\tTools []string `json:\"tools,omitempty\"` // tool allow list (nil = all, supports \"group:xxx\")\n\tSystemPrompt string `json:\"system_prompt,omitempty\"` // extra system prompt for this group\n\tTopics map[string]*TelegramTopicConfig `json:\"topics,omitempty\"` // per-topic overrides (key: thread ID string)\n\tQuota *QuotaWindow `json:\"quota,omitempty\"` // per-group quota override\n}\n\n// TelegramTopicConfig defines per-topic overrides within a Telegram group.\n// Matching TS TelegramTopicConfig in src/config/types.telegram.ts.\ntype TelegramTopicConfig struct {\n\tRequireMention *bool `json:\"require_mention,omitempty\"`\n\tMentionMode string `json:\"mention_mode,omitempty\"`\n\tGroupPolicy string `json:\"group_policy,omitempty\"`\n\tSkills []string `json:\"skills,omitempty\"`\n\tTools []string `json:\"tools,omitempty\"` // tool allow list (nil = inherit, supports \"group:xxx\")\n\tEnabled *bool `json:\"enabled,omitempty\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from,omitempty\"`\n\tSystemPrompt string `json:\"system_prompt,omitempty\"`\n}\n\ntype DiscordConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tToken string `json:\"token\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in groups (default true)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tMediaMaxBytes int64 `json:\"media_max_bytes,omitempty\"` // max media download size (default 25MB)\n\tSTTProxyURL string `json:\"stt_proxy_url,omitempty\"`\n\tSTTAPIKey string `json:\"stt_api_key,omitempty\"`\n\tSTTTenantID string `json:\"stt_tenant_id,omitempty\"`\n\tSTTTimeoutSeconds int `json:\"stt_timeout_seconds,omitempty\"`\n\tVoiceAgentID string `json:\"voice_agent_id,omitempty\"`\n}\n\ntype SlackConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tBotToken string `json:\"bot_token\"` // xoxb-... (Bot User OAuth Token)\n\tAppToken string `json:\"app_token\"` // xapp-... (App-Level Token for Socket Mode)\n\tUserToken string `json:\"user_token,omitempty\"` // xoxp-... (Optional: custom bot identity)\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"pairing\", \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in channels (default true)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tDMStream *bool `json:\"dm_stream,omitempty\"` // enable streaming for DMs (default false)\n\tGroupStream *bool `json:\"group_stream,omitempty\"` // enable streaming for groups (default false)\n\tNativeStream *bool `json:\"native_stream,omitempty\"` // use Slack ChatStreamer API if available (default false)\n\tReactionLevel string `json:\"reaction_level,omitempty\"` // \"off\" (default), \"minimal\", \"full\"\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tDebounceDelay int `json:\"debounce_delay,omitempty\"` // ms delay before dispatching rapid messages (default 300, 0=disabled)\n\tThreadTTL *int `json:\"thread_ttl,omitempty\"` // hours before thread participation expires (default 24, 0=disabled — always require @mention)\n\tMediaMaxBytes int64 `json:\"media_max_bytes,omitempty\"` // max file download size in bytes (default 20MB)\n}\n\ntype WhatsAppConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tAuthDir string `json:\"auth_dir,omitempty\"` // optional: SQLite auth dir override (desktop)\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default for DB instances), \"open\", \"allowlist\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"pairing\" (default for DB instances), \"open\" (default for config), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // only respond in groups when bot is @mentioned (default false)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 200, 0=disabled)\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n}\n\ntype ZaloConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tToken string `json:\"token\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tWebhookURL string `json:\"webhook_url,omitempty\"`\n\tWebhookSecret string `json:\"webhook_secret,omitempty\"`\n\tMediaMaxMB int `json:\"media_max_mb,omitempty\"` // default 5\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n}\n\ntype ZaloPersonalConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in groups (default true)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tCredentialsPath string `json:\"credentials_path,omitempty\"` // path to saved cookies JSON\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n}\n\ntype FeishuConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tAppID string `json:\"app_id\"`\n\tAppSecret string `json:\"app_secret\"`\n\tEncryptKey string `json:\"encrypt_key,omitempty\"`\n\tVerificationToken string `json:\"verification_token,omitempty\"`\n\tDomain string `json:\"domain,omitempty\"` // \"lark\" (default/global), \"feishu\" (China), or custom URL\n\tConnectionMode string `json:\"connection_mode,omitempty\"` // \"websocket\" (default), \"webhook\"\n\tWebhookPort int `json:\"webhook_port,omitempty\"` // default 3000\n\tWebhookPath string `json:\"webhook_path,omitempty\"` // default \"/feishu/events\"\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default)\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default)\n\tGroupAllowFrom FlexibleStringSlice `json:\"group_allow_from,omitempty\"`\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // default true (groups)\n\tTopicSessionMode string `json:\"topic_session_mode,omitempty\"` // \"disabled\" (default)\n\tTextChunkLimit int `json:\"text_chunk_limit,omitempty\"` // default 4000\n\tMediaMaxMB int `json:\"media_max_mb,omitempty\"` // default 30\n\tRenderMode string `json:\"render_mode,omitempty\"` // \"auto\", \"raw\", \"card\"\n\tStreaming *bool `json:\"streaming,omitempty\"` // default true\n\tReactionLevel string `json:\"reaction_level,omitempty\"` // \"off\" (default), \"minimal\", \"full\" — typing emoji reactions\n\tHistoryLimit int `json:\"history_limit,omitempty\"`\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tSTTProxyURL string `json:\"stt_proxy_url,omitempty\"`\n\tSTTAPIKey string `json:\"stt_api_key,omitempty\"`\n\tSTTTenantID string `json:\"stt_tenant_id,omitempty\"`\n\tSTTTimeoutSeconds int `json:\"stt_timeout_seconds,omitempty\"`\n\tVoiceAgentID string `json:\"voice_agent_id,omitempty\"`\n}\n\n// ProvidersConfig maps provider name to its config.\ntype ProvidersConfig struct {\n\tAnthropic ProviderConfig `json:\"anthropic\"`\n\tOpenAI ProviderConfig `json:\"openai\"`\n\tOpenRouter ProviderConfig `json:\"openrouter\"`\n\tGroq ProviderConfig `json:\"groq\"`\n\tGemini ProviderConfig `json:\"gemini\"`\n\tDeepSeek ProviderConfig `json:\"deepseek\"`\n\tMistral ProviderConfig `json:\"mistral\"`\n\tXAI ProviderConfig `json:\"xai\"`\n\tMiniMax ProviderConfig `json:\"minimax\"`\n\tCohere ProviderConfig `json:\"cohere\"`\n\tPerplexity ProviderConfig `json:\"perplexity\"`\n\tDashScope ProviderConfig `json:\"dashscope\"`\n\tBailian ProviderConfig `json:\"bailian\"`\n\tZai ProviderConfig `json:\"zai\"`\n\tZaiCoding ProviderConfig `json:\"zai_coding\"`\n\tOllama OllamaConfig `json:\"ollama\"` // local Ollama instance (no API key needed)\n\tOllamaCloud ProviderConfig `json:\"ollama_cloud\"` // Ollama Cloud (API key required)\n\tClaudeCLI ClaudeCLIConfig `json:\"claude_cli\"`\n\tACP ACPConfig `json:\"acp\"`\n\tNovita ProviderConfig `json:\"novita\"` // Novita AI (OpenAI-compatible endpoint)\n\tBytePlus ProviderConfig `json:\"byteplus\"` // BytePlus ModelArk (Seed 2.0)\n\tBytePlusCoding ProviderConfig `json:\"byteplus_coding\"` // BytePlus ModelArk Coding Plan\n\tVertex VertexConfig `json:\"vertex\"` // Google Cloud Vertex AI (OAuth2 service account + ADC)\n}\n\n// VertexConfig configures Google Cloud Vertex AI.\n// Credentials precedence: APIKey (inline JSON) > CredentialsFile (path) > ADC (both empty).\n// ProjectID and Region are required; Model optional (defaults to google/gemini-2.0-flash-001).\ntype VertexConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"` // service account JSON inline (secret — never persist in config.json)\n\tCredentialsFile string `json:\"credentials_file,omitempty\"` // path to service account JSON file\n\tProjectID string `json:\"project_id,omitempty\"`\n\tRegion string `json:\"region,omitempty\"`\n\tModel string `json:\"model,omitempty\"`\n}\n\n// OllamaConfig configures a local (or self-hosted) Ollama instance.\n// No API key is required — Ollama accepts any Bearer token value.\ntype OllamaConfig struct {\n\tHost string `json:\"host\"` // Ollama server base URL, e.g. http://localhost:11434\n}\n\n// ClaudeCLIConfig configures the Claude CLI provider (uses subscription, not API key).\ntype ClaudeCLIConfig struct {\n\tCLIPath string `json:\"cli_path\" yaml:\"cli_path\"` // path to claude binary (default: \"claude\")\n\tModel string `json:\"model\" yaml:\"model\"` // default model alias (default: \"sonnet\")\n\tBaseWorkDir string `json:\"base_work_dir\" yaml:\"base_work_dir\"` // base dir for agent workspaces\n\tPermMode string `json:\"perm_mode\" yaml:\"perm_mode\"` // permission mode (default: \"bypassPermissions\")\n}\n\n// ACPConfig configures the ACP (Agent Client Protocol) provider.\n// Orchestrates any ACP-compatible coding agent (Claude Code, Codex CLI, Gemini CLI) as a subprocess.\ntype ACPConfig struct {\n\tBinary string `json:\"binary\"` // agent binary name or path (e.g. \"claude\", \"codex\")\n\tArgs []string `json:\"args\"` // extra spawn args\n\tModel string `json:\"model\"` // default model/agent name\n\tWorkDir string `json:\"work_dir\"` // base workspace dir\n\tIdleTTL string `json:\"idle_ttl\"` // process idle TTL (e.g. \"5m\")\n\tPermMode string `json:\"perm_mode\"` // \"approve-all\" (default), \"approve-reads\", \"deny-all\"\n}\n\ntype ProviderConfig struct {\n\tAPIKey string `json:\"api_key\"`\n\tAPIBase string `json:\"api_base,omitempty\"`\n}\n\n// APIBaseForType returns the config-level api_base for a given provider type.\n// Used as a fallback when DB providers have no api_base set.\nfunc (p *ProvidersConfig) APIBaseForType(providerType string) string {\n\tswitch providerType {\n\tcase \"anthropic_native\":\n\t\treturn p.Anthropic.APIBase\n\tcase \"openai\", \"openai_compat\":\n\t\treturn p.OpenAI.APIBase\n\tcase \"openrouter\":\n\t\treturn p.OpenRouter.APIBase\n\tcase \"groq\":\n\t\treturn p.Groq.APIBase\n\tcase \"deepseek\":\n\t\treturn p.DeepSeek.APIBase\n\tcase \"gemini_native\":\n\t\treturn p.Gemini.APIBase\n\tcase \"mistral\":\n\t\treturn p.Mistral.APIBase\n\tcase \"xai\":\n\t\treturn p.XAI.APIBase\n\tcase \"minimax_native\":\n\t\treturn p.MiniMax.APIBase\n\tcase \"cohere\":\n\t\treturn p.Cohere.APIBase\n\tcase \"perplexity\":\n\t\treturn p.Perplexity.APIBase\n\tcase \"dashscope\":\n\t\treturn p.DashScope.APIBase\n\tcase \"bailian\":\n\t\treturn p.Bailian.APIBase\n\tcase \"zai\":\n\t\treturn p.Zai.APIBase\n\tcase \"zai_coding\":\n\t\treturn p.ZaiCoding.APIBase\n\tcase \"ollama_cloud\":\n\t\treturn p.OllamaCloud.APIBase\n\tcase \"novita\":\n\t\treturn p.Novita.APIBase\n\tcase \"byteplus\":\n\t\treturn p.BytePlus.APIBase\n\tcase \"byteplus_coding\":\n\t\treturn p.BytePlusCoding.APIBase\n\tcase \"vertex\":\n\t\t// Computed from project+region at registration time; no config-level static base.\n\t\treturn \"\"\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\n// HasAnyProvider returns true if at least one provider has an API key or CLI configured.\nfunc (c *Config) HasAnyProvider() bool {\n\tp := c.Providers\n\treturn p.Anthropic.APIKey != \"\" ||\n\t\tp.OpenAI.APIKey != \"\" ||\n\t\tp.OpenRouter.APIKey != \"\" ||\n\t\tp.Groq.APIKey != \"\" ||\n\t\tp.Gemini.APIKey != \"\" ||\n\t\tp.DeepSeek.APIKey != \"\" ||\n\t\tp.Mistral.APIKey != \"\" ||\n\t\tp.XAI.APIKey != \"\" ||\n\t\tp.MiniMax.APIKey != \"\" ||\n\t\tp.Cohere.APIKey != \"\" ||\n\t\tp.Perplexity.APIKey != \"\" ||\n\t\tp.DashScope.APIKey != \"\" ||\n\t\tp.Bailian.APIKey != \"\" ||\n\t\tp.Zai.APIKey != \"\" ||\n\t\tp.ZaiCoding.APIKey != \"\" ||\n\t\tp.Ollama.Host != \"\" ||\n\t\tp.OllamaCloud.APIKey != \"\" ||\n\t\tp.ClaudeCLI.CLIPath != \"\" ||\n\t\tp.ACP.Binary != \"\" ||\n\t\tp.Novita.APIKey != \"\" ||\n\t\tp.BytePlus.APIKey != \"\" ||\n\t\tp.BytePlusCoding.APIKey != \"\" ||\n\t\t(p.Vertex.ProjectID != \"\" && p.Vertex.Region != \"\")\n}\n\n// QuotaWindow defines request limits per time window. Zero means unlimited.\ntype QuotaWindow struct {\n\tHour int `json:\"hour,omitempty\"` // max requests per hour (0 = unlimited)\n\tDay int `json:\"day,omitempty\"` // max requests per day (0 = unlimited)\n\tWeek int `json:\"week,omitempty\"` // max requests per week (0 = unlimited)\n}\n\n// IsZero returns true if no limits are set.\nfunc (w QuotaWindow) IsZero() bool { return w.Hour == 0 && w.Day == 0 && w.Week == 0 }\n\n// QuotaConfig configures per-user/group request quotas.\n// Config merge priority: Groups > Channels > Providers > Default.\ntype QuotaConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tDefault QuotaWindow `json:\"default\"`\n\tProviders map[string]QuotaWindow `json:\"providers,omitempty\"` // key = provider name (e.g. \"anthropic\")\n\tChannels map[string]QuotaWindow `json:\"channels,omitempty\"` // key = channel name (e.g. \"telegram\")\n\tGroups map[string]QuotaWindow `json:\"groups,omitempty\"` // key = userID (e.g. \"group:telegram:-100123\")\n}\n\n// GatewayConfig controls the gateway server.\ntype GatewayConfig struct {\n\tHost string `json:\"host\"`\n\tPort int `json:\"port\"`\n\tToken string `json:\"token,omitempty\"` // bearer token for WS/HTTP auth\n\tOwnerIDs []string `json:\"owner_ids,omitempty\"` // sender IDs considered \"owner\"\n\tAllowedOrigins []string `json:\"allowed_origins,omitempty\"` // WebSocket CORS whitelist (empty = allow all)\n\tMaxMessageChars int `json:\"max_message_chars,omitempty\"` // max user message characters (default 32000)\n\tRateLimitRPM int `json:\"rate_limit_rpm,omitempty\"` // rate limit: requests per minute per user (default 20, 0 = disabled)\n\tInjectionAction string `json:\"injection_action,omitempty\"` // prompt injection action: \"log\", \"warn\" (default), \"block\", \"off\"\n\tInboundDebounceMs int `json:\"inbound_debounce_ms,omitempty\"` // merge rapid messages from same sender (default 1000ms, -1 = disabled)\n\tQuota *QuotaConfig `json:\"quota,omitempty\"` // per-user/group request quotas\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // deliver intermediate text during tool iterations (default false)\n\tToolStatus *bool `json:\"tool_status,omitempty\"` // show tool name in streaming preview during tool execution (default true)\n\tTaskRecoveryIntervalSec int `json:\"task_recovery_interval_sec,omitempty\"` // team task recovery ticker interval in seconds (default 300 = 5min)\n\tBackgroundProvider string `json:\"background_provider,omitempty\"` // LLM provider for background workers (vault enrichment, consolidation)\n\tBackgroundModel string `json:\"background_model,omitempty\"` // LLM model for background workers\n}\n\n// ToolsConfig controls tool availability, policy, and web search.\ntype ToolsConfig struct {\n\tProfile string `json:\"profile,omitempty\"` // global profile: \"minimal\", \"coding\", \"messaging\", \"full\"\n\tAllow []string `json:\"allow,omitempty\"` // global allow list (tool names or \"group:xxx\")\n\tDeny []string `json:\"deny,omitempty\"` // global deny list\n\tAlsoAllow []string `json:\"alsoAllow,omitempty\"` // additive: adds without removing existing\n\tByProvider map[string]*ToolPolicySpec `json:\"byProvider,omitempty\"` // per-provider overrides\n\tShellDenyGroups map[string]bool `json:\"shellDenyGroups,omitempty\"` // global shell deny-group toggles (group name -> denied); per-agent overrides win per-key\n\tExecApproval ExecApprovalCfg `json:\"execApproval\"` // exec command approval settings\n\tWebFetch WebFetchPolicyConfig `json:\"web_fetch\"` // domain policy for URL fetching\n\tBrowser BrowserToolConfig `json:\"browser\"`\n\tRateLimitPerHour int `json:\"rate_limit_per_hour,omitempty\"` // max tool executions per hour per session (0 = disabled)\n\tScrubCredentials *bool `json:\"scrub_credentials,omitempty\"` // auto-redact API keys/tokens in tool output (default true)\n\tMcpServers map[string]*MCPServerConfig `json:\"mcp_servers,omitempty\"` // external MCP server connections\n}\n\n// MCPServerConfig configures a single external MCP server connection.\ntype MCPServerConfig struct {\n\tTransport string `json:\"transport\"` // \"stdio\", \"sse\", \"streamable-http\"\n\tCommand string `json:\"command,omitempty\"` // stdio: command to spawn\n\tArgs []string `json:\"args,omitempty\"` // stdio: command arguments\n\tEnv map[string]string `json:\"env,omitempty\"` // stdio: extra environment variables\n\tURL string `json:\"url,omitempty\"` // sse/http: server URL\n\tHeaders map[string]string `json:\"headers,omitempty\"` // sse/http: extra HTTP headers\n\tEnabled *bool `json:\"enabled,omitempty\"` // default true\n\tToolPrefix string `json:\"tool_prefix,omitempty\"` // prefix for tool names (avoids collisions)\n\tTimeoutSec int `json:\"timeout_sec,omitempty\"` // per-tool-call timeout in seconds (default 60)\n}\n\n// IsEnabled returns whether this MCP server is enabled (default true).\nfunc (c *MCPServerConfig) IsEnabled() bool {\n\treturn c.Enabled == nil || *c.Enabled\n}\n\n// ExecApprovalCfg configures command execution approval (matching TS exec-approval.ts).\ntype ExecApprovalCfg struct {\n\tSecurity string `json:\"security,omitempty\"` // \"deny\", \"allowlist\", \"full\" (default \"full\")\n\tAsk string `json:\"ask,omitempty\"` // \"off\", \"on-miss\", \"always\" (default \"off\")\n\tAllowlist []string `json:\"allowlist,omitempty\"` // glob patterns for allowed commands\n}\n\n// WebFetchPolicyConfig controls domain filtering for the web_fetch tool.\ntype WebFetchPolicyConfig struct {\n\tPolicy string `json:\"policy,omitempty\"` // \"allow_all\" (default), \"allowlist\"\n\tAllowedDomains []string `json:\"allowed_domains,omitempty\"` // e.g. [\"github.com\", \"*.example.com\"]\n\tBlockedDomains []string `json:\"blocked_domains,omitempty\"` // always checked regardless of policy\n}\n\n// BrowserToolConfig controls the browser automation tool.\ntype BrowserToolConfig struct {\n\tEnabled bool `json:\"enabled\"` // enable the browser tool (default false)\n\tHeadless bool `json:\"headless,omitempty\"` // run Chrome in headless mode (ignored when RemoteURL is set)\n\tRemoteURL string `json:\"remote_url,omitempty\"` // CDP endpoint for remote Chrome sidecar, e.g. \"ws://chrome:9222\"\n\tActionTimeoutMs int `json:\"action_timeout_ms,omitempty\"` // per-action timeout in ms (default 30000)\n\tIdleTimeoutMs int `json:\"idle_timeout_ms,omitempty\"` // idle page auto-close in ms (default 600000, 0=disabled)\n\tMaxPages int `json:\"max_pages,omitempty\"` // max open pages per tenant (default 5)\n}\n\n// ToolPolicySpec defines a tool policy at any level (global, per-agent, per-provider).\ntype ToolPolicySpec struct {\n\tProfile string `json:\"profile,omitempty\"`\n\tAllow []string `json:\"allow,omitempty\"`\n\tDeny []string `json:\"deny,omitempty\"`\n\tAlsoAllow []string `json:\"alsoAllow,omitempty\"`\n\tByProvider map[string]*ToolPolicySpec `json:\"byProvider,omitempty\"`\n\tWait *WaitToolPolicy `json:\"wait,omitempty\"`\n\tToolCallPrefix string `json:\"toolCallPrefix,omitempty\"` // prefix to strip from model's tool call names before registry lookup\n}\n\n// WaitToolPolicy configures per-agent safety bounds for the wait tool.\ntype WaitToolPolicy struct {\n\tMinMs int `json:\"min_ms,omitempty\"`\n\tMaxMs int `json:\"max_ms,omitempty\"`\n}\n\n// SessionsConfig controls session behavior.\n// Matching TS src/config/sessions/types.ts + src/config/types.base.ts.\ntype SessionsConfig struct {\n\tScope string `json:\"scope,omitempty\"` // \"per-sender\" (default), \"global\"\n\tDmScope string `json:\"dm_scope,omitempty\"` // \"main\", \"per-peer\", \"per-channel-peer\" (default), \"per-account-channel-peer\"\n\tMainKey string `json:\"main_key,omitempty\"` // main session key suffix (default \"main\", used when dm_scope=\"main\")\n}\n\n// TtsConfig configures text-to-speech.\n// Matching TS src/config/types.tts.ts.\ntype TtsConfig struct {\n\tProvider string `json:\"provider,omitempty\"` // \"openai\", \"elevenlabs\", \"edge\", \"minimax\", \"gemini\"\n\tAuto string `json:\"auto,omitempty\"` // \"off\" (default), \"always\", \"inbound\", \"tagged\"\n\tMode string `json:\"mode,omitempty\"` // \"final\" (default), \"all\"\n\tMaxLength int `json:\"max_length,omitempty\"` // max text length before truncation (default 1500)\n\tTimeoutMs int `json:\"timeout_ms,omitempty\"` // API timeout in ms (default 30000)\n\tOpenAI TtsOpenAIConfig `json:\"openai\"`\n\tElevenLabs TtsElevenLabsConfig `json:\"elevenlabs\"`\n\tEdge TtsEdgeConfig `json:\"edge\"`\n\tMiniMax TtsMiniMaxConfig `json:\"minimax\"`\n\tGemini TtsGeminiConfig `json:\"gemini\"`\n}\n\n// TtsGeminiConfig configures the Google Gemini TTS provider.\ntype TtsGeminiConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"` // required; encrypted at rest\n\tAPIBase string `json:\"api_base,omitempty\"` // custom endpoint (optional; SSRF-gated)\n\tVoice string `json:\"voice,omitempty\"` // default \"Kore\"\n\tModel string `json:\"model,omitempty\"` // default \"gemini-2.5-flash-preview-tts\"\n\tSpeakers string `json:\"speakers,omitempty\"` // JSON-encoded []SpeakerVoice for multi-speaker mode\n}\n\n// TtsOpenAIConfig configures the OpenAI TTS provider.\ntype TtsOpenAIConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"`\n\tAPIBase string `json:\"api_base,omitempty\"` // custom endpoint URL\n\tModel string `json:\"model,omitempty\"` // default \"gpt-4o-mini-tts\"\n\tVoice string `json:\"voice,omitempty\"` // default \"alloy\"\n}\n\n// TtsElevenLabsConfig configures the ElevenLabs TTS provider.\ntype TtsElevenLabsConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"`\n\tBaseURL string `json:\"base_url,omitempty\"`\n\tVoiceID string `json:\"voice_id,omitempty\"` // default \"pMsXgVXv3BLzUgSXRplE\"\n\tModelID string `json:\"model_id,omitempty\"` // default \"eleven_multilingual_v2\"\n}\n\n// TtsEdgeConfig configures the Microsoft Edge TTS provider (free, no API key).\ntype TtsEdgeConfig struct {\n\tEnabled bool `json:\"enabled,omitempty\"`\n\tVoice string `json:\"voice,omitempty\"` // default \"en-US-MichelleNeural\"\n\tRate string `json:\"rate,omitempty\"` // speech rate, e.g. \"+0%\"\n}\n\n// TtsMiniMaxConfig configures the MiniMax TTS provider.\ntype TtsMiniMaxConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"`\n\tGroupID string `json:\"group_id,omitempty\"` // MiniMax GroupId (required)\n\tAPIBase string `json:\"api_base,omitempty\"` // default \"https://api.minimax.io/v1\"\n\tModel string `json:\"model,omitempty\"` // default \"speech-02-hd\"\n\tVoiceID string `json:\"voice_id,omitempty\"` // default \"Wise_Woman\"\n}\n\n// MergeChannelGroupQuotas merges per-group quota overrides from channel configs\n// (e.g., channels.telegram.groups[chatID].quota) into gateway.quota.groups.\n// This allows per-group quotas to be set at the channel level and picked up\n// by the QuotaChecker at runtime.\nfunc MergeChannelGroupQuotas(cfg *Config) {\n\tif cfg.Gateway.Quota == nil {\n\t\treturn\n\t}\n\tif cfg.Gateway.Quota.Groups == nil {\n\t\tcfg.Gateway.Quota.Groups = make(map[string]QuotaWindow)\n\t}\n\n\t// Telegram per-group quotas\n\tfor chatID, groupCfg := range cfg.Channels.Telegram.Groups {\n\t\tif groupCfg != nil && groupCfg.Quota != nil && !groupCfg.Quota.IsZero() {\n\t\t\tkey := \"group:telegram:\" + chatID\n\t\t\tcfg.Gateway.Quota.Groups[key] = *groupCfg.Quota\n\t\t}\n\t}\n}\n"}, "d48ae43e4f584c71dad21fd2044e6898bcb6ffea": {"t": 1785759240.698905, "n": 33805, "h": "1d1c2f976714e004", "c": "package config\n\n// PendingCompactionConfig configures LLM-based compaction of pending group messages.\n// When a group accumulates more than Threshold pending messages, older messages are\n// summarized by an LLM and replaced with a compact summary, keeping KeepRecent raw messages.\ntype PendingCompactionConfig struct {\n\tThreshold int `json:\"threshold,omitempty\"` // trigger compaction when entries exceed this (default 200)\n\tKeepRecent int `json:\"keep_recent,omitempty\"` // keep this many recent raw messages after compaction (default 40)\n\tMaxTokens int `json:\"max_tokens,omitempty\"` // max output tokens for LLM summarization (default 4096)\n\tProvider string `json:\"provider,omitempty\"` // LLM provider name (e.g. \"openai\"); empty = use agent's provider\n\tModel string `json:\"model,omitempty\"` // model for summarization; empty = use agent's model\n}\n\n// ChannelsConfig contains per-channel configuration.\ntype ChannelsConfig struct {\n\tTelegram TelegramConfig `json:\"telegram\"`\n\tDiscord DiscordConfig `json:\"discord\"`\n\tSlack SlackConfig `json:\"slack\"`\n\tWhatsApp WhatsAppConfig `json:\"whatsapp\"`\n\tZalo ZaloConfig `json:\"zalo\"`\n\tZaloPersonal ZaloPersonalConfig `json:\"zalo_personal\"`\n\tFeishu FeishuConfig `json:\"feishu\"`\n\tPendingCompaction *PendingCompactionConfig `json:\"pending_compaction,omitempty\"` // global pending message compaction settings\n}\n\ntype TelegramConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tToken string `json:\"token\"`\n\tProxy string `json:\"proxy,omitempty\"`\n\tAPIServer string `json:\"api_server,omitempty\"` // custom Telegram Bot API server URL (e.g. \"http://localhost:8081\")\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in groups (default true)\n\tMentionMode string `json:\"mention_mode,omitempty\"` // \"strict\" (default) = only respond when mentioned; \"yield\" = respond unless another bot is mentioned\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tDMStream *bool `json:\"dm_stream,omitempty\"` // enable streaming for DMs (default false) — edits placeholder progressively\n\tGroupStream *bool `json:\"group_stream,omitempty\"` // enable streaming for groups (default false) — sends new message, edits progressively\n\tDraftTransport *bool `json:\"draft_transport,omitempty\"` // use sendMessageDraft for DM streaming (default true) — stealth preview, no notifications per edit\n\tReasoningStream *bool `json:\"reasoning_stream,omitempty\"` // show reasoning as separate message when provider emits thinking events (default true)\n\tReactionLevel string `json:\"reaction_level,omitempty\"` // \"off\" (default), \"minimal\", \"full\" — status emoji reactions\n\tMediaMaxBytes int64 `json:\"media_max_bytes,omitempty\"` // max media download size in bytes (default 20MB)\n\tLinkPreview *bool `json:\"link_preview,omitempty\"` // enable URL previews in messages (default true)\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tForceIPv4 bool `json:\"force_ipv4,omitempty\"` // force IPv4 for all Telegram API requests (use when IPv6 routing is broken)\n\n\t// Optional STT (Speech-to-Text) pipeline for voice/audio inbound messages.\n\t// When stt_proxy_url is set, audio/voice messages are transcribed before being forwarded to the agent.\n\tSTTProxyURL string `json:\"stt_proxy_url,omitempty\"` // base URL of the STT proxy service (e.g. \"https://stt.example.com\")\n\tSTTAPIKey string `json:\"stt_api_key,omitempty\"` // Bearer token for the STT proxy\n\tSTTTenantID string `json:\"stt_tenant_id,omitempty\"` // optional tenant/org identifier forwarded to the STT proxy\n\tSTTTimeoutSeconds int `json:\"stt_timeout_seconds,omitempty\"` // per-request timeout for STT calls (default 30s)\n\n\t// Optional audio-aware routing: when set, voice/audio inbound messages are routed to this\n\t// agent instead of the default channel agent. Requires the named agent to exist in the config.\n\tVoiceAgentID string `json:\"voice_agent_id,omitempty\"` // agent ID to route voice inbound to (e.g. \"speaking-agent\")\n\n\t// Audio guard: intercept technical errors in voice agent replies and replace with friendly fallbacks.\n\t// Only active when VoiceAgentID is set. Custom error markers replace built-in defaults when provided.\n\tAudioGuardFallbackTranscript string `json:\"audio_guard_fallback_transcript,omitempty\"` // fallback with %s for transcript (e.g. \"I heard: \\\"%s\\\". Try again!\")\n\tAudioGuardFallbackNoTranscript string `json:\"audio_guard_fallback_no_transcript,omitempty\"` // fallback when no transcript available\n\tAudioGuardErrorMarkers []string `json:\"audio_guard_error_markers,omitempty\"` // custom error detection markers (replaces defaults)\n\n\t// Per-group (and per-topic) overrides. Key is chat ID string (e.g. \"-100123456\") or \"*\" for wildcard.\n\t// TS ref: channels.telegram.groups in src/config/types.telegram.ts.\n\tGroups map[string]*TelegramGroupConfig `json:\"groups,omitempty\"`\n}\n\n// TelegramGroupConfig defines per-group overrides for a Telegram channel.\n// Matching TS TelegramGroupConfig in src/config/types.telegram.ts.\ntype TelegramGroupConfig struct {\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // override group policy for this group\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // override require_mention for this group\n\tMentionMode string `json:\"mention_mode,omitempty\"` // override mention_mode for this group\n\tAllowFrom FlexibleStringSlice `json:\"allow_from,omitempty\"` // override allow_from for this group\n\tEnabled *bool `json:\"enabled,omitempty\"` // disable bot for this group (default: true)\n\tSkills []string `json:\"skills,omitempty\"` // skill whitelist (nil = all, [] = none)\n\tTools []string `json:\"tools,omitempty\"` // tool allow list (nil = all, supports \"group:xxx\")\n\tSystemPrompt string `json:\"system_prompt,omitempty\"` // extra system prompt for this group\n\tTopics map[string]*TelegramTopicConfig `json:\"topics,omitempty\"` // per-topic overrides (key: thread ID string)\n\tQuota *QuotaWindow `json:\"quota,omitempty\"` // per-group quota override\n}\n\n// TelegramTopicConfig defines per-topic overrides within a Telegram group.\n// Matching TS TelegramTopicConfig in src/config/types.telegram.ts.\ntype TelegramTopicConfig struct {\n\tRequireMention *bool `json:\"require_mention,omitempty\"`\n\tMentionMode string `json:\"mention_mode,omitempty\"`\n\tGroupPolicy string `json:\"group_policy,omitempty\"`\n\tSkills []string `json:\"skills,omitempty\"`\n\tTools []string `json:\"tools,omitempty\"` // tool allow list (nil = inherit, supports \"group:xxx\")\n\tEnabled *bool `json:\"enabled,omitempty\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from,omitempty\"`\n\tSystemPrompt string `json:\"system_prompt,omitempty\"`\n}\n\ntype DiscordConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tToken string `json:\"token\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in groups (default true)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tMediaMaxBytes int64 `json:\"media_max_bytes,omitempty\"` // max media download size (default 25MB)\n\tSTTProxyURL string `json:\"stt_proxy_url,omitempty\"`\n\tSTTAPIKey string `json:\"stt_api_key,omitempty\"`\n\tSTTTenantID string `json:\"stt_tenant_id,omitempty\"`\n\tSTTTimeoutSeconds int `json:\"stt_timeout_seconds,omitempty\"`\n\tVoiceAgentID string `json:\"voice_agent_id,omitempty\"`\n}\n\ntype SlackConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tBotToken string `json:\"bot_token\"` // xoxb-... (Bot User OAuth Token)\n\tAppToken string `json:\"app_token\"` // xapp-... (App-Level Token for Socket Mode)\n\tUserToken string `json:\"user_token,omitempty\"` // xoxp-... (Optional: custom bot identity)\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"pairing\", \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in channels (default true)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tDMStream *bool `json:\"dm_stream,omitempty\"` // enable streaming for DMs (default false)\n\tGroupStream *bool `json:\"group_stream,omitempty\"` // enable streaming for groups (default false)\n\tNativeStream *bool `json:\"native_stream,omitempty\"` // use Slack ChatStreamer API if available (default false)\n\tReactionLevel string `json:\"reaction_level,omitempty\"` // \"off\" (default), \"minimal\", \"full\"\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tDebounceDelay int `json:\"debounce_delay,omitempty\"` // ms delay before dispatching rapid messages (default 300, 0=disabled)\n\tThreadTTL *int `json:\"thread_ttl,omitempty\"` // hours before thread participation expires (default 24, 0=disabled — always require @mention)\n\tMediaMaxBytes int64 `json:\"media_max_bytes,omitempty\"` // max file download size in bytes (default 20MB)\n}\n\ntype WhatsAppConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tAuthDir string `json:\"auth_dir,omitempty\"` // optional: SQLite auth dir override (desktop)\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default for DB instances), \"open\", \"allowlist\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"pairing\" (default for DB instances), \"open\" (default for config), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // only respond in groups when bot is @mentioned (default false)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 200, 0=disabled)\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n}\n\ntype ZaloConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tToken string `json:\"token\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tWebhookURL string `json:\"webhook_url,omitempty\"`\n\tWebhookSecret string `json:\"webhook_secret,omitempty\"`\n\tMediaMaxMB int `json:\"media_max_mb,omitempty\"` // default 5\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n}\n\ntype ZaloPersonalConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in groups (default true)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tCredentialsPath string `json:\"credentials_path,omitempty\"` // path to saved cookies JSON\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n}\n\ntype FeishuConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tAppID string `json:\"app_id\"`\n\tAppSecret string `json:\"app_secret\"`\n\tEncryptKey string `json:\"encrypt_key,omitempty\"`\n\tVerificationToken string `json:\"verification_token,omitempty\"`\n\tDomain string `json:\"domain,omitempty\"` // \"lark\" (default/global), \"feishu\" (China), or custom URL\n\tConnectionMode string `json:\"connection_mode,omitempty\"` // \"websocket\" (default), \"webhook\"\n\tWebhookPort int `json:\"webhook_port,omitempty\"` // default 3000\n\tWebhookPath string `json:\"webhook_path,omitempty\"` // default \"/feishu/events\"\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default)\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default)\n\tGroupAllowFrom FlexibleStringSlice `json:\"group_allow_from,omitempty\"`\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // default true (groups)\n\tTopicSessionMode string `json:\"topic_session_mode,omitempty\"` // \"disabled\" (default)\n\tTextChunkLimit int `json:\"text_chunk_limit,omitempty\"` // default 4000\n\tMediaMaxMB int `json:\"media_max_mb,omitempty\"` // default 30\n\tRenderMode string `json:\"render_mode,omitempty\"` // \"auto\", \"raw\", \"card\"\n\tStreaming *bool `json:\"streaming,omitempty\"` // default true\n\tReactionLevel string `json:\"reaction_level,omitempty\"` // \"off\" (default), \"minimal\", \"full\" — typing emoji reactions\n\tHistoryLimit int `json:\"history_limit,omitempty\"`\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tSTTProxyURL string `json:\"stt_proxy_url,omitempty\"`\n\tSTTAPIKey string `json:\"stt_api_key,omitempty\"`\n\tSTTTenantID string `json:\"stt_tenant_id,omitempty\"`\n\tSTTTimeoutSeconds int `json:\"stt_timeout_seconds,omitempty\"`\n\tVoiceAgentID string `json:\"voice_agent_id,omitempty\"`\n}\n\n// ProvidersConfig maps provider name to its config.\ntype ProvidersConfig struct {\n\tAnthropic ProviderConfig `json:\"anthropic\"`\n\tOpenAI ProviderConfig `json:\"openai\"`\n\tOpenRouter ProviderConfig `json:\"openrouter\"`\n\tGroq ProviderConfig `json:\"groq\"`\n\tGemini ProviderConfig `json:\"gemini\"`\n\tDeepSeek ProviderConfig `json:\"deepseek\"`\n\tMistral ProviderConfig `json:\"mistral\"`\n\tXAI ProviderConfig `json:\"xai\"`\n\tMiniMax ProviderConfig `json:\"minimax\"`\n\tCohere ProviderConfig `json:\"cohere\"`\n\tPerplexity ProviderConfig `json:\"perplexity\"`\n\tDashScope ProviderConfig `json:\"dashscope\"`\n\tBailian ProviderConfig `json:\"bailian\"`\n\tZai ProviderConfig `json:\"zai\"`\n\tZaiCoding ProviderConfig `json:\"zai_coding\"`\n\tOllama OllamaConfig `json:\"ollama\"` // local Ollama instance (no API key needed)\n\tOllamaCloud ProviderConfig `json:\"ollama_cloud\"` // Ollama Cloud (API key required)\n\tClaudeCLI ClaudeCLIConfig `json:\"claude_cli\"`\n\tACP ACPConfig `json:\"acp\"`\n\tNovita ProviderConfig `json:\"novita\"` // Novita AI (OpenAI-compatible endpoint)\n\tBytePlus ProviderConfig `json:\"byteplus\"` // BytePlus ModelArk (Seed 2.0)\n\tBytePlusCoding ProviderConfig `json:\"byteplus_coding\"` // BytePlus ModelArk Coding Plan\n\tVertex VertexConfig `json:\"vertex\"` // Google Cloud Vertex AI (OAuth2 service account + ADC)\n}\n\n// VertexConfig configures Google Cloud Vertex AI.\n// Credentials precedence: APIKey (inline JSON) > CredentialsFile (path) > ADC (both empty).\n// ProjectID and Region are required; Model optional (defaults to google/gemini-2.0-flash-001).\ntype VertexConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"` // service account JSON inline (secret — never persist in config.json)\n\tCredentialsFile string `json:\"credentials_file,omitempty\"` // path to service account JSON file\n\tProjectID string `json:\"project_id,omitempty\"`\n\tRegion string `json:\"region,omitempty\"`\n\tModel string `json:\"model,omitempty\"`\n}\n\n// OllamaConfig configures a local (or self-hosted) Ollama instance.\n// No API key is required — Ollama accepts any Bearer token value.\ntype OllamaConfig struct {\n\tHost string `json:\"host\"` // Ollama server base URL, e.g. http://localhost:11434\n}\n\n// ClaudeCLIConfig configures the Claude CLI provider (uses subscription, not API key).\ntype ClaudeCLIConfig struct {\n\tCLIPath string `json:\"cli_path\" yaml:\"cli_path\"` // path to claude binary (default: \"claude\")\n\tModel string `json:\"model\" yaml:\"model\"` // default model alias (default: \"sonnet\")\n\tBaseWorkDir string `json:\"base_work_dir\" yaml:\"base_work_dir\"` // base dir for agent workspaces\n\tPermMode string `json:\"perm_mode\" yaml:\"perm_mode\"` // permission mode (default: \"bypassPermissions\")\n}\n\n// ACPConfig configures the ACP (Agent Client Protocol) provider.\n// Orchestrates any ACP-compatible coding agent (Claude Code, Codex CLI, Gemini CLI) as a subprocess.\ntype ACPConfig struct {\n\tBinary string `json:\"binary\"` // agent binary name or path (e.g. \"claude\", \"codex\")\n\tArgs []string `json:\"args\"` // extra spawn args\n\tModel string `json:\"model\"` // default model/agent name\n\tWorkDir string `json:\"work_dir\"` // base workspace dir\n\tIdleTTL string `json:\"idle_ttl\"` // process idle TTL (e.g. \"5m\")\n\tPermMode string `json:\"perm_mode\"` // \"approve-all\" (default), \"approve-reads\", \"deny-all\"\n}\n\ntype ProviderConfig struct {\n\tAPIKey string `json:\"api_key\"`\n\tAPIBase string `json:\"api_base,omitempty\"`\n}\n\n// APIBaseForType returns the config-level api_base for a given provider type.\n// Used as a fallback when DB providers have no api_base set.\nfunc (p *ProvidersConfig) APIBaseForType(providerType string) string {\n\tswitch providerType {\n\tcase \"anthropic_native\":\n\t\treturn p.Anthropic.APIBase\n\tcase \"openai\", \"openai_compat\":\n\t\treturn p.OpenAI.APIBase\n\tcase \"openrouter\":\n\t\treturn p.OpenRouter.APIBase\n\tcase \"groq\":\n\t\treturn p.Groq.APIBase\n\tcase \"deepseek\":\n\t\treturn p.DeepSeek.APIBase\n\tcase \"gemini_native\":\n\t\treturn p.Gemini.APIBase\n\tcase \"mistral\":\n\t\treturn p.Mistral.APIBase\n\tcase \"xai\":\n\t\treturn p.XAI.APIBase\n\tcase \"minimax_native\":\n\t\treturn p.MiniMax.APIBase\n\tcase \"cohere\":\n\t\treturn p.Cohere.APIBase\n\tcase \"perplexity\":\n\t\treturn p.Perplexity.APIBase\n\tcase \"dashscope\":\n\t\treturn p.DashScope.APIBase\n\tcase \"bailian\":\n\t\treturn p.Bailian.APIBase\n\tcase \"zai\":\n\t\treturn p.Zai.APIBase\n\tcase \"zai_coding\":\n\t\treturn p.ZaiCoding.APIBase\n\tcase \"ollama_cloud\":\n\t\treturn p.OllamaCloud.APIBase\n\tcase \"novita\":\n\t\treturn p.Novita.APIBase\n\tcase \"byteplus\":\n\t\treturn p.BytePlus.APIBase\n\tcase \"byteplus_coding\":\n\t\treturn p.BytePlusCoding.APIBase\n\tcase \"vertex\":\n\t\t// Computed from project+region at registration time; no config-level static base.\n\t\treturn \"\"\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\n// HasAnyProvider returns true if at least one provider has an API key or CLI configured.\nfunc (c *Config) HasAnyProvider() bool {\n\tp := c.Providers\n\treturn p.Anthropic.APIKey != \"\" ||\n\t\tp.OpenAI.APIKey != \"\" ||\n\t\tp.OpenRouter.APIKey != \"\" ||\n\t\tp.Groq.APIKey != \"\" ||\n\t\tp.Gemini.APIKey != \"\" ||\n\t\tp.DeepSeek.APIKey != \"\" ||\n\t\tp.Mistral.APIKey != \"\" ||\n\t\tp.XAI.APIKey != \"\" ||\n\t\tp.MiniMax.APIKey != \"\" ||\n\t\tp.Cohere.APIKey != \"\" ||\n\t\tp.Perplexity.APIKey != \"\" ||\n\t\tp.DashScope.APIKey != \"\" ||\n\t\tp.Bailian.APIKey != \"\" ||\n\t\tp.Zai.APIKey != \"\" ||\n\t\tp.ZaiCoding.APIKey != \"\" ||\n\t\tp.Ollama.Host != \"\" ||\n\t\tp.OllamaCloud.APIKey != \"\" ||\n\t\tp.ClaudeCLI.CLIPath != \"\" ||\n\t\tp.ACP.Binary != \"\" ||\n\t\tp.Novita.APIKey != \"\" ||\n\t\tp.BytePlus.APIKey != \"\" ||\n\t\tp.BytePlusCoding.APIKey != \"\" ||\n\t\t(p.Vertex.ProjectID != \"\" && p.Vertex.Region != \"\")\n}\n\n// QuotaWindow defines request limits per time window. Zero means unlimited.\ntype QuotaWindow struct {\n\tHour int `json:\"hour,omitempty\"` // max requests per hour (0 = unlimited)\n\tDay int `json:\"day,omitempty\"` // max requests per day (0 = unlimited)\n\tWeek int `json:\"week,omitempty\"` // max requests per week (0 = unlimited)\n}\n\n// IsZero returns true if no limits are set.\nfunc (w QuotaWindow) IsZero() bool { return w.Hour == 0 && w.Day == 0 && w.Week == 0 }\n\n// QuotaConfig configures per-user/group request quotas.\n// Config merge priority: Groups > Channels > Providers > Default.\ntype QuotaConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tDefault QuotaWindow `json:\"default\"`\n\tProviders map[string]QuotaWindow `json:\"providers,omitempty\"` // key = provider name (e.g. \"anthropic\")\n\tChannels map[string]QuotaWindow `json:\"channels,omitempty\"` // key = channel name (e.g. \"telegram\")\n\tGroups map[string]QuotaWindow `json:\"groups,omitempty\"` // key = userID (e.g. \"group:telegram:-100123\")\n}\n\n// GatewayConfig controls the gateway server.\ntype GatewayConfig struct {\n\tHost string `json:\"host\"`\n\tPort int `json:\"port\"`\n\tToken string `json:\"token,omitempty\"` // bearer token for WS/HTTP auth\n\tOwnerIDs []string `json:\"owner_ids,omitempty\"` // sender IDs considered \"owner\"\n\tAllowedOrigins []string `json:\"allowed_origins,omitempty\"` // WebSocket CORS whitelist (empty = allow all)\n\tMaxMessageChars int `json:\"max_message_chars,omitempty\"` // max user message characters (default 32000)\n\tRateLimitRPM int `json:\"rate_limit_rpm,omitempty\"` // rate limit: requests per minute per user (default 20, 0 = disabled)\n\tInjectionAction string `json:\"injection_action,omitempty\"` // prompt injection action: \"log\", \"warn\" (default), \"block\", \"off\"\n\tInboundDebounceMs int `json:\"inbound_debounce_ms,omitempty\"` // merge rapid messages from same sender (default 1000ms, -1 = disabled)\n\tQuota *QuotaConfig `json:\"quota,omitempty\"` // per-user/group request quotas\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // deliver intermediate text during tool iterations (default false)\n\tToolStatus *bool `json:\"tool_status,omitempty\"` // show tool name in streaming preview during tool execution (default true)\n\tTaskRecoveryIntervalSec int `json:\"task_recovery_interval_sec,omitempty\"` // team task recovery ticker interval in seconds (default 300 = 5min)\n\tBackgroundProvider string `json:\"background_provider,omitempty\"` // LLM provider for background workers (vault enrichment, consolidation)\n\tBackgroundModel string `json:\"background_model,omitempty\"` // LLM model for background workers\n}\n\n// ToolsConfig controls tool availability, policy, and web search.\ntype ToolsConfig struct {\n\tProfile string `json:\"profile,omitempty\"` // global profile: \"minimal\", \"coding\", \"messaging\", \"full\"\n\tAllow []string `json:\"allow,omitempty\"` // global allow list (tool names or \"group:xxx\")\n\tDeny []string `json:\"deny,omitempty\"` // global deny list\n\tAlsoAllow []string `json:\"alsoAllow,omitempty\"` // additive: adds without removing existing\n\tByProvider map[string]*ToolPolicySpec `json:\"byProvider,omitempty\"` // per-provider overrides\n\tShellDenyGroups map[string]bool `json:\"shellDenyGroups,omitempty\"` // global shell deny-group toggles (group name -> denied); per-agent overrides win per-key\n\tExecApproval ExecApprovalCfg `json:\"execApproval\"` // exec command approval settings\n\tWebFetch WebFetchPolicyConfig `json:\"web_fetch\"` // domain policy for URL fetching\n\tBrowser BrowserToolConfig `json:\"browser\"`\n\tRateLimitPerHour int `json:\"rate_limit_per_hour,omitempty\"` // max tool executions per hour per session (0 = disabled)\n\tScrubCredentials *bool `json:\"scrub_credentials,omitempty\"` // auto-redact API keys/tokens in tool output (default true)\n\tMcpServers map[string]*MCPServerConfig `json:\"mcp_servers,omitempty\"` // external MCP server connections\n}\n\n// MCPServerConfig configures a single external MCP server connection.\ntype MCPServerConfig struct {\n\tTransport string `json:\"transport\"` // \"stdio\", \"sse\", \"streamable-http\"\n\tCommand string `json:\"command,omitempty\"` // stdio: command to spawn\n\tArgs []string `json:\"args,omitempty\"` // stdio: command arguments\n\tEnv map[string]string `json:\"env,omitempty\"` // stdio: extra environment variables\n\tURL string `json:\"url,omitempty\"` // sse/http: server URL\n\tHeaders map[string]string `json:\"headers,omitempty\"` // sse/http: extra HTTP headers\n\tEnabled *bool `json:\"enabled,omitempty\"` // default true\n\tToolPrefix string `json:\"tool_prefix,omitempty\"` // prefix for tool names (avoids collisions)\n\tTimeoutSec int `json:\"timeout_sec,omitempty\"` // per-tool-call timeout in seconds (default 60)\n}\n\n// IsEnabled returns whether this MCP server is enabled (default true).\nfunc (c *MCPServerConfig) IsEnabled() bool {\n\treturn c.Enabled == nil || *c.Enabled\n}\n\n// ExecApprovalCfg configures command execution approval (matching TS exec-approval.ts).\ntype ExecApprovalCfg struct {\n\tSecurity string `json:\"security,omitempty\"` // \"deny\", \"allowlist\", \"full\" (default \"full\")\n\tAsk string `json:\"ask,omitempty\"` // \"off\", \"on-miss\", \"always\" (default \"off\")\n\tAllowlist []string `json:\"allowlist,omitempty\"` // glob patterns for allowed commands\n}\n\n// WebFetchPolicyConfig controls domain filtering for the web_fetch tool.\ntype WebFetchPolicyConfig struct {\n\tPolicy string `json:\"policy,omitempty\"` // \"allow_all\" (default), \"allowlist\"\n\tAllowedDomains []string `json:\"allowed_domains,omitempty\"` // e.g. [\"github.com\", \"*.example.com\"]\n\tBlockedDomains []string `json:\"blocked_domains,omitempty\"` // always checked regardless of policy\n}\n\n// BrowserToolConfig controls the browser automation tool.\ntype BrowserToolConfig struct {\n\tEnabled bool `json:\"enabled\"` // enable the browser tool (default false)\n\tHeadless bool `json:\"headless,omitempty\"` // run Chrome in headless mode (ignored when RemoteURL is set)\n\tRemoteURL string `json:\"remote_url,omitempty\"` // CDP endpoint for remote Chrome sidecar, e.g. \"ws://chrome:9222\"\n\tActionTimeoutMs int `json:\"action_timeout_ms,omitempty\"` // per-action timeout in ms (default 30000)\n\tIdleTimeoutMs int `json:\"idle_timeout_ms,omitempty\"` // idle page auto-close in ms (default 600000, 0=disabled)\n\tMaxPages int `json:\"max_pages,omitempty\"` // max open pages per tenant (default 5)\n}\n\n// ToolPolicySpec defines a tool policy at any level (global, per-agent, per-provider).\ntype ToolPolicySpec struct {\n\tProfile string `json:\"profile,omitempty\"`\n\tAllow []string `json:\"allow,omitempty\"`\n\tDeny []string `json:\"deny,omitempty\"`\n\tAlsoAllow []string `json:\"alsoAllow,omitempty\"`\n\tByProvider map[string]*ToolPolicySpec `json:\"byProvider,omitempty\"`\n\tWait *WaitToolPolicy `json:\"wait,omitempty\"`\n\tToolCallPrefix string `json:\"toolCallPrefix,omitempty\"` // prefix to strip from model's tool call names before registry lookup\n}\n\n// WaitToolPolicy configures per-agent safety bounds for the wait tool.\ntype WaitToolPolicy struct {\n\tMinMs int `json:\"min_ms,omitempty\"`\n\tMaxMs int `json:\"max_ms,omitempty\"`\n}\n\n// SessionsConfig controls session behavior.\n// Matching TS src/config/sessions/types.ts + src/config/types.base.ts.\ntype SessionsConfig struct {\n\tScope string `json:\"scope,omitempty\"` // \"per-sender\" (default), \"global\"\n\tDmScope string `json:\"dm_scope,omitempty\"` // \"main\", \"per-peer\", \"per-channel-peer\" (default), \"per-account-channel-peer\"\n\tMainKey string `json:\"main_key,omitempty\"` // main session key suffix (default \"main\", used when dm_scope=\"main\")\n}\n\n// TtsConfig configures text-to-speech.\n// Matching TS src/config/types.tts.ts.\ntype TtsConfig struct {\n\tProvider string `json:\"provider,omitempty\"` // \"openai\", \"elevenlabs\", \"edge\", \"minimax\", \"gemini\"\n\tAuto string `json:\"auto,omitempty\"` // \"off\" (default), \"always\", \"inbound\", \"tagged\"\n\tMode string `json:\"mode,omitempty\"` // \"final\" (default), \"all\"\n\tMaxLength int `json:\"max_length,omitempty\"` // max text length before truncation (default 1500)\n\tTimeoutMs int `json:\"timeout_ms,omitempty\"` // API timeout in ms (default 30000)\n\tOpenAI TtsOpenAIConfig `json:\"openai\"`\n\tElevenLabs TtsElevenLabsConfig `json:\"elevenlabs\"`\n\tEdge TtsEdgeConfig `json:\"edge\"`\n\tMiniMax TtsMiniMaxConfig `json:\"minimax\"`\n\tGemini TtsGeminiConfig `json:\"gemini\"`\n}\n\n// TtsGeminiConfig configures the Google Gemini TTS provider.\ntype TtsGeminiConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"` // required; encrypted at rest\n\tAPIBase string `json:\"api_base,omitempty\"` // custom endpoint (optional; SSRF-gated)\n\tVoice string `json:\"voice,omitempty\"` // default \"Kore\"\n\tModel string `json:\"model,omitempty\"` // default \"gemini-2.5-flash-preview-tts\"\n\tSpeakers string `json:\"speakers,omitempty\"` // JSON-encoded []SpeakerVoice for multi-speaker mode\n}\n\n// TtsOpenAIConfig configures the OpenAI TTS provider.\ntype TtsOpenAIConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"`\n\tAPIBase string `json:\"api_base,omitempty\"` // custom endpoint URL\n\tModel string `json:\"model,omitempty\"` // default \"gpt-4o-mini-tts\"\n\tVoice string `json:\"voice,omitempty\"` // default \"alloy\"\n}\n\n// TtsElevenLabsConfig configures the ElevenLabs TTS provider.\ntype TtsElevenLabsConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"`\n\tBaseURL string `json:\"base_url,omitempty\"`\n\tVoiceID string `json:\"voice_id,omitempty\"` // default \"pMsXgVXv3BLzUgSXRplE\"\n\tModelID string `json:\"model_id,omitempty\"` // default \"eleven_multilingual_v2\"\n}\n\n// TtsEdgeConfig configures the Microsoft Edge TTS provider (free, no API key).\ntype TtsEdgeConfig struct {\n\tEnabled bool `json:\"enabled,omitempty\"`\n\tVoice string `json:\"voice,omitempty\"` // default \"en-US-MichelleNeural\"\n\tRate string `json:\"rate,omitempty\"` // speech rate, e.g. \"+0%\"\n}\n\n// TtsMiniMaxConfig configures the MiniMax TTS provider.\ntype TtsMiniMaxConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"`\n\tGroupID string `json:\"group_id,omitempty\"` // MiniMax GroupId (required)\n\tAPIBase string `json:\"api_base,omitempty\"` // default \"https://api.minimax.io/v1\"\n\tModel string `json:\"model,omitempty\"` // default \"speech-02-hd\"\n\tVoiceID string `json:\"voice_id,omitempty\"` // default \"Wise_Woman\"\n}\n\n// MergeChannelGroupQuotas merges per-group quota overrides from channel configs\n// (e.g., channels.telegram.groups[chatID].quota) into gateway.quota.groups.\n// This allows per-group quotas to be set at the channel level and picked up\n// by the QuotaChecker at runtime.\nfunc MergeChannelGroupQuotas(cfg *Config) {\n\tif cfg.Gateway.Quota == nil {\n\t\treturn\n\t}\n\tif cfg.Gateway.Quota.Groups == nil {\n\t\tcfg.Gateway.Quota.Groups = make(map[string]QuotaWindow)\n\t}\n\n\t// Telegram per-group quotas\n\tfor chatID, groupCfg := range cfg.Channels.Telegram.Groups {\n\t\tif groupCfg != nil && groupCfg.Quota != nil && !groupCfg.Quota.IsZero() {\n\t\t\tkey := \"group:telegram:\" + chatID\n\t\t\tcfg.Gateway.Quota.Groups[key] = *groupCfg.Quota\n\t\t}\n\t}\n}\n"}, "2da52cfaee222c0dcc956f3bf500c2812edd6bbd": {"t": 1785759242.6647432, "n": 33564, "h": "5a9c82ac9ec84f03", "c": "package config\n\n// PendingCompactionConfig configures LLM-based compaction of pending group messages.\n// When a group accumulates more than Threshold pending messages, older messages are\n// summarized by an LLM and replaced with a compact summary, keeping KeepRecent raw messages.\ntype PendingCompactionConfig struct {\n\tThreshold int `json:\"threshold,omitempty\"` // trigger compaction when entries exceed this (default 200)\n\tKeepRecent int `json:\"keep_recent,omitempty\"` // keep this many recent raw messages after compaction (default 40)\n\tMaxTokens int `json:\"max_tokens,omitempty\"` // max output tokens for LLM summarization (default 4096)\n\tProvider string `json:\"provider,omitempty\"` // LLM provider name (e.g. \"openai\"); empty = use agent's provider\n\tModel string `json:\"model,omitempty\"` // model for summarization; empty = use agent's model\n}\n\n// ChannelsConfig contains per-channel configuration.\ntype ChannelsConfig struct {\n\tTelegram TelegramConfig `json:\"telegram\"`\n\tDiscord DiscordConfig `json:\"discord\"`\n\tSlack SlackConfig `json:\"slack\"`\n\tWhatsApp WhatsAppConfig `json:\"whatsapp\"`\n\tZalo ZaloConfig `json:\"zalo\"`\n\tZaloPersonal ZaloPersonalConfig `json:\"zalo_personal\"`\n\tFeishu FeishuConfig `json:\"feishu\"`\n\tPendingCompaction *PendingCompactionConfig `json:\"pending_compaction,omitempty\"` // global pending message compaction settings\n}\n\ntype TelegramConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tToken string `json:\"token\"`\n\tProxy string `json:\"proxy,omitempty\"`\n\tAPIServer string `json:\"api_server,omitempty\"` // custom Telegram Bot API server URL (e.g. \"http://localhost:8081\")\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in groups (default true)\n\tMentionMode string `json:\"mention_mode,omitempty\"` // \"strict\" (default) = only respond when mentioned; \"yield\" = respond unless another bot is mentioned\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tDMStream *bool `json:\"dm_stream,omitempty\"` // enable streaming for DMs (default false) — edits placeholder progressively\n\tGroupStream *bool `json:\"group_stream,omitempty\"` // enable streaming for groups (default false) — sends new message, edits progressively\n\tDraftTransport *bool `json:\"draft_transport,omitempty\"` // use sendMessageDraft for DM streaming (default true) — stealth preview, no notifications per edit\n\tReasoningStream *bool `json:\"reasoning_stream,omitempty\"` // show reasoning as separate message when provider emits thinking events (default true)\n\tReactionLevel string `json:\"reaction_level,omitempty\"` // \"off\" (default), \"minimal\", \"full\" — status emoji reactions\n\tMediaMaxBytes int64 `json:\"media_max_bytes,omitempty\"` // max media download size in bytes (default 20MB)\n\tLinkPreview *bool `json:\"link_preview,omitempty\"` // enable URL previews in messages (default true)\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tForceIPv4 bool `json:\"force_ipv4,omitempty\"` // force IPv4 for all Telegram API requests (use when IPv6 routing is broken)\n\n\t// Optional STT (Speech-to-Text) pipeline for voice/audio inbound messages.\n\t// When stt_proxy_url is set, audio/voice messages are transcribed before being forwarded to the agent.\n\tSTTProxyURL string `json:\"stt_proxy_url,omitempty\"` // base URL of the STT proxy service (e.g. \"https://stt.example.com\")\n\tSTTAPIKey string `json:\"stt_api_key,omitempty\"` // Bearer token for the STT proxy\n\tSTTTenantID string `json:\"stt_tenant_id,omitempty\"` // optional tenant/org identifier forwarded to the STT proxy\n\tSTTTimeoutSeconds int `json:\"stt_timeout_seconds,omitempty\"` // per-request timeout for STT calls (default 30s)\n\n\t// Optional audio-aware routing: when set, voice/audio inbound messages are routed to this\n\t// agent instead of the default channel agent. Requires the named agent to exist in the config.\n\tVoiceAgentID string `json:\"voice_agent_id,omitempty\"` // agent ID to route voice inbound to (e.g. \"speaking-agent\")\n\n\t// Audio guard: intercept technical errors in voice agent replies and replace with friendly fallbacks.\n\t// Only active when VoiceAgentID is set. Custom error markers replace built-in defaults when provided.\n\tAudioGuardFallbackTranscript string `json:\"audio_guard_fallback_transcript,omitempty\"` // fallback with %s for transcript (e.g. \"I heard: \\\"%s\\\". Try again!\")\n\tAudioGuardFallbackNoTranscript string `json:\"audio_guard_fallback_no_transcript,omitempty\"` // fallback when no transcript available\n\tAudioGuardErrorMarkers []string `json:\"audio_guard_error_markers,omitempty\"` // custom error detection markers (replaces defaults)\n\n\t// Per-group (and per-topic) overrides. Key is chat ID string (e.g. \"-100123456\") or \"*\" for wildcard.\n\t// TS ref: channels.telegram.groups in src/config/types.telegram.ts.\n\tGroups map[string]*TelegramGroupConfig `json:\"groups,omitempty\"`\n}\n\n// TelegramGroupConfig defines per-group overrides for a Telegram channel.\n// Matching TS TelegramGroupConfig in src/config/types.telegram.ts.\ntype TelegramGroupConfig struct {\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // override group policy for this group\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // override require_mention for this group\n\tMentionMode string `json:\"mention_mode,omitempty\"` // override mention_mode for this group\n\tAllowFrom FlexibleStringSlice `json:\"allow_from,omitempty\"` // override allow_from for this group\n\tEnabled *bool `json:\"enabled,omitempty\"` // disable bot for this group (default: true)\n\tSkills []string `json:\"skills,omitempty\"` // skill whitelist (nil = all, [] = none)\n\tTools []string `json:\"tools,omitempty\"` // tool allow list (nil = all, supports \"group:xxx\")\n\tSystemPrompt string `json:\"system_prompt,omitempty\"` // extra system prompt for this group\n\tTopics map[string]*TelegramTopicConfig `json:\"topics,omitempty\"` // per-topic overrides (key: thread ID string)\n\tQuota *QuotaWindow `json:\"quota,omitempty\"` // per-group quota override\n}\n\n// TelegramTopicConfig defines per-topic overrides within a Telegram group.\n// Matching TS TelegramTopicConfig in src/config/types.telegram.ts.\ntype TelegramTopicConfig struct {\n\tRequireMention *bool `json:\"require_mention,omitempty\"`\n\tMentionMode string `json:\"mention_mode,omitempty\"`\n\tGroupPolicy string `json:\"group_policy,omitempty\"`\n\tSkills []string `json:\"skills,omitempty\"`\n\tTools []string `json:\"tools,omitempty\"` // tool allow list (nil = inherit, supports \"group:xxx\")\n\tEnabled *bool `json:\"enabled,omitempty\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from,omitempty\"`\n\tSystemPrompt string `json:\"system_prompt,omitempty\"`\n}\n\ntype DiscordConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tToken string `json:\"token\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in groups (default true)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tMediaMaxBytes int64 `json:\"media_max_bytes,omitempty\"` // max media download size (default 25MB)\n\tSTTProxyURL string `json:\"stt_proxy_url,omitempty\"`\n\tSTTAPIKey string `json:\"stt_api_key,omitempty\"`\n\tSTTTenantID string `json:\"stt_tenant_id,omitempty\"`\n\tSTTTimeoutSeconds int `json:\"stt_timeout_seconds,omitempty\"`\n\tVoiceAgentID string `json:\"voice_agent_id,omitempty\"`\n}\n\ntype SlackConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tBotToken string `json:\"bot_token\"` // xoxb-... (Bot User OAuth Token)\n\tAppToken string `json:\"app_token\"` // xapp-... (App-Level Token for Socket Mode)\n\tUserToken string `json:\"user_token,omitempty\"` // xoxp-... (Optional: custom bot identity)\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"pairing\", \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in channels (default true)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tDMStream *bool `json:\"dm_stream,omitempty\"` // enable streaming for DMs (default false)\n\tGroupStream *bool `json:\"group_stream,omitempty\"` // enable streaming for groups (default false)\n\tNativeStream *bool `json:\"native_stream,omitempty\"` // use Slack ChatStreamer API if available (default false)\n\tReactionLevel string `json:\"reaction_level,omitempty\"` // \"off\" (default), \"minimal\", \"full\"\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tDebounceDelay int `json:\"debounce_delay,omitempty\"` // ms delay before dispatching rapid messages (default 300, 0=disabled)\n\tThreadTTL *int `json:\"thread_ttl,omitempty\"` // hours before thread participation expires (default 24, 0=disabled — always require @mention)\n\tMediaMaxBytes int64 `json:\"media_max_bytes,omitempty\"` // max file download size in bytes (default 20MB)\n}\n\ntype WhatsAppConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tAuthDir string `json:\"auth_dir,omitempty\"` // optional: SQLite auth dir override (desktop)\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default for DB instances), \"open\", \"allowlist\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"pairing\" (default for DB instances), \"open\" (default for config), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // only respond in groups when bot is @mentioned (default false)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 200, 0=disabled)\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n}\n\ntype ZaloConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tToken string `json:\"token\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tWebhookURL string `json:\"webhook_url,omitempty\"`\n\tWebhookSecret string `json:\"webhook_secret,omitempty\"`\n\tMediaMaxMB int `json:\"media_max_mb,omitempty\"` // default 5\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n}\n\ntype ZaloPersonalConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in groups (default true)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tCredentialsPath string `json:\"credentials_path,omitempty\"` // path to saved cookies JSON\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n}\n\ntype FeishuConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tAppID string `json:\"app_id\"`\n\tAppSecret string `json:\"app_secret\"`\n\tEncryptKey string `json:\"encrypt_key,omitempty\"`\n\tVerificationToken string `json:\"verification_token,omitempty\"`\n\tDomain string `json:\"domain,omitempty\"` // \"lark\" (default/global), \"feishu\" (China), or custom URL\n\tConnectionMode string `json:\"connection_mode,omitempty\"` // \"websocket\" (default), \"webhook\"\n\tWebhookPort int `json:\"webhook_port,omitempty\"` // default 3000\n\tWebhookPath string `json:\"webhook_path,omitempty\"` // default \"/feishu/events\"\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default)\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default)\n\tGroupAllowFrom FlexibleStringSlice `json:\"group_allow_from,omitempty\"`\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // default true (groups)\n\tTopicSessionMode string `json:\"topic_session_mode,omitempty\"` // \"disabled\" (default)\n\tTextChunkLimit int `json:\"text_chunk_limit,omitempty\"` // default 4000\n\tMediaMaxMB int `json:\"media_max_mb,omitempty\"` // default 30\n\tRenderMode string `json:\"render_mode,omitempty\"` // \"auto\", \"raw\", \"card\"\n\tStreaming *bool `json:\"streaming,omitempty\"` // default true\n\tReactionLevel string `json:\"reaction_level,omitempty\"` // \"off\" (default), \"minimal\", \"full\" — typing emoji reactions\n\tHistoryLimit int `json:\"history_limit,omitempty\"`\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tSTTProxyURL string `json:\"stt_proxy_url,omitempty\"`\n\tSTTAPIKey string `json:\"stt_api_key,omitempty\"`\n\tSTTTenantID string `json:\"stt_tenant_id,omitempty\"`\n\tSTTTimeoutSeconds int `json:\"stt_timeout_seconds,omitempty\"`\n\tVoiceAgentID string `json:\"voice_agent_id,omitempty\"`\n}\n\n// ProvidersConfig maps provider name to its config.\ntype ProvidersConfig struct {\n\tAnthropic ProviderConfig `json:\"anthropic\"`\n\tOpenAI ProviderConfig `json:\"openai\"`\n\tOpenRouter ProviderConfig `json:\"openrouter\"`\n\tGroq ProviderConfig `json:\"groq\"`\n\tGemini ProviderConfig `json:\"gemini\"`\n\tDeepSeek ProviderConfig `json:\"deepseek\"`\n\tMistral ProviderConfig `json:\"mistral\"`\n\tXAI ProviderConfig `json:\"xai\"`\n\tMiniMax ProviderConfig `json:\"minimax\"`\n\tCohere ProviderConfig `json:\"cohere\"`\n\tPerplexity ProviderConfig `json:\"perplexity\"`\n\tDashScope ProviderConfig `json:\"dashscope\"`\n\tBailian ProviderConfig `json:\"bailian\"`\n\tZai ProviderConfig `json:\"zai\"`\n\tZaiCoding ProviderConfig `json:\"zai_coding\"`\n\tOllama OllamaConfig `json:\"ollama\"` // local Ollama instance (no API key needed)\n\tOllamaCloud ProviderConfig `json:\"ollama_cloud\"` // Ollama Cloud (API key required)\n\tClaudeCLI ClaudeCLIConfig `json:\"claude_cli\"`\n\tACP ACPConfig `json:\"acp\"`\n\tNovita ProviderConfig `json:\"novita\"` // Novita AI (OpenAI-compatible endpoint)\n\tBytePlus ProviderConfig `json:\"byteplus\"` // BytePlus ModelArk (Seed 2.0)\n\tBytePlusCoding ProviderConfig `json:\"byteplus_coding\"` // BytePlus ModelArk Coding Plan\n\tVertex VertexConfig `json:\"vertex\"` // Google Cloud Vertex AI (OAuth2 service account + ADC)\n}\n\n// VertexConfig configures Google Cloud Vertex AI.\n// Credentials precedence: APIKey (inline JSON) > CredentialsFile (path) > ADC (both empty).\n// ProjectID and Region are required; Model optional (defaults to google/gemini-2.0-flash-001).\ntype VertexConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"` // service account JSON inline (secret — never persist in config.json)\n\tCredentialsFile string `json:\"credentials_file,omitempty\"` // path to service account JSON file\n\tProjectID string `json:\"project_id,omitempty\"`\n\tRegion string `json:\"region,omitempty\"`\n\tModel string `json:\"model,omitempty\"`\n}\n\n// OllamaConfig configures a local (or self-hosted) Ollama instance.\n// No API key is required — Ollama accepts any Bearer token value.\ntype OllamaConfig struct {\n\tHost string `json:\"host\"` // Ollama server base URL, e.g. http://localhost:11434\n}\n\n// ClaudeCLIConfig configures the Claude CLI provider (uses subscription, not API key).\ntype ClaudeCLIConfig struct {\n\tCLIPath string `json:\"cli_path\" yaml:\"cli_path\"` // path to claude binary (default: \"claude\")\n\tModel string `json:\"model\" yaml:\"model\"` // default model alias (default: \"sonnet\")\n\tBaseWorkDir string `json:\"base_work_dir\" yaml:\"base_work_dir\"` // base dir for agent workspaces\n\tPermMode string `json:\"perm_mode\" yaml:\"perm_mode\"` // permission mode (default: \"bypassPermissions\")\n}\n\n// ACPConfig configures the ACP (Agent Client Protocol) provider.\n// Orchestrates any ACP-compatible coding agent (Claude Code, Codex CLI, Gemini CLI) as a subprocess.\ntype ACPConfig struct {\n\tBinary string `json:\"binary\"` // agent binary name or path (e.g. \"claude\", \"codex\")\n\tArgs []string `json:\"args\"` // extra spawn args\n\tModel string `json:\"model\"` // default model/agent name\n\tWorkDir string `json:\"work_dir\"` // base workspace dir\n\tIdleTTL string `json:\"idle_ttl\"` // process idle TTL (e.g. \"5m\")\n\tPermMode string `json:\"perm_mode\"` // \"approve-all\" (default), \"approve-reads\", \"deny-all\"\n}\n\ntype ProviderConfig struct {\n\tAPIKey string `json:\"api_key\"`\n\tAPIBase string `json:\"api_base,omitempty\"`\n}\n\n// APIBaseForType returns the config-level api_base for a given provider type.\n// Used as a fallback when DB providers have no api_base set.\nfunc (p *ProvidersConfig) APIBaseForType(providerType string) string {\n\tswitch providerType {\n\tcase \"anthropic_native\":\n\t\treturn p.Anthropic.APIBase\n\tcase \"openai\", \"openai_compat\":\n\t\treturn p.OpenAI.APIBase\n\tcase \"openrouter\":\n\t\treturn p.OpenRouter.APIBase\n\tcase \"groq\":\n\t\treturn p.Groq.APIBase\n\tcase \"deepseek\":\n\t\treturn p.DeepSeek.APIBase\n\tcase \"gemini_native\":\n\t\treturn p.Gemini.APIBase\n\tcase \"mistral\":\n\t\treturn p.Mistral.APIBase\n\tcase \"xai\":\n\t\treturn p.XAI.APIBase\n\tcase \"minimax_native\":\n\t\treturn p.MiniMax.APIBase\n\tcase \"cohere\":\n\t\treturn p.Cohere.APIBase\n\tcase \"perplexity\":\n\t\treturn p.Perplexity.APIBase\n\tcase \"dashscope\":\n\t\treturn p.DashScope.APIBase\n\tcase \"bailian\":\n\t\treturn p.Bailian.APIBase\n\tcase \"zai\":\n\t\treturn p.Zai.APIBase\n\tcase \"zai_coding\":\n\t\treturn p.ZaiCoding.APIBase\n\tcase \"ollama_cloud\":\n\t\treturn p.OllamaCloud.APIBase\n\tcase \"novita\":\n\t\treturn p.Novita.APIBase\n\tcase \"byteplus\":\n\t\treturn p.BytePlus.APIBase\n\tcase \"byteplus_coding\":\n\t\treturn p.BytePlusCoding.APIBase\n\tcase \"vertex\":\n\t\t// Computed from project+region at registration time; no config-level static base.\n\t\treturn \"\"\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\n// HasAnyProvider returns true if at least one provider has an API key or CLI configured.\nfunc (c *Config) HasAnyProvider() bool {\n\tp := c.Providers\n\treturn p.Anthropic.APIKey != \"\" ||\n\t\tp.OpenAI.APIKey != \"\" ||\n\t\tp.OpenRouter.APIKey != \"\" ||\n\t\tp.Groq.APIKey != \"\" ||\n\t\tp.Gemini.APIKey != \"\" ||\n\t\tp.DeepSeek.APIKey != \"\" ||\n\t\tp.Mistral.APIKey != \"\" ||\n\t\tp.XAI.APIKey != \"\" ||\n\t\tp.MiniMax.APIKey != \"\" ||\n\t\tp.Cohere.APIKey != \"\" ||\n\t\tp.Perplexity.APIKey != \"\" ||\n\t\tp.DashScope.APIKey != \"\" ||\n\t\tp.Bailian.APIKey != \"\" ||\n\t\tp.Zai.APIKey != \"\" ||\n\t\tp.ZaiCoding.APIKey != \"\" ||\n\t\tp.Ollama.Host != \"\" ||\n\t\tp.OllamaCloud.APIKey != \"\" ||\n\t\tp.ClaudeCLI.CLIPath != \"\" ||\n\t\tp.ACP.Binary != \"\" ||\n\t\tp.Novita.APIKey != \"\" ||\n\t\tp.BytePlus.APIKey != \"\" ||\n\t\tp.BytePlusCoding.APIKey != \"\" ||\n\t\t(p.Vertex.ProjectID != \"\" && p.Vertex.Region != \"\")\n}\n\n// QuotaWindow defines request limits per time window. Zero means unlimited.\ntype QuotaWindow struct {\n\tHour int `json:\"hour,omitempty\"` // max requests per hour (0 = unlimited)\n\tDay int `json:\"day,omitempty\"` // max requests per day (0 = unlimited)\n\tWeek int `json:\"week,omitempty\"` // max requests per week (0 = unlimited)\n}\n\n// IsZero returns true if no limits are set.\nfunc (w QuotaWindow) IsZero() bool { return w.Hour == 0 && w.Day == 0 && w.Week == 0 }\n\n// QuotaConfig configures per-user/group request quotas.\n// Config merge priority: Groups > Channels > Providers > Default.\ntype QuotaConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tDefault QuotaWindow `json:\"default\"`\n\tProviders map[string]QuotaWindow `json:\"providers,omitempty\"` // key = provider name (e.g. \"anthropic\")\n\tChannels map[string]QuotaWindow `json:\"channels,omitempty\"` // key = channel name (e.g. \"telegram\")\n\tGroups map[string]QuotaWindow `json:\"groups,omitempty\"` // key = userID (e.g. \"group:telegram:-100123\")\n}\n\n// GatewayConfig controls the gateway server.\ntype GatewayConfig struct {\n\tHost string `json:\"host\"`\n\tPort int `json:\"port\"`\n\tToken string `json:\"token,omitempty\"` // bearer token for WS/HTTP auth\n\tOwnerIDs []string `json:\"owner_ids,omitempty\"` // sender IDs considered \"owner\"\n\tAllowedOrigins []string `json:\"allowed_origins,omitempty\"` // WebSocket CORS whitelist (empty = allow all)\n\tMaxMessageChars int `json:\"max_message_chars,omitempty\"` // max user message characters (default 32000)\n\tRateLimitRPM int `json:\"rate_limit_rpm,omitempty\"` // rate limit: requests per minute per user (default 20, 0 = disabled)\n\tInjectionAction string `json:\"injection_action,omitempty\"` // prompt injection action: \"log\", \"warn\" (default), \"block\", \"off\"\n\tInboundDebounceMs int `json:\"inbound_debounce_ms,omitempty\"` // merge rapid messages from same sender (default 1000ms, -1 = disabled)\n\tQuota *QuotaConfig `json:\"quota,omitempty\"` // per-user/group request quotas\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // deliver intermediate text during tool iterations (default false)\n\tToolStatus *bool `json:\"tool_status,omitempty\"` // show tool name in streaming preview during tool execution (default true)\n\tTaskRecoveryIntervalSec int `json:\"task_recovery_interval_sec,omitempty\"` // team task recovery ticker interval in seconds (default 300 = 5min)\n\tBackgroundProvider string `json:\"background_provider,omitempty\"` // LLM provider for background workers (vault enrichment, consolidation)\n\tBackgroundModel string `json:\"background_model,omitempty\"` // LLM model for background workers\n}\n\n// ToolsConfig controls tool availability, policy, and web search.\ntype ToolsConfig struct {\n\tProfile string `json:\"profile,omitempty\"` // global profile: \"minimal\", \"coding\", \"messaging\", \"full\"\n\tAllow []string `json:\"allow,omitempty\"` // global allow list (tool names or \"group:xxx\")\n\tDeny []string `json:\"deny,omitempty\"` // global deny list\n\tAlsoAllow []string `json:\"alsoAllow,omitempty\"` // additive: adds without removing existing\n\tByProvider map[string]*ToolPolicySpec `json:\"byProvider,omitempty\"` // per-provider overrides\n\tShellDenyGroups map[string]bool `json:\"shellDenyGroups,omitempty\"` // global shell deny-group toggles (group name -> denied); per-agent overrides win per-key\n\tExecApproval ExecApprovalCfg `json:\"execApproval\"` // exec command approval settings\n\tWebFetch WebFetchPolicyConfig `json:\"web_fetch\"` // domain policy for URL fetching\n\tBrowser BrowserToolConfig `json:\"browser\"`\n\tRateLimitPerHour int `json:\"rate_limit_per_hour,omitempty\"` // max tool executions per hour per session (0 = disabled)\n\tScrubCredentials *bool `json:\"scrub_credentials,omitempty\"` // auto-redact API keys/tokens in tool output (default true)\n\tMcpServers map[string]*MCPServerConfig `json:\"mcp_servers,omitempty\"` // external MCP server connections\n}\n\n// MCPServerConfig configures a single external MCP server connection.\ntype MCPServerConfig struct {\n\tTransport string `json:\"transport\"` // \"stdio\", \"sse\", \"streamable-http\"\n\tCommand string `json:\"command,omitempty\"` // stdio: command to spawn\n\tArgs []string `json:\"args,omitempty\"` // stdio: command arguments\n\tEnv map[string]string `json:\"env,omitempty\"` // stdio: extra environment variables\n\tURL string `json:\"url,omitempty\"` // sse/http: server URL\n\tHeaders map[string]string `json:\"headers,omitempty\"` // sse/http: extra HTTP headers\n\tEnabled *bool `json:\"enabled,omitempty\"` // default true\n\tToolPrefix string `json:\"tool_prefix,omitempty\"` // prefix for tool names (avoids collisions)\n\tTimeoutSec int `json:\"timeout_sec,omitempty\"` // per-tool-call timeout in seconds (default 60)\n}\n\n// IsEnabled returns whether this MCP server is enabled (default true).\nfunc (c *MCPServerConfig) IsEnabled() bool {\n\treturn c.Enabled == nil || *c.Enabled\n}\n\n// ExecApprovalCfg configures command execution approval (matching TS exec-approval.ts).\ntype ExecApprovalCfg struct {\n\tSecurity string `json:\"security,omitempty\"` // \"deny\", \"allowlist\", \"full\" (default \"full\")\n\tAsk string `json:\"ask,omitempty\"` // \"off\", \"on-miss\", \"always\" (default \"off\")\n\tAllowlist []string `json:\"allowlist,omitempty\"` // glob patterns for allowed commands\n}\n\n// WebFetchPolicyConfig controls domain filtering for the web_fetch tool.\ntype WebFetchPolicyConfig struct {\n\tPolicy string `json:\"policy,omitempty\"` // \"allow_all\" (default), \"allowlist\"\n\tAllowedDomains []string `json:\"allowed_domains,omitempty\"` // e.g. [\"github.com\", \"*.example.com\"]\n\tBlockedDomains []string `json:\"blocked_domains,omitempty\"` // always checked regardless of policy\n}\n\n// BrowserToolConfig controls the browser automation tool.\ntype BrowserToolConfig struct {\n\tEnabled bool `json:\"enabled\"` // enable the browser tool (default false)\n\tHeadless bool `json:\"headless,omitempty\"` // run Chrome in headless mode (ignored when RemoteURL is set)\n\tRemoteURL string `json:\"remote_url,omitempty\"` // CDP endpoint for remote Chrome sidecar, e.g. \"ws://chrome:9222\"\n\tActionTimeoutMs int `json:\"action_timeout_ms,omitempty\"` // per-action timeout in ms (default 30000)\n\tIdleTimeoutMs int `json:\"idle_timeout_ms,omitempty\"` // idle page auto-close in ms (default 600000, 0=disabled)\n\tMaxPages int `json:\"max_pages,omitempty\"` // max open pages per tenant (default 5)\n}\n\n// ToolPolicySpec defines a tool policy at any level (global, per-agent, per-provider).\ntype ToolPolicySpec struct {\n\tProfile string `json:\"profile,omitempty\"`\n\tAllow []string `json:\"allow,omitempty\"`\n\tDeny []string `json:\"deny,omitempty\"`\n\tAlsoAllow []string `json:\"alsoAllow,omitempty\"`\n\tByProvider map[string]*ToolPolicySpec `json:\"byProvider,omitempty\"`\n\tToolCallPrefix string `json:\"toolCallPrefix,omitempty\"` // prefix to strip from model's tool call names before registry lookup\n}\n\n\n// SessionsConfig controls session behavior.\n// Matching TS src/config/sessions/types.ts + src/config/types.base.ts.\ntype SessionsConfig struct {\n\tScope string `json:\"scope,omitempty\"` // \"per-sender\" (default), \"global\"\n\tDmScope string `json:\"dm_scope,omitempty\"` // \"main\", \"per-peer\", \"per-channel-peer\" (default), \"per-account-channel-peer\"\n\tMainKey string `json:\"main_key,omitempty\"` // main session key suffix (default \"main\", used when dm_scope=\"main\")\n}\n\n// TtsConfig configures text-to-speech.\n// Matching TS src/config/types.tts.ts.\ntype TtsConfig struct {\n\tProvider string `json:\"provider,omitempty\"` // \"openai\", \"elevenlabs\", \"edge\", \"minimax\", \"gemini\"\n\tAuto string `json:\"auto,omitempty\"` // \"off\" (default), \"always\", \"inbound\", \"tagged\"\n\tMode string `json:\"mode,omitempty\"` // \"final\" (default), \"all\"\n\tMaxLength int `json:\"max_length,omitempty\"` // max text length before truncation (default 1500)\n\tTimeoutMs int `json:\"timeout_ms,omitempty\"` // API timeout in ms (default 30000)\n\tOpenAI TtsOpenAIConfig `json:\"openai\"`\n\tElevenLabs TtsElevenLabsConfig `json:\"elevenlabs\"`\n\tEdge TtsEdgeConfig `json:\"edge\"`\n\tMiniMax TtsMiniMaxConfig `json:\"minimax\"`\n\tGemini TtsGeminiConfig `json:\"gemini\"`\n}\n\n// TtsGeminiConfig configures the Google Gemini TTS provider.\ntype TtsGeminiConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"` // required; encrypted at rest\n\tAPIBase string `json:\"api_base,omitempty\"` // custom endpoint (optional; SSRF-gated)\n\tVoice string `json:\"voice,omitempty\"` // default \"Kore\"\n\tModel string `json:\"model,omitempty\"` // default \"gemini-2.5-flash-preview-tts\"\n\tSpeakers string `json:\"speakers,omitempty\"` // JSON-encoded []SpeakerVoice for multi-speaker mode\n}\n\n// TtsOpenAIConfig configures the OpenAI TTS provider.\ntype TtsOpenAIConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"`\n\tAPIBase string `json:\"api_base,omitempty\"` // custom endpoint URL\n\tModel string `json:\"model,omitempty\"` // default \"gpt-4o-mini-tts\"\n\tVoice string `json:\"voice,omitempty\"` // default \"alloy\"\n}\n\n// TtsElevenLabsConfig configures the ElevenLabs TTS provider.\ntype TtsElevenLabsConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"`\n\tBaseURL string `json:\"base_url,omitempty\"`\n\tVoiceID string `json:\"voice_id,omitempty\"` // default \"pMsXgVXv3BLzUgSXRplE\"\n\tModelID string `json:\"model_id,omitempty\"` // default \"eleven_multilingual_v2\"\n}\n\n// TtsEdgeConfig configures the Microsoft Edge TTS provider (free, no API key).\ntype TtsEdgeConfig struct {\n\tEnabled bool `json:\"enabled,omitempty\"`\n\tVoice string `json:\"voice,omitempty\"` // default \"en-US-MichelleNeural\"\n\tRate string `json:\"rate,omitempty\"` // speech rate, e.g. \"+0%\"\n}\n\n// TtsMiniMaxConfig configures the MiniMax TTS provider.\ntype TtsMiniMaxConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"`\n\tGroupID string `json:\"group_id,omitempty\"` // MiniMax GroupId (required)\n\tAPIBase string `json:\"api_base,omitempty\"` // default \"https://api.minimax.io/v1\"\n\tModel string `json:\"model,omitempty\"` // default \"speech-02-hd\"\n\tVoiceID string `json:\"voice_id,omitempty\"` // default \"Wise_Woman\"\n}\n\n// MergeChannelGroupQuotas merges per-group quota overrides from channel configs\n// (e.g., channels.telegram.groups[chatID].quota) into gateway.quota.groups.\n// This allows per-group quotas to be set at the channel level and picked up\n// by the QuotaChecker at runtime.\nfunc MergeChannelGroupQuotas(cfg *Config) {\n\tif cfg.Gateway.Quota == nil {\n\t\treturn\n\t}\n\tif cfg.Gateway.Quota.Groups == nil {\n\t\tcfg.Gateway.Quota.Groups = make(map[string]QuotaWindow)\n\t}\n\n\t// Telegram per-group quotas\n\tfor chatID, groupCfg := range cfg.Channels.Telegram.Groups {\n\t\tif groupCfg != nil && groupCfg.Quota != nil && !groupCfg.Quota.IsZero() {\n\t\t\tkey := \"group:telegram:\" + chatID\n\t\t\tcfg.Gateway.Quota.Groups[key] = *groupCfg.Quota\n\t\t}\n\t}\n}\n"}, "df5d48c78dde62d95f91bf45c8e47036c437951f": {"t": 1785759244.4108078, "n": 32629, "h": "935f1e8926247915", "c": "package config\n\n// PendingCompactionConfig configures LLM-based compaction of pending group messages.\n// When a group accumulates more than Threshold pending messages, older messages are\n// summarized by an LLM and replaced with a compact summary, keeping KeepRecent raw messages.\ntype PendingCompactionConfig struct {\n\tThreshold int `json:\"threshold,omitempty\"` // trigger compaction when entries exceed this (default 200)\n\tKeepRecent int `json:\"keep_recent,omitempty\"` // keep this many recent raw messages after compaction (default 40)\n\tMaxTokens int `json:\"max_tokens,omitempty\"` // max output tokens for LLM summarization (default 4096)\n\tProvider string `json:\"provider,omitempty\"` // LLM provider name (e.g. \"openai\"); empty = use agent's provider\n\tModel string `json:\"model,omitempty\"` // model for summarization; empty = use agent's model\n}\n\n// ChannelsConfig contains per-channel configuration.\ntype ChannelsConfig struct {\n\tTelegram TelegramConfig `json:\"telegram\"`\n\tDiscord DiscordConfig `json:\"discord\"`\n\tSlack SlackConfig `json:\"slack\"`\n\tWhatsApp WhatsAppConfig `json:\"whatsapp\"`\n\tZalo ZaloConfig `json:\"zalo\"`\n\tZaloPersonal ZaloPersonalConfig `json:\"zalo_personal\"`\n\tFeishu FeishuConfig `json:\"feishu\"`\n\tPendingCompaction *PendingCompactionConfig `json:\"pending_compaction,omitempty\"` // global pending message compaction settings\n}\n\ntype TelegramConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tToken string `json:\"token\"`\n\tProxy string `json:\"proxy,omitempty\"`\n\tAPIServer string `json:\"api_server,omitempty\"` // custom Telegram Bot API server URL (e.g. \"http://localhost:8081\")\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in groups (default true)\n\tMentionMode string `json:\"mention_mode,omitempty\"` // \"strict\" (default) = only respond when mentioned; \"yield\" = respond unless another bot is mentioned\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tDMStream *bool `json:\"dm_stream,omitempty\"` // enable streaming for DMs (default false) — edits placeholder progressively\n\tGroupStream *bool `json:\"group_stream,omitempty\"` // enable streaming for groups (default false) — sends new message, edits progressively\n\tDraftTransport *bool `json:\"draft_transport,omitempty\"` // use sendMessageDraft for DM streaming (default true) — stealth preview, no notifications per edit\n\tReasoningStream *bool `json:\"reasoning_stream,omitempty\"` // show reasoning as separate message when provider emits thinking events (default true)\n\tReactionLevel string `json:\"reaction_level,omitempty\"` // \"off\" (default), \"minimal\", \"full\" — status emoji reactions\n\tMediaMaxBytes int64 `json:\"media_max_bytes,omitempty\"` // max media download size in bytes (default 20MB)\n\tLinkPreview *bool `json:\"link_preview,omitempty\"` // enable URL previews in messages (default true)\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tForceIPv4 bool `json:\"force_ipv4,omitempty\"` // force IPv4 for all Telegram API requests (use when IPv6 routing is broken)\n\n\t// Optional STT (Speech-to-Text) pipeline for voice/audio inbound messages.\n\t// When stt_proxy_url is set, audio/voice messages are transcribed before being forwarded to the agent.\n\tSTTProxyURL string `json:\"stt_proxy_url,omitempty\"` // base URL of the STT proxy service (e.g. \"https://stt.example.com\")\n\tSTTAPIKey string `json:\"stt_api_key,omitempty\"` // Bearer token for the STT proxy\n\tSTTTenantID string `json:\"stt_tenant_id,omitempty\"` // optional tenant/org identifier forwarded to the STT proxy\n\tSTTTimeoutSeconds int `json:\"stt_timeout_seconds,omitempty\"` // per-request timeout for STT calls (default 30s)\n\n\t// Optional audio-aware routing: when set, voice/audio inbound messages are routed to this\n\t// agent instead of the default channel agent. Requires the named agent to exist in the config.\n\tVoiceAgentID string `json:\"voice_agent_id,omitempty\"` // agent ID to route voice inbound to (e.g. \"speaking-agent\")\n\n\t// Audio guard: intercept technical errors in voice agent replies and replace with friendly fallbacks.\n\t// Only active when VoiceAgentID is set. Custom error markers replace built-in defaults when provided.\n\tAudioGuardFallbackTranscript string `json:\"audio_guard_fallback_transcript,omitempty\"` // fallback with %s for transcript (e.g. \"I heard: \\\"%s\\\". Try again!\")\n\tAudioGuardFallbackNoTranscript string `json:\"audio_guard_fallback_no_transcript,omitempty\"` // fallback when no transcript available\n\tAudioGuardErrorMarkers []string `json:\"audio_guard_error_markers,omitempty\"` // custom error detection markers (replaces defaults)\n\n\t// Per-group (and per-topic) overrides. Key is chat ID string (e.g. \"-100123456\") or \"*\" for wildcard.\n\t// TS ref: channels.telegram.groups in src/config/types.telegram.ts.\n\tGroups map[string]*TelegramGroupConfig `json:\"groups,omitempty\"`\n}\n\n// TelegramGroupConfig defines per-group overrides for a Telegram channel.\n// Matching TS TelegramGroupConfig in src/config/types.telegram.ts.\ntype TelegramGroupConfig struct {\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // override group policy for this group\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // override require_mention for this group\n\tMentionMode string `json:\"mention_mode,omitempty\"` // override mention_mode for this group\n\tAllowFrom FlexibleStringSlice `json:\"allow_from,omitempty\"` // override allow_from for this group\n\tEnabled *bool `json:\"enabled,omitempty\"` // disable bot for this group (default: true)\n\tSkills []string `json:\"skills,omitempty\"` // skill whitelist (nil = all, [] = none)\n\tTools []string `json:\"tools,omitempty\"` // tool allow list (nil = all, supports \"group:xxx\")\n\tSystemPrompt string `json:\"system_prompt,omitempty\"` // extra system prompt for this group\n\tTopics map[string]*TelegramTopicConfig `json:\"topics,omitempty\"` // per-topic overrides (key: thread ID string)\n\tQuota *QuotaWindow `json:\"quota,omitempty\"` // per-group quota override\n}\n\n// TelegramTopicConfig defines per-topic overrides within a Telegram group.\n// Matching TS TelegramTopicConfig in src/config/types.telegram.ts.\ntype TelegramTopicConfig struct {\n\tRequireMention *bool `json:\"require_mention,omitempty\"`\n\tMentionMode string `json:\"mention_mode,omitempty\"`\n\tGroupPolicy string `json:\"group_policy,omitempty\"`\n\tSkills []string `json:\"skills,omitempty\"`\n\tTools []string `json:\"tools,omitempty\"` // tool allow list (nil = inherit, supports \"group:xxx\")\n\tEnabled *bool `json:\"enabled,omitempty\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from,omitempty\"`\n\tSystemPrompt string `json:\"system_prompt,omitempty\"`\n}\n\ntype DiscordConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tToken string `json:\"token\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in groups (default true)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tMediaMaxBytes int64 `json:\"media_max_bytes,omitempty\"` // max media download size (default 25MB)\n\tSTTProxyURL string `json:\"stt_proxy_url,omitempty\"`\n\tSTTAPIKey string `json:\"stt_api_key,omitempty\"`\n\tSTTTenantID string `json:\"stt_tenant_id,omitempty\"`\n\tSTTTimeoutSeconds int `json:\"stt_timeout_seconds,omitempty\"`\n\tVoiceAgentID string `json:\"voice_agent_id,omitempty\"`\n}\n\ntype SlackConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tBotToken string `json:\"bot_token\"` // xoxb-... (Bot User OAuth Token)\n\tAppToken string `json:\"app_token\"` // xapp-... (App-Level Token for Socket Mode)\n\tUserToken string `json:\"user_token,omitempty\"` // xoxp-... (Optional: custom bot identity)\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"pairing\", \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in channels (default true)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tDMStream *bool `json:\"dm_stream,omitempty\"` // enable streaming for DMs (default false)\n\tGroupStream *bool `json:\"group_stream,omitempty\"` // enable streaming for groups (default false)\n\tNativeStream *bool `json:\"native_stream,omitempty\"` // use Slack ChatStreamer API if available (default false)\n\tReactionLevel string `json:\"reaction_level,omitempty\"` // \"off\" (default), \"minimal\", \"full\"\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tDebounceDelay int `json:\"debounce_delay,omitempty\"` // ms delay before dispatching rapid messages (default 300, 0=disabled)\n\tThreadTTL *int `json:\"thread_ttl,omitempty\"` // hours before thread participation expires (default 24, 0=disabled — always require @mention)\n\tMediaMaxBytes int64 `json:\"media_max_bytes,omitempty\"` // max file download size in bytes (default 20MB)\n}\n\ntype WhatsAppConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tAuthDir string `json:\"auth_dir,omitempty\"` // optional: SQLite auth dir override (desktop)\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default for DB instances), \"open\", \"allowlist\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"pairing\" (default for DB instances), \"open\" (default for config), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // only respond in groups when bot is @mentioned (default false)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 200, 0=disabled)\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n}\n\ntype ZaloConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tToken string `json:\"token\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tWebhookURL string `json:\"webhook_url,omitempty\"`\n\tWebhookSecret string `json:\"webhook_secret,omitempty\"`\n\tMediaMaxMB int `json:\"media_max_mb,omitempty\"` // default 5\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n}\n\ntype ZaloPersonalConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in groups (default true)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tCredentialsPath string `json:\"credentials_path,omitempty\"` // path to saved cookies JSON\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n}\n\ntype FeishuConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tAppID string `json:\"app_id\"`\n\tAppSecret string `json:\"app_secret\"`\n\tEncryptKey string `json:\"encrypt_key,omitempty\"`\n\tVerificationToken string `json:\"verification_token,omitempty\"`\n\tDomain string `json:\"domain,omitempty\"` // \"lark\" (default/global), \"feishu\" (China), or custom URL\n\tConnectionMode string `json:\"connection_mode,omitempty\"` // \"websocket\" (default), \"webhook\"\n\tWebhookPort int `json:\"webhook_port,omitempty\"` // default 3000\n\tWebhookPath string `json:\"webhook_path,omitempty\"` // default \"/feishu/events\"\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default)\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default)\n\tGroupAllowFrom FlexibleStringSlice `json:\"group_allow_from,omitempty\"`\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // default true (groups)\n\tTopicSessionMode string `json:\"topic_session_mode,omitempty\"` // \"disabled\" (default)\n\tTextChunkLimit int `json:\"text_chunk_limit,omitempty\"` // default 4000\n\tMediaMaxMB int `json:\"media_max_mb,omitempty\"` // default 30\n\tRenderMode string `json:\"render_mode,omitempty\"` // \"auto\", \"raw\", \"card\"\n\tStreaming *bool `json:\"streaming,omitempty\"` // default true\n\tReactionLevel string `json:\"reaction_level,omitempty\"` // \"off\" (default), \"minimal\", \"full\" — typing emoji reactions\n\tHistoryLimit int `json:\"history_limit,omitempty\"`\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tSTTProxyURL string `json:\"stt_proxy_url,omitempty\"`\n\tSTTAPIKey string `json:\"stt_api_key,omitempty\"`\n\tSTTTenantID string `json:\"stt_tenant_id,omitempty\"`\n\tSTTTimeoutSeconds int `json:\"stt_timeout_seconds,omitempty\"`\n\tVoiceAgentID string `json:\"voice_agent_id,omitempty\"`\n}\n\n// ProvidersConfig maps provider name to its config.\ntype ProvidersConfig struct {\n\tAnthropic ProviderConfig `json:\"anthropic\"`\n\tOpenAI ProviderConfig `json:\"openai\"`\n\tOpenRouter ProviderConfig `json:\"openrouter\"`\n\tGroq ProviderConfig `json:\"groq\"`\n\tGemini ProviderConfig `json:\"gemini\"`\n\tDeepSeek ProviderConfig `json:\"deepseek\"`\n\tMistral ProviderConfig `json:\"mistral\"`\n\tXAI ProviderConfig `json:\"xai\"`\n\tMiniMax ProviderConfig `json:\"minimax\"`\n\tCohere ProviderConfig `json:\"cohere\"`\n\tPerplexity ProviderConfig `json:\"perplexity\"`\n\tDashScope ProviderConfig `json:\"dashscope\"`\n\tBailian ProviderConfig `json:\"bailian\"`\n\tZai ProviderConfig `json:\"zai\"`\n\tZaiCoding ProviderConfig `json:\"zai_coding\"`\n\tOllama OllamaConfig `json:\"ollama\"` // local Ollama instance (no API key needed)\n\tOllamaCloud ProviderConfig `json:\"ollama_cloud\"` // Ollama Cloud (API key required)\n\tClaudeCLI ClaudeCLIConfig `json:\"claude_cli\"`\n\tACP ACPConfig `json:\"acp\"`\n\tNovita ProviderConfig `json:\"novita\"` // Novita AI (OpenAI-compatible endpoint)\n\tBytePlus ProviderConfig `json:\"byteplus\"` // BytePlus ModelArk (Seed 2.0)\n\tBytePlusCoding ProviderConfig `json:\"byteplus_coding\"` // BytePlus ModelArk Coding Plan\n}\n\n// OllamaConfig configures a local (or self-hosted) Ollama instance.\n// No API key is required — Ollama accepts any Bearer token value.\ntype OllamaConfig struct {\n\tHost string `json:\"host\"` // Ollama server base URL, e.g. http://localhost:11434\n}\n\n// ClaudeCLIConfig configures the Claude CLI provider (uses subscription, not API key).\ntype ClaudeCLIConfig struct {\n\tCLIPath string `json:\"cli_path\" yaml:\"cli_path\"` // path to claude binary (default: \"claude\")\n\tModel string `json:\"model\" yaml:\"model\"` // default model alias (default: \"sonnet\")\n\tBaseWorkDir string `json:\"base_work_dir\" yaml:\"base_work_dir\"` // base dir for agent workspaces\n\tPermMode string `json:\"perm_mode\" yaml:\"perm_mode\"` // permission mode (default: \"bypassPermissions\")\n}\n\n// ACPConfig configures the ACP (Agent Client Protocol) provider.\n// Orchestrates any ACP-compatible coding agent (Claude Code, Codex CLI, Gemini CLI) as a subprocess.\ntype ACPConfig struct {\n\tBinary string `json:\"binary\"` // agent binary name or path (e.g. \"claude\", \"codex\")\n\tArgs []string `json:\"args\"` // extra spawn args\n\tModel string `json:\"model\"` // default model/agent name\n\tWorkDir string `json:\"work_dir\"` // base workspace dir\n\tIdleTTL string `json:\"idle_ttl\"` // process idle TTL (e.g. \"5m\")\n\tPermMode string `json:\"perm_mode\"` // \"approve-all\" (default), \"approve-reads\", \"deny-all\"\n}\n\ntype ProviderConfig struct {\n\tAPIKey string `json:\"api_key\"`\n\tAPIBase string `json:\"api_base,omitempty\"`\n}\n\n// APIBaseForType returns the config-level api_base for a given provider type.\n// Used as a fallback when DB providers have no api_base set.\nfunc (p *ProvidersConfig) APIBaseForType(providerType string) string {\n\tswitch providerType {\n\tcase \"anthropic_native\":\n\t\treturn p.Anthropic.APIBase\n\tcase \"openai\", \"openai_compat\":\n\t\treturn p.OpenAI.APIBase\n\tcase \"openrouter\":\n\t\treturn p.OpenRouter.APIBase\n\tcase \"groq\":\n\t\treturn p.Groq.APIBase\n\tcase \"deepseek\":\n\t\treturn p.DeepSeek.APIBase\n\tcase \"gemini_native\":\n\t\treturn p.Gemini.APIBase\n\tcase \"mistral\":\n\t\treturn p.Mistral.APIBase\n\tcase \"xai\":\n\t\treturn p.XAI.APIBase\n\tcase \"minimax_native\":\n\t\treturn p.MiniMax.APIBase\n\tcase \"cohere\":\n\t\treturn p.Cohere.APIBase\n\tcase \"perplexity\":\n\t\treturn p.Perplexity.APIBase\n\tcase \"dashscope\":\n\t\treturn p.DashScope.APIBase\n\tcase \"bailian\":\n\t\treturn p.Bailian.APIBase\n\tcase \"zai\":\n\t\treturn p.Zai.APIBase\n\tcase \"zai_coding\":\n\t\treturn p.ZaiCoding.APIBase\n\tcase \"ollama_cloud\":\n\t\treturn p.OllamaCloud.APIBase\n\tcase \"novita\":\n\t\treturn p.Novita.APIBase\n\tcase \"byteplus\":\n\t\treturn p.BytePlus.APIBase\n\tcase \"byteplus_coding\":\n\t\treturn p.BytePlusCoding.APIBase\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\n// HasAnyProvider returns true if at least one provider has an API key or CLI configured.\nfunc (c *Config) HasAnyProvider() bool {\n\tp := c.Providers\n\treturn p.Anthropic.APIKey != \"\" ||\n\t\tp.OpenAI.APIKey != \"\" ||\n\t\tp.OpenRouter.APIKey != \"\" ||\n\t\tp.Groq.APIKey != \"\" ||\n\t\tp.Gemini.APIKey != \"\" ||\n\t\tp.DeepSeek.APIKey != \"\" ||\n\t\tp.Mistral.APIKey != \"\" ||\n\t\tp.XAI.APIKey != \"\" ||\n\t\tp.MiniMax.APIKey != \"\" ||\n\t\tp.Cohere.APIKey != \"\" ||\n\t\tp.Perplexity.APIKey != \"\" ||\n\t\tp.DashScope.APIKey != \"\" ||\n\t\tp.Bailian.APIKey != \"\" ||\n\t\tp.Zai.APIKey != \"\" ||\n\t\tp.ZaiCoding.APIKey != \"\" ||\n\t\tp.Ollama.Host != \"\" ||\n\t\tp.OllamaCloud.APIKey != \"\" ||\n\t\tp.ClaudeCLI.CLIPath != \"\" ||\n\t\tp.ACP.Binary != \"\" ||\n\t\tp.Novita.APIKey != \"\" ||\n\t\tp.BytePlus.APIKey != \"\" ||\n\t\tp.BytePlusCoding.APIKey != \"\"\n}\n\n// QuotaWindow defines request limits per time window. Zero means unlimited.\ntype QuotaWindow struct {\n\tHour int `json:\"hour,omitempty\"` // max requests per hour (0 = unlimited)\n\tDay int `json:\"day,omitempty\"` // max requests per day (0 = unlimited)\n\tWeek int `json:\"week,omitempty\"` // max requests per week (0 = unlimited)\n}\n\n// IsZero returns true if no limits are set.\nfunc (w QuotaWindow) IsZero() bool { return w.Hour == 0 && w.Day == 0 && w.Week == 0 }\n\n// QuotaConfig configures per-user/group request quotas.\n// Config merge priority: Groups > Channels > Providers > Default.\ntype QuotaConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tDefault QuotaWindow `json:\"default\"`\n\tProviders map[string]QuotaWindow `json:\"providers,omitempty\"` // key = provider name (e.g. \"anthropic\")\n\tChannels map[string]QuotaWindow `json:\"channels,omitempty\"` // key = channel name (e.g. \"telegram\")\n\tGroups map[string]QuotaWindow `json:\"groups,omitempty\"` // key = userID (e.g. \"group:telegram:-100123\")\n}\n\n// GatewayConfig controls the gateway server.\ntype GatewayConfig struct {\n\tHost string `json:\"host\"`\n\tPort int `json:\"port\"`\n\tToken string `json:\"token,omitempty\"` // bearer token for WS/HTTP auth\n\tOwnerIDs []string `json:\"owner_ids,omitempty\"` // sender IDs considered \"owner\"\n\tAllowedOrigins []string `json:\"allowed_origins,omitempty\"` // WebSocket CORS whitelist (empty = allow all)\n\tMaxMessageChars int `json:\"max_message_chars,omitempty\"` // max user message characters (default 32000)\n\tRateLimitRPM int `json:\"rate_limit_rpm,omitempty\"` // rate limit: requests per minute per user (default 20, 0 = disabled)\n\tInjectionAction string `json:\"injection_action,omitempty\"` // prompt injection action: \"log\", \"warn\" (default), \"block\", \"off\"\n\tInboundDebounceMs int `json:\"inbound_debounce_ms,omitempty\"` // merge rapid messages from same sender (default 1000ms, -1 = disabled)\n\tQuota *QuotaConfig `json:\"quota,omitempty\"` // per-user/group request quotas\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // deliver intermediate text during tool iterations (default false)\n\tToolStatus *bool `json:\"tool_status,omitempty\"` // show tool name in streaming preview during tool execution (default true)\n\tTaskRecoveryIntervalSec int `json:\"task_recovery_interval_sec,omitempty\"` // team task recovery ticker interval in seconds (default 300 = 5min)\n\tBackgroundProvider string `json:\"background_provider,omitempty\"` // LLM provider for background workers (vault enrichment, consolidation)\n\tBackgroundModel string `json:\"background_model,omitempty\"` // LLM model for background workers\n}\n\n// ToolsConfig controls tool availability, policy, and web search.\ntype ToolsConfig struct {\n\tProfile string `json:\"profile,omitempty\"` // global profile: \"minimal\", \"coding\", \"messaging\", \"full\"\n\tAllow []string `json:\"allow,omitempty\"` // global allow list (tool names or \"group:xxx\")\n\tDeny []string `json:\"deny,omitempty\"` // global deny list\n\tAlsoAllow []string `json:\"alsoAllow,omitempty\"` // additive: adds without removing existing\n\tByProvider map[string]*ToolPolicySpec `json:\"byProvider,omitempty\"` // per-provider overrides\n\tShellDenyGroups map[string]bool `json:\"shellDenyGroups,omitempty\"` // global shell deny-group toggles (group name -> denied); per-agent overrides win per-key\n\tExecApproval ExecApprovalCfg `json:\"execApproval\"` // exec command approval settings\n\tWebFetch WebFetchPolicyConfig `json:\"web_fetch\"` // domain policy for URL fetching\n\tBrowser BrowserToolConfig `json:\"browser\"`\n\tRateLimitPerHour int `json:\"rate_limit_per_hour,omitempty\"` // max tool executions per hour per session (0 = disabled)\n\tScrubCredentials *bool `json:\"scrub_credentials,omitempty\"` // auto-redact API keys/tokens in tool output (default true)\n\tMcpServers map[string]*MCPServerConfig `json:\"mcp_servers,omitempty\"` // external MCP server connections\n}\n\n// MCPServerConfig configures a single external MCP server connection.\ntype MCPServerConfig struct {\n\tTransport string `json:\"transport\"` // \"stdio\", \"sse\", \"streamable-http\"\n\tCommand string `json:\"command,omitempty\"` // stdio: command to spawn\n\tArgs []string `json:\"args,omitempty\"` // stdio: command arguments\n\tEnv map[string]string `json:\"env,omitempty\"` // stdio: extra environment variables\n\tURL string `json:\"url,omitempty\"` // sse/http: server URL\n\tHeaders map[string]string `json:\"headers,omitempty\"` // sse/http: extra HTTP headers\n\tEnabled *bool `json:\"enabled,omitempty\"` // default true\n\tToolPrefix string `json:\"tool_prefix,omitempty\"` // prefix for tool names (avoids collisions)\n\tTimeoutSec int `json:\"timeout_sec,omitempty\"` // per-tool-call timeout in seconds (default 60)\n}\n\n// IsEnabled returns whether this MCP server is enabled (default true).\nfunc (c *MCPServerConfig) IsEnabled() bool {\n\treturn c.Enabled == nil || *c.Enabled\n}\n\n// ExecApprovalCfg configures command execution approval (matching TS exec-approval.ts).\ntype ExecApprovalCfg struct {\n\tSecurity string `json:\"security,omitempty\"` // \"deny\", \"allowlist\", \"full\" (default \"full\")\n\tAsk string `json:\"ask,omitempty\"` // \"off\", \"on-miss\", \"always\" (default \"off\")\n\tAllowlist []string `json:\"allowlist,omitempty\"` // glob patterns for allowed commands\n}\n\n// WebFetchPolicyConfig controls domain filtering for the web_fetch tool.\ntype WebFetchPolicyConfig struct {\n\tPolicy string `json:\"policy,omitempty\"` // \"allow_all\" (default), \"allowlist\"\n\tAllowedDomains []string `json:\"allowed_domains,omitempty\"` // e.g. [\"github.com\", \"*.example.com\"]\n\tBlockedDomains []string `json:\"blocked_domains,omitempty\"` // always checked regardless of policy\n}\n\n// BrowserToolConfig controls the browser automation tool.\ntype BrowserToolConfig struct {\n\tEnabled bool `json:\"enabled\"` // enable the browser tool (default false)\n\tHeadless bool `json:\"headless,omitempty\"` // run Chrome in headless mode (ignored when RemoteURL is set)\n\tRemoteURL string `json:\"remote_url,omitempty\"` // CDP endpoint for remote Chrome sidecar, e.g. \"ws://chrome:9222\"\n\tActionTimeoutMs int `json:\"action_timeout_ms,omitempty\"` // per-action timeout in ms (default 30000)\n\tIdleTimeoutMs int `json:\"idle_timeout_ms,omitempty\"` // idle page auto-close in ms (default 600000, 0=disabled)\n\tMaxPages int `json:\"max_pages,omitempty\"` // max open pages per tenant (default 5)\n}\n\n// ToolPolicySpec defines a tool policy at any level (global, per-agent, per-provider).\ntype ToolPolicySpec struct {\n\tProfile string `json:\"profile,omitempty\"`\n\tAllow []string `json:\"allow,omitempty\"`\n\tDeny []string `json:\"deny,omitempty\"`\n\tAlsoAllow []string `json:\"alsoAllow,omitempty\"`\n\tByProvider map[string]*ToolPolicySpec `json:\"byProvider,omitempty\"`\n\tToolCallPrefix string `json:\"toolCallPrefix,omitempty\"` // prefix to strip from model's tool call names before registry lookup\n}\n\n\n// SessionsConfig controls session behavior.\n// Matching TS src/config/sessions/types.ts + src/config/types.base.ts.\ntype SessionsConfig struct {\n\tScope string `json:\"scope,omitempty\"` // \"per-sender\" (default), \"global\"\n\tDmScope string `json:\"dm_scope,omitempty\"` // \"main\", \"per-peer\", \"per-channel-peer\" (default), \"per-account-channel-peer\"\n\tMainKey string `json:\"main_key,omitempty\"` // main session key suffix (default \"main\", used when dm_scope=\"main\")\n}\n\n// TtsConfig configures text-to-speech.\n// Matching TS src/config/types.tts.ts.\ntype TtsConfig struct {\n\tProvider string `json:\"provider,omitempty\"` // \"openai\", \"elevenlabs\", \"edge\", \"minimax\", \"gemini\"\n\tAuto string `json:\"auto,omitempty\"` // \"off\" (default), \"always\", \"inbound\", \"tagged\"\n\tMode string `json:\"mode,omitempty\"` // \"final\" (default), \"all\"\n\tMaxLength int `json:\"max_length,omitempty\"` // max text length before truncation (default 1500)\n\tTimeoutMs int `json:\"timeout_ms,omitempty\"` // API timeout in ms (default 30000)\n\tOpenAI TtsOpenAIConfig `json:\"openai\"`\n\tElevenLabs TtsElevenLabsConfig `json:\"elevenlabs\"`\n\tEdge TtsEdgeConfig `json:\"edge\"`\n\tMiniMax TtsMiniMaxConfig `json:\"minimax\"`\n\tGemini TtsGeminiConfig `json:\"gemini\"`\n}\n\n// TtsGeminiConfig configures the Google Gemini TTS provider.\ntype TtsGeminiConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"` // required; encrypted at rest\n\tAPIBase string `json:\"api_base,omitempty\"` // custom endpoint (optional; SSRF-gated)\n\tVoice string `json:\"voice,omitempty\"` // default \"Kore\"\n\tModel string `json:\"model,omitempty\"` // default \"gemini-2.5-flash-preview-tts\"\n\tSpeakers string `json:\"speakers,omitempty\"` // JSON-encoded []SpeakerVoice for multi-speaker mode\n}\n\n// TtsOpenAIConfig configures the OpenAI TTS provider.\ntype TtsOpenAIConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"`\n\tAPIBase string `json:\"api_base,omitempty\"` // custom endpoint URL\n\tModel string `json:\"model,omitempty\"` // default \"gpt-4o-mini-tts\"\n\tVoice string `json:\"voice,omitempty\"` // default \"alloy\"\n}\n\n// TtsElevenLabsConfig configures the ElevenLabs TTS provider.\ntype TtsElevenLabsConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"`\n\tBaseURL string `json:\"base_url,omitempty\"`\n\tVoiceID string `json:\"voice_id,omitempty\"` // default \"pMsXgVXv3BLzUgSXRplE\"\n\tModelID string `json:\"model_id,omitempty\"` // default \"eleven_multilingual_v2\"\n}\n\n// TtsEdgeConfig configures the Microsoft Edge TTS provider (free, no API key).\ntype TtsEdgeConfig struct {\n\tEnabled bool `json:\"enabled,omitempty\"`\n\tVoice string `json:\"voice,omitempty\"` // default \"en-US-MichelleNeural\"\n\tRate string `json:\"rate,omitempty\"` // speech rate, e.g. \"+0%\"\n}\n\n// TtsMiniMaxConfig configures the MiniMax TTS provider.\ntype TtsMiniMaxConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"`\n\tGroupID string `json:\"group_id,omitempty\"` // MiniMax GroupId (required)\n\tAPIBase string `json:\"api_base,omitempty\"` // default \"https://api.minimax.io/v1\"\n\tModel string `json:\"model,omitempty\"` // default \"speech-02-hd\"\n\tVoiceID string `json:\"voice_id,omitempty\"` // default \"Wise_Woman\"\n}\n\n// MergeChannelGroupQuotas merges per-group quota overrides from channel configs\n// (e.g., channels.telegram.groups[chatID].quota) into gateway.quota.groups.\n// This allows per-group quotas to be set at the channel level and picked up\n// by the QuotaChecker at runtime.\nfunc MergeChannelGroupQuotas(cfg *Config) {\n\tif cfg.Gateway.Quota == nil {\n\t\treturn\n\t}\n\tif cfg.Gateway.Quota.Groups == nil {\n\t\tcfg.Gateway.Quota.Groups = make(map[string]QuotaWindow)\n\t}\n\n\t// Telegram per-group quotas\n\tfor chatID, groupCfg := range cfg.Channels.Telegram.Groups {\n\t\tif groupCfg != nil && groupCfg.Quota != nil && !groupCfg.Quota.IsZero() {\n\t\t\tkey := \"group:telegram:\" + chatID\n\t\t\tcfg.Gateway.Quota.Groups[key] = *groupCfg.Quota\n\t\t}\n\t}\n}\n"}, "613b6e38d77adf1603ace7b3bf6c3abe7241d6f3": {"t": 1785759246.5675437, "n": 32447, "h": "0c370759ce966174", "c": "package config\n\n// PendingCompactionConfig configures LLM-based compaction of pending group messages.\n// When a group accumulates more than Threshold pending messages, older messages are\n// summarized by an LLM and replaced with a compact summary, keeping KeepRecent raw messages.\ntype PendingCompactionConfig struct {\n\tThreshold int `json:\"threshold,omitempty\"` // trigger compaction when entries exceed this (default 200)\n\tKeepRecent int `json:\"keep_recent,omitempty\"` // keep this many recent raw messages after compaction (default 40)\n\tMaxTokens int `json:\"max_tokens,omitempty\"` // max output tokens for LLM summarization (default 4096)\n\tProvider string `json:\"provider,omitempty\"` // LLM provider name (e.g. \"openai\"); empty = use agent's provider\n\tModel string `json:\"model,omitempty\"` // model for summarization; empty = use agent's model\n}\n\n// ChannelsConfig contains per-channel configuration.\ntype ChannelsConfig struct {\n\tTelegram TelegramConfig `json:\"telegram\"`\n\tDiscord DiscordConfig `json:\"discord\"`\n\tSlack SlackConfig `json:\"slack\"`\n\tWhatsApp WhatsAppConfig `json:\"whatsapp\"`\n\tZalo ZaloConfig `json:\"zalo\"`\n\tZaloPersonal ZaloPersonalConfig `json:\"zalo_personal\"`\n\tFeishu FeishuConfig `json:\"feishu\"`\n\tPendingCompaction *PendingCompactionConfig `json:\"pending_compaction,omitempty\"` // global pending message compaction settings\n}\n\ntype TelegramConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tToken string `json:\"token\"`\n\tProxy string `json:\"proxy,omitempty\"`\n\tAPIServer string `json:\"api_server,omitempty\"` // custom Telegram Bot API server URL (e.g. \"http://localhost:8081\")\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in groups (default true)\n\tMentionMode string `json:\"mention_mode,omitempty\"` // \"strict\" (default) = only respond when mentioned; \"yield\" = respond unless another bot is mentioned\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tDMStream *bool `json:\"dm_stream,omitempty\"` // enable streaming for DMs (default false) — edits placeholder progressively\n\tGroupStream *bool `json:\"group_stream,omitempty\"` // enable streaming for groups (default false) — sends new message, edits progressively\n\tDraftTransport *bool `json:\"draft_transport,omitempty\"` // use sendMessageDraft for DM streaming (default true) — stealth preview, no notifications per edit\n\tReasoningStream *bool `json:\"reasoning_stream,omitempty\"` // show reasoning as separate message when provider emits thinking events (default true)\n\tReactionLevel string `json:\"reaction_level,omitempty\"` // \"off\" (default), \"minimal\", \"full\" — status emoji reactions\n\tMediaMaxBytes int64 `json:\"media_max_bytes,omitempty\"` // max media download size in bytes (default 20MB)\n\tLinkPreview *bool `json:\"link_preview,omitempty\"` // enable URL previews in messages (default true)\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tForceIPv4 bool `json:\"force_ipv4,omitempty\"` // force IPv4 for all Telegram API requests (use when IPv6 routing is broken)\n\n\t// Optional STT (Speech-to-Text) pipeline for voice/audio inbound messages.\n\t// When stt_proxy_url is set, audio/voice messages are transcribed before being forwarded to the agent.\n\tSTTProxyURL string `json:\"stt_proxy_url,omitempty\"` // base URL of the STT proxy service (e.g. \"https://stt.example.com\")\n\tSTTAPIKey string `json:\"stt_api_key,omitempty\"` // Bearer token for the STT proxy\n\tSTTTenantID string `json:\"stt_tenant_id,omitempty\"` // optional tenant/org identifier forwarded to the STT proxy\n\tSTTTimeoutSeconds int `json:\"stt_timeout_seconds,omitempty\"` // per-request timeout for STT calls (default 30s)\n\n\t// Optional audio-aware routing: when set, voice/audio inbound messages are routed to this\n\t// agent instead of the default channel agent. Requires the named agent to exist in the config.\n\tVoiceAgentID string `json:\"voice_agent_id,omitempty\"` // agent ID to route voice inbound to (e.g. \"speaking-agent\")\n\n\t// Audio guard: intercept technical errors in voice agent replies and replace with friendly fallbacks.\n\t// Only active when VoiceAgentID is set. Custom error markers replace built-in defaults when provided.\n\tAudioGuardFallbackTranscript string `json:\"audio_guard_fallback_transcript,omitempty\"` // fallback with %s for transcript (e.g. \"I heard: \\\"%s\\\". Try again!\")\n\tAudioGuardFallbackNoTranscript string `json:\"audio_guard_fallback_no_transcript,omitempty\"` // fallback when no transcript available\n\tAudioGuardErrorMarkers []string `json:\"audio_guard_error_markers,omitempty\"` // custom error detection markers (replaces defaults)\n\n\t// Per-group (and per-topic) overrides. Key is chat ID string (e.g. \"-100123456\") or \"*\" for wildcard.\n\t// TS ref: channels.telegram.groups in src/config/types.telegram.ts.\n\tGroups map[string]*TelegramGroupConfig `json:\"groups,omitempty\"`\n}\n\n// TelegramGroupConfig defines per-group overrides for a Telegram channel.\n// Matching TS TelegramGroupConfig in src/config/types.telegram.ts.\ntype TelegramGroupConfig struct {\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // override group policy for this group\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // override require_mention for this group\n\tMentionMode string `json:\"mention_mode,omitempty\"` // override mention_mode for this group\n\tAllowFrom FlexibleStringSlice `json:\"allow_from,omitempty\"` // override allow_from for this group\n\tEnabled *bool `json:\"enabled,omitempty\"` // disable bot for this group (default: true)\n\tSkills []string `json:\"skills,omitempty\"` // skill whitelist (nil = all, [] = none)\n\tTools []string `json:\"tools,omitempty\"` // tool allow list (nil = all, supports \"group:xxx\")\n\tSystemPrompt string `json:\"system_prompt,omitempty\"` // extra system prompt for this group\n\tTopics map[string]*TelegramTopicConfig `json:\"topics,omitempty\"` // per-topic overrides (key: thread ID string)\n\tQuota *QuotaWindow `json:\"quota,omitempty\"` // per-group quota override\n}\n\n// TelegramTopicConfig defines per-topic overrides within a Telegram group.\n// Matching TS TelegramTopicConfig in src/config/types.telegram.ts.\ntype TelegramTopicConfig struct {\n\tRequireMention *bool `json:\"require_mention,omitempty\"`\n\tMentionMode string `json:\"mention_mode,omitempty\"`\n\tGroupPolicy string `json:\"group_policy,omitempty\"`\n\tSkills []string `json:\"skills,omitempty\"`\n\tTools []string `json:\"tools,omitempty\"` // tool allow list (nil = inherit, supports \"group:xxx\")\n\tEnabled *bool `json:\"enabled,omitempty\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from,omitempty\"`\n\tSystemPrompt string `json:\"system_prompt,omitempty\"`\n}\n\ntype DiscordConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tToken string `json:\"token\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in groups (default true)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tMediaMaxBytes int64 `json:\"media_max_bytes,omitempty\"` // max media download size (default 25MB)\n\tSTTProxyURL string `json:\"stt_proxy_url,omitempty\"`\n\tSTTAPIKey string `json:\"stt_api_key,omitempty\"`\n\tSTTTenantID string `json:\"stt_tenant_id,omitempty\"`\n\tSTTTimeoutSeconds int `json:\"stt_timeout_seconds,omitempty\"`\n\tVoiceAgentID string `json:\"voice_agent_id,omitempty\"`\n}\n\ntype SlackConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tBotToken string `json:\"bot_token\"` // xoxb-... (Bot User OAuth Token)\n\tAppToken string `json:\"app_token\"` // xapp-... (App-Level Token for Socket Mode)\n\tUserToken string `json:\"user_token,omitempty\"` // xoxp-... (Optional: custom bot identity)\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"pairing\", \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in channels (default true)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tDMStream *bool `json:\"dm_stream,omitempty\"` // enable streaming for DMs (default false)\n\tGroupStream *bool `json:\"group_stream,omitempty\"` // enable streaming for groups (default false)\n\tNativeStream *bool `json:\"native_stream,omitempty\"` // use Slack ChatStreamer API if available (default false)\n\tReactionLevel string `json:\"reaction_level,omitempty\"` // \"off\" (default), \"minimal\", \"full\"\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tDebounceDelay int `json:\"debounce_delay,omitempty\"` // ms delay before dispatching rapid messages (default 300, 0=disabled)\n\tThreadTTL *int `json:\"thread_ttl,omitempty\"` // hours before thread participation expires (default 24, 0=disabled — always require @mention)\n\tMediaMaxBytes int64 `json:\"media_max_bytes,omitempty\"` // max file download size in bytes (default 20MB)\n}\n\ntype WhatsAppConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tAuthDir string `json:\"auth_dir,omitempty\"` // optional: SQLite auth dir override (desktop)\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default for DB instances), \"open\", \"allowlist\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"pairing\" (default for DB instances), \"open\" (default for config), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // only respond in groups when bot is @mentioned (default false)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 200, 0=disabled)\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n}\n\ntype ZaloConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tToken string `json:\"token\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tWebhookURL string `json:\"webhook_url,omitempty\"`\n\tWebhookSecret string `json:\"webhook_secret,omitempty\"`\n\tMediaMaxMB int `json:\"media_max_mb,omitempty\"` // default 5\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n}\n\ntype ZaloPersonalConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in groups (default true)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tCredentialsPath string `json:\"credentials_path,omitempty\"` // path to saved cookies JSON\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n}\n\ntype FeishuConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tAppID string `json:\"app_id\"`\n\tAppSecret string `json:\"app_secret\"`\n\tEncryptKey string `json:\"encrypt_key,omitempty\"`\n\tVerificationToken string `json:\"verification_token,omitempty\"`\n\tDomain string `json:\"domain,omitempty\"` // \"lark\" (default/global), \"feishu\" (China), or custom URL\n\tConnectionMode string `json:\"connection_mode,omitempty\"` // \"websocket\" (default), \"webhook\"\n\tWebhookPort int `json:\"webhook_port,omitempty\"` // default 3000\n\tWebhookPath string `json:\"webhook_path,omitempty\"` // default \"/feishu/events\"\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default)\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default)\n\tGroupAllowFrom FlexibleStringSlice `json:\"group_allow_from,omitempty\"`\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // default true (groups)\n\tTopicSessionMode string `json:\"topic_session_mode,omitempty\"` // \"disabled\" (default)\n\tTextChunkLimit int `json:\"text_chunk_limit,omitempty\"` // default 4000\n\tMediaMaxMB int `json:\"media_max_mb,omitempty\"` // default 30\n\tRenderMode string `json:\"render_mode,omitempty\"` // \"auto\", \"raw\", \"card\"\n\tStreaming *bool `json:\"streaming,omitempty\"` // default true\n\tReactionLevel string `json:\"reaction_level,omitempty\"` // \"off\" (default), \"minimal\", \"full\" — typing emoji reactions\n\tHistoryLimit int `json:\"history_limit,omitempty\"`\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tSTTProxyURL string `json:\"stt_proxy_url,omitempty\"`\n\tSTTAPIKey string `json:\"stt_api_key,omitempty\"`\n\tSTTTenantID string `json:\"stt_tenant_id,omitempty\"`\n\tSTTTimeoutSeconds int `json:\"stt_timeout_seconds,omitempty\"`\n\tVoiceAgentID string `json:\"voice_agent_id,omitempty\"`\n}\n\n// ProvidersConfig maps provider name to its config.\ntype ProvidersConfig struct {\n\tAnthropic ProviderConfig `json:\"anthropic\"`\n\tOpenAI ProviderConfig `json:\"openai\"`\n\tOpenRouter ProviderConfig `json:\"openrouter\"`\n\tGroq ProviderConfig `json:\"groq\"`\n\tGemini ProviderConfig `json:\"gemini\"`\n\tDeepSeek ProviderConfig `json:\"deepseek\"`\n\tMistral ProviderConfig `json:\"mistral\"`\n\tXAI ProviderConfig `json:\"xai\"`\n\tMiniMax ProviderConfig `json:\"minimax\"`\n\tCohere ProviderConfig `json:\"cohere\"`\n\tPerplexity ProviderConfig `json:\"perplexity\"`\n\tDashScope ProviderConfig `json:\"dashscope\"`\n\tBailian ProviderConfig `json:\"bailian\"`\n\tZai ProviderConfig `json:\"zai\"`\n\tZaiCoding ProviderConfig `json:\"zai_coding\"`\n\tOllama OllamaConfig `json:\"ollama\"` // local Ollama instance (no API key needed)\n\tOllamaCloud ProviderConfig `json:\"ollama_cloud\"` // Ollama Cloud (API key required)\n\tClaudeCLI ClaudeCLIConfig `json:\"claude_cli\"`\n\tACP ACPConfig `json:\"acp\"`\n\tNovita ProviderConfig `json:\"novita\"` // Novita AI (OpenAI-compatible endpoint)\n\tBytePlus ProviderConfig `json:\"byteplus\"` // BytePlus ModelArk (Seed 2.0)\n\tBytePlusCoding ProviderConfig `json:\"byteplus_coding\"` // BytePlus ModelArk Coding Plan\n}\n\n// OllamaConfig configures a local (or self-hosted) Ollama instance.\n// No API key is required — Ollama accepts any Bearer token value.\ntype OllamaConfig struct {\n\tHost string `json:\"host\"` // Ollama server base URL, e.g. http://localhost:11434\n}\n\n// ClaudeCLIConfig configures the Claude CLI provider (uses subscription, not API key).\ntype ClaudeCLIConfig struct {\n\tCLIPath string `json:\"cli_path\" yaml:\"cli_path\"` // path to claude binary (default: \"claude\")\n\tModel string `json:\"model\" yaml:\"model\"` // default model alias (default: \"sonnet\")\n\tBaseWorkDir string `json:\"base_work_dir\" yaml:\"base_work_dir\"` // base dir for agent workspaces\n\tPermMode string `json:\"perm_mode\" yaml:\"perm_mode\"` // permission mode (default: \"bypassPermissions\")\n}\n\n// ACPConfig configures the ACP (Agent Client Protocol) provider.\n// Orchestrates any ACP-compatible coding agent (Claude Code, Codex CLI, Gemini CLI) as a subprocess.\ntype ACPConfig struct {\n\tBinary string `json:\"binary\"` // agent binary name or path (e.g. \"claude\", \"codex\")\n\tArgs []string `json:\"args\"` // extra spawn args\n\tModel string `json:\"model\"` // default model/agent name\n\tWorkDir string `json:\"work_dir\"` // base workspace dir\n\tIdleTTL string `json:\"idle_ttl\"` // process idle TTL (e.g. \"5m\")\n\tPermMode string `json:\"perm_mode\"` // \"approve-all\" (default), \"approve-reads\", \"deny-all\"\n}\n\ntype ProviderConfig struct {\n\tAPIKey string `json:\"api_key\"`\n\tAPIBase string `json:\"api_base,omitempty\"`\n}\n\n// APIBaseForType returns the config-level api_base for a given provider type.\n// Used as a fallback when DB providers have no api_base set.\nfunc (p *ProvidersConfig) APIBaseForType(providerType string) string {\n\tswitch providerType {\n\tcase \"anthropic_native\":\n\t\treturn p.Anthropic.APIBase\n\tcase \"openai\", \"openai_compat\":\n\t\treturn p.OpenAI.APIBase\n\tcase \"openrouter\":\n\t\treturn p.OpenRouter.APIBase\n\tcase \"groq\":\n\t\treturn p.Groq.APIBase\n\tcase \"deepseek\":\n\t\treturn p.DeepSeek.APIBase\n\tcase \"gemini_native\":\n\t\treturn p.Gemini.APIBase\n\tcase \"mistral\":\n\t\treturn p.Mistral.APIBase\n\tcase \"xai\":\n\t\treturn p.XAI.APIBase\n\tcase \"minimax_native\":\n\t\treturn p.MiniMax.APIBase\n\tcase \"cohere\":\n\t\treturn p.Cohere.APIBase\n\tcase \"perplexity\":\n\t\treturn p.Perplexity.APIBase\n\tcase \"dashscope\":\n\t\treturn p.DashScope.APIBase\n\tcase \"bailian\":\n\t\treturn p.Bailian.APIBase\n\tcase \"zai\":\n\t\treturn p.Zai.APIBase\n\tcase \"zai_coding\":\n\t\treturn p.ZaiCoding.APIBase\n\tcase \"ollama_cloud\":\n\t\treturn p.OllamaCloud.APIBase\n\tcase \"novita\":\n\t\treturn p.Novita.APIBase\n\tcase \"byteplus\":\n\t\treturn p.BytePlus.APIBase\n\tcase \"byteplus_coding\":\n\t\treturn p.BytePlusCoding.APIBase\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\n// HasAnyProvider returns true if at least one provider has an API key or CLI configured.\nfunc (c *Config) HasAnyProvider() bool {\n\tp := c.Providers\n\treturn p.Anthropic.APIKey != \"\" ||\n\t\tp.OpenAI.APIKey != \"\" ||\n\t\tp.OpenRouter.APIKey != \"\" ||\n\t\tp.Groq.APIKey != \"\" ||\n\t\tp.Gemini.APIKey != \"\" ||\n\t\tp.DeepSeek.APIKey != \"\" ||\n\t\tp.Mistral.APIKey != \"\" ||\n\t\tp.XAI.APIKey != \"\" ||\n\t\tp.MiniMax.APIKey != \"\" ||\n\t\tp.Cohere.APIKey != \"\" ||\n\t\tp.Perplexity.APIKey != \"\" ||\n\t\tp.DashScope.APIKey != \"\" ||\n\t\tp.Bailian.APIKey != \"\" ||\n\t\tp.Zai.APIKey != \"\" ||\n\t\tp.ZaiCoding.APIKey != \"\" ||\n\t\tp.Ollama.Host != \"\" ||\n\t\tp.OllamaCloud.APIKey != \"\" ||\n\t\tp.ClaudeCLI.CLIPath != \"\" ||\n\t\tp.ACP.Binary != \"\" ||\n\t\tp.Novita.APIKey != \"\" ||\n\t\tp.BytePlus.APIKey != \"\" ||\n\t\tp.BytePlusCoding.APIKey != \"\"\n}\n\n// QuotaWindow defines request limits per time window. Zero means unlimited.\ntype QuotaWindow struct {\n\tHour int `json:\"hour,omitempty\"` // max requests per hour (0 = unlimited)\n\tDay int `json:\"day,omitempty\"` // max requests per day (0 = unlimited)\n\tWeek int `json:\"week,omitempty\"` // max requests per week (0 = unlimited)\n}\n\n// IsZero returns true if no limits are set.\nfunc (w QuotaWindow) IsZero() bool { return w.Hour == 0 && w.Day == 0 && w.Week == 0 }\n\n// QuotaConfig configures per-user/group request quotas.\n// Config merge priority: Groups > Channels > Providers > Default.\ntype QuotaConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tDefault QuotaWindow `json:\"default\"`\n\tProviders map[string]QuotaWindow `json:\"providers,omitempty\"` // key = provider name (e.g. \"anthropic\")\n\tChannels map[string]QuotaWindow `json:\"channels,omitempty\"` // key = channel name (e.g. \"telegram\")\n\tGroups map[string]QuotaWindow `json:\"groups,omitempty\"` // key = userID (e.g. \"group:telegram:-100123\")\n}\n\n// GatewayConfig controls the gateway server.\ntype GatewayConfig struct {\n\tHost string `json:\"host\"`\n\tPort int `json:\"port\"`\n\tToken string `json:\"token,omitempty\"` // bearer token for WS/HTTP auth\n\tOwnerIDs []string `json:\"owner_ids,omitempty\"` // sender IDs considered \"owner\"\n\tAllowedOrigins []string `json:\"allowed_origins,omitempty\"` // WebSocket CORS whitelist (empty = allow all)\n\tMaxMessageChars int `json:\"max_message_chars,omitempty\"` // max user message characters (default 32000)\n\tRateLimitRPM int `json:\"rate_limit_rpm,omitempty\"` // rate limit: requests per minute per user (default 20, 0 = disabled)\n\tInjectionAction string `json:\"injection_action,omitempty\"` // prompt injection action: \"log\", \"warn\" (default), \"block\", \"off\"\n\tInboundDebounceMs int `json:\"inbound_debounce_ms,omitempty\"` // merge rapid messages from same sender (default 1000ms, -1 = disabled)\n\tQuota *QuotaConfig `json:\"quota,omitempty\"` // per-user/group request quotas\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // deliver intermediate text during tool iterations (default false)\n\tToolStatus *bool `json:\"tool_status,omitempty\"` // show tool name in streaming preview during tool execution (default true)\n\tTaskRecoveryIntervalSec int `json:\"task_recovery_interval_sec,omitempty\"` // team task recovery ticker interval in seconds (default 300 = 5min)\n\tBackgroundProvider string `json:\"background_provider,omitempty\"` // LLM provider for background workers (vault enrichment, consolidation)\n\tBackgroundModel string `json:\"background_model,omitempty\"` // LLM model for background workers\n}\n\n// ToolsConfig controls tool availability, policy, and web search.\ntype ToolsConfig struct {\n\tProfile string `json:\"profile,omitempty\"` // global profile: \"minimal\", \"coding\", \"messaging\", \"full\"\n\tAllow []string `json:\"allow,omitempty\"` // global allow list (tool names or \"group:xxx\")\n\tDeny []string `json:\"deny,omitempty\"` // global deny list\n\tAlsoAllow []string `json:\"alsoAllow,omitempty\"` // additive: adds without removing existing\n\tByProvider map[string]*ToolPolicySpec `json:\"byProvider,omitempty\"` // per-provider overrides\n\tExecApproval ExecApprovalCfg `json:\"execApproval\"` // exec command approval settings\n\tWebFetch WebFetchPolicyConfig `json:\"web_fetch\"` // domain policy for URL fetching\n\tBrowser BrowserToolConfig `json:\"browser\"`\n\tRateLimitPerHour int `json:\"rate_limit_per_hour,omitempty\"` // max tool executions per hour per session (0 = disabled)\n\tScrubCredentials *bool `json:\"scrub_credentials,omitempty\"` // auto-redact API keys/tokens in tool output (default true)\n\tMcpServers map[string]*MCPServerConfig `json:\"mcp_servers,omitempty\"` // external MCP server connections\n}\n\n// MCPServerConfig configures a single external MCP server connection.\ntype MCPServerConfig struct {\n\tTransport string `json:\"transport\"` // \"stdio\", \"sse\", \"streamable-http\"\n\tCommand string `json:\"command,omitempty\"` // stdio: command to spawn\n\tArgs []string `json:\"args,omitempty\"` // stdio: command arguments\n\tEnv map[string]string `json:\"env,omitempty\"` // stdio: extra environment variables\n\tURL string `json:\"url,omitempty\"` // sse/http: server URL\n\tHeaders map[string]string `json:\"headers,omitempty\"` // sse/http: extra HTTP headers\n\tEnabled *bool `json:\"enabled,omitempty\"` // default true\n\tToolPrefix string `json:\"tool_prefix,omitempty\"` // prefix for tool names (avoids collisions)\n\tTimeoutSec int `json:\"timeout_sec,omitempty\"` // per-tool-call timeout in seconds (default 60)\n}\n\n// IsEnabled returns whether this MCP server is enabled (default true).\nfunc (c *MCPServerConfig) IsEnabled() bool {\n\treturn c.Enabled == nil || *c.Enabled\n}\n\n// ExecApprovalCfg configures command execution approval (matching TS exec-approval.ts).\ntype ExecApprovalCfg struct {\n\tSecurity string `json:\"security,omitempty\"` // \"deny\", \"allowlist\", \"full\" (default \"full\")\n\tAsk string `json:\"ask,omitempty\"` // \"off\", \"on-miss\", \"always\" (default \"off\")\n\tAllowlist []string `json:\"allowlist,omitempty\"` // glob patterns for allowed commands\n}\n\n// WebFetchPolicyConfig controls domain filtering for the web_fetch tool.\ntype WebFetchPolicyConfig struct {\n\tPolicy string `json:\"policy,omitempty\"` // \"allow_all\" (default), \"allowlist\"\n\tAllowedDomains []string `json:\"allowed_domains,omitempty\"` // e.g. [\"github.com\", \"*.example.com\"]\n\tBlockedDomains []string `json:\"blocked_domains,omitempty\"` // always checked regardless of policy\n}\n\n// BrowserToolConfig controls the browser automation tool.\ntype BrowserToolConfig struct {\n\tEnabled bool `json:\"enabled\"` // enable the browser tool (default false)\n\tHeadless bool `json:\"headless,omitempty\"` // run Chrome in headless mode (ignored when RemoteURL is set)\n\tRemoteURL string `json:\"remote_url,omitempty\"` // CDP endpoint for remote Chrome sidecar, e.g. \"ws://chrome:9222\"\n\tActionTimeoutMs int `json:\"action_timeout_ms,omitempty\"` // per-action timeout in ms (default 30000)\n\tIdleTimeoutMs int `json:\"idle_timeout_ms,omitempty\"` // idle page auto-close in ms (default 600000, 0=disabled)\n\tMaxPages int `json:\"max_pages,omitempty\"` // max open pages per tenant (default 5)\n}\n\n// ToolPolicySpec defines a tool policy at any level (global, per-agent, per-provider).\ntype ToolPolicySpec struct {\n\tProfile string `json:\"profile,omitempty\"`\n\tAllow []string `json:\"allow,omitempty\"`\n\tDeny []string `json:\"deny,omitempty\"`\n\tAlsoAllow []string `json:\"alsoAllow,omitempty\"`\n\tByProvider map[string]*ToolPolicySpec `json:\"byProvider,omitempty\"`\n\tToolCallPrefix string `json:\"toolCallPrefix,omitempty\"` // prefix to strip from model's tool call names before registry lookup\n}\n\n\n// SessionsConfig controls session behavior.\n// Matching TS src/config/sessions/types.ts + src/config/types.base.ts.\ntype SessionsConfig struct {\n\tScope string `json:\"scope,omitempty\"` // \"per-sender\" (default), \"global\"\n\tDmScope string `json:\"dm_scope,omitempty\"` // \"main\", \"per-peer\", \"per-channel-peer\" (default), \"per-account-channel-peer\"\n\tMainKey string `json:\"main_key,omitempty\"` // main session key suffix (default \"main\", used when dm_scope=\"main\")\n}\n\n// TtsConfig configures text-to-speech.\n// Matching TS src/config/types.tts.ts.\ntype TtsConfig struct {\n\tProvider string `json:\"provider,omitempty\"` // \"openai\", \"elevenlabs\", \"edge\", \"minimax\", \"gemini\"\n\tAuto string `json:\"auto,omitempty\"` // \"off\" (default), \"always\", \"inbound\", \"tagged\"\n\tMode string `json:\"mode,omitempty\"` // \"final\" (default), \"all\"\n\tMaxLength int `json:\"max_length,omitempty\"` // max text length before truncation (default 1500)\n\tTimeoutMs int `json:\"timeout_ms,omitempty\"` // API timeout in ms (default 30000)\n\tOpenAI TtsOpenAIConfig `json:\"openai\"`\n\tElevenLabs TtsElevenLabsConfig `json:\"elevenlabs\"`\n\tEdge TtsEdgeConfig `json:\"edge\"`\n\tMiniMax TtsMiniMaxConfig `json:\"minimax\"`\n\tGemini TtsGeminiConfig `json:\"gemini\"`\n}\n\n// TtsGeminiConfig configures the Google Gemini TTS provider.\ntype TtsGeminiConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"` // required; encrypted at rest\n\tAPIBase string `json:\"api_base,omitempty\"` // custom endpoint (optional; SSRF-gated)\n\tVoice string `json:\"voice,omitempty\"` // default \"Kore\"\n\tModel string `json:\"model,omitempty\"` // default \"gemini-2.5-flash-preview-tts\"\n\tSpeakers string `json:\"speakers,omitempty\"` // JSON-encoded []SpeakerVoice for multi-speaker mode\n}\n\n// TtsOpenAIConfig configures the OpenAI TTS provider.\ntype TtsOpenAIConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"`\n\tAPIBase string `json:\"api_base,omitempty\"` // custom endpoint URL\n\tModel string `json:\"model,omitempty\"` // default \"gpt-4o-mini-tts\"\n\tVoice string `json:\"voice,omitempty\"` // default \"alloy\"\n}\n\n// TtsElevenLabsConfig configures the ElevenLabs TTS provider.\ntype TtsElevenLabsConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"`\n\tBaseURL string `json:\"base_url,omitempty\"`\n\tVoiceID string `json:\"voice_id,omitempty\"` // default \"pMsXgVXv3BLzUgSXRplE\"\n\tModelID string `json:\"model_id,omitempty\"` // default \"eleven_multilingual_v2\"\n}\n\n// TtsEdgeConfig configures the Microsoft Edge TTS provider (free, no API key).\ntype TtsEdgeConfig struct {\n\tEnabled bool `json:\"enabled,omitempty\"`\n\tVoice string `json:\"voice,omitempty\"` // default \"en-US-MichelleNeural\"\n\tRate string `json:\"rate,omitempty\"` // speech rate, e.g. \"+0%\"\n}\n\n// TtsMiniMaxConfig configures the MiniMax TTS provider.\ntype TtsMiniMaxConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"`\n\tGroupID string `json:\"group_id,omitempty\"` // MiniMax GroupId (required)\n\tAPIBase string `json:\"api_base,omitempty\"` // default \"https://api.minimax.io/v1\"\n\tModel string `json:\"model,omitempty\"` // default \"speech-02-hd\"\n\tVoiceID string `json:\"voice_id,omitempty\"` // default \"Wise_Woman\"\n}\n\n// MergeChannelGroupQuotas merges per-group quota overrides from channel configs\n// (e.g., channels.telegram.groups[chatID].quota) into gateway.quota.groups.\n// This allows per-group quotas to be set at the channel level and picked up\n// by the QuotaChecker at runtime.\nfunc MergeChannelGroupQuotas(cfg *Config) {\n\tif cfg.Gateway.Quota == nil {\n\t\treturn\n\t}\n\tif cfg.Gateway.Quota.Groups == nil {\n\t\tcfg.Gateway.Quota.Groups = make(map[string]QuotaWindow)\n\t}\n\n\t// Telegram per-group quotas\n\tfor chatID, groupCfg := range cfg.Channels.Telegram.Groups {\n\t\tif groupCfg != nil && groupCfg.Quota != nil && !groupCfg.Quota.IsZero() {\n\t\t\tkey := \"group:telegram:\" + chatID\n\t\t\tcfg.Gateway.Quota.Groups[key] = *groupCfg.Quota\n\t\t}\n\t}\n}\n"}, "1af7c2bb2af04c2137b46ce87b4b81a118abf38a": {"t": 1785759248.1838148, "n": 31882, "h": "db3039695962b454", "c": "package config\n\n// PendingCompactionConfig configures LLM-based compaction of pending group messages.\n// When a group accumulates more than Threshold pending messages, older messages are\n// summarized by an LLM and replaced with a compact summary, keeping KeepRecent raw messages.\ntype PendingCompactionConfig struct {\n\tThreshold int `json:\"threshold,omitempty\"` // trigger compaction when entries exceed this (default 200)\n\tKeepRecent int `json:\"keep_recent,omitempty\"` // keep this many recent raw messages after compaction (default 40)\n\tMaxTokens int `json:\"max_tokens,omitempty\"` // max output tokens for LLM summarization (default 4096)\n\tProvider string `json:\"provider,omitempty\"` // LLM provider name (e.g. \"openai\"); empty = use agent's provider\n\tModel string `json:\"model,omitempty\"` // model for summarization; empty = use agent's model\n}\n\n// ChannelsConfig contains per-channel configuration.\ntype ChannelsConfig struct {\n\tTelegram TelegramConfig `json:\"telegram\"`\n\tDiscord DiscordConfig `json:\"discord\"`\n\tSlack SlackConfig `json:\"slack\"`\n\tWhatsApp WhatsAppConfig `json:\"whatsapp\"`\n\tZalo ZaloConfig `json:\"zalo\"`\n\tZaloPersonal ZaloPersonalConfig `json:\"zalo_personal\"`\n\tFeishu FeishuConfig `json:\"feishu\"`\n\tPendingCompaction *PendingCompactionConfig `json:\"pending_compaction,omitempty\"` // global pending message compaction settings\n}\n\ntype TelegramConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tToken string `json:\"token\"`\n\tProxy string `json:\"proxy,omitempty\"`\n\tAPIServer string `json:\"api_server,omitempty\"` // custom Telegram Bot API server URL (e.g. \"http://localhost:8081\")\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in groups (default true)\n\tMentionMode string `json:\"mention_mode,omitempty\"` // \"strict\" (default) = only respond when mentioned; \"yield\" = respond unless another bot is mentioned\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tDMStream *bool `json:\"dm_stream,omitempty\"` // enable streaming for DMs (default false) — edits placeholder progressively\n\tGroupStream *bool `json:\"group_stream,omitempty\"` // enable streaming for groups (default false) — sends new message, edits progressively\n\tDraftTransport *bool `json:\"draft_transport,omitempty\"` // use sendMessageDraft for DM streaming (default true) — stealth preview, no notifications per edit\n\tReasoningStream *bool `json:\"reasoning_stream,omitempty\"` // show reasoning as separate message when provider emits thinking events (default true)\n\tReactionLevel string `json:\"reaction_level,omitempty\"` // \"off\" (default), \"minimal\", \"full\" — status emoji reactions\n\tMediaMaxBytes int64 `json:\"media_max_bytes,omitempty\"` // max media download size in bytes (default 20MB)\n\tLinkPreview *bool `json:\"link_preview,omitempty\"` // enable URL previews in messages (default true)\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tForceIPv4 bool `json:\"force_ipv4,omitempty\"` // force IPv4 for all Telegram API requests (use when IPv6 routing is broken)\n\n\t// Optional STT (Speech-to-Text) pipeline for voice/audio inbound messages.\n\t// When stt_proxy_url is set, audio/voice messages are transcribed before being forwarded to the agent.\n\tSTTProxyURL string `json:\"stt_proxy_url,omitempty\"` // base URL of the STT proxy service (e.g. \"https://stt.example.com\")\n\tSTTAPIKey string `json:\"stt_api_key,omitempty\"` // Bearer token for the STT proxy\n\tSTTTenantID string `json:\"stt_tenant_id,omitempty\"` // optional tenant/org identifier forwarded to the STT proxy\n\tSTTTimeoutSeconds int `json:\"stt_timeout_seconds,omitempty\"` // per-request timeout for STT calls (default 30s)\n\n\t// Optional audio-aware routing: when set, voice/audio inbound messages are routed to this\n\t// agent instead of the default channel agent. Requires the named agent to exist in the config.\n\tVoiceAgentID string `json:\"voice_agent_id,omitempty\"` // agent ID to route voice inbound to (e.g. \"speaking-agent\")\n\n\t// Audio guard: intercept technical errors in voice agent replies and replace with friendly fallbacks.\n\t// Only active when VoiceAgentID is set. Custom error markers replace built-in defaults when provided.\n\tAudioGuardFallbackTranscript string `json:\"audio_guard_fallback_transcript,omitempty\"` // fallback with %s for transcript (e.g. \"I heard: \\\"%s\\\". Try again!\")\n\tAudioGuardFallbackNoTranscript string `json:\"audio_guard_fallback_no_transcript,omitempty\"` // fallback when no transcript available\n\tAudioGuardErrorMarkers []string `json:\"audio_guard_error_markers,omitempty\"` // custom error detection markers (replaces defaults)\n\n\t// Per-group (and per-topic) overrides. Key is chat ID string (e.g. \"-100123456\") or \"*\" for wildcard.\n\t// TS ref: channels.telegram.groups in src/config/types.telegram.ts.\n\tGroups map[string]*TelegramGroupConfig `json:\"groups,omitempty\"`\n}\n\n// TelegramGroupConfig defines per-group overrides for a Telegram channel.\n// Matching TS TelegramGroupConfig in src/config/types.telegram.ts.\ntype TelegramGroupConfig struct {\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // override group policy for this group\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // override require_mention for this group\n\tMentionMode string `json:\"mention_mode,omitempty\"` // override mention_mode for this group\n\tAllowFrom FlexibleStringSlice `json:\"allow_from,omitempty\"` // override allow_from for this group\n\tEnabled *bool `json:\"enabled,omitempty\"` // disable bot for this group (default: true)\n\tSkills []string `json:\"skills,omitempty\"` // skill whitelist (nil = all, [] = none)\n\tTools []string `json:\"tools,omitempty\"` // tool allow list (nil = all, supports \"group:xxx\")\n\tSystemPrompt string `json:\"system_prompt,omitempty\"` // extra system prompt for this group\n\tTopics map[string]*TelegramTopicConfig `json:\"topics,omitempty\"` // per-topic overrides (key: thread ID string)\n\tQuota *QuotaWindow `json:\"quota,omitempty\"` // per-group quota override\n}\n\n// TelegramTopicConfig defines per-topic overrides within a Telegram group.\n// Matching TS TelegramTopicConfig in src/config/types.telegram.ts.\ntype TelegramTopicConfig struct {\n\tRequireMention *bool `json:\"require_mention,omitempty\"`\n\tMentionMode string `json:\"mention_mode,omitempty\"`\n\tGroupPolicy string `json:\"group_policy,omitempty\"`\n\tSkills []string `json:\"skills,omitempty\"`\n\tTools []string `json:\"tools,omitempty\"` // tool allow list (nil = inherit, supports \"group:xxx\")\n\tEnabled *bool `json:\"enabled,omitempty\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from,omitempty\"`\n\tSystemPrompt string `json:\"system_prompt,omitempty\"`\n}\n\ntype DiscordConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tToken string `json:\"token\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in groups (default true)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tMediaMaxBytes int64 `json:\"media_max_bytes,omitempty\"` // max media download size (default 25MB)\n\tSTTProxyURL string `json:\"stt_proxy_url,omitempty\"`\n\tSTTAPIKey string `json:\"stt_api_key,omitempty\"`\n\tSTTTenantID string `json:\"stt_tenant_id,omitempty\"`\n\tSTTTimeoutSeconds int `json:\"stt_timeout_seconds,omitempty\"`\n\tVoiceAgentID string `json:\"voice_agent_id,omitempty\"`\n}\n\ntype SlackConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tBotToken string `json:\"bot_token\"` // xoxb-... (Bot User OAuth Token)\n\tAppToken string `json:\"app_token\"` // xapp-... (App-Level Token for Socket Mode)\n\tUserToken string `json:\"user_token,omitempty\"` // xoxp-... (Optional: custom bot identity)\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"pairing\", \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in channels (default true)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tDMStream *bool `json:\"dm_stream,omitempty\"` // enable streaming for DMs (default false)\n\tGroupStream *bool `json:\"group_stream,omitempty\"` // enable streaming for groups (default false)\n\tNativeStream *bool `json:\"native_stream,omitempty\"` // use Slack ChatStreamer API if available (default false)\n\tReactionLevel string `json:\"reaction_level,omitempty\"` // \"off\" (default), \"minimal\", \"full\"\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tDebounceDelay int `json:\"debounce_delay,omitempty\"` // ms delay before dispatching rapid messages (default 300, 0=disabled)\n\tThreadTTL *int `json:\"thread_ttl,omitempty\"` // hours before thread participation expires (default 24, 0=disabled — always require @mention)\n\tMediaMaxBytes int64 `json:\"media_max_bytes,omitempty\"` // max file download size in bytes (default 20MB)\n}\n\ntype WhatsAppConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tAuthDir string `json:\"auth_dir,omitempty\"` // optional: SQLite auth dir override (desktop)\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default for DB instances), \"open\", \"allowlist\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"pairing\" (default for DB instances), \"open\" (default for config), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // only respond in groups when bot is @mentioned (default false)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 200, 0=disabled)\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n}\n\ntype ZaloConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tToken string `json:\"token\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tWebhookURL string `json:\"webhook_url,omitempty\"`\n\tWebhookSecret string `json:\"webhook_secret,omitempty\"`\n\tMediaMaxMB int `json:\"media_max_mb,omitempty\"` // default 5\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n}\n\ntype ZaloPersonalConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in groups (default true)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tCredentialsPath string `json:\"credentials_path,omitempty\"` // path to saved cookies JSON\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n}\n\ntype FeishuConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tAppID string `json:\"app_id\"`\n\tAppSecret string `json:\"app_secret\"`\n\tEncryptKey string `json:\"encrypt_key,omitempty\"`\n\tVerificationToken string `json:\"verification_token,omitempty\"`\n\tDomain string `json:\"domain,omitempty\"` // \"lark\" (default/global), \"feishu\" (China), or custom URL\n\tConnectionMode string `json:\"connection_mode,omitempty\"` // \"websocket\" (default), \"webhook\"\n\tWebhookPort int `json:\"webhook_port,omitempty\"` // default 3000\n\tWebhookPath string `json:\"webhook_path,omitempty\"` // default \"/feishu/events\"\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default)\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default)\n\tGroupAllowFrom FlexibleStringSlice `json:\"group_allow_from,omitempty\"`\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // default true (groups)\n\tTopicSessionMode string `json:\"topic_session_mode,omitempty\"` // \"disabled\" (default)\n\tTextChunkLimit int `json:\"text_chunk_limit,omitempty\"` // default 4000\n\tMediaMaxMB int `json:\"media_max_mb,omitempty\"` // default 30\n\tRenderMode string `json:\"render_mode,omitempty\"` // \"auto\", \"raw\", \"card\"\n\tStreaming *bool `json:\"streaming,omitempty\"` // default true\n\tReactionLevel string `json:\"reaction_level,omitempty\"` // \"off\" (default), \"minimal\", \"full\" — typing emoji reactions\n\tHistoryLimit int `json:\"history_limit,omitempty\"`\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tSTTProxyURL string `json:\"stt_proxy_url,omitempty\"`\n\tSTTAPIKey string `json:\"stt_api_key,omitempty\"`\n\tSTTTenantID string `json:\"stt_tenant_id,omitempty\"`\n\tSTTTimeoutSeconds int `json:\"stt_timeout_seconds,omitempty\"`\n\tVoiceAgentID string `json:\"voice_agent_id,omitempty\"`\n}\n\n// ProvidersConfig maps provider name to its config.\ntype ProvidersConfig struct {\n\tAnthropic ProviderConfig `json:\"anthropic\"`\n\tOpenAI ProviderConfig `json:\"openai\"`\n\tOpenRouter ProviderConfig `json:\"openrouter\"`\n\tGroq ProviderConfig `json:\"groq\"`\n\tGemini ProviderConfig `json:\"gemini\"`\n\tDeepSeek ProviderConfig `json:\"deepseek\"`\n\tMistral ProviderConfig `json:\"mistral\"`\n\tXAI ProviderConfig `json:\"xai\"`\n\tMiniMax ProviderConfig `json:\"minimax\"`\n\tCohere ProviderConfig `json:\"cohere\"`\n\tPerplexity ProviderConfig `json:\"perplexity\"`\n\tDashScope ProviderConfig `json:\"dashscope\"`\n\tBailian ProviderConfig `json:\"bailian\"`\n\tZai ProviderConfig `json:\"zai\"`\n\tZaiCoding ProviderConfig `json:\"zai_coding\"`\n\tOllama OllamaConfig `json:\"ollama\"` // local Ollama instance (no API key needed)\n\tOllamaCloud ProviderConfig `json:\"ollama_cloud\"` // Ollama Cloud (API key required)\n\tClaudeCLI ClaudeCLIConfig `json:\"claude_cli\"`\n\tACP ACPConfig `json:\"acp\"`\n\tNovita ProviderConfig `json:\"novita\"` // Novita AI (OpenAI-compatible endpoint)\n\tBytePlus ProviderConfig `json:\"byteplus\"` // BytePlus ModelArk (Seed 2.0)\n\tBytePlusCoding ProviderConfig `json:\"byteplus_coding\"` // BytePlus ModelArk Coding Plan\n}\n\n// OllamaConfig configures a local (or self-hosted) Ollama instance.\n// No API key is required — Ollama accepts any Bearer token value.\ntype OllamaConfig struct {\n\tHost string `json:\"host\"` // Ollama server base URL, e.g. http://localhost:11434\n}\n\n// ClaudeCLIConfig configures the Claude CLI provider (uses subscription, not API key).\ntype ClaudeCLIConfig struct {\n\tCLIPath string `json:\"cli_path\" yaml:\"cli_path\"` // path to claude binary (default: \"claude\")\n\tModel string `json:\"model\" yaml:\"model\"` // default model alias (default: \"sonnet\")\n\tBaseWorkDir string `json:\"base_work_dir\" yaml:\"base_work_dir\"` // base dir for agent workspaces\n\tPermMode string `json:\"perm_mode\" yaml:\"perm_mode\"` // permission mode (default: \"bypassPermissions\")\n}\n\n// ACPConfig configures the ACP (Agent Client Protocol) provider.\n// Orchestrates any ACP-compatible coding agent (Claude Code, Codex CLI, Gemini CLI) as a subprocess.\ntype ACPConfig struct {\n\tBinary string `json:\"binary\"` // agent binary name or path (e.g. \"claude\", \"codex\")\n\tArgs []string `json:\"args\"` // extra spawn args\n\tModel string `json:\"model\"` // default model/agent name\n\tWorkDir string `json:\"work_dir\"` // base workspace dir\n\tIdleTTL string `json:\"idle_ttl\"` // process idle TTL (e.g. \"5m\")\n\tPermMode string `json:\"perm_mode\"` // \"approve-all\" (default), \"approve-reads\", \"deny-all\"\n}\n\ntype ProviderConfig struct {\n\tAPIKey string `json:\"api_key\"`\n\tAPIBase string `json:\"api_base,omitempty\"`\n}\n\n// APIBaseForType returns the config-level api_base for a given provider type.\n// Used as a fallback when DB providers have no api_base set.\nfunc (p *ProvidersConfig) APIBaseForType(providerType string) string {\n\tswitch providerType {\n\tcase \"anthropic_native\":\n\t\treturn p.Anthropic.APIBase\n\tcase \"openai\", \"openai_compat\":\n\t\treturn p.OpenAI.APIBase\n\tcase \"openrouter\":\n\t\treturn p.OpenRouter.APIBase\n\tcase \"groq\":\n\t\treturn p.Groq.APIBase\n\tcase \"deepseek\":\n\t\treturn p.DeepSeek.APIBase\n\tcase \"gemini_native\":\n\t\treturn p.Gemini.APIBase\n\tcase \"mistral\":\n\t\treturn p.Mistral.APIBase\n\tcase \"xai\":\n\t\treturn p.XAI.APIBase\n\tcase \"minimax_native\":\n\t\treturn p.MiniMax.APIBase\n\tcase \"cohere\":\n\t\treturn p.Cohere.APIBase\n\tcase \"perplexity\":\n\t\treturn p.Perplexity.APIBase\n\tcase \"dashscope\":\n\t\treturn p.DashScope.APIBase\n\tcase \"bailian\":\n\t\treturn p.Bailian.APIBase\n\tcase \"zai\":\n\t\treturn p.Zai.APIBase\n\tcase \"zai_coding\":\n\t\treturn p.ZaiCoding.APIBase\n\tcase \"ollama_cloud\":\n\t\treturn p.OllamaCloud.APIBase\n\tcase \"novita\":\n\t\treturn p.Novita.APIBase\n\tcase \"byteplus\":\n\t\treturn p.BytePlus.APIBase\n\tcase \"byteplus_coding\":\n\t\treturn p.BytePlusCoding.APIBase\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\n// HasAnyProvider returns true if at least one provider has an API key or CLI configured.\nfunc (c *Config) HasAnyProvider() bool {\n\tp := c.Providers\n\treturn p.Anthropic.APIKey != \"\" ||\n\t\tp.OpenAI.APIKey != \"\" ||\n\t\tp.OpenRouter.APIKey != \"\" ||\n\t\tp.Groq.APIKey != \"\" ||\n\t\tp.Gemini.APIKey != \"\" ||\n\t\tp.DeepSeek.APIKey != \"\" ||\n\t\tp.Mistral.APIKey != \"\" ||\n\t\tp.XAI.APIKey != \"\" ||\n\t\tp.MiniMax.APIKey != \"\" ||\n\t\tp.Cohere.APIKey != \"\" ||\n\t\tp.Perplexity.APIKey != \"\" ||\n\t\tp.DashScope.APIKey != \"\" ||\n\t\tp.Bailian.APIKey != \"\" ||\n\t\tp.Zai.APIKey != \"\" ||\n\t\tp.ZaiCoding.APIKey != \"\" ||\n\t\tp.Ollama.Host != \"\" ||\n\t\tp.OllamaCloud.APIKey != \"\" ||\n\t\tp.ClaudeCLI.CLIPath != \"\" ||\n\t\tp.ACP.Binary != \"\" ||\n\t\tp.Novita.APIKey != \"\" ||\n\t\tp.BytePlus.APIKey != \"\" ||\n\t\tp.BytePlusCoding.APIKey != \"\"\n}\n\n// QuotaWindow defines request limits per time window. Zero means unlimited.\ntype QuotaWindow struct {\n\tHour int `json:\"hour,omitempty\"` // max requests per hour (0 = unlimited)\n\tDay int `json:\"day,omitempty\"` // max requests per day (0 = unlimited)\n\tWeek int `json:\"week,omitempty\"` // max requests per week (0 = unlimited)\n}\n\n// IsZero returns true if no limits are set.\nfunc (w QuotaWindow) IsZero() bool { return w.Hour == 0 && w.Day == 0 && w.Week == 0 }\n\n// QuotaConfig configures per-user/group request quotas.\n// Config merge priority: Groups > Channels > Providers > Default.\ntype QuotaConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tDefault QuotaWindow `json:\"default\"`\n\tProviders map[string]QuotaWindow `json:\"providers,omitempty\"` // key = provider name (e.g. \"anthropic\")\n\tChannels map[string]QuotaWindow `json:\"channels,omitempty\"` // key = channel name (e.g. \"telegram\")\n\tGroups map[string]QuotaWindow `json:\"groups,omitempty\"` // key = userID (e.g. \"group:telegram:-100123\")\n}\n\n// GatewayConfig controls the gateway server.\ntype GatewayConfig struct {\n\tHost string `json:\"host\"`\n\tPort int `json:\"port\"`\n\tToken string `json:\"token,omitempty\"` // bearer token for WS/HTTP auth\n\tOwnerIDs []string `json:\"owner_ids,omitempty\"` // sender IDs considered \"owner\"\n\tAllowedOrigins []string `json:\"allowed_origins,omitempty\"` // WebSocket CORS whitelist (empty = allow all)\n\tMaxMessageChars int `json:\"max_message_chars,omitempty\"` // max user message characters (default 32000)\n\tRateLimitRPM int `json:\"rate_limit_rpm,omitempty\"` // rate limit: requests per minute per user (default 20, 0 = disabled)\n\tInjectionAction string `json:\"injection_action,omitempty\"` // prompt injection action: \"log\", \"warn\" (default), \"block\", \"off\"\n\tInboundDebounceMs int `json:\"inbound_debounce_ms,omitempty\"` // merge rapid messages from same sender (default 1000ms, -1 = disabled)\n\tQuota *QuotaConfig `json:\"quota,omitempty\"` // per-user/group request quotas\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // deliver intermediate text during tool iterations (default false)\n\tToolStatus *bool `json:\"tool_status,omitempty\"` // show tool name in streaming preview during tool execution (default true)\n\tTaskRecoveryIntervalSec int `json:\"task_recovery_interval_sec,omitempty\"` // team task recovery ticker interval in seconds (default 300 = 5min)\n\tBackgroundProvider string `json:\"background_provider,omitempty\"` // LLM provider for background workers (vault enrichment, consolidation)\n\tBackgroundModel string `json:\"background_model,omitempty\"` // LLM model for background workers\n}\n\n// ToolsConfig controls tool availability, policy, and web search.\ntype ToolsConfig struct {\n\tProfile string `json:\"profile,omitempty\"` // global profile: \"minimal\", \"coding\", \"messaging\", \"full\"\n\tAllow []string `json:\"allow,omitempty\"` // global allow list (tool names or \"group:xxx\")\n\tDeny []string `json:\"deny,omitempty\"` // global deny list\n\tAlsoAllow []string `json:\"alsoAllow,omitempty\"` // additive: adds without removing existing\n\tByProvider map[string]*ToolPolicySpec `json:\"byProvider,omitempty\"` // per-provider overrides\n\tExecApproval ExecApprovalCfg `json:\"execApproval\"` // exec command approval settings\n\tWebFetch WebFetchPolicyConfig `json:\"web_fetch\"` // domain policy for URL fetching\n\tBrowser BrowserToolConfig `json:\"browser\"`\n\tRateLimitPerHour int `json:\"rate_limit_per_hour,omitempty\"` // max tool executions per hour per session (0 = disabled)\n\tScrubCredentials *bool `json:\"scrub_credentials,omitempty\"` // auto-redact API keys/tokens in tool output (default true)\n\tMcpServers map[string]*MCPServerConfig `json:\"mcp_servers,omitempty\"` // external MCP server connections\n}\n\n// MCPServerConfig configures a single external MCP server connection.\ntype MCPServerConfig struct {\n\tTransport string `json:\"transport\"` // \"stdio\", \"sse\", \"streamable-http\"\n\tCommand string `json:\"command,omitempty\"` // stdio: command to spawn\n\tArgs []string `json:\"args,omitempty\"` // stdio: command arguments\n\tEnv map[string]string `json:\"env,omitempty\"` // stdio: extra environment variables\n\tURL string `json:\"url,omitempty\"` // sse/http: server URL\n\tHeaders map[string]string `json:\"headers,omitempty\"` // sse/http: extra HTTP headers\n\tEnabled *bool `json:\"enabled,omitempty\"` // default true\n\tToolPrefix string `json:\"tool_prefix,omitempty\"` // prefix for tool names (avoids collisions)\n\tTimeoutSec int `json:\"timeout_sec,omitempty\"` // per-tool-call timeout in seconds (default 60)\n}\n\n// IsEnabled returns whether this MCP server is enabled (default true).\nfunc (c *MCPServerConfig) IsEnabled() bool {\n\treturn c.Enabled == nil || *c.Enabled\n}\n\n// ExecApprovalCfg configures command execution approval (matching TS exec-approval.ts).\ntype ExecApprovalCfg struct {\n\tSecurity string `json:\"security,omitempty\"` // \"deny\", \"allowlist\", \"full\" (default \"full\")\n\tAsk string `json:\"ask,omitempty\"` // \"off\", \"on-miss\", \"always\" (default \"off\")\n\tAllowlist []string `json:\"allowlist,omitempty\"` // glob patterns for allowed commands\n}\n\n// WebFetchPolicyConfig controls domain filtering for the web_fetch tool.\ntype WebFetchPolicyConfig struct {\n\tPolicy string `json:\"policy,omitempty\"` // \"allow_all\" (default), \"allowlist\"\n\tAllowedDomains []string `json:\"allowed_domains,omitempty\"` // e.g. [\"github.com\", \"*.example.com\"]\n\tBlockedDomains []string `json:\"blocked_domains,omitempty\"` // always checked regardless of policy\n}\n\n// BrowserToolConfig controls the browser automation tool.\ntype BrowserToolConfig struct {\n\tEnabled bool `json:\"enabled\"` // enable the browser tool (default false)\n\tHeadless bool `json:\"headless,omitempty\"` // run Chrome in headless mode (ignored when RemoteURL is set)\n\tRemoteURL string `json:\"remote_url,omitempty\"` // CDP endpoint for remote Chrome sidecar, e.g. \"ws://chrome:9222\"\n\tActionTimeoutMs int `json:\"action_timeout_ms,omitempty\"` // per-action timeout in ms (default 30000)\n\tIdleTimeoutMs int `json:\"idle_timeout_ms,omitempty\"` // idle page auto-close in ms (default 600000, 0=disabled)\n\tMaxPages int `json:\"max_pages,omitempty\"` // max open pages per tenant (default 5)\n}\n\n// ToolPolicySpec defines a tool policy at any level (global, per-agent, per-provider).\ntype ToolPolicySpec struct {\n\tProfile string `json:\"profile,omitempty\"`\n\tAllow []string `json:\"allow,omitempty\"`\n\tDeny []string `json:\"deny,omitempty\"`\n\tAlsoAllow []string `json:\"alsoAllow,omitempty\"`\n\tByProvider map[string]*ToolPolicySpec `json:\"byProvider,omitempty\"`\n\tToolCallPrefix string `json:\"toolCallPrefix,omitempty\"` // prefix to strip from model's tool call names before registry lookup\n}\n\n\n// SessionsConfig controls session behavior.\n// Matching TS src/config/sessions/types.ts + src/config/types.base.ts.\ntype SessionsConfig struct {\n\tScope string `json:\"scope,omitempty\"` // \"per-sender\" (default), \"global\"\n\tDmScope string `json:\"dm_scope,omitempty\"` // \"main\", \"per-peer\", \"per-channel-peer\" (default), \"per-account-channel-peer\"\n\tMainKey string `json:\"main_key,omitempty\"` // main session key suffix (default \"main\", used when dm_scope=\"main\")\n}\n\n// TtsConfig configures text-to-speech.\n// Matching TS src/config/types.tts.ts.\ntype TtsConfig struct {\n\tProvider string `json:\"provider,omitempty\"` // \"openai\", \"elevenlabs\", \"edge\", \"minimax\"\n\tAuto string `json:\"auto,omitempty\"` // \"off\" (default), \"always\", \"inbound\", \"tagged\"\n\tMode string `json:\"mode,omitempty\"` // \"final\" (default), \"all\"\n\tMaxLength int `json:\"max_length,omitempty\"` // max text length before truncation (default 1500)\n\tTimeoutMs int `json:\"timeout_ms,omitempty\"` // API timeout in ms (default 30000)\n\tOpenAI TtsOpenAIConfig `json:\"openai\"`\n\tElevenLabs TtsElevenLabsConfig `json:\"elevenlabs\"`\n\tEdge TtsEdgeConfig `json:\"edge\"`\n\tMiniMax TtsMiniMaxConfig `json:\"minimax\"`\n}\n\n// TtsOpenAIConfig configures the OpenAI TTS provider.\ntype TtsOpenAIConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"`\n\tAPIBase string `json:\"api_base,omitempty\"` // custom endpoint URL\n\tModel string `json:\"model,omitempty\"` // default \"gpt-4o-mini-tts\"\n\tVoice string `json:\"voice,omitempty\"` // default \"alloy\"\n}\n\n// TtsElevenLabsConfig configures the ElevenLabs TTS provider.\ntype TtsElevenLabsConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"`\n\tBaseURL string `json:\"base_url,omitempty\"`\n\tVoiceID string `json:\"voice_id,omitempty\"` // default \"pMsXgVXv3BLzUgSXRplE\"\n\tModelID string `json:\"model_id,omitempty\"` // default \"eleven_multilingual_v2\"\n}\n\n// TtsEdgeConfig configures the Microsoft Edge TTS provider (free, no API key).\ntype TtsEdgeConfig struct {\n\tEnabled bool `json:\"enabled,omitempty\"`\n\tVoice string `json:\"voice,omitempty\"` // default \"en-US-MichelleNeural\"\n\tRate string `json:\"rate,omitempty\"` // speech rate, e.g. \"+0%\"\n}\n\n// TtsMiniMaxConfig configures the MiniMax TTS provider.\ntype TtsMiniMaxConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"`\n\tGroupID string `json:\"group_id,omitempty\"` // MiniMax GroupId (required)\n\tAPIBase string `json:\"api_base,omitempty\"` // default \"https://api.minimax.io/v1\"\n\tModel string `json:\"model,omitempty\"` // default \"speech-02-hd\"\n\tVoiceID string `json:\"voice_id,omitempty\"` // default \"Wise_Woman\"\n}\n\n// MergeChannelGroupQuotas merges per-group quota overrides from channel configs\n// (e.g., channels.telegram.groups[chatID].quota) into gateway.quota.groups.\n// This allows per-group quotas to be set at the channel level and picked up\n// by the QuotaChecker at runtime.\nfunc MergeChannelGroupQuotas(cfg *Config) {\n\tif cfg.Gateway.Quota == nil {\n\t\treturn\n\t}\n\tif cfg.Gateway.Quota.Groups == nil {\n\t\tcfg.Gateway.Quota.Groups = make(map[string]QuotaWindow)\n\t}\n\n\t// Telegram per-group quotas\n\tfor chatID, groupCfg := range cfg.Channels.Telegram.Groups {\n\t\tif groupCfg != nil && groupCfg.Quota != nil && !groupCfg.Quota.IsZero() {\n\t\t\tkey := \"group:telegram:\" + chatID\n\t\t\tcfg.Gateway.Quota.Groups[key] = *groupCfg.Quota\n\t\t}\n\t}\n}\n"}, "30a40bbe596af39064c526f4e5e404ecf7fee261": {"t": 1785759249.690381, "n": 32758, "h": "0e17d97f1132ab06", "c": "package config\n\n// PendingCompactionConfig configures LLM-based compaction of pending group messages.\n// When a group accumulates more than Threshold pending messages, older messages are\n// summarized by an LLM and replaced with a compact summary, keeping KeepRecent raw messages.\ntype PendingCompactionConfig struct {\n\tThreshold int `json:\"threshold,omitempty\"` // trigger compaction when entries exceed this (default 200)\n\tKeepRecent int `json:\"keep_recent,omitempty\"` // keep this many recent raw messages after compaction (default 40)\n\tMaxTokens int `json:\"max_tokens,omitempty\"` // max output tokens for LLM summarization (default 4096)\n\tProvider string `json:\"provider,omitempty\"` // LLM provider name (e.g. \"openai\"); empty = use agent's provider\n\tModel string `json:\"model,omitempty\"` // model for summarization; empty = use agent's model\n}\n\n// ChannelsConfig contains per-channel configuration.\ntype ChannelsConfig struct {\n\tTelegram TelegramConfig `json:\"telegram\"`\n\tDiscord DiscordConfig `json:\"discord\"`\n\tSlack SlackConfig `json:\"slack\"`\n\tWhatsApp WhatsAppConfig `json:\"whatsapp\"`\n\tZalo ZaloConfig `json:\"zalo\"`\n\tZaloPersonal ZaloPersonalConfig `json:\"zalo_personal\"`\n\tFeishu FeishuConfig `json:\"feishu\"`\n\tPendingCompaction *PendingCompactionConfig `json:\"pending_compaction,omitempty\"` // global pending message compaction settings\n}\n\ntype TelegramConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tToken string `json:\"token\"`\n\tProxy string `json:\"proxy,omitempty\"`\n\tAPIServer string `json:\"api_server,omitempty\"` // custom Telegram Bot API server URL (e.g. \"http://localhost:8081\")\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in groups (default true)\n\tMentionMode string `json:\"mention_mode,omitempty\"` // \"strict\" (default) = only respond when mentioned; \"yield\" = respond unless another bot is mentioned\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tDMStream *bool `json:\"dm_stream,omitempty\"` // enable streaming for DMs (default false) — edits placeholder progressively\n\tGroupStream *bool `json:\"group_stream,omitempty\"` // enable streaming for groups (default false) — sends new message, edits progressively\n\tDraftTransport *bool `json:\"draft_transport,omitempty\"` // use sendMessageDraft for DM streaming (default true) — stealth preview, no notifications per edit\n\tReasoningStream *bool `json:\"reasoning_stream,omitempty\"` // show reasoning as separate message when provider emits thinking events (default true)\n\tReactionLevel string `json:\"reaction_level,omitempty\"` // \"off\" (default), \"minimal\", \"full\" — status emoji reactions\n\tMediaMaxBytes int64 `json:\"media_max_bytes,omitempty\"` // max media download size in bytes (default 20MB)\n\tLinkPreview *bool `json:\"link_preview,omitempty\"` // enable URL previews in messages (default true)\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tForceIPv4 bool `json:\"force_ipv4,omitempty\"` // force IPv4 for all Telegram API requests (use when IPv6 routing is broken)\n\n\t// Optional STT (Speech-to-Text) pipeline for voice/audio inbound messages.\n\t// When stt_proxy_url is set, audio/voice messages are transcribed before being forwarded to the agent.\n\tSTTProxyURL string `json:\"stt_proxy_url,omitempty\"` // base URL of the STT proxy service (e.g. \"https://stt.example.com\")\n\tSTTAPIKey string `json:\"stt_api_key,omitempty\"` // Bearer token for the STT proxy\n\tSTTTenantID string `json:\"stt_tenant_id,omitempty\"` // optional tenant/org identifier forwarded to the STT proxy\n\tSTTTimeoutSeconds int `json:\"stt_timeout_seconds,omitempty\"` // per-request timeout for STT calls (default 30s)\n\n\t// Optional audio-aware routing: when set, voice/audio inbound messages are routed to this\n\t// agent instead of the default channel agent. Requires the named agent to exist in the config.\n\tVoiceAgentID string `json:\"voice_agent_id,omitempty\"` // agent ID to route voice inbound to (e.g. \"speaking-agent\")\n\n\t// Audio guard: intercept technical errors in voice agent replies and replace with friendly fallbacks.\n\t// Only active when VoiceAgentID is set. Custom error markers replace built-in defaults when provided.\n\tAudioGuardFallbackTranscript string `json:\"audio_guard_fallback_transcript,omitempty\"` // fallback with %s for transcript (e.g. \"I heard: \\\"%s\\\". Try again!\")\n\tAudioGuardFallbackNoTranscript string `json:\"audio_guard_fallback_no_transcript,omitempty\"` // fallback when no transcript available\n\tAudioGuardErrorMarkers []string `json:\"audio_guard_error_markers,omitempty\"` // custom error detection markers (replaces defaults)\n\n\t// Per-group (and per-topic) overrides. Key is chat ID string (e.g. \"-100123456\") or \"*\" for wildcard.\n\t// TS ref: channels.telegram.groups in src/config/types.telegram.ts.\n\tGroups map[string]*TelegramGroupConfig `json:\"groups,omitempty\"`\n}\n\n// TelegramGroupConfig defines per-group overrides for a Telegram channel.\n// Matching TS TelegramGroupConfig in src/config/types.telegram.ts.\ntype TelegramGroupConfig struct {\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // override group policy for this group\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // override require_mention for this group\n\tMentionMode string `json:\"mention_mode,omitempty\"` // override mention_mode for this group\n\tAllowFrom FlexibleStringSlice `json:\"allow_from,omitempty\"` // override allow_from for this group\n\tEnabled *bool `json:\"enabled,omitempty\"` // disable bot for this group (default: true)\n\tSkills []string `json:\"skills,omitempty\"` // skill whitelist (nil = all, [] = none)\n\tTools []string `json:\"tools,omitempty\"` // tool allow list (nil = all, supports \"group:xxx\")\n\tSystemPrompt string `json:\"system_prompt,omitempty\"` // extra system prompt for this group\n\tTopics map[string]*TelegramTopicConfig `json:\"topics,omitempty\"` // per-topic overrides (key: thread ID string)\n\tQuota *QuotaWindow `json:\"quota,omitempty\"` // per-group quota override\n}\n\n// TelegramTopicConfig defines per-topic overrides within a Telegram group.\n// Matching TS TelegramTopicConfig in src/config/types.telegram.ts.\ntype TelegramTopicConfig struct {\n\tRequireMention *bool `json:\"require_mention,omitempty\"`\n\tMentionMode string `json:\"mention_mode,omitempty\"`\n\tGroupPolicy string `json:\"group_policy,omitempty\"`\n\tSkills []string `json:\"skills,omitempty\"`\n\tTools []string `json:\"tools,omitempty\"` // tool allow list (nil = inherit, supports \"group:xxx\")\n\tEnabled *bool `json:\"enabled,omitempty\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from,omitempty\"`\n\tSystemPrompt string `json:\"system_prompt,omitempty\"`\n}\n\ntype DiscordConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tToken string `json:\"token\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in groups (default true)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tMediaMaxBytes int64 `json:\"media_max_bytes,omitempty\"` // max media download size (default 25MB)\n\tSTTProxyURL string `json:\"stt_proxy_url,omitempty\"`\n\tSTTAPIKey string `json:\"stt_api_key,omitempty\"`\n\tSTTTenantID string `json:\"stt_tenant_id,omitempty\"`\n\tSTTTimeoutSeconds int `json:\"stt_timeout_seconds,omitempty\"`\n\tVoiceAgentID string `json:\"voice_agent_id,omitempty\"`\n}\n\ntype SlackConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tBotToken string `json:\"bot_token\"` // xoxb-... (Bot User OAuth Token)\n\tAppToken string `json:\"app_token\"` // xapp-... (App-Level Token for Socket Mode)\n\tUserToken string `json:\"user_token,omitempty\"` // xoxp-... (Optional: custom bot identity)\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"pairing\", \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in channels (default true)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tDMStream *bool `json:\"dm_stream,omitempty\"` // enable streaming for DMs (default false)\n\tGroupStream *bool `json:\"group_stream,omitempty\"` // enable streaming for groups (default false)\n\tNativeStream *bool `json:\"native_stream,omitempty\"` // use Slack ChatStreamer API if available (default false)\n\tReactionLevel string `json:\"reaction_level,omitempty\"` // \"off\" (default), \"minimal\", \"full\"\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tDebounceDelay int `json:\"debounce_delay,omitempty\"` // ms delay before dispatching rapid messages (default 300, 0=disabled)\n\tThreadTTL *int `json:\"thread_ttl,omitempty\"` // hours before thread participation expires (default 24, 0=disabled — always require @mention)\n\tMediaMaxBytes int64 `json:\"media_max_bytes,omitempty\"` // max file download size in bytes (default 20MB)\n}\n\ntype WhatsAppConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tAuthDir string `json:\"auth_dir,omitempty\"` // optional: SQLite auth dir override (desktop)\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default for DB instances), \"open\", \"allowlist\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"pairing\" (default for DB instances), \"open\" (default for config), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // only respond in groups when bot is @mentioned (default false)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 200, 0=disabled)\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n}\n\ntype ZaloConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tToken string `json:\"token\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tWebhookURL string `json:\"webhook_url,omitempty\"`\n\tWebhookSecret string `json:\"webhook_secret,omitempty\"`\n\tMediaMaxMB int `json:\"media_max_mb,omitempty\"` // default 5\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n}\n\ntype ZaloPersonalConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in groups (default true)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tCredentialsPath string `json:\"credentials_path,omitempty\"` // path to saved cookies JSON\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n}\n\ntype FeishuConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tAppID string `json:\"app_id\"`\n\tAppSecret string `json:\"app_secret\"`\n\tEncryptKey string `json:\"encrypt_key,omitempty\"`\n\tVerificationToken string `json:\"verification_token,omitempty\"`\n\tDomain string `json:\"domain,omitempty\"` // \"lark\" (default/global), \"feishu\" (China), or custom URL\n\tConnectionMode string `json:\"connection_mode,omitempty\"` // \"websocket\" (default), \"webhook\"\n\tWebhookPort int `json:\"webhook_port,omitempty\"` // default 3000\n\tWebhookPath string `json:\"webhook_path,omitempty\"` // default \"/feishu/events\"\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default)\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default)\n\tGroupAllowFrom FlexibleStringSlice `json:\"group_allow_from,omitempty\"`\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // default true (groups)\n\tTopicSessionMode string `json:\"topic_session_mode,omitempty\"` // \"disabled\" (default)\n\tTextChunkLimit int `json:\"text_chunk_limit,omitempty\"` // default 4000\n\tMediaMaxMB int `json:\"media_max_mb,omitempty\"` // default 30\n\tRenderMode string `json:\"render_mode,omitempty\"` // \"auto\", \"raw\", \"card\"\n\tStreaming *bool `json:\"streaming,omitempty\"` // default true\n\tReactionLevel string `json:\"reaction_level,omitempty\"` // \"off\" (default), \"minimal\", \"full\" — typing emoji reactions\n\tHistoryLimit int `json:\"history_limit,omitempty\"`\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tSTTProxyURL string `json:\"stt_proxy_url,omitempty\"`\n\tSTTAPIKey string `json:\"stt_api_key,omitempty\"`\n\tSTTTenantID string `json:\"stt_tenant_id,omitempty\"`\n\tSTTTimeoutSeconds int `json:\"stt_timeout_seconds,omitempty\"`\n\tVoiceAgentID string `json:\"voice_agent_id,omitempty\"`\n}\n\n// ProvidersConfig maps provider name to its config.\ntype ProvidersConfig struct {\n\tAnthropic ProviderConfig `json:\"anthropic\"`\n\tOpenAI ProviderConfig `json:\"openai\"`\n\tOpenRouter ProviderConfig `json:\"openrouter\"`\n\tGroq ProviderConfig `json:\"groq\"`\n\tGemini ProviderConfig `json:\"gemini\"`\n\tDeepSeek ProviderConfig `json:\"deepseek\"`\n\tMistral ProviderConfig `json:\"mistral\"`\n\tXAI ProviderConfig `json:\"xai\"`\n\tMiniMax ProviderConfig `json:\"minimax\"`\n\tCohere ProviderConfig `json:\"cohere\"`\n\tPerplexity ProviderConfig `json:\"perplexity\"`\n\tDashScope ProviderConfig `json:\"dashscope\"`\n\tBailian ProviderConfig `json:\"bailian\"`\n\tZai ProviderConfig `json:\"zai\"`\n\tZaiCoding ProviderConfig `json:\"zai_coding\"`\n\tOllama OllamaConfig `json:\"ollama\"` // local Ollama instance (no API key needed)\n\tOllamaCloud ProviderConfig `json:\"ollama_cloud\"` // Ollama Cloud (API key required)\n\tClaudeCLI ClaudeCLIConfig `json:\"claude_cli\"`\n\tACP ACPConfig `json:\"acp\"`\n\tNovita ProviderConfig `json:\"novita\"` // Novita AI (OpenAI-compatible endpoint)\n\tBytePlus ProviderConfig `json:\"byteplus\"` // BytePlus ModelArk (Seed 2.0)\n\tBytePlusCoding ProviderConfig `json:\"byteplus_coding\"` // BytePlus ModelArk Coding Plan\n}\n\n// OllamaConfig configures a local (or self-hosted) Ollama instance.\n// No API key is required — Ollama accepts any Bearer token value.\ntype OllamaConfig struct {\n\tHost string `json:\"host\"` // Ollama server base URL, e.g. http://localhost:11434\n}\n\n// ClaudeCLIConfig configures the Claude CLI provider (uses subscription, not API key).\ntype ClaudeCLIConfig struct {\n\tCLIPath string `json:\"cli_path\" yaml:\"cli_path\"` // path to claude binary (default: \"claude\")\n\tModel string `json:\"model\" yaml:\"model\"` // default model alias (default: \"sonnet\")\n\tBaseWorkDir string `json:\"base_work_dir\" yaml:\"base_work_dir\"` // base dir for agent workspaces\n\tPermMode string `json:\"perm_mode\" yaml:\"perm_mode\"` // permission mode (default: \"bypassPermissions\")\n}\n\n// ACPConfig configures the ACP (Agent Client Protocol) provider.\n// Orchestrates any ACP-compatible coding agent (Claude Code, Codex CLI, Gemini CLI) as a subprocess.\ntype ACPConfig struct {\n\tBinary string `json:\"binary\"` // agent binary name or path (e.g. \"claude\", \"codex\")\n\tArgs []string `json:\"args\"` // extra spawn args\n\tModel string `json:\"model\"` // default model/agent name\n\tWorkDir string `json:\"work_dir\"` // base workspace dir\n\tIdleTTL string `json:\"idle_ttl\"` // process idle TTL (e.g. \"5m\")\n\tPermMode string `json:\"perm_mode\"` // \"approve-all\" (default), \"approve-reads\", \"deny-all\"\n}\n\ntype ProviderConfig struct {\n\tAPIKey string `json:\"api_key\"`\n\tAPIBase string `json:\"api_base,omitempty\"`\n}\n\n// APIBaseForType returns the config-level api_base for a given provider type.\n// Used as a fallback when DB providers have no api_base set.\nfunc (p *ProvidersConfig) APIBaseForType(providerType string) string {\n\tswitch providerType {\n\tcase \"anthropic_native\":\n\t\treturn p.Anthropic.APIBase\n\tcase \"openai\", \"openai_compat\":\n\t\treturn p.OpenAI.APIBase\n\tcase \"openrouter\":\n\t\treturn p.OpenRouter.APIBase\n\tcase \"groq\":\n\t\treturn p.Groq.APIBase\n\tcase \"deepseek\":\n\t\treturn p.DeepSeek.APIBase\n\tcase \"gemini_native\":\n\t\treturn p.Gemini.APIBase\n\tcase \"mistral\":\n\t\treturn p.Mistral.APIBase\n\tcase \"xai\":\n\t\treturn p.XAI.APIBase\n\tcase \"minimax_native\":\n\t\treturn p.MiniMax.APIBase\n\tcase \"cohere\":\n\t\treturn p.Cohere.APIBase\n\tcase \"perplexity\":\n\t\treturn p.Perplexity.APIBase\n\tcase \"dashscope\":\n\t\treturn p.DashScope.APIBase\n\tcase \"bailian\":\n\t\treturn p.Bailian.APIBase\n\tcase \"zai\":\n\t\treturn p.Zai.APIBase\n\tcase \"zai_coding\":\n\t\treturn p.ZaiCoding.APIBase\n\tcase \"ollama_cloud\":\n\t\treturn p.OllamaCloud.APIBase\n\tcase \"novita\":\n\t\treturn p.Novita.APIBase\n\tcase \"byteplus\":\n\t\treturn p.BytePlus.APIBase\n\tcase \"byteplus_coding\":\n\t\treturn p.BytePlusCoding.APIBase\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\n// HasAnyProvider returns true if at least one provider has an API key or CLI configured.\nfunc (c *Config) HasAnyProvider() bool {\n\tp := c.Providers\n\treturn p.Anthropic.APIKey != \"\" ||\n\t\tp.OpenAI.APIKey != \"\" ||\n\t\tp.OpenRouter.APIKey != \"\" ||\n\t\tp.Groq.APIKey != \"\" ||\n\t\tp.Gemini.APIKey != \"\" ||\n\t\tp.DeepSeek.APIKey != \"\" ||\n\t\tp.Mistral.APIKey != \"\" ||\n\t\tp.XAI.APIKey != \"\" ||\n\t\tp.MiniMax.APIKey != \"\" ||\n\t\tp.Cohere.APIKey != \"\" ||\n\t\tp.Perplexity.APIKey != \"\" ||\n\t\tp.DashScope.APIKey != \"\" ||\n\t\tp.Bailian.APIKey != \"\" ||\n\t\tp.Zai.APIKey != \"\" ||\n\t\tp.ZaiCoding.APIKey != \"\" ||\n\t\tp.Ollama.Host != \"\" ||\n\t\tp.OllamaCloud.APIKey != \"\" ||\n\t\tp.ClaudeCLI.CLIPath != \"\" ||\n\t\tp.ACP.Binary != \"\" ||\n\t\tp.Novita.APIKey != \"\" ||\n\t\tp.BytePlus.APIKey != \"\" ||\n\t\tp.BytePlusCoding.APIKey != \"\"\n}\n\n// QuotaWindow defines request limits per time window. Zero means unlimited.\ntype QuotaWindow struct {\n\tHour int `json:\"hour,omitempty\"` // max requests per hour (0 = unlimited)\n\tDay int `json:\"day,omitempty\"` // max requests per day (0 = unlimited)\n\tWeek int `json:\"week,omitempty\"` // max requests per week (0 = unlimited)\n}\n\n// IsZero returns true if no limits are set.\nfunc (w QuotaWindow) IsZero() bool { return w.Hour == 0 && w.Day == 0 && w.Week == 0 }\n\n// QuotaConfig configures per-user/group request quotas.\n// Config merge priority: Groups > Channels > Providers > Default.\ntype QuotaConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tDefault QuotaWindow `json:\"default\"`\n\tProviders map[string]QuotaWindow `json:\"providers,omitempty\"` // key = provider name (e.g. \"anthropic\")\n\tChannels map[string]QuotaWindow `json:\"channels,omitempty\"` // key = channel name (e.g. \"telegram\")\n\tGroups map[string]QuotaWindow `json:\"groups,omitempty\"` // key = userID (e.g. \"group:telegram:-100123\")\n}\n\n// GatewayConfig controls the gateway server.\ntype GatewayConfig struct {\n\tHost string `json:\"host\"`\n\tPort int `json:\"port\"`\n\tToken string `json:\"token,omitempty\"` // bearer token for WS/HTTP auth\n\tOwnerIDs []string `json:\"owner_ids,omitempty\"` // sender IDs considered \"owner\"\n\tAllowedOrigins []string `json:\"allowed_origins,omitempty\"` // WebSocket CORS whitelist (empty = allow all)\n\tMaxMessageChars int `json:\"max_message_chars,omitempty\"` // max user message characters (default 32000)\n\tRateLimitRPM int `json:\"rate_limit_rpm,omitempty\"` // rate limit: requests per minute per user (default 20, 0 = disabled)\n\tInjectionAction string `json:\"injection_action,omitempty\"` // prompt injection action: \"log\", \"warn\" (default), \"block\", \"off\"\n\tInboundDebounceMs int `json:\"inbound_debounce_ms,omitempty\"` // merge rapid messages from same sender (default 1000ms, -1 = disabled)\n\tQuota *QuotaConfig `json:\"quota,omitempty\"` // per-user/group request quotas\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // deliver intermediate text during tool iterations (default false)\n\tToolStatus *bool `json:\"tool_status,omitempty\"` // show tool name in streaming preview during tool execution (default true)\n\tTaskRecoveryIntervalSec int `json:\"task_recovery_interval_sec,omitempty\"` // team task recovery ticker interval in seconds (default 300 = 5min)\n\tBackgroundProvider string `json:\"background_provider,omitempty\"` // LLM provider for background workers (vault enrichment, consolidation)\n\tBackgroundModel string `json:\"background_model,omitempty\"` // LLM model for background workers\n}\n\n// ToolsConfig controls tool availability, policy, and web search.\ntype ToolsConfig struct {\n\tProfile string `json:\"profile,omitempty\"` // global profile: \"minimal\", \"coding\", \"messaging\", \"full\"\n\tAllow []string `json:\"allow,omitempty\"` // global allow list (tool names or \"group:xxx\")\n\tDeny []string `json:\"deny,omitempty\"` // global deny list\n\tAlsoAllow []string `json:\"alsoAllow,omitempty\"` // additive: adds without removing existing\n\tByProvider map[string]*ToolPolicySpec `json:\"byProvider,omitempty\"` // per-provider overrides\n\tExecApproval ExecApprovalCfg `json:\"execApproval\"` // exec command approval settings\n\tWebFetch WebFetchPolicyConfig `json:\"web_fetch\"` // domain policy for URL fetching\n\tWeb WebToolsConfig `json:\"web\"`\n\tBrowser BrowserToolConfig `json:\"browser\"`\n\tRateLimitPerHour int `json:\"rate_limit_per_hour,omitempty\"` // max tool executions per hour per session (0 = disabled)\n\tScrubCredentials *bool `json:\"scrub_credentials,omitempty\"` // auto-redact API keys/tokens in tool output (default true)\n\tMcpServers map[string]*MCPServerConfig `json:\"mcp_servers,omitempty\"` // external MCP server connections\n}\n\n// MCPServerConfig configures a single external MCP server connection.\ntype MCPServerConfig struct {\n\tTransport string `json:\"transport\"` // \"stdio\", \"sse\", \"streamable-http\"\n\tCommand string `json:\"command,omitempty\"` // stdio: command to spawn\n\tArgs []string `json:\"args,omitempty\"` // stdio: command arguments\n\tEnv map[string]string `json:\"env,omitempty\"` // stdio: extra environment variables\n\tURL string `json:\"url,omitempty\"` // sse/http: server URL\n\tHeaders map[string]string `json:\"headers,omitempty\"` // sse/http: extra HTTP headers\n\tEnabled *bool `json:\"enabled,omitempty\"` // default true\n\tToolPrefix string `json:\"tool_prefix,omitempty\"` // prefix for tool names (avoids collisions)\n\tTimeoutSec int `json:\"timeout_sec,omitempty\"` // per-tool-call timeout in seconds (default 60)\n}\n\n// IsEnabled returns whether this MCP server is enabled (default true).\nfunc (c *MCPServerConfig) IsEnabled() bool {\n\treturn c.Enabled == nil || *c.Enabled\n}\n\n// ExecApprovalCfg configures command execution approval (matching TS exec-approval.ts).\ntype ExecApprovalCfg struct {\n\tSecurity string `json:\"security,omitempty\"` // \"deny\", \"allowlist\", \"full\" (default \"full\")\n\tAsk string `json:\"ask,omitempty\"` // \"off\", \"on-miss\", \"always\" (default \"off\")\n\tAllowlist []string `json:\"allowlist,omitempty\"` // glob patterns for allowed commands\n}\n\n// WebFetchPolicyConfig controls domain filtering for the web_fetch tool.\ntype WebFetchPolicyConfig struct {\n\tPolicy string `json:\"policy,omitempty\"` // \"allow_all\" (default), \"allowlist\"\n\tAllowedDomains []string `json:\"allowed_domains,omitempty\"` // e.g. [\"github.com\", \"*.example.com\"]\n\tBlockedDomains []string `json:\"blocked_domains,omitempty\"` // always checked regardless of policy\n}\n\n// BrowserToolConfig controls the browser automation tool.\ntype BrowserToolConfig struct {\n\tEnabled bool `json:\"enabled\"` // enable the browser tool (default false)\n\tHeadless bool `json:\"headless,omitempty\"` // run Chrome in headless mode (ignored when RemoteURL is set)\n\tRemoteURL string `json:\"remote_url,omitempty\"` // CDP endpoint for remote Chrome sidecar, e.g. \"ws://chrome:9222\"\n\tActionTimeoutMs int `json:\"action_timeout_ms,omitempty\"` // per-action timeout in ms (default 30000)\n\tIdleTimeoutMs int `json:\"idle_timeout_ms,omitempty\"` // idle page auto-close in ms (default 600000, 0=disabled)\n\tMaxPages int `json:\"max_pages,omitempty\"` // max open pages per tenant (default 5)\n}\n\n// ToolPolicySpec defines a tool policy at any level (global, per-agent, per-provider).\ntype ToolPolicySpec struct {\n\tProfile string `json:\"profile,omitempty\"`\n\tAllow []string `json:\"allow,omitempty\"`\n\tDeny []string `json:\"deny,omitempty\"`\n\tAlsoAllow []string `json:\"alsoAllow,omitempty\"`\n\tByProvider map[string]*ToolPolicySpec `json:\"byProvider,omitempty\"`\n\tToolCallPrefix string `json:\"toolCallPrefix,omitempty\"` // prefix to strip from model's tool call names before registry lookup\n}\n\ntype WebToolsConfig struct {\n\tProviderOrder []string `json:\"provider_order,omitempty\"`\n\tExa ExaConfig `json:\"exa\"`\n\tTavily TavilyConfig `json:\"tavily\"`\n\tBrave BraveConfig `json:\"brave\"`\n\tDuckDuckGo DuckDuckGoConfig `json:\"duckduckgo\"`\n}\n\ntype ExaConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tAPIKey string `json:\"api_key\"`\n\tMaxResults int `json:\"max_results\"`\n}\n\ntype TavilyConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tAPIKey string `json:\"api_key\"`\n\tMaxResults int `json:\"max_results\"`\n}\n\ntype BraveConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tAPIKey string `json:\"api_key\"`\n\tMaxResults int `json:\"max_results\"`\n}\n\ntype DuckDuckGoConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tMaxResults int `json:\"max_results\"`\n}\n\n// SessionsConfig controls session behavior.\n// Matching TS src/config/sessions/types.ts + src/config/types.base.ts.\ntype SessionsConfig struct {\n\tScope string `json:\"scope,omitempty\"` // \"per-sender\" (default), \"global\"\n\tDmScope string `json:\"dm_scope,omitempty\"` // \"main\", \"per-peer\", \"per-channel-peer\" (default), \"per-account-channel-peer\"\n\tMainKey string `json:\"main_key,omitempty\"` // main session key suffix (default \"main\", used when dm_scope=\"main\")\n}\n\n// TtsConfig configures text-to-speech.\n// Matching TS src/config/types.tts.ts.\ntype TtsConfig struct {\n\tProvider string `json:\"provider,omitempty\"` // \"openai\", \"elevenlabs\", \"edge\", \"minimax\"\n\tAuto string `json:\"auto,omitempty\"` // \"off\" (default), \"always\", \"inbound\", \"tagged\"\n\tMode string `json:\"mode,omitempty\"` // \"final\" (default), \"all\"\n\tMaxLength int `json:\"max_length,omitempty\"` // max text length before truncation (default 1500)\n\tTimeoutMs int `json:\"timeout_ms,omitempty\"` // API timeout in ms (default 30000)\n\tOpenAI TtsOpenAIConfig `json:\"openai\"`\n\tElevenLabs TtsElevenLabsConfig `json:\"elevenlabs\"`\n\tEdge TtsEdgeConfig `json:\"edge\"`\n\tMiniMax TtsMiniMaxConfig `json:\"minimax\"`\n}\n\n// TtsOpenAIConfig configures the OpenAI TTS provider.\ntype TtsOpenAIConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"`\n\tAPIBase string `json:\"api_base,omitempty\"` // custom endpoint URL\n\tModel string `json:\"model,omitempty\"` // default \"gpt-4o-mini-tts\"\n\tVoice string `json:\"voice,omitempty\"` // default \"alloy\"\n}\n\n// TtsElevenLabsConfig configures the ElevenLabs TTS provider.\ntype TtsElevenLabsConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"`\n\tBaseURL string `json:\"base_url,omitempty\"`\n\tVoiceID string `json:\"voice_id,omitempty\"` // default \"pMsXgVXv3BLzUgSXRplE\"\n\tModelID string `json:\"model_id,omitempty\"` // default \"eleven_multilingual_v2\"\n}\n\n// TtsEdgeConfig configures the Microsoft Edge TTS provider (free, no API key).\ntype TtsEdgeConfig struct {\n\tEnabled bool `json:\"enabled,omitempty\"`\n\tVoice string `json:\"voice,omitempty\"` // default \"en-US-MichelleNeural\"\n\tRate string `json:\"rate,omitempty\"` // speech rate, e.g. \"+0%\"\n}\n\n// TtsMiniMaxConfig configures the MiniMax TTS provider.\ntype TtsMiniMaxConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"`\n\tGroupID string `json:\"group_id,omitempty\"` // MiniMax GroupId (required)\n\tAPIBase string `json:\"api_base,omitempty\"` // default \"https://api.minimax.io/v1\"\n\tModel string `json:\"model,omitempty\"` // default \"speech-02-hd\"\n\tVoiceID string `json:\"voice_id,omitempty\"` // default \"Wise_Woman\"\n}\n\n// MergeChannelGroupQuotas merges per-group quota overrides from channel configs\n// (e.g., channels.telegram.groups[chatID].quota) into gateway.quota.groups.\n// This allows per-group quotas to be set at the channel level and picked up\n// by the QuotaChecker at runtime.\nfunc MergeChannelGroupQuotas(cfg *Config) {\n\tif cfg.Gateway.Quota == nil {\n\t\treturn\n\t}\n\tif cfg.Gateway.Quota.Groups == nil {\n\t\tcfg.Gateway.Quota.Groups = make(map[string]QuotaWindow)\n\t}\n\n\t// Telegram per-group quotas\n\tfor chatID, groupCfg := range cfg.Channels.Telegram.Groups {\n\t\tif groupCfg != nil && groupCfg.Quota != nil && !groupCfg.Quota.IsZero() {\n\t\t\tkey := \"group:telegram:\" + chatID\n\t\t\tcfg.Gateway.Quota.Groups[key] = *groupCfg.Quota\n\t\t}\n\t}\n}\n"}, "9f77bfe711a1c3ad7cc671ee004dd1b5cfc848dd": {"t": 1785759251.204894, "n": 32312, "h": "11bae06512817a0a", "c": "package config\n\n// PendingCompactionConfig configures LLM-based compaction of pending group messages.\n// When a group accumulates more than Threshold pending messages, older messages are\n// summarized by an LLM and replaced with a compact summary, keeping KeepRecent raw messages.\ntype PendingCompactionConfig struct {\n\tThreshold int `json:\"threshold,omitempty\"` // trigger compaction when entries exceed this (default 200)\n\tKeepRecent int `json:\"keep_recent,omitempty\"` // keep this many recent raw messages after compaction (default 40)\n\tMaxTokens int `json:\"max_tokens,omitempty\"` // max output tokens for LLM summarization (default 4096)\n\tProvider string `json:\"provider,omitempty\"` // LLM provider name (e.g. \"openai\"); empty = use agent's provider\n\tModel string `json:\"model,omitempty\"` // model for summarization; empty = use agent's model\n}\n\n// ChannelsConfig contains per-channel configuration.\ntype ChannelsConfig struct {\n\tTelegram TelegramConfig `json:\"telegram\"`\n\tDiscord DiscordConfig `json:\"discord\"`\n\tSlack SlackConfig `json:\"slack\"`\n\tWhatsApp WhatsAppConfig `json:\"whatsapp\"`\n\tZalo ZaloConfig `json:\"zalo\"`\n\tZaloPersonal ZaloPersonalConfig `json:\"zalo_personal\"`\n\tFeishu FeishuConfig `json:\"feishu\"`\n\tPendingCompaction *PendingCompactionConfig `json:\"pending_compaction,omitempty\"` // global pending message compaction settings\n}\n\ntype TelegramConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tToken string `json:\"token\"`\n\tProxy string `json:\"proxy,omitempty\"`\n\tAPIServer string `json:\"api_server,omitempty\"` // custom Telegram Bot API server URL (e.g. \"http://localhost:8081\")\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in groups (default true)\n\tMentionMode string `json:\"mention_mode,omitempty\"` // \"strict\" (default) = only respond when mentioned; \"yield\" = respond unless another bot is mentioned\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tDMStream *bool `json:\"dm_stream,omitempty\"` // enable streaming for DMs (default false) — edits placeholder progressively\n\tGroupStream *bool `json:\"group_stream,omitempty\"` // enable streaming for groups (default false) — sends new message, edits progressively\n\tDraftTransport *bool `json:\"draft_transport,omitempty\"` // use sendMessageDraft for DM streaming (default true) — stealth preview, no notifications per edit\n\tReasoningStream *bool `json:\"reasoning_stream,omitempty\"` // show reasoning as separate message when provider emits thinking events (default true)\n\tReactionLevel string `json:\"reaction_level,omitempty\"` // \"off\" (default), \"minimal\", \"full\" — status emoji reactions\n\tMediaMaxBytes int64 `json:\"media_max_bytes,omitempty\"` // max media download size in bytes (default 20MB)\n\tLinkPreview *bool `json:\"link_preview,omitempty\"` // enable URL previews in messages (default true)\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tForceIPv4 bool `json:\"force_ipv4,omitempty\"` // force IPv4 for all Telegram API requests (use when IPv6 routing is broken)\n\n\t// Optional STT (Speech-to-Text) pipeline for voice/audio inbound messages.\n\t// When stt_proxy_url is set, audio/voice messages are transcribed before being forwarded to the agent.\n\tSTTProxyURL string `json:\"stt_proxy_url,omitempty\"` // base URL of the STT proxy service (e.g. \"https://stt.example.com\")\n\tSTTAPIKey string `json:\"stt_api_key,omitempty\"` // Bearer token for the STT proxy\n\tSTTTenantID string `json:\"stt_tenant_id,omitempty\"` // optional tenant/org identifier forwarded to the STT proxy\n\tSTTTimeoutSeconds int `json:\"stt_timeout_seconds,omitempty\"` // per-request timeout for STT calls (default 30s)\n\n\t// Optional audio-aware routing: when set, voice/audio inbound messages are routed to this\n\t// agent instead of the default channel agent. Requires the named agent to exist in the config.\n\tVoiceAgentID string `json:\"voice_agent_id,omitempty\"` // agent ID to route voice inbound to (e.g. \"speaking-agent\")\n\n\t// Audio guard: intercept technical errors in voice agent replies and replace with friendly fallbacks.\n\t// Only active when VoiceAgentID is set. Custom error markers replace built-in defaults when provided.\n\tAudioGuardFallbackTranscript string `json:\"audio_guard_fallback_transcript,omitempty\"` // fallback with %s for transcript (e.g. \"I heard: \\\"%s\\\". Try again!\")\n\tAudioGuardFallbackNoTranscript string `json:\"audio_guard_fallback_no_transcript,omitempty\"` // fallback when no transcript available\n\tAudioGuardErrorMarkers []string `json:\"audio_guard_error_markers,omitempty\"` // custom error detection markers (replaces defaults)\n\n\t// Per-group (and per-topic) overrides. Key is chat ID string (e.g. \"-100123456\") or \"*\" for wildcard.\n\t// TS ref: channels.telegram.groups in src/config/types.telegram.ts.\n\tGroups map[string]*TelegramGroupConfig `json:\"groups,omitempty\"`\n}\n\n// TelegramGroupConfig defines per-group overrides for a Telegram channel.\n// Matching TS TelegramGroupConfig in src/config/types.telegram.ts.\ntype TelegramGroupConfig struct {\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // override group policy for this group\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // override require_mention for this group\n\tMentionMode string `json:\"mention_mode,omitempty\"` // override mention_mode for this group\n\tAllowFrom FlexibleStringSlice `json:\"allow_from,omitempty\"` // override allow_from for this group\n\tEnabled *bool `json:\"enabled,omitempty\"` // disable bot for this group (default: true)\n\tSkills []string `json:\"skills,omitempty\"` // skill whitelist (nil = all, [] = none)\n\tTools []string `json:\"tools,omitempty\"` // tool allow list (nil = all, supports \"group:xxx\")\n\tSystemPrompt string `json:\"system_prompt,omitempty\"` // extra system prompt for this group\n\tTopics map[string]*TelegramTopicConfig `json:\"topics,omitempty\"` // per-topic overrides (key: thread ID string)\n\tQuota *QuotaWindow `json:\"quota,omitempty\"` // per-group quota override\n}\n\n// TelegramTopicConfig defines per-topic overrides within a Telegram group.\n// Matching TS TelegramTopicConfig in src/config/types.telegram.ts.\ntype TelegramTopicConfig struct {\n\tRequireMention *bool `json:\"require_mention,omitempty\"`\n\tMentionMode string `json:\"mention_mode,omitempty\"`\n\tGroupPolicy string `json:\"group_policy,omitempty\"`\n\tSkills []string `json:\"skills,omitempty\"`\n\tTools []string `json:\"tools,omitempty\"` // tool allow list (nil = inherit, supports \"group:xxx\")\n\tEnabled *bool `json:\"enabled,omitempty\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from,omitempty\"`\n\tSystemPrompt string `json:\"system_prompt,omitempty\"`\n}\n\ntype DiscordConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tToken string `json:\"token\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in groups (default true)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tMediaMaxBytes int64 `json:\"media_max_bytes,omitempty\"` // max media download size (default 25MB)\n\tSTTProxyURL string `json:\"stt_proxy_url,omitempty\"`\n\tSTTAPIKey string `json:\"stt_api_key,omitempty\"`\n\tSTTTenantID string `json:\"stt_tenant_id,omitempty\"`\n\tSTTTimeoutSeconds int `json:\"stt_timeout_seconds,omitempty\"`\n\tVoiceAgentID string `json:\"voice_agent_id,omitempty\"`\n}\n\ntype SlackConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tBotToken string `json:\"bot_token\"` // xoxb-... (Bot User OAuth Token)\n\tAppToken string `json:\"app_token\"` // xapp-... (App-Level Token for Socket Mode)\n\tUserToken string `json:\"user_token,omitempty\"` // xoxp-... (Optional: custom bot identity)\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"pairing\", \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in channels (default true)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tDMStream *bool `json:\"dm_stream,omitempty\"` // enable streaming for DMs (default false)\n\tGroupStream *bool `json:\"group_stream,omitempty\"` // enable streaming for groups (default false)\n\tNativeStream *bool `json:\"native_stream,omitempty\"` // use Slack ChatStreamer API if available (default false)\n\tReactionLevel string `json:\"reaction_level,omitempty\"` // \"off\" (default), \"minimal\", \"full\"\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tDebounceDelay int `json:\"debounce_delay,omitempty\"` // ms delay before dispatching rapid messages (default 300, 0=disabled)\n\tThreadTTL *int `json:\"thread_ttl,omitempty\"` // hours before thread participation expires (default 24, 0=disabled — always require @mention)\n\tMediaMaxBytes int64 `json:\"media_max_bytes,omitempty\"` // max file download size in bytes (default 20MB)\n}\n\ntype WhatsAppConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tAuthDir string `json:\"auth_dir,omitempty\"` // optional: SQLite auth dir override (desktop)\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default for DB instances), \"open\", \"allowlist\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"pairing\" (default for DB instances), \"open\" (default for config), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // only respond in groups when bot is @mentioned (default false)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 200, 0=disabled)\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n}\n\ntype ZaloConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tToken string `json:\"token\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tWebhookURL string `json:\"webhook_url,omitempty\"`\n\tWebhookSecret string `json:\"webhook_secret,omitempty\"`\n\tMediaMaxMB int `json:\"media_max_mb,omitempty\"` // default 5\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n}\n\ntype ZaloPersonalConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in groups (default true)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tCredentialsPath string `json:\"credentials_path,omitempty\"` // path to saved cookies JSON\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n}\n\ntype FeishuConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tAppID string `json:\"app_id\"`\n\tAppSecret string `json:\"app_secret\"`\n\tEncryptKey string `json:\"encrypt_key,omitempty\"`\n\tVerificationToken string `json:\"verification_token,omitempty\"`\n\tDomain string `json:\"domain,omitempty\"` // \"lark\" (default/global), \"feishu\" (China), or custom URL\n\tConnectionMode string `json:\"connection_mode,omitempty\"` // \"websocket\" (default), \"webhook\"\n\tWebhookPort int `json:\"webhook_port,omitempty\"` // default 3000\n\tWebhookPath string `json:\"webhook_path,omitempty\"` // default \"/feishu/events\"\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default)\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default)\n\tGroupAllowFrom FlexibleStringSlice `json:\"group_allow_from,omitempty\"`\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // default true (groups)\n\tTopicSessionMode string `json:\"topic_session_mode,omitempty\"` // \"disabled\" (default)\n\tTextChunkLimit int `json:\"text_chunk_limit,omitempty\"` // default 4000\n\tMediaMaxMB int `json:\"media_max_mb,omitempty\"` // default 30\n\tRenderMode string `json:\"render_mode,omitempty\"` // \"auto\", \"raw\", \"card\"\n\tStreaming *bool `json:\"streaming,omitempty\"` // default true\n\tReactionLevel string `json:\"reaction_level,omitempty\"` // \"off\" (default), \"minimal\", \"full\" — typing emoji reactions\n\tHistoryLimit int `json:\"history_limit,omitempty\"`\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tSTTProxyURL string `json:\"stt_proxy_url,omitempty\"`\n\tSTTAPIKey string `json:\"stt_api_key,omitempty\"`\n\tSTTTenantID string `json:\"stt_tenant_id,omitempty\"`\n\tSTTTimeoutSeconds int `json:\"stt_timeout_seconds,omitempty\"`\n\tVoiceAgentID string `json:\"voice_agent_id,omitempty\"`\n}\n\n// ProvidersConfig maps provider name to its config.\ntype ProvidersConfig struct {\n\tAnthropic ProviderConfig `json:\"anthropic\"`\n\tOpenAI ProviderConfig `json:\"openai\"`\n\tOpenRouter ProviderConfig `json:\"openrouter\"`\n\tGroq ProviderConfig `json:\"groq\"`\n\tGemini ProviderConfig `json:\"gemini\"`\n\tDeepSeek ProviderConfig `json:\"deepseek\"`\n\tMistral ProviderConfig `json:\"mistral\"`\n\tXAI ProviderConfig `json:\"xai\"`\n\tMiniMax ProviderConfig `json:\"minimax\"`\n\tCohere ProviderConfig `json:\"cohere\"`\n\tPerplexity ProviderConfig `json:\"perplexity\"`\n\tDashScope ProviderConfig `json:\"dashscope\"`\n\tBailian ProviderConfig `json:\"bailian\"`\n\tZai ProviderConfig `json:\"zai\"`\n\tZaiCoding ProviderConfig `json:\"zai_coding\"`\n\tOllama OllamaConfig `json:\"ollama\"` // local Ollama instance (no API key needed)\n\tOllamaCloud ProviderConfig `json:\"ollama_cloud\"` // Ollama Cloud (API key required)\n\tClaudeCLI ClaudeCLIConfig `json:\"claude_cli\"`\n\tACP ACPConfig `json:\"acp\"`\n\tNovita ProviderConfig `json:\"novita\"` // Novita AI (OpenAI-compatible endpoint)\n\tBytePlus ProviderConfig `json:\"byteplus\"` // BytePlus ModelArk (Seed 2.0)\n\tBytePlusCoding ProviderConfig `json:\"byteplus_coding\"` // BytePlus ModelArk Coding Plan\n}\n\n// OllamaConfig configures a local (or self-hosted) Ollama instance.\n// No API key is required — Ollama accepts any Bearer token value.\ntype OllamaConfig struct {\n\tHost string `json:\"host\"` // Ollama server base URL, e.g. http://localhost:11434\n}\n\n// ClaudeCLIConfig configures the Claude CLI provider (uses subscription, not API key).\ntype ClaudeCLIConfig struct {\n\tCLIPath string `json:\"cli_path\" yaml:\"cli_path\"` // path to claude binary (default: \"claude\")\n\tModel string `json:\"model\" yaml:\"model\"` // default model alias (default: \"sonnet\")\n\tBaseWorkDir string `json:\"base_work_dir\" yaml:\"base_work_dir\"` // base dir for agent workspaces\n\tPermMode string `json:\"perm_mode\" yaml:\"perm_mode\"` // permission mode (default: \"bypassPermissions\")\n}\n\n// ACPConfig configures the ACP (Agent Client Protocol) provider.\n// Orchestrates any ACP-compatible coding agent (Claude Code, Codex CLI, Gemini CLI) as a subprocess.\ntype ACPConfig struct {\n\tBinary string `json:\"binary\"` // agent binary name or path (e.g. \"claude\", \"codex\")\n\tArgs []string `json:\"args\"` // extra spawn args\n\tModel string `json:\"model\"` // default model/agent name\n\tWorkDir string `json:\"work_dir\"` // base workspace dir\n\tIdleTTL string `json:\"idle_ttl\"` // process idle TTL (e.g. \"5m\")\n\tPermMode string `json:\"perm_mode\"` // \"approve-all\" (default), \"approve-reads\", \"deny-all\"\n}\n\ntype ProviderConfig struct {\n\tAPIKey string `json:\"api_key\"`\n\tAPIBase string `json:\"api_base,omitempty\"`\n}\n\n// APIBaseForType returns the config-level api_base for a given provider type.\n// Used as a fallback when DB providers have no api_base set.\nfunc (p *ProvidersConfig) APIBaseForType(providerType string) string {\n\tswitch providerType {\n\tcase \"anthropic_native\":\n\t\treturn p.Anthropic.APIBase\n\tcase \"openai\", \"openai_compat\":\n\t\treturn p.OpenAI.APIBase\n\tcase \"openrouter\":\n\t\treturn p.OpenRouter.APIBase\n\tcase \"groq\":\n\t\treturn p.Groq.APIBase\n\tcase \"deepseek\":\n\t\treturn p.DeepSeek.APIBase\n\tcase \"gemini_native\":\n\t\treturn p.Gemini.APIBase\n\tcase \"mistral\":\n\t\treturn p.Mistral.APIBase\n\tcase \"xai\":\n\t\treturn p.XAI.APIBase\n\tcase \"minimax_native\":\n\t\treturn p.MiniMax.APIBase\n\tcase \"cohere\":\n\t\treturn p.Cohere.APIBase\n\tcase \"perplexity\":\n\t\treturn p.Perplexity.APIBase\n\tcase \"dashscope\":\n\t\treturn p.DashScope.APIBase\n\tcase \"bailian\":\n\t\treturn p.Bailian.APIBase\n\tcase \"zai\":\n\t\treturn p.Zai.APIBase\n\tcase \"zai_coding\":\n\t\treturn p.ZaiCoding.APIBase\n\tcase \"ollama_cloud\":\n\t\treturn p.OllamaCloud.APIBase\n\tcase \"novita\":\n\t\treturn p.Novita.APIBase\n\tcase \"byteplus\":\n\t\treturn p.BytePlus.APIBase\n\tcase \"byteplus_coding\":\n\t\treturn p.BytePlusCoding.APIBase\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\n// HasAnyProvider returns true if at least one provider has an API key or CLI configured.\nfunc (c *Config) HasAnyProvider() bool {\n\tp := c.Providers\n\treturn p.Anthropic.APIKey != \"\" ||\n\t\tp.OpenAI.APIKey != \"\" ||\n\t\tp.OpenRouter.APIKey != \"\" ||\n\t\tp.Groq.APIKey != \"\" ||\n\t\tp.Gemini.APIKey != \"\" ||\n\t\tp.DeepSeek.APIKey != \"\" ||\n\t\tp.Mistral.APIKey != \"\" ||\n\t\tp.XAI.APIKey != \"\" ||\n\t\tp.MiniMax.APIKey != \"\" ||\n\t\tp.Cohere.APIKey != \"\" ||\n\t\tp.Perplexity.APIKey != \"\" ||\n\t\tp.DashScope.APIKey != \"\" ||\n\t\tp.Bailian.APIKey != \"\" ||\n\t\tp.Zai.APIKey != \"\" ||\n\t\tp.ZaiCoding.APIKey != \"\" ||\n\t\tp.Ollama.Host != \"\" ||\n\t\tp.OllamaCloud.APIKey != \"\" ||\n\t\tp.ClaudeCLI.CLIPath != \"\" ||\n\t\tp.ACP.Binary != \"\" ||\n\t\tp.Novita.APIKey != \"\" ||\n\t\tp.BytePlus.APIKey != \"\" ||\n\t\tp.BytePlusCoding.APIKey != \"\"\n}\n\n// QuotaWindow defines request limits per time window. Zero means unlimited.\ntype QuotaWindow struct {\n\tHour int `json:\"hour,omitempty\"` // max requests per hour (0 = unlimited)\n\tDay int `json:\"day,omitempty\"` // max requests per day (0 = unlimited)\n\tWeek int `json:\"week,omitempty\"` // max requests per week (0 = unlimited)\n}\n\n// IsZero returns true if no limits are set.\nfunc (w QuotaWindow) IsZero() bool { return w.Hour == 0 && w.Day == 0 && w.Week == 0 }\n\n// QuotaConfig configures per-user/group request quotas.\n// Config merge priority: Groups > Channels > Providers > Default.\ntype QuotaConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tDefault QuotaWindow `json:\"default\"`\n\tProviders map[string]QuotaWindow `json:\"providers,omitempty\"` // key = provider name (e.g. \"anthropic\")\n\tChannels map[string]QuotaWindow `json:\"channels,omitempty\"` // key = channel name (e.g. \"telegram\")\n\tGroups map[string]QuotaWindow `json:\"groups,omitempty\"` // key = userID (e.g. \"group:telegram:-100123\")\n}\n\n// GatewayConfig controls the gateway server.\ntype GatewayConfig struct {\n\tHost string `json:\"host\"`\n\tPort int `json:\"port\"`\n\tToken string `json:\"token,omitempty\"` // bearer token for WS/HTTP auth\n\tOwnerIDs []string `json:\"owner_ids,omitempty\"` // sender IDs considered \"owner\"\n\tAllowedOrigins []string `json:\"allowed_origins,omitempty\"` // WebSocket CORS whitelist (empty = allow all)\n\tMaxMessageChars int `json:\"max_message_chars,omitempty\"` // max user message characters (default 32000)\n\tRateLimitRPM int `json:\"rate_limit_rpm,omitempty\"` // rate limit: requests per minute per user (default 20, 0 = disabled)\n\tInjectionAction string `json:\"injection_action,omitempty\"` // prompt injection action: \"log\", \"warn\" (default), \"block\", \"off\"\n\tInboundDebounceMs int `json:\"inbound_debounce_ms,omitempty\"` // merge rapid messages from same sender (default 1000ms, -1 = disabled)\n\tQuota *QuotaConfig `json:\"quota,omitempty\"` // per-user/group request quotas\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // deliver intermediate text during tool iterations (default false)\n\tToolStatus *bool `json:\"tool_status,omitempty\"` // show tool name in streaming preview during tool execution (default true)\n\tTaskRecoveryIntervalSec int `json:\"task_recovery_interval_sec,omitempty\"` // team task recovery ticker interval in seconds (default 300 = 5min)\n\tBackgroundProvider string `json:\"background_provider,omitempty\"` // LLM provider for background workers (vault enrichment, consolidation)\n\tBackgroundModel string `json:\"background_model,omitempty\"` // LLM model for background workers\n}\n\n// ToolsConfig controls tool availability, policy, and web search.\ntype ToolsConfig struct {\n\tProfile string `json:\"profile,omitempty\"` // global profile: \"minimal\", \"coding\", \"messaging\", \"full\"\n\tAllow []string `json:\"allow,omitempty\"` // global allow list (tool names or \"group:xxx\")\n\tDeny []string `json:\"deny,omitempty\"` // global deny list\n\tAlsoAllow []string `json:\"alsoAllow,omitempty\"` // additive: adds without removing existing\n\tByProvider map[string]*ToolPolicySpec `json:\"byProvider,omitempty\"` // per-provider overrides\n\tExecApproval ExecApprovalCfg `json:\"execApproval\"` // exec command approval settings\n\tWebFetch WebFetchPolicyConfig `json:\"web_fetch\"` // domain policy for URL fetching\n\tWeb WebToolsConfig `json:\"web\"`\n\tBrowser BrowserToolConfig `json:\"browser\"`\n\tRateLimitPerHour int `json:\"rate_limit_per_hour,omitempty\"` // max tool executions per hour per session (0 = disabled)\n\tScrubCredentials *bool `json:\"scrub_credentials,omitempty\"` // auto-redact API keys/tokens in tool output (default true)\n\tMcpServers map[string]*MCPServerConfig `json:\"mcp_servers,omitempty\"` // external MCP server connections\n}\n\n// MCPServerConfig configures a single external MCP server connection.\ntype MCPServerConfig struct {\n\tTransport string `json:\"transport\"` // \"stdio\", \"sse\", \"streamable-http\"\n\tCommand string `json:\"command,omitempty\"` // stdio: command to spawn\n\tArgs []string `json:\"args,omitempty\"` // stdio: command arguments\n\tEnv map[string]string `json:\"env,omitempty\"` // stdio: extra environment variables\n\tURL string `json:\"url,omitempty\"` // sse/http: server URL\n\tHeaders map[string]string `json:\"headers,omitempty\"` // sse/http: extra HTTP headers\n\tEnabled *bool `json:\"enabled,omitempty\"` // default true\n\tToolPrefix string `json:\"tool_prefix,omitempty\"` // prefix for tool names (avoids collisions)\n\tTimeoutSec int `json:\"timeout_sec,omitempty\"` // per-tool-call timeout in seconds (default 60)\n}\n\n// IsEnabled returns whether this MCP server is enabled (default true).\nfunc (c *MCPServerConfig) IsEnabled() bool {\n\treturn c.Enabled == nil || *c.Enabled\n}\n\n// ExecApprovalCfg configures command execution approval (matching TS exec-approval.ts).\ntype ExecApprovalCfg struct {\n\tSecurity string `json:\"security,omitempty\"` // \"deny\", \"allowlist\", \"full\" (default \"full\")\n\tAsk string `json:\"ask,omitempty\"` // \"off\", \"on-miss\", \"always\" (default \"off\")\n\tAllowlist []string `json:\"allowlist,omitempty\"` // glob patterns for allowed commands\n}\n\n// WebFetchPolicyConfig controls domain filtering for the web_fetch tool.\ntype WebFetchPolicyConfig struct {\n\tPolicy string `json:\"policy,omitempty\"` // \"allow_all\" (default), \"allowlist\"\n\tAllowedDomains []string `json:\"allowed_domains,omitempty\"` // e.g. [\"github.com\", \"*.example.com\"]\n\tBlockedDomains []string `json:\"blocked_domains,omitempty\"` // always checked regardless of policy\n}\n\n// BrowserToolConfig controls the browser automation tool.\ntype BrowserToolConfig struct {\n\tEnabled bool `json:\"enabled\"` // enable the browser tool (default false)\n\tHeadless bool `json:\"headless,omitempty\"` // run Chrome in headless mode (ignored when RemoteURL is set)\n\tRemoteURL string `json:\"remote_url,omitempty\"` // CDP endpoint for remote Chrome sidecar, e.g. \"ws://chrome:9222\"\n\tActionTimeoutMs int `json:\"action_timeout_ms,omitempty\"` // per-action timeout in ms (default 30000)\n\tIdleTimeoutMs int `json:\"idle_timeout_ms,omitempty\"` // idle page auto-close in ms (default 600000, 0=disabled)\n\tMaxPages int `json:\"max_pages,omitempty\"` // max open pages per tenant (default 5)\n}\n\n// ToolPolicySpec defines a tool policy at any level (global, per-agent, per-provider).\ntype ToolPolicySpec struct {\n\tProfile string `json:\"profile,omitempty\"`\n\tAllow []string `json:\"allow,omitempty\"`\n\tDeny []string `json:\"deny,omitempty\"`\n\tAlsoAllow []string `json:\"alsoAllow,omitempty\"`\n\tByProvider map[string]*ToolPolicySpec `json:\"byProvider,omitempty\"`\n\tToolCallPrefix string `json:\"toolCallPrefix,omitempty\"` // prefix to strip from model's tool call names before registry lookup\n}\n\ntype WebToolsConfig struct {\n\tBrave BraveConfig `json:\"brave\"`\n\tDuckDuckGo DuckDuckGoConfig `json:\"duckduckgo\"`\n}\n\ntype BraveConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tAPIKey string `json:\"api_key\"`\n\tMaxResults int `json:\"max_results\"`\n}\n\ntype DuckDuckGoConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tMaxResults int `json:\"max_results\"`\n}\n\n// SessionsConfig controls session behavior.\n// Matching TS src/config/sessions/types.ts + src/config/types.base.ts.\ntype SessionsConfig struct {\n\tScope string `json:\"scope,omitempty\"` // \"per-sender\" (default), \"global\"\n\tDmScope string `json:\"dm_scope,omitempty\"` // \"main\", \"per-peer\", \"per-channel-peer\" (default), \"per-account-channel-peer\"\n\tMainKey string `json:\"main_key,omitempty\"` // main session key suffix (default \"main\", used when dm_scope=\"main\")\n}\n\n// TtsConfig configures text-to-speech.\n// Matching TS src/config/types.tts.ts.\ntype TtsConfig struct {\n\tProvider string `json:\"provider,omitempty\"` // \"openai\", \"elevenlabs\", \"edge\", \"minimax\"\n\tAuto string `json:\"auto,omitempty\"` // \"off\" (default), \"always\", \"inbound\", \"tagged\"\n\tMode string `json:\"mode,omitempty\"` // \"final\" (default), \"all\"\n\tMaxLength int `json:\"max_length,omitempty\"` // max text length before truncation (default 1500)\n\tTimeoutMs int `json:\"timeout_ms,omitempty\"` // API timeout in ms (default 30000)\n\tOpenAI TtsOpenAIConfig `json:\"openai\"`\n\tElevenLabs TtsElevenLabsConfig `json:\"elevenlabs\"`\n\tEdge TtsEdgeConfig `json:\"edge\"`\n\tMiniMax TtsMiniMaxConfig `json:\"minimax\"`\n}\n\n// TtsOpenAIConfig configures the OpenAI TTS provider.\ntype TtsOpenAIConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"`\n\tAPIBase string `json:\"api_base,omitempty\"` // custom endpoint URL\n\tModel string `json:\"model,omitempty\"` // default \"gpt-4o-mini-tts\"\n\tVoice string `json:\"voice,omitempty\"` // default \"alloy\"\n}\n\n// TtsElevenLabsConfig configures the ElevenLabs TTS provider.\ntype TtsElevenLabsConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"`\n\tBaseURL string `json:\"base_url,omitempty\"`\n\tVoiceID string `json:\"voice_id,omitempty\"` // default \"pMsXgVXv3BLzUgSXRplE\"\n\tModelID string `json:\"model_id,omitempty\"` // default \"eleven_multilingual_v2\"\n}\n\n// TtsEdgeConfig configures the Microsoft Edge TTS provider (free, no API key).\ntype TtsEdgeConfig struct {\n\tEnabled bool `json:\"enabled,omitempty\"`\n\tVoice string `json:\"voice,omitempty\"` // default \"en-US-MichelleNeural\"\n\tRate string `json:\"rate,omitempty\"` // speech rate, e.g. \"+0%\"\n}\n\n// TtsMiniMaxConfig configures the MiniMax TTS provider.\ntype TtsMiniMaxConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"`\n\tGroupID string `json:\"group_id,omitempty\"` // MiniMax GroupId (required)\n\tAPIBase string `json:\"api_base,omitempty\"` // default \"https://api.minimax.io/v1\"\n\tModel string `json:\"model,omitempty\"` // default \"speech-02-hd\"\n\tVoiceID string `json:\"voice_id,omitempty\"` // default \"Wise_Woman\"\n}\n\n// MergeChannelGroupQuotas merges per-group quota overrides from channel configs\n// (e.g., channels.telegram.groups[chatID].quota) into gateway.quota.groups.\n// This allows per-group quotas to be set at the channel level and picked up\n// by the QuotaChecker at runtime.\nfunc MergeChannelGroupQuotas(cfg *Config) {\n\tif cfg.Gateway.Quota == nil {\n\t\treturn\n\t}\n\tif cfg.Gateway.Quota.Groups == nil {\n\t\tcfg.Gateway.Quota.Groups = make(map[string]QuotaWindow)\n\t}\n\n\t// Telegram per-group quotas\n\tfor chatID, groupCfg := range cfg.Channels.Telegram.Groups {\n\t\tif groupCfg != nil && groupCfg.Quota != nil && !groupCfg.Quota.IsZero() {\n\t\t\tkey := \"group:telegram:\" + chatID\n\t\t\tcfg.Gateway.Quota.Groups[key] = *groupCfg.Quota\n\t\t}\n\t}\n}\n"}, "0db1e93abf451d36f5df9c2a151291608e40f635": {"t": 1785759252.5630605, "n": 32035, "h": "f246c6812fe37b14", "c": "package config\n\n// PendingCompactionConfig configures LLM-based compaction of pending group messages.\n// When a group accumulates more than Threshold pending messages, older messages are\n// summarized by an LLM and replaced with a compact summary, keeping KeepRecent raw messages.\ntype PendingCompactionConfig struct {\n\tThreshold int `json:\"threshold,omitempty\"` // trigger compaction when entries exceed this (default 200)\n\tKeepRecent int `json:\"keep_recent,omitempty\"` // keep this many recent raw messages after compaction (default 40)\n\tMaxTokens int `json:\"max_tokens,omitempty\"` // max output tokens for LLM summarization (default 4096)\n\tProvider string `json:\"provider,omitempty\"` // LLM provider name (e.g. \"openai\"); empty = use agent's provider\n\tModel string `json:\"model,omitempty\"` // model for summarization; empty = use agent's model\n}\n\n// ChannelsConfig contains per-channel configuration.\ntype ChannelsConfig struct {\n\tTelegram TelegramConfig `json:\"telegram\"`\n\tDiscord DiscordConfig `json:\"discord\"`\n\tSlack SlackConfig `json:\"slack\"`\n\tWhatsApp WhatsAppConfig `json:\"whatsapp\"`\n\tZalo ZaloConfig `json:\"zalo\"`\n\tZaloPersonal ZaloPersonalConfig `json:\"zalo_personal\"`\n\tFeishu FeishuConfig `json:\"feishu\"`\n\tPendingCompaction *PendingCompactionConfig `json:\"pending_compaction,omitempty\"` // global pending message compaction settings\n}\n\ntype TelegramConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tToken string `json:\"token\"`\n\tProxy string `json:\"proxy,omitempty\"`\n\tAPIServer string `json:\"api_server,omitempty\"` // custom Telegram Bot API server URL (e.g. \"http://localhost:8081\")\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in groups (default true)\n\tMentionMode string `json:\"mention_mode,omitempty\"` // \"strict\" (default) = only respond when mentioned; \"yield\" = respond unless another bot is mentioned\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tDMStream *bool `json:\"dm_stream,omitempty\"` // enable streaming for DMs (default false) — edits placeholder progressively\n\tGroupStream *bool `json:\"group_stream,omitempty\"` // enable streaming for groups (default false) — sends new message, edits progressively\n\tDraftTransport *bool `json:\"draft_transport,omitempty\"` // use sendMessageDraft for DM streaming (default true) — stealth preview, no notifications per edit\n\tReasoningStream *bool `json:\"reasoning_stream,omitempty\"` // show reasoning as separate message when provider emits thinking events (default true)\n\tReactionLevel string `json:\"reaction_level,omitempty\"` // \"off\" (default), \"minimal\", \"full\" — status emoji reactions\n\tMediaMaxBytes int64 `json:\"media_max_bytes,omitempty\"` // max media download size in bytes (default 20MB)\n\tLinkPreview *bool `json:\"link_preview,omitempty\"` // enable URL previews in messages (default true)\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tForceIPv4 bool `json:\"force_ipv4,omitempty\"` // force IPv4 for all Telegram API requests (use when IPv6 routing is broken)\n\n\t// Optional STT (Speech-to-Text) pipeline for voice/audio inbound messages.\n\t// When stt_proxy_url is set, audio/voice messages are transcribed before being forwarded to the agent.\n\tSTTProxyURL string `json:\"stt_proxy_url,omitempty\"` // base URL of the STT proxy service (e.g. \"https://stt.example.com\")\n\tSTTAPIKey string `json:\"stt_api_key,omitempty\"` // Bearer token for the STT proxy\n\tSTTTenantID string `json:\"stt_tenant_id,omitempty\"` // optional tenant/org identifier forwarded to the STT proxy\n\tSTTTimeoutSeconds int `json:\"stt_timeout_seconds,omitempty\"` // per-request timeout for STT calls (default 30s)\n\n\t// Optional audio-aware routing: when set, voice/audio inbound messages are routed to this\n\t// agent instead of the default channel agent. Requires the named agent to exist in the config.\n\tVoiceAgentID string `json:\"voice_agent_id,omitempty\"` // agent ID to route voice inbound to (e.g. \"speaking-agent\")\n\n\t// Audio guard: intercept technical errors in voice agent replies and replace with friendly fallbacks.\n\t// Only active when VoiceAgentID is set. Custom error markers replace built-in defaults when provided.\n\tAudioGuardFallbackTranscript string `json:\"audio_guard_fallback_transcript,omitempty\"` // fallback with %s for transcript (e.g. \"I heard: \\\"%s\\\". Try again!\")\n\tAudioGuardFallbackNoTranscript string `json:\"audio_guard_fallback_no_transcript,omitempty\"` // fallback when no transcript available\n\tAudioGuardErrorMarkers []string `json:\"audio_guard_error_markers,omitempty\"` // custom error detection markers (replaces defaults)\n\n\t// Per-group (and per-topic) overrides. Key is chat ID string (e.g. \"-100123456\") or \"*\" for wildcard.\n\t// TS ref: channels.telegram.groups in src/config/types.telegram.ts.\n\tGroups map[string]*TelegramGroupConfig `json:\"groups,omitempty\"`\n}\n\n// TelegramGroupConfig defines per-group overrides for a Telegram channel.\n// Matching TS TelegramGroupConfig in src/config/types.telegram.ts.\ntype TelegramGroupConfig struct {\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // override group policy for this group\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // override require_mention for this group\n\tMentionMode string `json:\"mention_mode,omitempty\"` // override mention_mode for this group\n\tAllowFrom FlexibleStringSlice `json:\"allow_from,omitempty\"` // override allow_from for this group\n\tEnabled *bool `json:\"enabled,omitempty\"` // disable bot for this group (default: true)\n\tSkills []string `json:\"skills,omitempty\"` // skill whitelist (nil = all, [] = none)\n\tTools []string `json:\"tools,omitempty\"` // tool allow list (nil = all, supports \"group:xxx\")\n\tSystemPrompt string `json:\"system_prompt,omitempty\"` // extra system prompt for this group\n\tTopics map[string]*TelegramTopicConfig `json:\"topics,omitempty\"` // per-topic overrides (key: thread ID string)\n\tQuota *QuotaWindow `json:\"quota,omitempty\"` // per-group quota override\n}\n\n// TelegramTopicConfig defines per-topic overrides within a Telegram group.\n// Matching TS TelegramTopicConfig in src/config/types.telegram.ts.\ntype TelegramTopicConfig struct {\n\tRequireMention *bool `json:\"require_mention,omitempty\"`\n\tMentionMode string `json:\"mention_mode,omitempty\"`\n\tGroupPolicy string `json:\"group_policy,omitempty\"`\n\tSkills []string `json:\"skills,omitempty\"`\n\tTools []string `json:\"tools,omitempty\"` // tool allow list (nil = inherit, supports \"group:xxx\")\n\tEnabled *bool `json:\"enabled,omitempty\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from,omitempty\"`\n\tSystemPrompt string `json:\"system_prompt,omitempty\"`\n}\n\ntype DiscordConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tToken string `json:\"token\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in groups (default true)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tMediaMaxBytes int64 `json:\"media_max_bytes,omitempty\"` // max media download size (default 25MB)\n\tSTTProxyURL string `json:\"stt_proxy_url,omitempty\"`\n\tSTTAPIKey string `json:\"stt_api_key,omitempty\"`\n\tSTTTenantID string `json:\"stt_tenant_id,omitempty\"`\n\tSTTTimeoutSeconds int `json:\"stt_timeout_seconds,omitempty\"`\n\tVoiceAgentID string `json:\"voice_agent_id,omitempty\"`\n}\n\ntype SlackConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tBotToken string `json:\"bot_token\"` // xoxb-... (Bot User OAuth Token)\n\tAppToken string `json:\"app_token\"` // xapp-... (App-Level Token for Socket Mode)\n\tUserToken string `json:\"user_token,omitempty\"` // xoxp-... (Optional: custom bot identity)\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"pairing\", \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in channels (default true)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tDMStream *bool `json:\"dm_stream,omitempty\"` // enable streaming for DMs (default false)\n\tGroupStream *bool `json:\"group_stream,omitempty\"` // enable streaming for groups (default false)\n\tNativeStream *bool `json:\"native_stream,omitempty\"` // use Slack ChatStreamer API if available (default false)\n\tReactionLevel string `json:\"reaction_level,omitempty\"` // \"off\" (default), \"minimal\", \"full\"\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tDebounceDelay int `json:\"debounce_delay,omitempty\"` // ms delay before dispatching rapid messages (default 300, 0=disabled)\n\tThreadTTL *int `json:\"thread_ttl,omitempty\"` // hours before thread participation expires (default 24, 0=disabled — always require @mention)\n\tMediaMaxBytes int64 `json:\"media_max_bytes,omitempty\"` // max file download size in bytes (default 20MB)\n}\n\ntype WhatsAppConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tAuthDir string `json:\"auth_dir,omitempty\"` // optional: SQLite auth dir override (desktop)\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default for DB instances), \"open\", \"allowlist\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"pairing\" (default for DB instances), \"open\" (default for config), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // only respond in groups when bot is @mentioned (default false)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 200, 0=disabled)\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n}\n\ntype ZaloConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tToken string `json:\"token\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tWebhookURL string `json:\"webhook_url,omitempty\"`\n\tWebhookSecret string `json:\"webhook_secret,omitempty\"`\n\tMediaMaxMB int `json:\"media_max_mb,omitempty\"` // default 5\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n}\n\ntype ZaloPersonalConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in groups (default true)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tCredentialsPath string `json:\"credentials_path,omitempty\"` // path to saved cookies JSON\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n}\n\ntype FeishuConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tAppID string `json:\"app_id\"`\n\tAppSecret string `json:\"app_secret\"`\n\tEncryptKey string `json:\"encrypt_key,omitempty\"`\n\tVerificationToken string `json:\"verification_token,omitempty\"`\n\tDomain string `json:\"domain,omitempty\"` // \"lark\" (default/global), \"feishu\" (China), or custom URL\n\tConnectionMode string `json:\"connection_mode,omitempty\"` // \"websocket\" (default), \"webhook\"\n\tWebhookPort int `json:\"webhook_port,omitempty\"` // default 3000\n\tWebhookPath string `json:\"webhook_path,omitempty\"` // default \"/feishu/events\"\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default)\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default)\n\tGroupAllowFrom FlexibleStringSlice `json:\"group_allow_from,omitempty\"`\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // default true (groups)\n\tTopicSessionMode string `json:\"topic_session_mode,omitempty\"` // \"disabled\" (default)\n\tTextChunkLimit int `json:\"text_chunk_limit,omitempty\"` // default 4000\n\tMediaMaxMB int `json:\"media_max_mb,omitempty\"` // default 30\n\tRenderMode string `json:\"render_mode,omitempty\"` // \"auto\", \"raw\", \"card\"\n\tStreaming *bool `json:\"streaming,omitempty\"` // default true\n\tReactionLevel string `json:\"reaction_level,omitempty\"` // \"off\" (default), \"minimal\", \"full\" — typing emoji reactions\n\tHistoryLimit int `json:\"history_limit,omitempty\"`\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tSTTProxyURL string `json:\"stt_proxy_url,omitempty\"`\n\tSTTAPIKey string `json:\"stt_api_key,omitempty\"`\n\tSTTTenantID string `json:\"stt_tenant_id,omitempty\"`\n\tSTTTimeoutSeconds int `json:\"stt_timeout_seconds,omitempty\"`\n\tVoiceAgentID string `json:\"voice_agent_id,omitempty\"`\n}\n\n// ProvidersConfig maps provider name to its config.\ntype ProvidersConfig struct {\n\tAnthropic ProviderConfig `json:\"anthropic\"`\n\tOpenAI ProviderConfig `json:\"openai\"`\n\tOpenRouter ProviderConfig `json:\"openrouter\"`\n\tGroq ProviderConfig `json:\"groq\"`\n\tGemini ProviderConfig `json:\"gemini\"`\n\tDeepSeek ProviderConfig `json:\"deepseek\"`\n\tMistral ProviderConfig `json:\"mistral\"`\n\tXAI ProviderConfig `json:\"xai\"`\n\tMiniMax ProviderConfig `json:\"minimax\"`\n\tCohere ProviderConfig `json:\"cohere\"`\n\tPerplexity ProviderConfig `json:\"perplexity\"`\n\tDashScope ProviderConfig `json:\"dashscope\"`\n\tBailian ProviderConfig `json:\"bailian\"`\n\tZai ProviderConfig `json:\"zai\"`\n\tZaiCoding ProviderConfig `json:\"zai_coding\"`\n\tOllama OllamaConfig `json:\"ollama\"` // local Ollama instance (no API key needed)\n\tOllamaCloud ProviderConfig `json:\"ollama_cloud\"` // Ollama Cloud (API key required)\n\tClaudeCLI ClaudeCLIConfig `json:\"claude_cli\"`\n\tACP ACPConfig `json:\"acp\"`\n\tNovita ProviderConfig `json:\"novita\"` // Novita AI (OpenAI-compatible endpoint)\n\tBytePlus ProviderConfig `json:\"byteplus\"` // BytePlus ModelArk (Seed 2.0)\n\tBytePlusCoding ProviderConfig `json:\"byteplus_coding\"` // BytePlus ModelArk Coding Plan\n}\n\n// OllamaConfig configures a local (or self-hosted) Ollama instance.\n// No API key is required — Ollama accepts any Bearer token value.\ntype OllamaConfig struct {\n\tHost string `json:\"host\"` // Ollama server base URL, e.g. http://localhost:11434\n}\n\n// ClaudeCLIConfig configures the Claude CLI provider (uses subscription, not API key).\ntype ClaudeCLIConfig struct {\n\tCLIPath string `json:\"cli_path\" yaml:\"cli_path\"` // path to claude binary (default: \"claude\")\n\tModel string `json:\"model\" yaml:\"model\"` // default model alias (default: \"sonnet\")\n\tBaseWorkDir string `json:\"base_work_dir\" yaml:\"base_work_dir\"` // base dir for agent workspaces\n\tPermMode string `json:\"perm_mode\" yaml:\"perm_mode\"` // permission mode (default: \"bypassPermissions\")\n}\n\n// ACPConfig configures the ACP (Agent Client Protocol) provider.\n// Orchestrates any ACP-compatible coding agent (Claude Code, Codex CLI, Gemini CLI) as a subprocess.\ntype ACPConfig struct {\n\tBinary string `json:\"binary\"` // agent binary name or path (e.g. \"claude\", \"codex\")\n\tArgs []string `json:\"args\"` // extra spawn args\n\tModel string `json:\"model\"` // default model/agent name\n\tWorkDir string `json:\"work_dir\"` // base workspace dir\n\tIdleTTL string `json:\"idle_ttl\"` // process idle TTL (e.g. \"5m\")\n\tPermMode string `json:\"perm_mode\"` // \"approve-all\" (default), \"approve-reads\", \"deny-all\"\n}\n\ntype ProviderConfig struct {\n\tAPIKey string `json:\"api_key\"`\n\tAPIBase string `json:\"api_base,omitempty\"`\n}\n\n// APIBaseForType returns the config-level api_base for a given provider type.\n// Used as a fallback when DB providers have no api_base set.\nfunc (p *ProvidersConfig) APIBaseForType(providerType string) string {\n\tswitch providerType {\n\tcase \"anthropic_native\":\n\t\treturn p.Anthropic.APIBase\n\tcase \"openai\", \"openai_compat\":\n\t\treturn p.OpenAI.APIBase\n\tcase \"openrouter\":\n\t\treturn p.OpenRouter.APIBase\n\tcase \"groq\":\n\t\treturn p.Groq.APIBase\n\tcase \"deepseek\":\n\t\treturn p.DeepSeek.APIBase\n\tcase \"gemini_native\":\n\t\treturn p.Gemini.APIBase\n\tcase \"mistral\":\n\t\treturn p.Mistral.APIBase\n\tcase \"xai\":\n\t\treturn p.XAI.APIBase\n\tcase \"minimax_native\":\n\t\treturn p.MiniMax.APIBase\n\tcase \"cohere\":\n\t\treturn p.Cohere.APIBase\n\tcase \"perplexity\":\n\t\treturn p.Perplexity.APIBase\n\tcase \"dashscope\":\n\t\treturn p.DashScope.APIBase\n\tcase \"bailian\":\n\t\treturn p.Bailian.APIBase\n\tcase \"zai\":\n\t\treturn p.Zai.APIBase\n\tcase \"zai_coding\":\n\t\treturn p.ZaiCoding.APIBase\n\tcase \"ollama_cloud\":\n\t\treturn p.OllamaCloud.APIBase\n\tcase \"novita\":\n\t\treturn p.Novita.APIBase\n\tcase \"byteplus\":\n\t\treturn p.BytePlus.APIBase\n\tcase \"byteplus_coding\":\n\t\treturn p.BytePlusCoding.APIBase\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\n// HasAnyProvider returns true if at least one provider has an API key or CLI configured.\nfunc (c *Config) HasAnyProvider() bool {\n\tp := c.Providers\n\treturn p.Anthropic.APIKey != \"\" ||\n\t\tp.OpenAI.APIKey != \"\" ||\n\t\tp.OpenRouter.APIKey != \"\" ||\n\t\tp.Groq.APIKey != \"\" ||\n\t\tp.Gemini.APIKey != \"\" ||\n\t\tp.DeepSeek.APIKey != \"\" ||\n\t\tp.Mistral.APIKey != \"\" ||\n\t\tp.XAI.APIKey != \"\" ||\n\t\tp.MiniMax.APIKey != \"\" ||\n\t\tp.Cohere.APIKey != \"\" ||\n\t\tp.Perplexity.APIKey != \"\" ||\n\t\tp.DashScope.APIKey != \"\" ||\n\t\tp.Bailian.APIKey != \"\" ||\n\t\tp.Zai.APIKey != \"\" ||\n\t\tp.ZaiCoding.APIKey != \"\" ||\n\t\tp.Ollama.Host != \"\" ||\n\t\tp.OllamaCloud.APIKey != \"\" ||\n\t\tp.ClaudeCLI.CLIPath != \"\" ||\n\t\tp.ACP.Binary != \"\" ||\n\t\tp.Novita.APIKey != \"\" ||\n\t\tp.BytePlus.APIKey != \"\" ||\n\t\tp.BytePlusCoding.APIKey != \"\"\n}\n\n// QuotaWindow defines request limits per time window. Zero means unlimited.\ntype QuotaWindow struct {\n\tHour int `json:\"hour,omitempty\"` // max requests per hour (0 = unlimited)\n\tDay int `json:\"day,omitempty\"` // max requests per day (0 = unlimited)\n\tWeek int `json:\"week,omitempty\"` // max requests per week (0 = unlimited)\n}\n\n// IsZero returns true if no limits are set.\nfunc (w QuotaWindow) IsZero() bool { return w.Hour == 0 && w.Day == 0 && w.Week == 0 }\n\n// QuotaConfig configures per-user/group request quotas.\n// Config merge priority: Groups > Channels > Providers > Default.\ntype QuotaConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tDefault QuotaWindow `json:\"default\"`\n\tProviders map[string]QuotaWindow `json:\"providers,omitempty\"` // key = provider name (e.g. \"anthropic\")\n\tChannels map[string]QuotaWindow `json:\"channels,omitempty\"` // key = channel name (e.g. \"telegram\")\n\tGroups map[string]QuotaWindow `json:\"groups,omitempty\"` // key = userID (e.g. \"group:telegram:-100123\")\n}\n\n// GatewayConfig controls the gateway server.\ntype GatewayConfig struct {\n\tHost string `json:\"host\"`\n\tPort int `json:\"port\"`\n\tToken string `json:\"token,omitempty\"` // bearer token for WS/HTTP auth\n\tOwnerIDs []string `json:\"owner_ids,omitempty\"` // sender IDs considered \"owner\"\n\tAllowedOrigins []string `json:\"allowed_origins,omitempty\"` // WebSocket CORS whitelist (empty = allow all)\n\tMaxMessageChars int `json:\"max_message_chars,omitempty\"` // max user message characters (default 32000)\n\tRateLimitRPM int `json:\"rate_limit_rpm,omitempty\"` // rate limit: requests per minute per user (default 20, 0 = disabled)\n\tInjectionAction string `json:\"injection_action,omitempty\"` // prompt injection action: \"log\", \"warn\" (default), \"block\", \"off\"\n\tInboundDebounceMs int `json:\"inbound_debounce_ms,omitempty\"` // merge rapid messages from same sender (default 1000ms, -1 = disabled)\n\tQuota *QuotaConfig `json:\"quota,omitempty\"` // per-user/group request quotas\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // deliver intermediate text during tool iterations (default false)\n\tToolStatus *bool `json:\"tool_status,omitempty\"` // show tool name in streaming preview during tool execution (default true)\n\tTaskRecoveryIntervalSec int `json:\"task_recovery_interval_sec,omitempty\"` // team task recovery ticker interval in seconds (default 300 = 5min)\n}\n\n// ToolsConfig controls tool availability, policy, and web search.\ntype ToolsConfig struct {\n\tProfile string `json:\"profile,omitempty\"` // global profile: \"minimal\", \"coding\", \"messaging\", \"full\"\n\tAllow []string `json:\"allow,omitempty\"` // global allow list (tool names or \"group:xxx\")\n\tDeny []string `json:\"deny,omitempty\"` // global deny list\n\tAlsoAllow []string `json:\"alsoAllow,omitempty\"` // additive: adds without removing existing\n\tByProvider map[string]*ToolPolicySpec `json:\"byProvider,omitempty\"` // per-provider overrides\n\tExecApproval ExecApprovalCfg `json:\"execApproval\"` // exec command approval settings\n\tWebFetch WebFetchPolicyConfig `json:\"web_fetch\"` // domain policy for URL fetching\n\tWeb WebToolsConfig `json:\"web\"`\n\tBrowser BrowserToolConfig `json:\"browser\"`\n\tRateLimitPerHour int `json:\"rate_limit_per_hour,omitempty\"` // max tool executions per hour per session (0 = disabled)\n\tScrubCredentials *bool `json:\"scrub_credentials,omitempty\"` // auto-redact API keys/tokens in tool output (default true)\n\tMcpServers map[string]*MCPServerConfig `json:\"mcp_servers,omitempty\"` // external MCP server connections\n}\n\n// MCPServerConfig configures a single external MCP server connection.\ntype MCPServerConfig struct {\n\tTransport string `json:\"transport\"` // \"stdio\", \"sse\", \"streamable-http\"\n\tCommand string `json:\"command,omitempty\"` // stdio: command to spawn\n\tArgs []string `json:\"args,omitempty\"` // stdio: command arguments\n\tEnv map[string]string `json:\"env,omitempty\"` // stdio: extra environment variables\n\tURL string `json:\"url,omitempty\"` // sse/http: server URL\n\tHeaders map[string]string `json:\"headers,omitempty\"` // sse/http: extra HTTP headers\n\tEnabled *bool `json:\"enabled,omitempty\"` // default true\n\tToolPrefix string `json:\"tool_prefix,omitempty\"` // prefix for tool names (avoids collisions)\n\tTimeoutSec int `json:\"timeout_sec,omitempty\"` // per-tool-call timeout in seconds (default 60)\n}\n\n// IsEnabled returns whether this MCP server is enabled (default true).\nfunc (c *MCPServerConfig) IsEnabled() bool {\n\treturn c.Enabled == nil || *c.Enabled\n}\n\n// ExecApprovalCfg configures command execution approval (matching TS exec-approval.ts).\ntype ExecApprovalCfg struct {\n\tSecurity string `json:\"security,omitempty\"` // \"deny\", \"allowlist\", \"full\" (default \"full\")\n\tAsk string `json:\"ask,omitempty\"` // \"off\", \"on-miss\", \"always\" (default \"off\")\n\tAllowlist []string `json:\"allowlist,omitempty\"` // glob patterns for allowed commands\n}\n\n// WebFetchPolicyConfig controls domain filtering for the web_fetch tool.\ntype WebFetchPolicyConfig struct {\n\tPolicy string `json:\"policy,omitempty\"` // \"allow_all\" (default), \"allowlist\"\n\tAllowedDomains []string `json:\"allowed_domains,omitempty\"` // e.g. [\"github.com\", \"*.example.com\"]\n\tBlockedDomains []string `json:\"blocked_domains,omitempty\"` // always checked regardless of policy\n}\n\n// BrowserToolConfig controls the browser automation tool.\ntype BrowserToolConfig struct {\n\tEnabled bool `json:\"enabled\"` // enable the browser tool (default false)\n\tHeadless bool `json:\"headless,omitempty\"` // run Chrome in headless mode (ignored when RemoteURL is set)\n\tRemoteURL string `json:\"remote_url,omitempty\"` // CDP endpoint for remote Chrome sidecar, e.g. \"ws://chrome:9222\"\n\tActionTimeoutMs int `json:\"action_timeout_ms,omitempty\"` // per-action timeout in ms (default 30000)\n\tIdleTimeoutMs int `json:\"idle_timeout_ms,omitempty\"` // idle page auto-close in ms (default 600000, 0=disabled)\n\tMaxPages int `json:\"max_pages,omitempty\"` // max open pages per tenant (default 5)\n}\n\n// ToolPolicySpec defines a tool policy at any level (global, per-agent, per-provider).\ntype ToolPolicySpec struct {\n\tProfile string `json:\"profile,omitempty\"`\n\tAllow []string `json:\"allow,omitempty\"`\n\tDeny []string `json:\"deny,omitempty\"`\n\tAlsoAllow []string `json:\"alsoAllow,omitempty\"`\n\tByProvider map[string]*ToolPolicySpec `json:\"byProvider,omitempty\"`\n\tToolCallPrefix string `json:\"toolCallPrefix,omitempty\"` // prefix to strip from model's tool call names before registry lookup\n}\n\ntype WebToolsConfig struct {\n\tBrave BraveConfig `json:\"brave\"`\n\tDuckDuckGo DuckDuckGoConfig `json:\"duckduckgo\"`\n}\n\ntype BraveConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tAPIKey string `json:\"api_key\"`\n\tMaxResults int `json:\"max_results\"`\n}\n\ntype DuckDuckGoConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tMaxResults int `json:\"max_results\"`\n}\n\n// SessionsConfig controls session behavior.\n// Matching TS src/config/sessions/types.ts + src/config/types.base.ts.\ntype SessionsConfig struct {\n\tScope string `json:\"scope,omitempty\"` // \"per-sender\" (default), \"global\"\n\tDmScope string `json:\"dm_scope,omitempty\"` // \"main\", \"per-peer\", \"per-channel-peer\" (default), \"per-account-channel-peer\"\n\tMainKey string `json:\"main_key,omitempty\"` // main session key suffix (default \"main\", used when dm_scope=\"main\")\n}\n\n// TtsConfig configures text-to-speech.\n// Matching TS src/config/types.tts.ts.\ntype TtsConfig struct {\n\tProvider string `json:\"provider,omitempty\"` // \"openai\", \"elevenlabs\", \"edge\", \"minimax\"\n\tAuto string `json:\"auto,omitempty\"` // \"off\" (default), \"always\", \"inbound\", \"tagged\"\n\tMode string `json:\"mode,omitempty\"` // \"final\" (default), \"all\"\n\tMaxLength int `json:\"max_length,omitempty\"` // max text length before truncation (default 1500)\n\tTimeoutMs int `json:\"timeout_ms,omitempty\"` // API timeout in ms (default 30000)\n\tOpenAI TtsOpenAIConfig `json:\"openai\"`\n\tElevenLabs TtsElevenLabsConfig `json:\"elevenlabs\"`\n\tEdge TtsEdgeConfig `json:\"edge\"`\n\tMiniMax TtsMiniMaxConfig `json:\"minimax\"`\n}\n\n// TtsOpenAIConfig configures the OpenAI TTS provider.\ntype TtsOpenAIConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"`\n\tAPIBase string `json:\"api_base,omitempty\"` // custom endpoint URL\n\tModel string `json:\"model,omitempty\"` // default \"gpt-4o-mini-tts\"\n\tVoice string `json:\"voice,omitempty\"` // default \"alloy\"\n}\n\n// TtsElevenLabsConfig configures the ElevenLabs TTS provider.\ntype TtsElevenLabsConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"`\n\tBaseURL string `json:\"base_url,omitempty\"`\n\tVoiceID string `json:\"voice_id,omitempty\"` // default \"pMsXgVXv3BLzUgSXRplE\"\n\tModelID string `json:\"model_id,omitempty\"` // default \"eleven_multilingual_v2\"\n}\n\n// TtsEdgeConfig configures the Microsoft Edge TTS provider (free, no API key).\ntype TtsEdgeConfig struct {\n\tEnabled bool `json:\"enabled,omitempty\"`\n\tVoice string `json:\"voice,omitempty\"` // default \"en-US-MichelleNeural\"\n\tRate string `json:\"rate,omitempty\"` // speech rate, e.g. \"+0%\"\n}\n\n// TtsMiniMaxConfig configures the MiniMax TTS provider.\ntype TtsMiniMaxConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"`\n\tGroupID string `json:\"group_id,omitempty\"` // MiniMax GroupId (required)\n\tAPIBase string `json:\"api_base,omitempty\"` // default \"https://api.minimax.io/v1\"\n\tModel string `json:\"model,omitempty\"` // default \"speech-02-hd\"\n\tVoiceID string `json:\"voice_id,omitempty\"` // default \"Wise_Woman\"\n}\n\n// MergeChannelGroupQuotas merges per-group quota overrides from channel configs\n// (e.g., channels.telegram.groups[chatID].quota) into gateway.quota.groups.\n// This allows per-group quotas to be set at the channel level and picked up\n// by the QuotaChecker at runtime.\nfunc MergeChannelGroupQuotas(cfg *Config) {\n\tif cfg.Gateway.Quota == nil {\n\t\treturn\n\t}\n\tif cfg.Gateway.Quota.Groups == nil {\n\t\tcfg.Gateway.Quota.Groups = make(map[string]QuotaWindow)\n\t}\n\n\t// Telegram per-group quotas\n\tfor chatID, groupCfg := range cfg.Channels.Telegram.Groups {\n\t\tif groupCfg != nil && groupCfg.Quota != nil && !groupCfg.Quota.IsZero() {\n\t\t\tkey := \"group:telegram:\" + chatID\n\t\t\tcfg.Gateway.Quota.Groups[key] = *groupCfg.Quota\n\t\t}\n\t}\n}\n"}, "aa5158d4a26422d01a2107ed67387734b7395362": {"t": 1785759254.161044, "n": 31593, "h": "d9ea9f9de7dd6a83", "c": "package config\n\n// PendingCompactionConfig configures LLM-based compaction of pending group messages.\n// When a group accumulates more than Threshold pending messages, older messages are\n// summarized by an LLM and replaced with a compact summary, keeping KeepRecent raw messages.\ntype PendingCompactionConfig struct {\n\tThreshold int `json:\"threshold,omitempty\"` // trigger compaction when entries exceed this (default 200)\n\tKeepRecent int `json:\"keep_recent,omitempty\"` // keep this many recent raw messages after compaction (default 40)\n\tMaxTokens int `json:\"max_tokens,omitempty\"` // max output tokens for LLM summarization (default 4096)\n\tProvider string `json:\"provider,omitempty\"` // LLM provider name (e.g. \"openai\"); empty = use agent's provider\n\tModel string `json:\"model,omitempty\"` // model for summarization; empty = use agent's model\n}\n\n// ChannelsConfig contains per-channel configuration.\ntype ChannelsConfig struct {\n\tTelegram TelegramConfig `json:\"telegram\"`\n\tDiscord DiscordConfig `json:\"discord\"`\n\tSlack SlackConfig `json:\"slack\"`\n\tWhatsApp WhatsAppConfig `json:\"whatsapp\"`\n\tZalo ZaloConfig `json:\"zalo\"`\n\tZaloPersonal ZaloPersonalConfig `json:\"zalo_personal\"`\n\tFeishu FeishuConfig `json:\"feishu\"`\n\tPendingCompaction *PendingCompactionConfig `json:\"pending_compaction,omitempty\"` // global pending message compaction settings\n}\n\ntype TelegramConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tToken string `json:\"token\"`\n\tProxy string `json:\"proxy,omitempty\"`\n\tAPIServer string `json:\"api_server,omitempty\"` // custom Telegram Bot API server URL (e.g. \"http://localhost:8081\")\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in groups (default true)\n\tMentionMode string `json:\"mention_mode,omitempty\"` // \"strict\" (default) = only respond when mentioned; \"yield\" = respond unless another bot is mentioned\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tDMStream *bool `json:\"dm_stream,omitempty\"` // enable streaming for DMs (default false) — edits placeholder progressively\n\tGroupStream *bool `json:\"group_stream,omitempty\"` // enable streaming for groups (default false) — sends new message, edits progressively\n\tDraftTransport *bool `json:\"draft_transport,omitempty\"` // use sendMessageDraft for DM streaming (default true) — stealth preview, no notifications per edit\n\tReasoningStream *bool `json:\"reasoning_stream,omitempty\"` // show reasoning as separate message when provider emits thinking events (default true)\n\tReactionLevel string `json:\"reaction_level,omitempty\"` // \"off\" (default), \"minimal\", \"full\" — status emoji reactions\n\tMediaMaxBytes int64 `json:\"media_max_bytes,omitempty\"` // max media download size in bytes (default 20MB)\n\tLinkPreview *bool `json:\"link_preview,omitempty\"` // enable URL previews in messages (default true)\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tForceIPv4 bool `json:\"force_ipv4,omitempty\"` // force IPv4 for all Telegram API requests (use when IPv6 routing is broken)\n\n\t// Optional STT (Speech-to-Text) pipeline for voice/audio inbound messages.\n\t// When stt_proxy_url is set, audio/voice messages are transcribed before being forwarded to the agent.\n\tSTTProxyURL string `json:\"stt_proxy_url,omitempty\"` // base URL of the STT proxy service (e.g. \"https://stt.example.com\")\n\tSTTAPIKey string `json:\"stt_api_key,omitempty\"` // Bearer token for the STT proxy\n\tSTTTenantID string `json:\"stt_tenant_id,omitempty\"` // optional tenant/org identifier forwarded to the STT proxy\n\tSTTTimeoutSeconds int `json:\"stt_timeout_seconds,omitempty\"` // per-request timeout for STT calls (default 30s)\n\n\t// Optional audio-aware routing: when set, voice/audio inbound messages are routed to this\n\t// agent instead of the default channel agent. Requires the named agent to exist in the config.\n\tVoiceAgentID string `json:\"voice_agent_id,omitempty\"` // agent ID to route voice inbound to (e.g. \"speaking-agent\")\n\n\t// Audio guard: intercept technical errors in voice agent replies and replace with friendly fallbacks.\n\t// Only active when VoiceAgentID is set. Custom error markers replace built-in defaults when provided.\n\tAudioGuardFallbackTranscript string `json:\"audio_guard_fallback_transcript,omitempty\"` // fallback with %s for transcript (e.g. \"I heard: \\\"%s\\\". Try again!\")\n\tAudioGuardFallbackNoTranscript string `json:\"audio_guard_fallback_no_transcript,omitempty\"` // fallback when no transcript available\n\tAudioGuardErrorMarkers []string `json:\"audio_guard_error_markers,omitempty\"` // custom error detection markers (replaces defaults)\n\n\t// Per-group (and per-topic) overrides. Key is chat ID string (e.g. \"-100123456\") or \"*\" for wildcard.\n\t// TS ref: channels.telegram.groups in src/config/types.telegram.ts.\n\tGroups map[string]*TelegramGroupConfig `json:\"groups,omitempty\"`\n}\n\n// TelegramGroupConfig defines per-group overrides for a Telegram channel.\n// Matching TS TelegramGroupConfig in src/config/types.telegram.ts.\ntype TelegramGroupConfig struct {\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // override group policy for this group\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // override require_mention for this group\n\tMentionMode string `json:\"mention_mode,omitempty\"` // override mention_mode for this group\n\tAllowFrom FlexibleStringSlice `json:\"allow_from,omitempty\"` // override allow_from for this group\n\tEnabled *bool `json:\"enabled,omitempty\"` // disable bot for this group (default: true)\n\tSkills []string `json:\"skills,omitempty\"` // skill whitelist (nil = all, [] = none)\n\tTools []string `json:\"tools,omitempty\"` // tool allow list (nil = all, supports \"group:xxx\")\n\tSystemPrompt string `json:\"system_prompt,omitempty\"` // extra system prompt for this group\n\tTopics map[string]*TelegramTopicConfig `json:\"topics,omitempty\"` // per-topic overrides (key: thread ID string)\n\tQuota *QuotaWindow `json:\"quota,omitempty\"` // per-group quota override\n}\n\n// TelegramTopicConfig defines per-topic overrides within a Telegram group.\n// Matching TS TelegramTopicConfig in src/config/types.telegram.ts.\ntype TelegramTopicConfig struct {\n\tRequireMention *bool `json:\"require_mention,omitempty\"`\n\tMentionMode string `json:\"mention_mode,omitempty\"`\n\tGroupPolicy string `json:\"group_policy,omitempty\"`\n\tSkills []string `json:\"skills,omitempty\"`\n\tTools []string `json:\"tools,omitempty\"` // tool allow list (nil = inherit, supports \"group:xxx\")\n\tEnabled *bool `json:\"enabled,omitempty\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from,omitempty\"`\n\tSystemPrompt string `json:\"system_prompt,omitempty\"`\n}\n\ntype DiscordConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tToken string `json:\"token\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in groups (default true)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tMediaMaxBytes int64 `json:\"media_max_bytes,omitempty\"` // max media download size (default 25MB)\n\tSTTProxyURL string `json:\"stt_proxy_url,omitempty\"`\n\tSTTAPIKey string `json:\"stt_api_key,omitempty\"`\n\tSTTTenantID string `json:\"stt_tenant_id,omitempty\"`\n\tSTTTimeoutSeconds int `json:\"stt_timeout_seconds,omitempty\"`\n\tVoiceAgentID string `json:\"voice_agent_id,omitempty\"`\n}\n\ntype SlackConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tBotToken string `json:\"bot_token\"` // xoxb-... (Bot User OAuth Token)\n\tAppToken string `json:\"app_token\"` // xapp-... (App-Level Token for Socket Mode)\n\tUserToken string `json:\"user_token,omitempty\"` // xoxp-... (Optional: custom bot identity)\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"pairing\", \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in channels (default true)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tDMStream *bool `json:\"dm_stream,omitempty\"` // enable streaming for DMs (default false)\n\tGroupStream *bool `json:\"group_stream,omitempty\"` // enable streaming for groups (default false)\n\tNativeStream *bool `json:\"native_stream,omitempty\"` // use Slack ChatStreamer API if available (default false)\n\tReactionLevel string `json:\"reaction_level,omitempty\"` // \"off\" (default), \"minimal\", \"full\"\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tDebounceDelay int `json:\"debounce_delay,omitempty\"` // ms delay before dispatching rapid messages (default 300, 0=disabled)\n\tThreadTTL *int `json:\"thread_ttl,omitempty\"` // hours before thread participation expires (default 24, 0=disabled — always require @mention)\n\tMediaMaxBytes int64 `json:\"media_max_bytes,omitempty\"` // max file download size in bytes (default 20MB)\n}\n\ntype WhatsAppConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tBridgeURL string `json:\"bridge_url\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n}\n\ntype ZaloConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tToken string `json:\"token\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tWebhookURL string `json:\"webhook_url,omitempty\"`\n\tWebhookSecret string `json:\"webhook_secret,omitempty\"`\n\tMediaMaxMB int `json:\"media_max_mb,omitempty\"` // default 5\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n}\n\ntype ZaloPersonalConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in groups (default true)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tCredentialsPath string `json:\"credentials_path,omitempty\"` // path to saved cookies JSON\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n}\n\ntype FeishuConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tAppID string `json:\"app_id\"`\n\tAppSecret string `json:\"app_secret\"`\n\tEncryptKey string `json:\"encrypt_key,omitempty\"`\n\tVerificationToken string `json:\"verification_token,omitempty\"`\n\tDomain string `json:\"domain,omitempty\"` // \"lark\" (default/global), \"feishu\" (China), or custom URL\n\tConnectionMode string `json:\"connection_mode,omitempty\"` // \"websocket\" (default), \"webhook\"\n\tWebhookPort int `json:\"webhook_port,omitempty\"` // default 3000\n\tWebhookPath string `json:\"webhook_path,omitempty\"` // default \"/feishu/events\"\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default)\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default)\n\tGroupAllowFrom FlexibleStringSlice `json:\"group_allow_from,omitempty\"`\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // default true (groups)\n\tTopicSessionMode string `json:\"topic_session_mode,omitempty\"` // \"disabled\" (default)\n\tTextChunkLimit int `json:\"text_chunk_limit,omitempty\"` // default 4000\n\tMediaMaxMB int `json:\"media_max_mb,omitempty\"` // default 30\n\tRenderMode string `json:\"render_mode,omitempty\"` // \"auto\", \"raw\", \"card\"\n\tStreaming *bool `json:\"streaming,omitempty\"` // default true\n\tReactionLevel string `json:\"reaction_level,omitempty\"` // \"off\" (default), \"minimal\", \"full\" — typing emoji reactions\n\tHistoryLimit int `json:\"history_limit,omitempty\"`\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tSTTProxyURL string `json:\"stt_proxy_url,omitempty\"`\n\tSTTAPIKey string `json:\"stt_api_key,omitempty\"`\n\tSTTTenantID string `json:\"stt_tenant_id,omitempty\"`\n\tSTTTimeoutSeconds int `json:\"stt_timeout_seconds,omitempty\"`\n\tVoiceAgentID string `json:\"voice_agent_id,omitempty\"`\n}\n\n// ProvidersConfig maps provider name to its config.\ntype ProvidersConfig struct {\n\tAnthropic ProviderConfig `json:\"anthropic\"`\n\tOpenAI ProviderConfig `json:\"openai\"`\n\tOpenRouter ProviderConfig `json:\"openrouter\"`\n\tGroq ProviderConfig `json:\"groq\"`\n\tGemini ProviderConfig `json:\"gemini\"`\n\tDeepSeek ProviderConfig `json:\"deepseek\"`\n\tMistral ProviderConfig `json:\"mistral\"`\n\tXAI ProviderConfig `json:\"xai\"`\n\tMiniMax ProviderConfig `json:\"minimax\"`\n\tCohere ProviderConfig `json:\"cohere\"`\n\tPerplexity ProviderConfig `json:\"perplexity\"`\n\tDashScope ProviderConfig `json:\"dashscope\"`\n\tBailian ProviderConfig `json:\"bailian\"`\n\tZai ProviderConfig `json:\"zai\"`\n\tZaiCoding ProviderConfig `json:\"zai_coding\"`\n\tOllama OllamaConfig `json:\"ollama\"` // local Ollama instance (no API key needed)\n\tOllamaCloud ProviderConfig `json:\"ollama_cloud\"` // Ollama Cloud (API key required)\n\tClaudeCLI ClaudeCLIConfig `json:\"claude_cli\"`\n\tACP ACPConfig `json:\"acp\"`\n\tNovita ProviderConfig `json:\"novita\"` // Novita AI (OpenAI-compatible endpoint)\n\tBytePlus ProviderConfig `json:\"byteplus\"` // BytePlus ModelArk (Seed 2.0)\n\tBytePlusCoding ProviderConfig `json:\"byteplus_coding\"` // BytePlus ModelArk Coding Plan\n}\n\n// OllamaConfig configures a local (or self-hosted) Ollama instance.\n// No API key is required — Ollama accepts any Bearer token value.\ntype OllamaConfig struct {\n\tHost string `json:\"host\"` // Ollama server base URL, e.g. http://localhost:11434\n}\n\n// ClaudeCLIConfig configures the Claude CLI provider (uses subscription, not API key).\ntype ClaudeCLIConfig struct {\n\tCLIPath string `json:\"cli_path\" yaml:\"cli_path\"` // path to claude binary (default: \"claude\")\n\tModel string `json:\"model\" yaml:\"model\"` // default model alias (default: \"sonnet\")\n\tBaseWorkDir string `json:\"base_work_dir\" yaml:\"base_work_dir\"` // base dir for agent workspaces\n\tPermMode string `json:\"perm_mode\" yaml:\"perm_mode\"` // permission mode (default: \"bypassPermissions\")\n}\n\n// ACPConfig configures the ACP (Agent Client Protocol) provider.\n// Orchestrates any ACP-compatible coding agent (Claude Code, Codex CLI, Gemini CLI) as a subprocess.\ntype ACPConfig struct {\n\tBinary string `json:\"binary\"` // agent binary name or path (e.g. \"claude\", \"codex\")\n\tArgs []string `json:\"args\"` // extra spawn args\n\tModel string `json:\"model\"` // default model/agent name\n\tWorkDir string `json:\"work_dir\"` // base workspace dir\n\tIdleTTL string `json:\"idle_ttl\"` // process idle TTL (e.g. \"5m\")\n\tPermMode string `json:\"perm_mode\"` // \"approve-all\" (default), \"approve-reads\", \"deny-all\"\n}\n\ntype ProviderConfig struct {\n\tAPIKey string `json:\"api_key\"`\n\tAPIBase string `json:\"api_base,omitempty\"`\n}\n\n// APIBaseForType returns the config-level api_base for a given provider type.\n// Used as a fallback when DB providers have no api_base set.\nfunc (p *ProvidersConfig) APIBaseForType(providerType string) string {\n\tswitch providerType {\n\tcase \"anthropic_native\":\n\t\treturn p.Anthropic.APIBase\n\tcase \"openai\", \"openai_compat\":\n\t\treturn p.OpenAI.APIBase\n\tcase \"openrouter\":\n\t\treturn p.OpenRouter.APIBase\n\tcase \"groq\":\n\t\treturn p.Groq.APIBase\n\tcase \"deepseek\":\n\t\treturn p.DeepSeek.APIBase\n\tcase \"gemini_native\":\n\t\treturn p.Gemini.APIBase\n\tcase \"mistral\":\n\t\treturn p.Mistral.APIBase\n\tcase \"xai\":\n\t\treturn p.XAI.APIBase\n\tcase \"minimax_native\":\n\t\treturn p.MiniMax.APIBase\n\tcase \"cohere\":\n\t\treturn p.Cohere.APIBase\n\tcase \"perplexity\":\n\t\treturn p.Perplexity.APIBase\n\tcase \"dashscope\":\n\t\treturn p.DashScope.APIBase\n\tcase \"bailian\":\n\t\treturn p.Bailian.APIBase\n\tcase \"zai\":\n\t\treturn p.Zai.APIBase\n\tcase \"zai_coding\":\n\t\treturn p.ZaiCoding.APIBase\n\tcase \"ollama_cloud\":\n\t\treturn p.OllamaCloud.APIBase\n\tcase \"novita\":\n\t\treturn p.Novita.APIBase\n\tcase \"byteplus\":\n\t\treturn p.BytePlus.APIBase\n\tcase \"byteplus_coding\":\n\t\treturn p.BytePlusCoding.APIBase\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\n// HasAnyProvider returns true if at least one provider has an API key or CLI configured.\nfunc (c *Config) HasAnyProvider() bool {\n\tp := c.Providers\n\treturn p.Anthropic.APIKey != \"\" ||\n\t\tp.OpenAI.APIKey != \"\" ||\n\t\tp.OpenRouter.APIKey != \"\" ||\n\t\tp.Groq.APIKey != \"\" ||\n\t\tp.Gemini.APIKey != \"\" ||\n\t\tp.DeepSeek.APIKey != \"\" ||\n\t\tp.Mistral.APIKey != \"\" ||\n\t\tp.XAI.APIKey != \"\" ||\n\t\tp.MiniMax.APIKey != \"\" ||\n\t\tp.Cohere.APIKey != \"\" ||\n\t\tp.Perplexity.APIKey != \"\" ||\n\t\tp.DashScope.APIKey != \"\" ||\n\t\tp.Bailian.APIKey != \"\" ||\n\t\tp.Zai.APIKey != \"\" ||\n\t\tp.ZaiCoding.APIKey != \"\" ||\n\t\tp.Ollama.Host != \"\" ||\n\t\tp.OllamaCloud.APIKey != \"\" ||\n\t\tp.ClaudeCLI.CLIPath != \"\" ||\n\t\tp.ACP.Binary != \"\" ||\n\t\tp.Novita.APIKey != \"\" ||\n\t\tp.BytePlus.APIKey != \"\" ||\n\t\tp.BytePlusCoding.APIKey != \"\"\n}\n\n// QuotaWindow defines request limits per time window. Zero means unlimited.\ntype QuotaWindow struct {\n\tHour int `json:\"hour,omitempty\"` // max requests per hour (0 = unlimited)\n\tDay int `json:\"day,omitempty\"` // max requests per day (0 = unlimited)\n\tWeek int `json:\"week,omitempty\"` // max requests per week (0 = unlimited)\n}\n\n// IsZero returns true if no limits are set.\nfunc (w QuotaWindow) IsZero() bool { return w.Hour == 0 && w.Day == 0 && w.Week == 0 }\n\n// QuotaConfig configures per-user/group request quotas.\n// Config merge priority: Groups > Channels > Providers > Default.\ntype QuotaConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tDefault QuotaWindow `json:\"default\"`\n\tProviders map[string]QuotaWindow `json:\"providers,omitempty\"` // key = provider name (e.g. \"anthropic\")\n\tChannels map[string]QuotaWindow `json:\"channels,omitempty\"` // key = channel name (e.g. \"telegram\")\n\tGroups map[string]QuotaWindow `json:\"groups,omitempty\"` // key = userID (e.g. \"group:telegram:-100123\")\n}\n\n// GatewayConfig controls the gateway server.\ntype GatewayConfig struct {\n\tHost string `json:\"host\"`\n\tPort int `json:\"port\"`\n\tToken string `json:\"token,omitempty\"` // bearer token for WS/HTTP auth\n\tOwnerIDs []string `json:\"owner_ids,omitempty\"` // sender IDs considered \"owner\"\n\tAllowedOrigins []string `json:\"allowed_origins,omitempty\"` // WebSocket CORS whitelist (empty = allow all)\n\tMaxMessageChars int `json:\"max_message_chars,omitempty\"` // max user message characters (default 32000)\n\tRateLimitRPM int `json:\"rate_limit_rpm,omitempty\"` // rate limit: requests per minute per user (default 20, 0 = disabled)\n\tInjectionAction string `json:\"injection_action,omitempty\"` // prompt injection action: \"log\", \"warn\" (default), \"block\", \"off\"\n\tInboundDebounceMs int `json:\"inbound_debounce_ms,omitempty\"` // merge rapid messages from same sender (default 1000ms, -1 = disabled)\n\tQuota *QuotaConfig `json:\"quota,omitempty\"` // per-user/group request quotas\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // deliver intermediate text during tool iterations (default false)\n\tToolStatus *bool `json:\"tool_status,omitempty\"` // show tool name in streaming preview during tool execution (default true)\n\tTaskRecoveryIntervalSec int `json:\"task_recovery_interval_sec,omitempty\"` // team task recovery ticker interval in seconds (default 300 = 5min)\n}\n\n// ToolsConfig controls tool availability, policy, and web search.\ntype ToolsConfig struct {\n\tProfile string `json:\"profile,omitempty\"` // global profile: \"minimal\", \"coding\", \"messaging\", \"full\"\n\tAllow []string `json:\"allow,omitempty\"` // global allow list (tool names or \"group:xxx\")\n\tDeny []string `json:\"deny,omitempty\"` // global deny list\n\tAlsoAllow []string `json:\"alsoAllow,omitempty\"` // additive: adds without removing existing\n\tByProvider map[string]*ToolPolicySpec `json:\"byProvider,omitempty\"` // per-provider overrides\n\tExecApproval ExecApprovalCfg `json:\"execApproval\"` // exec command approval settings\n\tWebFetch WebFetchPolicyConfig `json:\"web_fetch\"` // domain policy for URL fetching\n\tWeb WebToolsConfig `json:\"web\"`\n\tBrowser BrowserToolConfig `json:\"browser\"`\n\tRateLimitPerHour int `json:\"rate_limit_per_hour,omitempty\"` // max tool executions per hour per session (0 = disabled)\n\tScrubCredentials *bool `json:\"scrub_credentials,omitempty\"` // auto-redact API keys/tokens in tool output (default true)\n\tMcpServers map[string]*MCPServerConfig `json:\"mcp_servers,omitempty\"` // external MCP server connections\n}\n\n// MCPServerConfig configures a single external MCP server connection.\ntype MCPServerConfig struct {\n\tTransport string `json:\"transport\"` // \"stdio\", \"sse\", \"streamable-http\"\n\tCommand string `json:\"command,omitempty\"` // stdio: command to spawn\n\tArgs []string `json:\"args,omitempty\"` // stdio: command arguments\n\tEnv map[string]string `json:\"env,omitempty\"` // stdio: extra environment variables\n\tURL string `json:\"url,omitempty\"` // sse/http: server URL\n\tHeaders map[string]string `json:\"headers,omitempty\"` // sse/http: extra HTTP headers\n\tEnabled *bool `json:\"enabled,omitempty\"` // default true\n\tToolPrefix string `json:\"tool_prefix,omitempty\"` // prefix for tool names (avoids collisions)\n\tTimeoutSec int `json:\"timeout_sec,omitempty\"` // per-tool-call timeout in seconds (default 60)\n}\n\n// IsEnabled returns whether this MCP server is enabled (default true).\nfunc (c *MCPServerConfig) IsEnabled() bool {\n\treturn c.Enabled == nil || *c.Enabled\n}\n\n// ExecApprovalCfg configures command execution approval (matching TS exec-approval.ts).\ntype ExecApprovalCfg struct {\n\tSecurity string `json:\"security,omitempty\"` // \"deny\", \"allowlist\", \"full\" (default \"full\")\n\tAsk string `json:\"ask,omitempty\"` // \"off\", \"on-miss\", \"always\" (default \"off\")\n\tAllowlist []string `json:\"allowlist,omitempty\"` // glob patterns for allowed commands\n}\n\n// WebFetchPolicyConfig controls domain filtering for the web_fetch tool.\ntype WebFetchPolicyConfig struct {\n\tPolicy string `json:\"policy,omitempty\"` // \"allow_all\" (default), \"allowlist\"\n\tAllowedDomains []string `json:\"allowed_domains,omitempty\"` // e.g. [\"github.com\", \"*.example.com\"]\n\tBlockedDomains []string `json:\"blocked_domains,omitempty\"` // always checked regardless of policy\n}\n\n// BrowserToolConfig controls the browser automation tool.\ntype BrowserToolConfig struct {\n\tEnabled bool `json:\"enabled\"` // enable the browser tool (default false)\n\tHeadless bool `json:\"headless,omitempty\"` // run Chrome in headless mode (ignored when RemoteURL is set)\n\tRemoteURL string `json:\"remote_url,omitempty\"` // CDP endpoint for remote Chrome sidecar, e.g. \"ws://chrome:9222\"\n\tActionTimeoutMs int `json:\"action_timeout_ms,omitempty\"` // per-action timeout in ms (default 30000)\n\tIdleTimeoutMs int `json:\"idle_timeout_ms,omitempty\"` // idle page auto-close in ms (default 600000, 0=disabled)\n\tMaxPages int `json:\"max_pages,omitempty\"` // max open pages per tenant (default 5)\n}\n\n// ToolPolicySpec defines a tool policy at any level (global, per-agent, per-provider).\ntype ToolPolicySpec struct {\n\tProfile string `json:\"profile,omitempty\"`\n\tAllow []string `json:\"allow,omitempty\"`\n\tDeny []string `json:\"deny,omitempty\"`\n\tAlsoAllow []string `json:\"alsoAllow,omitempty\"`\n\tByProvider map[string]*ToolPolicySpec `json:\"byProvider,omitempty\"`\n\tToolCallPrefix string `json:\"toolCallPrefix,omitempty\"` // prefix to strip from model's tool call names before registry lookup\n}\n\ntype WebToolsConfig struct {\n\tBrave BraveConfig `json:\"brave\"`\n\tDuckDuckGo DuckDuckGoConfig `json:\"duckduckgo\"`\n}\n\ntype BraveConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tAPIKey string `json:\"api_key\"`\n\tMaxResults int `json:\"max_results\"`\n}\n\ntype DuckDuckGoConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tMaxResults int `json:\"max_results\"`\n}\n\n// SessionsConfig controls session behavior.\n// Matching TS src/config/sessions/types.ts + src/config/types.base.ts.\ntype SessionsConfig struct {\n\tScope string `json:\"scope,omitempty\"` // \"per-sender\" (default), \"global\"\n\tDmScope string `json:\"dm_scope,omitempty\"` // \"main\", \"per-peer\", \"per-channel-peer\" (default), \"per-account-channel-peer\"\n\tMainKey string `json:\"main_key,omitempty\"` // main session key suffix (default \"main\", used when dm_scope=\"main\")\n}\n\n// TtsConfig configures text-to-speech.\n// Matching TS src/config/types.tts.ts.\ntype TtsConfig struct {\n\tProvider string `json:\"provider,omitempty\"` // \"openai\", \"elevenlabs\", \"edge\", \"minimax\"\n\tAuto string `json:\"auto,omitempty\"` // \"off\" (default), \"always\", \"inbound\", \"tagged\"\n\tMode string `json:\"mode,omitempty\"` // \"final\" (default), \"all\"\n\tMaxLength int `json:\"max_length,omitempty\"` // max text length before truncation (default 1500)\n\tTimeoutMs int `json:\"timeout_ms,omitempty\"` // API timeout in ms (default 30000)\n\tOpenAI TtsOpenAIConfig `json:\"openai\"`\n\tElevenLabs TtsElevenLabsConfig `json:\"elevenlabs\"`\n\tEdge TtsEdgeConfig `json:\"edge\"`\n\tMiniMax TtsMiniMaxConfig `json:\"minimax\"`\n}\n\n// TtsOpenAIConfig configures the OpenAI TTS provider.\ntype TtsOpenAIConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"`\n\tAPIBase string `json:\"api_base,omitempty\"` // custom endpoint URL\n\tModel string `json:\"model,omitempty\"` // default \"gpt-4o-mini-tts\"\n\tVoice string `json:\"voice,omitempty\"` // default \"alloy\"\n}\n\n// TtsElevenLabsConfig configures the ElevenLabs TTS provider.\ntype TtsElevenLabsConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"`\n\tBaseURL string `json:\"base_url,omitempty\"`\n\tVoiceID string `json:\"voice_id,omitempty\"` // default \"pMsXgVXv3BLzUgSXRplE\"\n\tModelID string `json:\"model_id,omitempty\"` // default \"eleven_multilingual_v2\"\n}\n\n// TtsEdgeConfig configures the Microsoft Edge TTS provider (free, no API key).\ntype TtsEdgeConfig struct {\n\tEnabled bool `json:\"enabled,omitempty\"`\n\tVoice string `json:\"voice,omitempty\"` // default \"en-US-MichelleNeural\"\n\tRate string `json:\"rate,omitempty\"` // speech rate, e.g. \"+0%\"\n}\n\n// TtsMiniMaxConfig configures the MiniMax TTS provider.\ntype TtsMiniMaxConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"`\n\tGroupID string `json:\"group_id,omitempty\"` // MiniMax GroupId (required)\n\tAPIBase string `json:\"api_base,omitempty\"` // default \"https://api.minimax.io/v1\"\n\tModel string `json:\"model,omitempty\"` // default \"speech-02-hd\"\n\tVoiceID string `json:\"voice_id,omitempty\"` // default \"Wise_Woman\"\n}\n\n// MergeChannelGroupQuotas merges per-group quota overrides from channel configs\n// (e.g., channels.telegram.groups[chatID].quota) into gateway.quota.groups.\n// This allows per-group quotas to be set at the channel level and picked up\n// by the QuotaChecker at runtime.\nfunc MergeChannelGroupQuotas(cfg *Config) {\n\tif cfg.Gateway.Quota == nil {\n\t\treturn\n\t}\n\tif cfg.Gateway.Quota.Groups == nil {\n\t\tcfg.Gateway.Quota.Groups = make(map[string]QuotaWindow)\n\t}\n\n\t// Telegram per-group quotas\n\tfor chatID, groupCfg := range cfg.Channels.Telegram.Groups {\n\t\tif groupCfg != nil && groupCfg.Quota != nil && !groupCfg.Quota.IsZero() {\n\t\t\tkey := \"group:telegram:\" + chatID\n\t\t\tcfg.Gateway.Quota.Groups[key] = *groupCfg.Quota\n\t\t}\n\t}\n}\n"}, "bd1411028b32b9b0351393e09cee7c0f3b5e1c78": {"t": 1785759255.569011, "n": 31233, "h": "9327e61d84e55682", "c": "package config\n\n// PendingCompactionConfig configures LLM-based compaction of pending group messages.\n// When a group accumulates more than Threshold pending messages, older messages are\n// summarized by an LLM and replaced with a compact summary, keeping KeepRecent raw messages.\ntype PendingCompactionConfig struct {\n\tThreshold int `json:\"threshold,omitempty\"` // trigger compaction when entries exceed this (default 200)\n\tKeepRecent int `json:\"keep_recent,omitempty\"` // keep this many recent raw messages after compaction (default 40)\n\tMaxTokens int `json:\"max_tokens,omitempty\"` // max output tokens for LLM summarization (default 4096)\n\tProvider string `json:\"provider,omitempty\"` // LLM provider name (e.g. \"openai\"); empty = use agent's provider\n\tModel string `json:\"model,omitempty\"` // model for summarization; empty = use agent's model\n}\n\n// ChannelsConfig contains per-channel configuration.\ntype ChannelsConfig struct {\n\tTelegram TelegramConfig `json:\"telegram\"`\n\tDiscord DiscordConfig `json:\"discord\"`\n\tSlack SlackConfig `json:\"slack\"`\n\tWhatsApp WhatsAppConfig `json:\"whatsapp\"`\n\tZalo ZaloConfig `json:\"zalo\"`\n\tZaloPersonal ZaloPersonalConfig `json:\"zalo_personal\"`\n\tFeishu FeishuConfig `json:\"feishu\"`\n\tPendingCompaction *PendingCompactionConfig `json:\"pending_compaction,omitempty\"` // global pending message compaction settings\n}\n\ntype TelegramConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tToken string `json:\"token\"`\n\tProxy string `json:\"proxy,omitempty\"`\n\tAPIServer string `json:\"api_server,omitempty\"` // custom Telegram Bot API server URL (e.g. \"http://localhost:8081\")\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in groups (default true)\n\tMentionMode string `json:\"mention_mode,omitempty\"` // \"strict\" (default) = only respond when mentioned; \"yield\" = respond unless another bot is mentioned\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tDMStream *bool `json:\"dm_stream,omitempty\"` // enable streaming for DMs (default false) — edits placeholder progressively\n\tGroupStream *bool `json:\"group_stream,omitempty\"` // enable streaming for groups (default false) — sends new message, edits progressively\n\tDraftTransport *bool `json:\"draft_transport,omitempty\"` // use sendMessageDraft for DM streaming (default true) — stealth preview, no notifications per edit\n\tReasoningStream *bool `json:\"reasoning_stream,omitempty\"` // show reasoning as separate message when provider emits thinking events (default true)\n\tReactionLevel string `json:\"reaction_level,omitempty\"` // \"off\" (default), \"minimal\", \"full\" — status emoji reactions\n\tMediaMaxBytes int64 `json:\"media_max_bytes,omitempty\"` // max media download size in bytes (default 20MB)\n\tLinkPreview *bool `json:\"link_preview,omitempty\"` // enable URL previews in messages (default true)\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tForceIPv4 bool `json:\"force_ipv4,omitempty\"` // force IPv4 for all Telegram API requests (use when IPv6 routing is broken)\n\n\t// Optional STT (Speech-to-Text) pipeline for voice/audio inbound messages.\n\t// When stt_proxy_url is set, audio/voice messages are transcribed before being forwarded to the agent.\n\tSTTProxyURL string `json:\"stt_proxy_url,omitempty\"` // base URL of the STT proxy service (e.g. \"https://stt.example.com\")\n\tSTTAPIKey string `json:\"stt_api_key,omitempty\"` // Bearer token for the STT proxy\n\tSTTTenantID string `json:\"stt_tenant_id,omitempty\"` // optional tenant/org identifier forwarded to the STT proxy\n\tSTTTimeoutSeconds int `json:\"stt_timeout_seconds,omitempty\"` // per-request timeout for STT calls (default 30s)\n\n\t// Optional audio-aware routing: when set, voice/audio inbound messages are routed to this\n\t// agent instead of the default channel agent. Requires the named agent to exist in the config.\n\tVoiceAgentID string `json:\"voice_agent_id,omitempty\"` // agent ID to route voice inbound to (e.g. \"speaking-agent\")\n\n\t// Audio guard: intercept technical errors in voice agent replies and replace with friendly fallbacks.\n\t// Only active when VoiceAgentID is set. Custom error markers replace built-in defaults when provided.\n\tAudioGuardFallbackTranscript string `json:\"audio_guard_fallback_transcript,omitempty\"` // fallback with %s for transcript (e.g. \"I heard: \\\"%s\\\". Try again!\")\n\tAudioGuardFallbackNoTranscript string `json:\"audio_guard_fallback_no_transcript,omitempty\"` // fallback when no transcript available\n\tAudioGuardErrorMarkers []string `json:\"audio_guard_error_markers,omitempty\"` // custom error detection markers (replaces defaults)\n\n\t// Per-group (and per-topic) overrides. Key is chat ID string (e.g. \"-100123456\") or \"*\" for wildcard.\n\t// TS ref: channels.telegram.groups in src/config/types.telegram.ts.\n\tGroups map[string]*TelegramGroupConfig `json:\"groups,omitempty\"`\n}\n\n// TelegramGroupConfig defines per-group overrides for a Telegram channel.\n// Matching TS TelegramGroupConfig in src/config/types.telegram.ts.\ntype TelegramGroupConfig struct {\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // override group policy for this group\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // override require_mention for this group\n\tMentionMode string `json:\"mention_mode,omitempty\"` // override mention_mode for this group\n\tAllowFrom FlexibleStringSlice `json:\"allow_from,omitempty\"` // override allow_from for this group\n\tEnabled *bool `json:\"enabled,omitempty\"` // disable bot for this group (default: true)\n\tSkills []string `json:\"skills,omitempty\"` // skill whitelist (nil = all, [] = none)\n\tTools []string `json:\"tools,omitempty\"` // tool allow list (nil = all, supports \"group:xxx\")\n\tSystemPrompt string `json:\"system_prompt,omitempty\"` // extra system prompt for this group\n\tTopics map[string]*TelegramTopicConfig `json:\"topics,omitempty\"` // per-topic overrides (key: thread ID string)\n\tQuota *QuotaWindow `json:\"quota,omitempty\"` // per-group quota override\n}\n\n// TelegramTopicConfig defines per-topic overrides within a Telegram group.\n// Matching TS TelegramTopicConfig in src/config/types.telegram.ts.\ntype TelegramTopicConfig struct {\n\tRequireMention *bool `json:\"require_mention,omitempty\"`\n\tMentionMode string `json:\"mention_mode,omitempty\"`\n\tGroupPolicy string `json:\"group_policy,omitempty\"`\n\tSkills []string `json:\"skills,omitempty\"`\n\tTools []string `json:\"tools,omitempty\"` // tool allow list (nil = inherit, supports \"group:xxx\")\n\tEnabled *bool `json:\"enabled,omitempty\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from,omitempty\"`\n\tSystemPrompt string `json:\"system_prompt,omitempty\"`\n}\n\ntype DiscordConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tToken string `json:\"token\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in groups (default true)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tMediaMaxBytes int64 `json:\"media_max_bytes,omitempty\"` // max media download size (default 25MB)\n\tSTTProxyURL string `json:\"stt_proxy_url,omitempty\"`\n\tSTTAPIKey string `json:\"stt_api_key,omitempty\"`\n\tSTTTenantID string `json:\"stt_tenant_id,omitempty\"`\n\tSTTTimeoutSeconds int `json:\"stt_timeout_seconds,omitempty\"`\n\tVoiceAgentID string `json:\"voice_agent_id,omitempty\"`\n}\n\ntype SlackConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tBotToken string `json:\"bot_token\"` // xoxb-... (Bot User OAuth Token)\n\tAppToken string `json:\"app_token\"` // xapp-... (App-Level Token for Socket Mode)\n\tUserToken string `json:\"user_token,omitempty\"` // xoxp-... (Optional: custom bot identity)\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"pairing\", \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in channels (default true)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tDMStream *bool `json:\"dm_stream,omitempty\"` // enable streaming for DMs (default false)\n\tGroupStream *bool `json:\"group_stream,omitempty\"` // enable streaming for groups (default false)\n\tNativeStream *bool `json:\"native_stream,omitempty\"` // use Slack ChatStreamer API if available (default false)\n\tReactionLevel string `json:\"reaction_level,omitempty\"` // \"off\" (default), \"minimal\", \"full\"\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tDebounceDelay int `json:\"debounce_delay,omitempty\"` // ms delay before dispatching rapid messages (default 300, 0=disabled)\n\tThreadTTL *int `json:\"thread_ttl,omitempty\"` // hours before thread participation expires (default 24, 0=disabled — always require @mention)\n\tMediaMaxBytes int64 `json:\"media_max_bytes,omitempty\"` // max file download size in bytes (default 20MB)\n}\n\ntype WhatsAppConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tBridgeURL string `json:\"bridge_url\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n}\n\ntype ZaloConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tToken string `json:\"token\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tWebhookURL string `json:\"webhook_url,omitempty\"`\n\tWebhookSecret string `json:\"webhook_secret,omitempty\"`\n\tMediaMaxMB int `json:\"media_max_mb,omitempty\"` // default 5\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n}\n\ntype ZaloPersonalConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in groups (default true)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tCredentialsPath string `json:\"credentials_path,omitempty\"` // path to saved cookies JSON\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n}\n\ntype FeishuConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tAppID string `json:\"app_id\"`\n\tAppSecret string `json:\"app_secret\"`\n\tEncryptKey string `json:\"encrypt_key,omitempty\"`\n\tVerificationToken string `json:\"verification_token,omitempty\"`\n\tDomain string `json:\"domain,omitempty\"` // \"lark\" (default/global), \"feishu\" (China), or custom URL\n\tConnectionMode string `json:\"connection_mode,omitempty\"` // \"websocket\" (default), \"webhook\"\n\tWebhookPort int `json:\"webhook_port,omitempty\"` // default 3000\n\tWebhookPath string `json:\"webhook_path,omitempty\"` // default \"/feishu/events\"\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default)\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default)\n\tGroupAllowFrom FlexibleStringSlice `json:\"group_allow_from,omitempty\"`\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // default true (groups)\n\tTopicSessionMode string `json:\"topic_session_mode,omitempty\"` // \"disabled\" (default)\n\tTextChunkLimit int `json:\"text_chunk_limit,omitempty\"` // default 4000\n\tMediaMaxMB int `json:\"media_max_mb,omitempty\"` // default 30\n\tRenderMode string `json:\"render_mode,omitempty\"` // \"auto\", \"raw\", \"card\"\n\tStreaming *bool `json:\"streaming,omitempty\"` // default true\n\tReactionLevel string `json:\"reaction_level,omitempty\"` // \"off\" (default), \"minimal\", \"full\" — typing emoji reactions\n\tHistoryLimit int `json:\"history_limit,omitempty\"`\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tSTTProxyURL string `json:\"stt_proxy_url,omitempty\"`\n\tSTTAPIKey string `json:\"stt_api_key,omitempty\"`\n\tSTTTenantID string `json:\"stt_tenant_id,omitempty\"`\n\tSTTTimeoutSeconds int `json:\"stt_timeout_seconds,omitempty\"`\n\tVoiceAgentID string `json:\"voice_agent_id,omitempty\"`\n}\n\n// ProvidersConfig maps provider name to its config.\ntype ProvidersConfig struct {\n\tAnthropic ProviderConfig `json:\"anthropic\"`\n\tOpenAI ProviderConfig `json:\"openai\"`\n\tOpenRouter ProviderConfig `json:\"openrouter\"`\n\tGroq ProviderConfig `json:\"groq\"`\n\tGemini ProviderConfig `json:\"gemini\"`\n\tDeepSeek ProviderConfig `json:\"deepseek\"`\n\tMistral ProviderConfig `json:\"mistral\"`\n\tXAI ProviderConfig `json:\"xai\"`\n\tMiniMax ProviderConfig `json:\"minimax\"`\n\tCohere ProviderConfig `json:\"cohere\"`\n\tPerplexity ProviderConfig `json:\"perplexity\"`\n\tDashScope ProviderConfig `json:\"dashscope\"`\n\tBailian ProviderConfig `json:\"bailian\"`\n\tZai ProviderConfig `json:\"zai\"`\n\tZaiCoding ProviderConfig `json:\"zai_coding\"`\n\tOllama OllamaConfig `json:\"ollama\"` // local Ollama instance (no API key needed)\n\tOllamaCloud ProviderConfig `json:\"ollama_cloud\"` // Ollama Cloud (API key required)\n\tClaudeCLI ClaudeCLIConfig `json:\"claude_cli\"`\n\tACP ACPConfig `json:\"acp\"`\n\tNovita ProviderConfig `json:\"novita\"` // Novita AI (OpenAI-compatible endpoint)\n}\n\n// OllamaConfig configures a local (or self-hosted) Ollama instance.\n// No API key is required — Ollama accepts any Bearer token value.\ntype OllamaConfig struct {\n\tHost string `json:\"host\"` // Ollama server base URL, e.g. http://localhost:11434\n}\n\n// ClaudeCLIConfig configures the Claude CLI provider (uses subscription, not API key).\ntype ClaudeCLIConfig struct {\n\tCLIPath string `json:\"cli_path\" yaml:\"cli_path\"` // path to claude binary (default: \"claude\")\n\tModel string `json:\"model\" yaml:\"model\"` // default model alias (default: \"sonnet\")\n\tBaseWorkDir string `json:\"base_work_dir\" yaml:\"base_work_dir\"` // base dir for agent workspaces\n\tPermMode string `json:\"perm_mode\" yaml:\"perm_mode\"` // permission mode (default: \"bypassPermissions\")\n}\n\n// ACPConfig configures the ACP (Agent Client Protocol) provider.\n// Orchestrates any ACP-compatible coding agent (Claude Code, Codex CLI, Gemini CLI) as a subprocess.\ntype ACPConfig struct {\n\tBinary string `json:\"binary\"` // agent binary name or path (e.g. \"claude\", \"codex\")\n\tArgs []string `json:\"args\"` // extra spawn args\n\tModel string `json:\"model\"` // default model/agent name\n\tWorkDir string `json:\"work_dir\"` // base workspace dir\n\tIdleTTL string `json:\"idle_ttl\"` // process idle TTL (e.g. \"5m\")\n\tPermMode string `json:\"perm_mode\"` // \"approve-all\" (default), \"approve-reads\", \"deny-all\"\n}\n\ntype ProviderConfig struct {\n\tAPIKey string `json:\"api_key\"`\n\tAPIBase string `json:\"api_base,omitempty\"`\n}\n\n// APIBaseForType returns the config-level api_base for a given provider type.\n// Used as a fallback when DB providers have no api_base set.\nfunc (p *ProvidersConfig) APIBaseForType(providerType string) string {\n\tswitch providerType {\n\tcase \"anthropic_native\":\n\t\treturn p.Anthropic.APIBase\n\tcase \"openai\", \"openai_compat\":\n\t\treturn p.OpenAI.APIBase\n\tcase \"openrouter\":\n\t\treturn p.OpenRouter.APIBase\n\tcase \"groq\":\n\t\treturn p.Groq.APIBase\n\tcase \"deepseek\":\n\t\treturn p.DeepSeek.APIBase\n\tcase \"gemini_native\":\n\t\treturn p.Gemini.APIBase\n\tcase \"mistral\":\n\t\treturn p.Mistral.APIBase\n\tcase \"xai\":\n\t\treturn p.XAI.APIBase\n\tcase \"minimax_native\":\n\t\treturn p.MiniMax.APIBase\n\tcase \"cohere\":\n\t\treturn p.Cohere.APIBase\n\tcase \"perplexity\":\n\t\treturn p.Perplexity.APIBase\n\tcase \"dashscope\":\n\t\treturn p.DashScope.APIBase\n\tcase \"bailian\":\n\t\treturn p.Bailian.APIBase\n\tcase \"zai\":\n\t\treturn p.Zai.APIBase\n\tcase \"zai_coding\":\n\t\treturn p.ZaiCoding.APIBase\n\tcase \"ollama_cloud\":\n\t\treturn p.OllamaCloud.APIBase\n\tcase \"novita\":\n\t\treturn p.Novita.APIBase\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\n// HasAnyProvider returns true if at least one provider has an API key or CLI configured.\nfunc (c *Config) HasAnyProvider() bool {\n\tp := c.Providers\n\treturn p.Anthropic.APIKey != \"\" ||\n\t\tp.OpenAI.APIKey != \"\" ||\n\t\tp.OpenRouter.APIKey != \"\" ||\n\t\tp.Groq.APIKey != \"\" ||\n\t\tp.Gemini.APIKey != \"\" ||\n\t\tp.DeepSeek.APIKey != \"\" ||\n\t\tp.Mistral.APIKey != \"\" ||\n\t\tp.XAI.APIKey != \"\" ||\n\t\tp.MiniMax.APIKey != \"\" ||\n\t\tp.Cohere.APIKey != \"\" ||\n\t\tp.Perplexity.APIKey != \"\" ||\n\t\tp.DashScope.APIKey != \"\" ||\n\t\tp.Bailian.APIKey != \"\" ||\n\t\tp.Zai.APIKey != \"\" ||\n\t\tp.ZaiCoding.APIKey != \"\" ||\n\t\tp.Ollama.Host != \"\" ||\n\t\tp.OllamaCloud.APIKey != \"\" ||\n\t\tp.ClaudeCLI.CLIPath != \"\" ||\n\t\tp.ACP.Binary != \"\" ||\n\t\tp.Novita.APIKey != \"\"\n}\n\n// QuotaWindow defines request limits per time window. Zero means unlimited.\ntype QuotaWindow struct {\n\tHour int `json:\"hour,omitempty\"` // max requests per hour (0 = unlimited)\n\tDay int `json:\"day,omitempty\"` // max requests per day (0 = unlimited)\n\tWeek int `json:\"week,omitempty\"` // max requests per week (0 = unlimited)\n}\n\n// IsZero returns true if no limits are set.\nfunc (w QuotaWindow) IsZero() bool { return w.Hour == 0 && w.Day == 0 && w.Week == 0 }\n\n// QuotaConfig configures per-user/group request quotas.\n// Config merge priority: Groups > Channels > Providers > Default.\ntype QuotaConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tDefault QuotaWindow `json:\"default\"`\n\tProviders map[string]QuotaWindow `json:\"providers,omitempty\"` // key = provider name (e.g. \"anthropic\")\n\tChannels map[string]QuotaWindow `json:\"channels,omitempty\"` // key = channel name (e.g. \"telegram\")\n\tGroups map[string]QuotaWindow `json:\"groups,omitempty\"` // key = userID (e.g. \"group:telegram:-100123\")\n}\n\n// GatewayConfig controls the gateway server.\ntype GatewayConfig struct {\n\tHost string `json:\"host\"`\n\tPort int `json:\"port\"`\n\tToken string `json:\"token,omitempty\"` // bearer token for WS/HTTP auth\n\tOwnerIDs []string `json:\"owner_ids,omitempty\"` // sender IDs considered \"owner\"\n\tAllowedOrigins []string `json:\"allowed_origins,omitempty\"` // WebSocket CORS whitelist (empty = allow all)\n\tMaxMessageChars int `json:\"max_message_chars,omitempty\"` // max user message characters (default 32000)\n\tRateLimitRPM int `json:\"rate_limit_rpm,omitempty\"` // rate limit: requests per minute per user (default 20, 0 = disabled)\n\tInjectionAction string `json:\"injection_action,omitempty\"` // prompt injection action: \"log\", \"warn\" (default), \"block\", \"off\"\n\tInboundDebounceMs int `json:\"inbound_debounce_ms,omitempty\"` // merge rapid messages from same sender (default 1000ms, -1 = disabled)\n\tQuota *QuotaConfig `json:\"quota,omitempty\"` // per-user/group request quotas\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // deliver intermediate text during tool iterations (default false)\n\tToolStatus *bool `json:\"tool_status,omitempty\"` // show tool name in streaming preview during tool execution (default true)\n\tTaskRecoveryIntervalSec int `json:\"task_recovery_interval_sec,omitempty\"` // team task recovery ticker interval in seconds (default 300 = 5min)\n}\n\n// ToolsConfig controls tool availability, policy, and web search.\ntype ToolsConfig struct {\n\tProfile string `json:\"profile,omitempty\"` // global profile: \"minimal\", \"coding\", \"messaging\", \"full\"\n\tAllow []string `json:\"allow,omitempty\"` // global allow list (tool names or \"group:xxx\")\n\tDeny []string `json:\"deny,omitempty\"` // global deny list\n\tAlsoAllow []string `json:\"alsoAllow,omitempty\"` // additive: adds without removing existing\n\tByProvider map[string]*ToolPolicySpec `json:\"byProvider,omitempty\"` // per-provider overrides\n\tExecApproval ExecApprovalCfg `json:\"execApproval\"` // exec command approval settings\n\tWebFetch WebFetchPolicyConfig `json:\"web_fetch\"` // domain policy for URL fetching\n\tWeb WebToolsConfig `json:\"web\"`\n\tBrowser BrowserToolConfig `json:\"browser\"`\n\tRateLimitPerHour int `json:\"rate_limit_per_hour,omitempty\"` // max tool executions per hour per session (0 = disabled)\n\tScrubCredentials *bool `json:\"scrub_credentials,omitempty\"` // auto-redact API keys/tokens in tool output (default true)\n\tMcpServers map[string]*MCPServerConfig `json:\"mcp_servers,omitempty\"` // external MCP server connections\n}\n\n// MCPServerConfig configures a single external MCP server connection.\ntype MCPServerConfig struct {\n\tTransport string `json:\"transport\"` // \"stdio\", \"sse\", \"streamable-http\"\n\tCommand string `json:\"command,omitempty\"` // stdio: command to spawn\n\tArgs []string `json:\"args,omitempty\"` // stdio: command arguments\n\tEnv map[string]string `json:\"env,omitempty\"` // stdio: extra environment variables\n\tURL string `json:\"url,omitempty\"` // sse/http: server URL\n\tHeaders map[string]string `json:\"headers,omitempty\"` // sse/http: extra HTTP headers\n\tEnabled *bool `json:\"enabled,omitempty\"` // default true\n\tToolPrefix string `json:\"tool_prefix,omitempty\"` // prefix for tool names (avoids collisions)\n\tTimeoutSec int `json:\"timeout_sec,omitempty\"` // per-tool-call timeout in seconds (default 60)\n}\n\n// IsEnabled returns whether this MCP server is enabled (default true).\nfunc (c *MCPServerConfig) IsEnabled() bool {\n\treturn c.Enabled == nil || *c.Enabled\n}\n\n// ExecApprovalCfg configures command execution approval (matching TS exec-approval.ts).\ntype ExecApprovalCfg struct {\n\tSecurity string `json:\"security,omitempty\"` // \"deny\", \"allowlist\", \"full\" (default \"full\")\n\tAsk string `json:\"ask,omitempty\"` // \"off\", \"on-miss\", \"always\" (default \"off\")\n\tAllowlist []string `json:\"allowlist,omitempty\"` // glob patterns for allowed commands\n}\n\n// WebFetchPolicyConfig controls domain filtering for the web_fetch tool.\ntype WebFetchPolicyConfig struct {\n\tPolicy string `json:\"policy,omitempty\"` // \"allow_all\" (default), \"allowlist\"\n\tAllowedDomains []string `json:\"allowed_domains,omitempty\"` // e.g. [\"github.com\", \"*.example.com\"]\n\tBlockedDomains []string `json:\"blocked_domains,omitempty\"` // always checked regardless of policy\n}\n\n// BrowserToolConfig controls the browser automation tool.\ntype BrowserToolConfig struct {\n\tEnabled bool `json:\"enabled\"` // enable the browser tool (default false)\n\tHeadless bool `json:\"headless,omitempty\"` // run Chrome in headless mode (ignored when RemoteURL is set)\n\tRemoteURL string `json:\"remote_url,omitempty\"` // CDP endpoint for remote Chrome sidecar, e.g. \"ws://chrome:9222\"\n\tActionTimeoutMs int `json:\"action_timeout_ms,omitempty\"` // per-action timeout in ms (default 30000)\n\tIdleTimeoutMs int `json:\"idle_timeout_ms,omitempty\"` // idle page auto-close in ms (default 600000, 0=disabled)\n\tMaxPages int `json:\"max_pages,omitempty\"` // max open pages per tenant (default 5)\n}\n\n// ToolPolicySpec defines a tool policy at any level (global, per-agent, per-provider).\ntype ToolPolicySpec struct {\n\tProfile string `json:\"profile,omitempty\"`\n\tAllow []string `json:\"allow,omitempty\"`\n\tDeny []string `json:\"deny,omitempty\"`\n\tAlsoAllow []string `json:\"alsoAllow,omitempty\"`\n\tByProvider map[string]*ToolPolicySpec `json:\"byProvider,omitempty\"`\n\tToolCallPrefix string `json:\"toolCallPrefix,omitempty\"` // prefix to strip from model's tool call names before registry lookup\n}\n\ntype WebToolsConfig struct {\n\tBrave BraveConfig `json:\"brave\"`\n\tDuckDuckGo DuckDuckGoConfig `json:\"duckduckgo\"`\n}\n\ntype BraveConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tAPIKey string `json:\"api_key\"`\n\tMaxResults int `json:\"max_results\"`\n}\n\ntype DuckDuckGoConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tMaxResults int `json:\"max_results\"`\n}\n\n// SessionsConfig controls session behavior.\n// Matching TS src/config/sessions/types.ts + src/config/types.base.ts.\ntype SessionsConfig struct {\n\tScope string `json:\"scope,omitempty\"` // \"per-sender\" (default), \"global\"\n\tDmScope string `json:\"dm_scope,omitempty\"` // \"main\", \"per-peer\", \"per-channel-peer\" (default), \"per-account-channel-peer\"\n\tMainKey string `json:\"main_key,omitempty\"` // main session key suffix (default \"main\", used when dm_scope=\"main\")\n}\n\n// TtsConfig configures text-to-speech.\n// Matching TS src/config/types.tts.ts.\ntype TtsConfig struct {\n\tProvider string `json:\"provider,omitempty\"` // \"openai\", \"elevenlabs\", \"edge\", \"minimax\"\n\tAuto string `json:\"auto,omitempty\"` // \"off\" (default), \"always\", \"inbound\", \"tagged\"\n\tMode string `json:\"mode,omitempty\"` // \"final\" (default), \"all\"\n\tMaxLength int `json:\"max_length,omitempty\"` // max text length before truncation (default 1500)\n\tTimeoutMs int `json:\"timeout_ms,omitempty\"` // API timeout in ms (default 30000)\n\tOpenAI TtsOpenAIConfig `json:\"openai\"`\n\tElevenLabs TtsElevenLabsConfig `json:\"elevenlabs\"`\n\tEdge TtsEdgeConfig `json:\"edge\"`\n\tMiniMax TtsMiniMaxConfig `json:\"minimax\"`\n}\n\n// TtsOpenAIConfig configures the OpenAI TTS provider.\ntype TtsOpenAIConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"`\n\tAPIBase string `json:\"api_base,omitempty\"` // custom endpoint URL\n\tModel string `json:\"model,omitempty\"` // default \"gpt-4o-mini-tts\"\n\tVoice string `json:\"voice,omitempty\"` // default \"alloy\"\n}\n\n// TtsElevenLabsConfig configures the ElevenLabs TTS provider.\ntype TtsElevenLabsConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"`\n\tBaseURL string `json:\"base_url,omitempty\"`\n\tVoiceID string `json:\"voice_id,omitempty\"` // default \"pMsXgVXv3BLzUgSXRplE\"\n\tModelID string `json:\"model_id,omitempty\"` // default \"eleven_multilingual_v2\"\n}\n\n// TtsEdgeConfig configures the Microsoft Edge TTS provider (free, no API key).\ntype TtsEdgeConfig struct {\n\tEnabled bool `json:\"enabled,omitempty\"`\n\tVoice string `json:\"voice,omitempty\"` // default \"en-US-MichelleNeural\"\n\tRate string `json:\"rate,omitempty\"` // speech rate, e.g. \"+0%\"\n}\n\n// TtsMiniMaxConfig configures the MiniMax TTS provider.\ntype TtsMiniMaxConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"`\n\tGroupID string `json:\"group_id,omitempty\"` // MiniMax GroupId (required)\n\tAPIBase string `json:\"api_base,omitempty\"` // default \"https://api.minimax.io/v1\"\n\tModel string `json:\"model,omitempty\"` // default \"speech-02-hd\"\n\tVoiceID string `json:\"voice_id,omitempty\"` // default \"Wise_Woman\"\n}\n\n// MergeChannelGroupQuotas merges per-group quota overrides from channel configs\n// (e.g., channels.telegram.groups[chatID].quota) into gateway.quota.groups.\n// This allows per-group quotas to be set at the channel level and picked up\n// by the QuotaChecker at runtime.\nfunc MergeChannelGroupQuotas(cfg *Config) {\n\tif cfg.Gateway.Quota == nil {\n\t\treturn\n\t}\n\tif cfg.Gateway.Quota.Groups == nil {\n\t\tcfg.Gateway.Quota.Groups = make(map[string]QuotaWindow)\n\t}\n\n\t// Telegram per-group quotas\n\tfor chatID, groupCfg := range cfg.Channels.Telegram.Groups {\n\t\tif groupCfg != nil && groupCfg.Quota != nil && !groupCfg.Quota.IsZero() {\n\t\t\tkey := \"group:telegram:\" + chatID\n\t\t\tcfg.Gateway.Quota.Groups[key] = *groupCfg.Quota\n\t\t}\n\t}\n}\n"}, "0dab087fa3b78efc5414d52bf3997a5fea42808b": {"t": 1785759257.1067867, "n": 31077, "h": "94a5ac2b74388dbc", "c": "package config\n\n// PendingCompactionConfig configures LLM-based compaction of pending group messages.\n// When a group accumulates more than Threshold pending messages, older messages are\n// summarized by an LLM and replaced with a compact summary, keeping KeepRecent raw messages.\ntype PendingCompactionConfig struct {\n\tThreshold int `json:\"threshold,omitempty\"` // trigger compaction when entries exceed this (default 200)\n\tKeepRecent int `json:\"keep_recent,omitempty\"` // keep this many recent raw messages after compaction (default 40)\n\tMaxTokens int `json:\"max_tokens,omitempty\"` // max output tokens for LLM summarization (default 4096)\n\tProvider string `json:\"provider,omitempty\"` // LLM provider name (e.g. \"openai\"); empty = use agent's provider\n\tModel string `json:\"model,omitempty\"` // model for summarization; empty = use agent's model\n}\n\n// ChannelsConfig contains per-channel configuration.\ntype ChannelsConfig struct {\n\tTelegram TelegramConfig `json:\"telegram\"`\n\tDiscord DiscordConfig `json:\"discord\"`\n\tSlack SlackConfig `json:\"slack\"`\n\tWhatsApp WhatsAppConfig `json:\"whatsapp\"`\n\tZalo ZaloConfig `json:\"zalo\"`\n\tZaloPersonal ZaloPersonalConfig `json:\"zalo_personal\"`\n\tFeishu FeishuConfig `json:\"feishu\"`\n\tPendingCompaction *PendingCompactionConfig `json:\"pending_compaction,omitempty\"` // global pending message compaction settings\n}\n\ntype TelegramConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tToken string `json:\"token\"`\n\tProxy string `json:\"proxy,omitempty\"`\n\tAPIServer string `json:\"api_server,omitempty\"` // custom Telegram Bot API server URL (e.g. \"http://localhost:8081\")\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in groups (default true)\n\tMentionMode string `json:\"mention_mode,omitempty\"` // \"strict\" (default) = only respond when mentioned; \"yield\" = respond unless another bot is mentioned\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tDMStream *bool `json:\"dm_stream,omitempty\"` // enable streaming for DMs (default false) — edits placeholder progressively\n\tGroupStream *bool `json:\"group_stream,omitempty\"` // enable streaming for groups (default false) — sends new message, edits progressively\n\tDraftTransport *bool `json:\"draft_transport,omitempty\"` // use sendMessageDraft for DM streaming (default true) — stealth preview, no notifications per edit\n\tReasoningStream *bool `json:\"reasoning_stream,omitempty\"` // show reasoning as separate message when provider emits thinking events (default true)\n\tReactionLevel string `json:\"reaction_level,omitempty\"` // \"off\" (default), \"minimal\", \"full\" — status emoji reactions\n\tMediaMaxBytes int64 `json:\"media_max_bytes,omitempty\"` // max media download size in bytes (default 20MB)\n\tLinkPreview *bool `json:\"link_preview,omitempty\"` // enable URL previews in messages (default true)\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tForceIPv4 bool `json:\"force_ipv4,omitempty\"` // force IPv4 for all Telegram API requests (use when IPv6 routing is broken)\n\n\t// Optional STT (Speech-to-Text) pipeline for voice/audio inbound messages.\n\t// When stt_proxy_url is set, audio/voice messages are transcribed before being forwarded to the agent.\n\tSTTProxyURL string `json:\"stt_proxy_url,omitempty\"` // base URL of the STT proxy service (e.g. \"https://stt.example.com\")\n\tSTTAPIKey string `json:\"stt_api_key,omitempty\"` // Bearer token for the STT proxy\n\tSTTTenantID string `json:\"stt_tenant_id,omitempty\"` // optional tenant/org identifier forwarded to the STT proxy\n\tSTTTimeoutSeconds int `json:\"stt_timeout_seconds,omitempty\"` // per-request timeout for STT calls (default 30s)\n\n\t// Optional audio-aware routing: when set, voice/audio inbound messages are routed to this\n\t// agent instead of the default channel agent. Requires the named agent to exist in the config.\n\tVoiceAgentID string `json:\"voice_agent_id,omitempty\"` // agent ID to route voice inbound to (e.g. \"speaking-agent\")\n\n\t// Audio guard: intercept technical errors in voice agent replies and replace with friendly fallbacks.\n\t// Only active when VoiceAgentID is set. Custom error markers replace built-in defaults when provided.\n\tAudioGuardFallbackTranscript string `json:\"audio_guard_fallback_transcript,omitempty\"` // fallback with %s for transcript (e.g. \"I heard: \\\"%s\\\". Try again!\")\n\tAudioGuardFallbackNoTranscript string `json:\"audio_guard_fallback_no_transcript,omitempty\"` // fallback when no transcript available\n\tAudioGuardErrorMarkers []string `json:\"audio_guard_error_markers,omitempty\"` // custom error detection markers (replaces defaults)\n\n\t// Per-group (and per-topic) overrides. Key is chat ID string (e.g. \"-100123456\") or \"*\" for wildcard.\n\t// TS ref: channels.telegram.groups in src/config/types.telegram.ts.\n\tGroups map[string]*TelegramGroupConfig `json:\"groups,omitempty\"`\n}\n\n// TelegramGroupConfig defines per-group overrides for a Telegram channel.\n// Matching TS TelegramGroupConfig in src/config/types.telegram.ts.\ntype TelegramGroupConfig struct {\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // override group policy for this group\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // override require_mention for this group\n\tMentionMode string `json:\"mention_mode,omitempty\"` // override mention_mode for this group\n\tAllowFrom FlexibleStringSlice `json:\"allow_from,omitempty\"` // override allow_from for this group\n\tEnabled *bool `json:\"enabled,omitempty\"` // disable bot for this group (default: true)\n\tSkills []string `json:\"skills,omitempty\"` // skill whitelist (nil = all, [] = none)\n\tTools []string `json:\"tools,omitempty\"` // tool allow list (nil = all, supports \"group:xxx\")\n\tSystemPrompt string `json:\"system_prompt,omitempty\"` // extra system prompt for this group\n\tTopics map[string]*TelegramTopicConfig `json:\"topics,omitempty\"` // per-topic overrides (key: thread ID string)\n\tQuota *QuotaWindow `json:\"quota,omitempty\"` // per-group quota override\n}\n\n// TelegramTopicConfig defines per-topic overrides within a Telegram group.\n// Matching TS TelegramTopicConfig in src/config/types.telegram.ts.\ntype TelegramTopicConfig struct {\n\tRequireMention *bool `json:\"require_mention,omitempty\"`\n\tMentionMode string `json:\"mention_mode,omitempty\"`\n\tGroupPolicy string `json:\"group_policy,omitempty\"`\n\tSkills []string `json:\"skills,omitempty\"`\n\tTools []string `json:\"tools,omitempty\"` // tool allow list (nil = inherit, supports \"group:xxx\")\n\tEnabled *bool `json:\"enabled,omitempty\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from,omitempty\"`\n\tSystemPrompt string `json:\"system_prompt,omitempty\"`\n}\n\ntype DiscordConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tToken string `json:\"token\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in groups (default true)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tMediaMaxBytes int64 `json:\"media_max_bytes,omitempty\"` // max media download size (default 25MB)\n\tSTTProxyURL string `json:\"stt_proxy_url,omitempty\"`\n\tSTTAPIKey string `json:\"stt_api_key,omitempty\"`\n\tSTTTenantID string `json:\"stt_tenant_id,omitempty\"`\n\tSTTTimeoutSeconds int `json:\"stt_timeout_seconds,omitempty\"`\n\tVoiceAgentID string `json:\"voice_agent_id,omitempty\"`\n}\n\ntype SlackConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tBotToken string `json:\"bot_token\"` // xoxb-... (Bot User OAuth Token)\n\tAppToken string `json:\"app_token\"` // xapp-... (App-Level Token for Socket Mode)\n\tUserToken string `json:\"user_token,omitempty\"` // xoxp-... (Optional: custom bot identity)\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"pairing\", \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in channels (default true)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tDMStream *bool `json:\"dm_stream,omitempty\"` // enable streaming for DMs (default false)\n\tGroupStream *bool `json:\"group_stream,omitempty\"` // enable streaming for groups (default false)\n\tNativeStream *bool `json:\"native_stream,omitempty\"` // use Slack ChatStreamer API if available (default false)\n\tReactionLevel string `json:\"reaction_level,omitempty\"` // \"off\" (default), \"minimal\", \"full\"\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tDebounceDelay int `json:\"debounce_delay,omitempty\"` // ms delay before dispatching rapid messages (default 300, 0=disabled)\n\tThreadTTL *int `json:\"thread_ttl,omitempty\"` // hours before thread participation expires (default 24, 0=disabled — always require @mention)\n\tMediaMaxBytes int64 `json:\"media_max_bytes,omitempty\"` // max file download size in bytes (default 20MB)\n}\n\ntype WhatsAppConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tBridgeURL string `json:\"bridge_url\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n}\n\ntype ZaloConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tToken string `json:\"token\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tWebhookURL string `json:\"webhook_url,omitempty\"`\n\tWebhookSecret string `json:\"webhook_secret,omitempty\"`\n\tMediaMaxMB int `json:\"media_max_mb,omitempty\"` // default 5\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n}\n\ntype ZaloPersonalConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in groups (default true)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tCredentialsPath string `json:\"credentials_path,omitempty\"` // path to saved cookies JSON\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n}\n\ntype FeishuConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tAppID string `json:\"app_id\"`\n\tAppSecret string `json:\"app_secret\"`\n\tEncryptKey string `json:\"encrypt_key,omitempty\"`\n\tVerificationToken string `json:\"verification_token,omitempty\"`\n\tDomain string `json:\"domain,omitempty\"` // \"lark\" (default/global), \"feishu\" (China), or custom URL\n\tConnectionMode string `json:\"connection_mode,omitempty\"` // \"websocket\" (default), \"webhook\"\n\tWebhookPort int `json:\"webhook_port,omitempty\"` // default 3000\n\tWebhookPath string `json:\"webhook_path,omitempty\"` // default \"/feishu/events\"\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default)\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default)\n\tGroupAllowFrom FlexibleStringSlice `json:\"group_allow_from,omitempty\"`\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // default true (groups)\n\tTopicSessionMode string `json:\"topic_session_mode,omitempty\"` // \"disabled\" (default)\n\tTextChunkLimit int `json:\"text_chunk_limit,omitempty\"` // default 4000\n\tMediaMaxMB int `json:\"media_max_mb,omitempty\"` // default 30\n\tRenderMode string `json:\"render_mode,omitempty\"` // \"auto\", \"raw\", \"card\"\n\tStreaming *bool `json:\"streaming,omitempty\"` // default true\n\tReactionLevel string `json:\"reaction_level,omitempty\"` // \"off\" (default), \"minimal\", \"full\" — typing emoji reactions\n\tHistoryLimit int `json:\"history_limit,omitempty\"`\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tSTTProxyURL string `json:\"stt_proxy_url,omitempty\"`\n\tSTTAPIKey string `json:\"stt_api_key,omitempty\"`\n\tSTTTenantID string `json:\"stt_tenant_id,omitempty\"`\n\tSTTTimeoutSeconds int `json:\"stt_timeout_seconds,omitempty\"`\n\tVoiceAgentID string `json:\"voice_agent_id,omitempty\"`\n}\n\n// ProvidersConfig maps provider name to its config.\ntype ProvidersConfig struct {\n\tAnthropic ProviderConfig `json:\"anthropic\"`\n\tOpenAI ProviderConfig `json:\"openai\"`\n\tOpenRouter ProviderConfig `json:\"openrouter\"`\n\tGroq ProviderConfig `json:\"groq\"`\n\tGemini ProviderConfig `json:\"gemini\"`\n\tDeepSeek ProviderConfig `json:\"deepseek\"`\n\tMistral ProviderConfig `json:\"mistral\"`\n\tXAI ProviderConfig `json:\"xai\"`\n\tMiniMax ProviderConfig `json:\"minimax\"`\n\tCohere ProviderConfig `json:\"cohere\"`\n\tPerplexity ProviderConfig `json:\"perplexity\"`\n\tDashScope ProviderConfig `json:\"dashscope\"`\n\tBailian ProviderConfig `json:\"bailian\"`\n\tZai ProviderConfig `json:\"zai\"`\n\tZaiCoding ProviderConfig `json:\"zai_coding\"`\n\tOllama OllamaConfig `json:\"ollama\"` // local Ollama instance (no API key needed)\n\tOllamaCloud ProviderConfig `json:\"ollama_cloud\"` // Ollama Cloud (API key required)\n\tClaudeCLI ClaudeCLIConfig `json:\"claude_cli\"`\n\tACP ACPConfig `json:\"acp\"`\n}\n\n// OllamaConfig configures a local (or self-hosted) Ollama instance.\n// No API key is required — Ollama accepts any Bearer token value.\ntype OllamaConfig struct {\n\tHost string `json:\"host\"` // Ollama server base URL, e.g. http://localhost:11434\n}\n\n// ClaudeCLIConfig configures the Claude CLI provider (uses subscription, not API key).\ntype ClaudeCLIConfig struct {\n\tCLIPath string `json:\"cli_path\" yaml:\"cli_path\"` // path to claude binary (default: \"claude\")\n\tModel string `json:\"model\" yaml:\"model\"` // default model alias (default: \"sonnet\")\n\tBaseWorkDir string `json:\"base_work_dir\" yaml:\"base_work_dir\"` // base dir for agent workspaces\n\tPermMode string `json:\"perm_mode\" yaml:\"perm_mode\"` // permission mode (default: \"bypassPermissions\")\n}\n\n// ACPConfig configures the ACP (Agent Client Protocol) provider.\n// Orchestrates any ACP-compatible coding agent (Claude Code, Codex CLI, Gemini CLI) as a subprocess.\ntype ACPConfig struct {\n\tBinary string `json:\"binary\"` // agent binary name or path (e.g. \"claude\", \"codex\")\n\tArgs []string `json:\"args\"` // extra spawn args\n\tModel string `json:\"model\"` // default model/agent name\n\tWorkDir string `json:\"work_dir\"` // base workspace dir\n\tIdleTTL string `json:\"idle_ttl\"` // process idle TTL (e.g. \"5m\")\n\tPermMode string `json:\"perm_mode\"` // \"approve-all\" (default), \"approve-reads\", \"deny-all\"\n}\n\ntype ProviderConfig struct {\n\tAPIKey string `json:\"api_key\"`\n\tAPIBase string `json:\"api_base,omitempty\"`\n}\n\n// APIBaseForType returns the config-level api_base for a given provider type.\n// Used as a fallback when DB providers have no api_base set.\nfunc (p *ProvidersConfig) APIBaseForType(providerType string) string {\n\tswitch providerType {\n\tcase \"anthropic_native\":\n\t\treturn p.Anthropic.APIBase\n\tcase \"openai\", \"openai_compat\":\n\t\treturn p.OpenAI.APIBase\n\tcase \"openrouter\":\n\t\treturn p.OpenRouter.APIBase\n\tcase \"groq\":\n\t\treturn p.Groq.APIBase\n\tcase \"deepseek\":\n\t\treturn p.DeepSeek.APIBase\n\tcase \"gemini_native\":\n\t\treturn p.Gemini.APIBase\n\tcase \"mistral\":\n\t\treturn p.Mistral.APIBase\n\tcase \"xai\":\n\t\treturn p.XAI.APIBase\n\tcase \"minimax_native\":\n\t\treturn p.MiniMax.APIBase\n\tcase \"cohere\":\n\t\treturn p.Cohere.APIBase\n\tcase \"perplexity\":\n\t\treturn p.Perplexity.APIBase\n\tcase \"dashscope\":\n\t\treturn p.DashScope.APIBase\n\tcase \"bailian\":\n\t\treturn p.Bailian.APIBase\n\tcase \"zai\":\n\t\treturn p.Zai.APIBase\n\tcase \"zai_coding\":\n\t\treturn p.ZaiCoding.APIBase\n\tcase \"ollama_cloud\":\n\t\treturn p.OllamaCloud.APIBase\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\n// HasAnyProvider returns true if at least one provider has an API key or CLI configured.\nfunc (c *Config) HasAnyProvider() bool {\n\tp := c.Providers\n\treturn p.Anthropic.APIKey != \"\" ||\n\t\tp.OpenAI.APIKey != \"\" ||\n\t\tp.OpenRouter.APIKey != \"\" ||\n\t\tp.Groq.APIKey != \"\" ||\n\t\tp.Gemini.APIKey != \"\" ||\n\t\tp.DeepSeek.APIKey != \"\" ||\n\t\tp.Mistral.APIKey != \"\" ||\n\t\tp.XAI.APIKey != \"\" ||\n\t\tp.MiniMax.APIKey != \"\" ||\n\t\tp.Cohere.APIKey != \"\" ||\n\t\tp.Perplexity.APIKey != \"\" ||\n\t\tp.DashScope.APIKey != \"\" ||\n\t\tp.Bailian.APIKey != \"\" ||\n\t\tp.Zai.APIKey != \"\" ||\n\t\tp.ZaiCoding.APIKey != \"\" ||\n\t\tp.Ollama.Host != \"\" ||\n\t\tp.OllamaCloud.APIKey != \"\" ||\n\t\tp.ClaudeCLI.CLIPath != \"\" ||\n\t\tp.ACP.Binary != \"\"\n}\n\n// QuotaWindow defines request limits per time window. Zero means unlimited.\ntype QuotaWindow struct {\n\tHour int `json:\"hour,omitempty\"` // max requests per hour (0 = unlimited)\n\tDay int `json:\"day,omitempty\"` // max requests per day (0 = unlimited)\n\tWeek int `json:\"week,omitempty\"` // max requests per week (0 = unlimited)\n}\n\n// IsZero returns true if no limits are set.\nfunc (w QuotaWindow) IsZero() bool { return w.Hour == 0 && w.Day == 0 && w.Week == 0 }\n\n// QuotaConfig configures per-user/group request quotas.\n// Config merge priority: Groups > Channels > Providers > Default.\ntype QuotaConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tDefault QuotaWindow `json:\"default\"`\n\tProviders map[string]QuotaWindow `json:\"providers,omitempty\"` // key = provider name (e.g. \"anthropic\")\n\tChannels map[string]QuotaWindow `json:\"channels,omitempty\"` // key = channel name (e.g. \"telegram\")\n\tGroups map[string]QuotaWindow `json:\"groups,omitempty\"` // key = userID (e.g. \"group:telegram:-100123\")\n}\n\n// GatewayConfig controls the gateway server.\ntype GatewayConfig struct {\n\tHost string `json:\"host\"`\n\tPort int `json:\"port\"`\n\tToken string `json:\"token,omitempty\"` // bearer token for WS/HTTP auth\n\tOwnerIDs []string `json:\"owner_ids,omitempty\"` // sender IDs considered \"owner\"\n\tAllowedOrigins []string `json:\"allowed_origins,omitempty\"` // WebSocket CORS whitelist (empty = allow all)\n\tMaxMessageChars int `json:\"max_message_chars,omitempty\"` // max user message characters (default 32000)\n\tRateLimitRPM int `json:\"rate_limit_rpm,omitempty\"` // rate limit: requests per minute per user (default 20, 0 = disabled)\n\tInjectionAction string `json:\"injection_action,omitempty\"` // prompt injection action: \"log\", \"warn\" (default), \"block\", \"off\"\n\tInboundDebounceMs int `json:\"inbound_debounce_ms,omitempty\"` // merge rapid messages from same sender (default 1000ms, -1 = disabled)\n\tQuota *QuotaConfig `json:\"quota,omitempty\"` // per-user/group request quotas\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // deliver intermediate text during tool iterations (default false)\n\tToolStatus *bool `json:\"tool_status,omitempty\"` // show tool name in streaming preview during tool execution (default true)\n\tTaskRecoveryIntervalSec int `json:\"task_recovery_interval_sec,omitempty\"` // team task recovery ticker interval in seconds (default 300 = 5min)\n}\n\n// ToolsConfig controls tool availability, policy, and web search.\ntype ToolsConfig struct {\n\tProfile string `json:\"profile,omitempty\"` // global profile: \"minimal\", \"coding\", \"messaging\", \"full\"\n\tAllow []string `json:\"allow,omitempty\"` // global allow list (tool names or \"group:xxx\")\n\tDeny []string `json:\"deny,omitempty\"` // global deny list\n\tAlsoAllow []string `json:\"alsoAllow,omitempty\"` // additive: adds without removing existing\n\tByProvider map[string]*ToolPolicySpec `json:\"byProvider,omitempty\"` // per-provider overrides\n\tExecApproval ExecApprovalCfg `json:\"execApproval\"` // exec command approval settings\n\tWebFetch WebFetchPolicyConfig `json:\"web_fetch\"` // domain policy for URL fetching\n\tWeb WebToolsConfig `json:\"web\"`\n\tBrowser BrowserToolConfig `json:\"browser\"`\n\tRateLimitPerHour int `json:\"rate_limit_per_hour,omitempty\"` // max tool executions per hour per session (0 = disabled)\n\tScrubCredentials *bool `json:\"scrub_credentials,omitempty\"` // auto-redact API keys/tokens in tool output (default true)\n\tMcpServers map[string]*MCPServerConfig `json:\"mcp_servers,omitempty\"` // external MCP server connections\n}\n\n// MCPServerConfig configures a single external MCP server connection.\ntype MCPServerConfig struct {\n\tTransport string `json:\"transport\"` // \"stdio\", \"sse\", \"streamable-http\"\n\tCommand string `json:\"command,omitempty\"` // stdio: command to spawn\n\tArgs []string `json:\"args,omitempty\"` // stdio: command arguments\n\tEnv map[string]string `json:\"env,omitempty\"` // stdio: extra environment variables\n\tURL string `json:\"url,omitempty\"` // sse/http: server URL\n\tHeaders map[string]string `json:\"headers,omitempty\"` // sse/http: extra HTTP headers\n\tEnabled *bool `json:\"enabled,omitempty\"` // default true\n\tToolPrefix string `json:\"tool_prefix,omitempty\"` // prefix for tool names (avoids collisions)\n\tTimeoutSec int `json:\"timeout_sec,omitempty\"` // per-tool-call timeout in seconds (default 60)\n}\n\n// IsEnabled returns whether this MCP server is enabled (default true).\nfunc (c *MCPServerConfig) IsEnabled() bool {\n\treturn c.Enabled == nil || *c.Enabled\n}\n\n// ExecApprovalCfg configures command execution approval (matching TS exec-approval.ts).\ntype ExecApprovalCfg struct {\n\tSecurity string `json:\"security,omitempty\"` // \"deny\", \"allowlist\", \"full\" (default \"full\")\n\tAsk string `json:\"ask,omitempty\"` // \"off\", \"on-miss\", \"always\" (default \"off\")\n\tAllowlist []string `json:\"allowlist,omitempty\"` // glob patterns for allowed commands\n}\n\n// WebFetchPolicyConfig controls domain filtering for the web_fetch tool.\ntype WebFetchPolicyConfig struct {\n\tPolicy string `json:\"policy,omitempty\"` // \"allow_all\" (default), \"allowlist\"\n\tAllowedDomains []string `json:\"allowed_domains,omitempty\"` // e.g. [\"github.com\", \"*.example.com\"]\n\tBlockedDomains []string `json:\"blocked_domains,omitempty\"` // always checked regardless of policy\n}\n\n// BrowserToolConfig controls the browser automation tool.\ntype BrowserToolConfig struct {\n\tEnabled bool `json:\"enabled\"` // enable the browser tool (default false)\n\tHeadless bool `json:\"headless,omitempty\"` // run Chrome in headless mode (ignored when RemoteURL is set)\n\tRemoteURL string `json:\"remote_url,omitempty\"` // CDP endpoint for remote Chrome sidecar, e.g. \"ws://chrome:9222\"\n\tActionTimeoutMs int `json:\"action_timeout_ms,omitempty\"` // per-action timeout in ms (default 30000)\n\tIdleTimeoutMs int `json:\"idle_timeout_ms,omitempty\"` // idle page auto-close in ms (default 600000, 0=disabled)\n\tMaxPages int `json:\"max_pages,omitempty\"` // max open pages per tenant (default 5)\n}\n\n// ToolPolicySpec defines a tool policy at any level (global, per-agent, per-provider).\ntype ToolPolicySpec struct {\n\tProfile string `json:\"profile,omitempty\"`\n\tAllow []string `json:\"allow,omitempty\"`\n\tDeny []string `json:\"deny,omitempty\"`\n\tAlsoAllow []string `json:\"alsoAllow,omitempty\"`\n\tByProvider map[string]*ToolPolicySpec `json:\"byProvider,omitempty\"`\n\tToolCallPrefix string `json:\"toolCallPrefix,omitempty\"` // prefix to strip from model's tool call names before registry lookup\n}\n\ntype WebToolsConfig struct {\n\tBrave BraveConfig `json:\"brave\"`\n\tDuckDuckGo DuckDuckGoConfig `json:\"duckduckgo\"`\n}\n\ntype BraveConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tAPIKey string `json:\"api_key\"`\n\tMaxResults int `json:\"max_results\"`\n}\n\ntype DuckDuckGoConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tMaxResults int `json:\"max_results\"`\n}\n\n// SessionsConfig controls session behavior.\n// Matching TS src/config/sessions/types.ts + src/config/types.base.ts.\ntype SessionsConfig struct {\n\tScope string `json:\"scope,omitempty\"` // \"per-sender\" (default), \"global\"\n\tDmScope string `json:\"dm_scope,omitempty\"` // \"main\", \"per-peer\", \"per-channel-peer\" (default), \"per-account-channel-peer\"\n\tMainKey string `json:\"main_key,omitempty\"` // main session key suffix (default \"main\", used when dm_scope=\"main\")\n}\n\n// TtsConfig configures text-to-speech.\n// Matching TS src/config/types.tts.ts.\ntype TtsConfig struct {\n\tProvider string `json:\"provider,omitempty\"` // \"openai\", \"elevenlabs\", \"edge\", \"minimax\"\n\tAuto string `json:\"auto,omitempty\"` // \"off\" (default), \"always\", \"inbound\", \"tagged\"\n\tMode string `json:\"mode,omitempty\"` // \"final\" (default), \"all\"\n\tMaxLength int `json:\"max_length,omitempty\"` // max text length before truncation (default 1500)\n\tTimeoutMs int `json:\"timeout_ms,omitempty\"` // API timeout in ms (default 30000)\n\tOpenAI TtsOpenAIConfig `json:\"openai\"`\n\tElevenLabs TtsElevenLabsConfig `json:\"elevenlabs\"`\n\tEdge TtsEdgeConfig `json:\"edge\"`\n\tMiniMax TtsMiniMaxConfig `json:\"minimax\"`\n}\n\n// TtsOpenAIConfig configures the OpenAI TTS provider.\ntype TtsOpenAIConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"`\n\tAPIBase string `json:\"api_base,omitempty\"` // custom endpoint URL\n\tModel string `json:\"model,omitempty\"` // default \"gpt-4o-mini-tts\"\n\tVoice string `json:\"voice,omitempty\"` // default \"alloy\"\n}\n\n// TtsElevenLabsConfig configures the ElevenLabs TTS provider.\ntype TtsElevenLabsConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"`\n\tBaseURL string `json:\"base_url,omitempty\"`\n\tVoiceID string `json:\"voice_id,omitempty\"` // default \"pMsXgVXv3BLzUgSXRplE\"\n\tModelID string `json:\"model_id,omitempty\"` // default \"eleven_multilingual_v2\"\n}\n\n// TtsEdgeConfig configures the Microsoft Edge TTS provider (free, no API key).\ntype TtsEdgeConfig struct {\n\tEnabled bool `json:\"enabled,omitempty\"`\n\tVoice string `json:\"voice,omitempty\"` // default \"en-US-MichelleNeural\"\n\tRate string `json:\"rate,omitempty\"` // speech rate, e.g. \"+0%\"\n}\n\n// TtsMiniMaxConfig configures the MiniMax TTS provider.\ntype TtsMiniMaxConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"`\n\tGroupID string `json:\"group_id,omitempty\"` // MiniMax GroupId (required)\n\tAPIBase string `json:\"api_base,omitempty\"` // default \"https://api.minimax.io/v1\"\n\tModel string `json:\"model,omitempty\"` // default \"speech-02-hd\"\n\tVoiceID string `json:\"voice_id,omitempty\"` // default \"Wise_Woman\"\n}\n\n// MergeChannelGroupQuotas merges per-group quota overrides from channel configs\n// (e.g., channels.telegram.groups[chatID].quota) into gateway.quota.groups.\n// This allows per-group quotas to be set at the channel level and picked up\n// by the QuotaChecker at runtime.\nfunc MergeChannelGroupQuotas(cfg *Config) {\n\tif cfg.Gateway.Quota == nil {\n\t\treturn\n\t}\n\tif cfg.Gateway.Quota.Groups == nil {\n\t\tcfg.Gateway.Quota.Groups = make(map[string]QuotaWindow)\n\t}\n\n\t// Telegram per-group quotas\n\tfor chatID, groupCfg := range cfg.Channels.Telegram.Groups {\n\t\tif groupCfg != nil && groupCfg.Quota != nil && !groupCfg.Quota.IsZero() {\n\t\t\tkey := \"group:telegram:\" + chatID\n\t\t\tcfg.Gateway.Quota.Groups[key] = *groupCfg.Quota\n\t\t}\n\t}\n}\n"}, "ee4d34ae28aa04b4e666d302debb1ff35d5c266a": {"t": 1785759258.5653458, "n": 30712, "h": "9919be9af9155f54", "c": "package config\n\n// PendingCompactionConfig configures LLM-based compaction of pending group messages.\n// When a group accumulates more than Threshold pending messages, older messages are\n// summarized by an LLM and replaced with a compact summary, keeping KeepRecent raw messages.\ntype PendingCompactionConfig struct {\n\tThreshold int `json:\"threshold,omitempty\"` // trigger compaction when entries exceed this (default 200)\n\tKeepRecent int `json:\"keep_recent,omitempty\"` // keep this many recent raw messages after compaction (default 40)\n\tMaxTokens int `json:\"max_tokens,omitempty\"` // max output tokens for LLM summarization (default 4096)\n\tProvider string `json:\"provider,omitempty\"` // LLM provider name (e.g. \"openai\"); empty = use agent's provider\n\tModel string `json:\"model,omitempty\"` // model for summarization; empty = use agent's model\n}\n\n// ChannelsConfig contains per-channel configuration.\ntype ChannelsConfig struct {\n\tTelegram TelegramConfig `json:\"telegram\"`\n\tDiscord DiscordConfig `json:\"discord\"`\n\tSlack SlackConfig `json:\"slack\"`\n\tWhatsApp WhatsAppConfig `json:\"whatsapp\"`\n\tZalo ZaloConfig `json:\"zalo\"`\n\tZaloPersonal ZaloPersonalConfig `json:\"zalo_personal\"`\n\tFeishu FeishuConfig `json:\"feishu\"`\n\tPendingCompaction *PendingCompactionConfig `json:\"pending_compaction,omitempty\"` // global pending message compaction settings\n}\n\ntype TelegramConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tToken string `json:\"token\"`\n\tProxy string `json:\"proxy,omitempty\"`\n\tAPIServer string `json:\"api_server,omitempty\"` // custom Telegram Bot API server URL (e.g. \"http://localhost:8081\")\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in groups (default true)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tDMStream *bool `json:\"dm_stream,omitempty\"` // enable streaming for DMs (default false) — edits placeholder progressively\n\tGroupStream *bool `json:\"group_stream,omitempty\"` // enable streaming for groups (default false) — sends new message, edits progressively\n\tDraftTransport *bool `json:\"draft_transport,omitempty\"` // use sendMessageDraft for DM streaming (default true) — stealth preview, no notifications per edit\n\tReasoningStream *bool `json:\"reasoning_stream,omitempty\"` // show reasoning as separate message when provider emits thinking events (default true)\n\tReactionLevel string `json:\"reaction_level,omitempty\"` // \"off\" (default), \"minimal\", \"full\" — status emoji reactions\n\tMediaMaxBytes int64 `json:\"media_max_bytes,omitempty\"` // max media download size in bytes (default 20MB)\n\tLinkPreview *bool `json:\"link_preview,omitempty\"` // enable URL previews in messages (default true)\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tForceIPv4 bool `json:\"force_ipv4,omitempty\"` // force IPv4 for all Telegram API requests (use when IPv6 routing is broken)\n\n\t// Optional STT (Speech-to-Text) pipeline for voice/audio inbound messages.\n\t// When stt_proxy_url is set, audio/voice messages are transcribed before being forwarded to the agent.\n\tSTTProxyURL string `json:\"stt_proxy_url,omitempty\"` // base URL of the STT proxy service (e.g. \"https://stt.example.com\")\n\tSTTAPIKey string `json:\"stt_api_key,omitempty\"` // Bearer token for the STT proxy\n\tSTTTenantID string `json:\"stt_tenant_id,omitempty\"` // optional tenant/org identifier forwarded to the STT proxy\n\tSTTTimeoutSeconds int `json:\"stt_timeout_seconds,omitempty\"` // per-request timeout for STT calls (default 30s)\n\n\t// Optional audio-aware routing: when set, voice/audio inbound messages are routed to this\n\t// agent instead of the default channel agent. Requires the named agent to exist in the config.\n\tVoiceAgentID string `json:\"voice_agent_id,omitempty\"` // agent ID to route voice inbound to (e.g. \"speaking-agent\")\n\n\t// Audio guard: intercept technical errors in voice agent replies and replace with friendly fallbacks.\n\t// Only active when VoiceAgentID is set. Custom error markers replace built-in defaults when provided.\n\tAudioGuardFallbackTranscript string `json:\"audio_guard_fallback_transcript,omitempty\"` // fallback with %s for transcript (e.g. \"I heard: \\\"%s\\\". Try again!\")\n\tAudioGuardFallbackNoTranscript string `json:\"audio_guard_fallback_no_transcript,omitempty\"` // fallback when no transcript available\n\tAudioGuardErrorMarkers []string `json:\"audio_guard_error_markers,omitempty\"` // custom error detection markers (replaces defaults)\n\n\t// Per-group (and per-topic) overrides. Key is chat ID string (e.g. \"-100123456\") or \"*\" for wildcard.\n\t// TS ref: channels.telegram.groups in src/config/types.telegram.ts.\n\tGroups map[string]*TelegramGroupConfig `json:\"groups,omitempty\"`\n}\n\n// TelegramGroupConfig defines per-group overrides for a Telegram channel.\n// Matching TS TelegramGroupConfig in src/config/types.telegram.ts.\ntype TelegramGroupConfig struct {\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // override group policy for this group\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // override require_mention for this group\n\tAllowFrom FlexibleStringSlice `json:\"allow_from,omitempty\"` // override allow_from for this group\n\tEnabled *bool `json:\"enabled,omitempty\"` // disable bot for this group (default: true)\n\tSkills []string `json:\"skills,omitempty\"` // skill whitelist (nil = all, [] = none)\n\tTools []string `json:\"tools,omitempty\"` // tool allow list (nil = all, supports \"group:xxx\")\n\tSystemPrompt string `json:\"system_prompt,omitempty\"` // extra system prompt for this group\n\tTopics map[string]*TelegramTopicConfig `json:\"topics,omitempty\"` // per-topic overrides (key: thread ID string)\n\tQuota *QuotaWindow `json:\"quota,omitempty\"` // per-group quota override\n}\n\n// TelegramTopicConfig defines per-topic overrides within a Telegram group.\n// Matching TS TelegramTopicConfig in src/config/types.telegram.ts.\ntype TelegramTopicConfig struct {\n\tRequireMention *bool `json:\"require_mention,omitempty\"`\n\tGroupPolicy string `json:\"group_policy,omitempty\"`\n\tSkills []string `json:\"skills,omitempty\"`\n\tTools []string `json:\"tools,omitempty\"` // tool allow list (nil = inherit, supports \"group:xxx\")\n\tEnabled *bool `json:\"enabled,omitempty\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from,omitempty\"`\n\tSystemPrompt string `json:\"system_prompt,omitempty\"`\n}\n\ntype DiscordConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tToken string `json:\"token\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in groups (default true)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tMediaMaxBytes int64 `json:\"media_max_bytes,omitempty\"` // max media download size (default 25MB)\n\tSTTProxyURL string `json:\"stt_proxy_url,omitempty\"`\n\tSTTAPIKey string `json:\"stt_api_key,omitempty\"`\n\tSTTTenantID string `json:\"stt_tenant_id,omitempty\"`\n\tSTTTimeoutSeconds int `json:\"stt_timeout_seconds,omitempty\"`\n\tVoiceAgentID string `json:\"voice_agent_id,omitempty\"`\n}\n\ntype SlackConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tBotToken string `json:\"bot_token\"` // xoxb-... (Bot User OAuth Token)\n\tAppToken string `json:\"app_token\"` // xapp-... (App-Level Token for Socket Mode)\n\tUserToken string `json:\"user_token,omitempty\"` // xoxp-... (Optional: custom bot identity)\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"pairing\", \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in channels (default true)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tDMStream *bool `json:\"dm_stream,omitempty\"` // enable streaming for DMs (default false)\n\tGroupStream *bool `json:\"group_stream,omitempty\"` // enable streaming for groups (default false)\n\tNativeStream *bool `json:\"native_stream,omitempty\"` // use Slack ChatStreamer API if available (default false)\n\tReactionLevel string `json:\"reaction_level,omitempty\"` // \"off\" (default), \"minimal\", \"full\"\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tDebounceDelay int `json:\"debounce_delay,omitempty\"` // ms delay before dispatching rapid messages (default 300, 0=disabled)\n\tThreadTTL *int `json:\"thread_ttl,omitempty\"` // hours before thread participation expires (default 24, 0=disabled — always require @mention)\n\tMediaMaxBytes int64 `json:\"media_max_bytes,omitempty\"` // max file download size in bytes (default 20MB)\n}\n\ntype WhatsAppConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tBridgeURL string `json:\"bridge_url\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n}\n\ntype ZaloConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tToken string `json:\"token\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tWebhookURL string `json:\"webhook_url,omitempty\"`\n\tWebhookSecret string `json:\"webhook_secret,omitempty\"`\n\tMediaMaxMB int `json:\"media_max_mb,omitempty\"` // default 5\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n}\n\ntype ZaloPersonalConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in groups (default true)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tCredentialsPath string `json:\"credentials_path,omitempty\"` // path to saved cookies JSON\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n}\n\ntype FeishuConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tAppID string `json:\"app_id\"`\n\tAppSecret string `json:\"app_secret\"`\n\tEncryptKey string `json:\"encrypt_key,omitempty\"`\n\tVerificationToken string `json:\"verification_token,omitempty\"`\n\tDomain string `json:\"domain,omitempty\"` // \"lark\" (default/global), \"feishu\" (China), or custom URL\n\tConnectionMode string `json:\"connection_mode,omitempty\"` // \"websocket\" (default), \"webhook\"\n\tWebhookPort int `json:\"webhook_port,omitempty\"` // default 3000\n\tWebhookPath string `json:\"webhook_path,omitempty\"` // default \"/feishu/events\"\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default)\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default)\n\tGroupAllowFrom FlexibleStringSlice `json:\"group_allow_from,omitempty\"`\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // default true (groups)\n\tTopicSessionMode string `json:\"topic_session_mode,omitempty\"` // \"disabled\" (default)\n\tTextChunkLimit int `json:\"text_chunk_limit,omitempty\"` // default 4000\n\tMediaMaxMB int `json:\"media_max_mb,omitempty\"` // default 30\n\tRenderMode string `json:\"render_mode,omitempty\"` // \"auto\", \"raw\", \"card\"\n\tStreaming *bool `json:\"streaming,omitempty\"` // default true\n\tReactionLevel string `json:\"reaction_level,omitempty\"` // \"off\" (default), \"minimal\", \"full\" — typing emoji reactions\n\tHistoryLimit int `json:\"history_limit,omitempty\"`\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tSTTProxyURL string `json:\"stt_proxy_url,omitempty\"`\n\tSTTAPIKey string `json:\"stt_api_key,omitempty\"`\n\tSTTTenantID string `json:\"stt_tenant_id,omitempty\"`\n\tSTTTimeoutSeconds int `json:\"stt_timeout_seconds,omitempty\"`\n\tVoiceAgentID string `json:\"voice_agent_id,omitempty\"`\n}\n\n// ProvidersConfig maps provider name to its config.\ntype ProvidersConfig struct {\n\tAnthropic ProviderConfig `json:\"anthropic\"`\n\tOpenAI ProviderConfig `json:\"openai\"`\n\tOpenRouter ProviderConfig `json:\"openrouter\"`\n\tGroq ProviderConfig `json:\"groq\"`\n\tGemini ProviderConfig `json:\"gemini\"`\n\tDeepSeek ProviderConfig `json:\"deepseek\"`\n\tMistral ProviderConfig `json:\"mistral\"`\n\tXAI ProviderConfig `json:\"xai\"`\n\tMiniMax ProviderConfig `json:\"minimax\"`\n\tCohere ProviderConfig `json:\"cohere\"`\n\tPerplexity ProviderConfig `json:\"perplexity\"`\n\tDashScope ProviderConfig `json:\"dashscope\"`\n\tBailian ProviderConfig `json:\"bailian\"`\n\tZai ProviderConfig `json:\"zai\"`\n\tZaiCoding ProviderConfig `json:\"zai_coding\"`\n\tOllama OllamaConfig `json:\"ollama\"` // local Ollama instance (no API key needed)\n\tOllamaCloud ProviderConfig `json:\"ollama_cloud\"` // Ollama Cloud (API key required)\n\tClaudeCLI ClaudeCLIConfig `json:\"claude_cli\"`\n\tACP ACPConfig `json:\"acp\"`\n}\n\n// OllamaConfig configures a local (or self-hosted) Ollama instance.\n// No API key is required — Ollama accepts any Bearer token value.\ntype OllamaConfig struct {\n\tHost string `json:\"host\"` // Ollama server base URL, e.g. http://localhost:11434\n}\n\n// ClaudeCLIConfig configures the Claude CLI provider (uses subscription, not API key).\ntype ClaudeCLIConfig struct {\n\tCLIPath string `json:\"cli_path\" yaml:\"cli_path\"` // path to claude binary (default: \"claude\")\n\tModel string `json:\"model\" yaml:\"model\"` // default model alias (default: \"sonnet\")\n\tBaseWorkDir string `json:\"base_work_dir\" yaml:\"base_work_dir\"` // base dir for agent workspaces\n\tPermMode string `json:\"perm_mode\" yaml:\"perm_mode\"` // permission mode (default: \"bypassPermissions\")\n}\n\n// ACPConfig configures the ACP (Agent Client Protocol) provider.\n// Orchestrates any ACP-compatible coding agent (Claude Code, Codex CLI, Gemini CLI) as a subprocess.\ntype ACPConfig struct {\n\tBinary string `json:\"binary\"` // agent binary name or path (e.g. \"claude\", \"codex\")\n\tArgs []string `json:\"args\"` // extra spawn args\n\tModel string `json:\"model\"` // default model/agent name\n\tWorkDir string `json:\"work_dir\"` // base workspace dir\n\tIdleTTL string `json:\"idle_ttl\"` // process idle TTL (e.g. \"5m\")\n\tPermMode string `json:\"perm_mode\"` // \"approve-all\" (default), \"approve-reads\", \"deny-all\"\n}\n\ntype ProviderConfig struct {\n\tAPIKey string `json:\"api_key\"`\n\tAPIBase string `json:\"api_base,omitempty\"`\n}\n\n// APIBaseForType returns the config-level api_base for a given provider type.\n// Used as a fallback when DB providers have no api_base set.\nfunc (p *ProvidersConfig) APIBaseForType(providerType string) string {\n\tswitch providerType {\n\tcase \"anthropic_native\":\n\t\treturn p.Anthropic.APIBase\n\tcase \"openai\", \"openai_compat\":\n\t\treturn p.OpenAI.APIBase\n\tcase \"openrouter\":\n\t\treturn p.OpenRouter.APIBase\n\tcase \"groq\":\n\t\treturn p.Groq.APIBase\n\tcase \"deepseek\":\n\t\treturn p.DeepSeek.APIBase\n\tcase \"gemini_native\":\n\t\treturn p.Gemini.APIBase\n\tcase \"mistral\":\n\t\treturn p.Mistral.APIBase\n\tcase \"xai\":\n\t\treturn p.XAI.APIBase\n\tcase \"minimax_native\":\n\t\treturn p.MiniMax.APIBase\n\tcase \"cohere\":\n\t\treturn p.Cohere.APIBase\n\tcase \"perplexity\":\n\t\treturn p.Perplexity.APIBase\n\tcase \"dashscope\":\n\t\treturn p.DashScope.APIBase\n\tcase \"bailian\":\n\t\treturn p.Bailian.APIBase\n\tcase \"zai\":\n\t\treturn p.Zai.APIBase\n\tcase \"zai_coding\":\n\t\treturn p.ZaiCoding.APIBase\n\tcase \"ollama_cloud\":\n\t\treturn p.OllamaCloud.APIBase\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\n// HasAnyProvider returns true if at least one provider has an API key or CLI configured.\nfunc (c *Config) HasAnyProvider() bool {\n\tp := c.Providers\n\treturn p.Anthropic.APIKey != \"\" ||\n\t\tp.OpenAI.APIKey != \"\" ||\n\t\tp.OpenRouter.APIKey != \"\" ||\n\t\tp.Groq.APIKey != \"\" ||\n\t\tp.Gemini.APIKey != \"\" ||\n\t\tp.DeepSeek.APIKey != \"\" ||\n\t\tp.Mistral.APIKey != \"\" ||\n\t\tp.XAI.APIKey != \"\" ||\n\t\tp.MiniMax.APIKey != \"\" ||\n\t\tp.Cohere.APIKey != \"\" ||\n\t\tp.Perplexity.APIKey != \"\" ||\n\t\tp.DashScope.APIKey != \"\" ||\n\t\tp.Bailian.APIKey != \"\" ||\n\t\tp.Zai.APIKey != \"\" ||\n\t\tp.ZaiCoding.APIKey != \"\" ||\n\t\tp.Ollama.Host != \"\" ||\n\t\tp.OllamaCloud.APIKey != \"\" ||\n\t\tp.ClaudeCLI.CLIPath != \"\" ||\n\t\tp.ACP.Binary != \"\"\n}\n\n// QuotaWindow defines request limits per time window. Zero means unlimited.\ntype QuotaWindow struct {\n\tHour int `json:\"hour,omitempty\"` // max requests per hour (0 = unlimited)\n\tDay int `json:\"day,omitempty\"` // max requests per day (0 = unlimited)\n\tWeek int `json:\"week,omitempty\"` // max requests per week (0 = unlimited)\n}\n\n// IsZero returns true if no limits are set.\nfunc (w QuotaWindow) IsZero() bool { return w.Hour == 0 && w.Day == 0 && w.Week == 0 }\n\n// QuotaConfig configures per-user/group request quotas.\n// Config merge priority: Groups > Channels > Providers > Default.\ntype QuotaConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tDefault QuotaWindow `json:\"default\"`\n\tProviders map[string]QuotaWindow `json:\"providers,omitempty\"` // key = provider name (e.g. \"anthropic\")\n\tChannels map[string]QuotaWindow `json:\"channels,omitempty\"` // key = channel name (e.g. \"telegram\")\n\tGroups map[string]QuotaWindow `json:\"groups,omitempty\"` // key = userID (e.g. \"group:telegram:-100123\")\n}\n\n// GatewayConfig controls the gateway server.\ntype GatewayConfig struct {\n\tHost string `json:\"host\"`\n\tPort int `json:\"port\"`\n\tToken string `json:\"token,omitempty\"` // bearer token for WS/HTTP auth\n\tOwnerIDs []string `json:\"owner_ids,omitempty\"` // sender IDs considered \"owner\"\n\tAllowedOrigins []string `json:\"allowed_origins,omitempty\"` // WebSocket CORS whitelist (empty = allow all)\n\tMaxMessageChars int `json:\"max_message_chars,omitempty\"` // max user message characters (default 32000)\n\tRateLimitRPM int `json:\"rate_limit_rpm,omitempty\"` // rate limit: requests per minute per user (default 20, 0 = disabled)\n\tInjectionAction string `json:\"injection_action,omitempty\"` // prompt injection action: \"log\", \"warn\" (default), \"block\", \"off\"\n\tInboundDebounceMs int `json:\"inbound_debounce_ms,omitempty\"` // merge rapid messages from same sender (default 1000ms, -1 = disabled)\n\tQuota *QuotaConfig `json:\"quota,omitempty\"` // per-user/group request quotas\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // deliver intermediate text during tool iterations (default false)\n\tToolStatus *bool `json:\"tool_status,omitempty\"` // show tool name in streaming preview during tool execution (default true)\n\tTaskRecoveryIntervalSec int `json:\"task_recovery_interval_sec,omitempty\"` // team task recovery ticker interval in seconds (default 300 = 5min)\n}\n\n// ToolsConfig controls tool availability, policy, and web search.\ntype ToolsConfig struct {\n\tProfile string `json:\"profile,omitempty\"` // global profile: \"minimal\", \"coding\", \"messaging\", \"full\"\n\tAllow []string `json:\"allow,omitempty\"` // global allow list (tool names or \"group:xxx\")\n\tDeny []string `json:\"deny,omitempty\"` // global deny list\n\tAlsoAllow []string `json:\"alsoAllow,omitempty\"` // additive: adds without removing existing\n\tByProvider map[string]*ToolPolicySpec `json:\"byProvider,omitempty\"` // per-provider overrides\n\tExecApproval ExecApprovalCfg `json:\"execApproval\"` // exec command approval settings\n\tWebFetch WebFetchPolicyConfig `json:\"web_fetch\"` // domain policy for URL fetching\n\tWeb WebToolsConfig `json:\"web\"`\n\tBrowser BrowserToolConfig `json:\"browser\"`\n\tRateLimitPerHour int `json:\"rate_limit_per_hour,omitempty\"` // max tool executions per hour per session (0 = disabled)\n\tScrubCredentials *bool `json:\"scrub_credentials,omitempty\"` // auto-redact API keys/tokens in tool output (default true)\n\tMcpServers map[string]*MCPServerConfig `json:\"mcp_servers,omitempty\"` // external MCP server connections\n}\n\n// MCPServerConfig configures a single external MCP server connection.\ntype MCPServerConfig struct {\n\tTransport string `json:\"transport\"` // \"stdio\", \"sse\", \"streamable-http\"\n\tCommand string `json:\"command,omitempty\"` // stdio: command to spawn\n\tArgs []string `json:\"args,omitempty\"` // stdio: command arguments\n\tEnv map[string]string `json:\"env,omitempty\"` // stdio: extra environment variables\n\tURL string `json:\"url,omitempty\"` // sse/http: server URL\n\tHeaders map[string]string `json:\"headers,omitempty\"` // sse/http: extra HTTP headers\n\tEnabled *bool `json:\"enabled,omitempty\"` // default true\n\tToolPrefix string `json:\"tool_prefix,omitempty\"` // prefix for tool names (avoids collisions)\n\tTimeoutSec int `json:\"timeout_sec,omitempty\"` // per-tool-call timeout in seconds (default 60)\n}\n\n// IsEnabled returns whether this MCP server is enabled (default true).\nfunc (c *MCPServerConfig) IsEnabled() bool {\n\treturn c.Enabled == nil || *c.Enabled\n}\n\n// ExecApprovalCfg configures command execution approval (matching TS exec-approval.ts).\ntype ExecApprovalCfg struct {\n\tSecurity string `json:\"security,omitempty\"` // \"deny\", \"allowlist\", \"full\" (default \"full\")\n\tAsk string `json:\"ask,omitempty\"` // \"off\", \"on-miss\", \"always\" (default \"off\")\n\tAllowlist []string `json:\"allowlist,omitempty\"` // glob patterns for allowed commands\n}\n\n// WebFetchPolicyConfig controls domain filtering for the web_fetch tool.\ntype WebFetchPolicyConfig struct {\n\tPolicy string `json:\"policy,omitempty\"` // \"allow_all\" (default), \"allowlist\"\n\tAllowedDomains []string `json:\"allowed_domains,omitempty\"` // e.g. [\"github.com\", \"*.example.com\"]\n\tBlockedDomains []string `json:\"blocked_domains,omitempty\"` // always checked regardless of policy\n}\n\n// BrowserToolConfig controls the browser automation tool.\ntype BrowserToolConfig struct {\n\tEnabled bool `json:\"enabled\"` // enable the browser tool (default false)\n\tHeadless bool `json:\"headless,omitempty\"` // run Chrome in headless mode (ignored when RemoteURL is set)\n\tRemoteURL string `json:\"remote_url,omitempty\"` // CDP endpoint for remote Chrome sidecar, e.g. \"ws://chrome:9222\"\n\tActionTimeoutMs int `json:\"action_timeout_ms,omitempty\"` // per-action timeout in ms (default 30000)\n\tIdleTimeoutMs int `json:\"idle_timeout_ms,omitempty\"` // idle page auto-close in ms (default 600000, 0=disabled)\n\tMaxPages int `json:\"max_pages,omitempty\"` // max open pages per tenant (default 5)\n}\n\n// ToolPolicySpec defines a tool policy at any level (global, per-agent, per-provider).\ntype ToolPolicySpec struct {\n\tProfile string `json:\"profile,omitempty\"`\n\tAllow []string `json:\"allow,omitempty\"`\n\tDeny []string `json:\"deny,omitempty\"`\n\tAlsoAllow []string `json:\"alsoAllow,omitempty\"`\n\tByProvider map[string]*ToolPolicySpec `json:\"byProvider,omitempty\"`\n\tToolCallPrefix string `json:\"toolCallPrefix,omitempty\"` // prefix to strip from model's tool call names before registry lookup\n}\n\ntype WebToolsConfig struct {\n\tBrave BraveConfig `json:\"brave\"`\n\tDuckDuckGo DuckDuckGoConfig `json:\"duckduckgo\"`\n}\n\ntype BraveConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tAPIKey string `json:\"api_key\"`\n\tMaxResults int `json:\"max_results\"`\n}\n\ntype DuckDuckGoConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tMaxResults int `json:\"max_results\"`\n}\n\n// SessionsConfig controls session behavior.\n// Matching TS src/config/sessions/types.ts + src/config/types.base.ts.\ntype SessionsConfig struct {\n\tScope string `json:\"scope,omitempty\"` // \"per-sender\" (default), \"global\"\n\tDmScope string `json:\"dm_scope,omitempty\"` // \"main\", \"per-peer\", \"per-channel-peer\" (default), \"per-account-channel-peer\"\n\tMainKey string `json:\"main_key,omitempty\"` // main session key suffix (default \"main\", used when dm_scope=\"main\")\n}\n\n// TtsConfig configures text-to-speech.\n// Matching TS src/config/types.tts.ts.\ntype TtsConfig struct {\n\tProvider string `json:\"provider,omitempty\"` // \"openai\", \"elevenlabs\", \"edge\", \"minimax\"\n\tAuto string `json:\"auto,omitempty\"` // \"off\" (default), \"always\", \"inbound\", \"tagged\"\n\tMode string `json:\"mode,omitempty\"` // \"final\" (default), \"all\"\n\tMaxLength int `json:\"max_length,omitempty\"` // max text length before truncation (default 1500)\n\tTimeoutMs int `json:\"timeout_ms,omitempty\"` // API timeout in ms (default 30000)\n\tOpenAI TtsOpenAIConfig `json:\"openai\"`\n\tElevenLabs TtsElevenLabsConfig `json:\"elevenlabs\"`\n\tEdge TtsEdgeConfig `json:\"edge\"`\n\tMiniMax TtsMiniMaxConfig `json:\"minimax\"`\n}\n\n// TtsOpenAIConfig configures the OpenAI TTS provider.\ntype TtsOpenAIConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"`\n\tAPIBase string `json:\"api_base,omitempty\"` // custom endpoint URL\n\tModel string `json:\"model,omitempty\"` // default \"gpt-4o-mini-tts\"\n\tVoice string `json:\"voice,omitempty\"` // default \"alloy\"\n}\n\n// TtsElevenLabsConfig configures the ElevenLabs TTS provider.\ntype TtsElevenLabsConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"`\n\tBaseURL string `json:\"base_url,omitempty\"`\n\tVoiceID string `json:\"voice_id,omitempty\"` // default \"pMsXgVXv3BLzUgSXRplE\"\n\tModelID string `json:\"model_id,omitempty\"` // default \"eleven_multilingual_v2\"\n}\n\n// TtsEdgeConfig configures the Microsoft Edge TTS provider (free, no API key).\ntype TtsEdgeConfig struct {\n\tEnabled bool `json:\"enabled,omitempty\"`\n\tVoice string `json:\"voice,omitempty\"` // default \"en-US-MichelleNeural\"\n\tRate string `json:\"rate,omitempty\"` // speech rate, e.g. \"+0%\"\n}\n\n// TtsMiniMaxConfig configures the MiniMax TTS provider.\ntype TtsMiniMaxConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"`\n\tGroupID string `json:\"group_id,omitempty\"` // MiniMax GroupId (required)\n\tAPIBase string `json:\"api_base,omitempty\"` // default \"https://api.minimax.io/v1\"\n\tModel string `json:\"model,omitempty\"` // default \"speech-02-hd\"\n\tVoiceID string `json:\"voice_id,omitempty\"` // default \"Wise_Woman\"\n}\n\n// MergeChannelGroupQuotas merges per-group quota overrides from channel configs\n// (e.g., channels.telegram.groups[chatID].quota) into gateway.quota.groups.\n// This allows per-group quotas to be set at the channel level and picked up\n// by the QuotaChecker at runtime.\nfunc MergeChannelGroupQuotas(cfg *Config) {\n\tif cfg.Gateway.Quota == nil {\n\t\treturn\n\t}\n\tif cfg.Gateway.Quota.Groups == nil {\n\t\tcfg.Gateway.Quota.Groups = make(map[string]QuotaWindow)\n\t}\n\n\t// Telegram per-group quotas\n\tfor chatID, groupCfg := range cfg.Channels.Telegram.Groups {\n\t\tif groupCfg != nil && groupCfg.Quota != nil && !groupCfg.Quota.IsZero() {\n\t\t\tkey := \"group:telegram:\" + chatID\n\t\t\tcfg.Gateway.Quota.Groups[key] = *groupCfg.Quota\n\t\t}\n\t}\n}\n"}, "651072a9eae5a2f8db7fee61094b0934974877ca": {"t": 1785759260.3667612, "n": 30349, "h": "36f2cfa585d0ec7e", "c": "package config\n\n// PendingCompactionConfig configures LLM-based compaction of pending group messages.\n// When a group accumulates more than Threshold pending messages, older messages are\n// summarized by an LLM and replaced with a compact summary, keeping KeepRecent raw messages.\ntype PendingCompactionConfig struct {\n\tThreshold int `json:\"threshold,omitempty\"` // trigger compaction when entries exceed this (default 200)\n\tKeepRecent int `json:\"keep_recent,omitempty\"` // keep this many recent raw messages after compaction (default 40)\n\tMaxTokens int `json:\"max_tokens,omitempty\"` // max output tokens for LLM summarization (default 4096)\n\tProvider string `json:\"provider,omitempty\"` // LLM provider name (e.g. \"openai\"); empty = use agent's provider\n\tModel string `json:\"model,omitempty\"` // model for summarization; empty = use agent's model\n}\n\n// ChannelsConfig contains per-channel configuration.\ntype ChannelsConfig struct {\n\tTelegram TelegramConfig `json:\"telegram\"`\n\tDiscord DiscordConfig `json:\"discord\"`\n\tSlack SlackConfig `json:\"slack\"`\n\tWhatsApp WhatsAppConfig `json:\"whatsapp\"`\n\tZalo ZaloConfig `json:\"zalo\"`\n\tZaloPersonal ZaloPersonalConfig `json:\"zalo_personal\"`\n\tFeishu FeishuConfig `json:\"feishu\"`\n\tPendingCompaction *PendingCompactionConfig `json:\"pending_compaction,omitempty\"` // global pending message compaction settings\n}\n\ntype TelegramConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tToken string `json:\"token\"`\n\tProxy string `json:\"proxy,omitempty\"`\n\tAPIServer string `json:\"api_server,omitempty\"` // custom Telegram Bot API server URL (e.g. \"http://localhost:8081\")\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in groups (default true)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tDMStream *bool `json:\"dm_stream,omitempty\"` // enable streaming for DMs (default false) — edits placeholder progressively\n\tGroupStream *bool `json:\"group_stream,omitempty\"` // enable streaming for groups (default false) — sends new message, edits progressively\n\tDraftTransport *bool `json:\"draft_transport,omitempty\"` // use sendMessageDraft for DM streaming (default true) — stealth preview, no notifications per edit\n\tReasoningStream *bool `json:\"reasoning_stream,omitempty\"` // show reasoning as separate message when provider emits thinking events (default true)\n\tReactionLevel string `json:\"reaction_level,omitempty\"` // \"off\" (default), \"minimal\", \"full\" — status emoji reactions\n\tMediaMaxBytes int64 `json:\"media_max_bytes,omitempty\"` // max media download size in bytes (default 20MB)\n\tLinkPreview *bool `json:\"link_preview,omitempty\"` // enable URL previews in messages (default true)\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tForceIPv4 bool `json:\"force_ipv4,omitempty\"` // force IPv4 for all Telegram API requests (use when IPv6 routing is broken)\n\n\t// Optional STT (Speech-to-Text) pipeline for voice/audio inbound messages.\n\t// When stt_proxy_url is set, audio/voice messages are transcribed before being forwarded to the agent.\n\tSTTProxyURL string `json:\"stt_proxy_url,omitempty\"` // base URL of the STT proxy service (e.g. \"https://stt.example.com\")\n\tSTTAPIKey string `json:\"stt_api_key,omitempty\"` // Bearer token for the STT proxy\n\tSTTTenantID string `json:\"stt_tenant_id,omitempty\"` // optional tenant/org identifier forwarded to the STT proxy\n\tSTTTimeoutSeconds int `json:\"stt_timeout_seconds,omitempty\"` // per-request timeout for STT calls (default 30s)\n\n\t// Optional audio-aware routing: when set, voice/audio inbound messages are routed to this\n\t// agent instead of the default channel agent. Requires the named agent to exist in the config.\n\tVoiceAgentID string `json:\"voice_agent_id,omitempty\"` // agent ID to route voice inbound to (e.g. \"speaking-agent\")\n\n\t// Audio guard: intercept technical errors in voice agent replies and replace with friendly fallbacks.\n\t// Only active when VoiceAgentID is set. Custom error markers replace built-in defaults when provided.\n\tAudioGuardFallbackTranscript string `json:\"audio_guard_fallback_transcript,omitempty\"` // fallback with %s for transcript (e.g. \"I heard: \\\"%s\\\". Try again!\")\n\tAudioGuardFallbackNoTranscript string `json:\"audio_guard_fallback_no_transcript,omitempty\"` // fallback when no transcript available\n\tAudioGuardErrorMarkers []string `json:\"audio_guard_error_markers,omitempty\"` // custom error detection markers (replaces defaults)\n\n\t// Per-group (and per-topic) overrides. Key is chat ID string (e.g. \"-100123456\") or \"*\" for wildcard.\n\t// TS ref: channels.telegram.groups in src/config/types.telegram.ts.\n\tGroups map[string]*TelegramGroupConfig `json:\"groups,omitempty\"`\n}\n\n// TelegramGroupConfig defines per-group overrides for a Telegram channel.\n// Matching TS TelegramGroupConfig in src/config/types.telegram.ts.\ntype TelegramGroupConfig struct {\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // override group policy for this group\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // override require_mention for this group\n\tAllowFrom FlexibleStringSlice `json:\"allow_from,omitempty\"` // override allow_from for this group\n\tEnabled *bool `json:\"enabled,omitempty\"` // disable bot for this group (default: true)\n\tSkills []string `json:\"skills,omitempty\"` // skill whitelist (nil = all, [] = none)\n\tTools []string `json:\"tools,omitempty\"` // tool allow list (nil = all, supports \"group:xxx\")\n\tSystemPrompt string `json:\"system_prompt,omitempty\"` // extra system prompt for this group\n\tTopics map[string]*TelegramTopicConfig `json:\"topics,omitempty\"` // per-topic overrides (key: thread ID string)\n\tQuota *QuotaWindow `json:\"quota,omitempty\"` // per-group quota override\n}\n\n// TelegramTopicConfig defines per-topic overrides within a Telegram group.\n// Matching TS TelegramTopicConfig in src/config/types.telegram.ts.\ntype TelegramTopicConfig struct {\n\tRequireMention *bool `json:\"require_mention,omitempty\"`\n\tGroupPolicy string `json:\"group_policy,omitempty\"`\n\tSkills []string `json:\"skills,omitempty\"`\n\tTools []string `json:\"tools,omitempty\"` // tool allow list (nil = inherit, supports \"group:xxx\")\n\tEnabled *bool `json:\"enabled,omitempty\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from,omitempty\"`\n\tSystemPrompt string `json:\"system_prompt,omitempty\"`\n}\n\ntype DiscordConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tToken string `json:\"token\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in groups (default true)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tMediaMaxBytes int64 `json:\"media_max_bytes,omitempty\"` // max media download size (default 25MB)\n\tSTTProxyURL string `json:\"stt_proxy_url,omitempty\"`\n\tSTTAPIKey string `json:\"stt_api_key,omitempty\"`\n\tSTTTenantID string `json:\"stt_tenant_id,omitempty\"`\n\tSTTTimeoutSeconds int `json:\"stt_timeout_seconds,omitempty\"`\n\tVoiceAgentID string `json:\"voice_agent_id,omitempty\"`\n}\n\ntype SlackConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tBotToken string `json:\"bot_token\"` // xoxb-... (Bot User OAuth Token)\n\tAppToken string `json:\"app_token\"` // xapp-... (App-Level Token for Socket Mode)\n\tUserToken string `json:\"user_token,omitempty\"` // xoxp-... (Optional: custom bot identity)\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"pairing\", \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in channels (default true)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tDMStream *bool `json:\"dm_stream,omitempty\"` // enable streaming for DMs (default false)\n\tGroupStream *bool `json:\"group_stream,omitempty\"` // enable streaming for groups (default false)\n\tNativeStream *bool `json:\"native_stream,omitempty\"` // use Slack ChatStreamer API if available (default false)\n\tReactionLevel string `json:\"reaction_level,omitempty\"` // \"off\" (default), \"minimal\", \"full\"\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tDebounceDelay int `json:\"debounce_delay,omitempty\"` // ms delay before dispatching rapid messages (default 300, 0=disabled)\n\tThreadTTL *int `json:\"thread_ttl,omitempty\"` // hours before thread participation expires (default 24, 0=disabled — always require @mention)\n\tMediaMaxBytes int64 `json:\"media_max_bytes,omitempty\"` // max file download size in bytes (default 20MB)\n}\n\ntype WhatsAppConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tBridgeURL string `json:\"bridge_url\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n}\n\ntype ZaloConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tToken string `json:\"token\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tWebhookURL string `json:\"webhook_url,omitempty\"`\n\tWebhookSecret string `json:\"webhook_secret,omitempty\"`\n\tMediaMaxMB int `json:\"media_max_mb,omitempty\"` // default 5\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n}\n\ntype ZaloPersonalConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in groups (default true)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tCredentialsPath string `json:\"credentials_path,omitempty\"` // path to saved cookies JSON\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n}\n\ntype FeishuConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tAppID string `json:\"app_id\"`\n\tAppSecret string `json:\"app_secret\"`\n\tEncryptKey string `json:\"encrypt_key,omitempty\"`\n\tVerificationToken string `json:\"verification_token,omitempty\"`\n\tDomain string `json:\"domain,omitempty\"` // \"lark\" (default/global), \"feishu\" (China), or custom URL\n\tConnectionMode string `json:\"connection_mode,omitempty\"` // \"websocket\" (default), \"webhook\"\n\tWebhookPort int `json:\"webhook_port,omitempty\"` // default 3000\n\tWebhookPath string `json:\"webhook_path,omitempty\"` // default \"/feishu/events\"\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default)\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default)\n\tGroupAllowFrom FlexibleStringSlice `json:\"group_allow_from,omitempty\"`\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // default true (groups)\n\tTopicSessionMode string `json:\"topic_session_mode,omitempty\"` // \"disabled\" (default)\n\tTextChunkLimit int `json:\"text_chunk_limit,omitempty\"` // default 4000\n\tMediaMaxMB int `json:\"media_max_mb,omitempty\"` // default 30\n\tRenderMode string `json:\"render_mode,omitempty\"` // \"auto\", \"raw\", \"card\"\n\tStreaming *bool `json:\"streaming,omitempty\"` // default true\n\tReactionLevel string `json:\"reaction_level,omitempty\"` // \"off\" (default), \"minimal\", \"full\" — typing emoji reactions\n\tHistoryLimit int `json:\"history_limit,omitempty\"`\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tSTTProxyURL string `json:\"stt_proxy_url,omitempty\"`\n\tSTTAPIKey string `json:\"stt_api_key,omitempty\"`\n\tSTTTenantID string `json:\"stt_tenant_id,omitempty\"`\n\tSTTTimeoutSeconds int `json:\"stt_timeout_seconds,omitempty\"`\n\tVoiceAgentID string `json:\"voice_agent_id,omitempty\"`\n}\n\n// ProvidersConfig maps provider name to its config.\ntype ProvidersConfig struct {\n\tAnthropic ProviderConfig `json:\"anthropic\"`\n\tOpenAI ProviderConfig `json:\"openai\"`\n\tOpenRouter ProviderConfig `json:\"openrouter\"`\n\tGroq ProviderConfig `json:\"groq\"`\n\tGemini ProviderConfig `json:\"gemini\"`\n\tDeepSeek ProviderConfig `json:\"deepseek\"`\n\tMistral ProviderConfig `json:\"mistral\"`\n\tXAI ProviderConfig `json:\"xai\"`\n\tMiniMax ProviderConfig `json:\"minimax\"`\n\tCohere ProviderConfig `json:\"cohere\"`\n\tPerplexity ProviderConfig `json:\"perplexity\"`\n\tDashScope ProviderConfig `json:\"dashscope\"`\n\tBailian ProviderConfig `json:\"bailian\"`\n\tZai ProviderConfig `json:\"zai\"`\n\tZaiCoding ProviderConfig `json:\"zai_coding\"`\n\tOllama OllamaConfig `json:\"ollama\"` // local Ollama instance (no API key needed)\n\tOllamaCloud ProviderConfig `json:\"ollama_cloud\"` // Ollama Cloud (API key required)\n\tClaudeCLI ClaudeCLIConfig `json:\"claude_cli\"`\n\tACP ACPConfig `json:\"acp\"`\n}\n\n// OllamaConfig configures a local (or self-hosted) Ollama instance.\n// No API key is required — Ollama accepts any Bearer token value.\ntype OllamaConfig struct {\n\tHost string `json:\"host\"` // Ollama server base URL, e.g. http://localhost:11434\n}\n\n// ClaudeCLIConfig configures the Claude CLI provider (uses subscription, not API key).\ntype ClaudeCLIConfig struct {\n\tCLIPath string `json:\"cli_path\" yaml:\"cli_path\"` // path to claude binary (default: \"claude\")\n\tModel string `json:\"model\" yaml:\"model\"` // default model alias (default: \"sonnet\")\n\tBaseWorkDir string `json:\"base_work_dir\" yaml:\"base_work_dir\"` // base dir for agent workspaces\n\tPermMode string `json:\"perm_mode\" yaml:\"perm_mode\"` // permission mode (default: \"bypassPermissions\")\n}\n\n// ACPConfig configures the ACP (Agent Client Protocol) provider.\n// Orchestrates any ACP-compatible coding agent (Claude Code, Codex CLI, Gemini CLI) as a subprocess.\ntype ACPConfig struct {\n\tBinary string `json:\"binary\"` // agent binary name or path (e.g. \"claude\", \"codex\")\n\tArgs []string `json:\"args\"` // extra spawn args\n\tModel string `json:\"model\"` // default model/agent name\n\tWorkDir string `json:\"work_dir\"` // base workspace dir\n\tIdleTTL string `json:\"idle_ttl\"` // process idle TTL (e.g. \"5m\")\n\tPermMode string `json:\"perm_mode\"` // \"approve-all\" (default), \"approve-reads\", \"deny-all\"\n}\n\ntype ProviderConfig struct {\n\tAPIKey string `json:\"api_key\"`\n\tAPIBase string `json:\"api_base,omitempty\"`\n}\n\n// APIBaseForType returns the config-level api_base for a given provider type.\n// Used as a fallback when DB providers have no api_base set.\nfunc (p *ProvidersConfig) APIBaseForType(providerType string) string {\n\tswitch providerType {\n\tcase \"anthropic_native\":\n\t\treturn p.Anthropic.APIBase\n\tcase \"openai\", \"openai_compat\":\n\t\treturn p.OpenAI.APIBase\n\tcase \"openrouter\":\n\t\treturn p.OpenRouter.APIBase\n\tcase \"groq\":\n\t\treturn p.Groq.APIBase\n\tcase \"deepseek\":\n\t\treturn p.DeepSeek.APIBase\n\tcase \"gemini_native\":\n\t\treturn p.Gemini.APIBase\n\tcase \"mistral\":\n\t\treturn p.Mistral.APIBase\n\tcase \"xai\":\n\t\treturn p.XAI.APIBase\n\tcase \"minimax_native\":\n\t\treturn p.MiniMax.APIBase\n\tcase \"cohere\":\n\t\treturn p.Cohere.APIBase\n\tcase \"perplexity\":\n\t\treturn p.Perplexity.APIBase\n\tcase \"dashscope\":\n\t\treturn p.DashScope.APIBase\n\tcase \"bailian\":\n\t\treturn p.Bailian.APIBase\n\tcase \"zai\":\n\t\treturn p.Zai.APIBase\n\tcase \"zai_coding\":\n\t\treturn p.ZaiCoding.APIBase\n\tcase \"ollama_cloud\":\n\t\treturn p.OllamaCloud.APIBase\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\n// HasAnyProvider returns true if at least one provider has an API key or CLI configured.\nfunc (c *Config) HasAnyProvider() bool {\n\tp := c.Providers\n\treturn p.Anthropic.APIKey != \"\" ||\n\t\tp.OpenAI.APIKey != \"\" ||\n\t\tp.OpenRouter.APIKey != \"\" ||\n\t\tp.Groq.APIKey != \"\" ||\n\t\tp.Gemini.APIKey != \"\" ||\n\t\tp.DeepSeek.APIKey != \"\" ||\n\t\tp.Mistral.APIKey != \"\" ||\n\t\tp.XAI.APIKey != \"\" ||\n\t\tp.MiniMax.APIKey != \"\" ||\n\t\tp.Cohere.APIKey != \"\" ||\n\t\tp.Perplexity.APIKey != \"\" ||\n\t\tp.DashScope.APIKey != \"\" ||\n\t\tp.Bailian.APIKey != \"\" ||\n\t\tp.Zai.APIKey != \"\" ||\n\t\tp.ZaiCoding.APIKey != \"\" ||\n\t\tp.Ollama.Host != \"\" ||\n\t\tp.OllamaCloud.APIKey != \"\" ||\n\t\tp.ClaudeCLI.CLIPath != \"\" ||\n\t\tp.ACP.Binary != \"\"\n}\n\n// QuotaWindow defines request limits per time window. Zero means unlimited.\ntype QuotaWindow struct {\n\tHour int `json:\"hour,omitempty\"` // max requests per hour (0 = unlimited)\n\tDay int `json:\"day,omitempty\"` // max requests per day (0 = unlimited)\n\tWeek int `json:\"week,omitempty\"` // max requests per week (0 = unlimited)\n}\n\n// IsZero returns true if no limits are set.\nfunc (w QuotaWindow) IsZero() bool { return w.Hour == 0 && w.Day == 0 && w.Week == 0 }\n\n// QuotaConfig configures per-user/group request quotas.\n// Config merge priority: Groups > Channels > Providers > Default.\ntype QuotaConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tDefault QuotaWindow `json:\"default\"`\n\tProviders map[string]QuotaWindow `json:\"providers,omitempty\"` // key = provider name (e.g. \"anthropic\")\n\tChannels map[string]QuotaWindow `json:\"channels,omitempty\"` // key = channel name (e.g. \"telegram\")\n\tGroups map[string]QuotaWindow `json:\"groups,omitempty\"` // key = userID (e.g. \"group:telegram:-100123\")\n}\n\n// GatewayConfig controls the gateway server.\ntype GatewayConfig struct {\n\tHost string `json:\"host\"`\n\tPort int `json:\"port\"`\n\tToken string `json:\"token,omitempty\"` // bearer token for WS/HTTP auth\n\tOwnerIDs []string `json:\"owner_ids,omitempty\"` // sender IDs considered \"owner\"\n\tAllowedOrigins []string `json:\"allowed_origins,omitempty\"` // WebSocket CORS whitelist (empty = allow all)\n\tMaxMessageChars int `json:\"max_message_chars,omitempty\"` // max user message characters (default 32000)\n\tRateLimitRPM int `json:\"rate_limit_rpm,omitempty\"` // rate limit: requests per minute per user (default 20, 0 = disabled)\n\tInjectionAction string `json:\"injection_action,omitempty\"` // prompt injection action: \"log\", \"warn\" (default), \"block\", \"off\"\n\tInboundDebounceMs int `json:\"inbound_debounce_ms,omitempty\"` // merge rapid messages from same sender (default 1000ms, -1 = disabled)\n\tQuota *QuotaConfig `json:\"quota,omitempty\"` // per-user/group request quotas\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // deliver intermediate text during tool iterations (default false)\n\tToolStatus *bool `json:\"tool_status,omitempty\"` // show tool name in streaming preview during tool execution (default true)\n\tTaskRecoveryIntervalSec int `json:\"task_recovery_interval_sec,omitempty\"` // team task recovery ticker interval in seconds (default 300 = 5min)\n}\n\n// ToolsConfig controls tool availability, policy, and web search.\ntype ToolsConfig struct {\n\tProfile string `json:\"profile,omitempty\"` // global profile: \"minimal\", \"coding\", \"messaging\", \"full\"\n\tAllow []string `json:\"allow,omitempty\"` // global allow list (tool names or \"group:xxx\")\n\tDeny []string `json:\"deny,omitempty\"` // global deny list\n\tAlsoAllow []string `json:\"alsoAllow,omitempty\"` // additive: adds without removing existing\n\tByProvider map[string]*ToolPolicySpec `json:\"byProvider,omitempty\"` // per-provider overrides\n\tExecApproval ExecApprovalCfg `json:\"execApproval\"` // exec command approval settings\n\tWebFetch WebFetchPolicyConfig `json:\"web_fetch\"` // domain policy for URL fetching\n\tWeb WebToolsConfig `json:\"web\"`\n\tBrowser BrowserToolConfig `json:\"browser\"`\n\tRateLimitPerHour int `json:\"rate_limit_per_hour,omitempty\"` // max tool executions per hour per session (0 = disabled)\n\tScrubCredentials *bool `json:\"scrub_credentials,omitempty\"` // auto-redact API keys/tokens in tool output (default true)\n\tMcpServers map[string]*MCPServerConfig `json:\"mcp_servers,omitempty\"` // external MCP server connections\n}\n\n// MCPServerConfig configures a single external MCP server connection.\ntype MCPServerConfig struct {\n\tTransport string `json:\"transport\"` // \"stdio\", \"sse\", \"streamable-http\"\n\tCommand string `json:\"command,omitempty\"` // stdio: command to spawn\n\tArgs []string `json:\"args,omitempty\"` // stdio: command arguments\n\tEnv map[string]string `json:\"env,omitempty\"` // stdio: extra environment variables\n\tURL string `json:\"url,omitempty\"` // sse/http: server URL\n\tHeaders map[string]string `json:\"headers,omitempty\"` // sse/http: extra HTTP headers\n\tEnabled *bool `json:\"enabled,omitempty\"` // default true\n\tToolPrefix string `json:\"tool_prefix,omitempty\"` // prefix for tool names (avoids collisions)\n\tTimeoutSec int `json:\"timeout_sec,omitempty\"` // per-tool-call timeout in seconds (default 60)\n}\n\n// IsEnabled returns whether this MCP server is enabled (default true).\nfunc (c *MCPServerConfig) IsEnabled() bool {\n\treturn c.Enabled == nil || *c.Enabled\n}\n\n// ExecApprovalCfg configures command execution approval (matching TS exec-approval.ts).\ntype ExecApprovalCfg struct {\n\tSecurity string `json:\"security,omitempty\"` // \"deny\", \"allowlist\", \"full\" (default \"full\")\n\tAsk string `json:\"ask,omitempty\"` // \"off\", \"on-miss\", \"always\" (default \"off\")\n\tAllowlist []string `json:\"allowlist,omitempty\"` // glob patterns for allowed commands\n}\n\n// WebFetchPolicyConfig controls domain filtering for the web_fetch tool.\ntype WebFetchPolicyConfig struct {\n\tPolicy string `json:\"policy,omitempty\"` // \"allow_all\" (default), \"allowlist\"\n\tAllowedDomains []string `json:\"allowed_domains,omitempty\"` // e.g. [\"github.com\", \"*.example.com\"]\n\tBlockedDomains []string `json:\"blocked_domains,omitempty\"` // always checked regardless of policy\n}\n\n// BrowserToolConfig controls the browser automation tool.\ntype BrowserToolConfig struct {\n\tEnabled bool `json:\"enabled\"` // enable the browser tool (default false)\n\tHeadless bool `json:\"headless,omitempty\"` // run Chrome in headless mode (ignored when RemoteURL is set)\n\tRemoteURL string `json:\"remote_url,omitempty\"` // CDP endpoint for remote Chrome sidecar, e.g. \"ws://chrome:9222\"\n}\n\n// ToolPolicySpec defines a tool policy at any level (global, per-agent, per-provider).\ntype ToolPolicySpec struct {\n\tProfile string `json:\"profile,omitempty\"`\n\tAllow []string `json:\"allow,omitempty\"`\n\tDeny []string `json:\"deny,omitempty\"`\n\tAlsoAllow []string `json:\"alsoAllow,omitempty\"`\n\tByProvider map[string]*ToolPolicySpec `json:\"byProvider,omitempty\"`\n\tToolCallPrefix string `json:\"toolCallPrefix,omitempty\"` // prefix to strip from model's tool call names before registry lookup\n}\n\ntype WebToolsConfig struct {\n\tBrave BraveConfig `json:\"brave\"`\n\tDuckDuckGo DuckDuckGoConfig `json:\"duckduckgo\"`\n}\n\ntype BraveConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tAPIKey string `json:\"api_key\"`\n\tMaxResults int `json:\"max_results\"`\n}\n\ntype DuckDuckGoConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tMaxResults int `json:\"max_results\"`\n}\n\n// SessionsConfig controls session behavior.\n// Matching TS src/config/sessions/types.ts + src/config/types.base.ts.\ntype SessionsConfig struct {\n\tScope string `json:\"scope,omitempty\"` // \"per-sender\" (default), \"global\"\n\tDmScope string `json:\"dm_scope,omitempty\"` // \"main\", \"per-peer\", \"per-channel-peer\" (default), \"per-account-channel-peer\"\n\tMainKey string `json:\"main_key,omitempty\"` // main session key suffix (default \"main\", used when dm_scope=\"main\")\n}\n\n// TtsConfig configures text-to-speech.\n// Matching TS src/config/types.tts.ts.\ntype TtsConfig struct {\n\tProvider string `json:\"provider,omitempty\"` // \"openai\", \"elevenlabs\", \"edge\", \"minimax\"\n\tAuto string `json:\"auto,omitempty\"` // \"off\" (default), \"always\", \"inbound\", \"tagged\"\n\tMode string `json:\"mode,omitempty\"` // \"final\" (default), \"all\"\n\tMaxLength int `json:\"max_length,omitempty\"` // max text length before truncation (default 1500)\n\tTimeoutMs int `json:\"timeout_ms,omitempty\"` // API timeout in ms (default 30000)\n\tOpenAI TtsOpenAIConfig `json:\"openai\"`\n\tElevenLabs TtsElevenLabsConfig `json:\"elevenlabs\"`\n\tEdge TtsEdgeConfig `json:\"edge\"`\n\tMiniMax TtsMiniMaxConfig `json:\"minimax\"`\n}\n\n// TtsOpenAIConfig configures the OpenAI TTS provider.\ntype TtsOpenAIConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"`\n\tAPIBase string `json:\"api_base,omitempty\"` // custom endpoint URL\n\tModel string `json:\"model,omitempty\"` // default \"gpt-4o-mini-tts\"\n\tVoice string `json:\"voice,omitempty\"` // default \"alloy\"\n}\n\n// TtsElevenLabsConfig configures the ElevenLabs TTS provider.\ntype TtsElevenLabsConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"`\n\tBaseURL string `json:\"base_url,omitempty\"`\n\tVoiceID string `json:\"voice_id,omitempty\"` // default \"pMsXgVXv3BLzUgSXRplE\"\n\tModelID string `json:\"model_id,omitempty\"` // default \"eleven_multilingual_v2\"\n}\n\n// TtsEdgeConfig configures the Microsoft Edge TTS provider (free, no API key).\ntype TtsEdgeConfig struct {\n\tEnabled bool `json:\"enabled,omitempty\"`\n\tVoice string `json:\"voice,omitempty\"` // default \"en-US-MichelleNeural\"\n\tRate string `json:\"rate,omitempty\"` // speech rate, e.g. \"+0%\"\n}\n\n// TtsMiniMaxConfig configures the MiniMax TTS provider.\ntype TtsMiniMaxConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"`\n\tGroupID string `json:\"group_id,omitempty\"` // MiniMax GroupId (required)\n\tAPIBase string `json:\"api_base,omitempty\"` // default \"https://api.minimax.io/v1\"\n\tModel string `json:\"model,omitempty\"` // default \"speech-02-hd\"\n\tVoiceID string `json:\"voice_id,omitempty\"` // default \"Wise_Woman\"\n}\n\n// MergeChannelGroupQuotas merges per-group quota overrides from channel configs\n// (e.g., channels.telegram.groups[chatID].quota) into gateway.quota.groups.\n// This allows per-group quotas to be set at the channel level and picked up\n// by the QuotaChecker at runtime.\nfunc MergeChannelGroupQuotas(cfg *Config) {\n\tif cfg.Gateway.Quota == nil {\n\t\treturn\n\t}\n\tif cfg.Gateway.Quota.Groups == nil {\n\t\tcfg.Gateway.Quota.Groups = make(map[string]QuotaWindow)\n\t}\n\n\t// Telegram per-group quotas\n\tfor chatID, groupCfg := range cfg.Channels.Telegram.Groups {\n\t\tif groupCfg != nil && groupCfg.Quota != nil && !groupCfg.Quota.IsZero() {\n\t\t\tkey := \"group:telegram:\" + chatID\n\t\t\tcfg.Gateway.Quota.Groups[key] = *groupCfg.Quota\n\t\t}\n\t}\n}\n"}, "ffb536861ff88f9743ab85c66288a26f0d09917d": {"t": 1785759262.4803092, "n": 30348, "h": "601e3639bb908518", "c": "package config\n\n// PendingCompactionConfig configures LLM-based compaction of pending group messages.\n// When a group accumulates more than Threshold pending messages, older messages are\n// summarized by an LLM and replaced with a compact summary, keeping KeepRecent raw messages.\ntype PendingCompactionConfig struct {\n\tThreshold int `json:\"threshold,omitempty\"` // trigger compaction when entries exceed this (default 50)\n\tKeepRecent int `json:\"keep_recent,omitempty\"` // keep this many recent raw messages after compaction (default 15)\n\tMaxTokens int `json:\"max_tokens,omitempty\"` // max output tokens for LLM summarization (default 4096)\n\tProvider string `json:\"provider,omitempty\"` // LLM provider name (e.g. \"openai\"); empty = use agent's provider\n\tModel string `json:\"model,omitempty\"` // model for summarization; empty = use agent's model\n}\n\n// ChannelsConfig contains per-channel configuration.\ntype ChannelsConfig struct {\n\tTelegram TelegramConfig `json:\"telegram\"`\n\tDiscord DiscordConfig `json:\"discord\"`\n\tSlack SlackConfig `json:\"slack\"`\n\tWhatsApp WhatsAppConfig `json:\"whatsapp\"`\n\tZalo ZaloConfig `json:\"zalo\"`\n\tZaloPersonal ZaloPersonalConfig `json:\"zalo_personal\"`\n\tFeishu FeishuConfig `json:\"feishu\"`\n\tPendingCompaction *PendingCompactionConfig `json:\"pending_compaction,omitempty\"` // global pending message compaction settings\n}\n\ntype TelegramConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tToken string `json:\"token\"`\n\tProxy string `json:\"proxy,omitempty\"`\n\tAPIServer string `json:\"api_server,omitempty\"` // custom Telegram Bot API server URL (e.g. \"http://localhost:8081\")\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in groups (default true)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tDMStream *bool `json:\"dm_stream,omitempty\"` // enable streaming for DMs (default false) — edits placeholder progressively\n\tGroupStream *bool `json:\"group_stream,omitempty\"` // enable streaming for groups (default false) — sends new message, edits progressively\n\tDraftTransport *bool `json:\"draft_transport,omitempty\"` // use sendMessageDraft for DM streaming (default true) — stealth preview, no notifications per edit\n\tReasoningStream *bool `json:\"reasoning_stream,omitempty\"` // show reasoning as separate message when provider emits thinking events (default true)\n\tReactionLevel string `json:\"reaction_level,omitempty\"` // \"off\" (default), \"minimal\", \"full\" — status emoji reactions\n\tMediaMaxBytes int64 `json:\"media_max_bytes,omitempty\"` // max media download size in bytes (default 20MB)\n\tLinkPreview *bool `json:\"link_preview,omitempty\"` // enable URL previews in messages (default true)\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tForceIPv4 bool `json:\"force_ipv4,omitempty\"` // force IPv4 for all Telegram API requests (use when IPv6 routing is broken)\n\n\t// Optional STT (Speech-to-Text) pipeline for voice/audio inbound messages.\n\t// When stt_proxy_url is set, audio/voice messages are transcribed before being forwarded to the agent.\n\tSTTProxyURL string `json:\"stt_proxy_url,omitempty\"` // base URL of the STT proxy service (e.g. \"https://stt.example.com\")\n\tSTTAPIKey string `json:\"stt_api_key,omitempty\"` // Bearer token for the STT proxy\n\tSTTTenantID string `json:\"stt_tenant_id,omitempty\"` // optional tenant/org identifier forwarded to the STT proxy\n\tSTTTimeoutSeconds int `json:\"stt_timeout_seconds,omitempty\"` // per-request timeout for STT calls (default 30s)\n\n\t// Optional audio-aware routing: when set, voice/audio inbound messages are routed to this\n\t// agent instead of the default channel agent. Requires the named agent to exist in the config.\n\tVoiceAgentID string `json:\"voice_agent_id,omitempty\"` // agent ID to route voice inbound to (e.g. \"speaking-agent\")\n\n\t// Audio guard: intercept technical errors in voice agent replies and replace with friendly fallbacks.\n\t// Only active when VoiceAgentID is set. Custom error markers replace built-in defaults when provided.\n\tAudioGuardFallbackTranscript string `json:\"audio_guard_fallback_transcript,omitempty\"` // fallback with %s for transcript (e.g. \"I heard: \\\"%s\\\". Try again!\")\n\tAudioGuardFallbackNoTranscript string `json:\"audio_guard_fallback_no_transcript,omitempty\"` // fallback when no transcript available\n\tAudioGuardErrorMarkers []string `json:\"audio_guard_error_markers,omitempty\"` // custom error detection markers (replaces defaults)\n\n\t// Per-group (and per-topic) overrides. Key is chat ID string (e.g. \"-100123456\") or \"*\" for wildcard.\n\t// TS ref: channels.telegram.groups in src/config/types.telegram.ts.\n\tGroups map[string]*TelegramGroupConfig `json:\"groups,omitempty\"`\n}\n\n// TelegramGroupConfig defines per-group overrides for a Telegram channel.\n// Matching TS TelegramGroupConfig in src/config/types.telegram.ts.\ntype TelegramGroupConfig struct {\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // override group policy for this group\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // override require_mention for this group\n\tAllowFrom FlexibleStringSlice `json:\"allow_from,omitempty\"` // override allow_from for this group\n\tEnabled *bool `json:\"enabled,omitempty\"` // disable bot for this group (default: true)\n\tSkills []string `json:\"skills,omitempty\"` // skill whitelist (nil = all, [] = none)\n\tTools []string `json:\"tools,omitempty\"` // tool allow list (nil = all, supports \"group:xxx\")\n\tSystemPrompt string `json:\"system_prompt,omitempty\"` // extra system prompt for this group\n\tTopics map[string]*TelegramTopicConfig `json:\"topics,omitempty\"` // per-topic overrides (key: thread ID string)\n\tQuota *QuotaWindow `json:\"quota,omitempty\"` // per-group quota override\n}\n\n// TelegramTopicConfig defines per-topic overrides within a Telegram group.\n// Matching TS TelegramTopicConfig in src/config/types.telegram.ts.\ntype TelegramTopicConfig struct {\n\tRequireMention *bool `json:\"require_mention,omitempty\"`\n\tGroupPolicy string `json:\"group_policy,omitempty\"`\n\tSkills []string `json:\"skills,omitempty\"`\n\tTools []string `json:\"tools,omitempty\"` // tool allow list (nil = inherit, supports \"group:xxx\")\n\tEnabled *bool `json:\"enabled,omitempty\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from,omitempty\"`\n\tSystemPrompt string `json:\"system_prompt,omitempty\"`\n}\n\ntype DiscordConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tToken string `json:\"token\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in groups (default true)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tMediaMaxBytes int64 `json:\"media_max_bytes,omitempty\"` // max media download size (default 25MB)\n\tSTTProxyURL string `json:\"stt_proxy_url,omitempty\"`\n\tSTTAPIKey string `json:\"stt_api_key,omitempty\"`\n\tSTTTenantID string `json:\"stt_tenant_id,omitempty\"`\n\tSTTTimeoutSeconds int `json:\"stt_timeout_seconds,omitempty\"`\n\tVoiceAgentID string `json:\"voice_agent_id,omitempty\"`\n}\n\ntype SlackConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tBotToken string `json:\"bot_token\"` // xoxb-... (Bot User OAuth Token)\n\tAppToken string `json:\"app_token\"` // xapp-... (App-Level Token for Socket Mode)\n\tUserToken string `json:\"user_token,omitempty\"` // xoxp-... (Optional: custom bot identity)\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"pairing\", \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in channels (default true)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tDMStream *bool `json:\"dm_stream,omitempty\"` // enable streaming for DMs (default false)\n\tGroupStream *bool `json:\"group_stream,omitempty\"` // enable streaming for groups (default false)\n\tNativeStream *bool `json:\"native_stream,omitempty\"` // use Slack ChatStreamer API if available (default false)\n\tReactionLevel string `json:\"reaction_level,omitempty\"` // \"off\" (default), \"minimal\", \"full\"\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tDebounceDelay int `json:\"debounce_delay,omitempty\"` // ms delay before dispatching rapid messages (default 300, 0=disabled)\n\tThreadTTL *int `json:\"thread_ttl,omitempty\"` // hours before thread participation expires (default 24, 0=disabled — always require @mention)\n\tMediaMaxBytes int64 `json:\"media_max_bytes,omitempty\"` // max file download size in bytes (default 20MB)\n}\n\ntype WhatsAppConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tBridgeURL string `json:\"bridge_url\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n}\n\ntype ZaloConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tToken string `json:\"token\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tWebhookURL string `json:\"webhook_url,omitempty\"`\n\tWebhookSecret string `json:\"webhook_secret,omitempty\"`\n\tMediaMaxMB int `json:\"media_max_mb,omitempty\"` // default 5\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n}\n\ntype ZaloPersonalConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in groups (default true)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tCredentialsPath string `json:\"credentials_path,omitempty\"` // path to saved cookies JSON\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n}\n\ntype FeishuConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tAppID string `json:\"app_id\"`\n\tAppSecret string `json:\"app_secret\"`\n\tEncryptKey string `json:\"encrypt_key,omitempty\"`\n\tVerificationToken string `json:\"verification_token,omitempty\"`\n\tDomain string `json:\"domain,omitempty\"` // \"lark\" (default/global), \"feishu\" (China), or custom URL\n\tConnectionMode string `json:\"connection_mode,omitempty\"` // \"websocket\" (default), \"webhook\"\n\tWebhookPort int `json:\"webhook_port,omitempty\"` // default 3000\n\tWebhookPath string `json:\"webhook_path,omitempty\"` // default \"/feishu/events\"\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default)\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default)\n\tGroupAllowFrom FlexibleStringSlice `json:\"group_allow_from,omitempty\"`\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // default true (groups)\n\tTopicSessionMode string `json:\"topic_session_mode,omitempty\"` // \"disabled\" (default)\n\tTextChunkLimit int `json:\"text_chunk_limit,omitempty\"` // default 4000\n\tMediaMaxMB int `json:\"media_max_mb,omitempty\"` // default 30\n\tRenderMode string `json:\"render_mode,omitempty\"` // \"auto\", \"raw\", \"card\"\n\tStreaming *bool `json:\"streaming,omitempty\"` // default true\n\tReactionLevel string `json:\"reaction_level,omitempty\"` // \"off\" (default), \"minimal\", \"full\" — typing emoji reactions\n\tHistoryLimit int `json:\"history_limit,omitempty\"`\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tSTTProxyURL string `json:\"stt_proxy_url,omitempty\"`\n\tSTTAPIKey string `json:\"stt_api_key,omitempty\"`\n\tSTTTenantID string `json:\"stt_tenant_id,omitempty\"`\n\tSTTTimeoutSeconds int `json:\"stt_timeout_seconds,omitempty\"`\n\tVoiceAgentID string `json:\"voice_agent_id,omitempty\"`\n}\n\n// ProvidersConfig maps provider name to its config.\ntype ProvidersConfig struct {\n\tAnthropic ProviderConfig `json:\"anthropic\"`\n\tOpenAI ProviderConfig `json:\"openai\"`\n\tOpenRouter ProviderConfig `json:\"openrouter\"`\n\tGroq ProviderConfig `json:\"groq\"`\n\tGemini ProviderConfig `json:\"gemini\"`\n\tDeepSeek ProviderConfig `json:\"deepseek\"`\n\tMistral ProviderConfig `json:\"mistral\"`\n\tXAI ProviderConfig `json:\"xai\"`\n\tMiniMax ProviderConfig `json:\"minimax\"`\n\tCohere ProviderConfig `json:\"cohere\"`\n\tPerplexity ProviderConfig `json:\"perplexity\"`\n\tDashScope ProviderConfig `json:\"dashscope\"`\n\tBailian ProviderConfig `json:\"bailian\"`\n\tZai ProviderConfig `json:\"zai\"`\n\tZaiCoding ProviderConfig `json:\"zai_coding\"`\n\tOllama OllamaConfig `json:\"ollama\"` // local Ollama instance (no API key needed)\n\tOllamaCloud ProviderConfig `json:\"ollama_cloud\"` // Ollama Cloud (API key required)\n\tClaudeCLI ClaudeCLIConfig `json:\"claude_cli\"`\n\tACP ACPConfig `json:\"acp\"`\n}\n\n// OllamaConfig configures a local (or self-hosted) Ollama instance.\n// No API key is required — Ollama accepts any Bearer token value.\ntype OllamaConfig struct {\n\tHost string `json:\"host\"` // Ollama server base URL, e.g. http://localhost:11434\n}\n\n// ClaudeCLIConfig configures the Claude CLI provider (uses subscription, not API key).\ntype ClaudeCLIConfig struct {\n\tCLIPath string `json:\"cli_path\" yaml:\"cli_path\"` // path to claude binary (default: \"claude\")\n\tModel string `json:\"model\" yaml:\"model\"` // default model alias (default: \"sonnet\")\n\tBaseWorkDir string `json:\"base_work_dir\" yaml:\"base_work_dir\"` // base dir for agent workspaces\n\tPermMode string `json:\"perm_mode\" yaml:\"perm_mode\"` // permission mode (default: \"bypassPermissions\")\n}\n\n// ACPConfig configures the ACP (Agent Client Protocol) provider.\n// Orchestrates any ACP-compatible coding agent (Claude Code, Codex CLI, Gemini CLI) as a subprocess.\ntype ACPConfig struct {\n\tBinary string `json:\"binary\"` // agent binary name or path (e.g. \"claude\", \"codex\")\n\tArgs []string `json:\"args\"` // extra spawn args\n\tModel string `json:\"model\"` // default model/agent name\n\tWorkDir string `json:\"work_dir\"` // base workspace dir\n\tIdleTTL string `json:\"idle_ttl\"` // process idle TTL (e.g. \"5m\")\n\tPermMode string `json:\"perm_mode\"` // \"approve-all\" (default), \"approve-reads\", \"deny-all\"\n}\n\ntype ProviderConfig struct {\n\tAPIKey string `json:\"api_key\"`\n\tAPIBase string `json:\"api_base,omitempty\"`\n}\n\n// APIBaseForType returns the config-level api_base for a given provider type.\n// Used as a fallback when DB providers have no api_base set.\nfunc (p *ProvidersConfig) APIBaseForType(providerType string) string {\n\tswitch providerType {\n\tcase \"anthropic_native\":\n\t\treturn p.Anthropic.APIBase\n\tcase \"openai\", \"openai_compat\":\n\t\treturn p.OpenAI.APIBase\n\tcase \"openrouter\":\n\t\treturn p.OpenRouter.APIBase\n\tcase \"groq\":\n\t\treturn p.Groq.APIBase\n\tcase \"deepseek\":\n\t\treturn p.DeepSeek.APIBase\n\tcase \"gemini_native\":\n\t\treturn p.Gemini.APIBase\n\tcase \"mistral\":\n\t\treturn p.Mistral.APIBase\n\tcase \"xai\":\n\t\treturn p.XAI.APIBase\n\tcase \"minimax_native\":\n\t\treturn p.MiniMax.APIBase\n\tcase \"cohere\":\n\t\treturn p.Cohere.APIBase\n\tcase \"perplexity\":\n\t\treturn p.Perplexity.APIBase\n\tcase \"dashscope\":\n\t\treturn p.DashScope.APIBase\n\tcase \"bailian\":\n\t\treturn p.Bailian.APIBase\n\tcase \"zai\":\n\t\treturn p.Zai.APIBase\n\tcase \"zai_coding\":\n\t\treturn p.ZaiCoding.APIBase\n\tcase \"ollama_cloud\":\n\t\treturn p.OllamaCloud.APIBase\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\n// HasAnyProvider returns true if at least one provider has an API key or CLI configured.\nfunc (c *Config) HasAnyProvider() bool {\n\tp := c.Providers\n\treturn p.Anthropic.APIKey != \"\" ||\n\t\tp.OpenAI.APIKey != \"\" ||\n\t\tp.OpenRouter.APIKey != \"\" ||\n\t\tp.Groq.APIKey != \"\" ||\n\t\tp.Gemini.APIKey != \"\" ||\n\t\tp.DeepSeek.APIKey != \"\" ||\n\t\tp.Mistral.APIKey != \"\" ||\n\t\tp.XAI.APIKey != \"\" ||\n\t\tp.MiniMax.APIKey != \"\" ||\n\t\tp.Cohere.APIKey != \"\" ||\n\t\tp.Perplexity.APIKey != \"\" ||\n\t\tp.DashScope.APIKey != \"\" ||\n\t\tp.Bailian.APIKey != \"\" ||\n\t\tp.Zai.APIKey != \"\" ||\n\t\tp.ZaiCoding.APIKey != \"\" ||\n\t\tp.Ollama.Host != \"\" ||\n\t\tp.OllamaCloud.APIKey != \"\" ||\n\t\tp.ClaudeCLI.CLIPath != \"\" ||\n\t\tp.ACP.Binary != \"\"\n}\n\n// QuotaWindow defines request limits per time window. Zero means unlimited.\ntype QuotaWindow struct {\n\tHour int `json:\"hour,omitempty\"` // max requests per hour (0 = unlimited)\n\tDay int `json:\"day,omitempty\"` // max requests per day (0 = unlimited)\n\tWeek int `json:\"week,omitempty\"` // max requests per week (0 = unlimited)\n}\n\n// IsZero returns true if no limits are set.\nfunc (w QuotaWindow) IsZero() bool { return w.Hour == 0 && w.Day == 0 && w.Week == 0 }\n\n// QuotaConfig configures per-user/group request quotas.\n// Config merge priority: Groups > Channels > Providers > Default.\ntype QuotaConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tDefault QuotaWindow `json:\"default\"`\n\tProviders map[string]QuotaWindow `json:\"providers,omitempty\"` // key = provider name (e.g. \"anthropic\")\n\tChannels map[string]QuotaWindow `json:\"channels,omitempty\"` // key = channel name (e.g. \"telegram\")\n\tGroups map[string]QuotaWindow `json:\"groups,omitempty\"` // key = userID (e.g. \"group:telegram:-100123\")\n}\n\n// GatewayConfig controls the gateway server.\ntype GatewayConfig struct {\n\tHost string `json:\"host\"`\n\tPort int `json:\"port\"`\n\tToken string `json:\"token,omitempty\"` // bearer token for WS/HTTP auth\n\tOwnerIDs []string `json:\"owner_ids,omitempty\"` // sender IDs considered \"owner\"\n\tAllowedOrigins []string `json:\"allowed_origins,omitempty\"` // WebSocket CORS whitelist (empty = allow all)\n\tMaxMessageChars int `json:\"max_message_chars,omitempty\"` // max user message characters (default 32000)\n\tRateLimitRPM int `json:\"rate_limit_rpm,omitempty\"` // rate limit: requests per minute per user (default 20, 0 = disabled)\n\tInjectionAction string `json:\"injection_action,omitempty\"` // prompt injection action: \"log\", \"warn\" (default), \"block\", \"off\"\n\tInboundDebounceMs int `json:\"inbound_debounce_ms,omitempty\"` // merge rapid messages from same sender (default 1000ms, -1 = disabled)\n\tQuota *QuotaConfig `json:\"quota,omitempty\"` // per-user/group request quotas\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // deliver intermediate text during tool iterations (default false)\n\tToolStatus *bool `json:\"tool_status,omitempty\"` // show tool name in streaming preview during tool execution (default true)\n\tTaskRecoveryIntervalSec int `json:\"task_recovery_interval_sec,omitempty\"` // team task recovery ticker interval in seconds (default 300 = 5min)\n}\n\n// ToolsConfig controls tool availability, policy, and web search.\ntype ToolsConfig struct {\n\tProfile string `json:\"profile,omitempty\"` // global profile: \"minimal\", \"coding\", \"messaging\", \"full\"\n\tAllow []string `json:\"allow,omitempty\"` // global allow list (tool names or \"group:xxx\")\n\tDeny []string `json:\"deny,omitempty\"` // global deny list\n\tAlsoAllow []string `json:\"alsoAllow,omitempty\"` // additive: adds without removing existing\n\tByProvider map[string]*ToolPolicySpec `json:\"byProvider,omitempty\"` // per-provider overrides\n\tExecApproval ExecApprovalCfg `json:\"execApproval\"` // exec command approval settings\n\tWebFetch WebFetchPolicyConfig `json:\"web_fetch\"` // domain policy for URL fetching\n\tWeb WebToolsConfig `json:\"web\"`\n\tBrowser BrowserToolConfig `json:\"browser\"`\n\tRateLimitPerHour int `json:\"rate_limit_per_hour,omitempty\"` // max tool executions per hour per session (0 = disabled)\n\tScrubCredentials *bool `json:\"scrub_credentials,omitempty\"` // auto-redact API keys/tokens in tool output (default true)\n\tMcpServers map[string]*MCPServerConfig `json:\"mcp_servers,omitempty\"` // external MCP server connections\n}\n\n// MCPServerConfig configures a single external MCP server connection.\ntype MCPServerConfig struct {\n\tTransport string `json:\"transport\"` // \"stdio\", \"sse\", \"streamable-http\"\n\tCommand string `json:\"command,omitempty\"` // stdio: command to spawn\n\tArgs []string `json:\"args,omitempty\"` // stdio: command arguments\n\tEnv map[string]string `json:\"env,omitempty\"` // stdio: extra environment variables\n\tURL string `json:\"url,omitempty\"` // sse/http: server URL\n\tHeaders map[string]string `json:\"headers,omitempty\"` // sse/http: extra HTTP headers\n\tEnabled *bool `json:\"enabled,omitempty\"` // default true\n\tToolPrefix string `json:\"tool_prefix,omitempty\"` // prefix for tool names (avoids collisions)\n\tTimeoutSec int `json:\"timeout_sec,omitempty\"` // per-tool-call timeout in seconds (default 60)\n}\n\n// IsEnabled returns whether this MCP server is enabled (default true).\nfunc (c *MCPServerConfig) IsEnabled() bool {\n\treturn c.Enabled == nil || *c.Enabled\n}\n\n// ExecApprovalCfg configures command execution approval (matching TS exec-approval.ts).\ntype ExecApprovalCfg struct {\n\tSecurity string `json:\"security,omitempty\"` // \"deny\", \"allowlist\", \"full\" (default \"full\")\n\tAsk string `json:\"ask,omitempty\"` // \"off\", \"on-miss\", \"always\" (default \"off\")\n\tAllowlist []string `json:\"allowlist,omitempty\"` // glob patterns for allowed commands\n}\n\n// WebFetchPolicyConfig controls domain filtering for the web_fetch tool.\ntype WebFetchPolicyConfig struct {\n\tPolicy string `json:\"policy,omitempty\"` // \"allow_all\" (default), \"allowlist\"\n\tAllowedDomains []string `json:\"allowed_domains,omitempty\"` // e.g. [\"github.com\", \"*.example.com\"]\n\tBlockedDomains []string `json:\"blocked_domains,omitempty\"` // always checked regardless of policy\n}\n\n// BrowserToolConfig controls the browser automation tool.\ntype BrowserToolConfig struct {\n\tEnabled bool `json:\"enabled\"` // enable the browser tool (default false)\n\tHeadless bool `json:\"headless,omitempty\"` // run Chrome in headless mode (ignored when RemoteURL is set)\n\tRemoteURL string `json:\"remote_url,omitempty\"` // CDP endpoint for remote Chrome sidecar, e.g. \"ws://chrome:9222\"\n}\n\n// ToolPolicySpec defines a tool policy at any level (global, per-agent, per-provider).\ntype ToolPolicySpec struct {\n\tProfile string `json:\"profile,omitempty\"`\n\tAllow []string `json:\"allow,omitempty\"`\n\tDeny []string `json:\"deny,omitempty\"`\n\tAlsoAllow []string `json:\"alsoAllow,omitempty\"`\n\tByProvider map[string]*ToolPolicySpec `json:\"byProvider,omitempty\"`\n\tToolCallPrefix string `json:\"toolCallPrefix,omitempty\"` // prefix to strip from model's tool call names before registry lookup\n}\n\ntype WebToolsConfig struct {\n\tBrave BraveConfig `json:\"brave\"`\n\tDuckDuckGo DuckDuckGoConfig `json:\"duckduckgo\"`\n}\n\ntype BraveConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tAPIKey string `json:\"api_key\"`\n\tMaxResults int `json:\"max_results\"`\n}\n\ntype DuckDuckGoConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tMaxResults int `json:\"max_results\"`\n}\n\n// SessionsConfig controls session behavior.\n// Matching TS src/config/sessions/types.ts + src/config/types.base.ts.\ntype SessionsConfig struct {\n\tScope string `json:\"scope,omitempty\"` // \"per-sender\" (default), \"global\"\n\tDmScope string `json:\"dm_scope,omitempty\"` // \"main\", \"per-peer\", \"per-channel-peer\" (default), \"per-account-channel-peer\"\n\tMainKey string `json:\"main_key,omitempty\"` // main session key suffix (default \"main\", used when dm_scope=\"main\")\n}\n\n// TtsConfig configures text-to-speech.\n// Matching TS src/config/types.tts.ts.\ntype TtsConfig struct {\n\tProvider string `json:\"provider,omitempty\"` // \"openai\", \"elevenlabs\", \"edge\", \"minimax\"\n\tAuto string `json:\"auto,omitempty\"` // \"off\" (default), \"always\", \"inbound\", \"tagged\"\n\tMode string `json:\"mode,omitempty\"` // \"final\" (default), \"all\"\n\tMaxLength int `json:\"max_length,omitempty\"` // max text length before truncation (default 1500)\n\tTimeoutMs int `json:\"timeout_ms,omitempty\"` // API timeout in ms (default 30000)\n\tOpenAI TtsOpenAIConfig `json:\"openai\"`\n\tElevenLabs TtsElevenLabsConfig `json:\"elevenlabs\"`\n\tEdge TtsEdgeConfig `json:\"edge\"`\n\tMiniMax TtsMiniMaxConfig `json:\"minimax\"`\n}\n\n// TtsOpenAIConfig configures the OpenAI TTS provider.\ntype TtsOpenAIConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"`\n\tAPIBase string `json:\"api_base,omitempty\"` // custom endpoint URL\n\tModel string `json:\"model,omitempty\"` // default \"gpt-4o-mini-tts\"\n\tVoice string `json:\"voice,omitempty\"` // default \"alloy\"\n}\n\n// TtsElevenLabsConfig configures the ElevenLabs TTS provider.\ntype TtsElevenLabsConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"`\n\tBaseURL string `json:\"base_url,omitempty\"`\n\tVoiceID string `json:\"voice_id,omitempty\"` // default \"pMsXgVXv3BLzUgSXRplE\"\n\tModelID string `json:\"model_id,omitempty\"` // default \"eleven_multilingual_v2\"\n}\n\n// TtsEdgeConfig configures the Microsoft Edge TTS provider (free, no API key).\ntype TtsEdgeConfig struct {\n\tEnabled bool `json:\"enabled,omitempty\"`\n\tVoice string `json:\"voice,omitempty\"` // default \"en-US-MichelleNeural\"\n\tRate string `json:\"rate,omitempty\"` // speech rate, e.g. \"+0%\"\n}\n\n// TtsMiniMaxConfig configures the MiniMax TTS provider.\ntype TtsMiniMaxConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"`\n\tGroupID string `json:\"group_id,omitempty\"` // MiniMax GroupId (required)\n\tAPIBase string `json:\"api_base,omitempty\"` // default \"https://api.minimax.io/v1\"\n\tModel string `json:\"model,omitempty\"` // default \"speech-02-hd\"\n\tVoiceID string `json:\"voice_id,omitempty\"` // default \"Wise_Woman\"\n}\n\n// MergeChannelGroupQuotas merges per-group quota overrides from channel configs\n// (e.g., channels.telegram.groups[chatID].quota) into gateway.quota.groups.\n// This allows per-group quotas to be set at the channel level and picked up\n// by the QuotaChecker at runtime.\nfunc MergeChannelGroupQuotas(cfg *Config) {\n\tif cfg.Gateway.Quota == nil {\n\t\treturn\n\t}\n\tif cfg.Gateway.Quota.Groups == nil {\n\t\tcfg.Gateway.Quota.Groups = make(map[string]QuotaWindow)\n\t}\n\n\t// Telegram per-group quotas\n\tfor chatID, groupCfg := range cfg.Channels.Telegram.Groups {\n\t\tif groupCfg != nil && groupCfg.Quota != nil && !groupCfg.Quota.IsZero() {\n\t\t\tkey := \"group:telegram:\" + chatID\n\t\t\tcfg.Gateway.Quota.Groups[key] = *groupCfg.Quota\n\t\t}\n\t}\n}\n"}, "9f80842bbc01bc3efbe4aa3debaf294cbc84db5e": {"t": 1785759264.0842977, "n": 30220, "h": "e66277e363e750bc", "c": "package config\n\n// PendingCompactionConfig configures LLM-based compaction of pending group messages.\n// When a group accumulates more than Threshold pending messages, older messages are\n// summarized by an LLM and replaced with a compact summary, keeping KeepRecent raw messages.\ntype PendingCompactionConfig struct {\n\tThreshold int `json:\"threshold,omitempty\"` // trigger compaction when entries exceed this (default 50)\n\tKeepRecent int `json:\"keep_recent,omitempty\"` // keep this many recent raw messages after compaction (default 15)\n\tMaxTokens int `json:\"max_tokens,omitempty\"` // max output tokens for LLM summarization (default 4096)\n\tProvider string `json:\"provider,omitempty\"` // LLM provider name (e.g. \"openai\"); empty = use agent's provider\n\tModel string `json:\"model,omitempty\"` // model for summarization; empty = use agent's model\n}\n\n// ChannelsConfig contains per-channel configuration.\ntype ChannelsConfig struct {\n\tTelegram TelegramConfig `json:\"telegram\"`\n\tDiscord DiscordConfig `json:\"discord\"`\n\tSlack SlackConfig `json:\"slack\"`\n\tWhatsApp WhatsAppConfig `json:\"whatsapp\"`\n\tZalo ZaloConfig `json:\"zalo\"`\n\tZaloPersonal ZaloPersonalConfig `json:\"zalo_personal\"`\n\tFeishu FeishuConfig `json:\"feishu\"`\n\tPendingCompaction *PendingCompactionConfig `json:\"pending_compaction,omitempty\"` // global pending message compaction settings\n}\n\ntype TelegramConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tToken string `json:\"token\"`\n\tProxy string `json:\"proxy,omitempty\"`\n\tAPIServer string `json:\"api_server,omitempty\"` // custom Telegram Bot API server URL (e.g. \"http://localhost:8081\")\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in groups (default true)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tDMStream *bool `json:\"dm_stream,omitempty\"` // enable streaming for DMs (default false) — edits placeholder progressively\n\tGroupStream *bool `json:\"group_stream,omitempty\"` // enable streaming for groups (default false) — sends new message, edits progressively\n\tDraftTransport *bool `json:\"draft_transport,omitempty\"` // use sendMessageDraft for DM streaming (default true) — stealth preview, no notifications per edit\n\tReasoningStream *bool `json:\"reasoning_stream,omitempty\"` // show reasoning as separate message when provider emits thinking events (default true)\n\tReactionLevel string `json:\"reaction_level,omitempty\"` // \"off\" (default), \"minimal\", \"full\" — status emoji reactions\n\tMediaMaxBytes int64 `json:\"media_max_bytes,omitempty\"` // max media download size in bytes (default 20MB)\n\tLinkPreview *bool `json:\"link_preview,omitempty\"` // enable URL previews in messages (default true)\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tForceIPv4 bool `json:\"force_ipv4,omitempty\"` // force IPv4 for all Telegram API requests (use when IPv6 routing is broken)\n\n\t// Optional STT (Speech-to-Text) pipeline for voice/audio inbound messages.\n\t// When stt_proxy_url is set, audio/voice messages are transcribed before being forwarded to the agent.\n\tSTTProxyURL string `json:\"stt_proxy_url,omitempty\"` // base URL of the STT proxy service (e.g. \"https://stt.example.com\")\n\tSTTAPIKey string `json:\"stt_api_key,omitempty\"` // Bearer token for the STT proxy\n\tSTTTenantID string `json:\"stt_tenant_id,omitempty\"` // optional tenant/org identifier forwarded to the STT proxy\n\tSTTTimeoutSeconds int `json:\"stt_timeout_seconds,omitempty\"` // per-request timeout for STT calls (default 30s)\n\n\t// Optional audio-aware routing: when set, voice/audio inbound messages are routed to this\n\t// agent instead of the default channel agent. Requires the named agent to exist in the config.\n\tVoiceAgentID string `json:\"voice_agent_id,omitempty\"` // agent ID to route voice inbound to (e.g. \"speaking-agent\")\n\n\t// Audio guard: intercept technical errors in voice agent replies and replace with friendly fallbacks.\n\t// Only active when VoiceAgentID is set. Custom error markers replace built-in defaults when provided.\n\tAudioGuardFallbackTranscript string `json:\"audio_guard_fallback_transcript,omitempty\"` // fallback with %s for transcript (e.g. \"I heard: \\\"%s\\\". Try again!\")\n\tAudioGuardFallbackNoTranscript string `json:\"audio_guard_fallback_no_transcript,omitempty\"` // fallback when no transcript available\n\tAudioGuardErrorMarkers []string `json:\"audio_guard_error_markers,omitempty\"` // custom error detection markers (replaces defaults)\n\n\t// Per-group (and per-topic) overrides. Key is chat ID string (e.g. \"-100123456\") or \"*\" for wildcard.\n\t// TS ref: channels.telegram.groups in src/config/types.telegram.ts.\n\tGroups map[string]*TelegramGroupConfig `json:\"groups,omitempty\"`\n}\n\n// TelegramGroupConfig defines per-group overrides for a Telegram channel.\n// Matching TS TelegramGroupConfig in src/config/types.telegram.ts.\ntype TelegramGroupConfig struct {\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // override group policy for this group\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // override require_mention for this group\n\tAllowFrom FlexibleStringSlice `json:\"allow_from,omitempty\"` // override allow_from for this group\n\tEnabled *bool `json:\"enabled,omitempty\"` // disable bot for this group (default: true)\n\tSkills []string `json:\"skills,omitempty\"` // skill whitelist (nil = all, [] = none)\n\tTools []string `json:\"tools,omitempty\"` // tool allow list (nil = all, supports \"group:xxx\")\n\tSystemPrompt string `json:\"system_prompt,omitempty\"` // extra system prompt for this group\n\tTopics map[string]*TelegramTopicConfig `json:\"topics,omitempty\"` // per-topic overrides (key: thread ID string)\n\tQuota *QuotaWindow `json:\"quota,omitempty\"` // per-group quota override\n}\n\n// TelegramTopicConfig defines per-topic overrides within a Telegram group.\n// Matching TS TelegramTopicConfig in src/config/types.telegram.ts.\ntype TelegramTopicConfig struct {\n\tRequireMention *bool `json:\"require_mention,omitempty\"`\n\tGroupPolicy string `json:\"group_policy,omitempty\"`\n\tSkills []string `json:\"skills,omitempty\"`\n\tTools []string `json:\"tools,omitempty\"` // tool allow list (nil = inherit, supports \"group:xxx\")\n\tEnabled *bool `json:\"enabled,omitempty\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from,omitempty\"`\n\tSystemPrompt string `json:\"system_prompt,omitempty\"`\n}\n\ntype DiscordConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tToken string `json:\"token\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in groups (default true)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tMediaMaxBytes int64 `json:\"media_max_bytes,omitempty\"` // max media download size (default 25MB)\n\tSTTProxyURL string `json:\"stt_proxy_url,omitempty\"`\n\tSTTAPIKey string `json:\"stt_api_key,omitempty\"`\n\tSTTTenantID string `json:\"stt_tenant_id,omitempty\"`\n\tSTTTimeoutSeconds int `json:\"stt_timeout_seconds,omitempty\"`\n\tVoiceAgentID string `json:\"voice_agent_id,omitempty\"`\n}\n\ntype SlackConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tBotToken string `json:\"bot_token\"` // xoxb-... (Bot User OAuth Token)\n\tAppToken string `json:\"app_token\"` // xapp-... (App-Level Token for Socket Mode)\n\tUserToken string `json:\"user_token,omitempty\"` // xoxp-... (Optional: custom bot identity)\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"pairing\", \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in channels (default true)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tDMStream *bool `json:\"dm_stream,omitempty\"` // enable streaming for DMs (default false)\n\tGroupStream *bool `json:\"group_stream,omitempty\"` // enable streaming for groups (default false)\n\tNativeStream *bool `json:\"native_stream,omitempty\"` // use Slack ChatStreamer API if available (default false)\n\tReactionLevel string `json:\"reaction_level,omitempty\"` // \"off\" (default), \"minimal\", \"full\"\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tDebounceDelay int `json:\"debounce_delay,omitempty\"` // ms delay before dispatching rapid messages (default 300, 0=disabled)\n\tThreadTTL *int `json:\"thread_ttl,omitempty\"` // hours before thread participation expires (default 24, 0=disabled — always require @mention)\n\tMediaMaxBytes int64 `json:\"media_max_bytes,omitempty\"` // max file download size in bytes (default 20MB)\n}\n\ntype WhatsAppConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tBridgeURL string `json:\"bridge_url\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n}\n\ntype ZaloConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tToken string `json:\"token\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tWebhookURL string `json:\"webhook_url,omitempty\"`\n\tWebhookSecret string `json:\"webhook_secret,omitempty\"`\n\tMediaMaxMB int `json:\"media_max_mb,omitempty\"` // default 5\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n}\n\ntype ZaloPersonalConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in groups (default true)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tCredentialsPath string `json:\"credentials_path,omitempty\"` // path to saved cookies JSON\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n}\n\ntype FeishuConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tAppID string `json:\"app_id\"`\n\tAppSecret string `json:\"app_secret\"`\n\tEncryptKey string `json:\"encrypt_key,omitempty\"`\n\tVerificationToken string `json:\"verification_token,omitempty\"`\n\tDomain string `json:\"domain,omitempty\"` // \"lark\" (default/global), \"feishu\" (China), or custom URL\n\tConnectionMode string `json:\"connection_mode,omitempty\"` // \"websocket\" (default), \"webhook\"\n\tWebhookPort int `json:\"webhook_port,omitempty\"` // default 3000\n\tWebhookPath string `json:\"webhook_path,omitempty\"` // default \"/feishu/events\"\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default)\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default)\n\tGroupAllowFrom FlexibleStringSlice `json:\"group_allow_from,omitempty\"`\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // default true (groups)\n\tTopicSessionMode string `json:\"topic_session_mode,omitempty\"` // \"disabled\" (default)\n\tTextChunkLimit int `json:\"text_chunk_limit,omitempty\"` // default 4000\n\tMediaMaxMB int `json:\"media_max_mb,omitempty\"` // default 30\n\tRenderMode string `json:\"render_mode,omitempty\"` // \"auto\", \"raw\", \"card\"\n\tStreaming *bool `json:\"streaming,omitempty\"` // default true\n\tReactionLevel string `json:\"reaction_level,omitempty\"` // \"off\" (default), \"minimal\", \"full\" — typing emoji reactions\n\tHistoryLimit int `json:\"history_limit,omitempty\"`\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tSTTProxyURL string `json:\"stt_proxy_url,omitempty\"`\n\tSTTAPIKey string `json:\"stt_api_key,omitempty\"`\n\tSTTTenantID string `json:\"stt_tenant_id,omitempty\"`\n\tSTTTimeoutSeconds int `json:\"stt_timeout_seconds,omitempty\"`\n\tVoiceAgentID string `json:\"voice_agent_id,omitempty\"`\n}\n\n// ProvidersConfig maps provider name to its config.\ntype ProvidersConfig struct {\n\tAnthropic ProviderConfig `json:\"anthropic\"`\n\tOpenAI ProviderConfig `json:\"openai\"`\n\tOpenRouter ProviderConfig `json:\"openrouter\"`\n\tGroq ProviderConfig `json:\"groq\"`\n\tGemini ProviderConfig `json:\"gemini\"`\n\tDeepSeek ProviderConfig `json:\"deepseek\"`\n\tMistral ProviderConfig `json:\"mistral\"`\n\tXAI ProviderConfig `json:\"xai\"`\n\tMiniMax ProviderConfig `json:\"minimax\"`\n\tCohere ProviderConfig `json:\"cohere\"`\n\tPerplexity ProviderConfig `json:\"perplexity\"`\n\tDashScope ProviderConfig `json:\"dashscope\"`\n\tBailian ProviderConfig `json:\"bailian\"`\n\tZai ProviderConfig `json:\"zai\"`\n\tZaiCoding ProviderConfig `json:\"zai_coding\"`\n\tOllama OllamaConfig `json:\"ollama\"` // local Ollama instance (no API key needed)\n\tOllamaCloud ProviderConfig `json:\"ollama_cloud\"` // Ollama Cloud (API key required)\n\tClaudeCLI ClaudeCLIConfig `json:\"claude_cli\"`\n\tACP ACPConfig `json:\"acp\"`\n}\n\n// OllamaConfig configures a local (or self-hosted) Ollama instance.\n// No API key is required — Ollama accepts any Bearer token value.\ntype OllamaConfig struct {\n\tHost string `json:\"host\"` // Ollama server base URL, e.g. http://localhost:11434\n}\n\n// ClaudeCLIConfig configures the Claude CLI provider (uses subscription, not API key).\ntype ClaudeCLIConfig struct {\n\tCLIPath string `json:\"cli_path\" yaml:\"cli_path\"` // path to claude binary (default: \"claude\")\n\tModel string `json:\"model\" yaml:\"model\"` // default model alias (default: \"sonnet\")\n\tBaseWorkDir string `json:\"base_work_dir\" yaml:\"base_work_dir\"` // base dir for agent workspaces\n\tPermMode string `json:\"perm_mode\" yaml:\"perm_mode\"` // permission mode (default: \"bypassPermissions\")\n}\n\n// ACPConfig configures the ACP (Agent Client Protocol) provider.\n// Orchestrates any ACP-compatible coding agent (Claude Code, Codex CLI, Gemini CLI) as a subprocess.\ntype ACPConfig struct {\n\tBinary string `json:\"binary\"` // agent binary name or path (e.g. \"claude\", \"codex\")\n\tArgs []string `json:\"args\"` // extra spawn args\n\tModel string `json:\"model\"` // default model/agent name\n\tWorkDir string `json:\"work_dir\"` // base workspace dir\n\tIdleTTL string `json:\"idle_ttl\"` // process idle TTL (e.g. \"5m\")\n\tPermMode string `json:\"perm_mode\"` // \"approve-all\" (default), \"approve-reads\", \"deny-all\"\n}\n\ntype ProviderConfig struct {\n\tAPIKey string `json:\"api_key\"`\n\tAPIBase string `json:\"api_base,omitempty\"`\n}\n\n// APIBaseForType returns the config-level api_base for a given provider type.\n// Used as a fallback when DB providers have no api_base set.\nfunc (p *ProvidersConfig) APIBaseForType(providerType string) string {\n\tswitch providerType {\n\tcase \"anthropic_native\":\n\t\treturn p.Anthropic.APIBase\n\tcase \"openai\", \"openai_compat\":\n\t\treturn p.OpenAI.APIBase\n\tcase \"openrouter\":\n\t\treturn p.OpenRouter.APIBase\n\tcase \"groq\":\n\t\treturn p.Groq.APIBase\n\tcase \"deepseek\":\n\t\treturn p.DeepSeek.APIBase\n\tcase \"gemini_native\":\n\t\treturn p.Gemini.APIBase\n\tcase \"mistral\":\n\t\treturn p.Mistral.APIBase\n\tcase \"xai\":\n\t\treturn p.XAI.APIBase\n\tcase \"minimax_native\":\n\t\treturn p.MiniMax.APIBase\n\tcase \"cohere\":\n\t\treturn p.Cohere.APIBase\n\tcase \"perplexity\":\n\t\treturn p.Perplexity.APIBase\n\tcase \"dashscope\":\n\t\treturn p.DashScope.APIBase\n\tcase \"bailian\":\n\t\treturn p.Bailian.APIBase\n\tcase \"zai\":\n\t\treturn p.Zai.APIBase\n\tcase \"zai_coding\":\n\t\treturn p.ZaiCoding.APIBase\n\tcase \"ollama_cloud\":\n\t\treturn p.OllamaCloud.APIBase\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\n// HasAnyProvider returns true if at least one provider has an API key or CLI configured.\nfunc (c *Config) HasAnyProvider() bool {\n\tp := c.Providers\n\treturn p.Anthropic.APIKey != \"\" ||\n\t\tp.OpenAI.APIKey != \"\" ||\n\t\tp.OpenRouter.APIKey != \"\" ||\n\t\tp.Groq.APIKey != \"\" ||\n\t\tp.Gemini.APIKey != \"\" ||\n\t\tp.DeepSeek.APIKey != \"\" ||\n\t\tp.Mistral.APIKey != \"\" ||\n\t\tp.XAI.APIKey != \"\" ||\n\t\tp.MiniMax.APIKey != \"\" ||\n\t\tp.Cohere.APIKey != \"\" ||\n\t\tp.Perplexity.APIKey != \"\" ||\n\t\tp.DashScope.APIKey != \"\" ||\n\t\tp.Bailian.APIKey != \"\" ||\n\t\tp.Zai.APIKey != \"\" ||\n\t\tp.ZaiCoding.APIKey != \"\" ||\n\t\tp.Ollama.Host != \"\" ||\n\t\tp.OllamaCloud.APIKey != \"\" ||\n\t\tp.ClaudeCLI.CLIPath != \"\" ||\n\t\tp.ACP.Binary != \"\"\n}\n\n// QuotaWindow defines request limits per time window. Zero means unlimited.\ntype QuotaWindow struct {\n\tHour int `json:\"hour,omitempty\"` // max requests per hour (0 = unlimited)\n\tDay int `json:\"day,omitempty\"` // max requests per day (0 = unlimited)\n\tWeek int `json:\"week,omitempty\"` // max requests per week (0 = unlimited)\n}\n\n// IsZero returns true if no limits are set.\nfunc (w QuotaWindow) IsZero() bool { return w.Hour == 0 && w.Day == 0 && w.Week == 0 }\n\n// QuotaConfig configures per-user/group request quotas.\n// Config merge priority: Groups > Channels > Providers > Default.\ntype QuotaConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tDefault QuotaWindow `json:\"default\"`\n\tProviders map[string]QuotaWindow `json:\"providers,omitempty\"` // key = provider name (e.g. \"anthropic\")\n\tChannels map[string]QuotaWindow `json:\"channels,omitempty\"` // key = channel name (e.g. \"telegram\")\n\tGroups map[string]QuotaWindow `json:\"groups,omitempty\"` // key = userID (e.g. \"group:telegram:-100123\")\n}\n\n// GatewayConfig controls the gateway server.\ntype GatewayConfig struct {\n\tHost string `json:\"host\"`\n\tPort int `json:\"port\"`\n\tToken string `json:\"token,omitempty\"` // bearer token for WS/HTTP auth\n\tOwnerIDs []string `json:\"owner_ids,omitempty\"` // sender IDs considered \"owner\"\n\tAllowedOrigins []string `json:\"allowed_origins,omitempty\"` // WebSocket CORS whitelist (empty = allow all)\n\tMaxMessageChars int `json:\"max_message_chars,omitempty\"` // max user message characters (default 32000)\n\tRateLimitRPM int `json:\"rate_limit_rpm,omitempty\"` // rate limit: requests per minute per user (default 20, 0 = disabled)\n\tInjectionAction string `json:\"injection_action,omitempty\"` // prompt injection action: \"log\", \"warn\" (default), \"block\", \"off\"\n\tInboundDebounceMs int `json:\"inbound_debounce_ms,omitempty\"` // merge rapid messages from same sender (default 1000ms, -1 = disabled)\n\tQuota *QuotaConfig `json:\"quota,omitempty\"` // per-user/group request quotas\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // deliver intermediate text during tool iterations (default false)\n\tToolStatus *bool `json:\"tool_status,omitempty\"` // show tool name in streaming preview during tool execution (default true)\n\tTaskRecoveryIntervalSec int `json:\"task_recovery_interval_sec,omitempty\"` // team task recovery ticker interval in seconds (default 300 = 5min)\n}\n\n// ToolsConfig controls tool availability, policy, and web search.\ntype ToolsConfig struct {\n\tProfile string `json:\"profile,omitempty\"` // global profile: \"minimal\", \"coding\", \"messaging\", \"full\"\n\tAllow []string `json:\"allow,omitempty\"` // global allow list (tool names or \"group:xxx\")\n\tDeny []string `json:\"deny,omitempty\"` // global deny list\n\tAlsoAllow []string `json:\"alsoAllow,omitempty\"` // additive: adds without removing existing\n\tByProvider map[string]*ToolPolicySpec `json:\"byProvider,omitempty\"` // per-provider overrides\n\tExecApproval ExecApprovalCfg `json:\"execApproval\"` // exec command approval settings\n\tWebFetch WebFetchPolicyConfig `json:\"web_fetch\"` // domain policy for URL fetching\n\tWeb WebToolsConfig `json:\"web\"`\n\tBrowser BrowserToolConfig `json:\"browser\"`\n\tRateLimitPerHour int `json:\"rate_limit_per_hour,omitempty\"` // max tool executions per hour per session (0 = disabled)\n\tScrubCredentials *bool `json:\"scrub_credentials,omitempty\"` // auto-redact API keys/tokens in tool output (default true)\n\tMcpServers map[string]*MCPServerConfig `json:\"mcp_servers,omitempty\"` // external MCP server connections\n}\n\n// MCPServerConfig configures a single external MCP server connection.\ntype MCPServerConfig struct {\n\tTransport string `json:\"transport\"` // \"stdio\", \"sse\", \"streamable-http\"\n\tCommand string `json:\"command,omitempty\"` // stdio: command to spawn\n\tArgs []string `json:\"args,omitempty\"` // stdio: command arguments\n\tEnv map[string]string `json:\"env,omitempty\"` // stdio: extra environment variables\n\tURL string `json:\"url,omitempty\"` // sse/http: server URL\n\tHeaders map[string]string `json:\"headers,omitempty\"` // sse/http: extra HTTP headers\n\tEnabled *bool `json:\"enabled,omitempty\"` // default true\n\tToolPrefix string `json:\"tool_prefix,omitempty\"` // prefix for tool names (avoids collisions)\n\tTimeoutSec int `json:\"timeout_sec,omitempty\"` // per-tool-call timeout in seconds (default 60)\n}\n\n// IsEnabled returns whether this MCP server is enabled (default true).\nfunc (c *MCPServerConfig) IsEnabled() bool {\n\treturn c.Enabled == nil || *c.Enabled\n}\n\n// ExecApprovalCfg configures command execution approval (matching TS exec-approval.ts).\ntype ExecApprovalCfg struct {\n\tSecurity string `json:\"security,omitempty\"` // \"deny\", \"allowlist\", \"full\" (default \"full\")\n\tAsk string `json:\"ask,omitempty\"` // \"off\", \"on-miss\", \"always\" (default \"off\")\n\tAllowlist []string `json:\"allowlist,omitempty\"` // glob patterns for allowed commands\n}\n\n// WebFetchPolicyConfig controls domain filtering for the web_fetch tool.\ntype WebFetchPolicyConfig struct {\n\tPolicy string `json:\"policy,omitempty\"` // \"allow_all\" (default), \"allowlist\"\n\tAllowedDomains []string `json:\"allowed_domains,omitempty\"` // e.g. [\"github.com\", \"*.example.com\"]\n\tBlockedDomains []string `json:\"blocked_domains,omitempty\"` // always checked regardless of policy\n}\n\n// BrowserToolConfig controls the browser automation tool.\ntype BrowserToolConfig struct {\n\tEnabled bool `json:\"enabled\"` // enable the browser tool (default false)\n\tHeadless bool `json:\"headless,omitempty\"` // run Chrome in headless mode (ignored when RemoteURL is set)\n\tRemoteURL string `json:\"remote_url,omitempty\"` // CDP endpoint for remote Chrome sidecar, e.g. \"ws://chrome:9222\"\n}\n\n// ToolPolicySpec defines a tool policy at any level (global, per-agent, per-provider).\ntype ToolPolicySpec struct {\n\tProfile string `json:\"profile,omitempty\"`\n\tAllow []string `json:\"allow,omitempty\"`\n\tDeny []string `json:\"deny,omitempty\"`\n\tAlsoAllow []string `json:\"alsoAllow,omitempty\"`\n\tByProvider map[string]*ToolPolicySpec `json:\"byProvider,omitempty\"`\n}\n\ntype WebToolsConfig struct {\n\tBrave BraveConfig `json:\"brave\"`\n\tDuckDuckGo DuckDuckGoConfig `json:\"duckduckgo\"`\n}\n\ntype BraveConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tAPIKey string `json:\"api_key\"`\n\tMaxResults int `json:\"max_results\"`\n}\n\ntype DuckDuckGoConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tMaxResults int `json:\"max_results\"`\n}\n\n// SessionsConfig controls session behavior.\n// Matching TS src/config/sessions/types.ts + src/config/types.base.ts.\ntype SessionsConfig struct {\n\tScope string `json:\"scope,omitempty\"` // \"per-sender\" (default), \"global\"\n\tDmScope string `json:\"dm_scope,omitempty\"` // \"main\", \"per-peer\", \"per-channel-peer\" (default), \"per-account-channel-peer\"\n\tMainKey string `json:\"main_key,omitempty\"` // main session key suffix (default \"main\", used when dm_scope=\"main\")\n}\n\n// TtsConfig configures text-to-speech.\n// Matching TS src/config/types.tts.ts.\ntype TtsConfig struct {\n\tProvider string `json:\"provider,omitempty\"` // \"openai\", \"elevenlabs\", \"edge\", \"minimax\"\n\tAuto string `json:\"auto,omitempty\"` // \"off\" (default), \"always\", \"inbound\", \"tagged\"\n\tMode string `json:\"mode,omitempty\"` // \"final\" (default), \"all\"\n\tMaxLength int `json:\"max_length,omitempty\"` // max text length before truncation (default 1500)\n\tTimeoutMs int `json:\"timeout_ms,omitempty\"` // API timeout in ms (default 30000)\n\tOpenAI TtsOpenAIConfig `json:\"openai\"`\n\tElevenLabs TtsElevenLabsConfig `json:\"elevenlabs\"`\n\tEdge TtsEdgeConfig `json:\"edge\"`\n\tMiniMax TtsMiniMaxConfig `json:\"minimax\"`\n}\n\n// TtsOpenAIConfig configures the OpenAI TTS provider.\ntype TtsOpenAIConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"`\n\tAPIBase string `json:\"api_base,omitempty\"` // custom endpoint URL\n\tModel string `json:\"model,omitempty\"` // default \"gpt-4o-mini-tts\"\n\tVoice string `json:\"voice,omitempty\"` // default \"alloy\"\n}\n\n// TtsElevenLabsConfig configures the ElevenLabs TTS provider.\ntype TtsElevenLabsConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"`\n\tBaseURL string `json:\"base_url,omitempty\"`\n\tVoiceID string `json:\"voice_id,omitempty\"` // default \"pMsXgVXv3BLzUgSXRplE\"\n\tModelID string `json:\"model_id,omitempty\"` // default \"eleven_multilingual_v2\"\n}\n\n// TtsEdgeConfig configures the Microsoft Edge TTS provider (free, no API key).\ntype TtsEdgeConfig struct {\n\tEnabled bool `json:\"enabled,omitempty\"`\n\tVoice string `json:\"voice,omitempty\"` // default \"en-US-MichelleNeural\"\n\tRate string `json:\"rate,omitempty\"` // speech rate, e.g. \"+0%\"\n}\n\n// TtsMiniMaxConfig configures the MiniMax TTS provider.\ntype TtsMiniMaxConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"`\n\tGroupID string `json:\"group_id,omitempty\"` // MiniMax GroupId (required)\n\tAPIBase string `json:\"api_base,omitempty\"` // default \"https://api.minimax.io/v1\"\n\tModel string `json:\"model,omitempty\"` // default \"speech-02-hd\"\n\tVoiceID string `json:\"voice_id,omitempty\"` // default \"Wise_Woman\"\n}\n\n// MergeChannelGroupQuotas merges per-group quota overrides from channel configs\n// (e.g., channels.telegram.groups[chatID].quota) into gateway.quota.groups.\n// This allows per-group quotas to be set at the channel level and picked up\n// by the QuotaChecker at runtime.\nfunc MergeChannelGroupQuotas(cfg *Config) {\n\tif cfg.Gateway.Quota == nil {\n\t\treturn\n\t}\n\tif cfg.Gateway.Quota.Groups == nil {\n\t\tcfg.Gateway.Quota.Groups = make(map[string]QuotaWindow)\n\t}\n\n\t// Telegram per-group quotas\n\tfor chatID, groupCfg := range cfg.Channels.Telegram.Groups {\n\t\tif groupCfg != nil && groupCfg.Quota != nil && !groupCfg.Quota.IsZero() {\n\t\t\tkey := \"group:telegram:\" + chatID\n\t\t\tcfg.Gateway.Quota.Groups[key] = *groupCfg.Quota\n\t\t}\n\t}\n}\n"}, "3f9401257cc14fd4df4aa77487cdfe1d86cf3de3": {"t": 1785759265.7911952, "n": 29559, "h": "359a8a77af008043", "c": "package config\n\n// PendingCompactionConfig configures LLM-based compaction of pending group messages.\n// When a group accumulates more than Threshold pending messages, older messages are\n// summarized by an LLM and replaced with a compact summary, keeping KeepRecent raw messages.\ntype PendingCompactionConfig struct {\n\tThreshold int `json:\"threshold,omitempty\"` // trigger compaction when entries exceed this (default 50)\n\tKeepRecent int `json:\"keep_recent,omitempty\"` // keep this many recent raw messages after compaction (default 15)\n\tMaxTokens int `json:\"max_tokens,omitempty\"` // max output tokens for LLM summarization (default 4096)\n\tProvider string `json:\"provider,omitempty\"` // LLM provider name (e.g. \"openai\"); empty = use agent's provider\n\tModel string `json:\"model,omitempty\"` // model for summarization; empty = use agent's model\n}\n\n// ChannelsConfig contains per-channel configuration.\ntype ChannelsConfig struct {\n\tTelegram TelegramConfig `json:\"telegram\"`\n\tDiscord DiscordConfig `json:\"discord\"`\n\tSlack SlackConfig `json:\"slack\"`\n\tWhatsApp WhatsAppConfig `json:\"whatsapp\"`\n\tZalo ZaloConfig `json:\"zalo\"`\n\tZaloPersonal ZaloPersonalConfig `json:\"zalo_personal\"`\n\tFeishu FeishuConfig `json:\"feishu\"`\n\tPendingCompaction *PendingCompactionConfig `json:\"pending_compaction,omitempty\"` // global pending message compaction settings\n}\n\ntype TelegramConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tToken string `json:\"token\"`\n\tProxy string `json:\"proxy,omitempty\"`\n\tAPIServer string `json:\"api_server,omitempty\"` // custom Telegram Bot API server URL (e.g. \"http://localhost:8081\")\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in groups (default true)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tDMStream *bool `json:\"dm_stream,omitempty\"` // enable streaming for DMs (default false) — edits placeholder progressively\n\tGroupStream *bool `json:\"group_stream,omitempty\"` // enable streaming for groups (default false) — sends new message, edits progressively\n\tDraftTransport *bool `json:\"draft_transport,omitempty\"` // use sendMessageDraft for DM streaming (default true) — stealth preview, no notifications per edit\n\tReasoningStream *bool `json:\"reasoning_stream,omitempty\"` // show reasoning as separate message when provider emits thinking events (default true)\n\tReactionLevel string `json:\"reaction_level,omitempty\"` // \"off\" (default), \"minimal\", \"full\" — status emoji reactions\n\tMediaMaxBytes int64 `json:\"media_max_bytes,omitempty\"` // max media download size in bytes (default 20MB)\n\tLinkPreview *bool `json:\"link_preview,omitempty\"` // enable URL previews in messages (default true)\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tForceIPv4 bool `json:\"force_ipv4,omitempty\"` // force IPv4 for all Telegram API requests (use when IPv6 routing is broken)\n\n\t// Optional STT (Speech-to-Text) pipeline for voice/audio inbound messages.\n\t// When stt_proxy_url is set, audio/voice messages are transcribed before being forwarded to the agent.\n\tSTTProxyURL string `json:\"stt_proxy_url,omitempty\"` // base URL of the STT proxy service (e.g. \"https://stt.example.com\")\n\tSTTAPIKey string `json:\"stt_api_key,omitempty\"` // Bearer token for the STT proxy\n\tSTTTenantID string `json:\"stt_tenant_id,omitempty\"` // optional tenant/org identifier forwarded to the STT proxy\n\tSTTTimeoutSeconds int `json:\"stt_timeout_seconds,omitempty\"` // per-request timeout for STT calls (default 30s)\n\n\t// Optional audio-aware routing: when set, voice/audio inbound messages are routed to this\n\t// agent instead of the default channel agent. Requires the named agent to exist in the config.\n\tVoiceAgentID string `json:\"voice_agent_id,omitempty\"` // agent ID to route voice inbound to (e.g. \"speaking-agent\")\n\n\t// Per-group (and per-topic) overrides. Key is chat ID string (e.g. \"-100123456\") or \"*\" for wildcard.\n\t// TS ref: channels.telegram.groups in src/config/types.telegram.ts.\n\tGroups map[string]*TelegramGroupConfig `json:\"groups,omitempty\"`\n}\n\n// TelegramGroupConfig defines per-group overrides for a Telegram channel.\n// Matching TS TelegramGroupConfig in src/config/types.telegram.ts.\ntype TelegramGroupConfig struct {\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // override group policy for this group\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // override require_mention for this group\n\tAllowFrom FlexibleStringSlice `json:\"allow_from,omitempty\"` // override allow_from for this group\n\tEnabled *bool `json:\"enabled,omitempty\"` // disable bot for this group (default: true)\n\tSkills []string `json:\"skills,omitempty\"` // skill whitelist (nil = all, [] = none)\n\tTools []string `json:\"tools,omitempty\"` // tool allow list (nil = all, supports \"group:xxx\")\n\tSystemPrompt string `json:\"system_prompt,omitempty\"` // extra system prompt for this group\n\tTopics map[string]*TelegramTopicConfig `json:\"topics,omitempty\"` // per-topic overrides (key: thread ID string)\n\tQuota *QuotaWindow `json:\"quota,omitempty\"` // per-group quota override\n}\n\n// TelegramTopicConfig defines per-topic overrides within a Telegram group.\n// Matching TS TelegramTopicConfig in src/config/types.telegram.ts.\ntype TelegramTopicConfig struct {\n\tRequireMention *bool `json:\"require_mention,omitempty\"`\n\tGroupPolicy string `json:\"group_policy,omitempty\"`\n\tSkills []string `json:\"skills,omitempty\"`\n\tTools []string `json:\"tools,omitempty\"` // tool allow list (nil = inherit, supports \"group:xxx\")\n\tEnabled *bool `json:\"enabled,omitempty\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from,omitempty\"`\n\tSystemPrompt string `json:\"system_prompt,omitempty\"`\n}\n\ntype DiscordConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tToken string `json:\"token\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in groups (default true)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tMediaMaxBytes int64 `json:\"media_max_bytes,omitempty\"` // max media download size (default 25MB)\n\tSTTProxyURL string `json:\"stt_proxy_url,omitempty\"`\n\tSTTAPIKey string `json:\"stt_api_key,omitempty\"`\n\tSTTTenantID string `json:\"stt_tenant_id,omitempty\"`\n\tSTTTimeoutSeconds int `json:\"stt_timeout_seconds,omitempty\"`\n\tVoiceAgentID string `json:\"voice_agent_id,omitempty\"`\n}\n\ntype SlackConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tBotToken string `json:\"bot_token\"` // xoxb-... (Bot User OAuth Token)\n\tAppToken string `json:\"app_token\"` // xapp-... (App-Level Token for Socket Mode)\n\tUserToken string `json:\"user_token,omitempty\"` // xoxp-... (Optional: custom bot identity)\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"pairing\", \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in channels (default true)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tDMStream *bool `json:\"dm_stream,omitempty\"` // enable streaming for DMs (default false)\n\tGroupStream *bool `json:\"group_stream,omitempty\"` // enable streaming for groups (default false)\n\tNativeStream *bool `json:\"native_stream,omitempty\"` // use Slack ChatStreamer API if available (default false)\n\tReactionLevel string `json:\"reaction_level,omitempty\"` // \"off\" (default), \"minimal\", \"full\"\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tDebounceDelay int `json:\"debounce_delay,omitempty\"` // ms delay before dispatching rapid messages (default 300, 0=disabled)\n\tThreadTTL *int `json:\"thread_ttl,omitempty\"` // hours before thread participation expires (default 24, 0=disabled — always require @mention)\n\tMediaMaxBytes int64 `json:\"media_max_bytes,omitempty\"` // max file download size in bytes (default 20MB)\n}\n\ntype WhatsAppConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tBridgeURL string `json:\"bridge_url\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n}\n\ntype ZaloConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tToken string `json:\"token\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tWebhookURL string `json:\"webhook_url,omitempty\"`\n\tWebhookSecret string `json:\"webhook_secret,omitempty\"`\n\tMediaMaxMB int `json:\"media_max_mb,omitempty\"` // default 5\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n}\n\ntype ZaloPersonalConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in groups (default true)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tCredentialsPath string `json:\"credentials_path,omitempty\"` // path to saved cookies JSON\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n}\n\ntype FeishuConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tAppID string `json:\"app_id\"`\n\tAppSecret string `json:\"app_secret\"`\n\tEncryptKey string `json:\"encrypt_key,omitempty\"`\n\tVerificationToken string `json:\"verification_token,omitempty\"`\n\tDomain string `json:\"domain,omitempty\"` // \"lark\" (default/global), \"feishu\" (China), or custom URL\n\tConnectionMode string `json:\"connection_mode,omitempty\"` // \"websocket\" (default), \"webhook\"\n\tWebhookPort int `json:\"webhook_port,omitempty\"` // default 3000\n\tWebhookPath string `json:\"webhook_path,omitempty\"` // default \"/feishu/events\"\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default)\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default)\n\tGroupAllowFrom FlexibleStringSlice `json:\"group_allow_from,omitempty\"`\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // default true (groups)\n\tTopicSessionMode string `json:\"topic_session_mode,omitempty\"` // \"disabled\" (default)\n\tTextChunkLimit int `json:\"text_chunk_limit,omitempty\"` // default 4000\n\tMediaMaxMB int `json:\"media_max_mb,omitempty\"` // default 30\n\tRenderMode string `json:\"render_mode,omitempty\"` // \"auto\", \"raw\", \"card\"\n\tStreaming *bool `json:\"streaming,omitempty\"` // default true\n\tReactionLevel string `json:\"reaction_level,omitempty\"` // \"off\" (default), \"minimal\", \"full\" — typing emoji reactions\n\tHistoryLimit int `json:\"history_limit,omitempty\"`\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tSTTProxyURL string `json:\"stt_proxy_url,omitempty\"`\n\tSTTAPIKey string `json:\"stt_api_key,omitempty\"`\n\tSTTTenantID string `json:\"stt_tenant_id,omitempty\"`\n\tSTTTimeoutSeconds int `json:\"stt_timeout_seconds,omitempty\"`\n\tVoiceAgentID string `json:\"voice_agent_id,omitempty\"`\n}\n\n// ProvidersConfig maps provider name to its config.\ntype ProvidersConfig struct {\n\tAnthropic ProviderConfig `json:\"anthropic\"`\n\tOpenAI ProviderConfig `json:\"openai\"`\n\tOpenRouter ProviderConfig `json:\"openrouter\"`\n\tGroq ProviderConfig `json:\"groq\"`\n\tGemini ProviderConfig `json:\"gemini\"`\n\tDeepSeek ProviderConfig `json:\"deepseek\"`\n\tMistral ProviderConfig `json:\"mistral\"`\n\tXAI ProviderConfig `json:\"xai\"`\n\tMiniMax ProviderConfig `json:\"minimax\"`\n\tCohere ProviderConfig `json:\"cohere\"`\n\tPerplexity ProviderConfig `json:\"perplexity\"`\n\tDashScope ProviderConfig `json:\"dashscope\"`\n\tBailian ProviderConfig `json:\"bailian\"`\n\tZai ProviderConfig `json:\"zai\"`\n\tZaiCoding ProviderConfig `json:\"zai_coding\"`\n\tOllama OllamaConfig `json:\"ollama\"` // local Ollama instance (no API key needed)\n\tOllamaCloud ProviderConfig `json:\"ollama_cloud\"` // Ollama Cloud (API key required)\n\tClaudeCLI ClaudeCLIConfig `json:\"claude_cli\"`\n\tACP ACPConfig `json:\"acp\"`\n}\n\n// OllamaConfig configures a local (or self-hosted) Ollama instance.\n// No API key is required — Ollama accepts any Bearer token value.\ntype OllamaConfig struct {\n\tHost string `json:\"host\"` // Ollama server base URL, e.g. http://localhost:11434\n}\n\n// ClaudeCLIConfig configures the Claude CLI provider (uses subscription, not API key).\ntype ClaudeCLIConfig struct {\n\tCLIPath string `json:\"cli_path\" yaml:\"cli_path\"` // path to claude binary (default: \"claude\")\n\tModel string `json:\"model\" yaml:\"model\"` // default model alias (default: \"sonnet\")\n\tBaseWorkDir string `json:\"base_work_dir\" yaml:\"base_work_dir\"` // base dir for agent workspaces\n\tPermMode string `json:\"perm_mode\" yaml:\"perm_mode\"` // permission mode (default: \"bypassPermissions\")\n}\n\n// ACPConfig configures the ACP (Agent Client Protocol) provider.\n// Orchestrates any ACP-compatible coding agent (Claude Code, Codex CLI, Gemini CLI) as a subprocess.\ntype ACPConfig struct {\n\tBinary string `json:\"binary\"` // agent binary name or path (e.g. \"claude\", \"codex\")\n\tArgs []string `json:\"args\"` // extra spawn args\n\tModel string `json:\"model\"` // default model/agent name\n\tWorkDir string `json:\"work_dir\"` // base workspace dir\n\tIdleTTL string `json:\"idle_ttl\"` // process idle TTL (e.g. \"5m\")\n\tPermMode string `json:\"perm_mode\"` // \"approve-all\" (default), \"approve-reads\", \"deny-all\"\n}\n\ntype ProviderConfig struct {\n\tAPIKey string `json:\"api_key\"`\n\tAPIBase string `json:\"api_base,omitempty\"`\n}\n\n// APIBaseForType returns the config-level api_base for a given provider type.\n// Used as a fallback when DB providers have no api_base set.\nfunc (p *ProvidersConfig) APIBaseForType(providerType string) string {\n\tswitch providerType {\n\tcase \"anthropic_native\":\n\t\treturn p.Anthropic.APIBase\n\tcase \"openai\", \"openai_compat\":\n\t\treturn p.OpenAI.APIBase\n\tcase \"openrouter\":\n\t\treturn p.OpenRouter.APIBase\n\tcase \"groq\":\n\t\treturn p.Groq.APIBase\n\tcase \"deepseek\":\n\t\treturn p.DeepSeek.APIBase\n\tcase \"gemini_native\":\n\t\treturn p.Gemini.APIBase\n\tcase \"mistral\":\n\t\treturn p.Mistral.APIBase\n\tcase \"xai\":\n\t\treturn p.XAI.APIBase\n\tcase \"minimax_native\":\n\t\treturn p.MiniMax.APIBase\n\tcase \"cohere\":\n\t\treturn p.Cohere.APIBase\n\tcase \"perplexity\":\n\t\treturn p.Perplexity.APIBase\n\tcase \"dashscope\":\n\t\treturn p.DashScope.APIBase\n\tcase \"bailian\":\n\t\treturn p.Bailian.APIBase\n\tcase \"zai\":\n\t\treturn p.Zai.APIBase\n\tcase \"zai_coding\":\n\t\treturn p.ZaiCoding.APIBase\n\tcase \"ollama_cloud\":\n\t\treturn p.OllamaCloud.APIBase\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\n// HasAnyProvider returns true if at least one provider has an API key or CLI configured.\nfunc (c *Config) HasAnyProvider() bool {\n\tp := c.Providers\n\treturn p.Anthropic.APIKey != \"\" ||\n\t\tp.OpenAI.APIKey != \"\" ||\n\t\tp.OpenRouter.APIKey != \"\" ||\n\t\tp.Groq.APIKey != \"\" ||\n\t\tp.Gemini.APIKey != \"\" ||\n\t\tp.DeepSeek.APIKey != \"\" ||\n\t\tp.Mistral.APIKey != \"\" ||\n\t\tp.XAI.APIKey != \"\" ||\n\t\tp.MiniMax.APIKey != \"\" ||\n\t\tp.Cohere.APIKey != \"\" ||\n\t\tp.Perplexity.APIKey != \"\" ||\n\t\tp.DashScope.APIKey != \"\" ||\n\t\tp.Bailian.APIKey != \"\" ||\n\t\tp.Zai.APIKey != \"\" ||\n\t\tp.ZaiCoding.APIKey != \"\" ||\n\t\tp.Ollama.Host != \"\" ||\n\t\tp.OllamaCloud.APIKey != \"\" ||\n\t\tp.ClaudeCLI.CLIPath != \"\" ||\n\t\tp.ACP.Binary != \"\"\n}\n\n// QuotaWindow defines request limits per time window. Zero means unlimited.\ntype QuotaWindow struct {\n\tHour int `json:\"hour,omitempty\"` // max requests per hour (0 = unlimited)\n\tDay int `json:\"day,omitempty\"` // max requests per day (0 = unlimited)\n\tWeek int `json:\"week,omitempty\"` // max requests per week (0 = unlimited)\n}\n\n// IsZero returns true if no limits are set.\nfunc (w QuotaWindow) IsZero() bool { return w.Hour == 0 && w.Day == 0 && w.Week == 0 }\n\n// QuotaConfig configures per-user/group request quotas.\n// Config merge priority: Groups > Channels > Providers > Default.\ntype QuotaConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tDefault QuotaWindow `json:\"default\"`\n\tProviders map[string]QuotaWindow `json:\"providers,omitempty\"` // key = provider name (e.g. \"anthropic\")\n\tChannels map[string]QuotaWindow `json:\"channels,omitempty\"` // key = channel name (e.g. \"telegram\")\n\tGroups map[string]QuotaWindow `json:\"groups,omitempty\"` // key = userID (e.g. \"group:telegram:-100123\")\n}\n\n// GatewayConfig controls the gateway server.\ntype GatewayConfig struct {\n\tHost string `json:\"host\"`\n\tPort int `json:\"port\"`\n\tToken string `json:\"token,omitempty\"` // bearer token for WS/HTTP auth\n\tOwnerIDs []string `json:\"owner_ids,omitempty\"` // sender IDs considered \"owner\"\n\tAllowedOrigins []string `json:\"allowed_origins,omitempty\"` // WebSocket CORS whitelist (empty = allow all)\n\tMaxMessageChars int `json:\"max_message_chars,omitempty\"` // max user message characters (default 32000)\n\tRateLimitRPM int `json:\"rate_limit_rpm,omitempty\"` // rate limit: requests per minute per user (default 20, 0 = disabled)\n\tInjectionAction string `json:\"injection_action,omitempty\"` // prompt injection action: \"log\", \"warn\" (default), \"block\", \"off\"\n\tInboundDebounceMs int `json:\"inbound_debounce_ms,omitempty\"` // merge rapid messages from same sender (default 1000ms, -1 = disabled)\n\tQuota *QuotaConfig `json:\"quota,omitempty\"` // per-user/group request quotas\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // deliver intermediate text during tool iterations (default false)\n\tToolStatus *bool `json:\"tool_status,omitempty\"` // show tool name in streaming preview during tool execution (default true)\n\tTaskRecoveryIntervalSec int `json:\"task_recovery_interval_sec,omitempty\"` // team task recovery ticker interval in seconds (default 300 = 5min)\n}\n\n// ToolsConfig controls tool availability, policy, and web search.\ntype ToolsConfig struct {\n\tProfile string `json:\"profile,omitempty\"` // global profile: \"minimal\", \"coding\", \"messaging\", \"full\"\n\tAllow []string `json:\"allow,omitempty\"` // global allow list (tool names or \"group:xxx\")\n\tDeny []string `json:\"deny,omitempty\"` // global deny list\n\tAlsoAllow []string `json:\"alsoAllow,omitempty\"` // additive: adds without removing existing\n\tByProvider map[string]*ToolPolicySpec `json:\"byProvider,omitempty\"` // per-provider overrides\n\tExecApproval ExecApprovalCfg `json:\"execApproval\"` // exec command approval settings\n\tWebFetch WebFetchPolicyConfig `json:\"web_fetch\"` // domain policy for URL fetching\n\tWeb WebToolsConfig `json:\"web\"`\n\tBrowser BrowserToolConfig `json:\"browser\"`\n\tRateLimitPerHour int `json:\"rate_limit_per_hour,omitempty\"` // max tool executions per hour per session (0 = disabled)\n\tScrubCredentials *bool `json:\"scrub_credentials,omitempty\"` // auto-redact API keys/tokens in tool output (default true)\n\tMcpServers map[string]*MCPServerConfig `json:\"mcp_servers,omitempty\"` // external MCP server connections\n}\n\n// MCPServerConfig configures a single external MCP server connection.\ntype MCPServerConfig struct {\n\tTransport string `json:\"transport\"` // \"stdio\", \"sse\", \"streamable-http\"\n\tCommand string `json:\"command,omitempty\"` // stdio: command to spawn\n\tArgs []string `json:\"args,omitempty\"` // stdio: command arguments\n\tEnv map[string]string `json:\"env,omitempty\"` // stdio: extra environment variables\n\tURL string `json:\"url,omitempty\"` // sse/http: server URL\n\tHeaders map[string]string `json:\"headers,omitempty\"` // sse/http: extra HTTP headers\n\tEnabled *bool `json:\"enabled,omitempty\"` // default true\n\tToolPrefix string `json:\"tool_prefix,omitempty\"` // prefix for tool names (avoids collisions)\n\tTimeoutSec int `json:\"timeout_sec,omitempty\"` // per-tool-call timeout in seconds (default 60)\n}\n\n// IsEnabled returns whether this MCP server is enabled (default true).\nfunc (c *MCPServerConfig) IsEnabled() bool {\n\treturn c.Enabled == nil || *c.Enabled\n}\n\n// ExecApprovalCfg configures command execution approval (matching TS exec-approval.ts).\ntype ExecApprovalCfg struct {\n\tSecurity string `json:\"security,omitempty\"` // \"deny\", \"allowlist\", \"full\" (default \"full\")\n\tAsk string `json:\"ask,omitempty\"` // \"off\", \"on-miss\", \"always\" (default \"off\")\n\tAllowlist []string `json:\"allowlist,omitempty\"` // glob patterns for allowed commands\n}\n\n// WebFetchPolicyConfig controls domain filtering for the web_fetch tool.\ntype WebFetchPolicyConfig struct {\n\tPolicy string `json:\"policy,omitempty\"` // \"allow_all\" (default), \"allowlist\"\n\tAllowedDomains []string `json:\"allowed_domains,omitempty\"` // e.g. [\"github.com\", \"*.example.com\"]\n\tBlockedDomains []string `json:\"blocked_domains,omitempty\"` // always checked regardless of policy\n}\n\n// BrowserToolConfig controls the browser automation tool.\ntype BrowserToolConfig struct {\n\tEnabled bool `json:\"enabled\"` // enable the browser tool (default false)\n\tHeadless bool `json:\"headless,omitempty\"` // run Chrome in headless mode (ignored when RemoteURL is set)\n\tRemoteURL string `json:\"remote_url,omitempty\"` // CDP endpoint for remote Chrome sidecar, e.g. \"ws://chrome:9222\"\n}\n\n// ToolPolicySpec defines a tool policy at any level (global, per-agent, per-provider).\ntype ToolPolicySpec struct {\n\tProfile string `json:\"profile,omitempty\"`\n\tAllow []string `json:\"allow,omitempty\"`\n\tDeny []string `json:\"deny,omitempty\"`\n\tAlsoAllow []string `json:\"alsoAllow,omitempty\"`\n\tByProvider map[string]*ToolPolicySpec `json:\"byProvider,omitempty\"`\n}\n\ntype WebToolsConfig struct {\n\tBrave BraveConfig `json:\"brave\"`\n\tDuckDuckGo DuckDuckGoConfig `json:\"duckduckgo\"`\n}\n\ntype BraveConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tAPIKey string `json:\"api_key\"`\n\tMaxResults int `json:\"max_results\"`\n}\n\ntype DuckDuckGoConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tMaxResults int `json:\"max_results\"`\n}\n\n// SessionsConfig controls session behavior.\n// Matching TS src/config/sessions/types.ts + src/config/types.base.ts.\ntype SessionsConfig struct {\n\tScope string `json:\"scope,omitempty\"` // \"per-sender\" (default), \"global\"\n\tDmScope string `json:\"dm_scope,omitempty\"` // \"main\", \"per-peer\", \"per-channel-peer\" (default), \"per-account-channel-peer\"\n\tMainKey string `json:\"main_key,omitempty\"` // main session key suffix (default \"main\", used when dm_scope=\"main\")\n}\n\n// TtsConfig configures text-to-speech.\n// Matching TS src/config/types.tts.ts.\ntype TtsConfig struct {\n\tProvider string `json:\"provider,omitempty\"` // \"openai\", \"elevenlabs\", \"edge\", \"minimax\"\n\tAuto string `json:\"auto,omitempty\"` // \"off\" (default), \"always\", \"inbound\", \"tagged\"\n\tMode string `json:\"mode,omitempty\"` // \"final\" (default), \"all\"\n\tMaxLength int `json:\"max_length,omitempty\"` // max text length before truncation (default 1500)\n\tTimeoutMs int `json:\"timeout_ms,omitempty\"` // API timeout in ms (default 30000)\n\tOpenAI TtsOpenAIConfig `json:\"openai\"`\n\tElevenLabs TtsElevenLabsConfig `json:\"elevenlabs\"`\n\tEdge TtsEdgeConfig `json:\"edge\"`\n\tMiniMax TtsMiniMaxConfig `json:\"minimax\"`\n}\n\n// TtsOpenAIConfig configures the OpenAI TTS provider.\ntype TtsOpenAIConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"`\n\tAPIBase string `json:\"api_base,omitempty\"` // custom endpoint URL\n\tModel string `json:\"model,omitempty\"` // default \"gpt-4o-mini-tts\"\n\tVoice string `json:\"voice,omitempty\"` // default \"alloy\"\n}\n\n// TtsElevenLabsConfig configures the ElevenLabs TTS provider.\ntype TtsElevenLabsConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"`\n\tBaseURL string `json:\"base_url,omitempty\"`\n\tVoiceID string `json:\"voice_id,omitempty\"` // default \"pMsXgVXv3BLzUgSXRplE\"\n\tModelID string `json:\"model_id,omitempty\"` // default \"eleven_multilingual_v2\"\n}\n\n// TtsEdgeConfig configures the Microsoft Edge TTS provider (free, no API key).\ntype TtsEdgeConfig struct {\n\tEnabled bool `json:\"enabled,omitempty\"`\n\tVoice string `json:\"voice,omitempty\"` // default \"en-US-MichelleNeural\"\n\tRate string `json:\"rate,omitempty\"` // speech rate, e.g. \"+0%\"\n}\n\n// TtsMiniMaxConfig configures the MiniMax TTS provider.\ntype TtsMiniMaxConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"`\n\tGroupID string `json:\"group_id,omitempty\"` // MiniMax GroupId (required)\n\tAPIBase string `json:\"api_base,omitempty\"` // default \"https://api.minimax.io/v1\"\n\tModel string `json:\"model,omitempty\"` // default \"speech-02-hd\"\n\tVoiceID string `json:\"voice_id,omitempty\"` // default \"Wise_Woman\"\n}\n\n// MergeChannelGroupQuotas merges per-group quota overrides from channel configs\n// (e.g., channels.telegram.groups[chatID].quota) into gateway.quota.groups.\n// This allows per-group quotas to be set at the channel level and picked up\n// by the QuotaChecker at runtime.\nfunc MergeChannelGroupQuotas(cfg *Config) {\n\tif cfg.Gateway.Quota == nil {\n\t\treturn\n\t}\n\tif cfg.Gateway.Quota.Groups == nil {\n\t\tcfg.Gateway.Quota.Groups = make(map[string]QuotaWindow)\n\t}\n\n\t// Telegram per-group quotas\n\tfor chatID, groupCfg := range cfg.Channels.Telegram.Groups {\n\t\tif groupCfg != nil && groupCfg.Quota != nil && !groupCfg.Quota.IsZero() {\n\t\t\tkey := \"group:telegram:\" + chatID\n\t\t\tcfg.Gateway.Quota.Groups[key] = *groupCfg.Quota\n\t\t}\n\t}\n}\n"}, "dc51018563e5998b452e10e4b71719510df09b35": {"t": 1785759267.5148747, "n": 29410, "h": "3443663623352df8", "c": "package config\n\n// PendingCompactionConfig configures LLM-based compaction of pending group messages.\n// When a group accumulates more than Threshold pending messages, older messages are\n// summarized by an LLM and replaced with a compact summary, keeping KeepRecent raw messages.\ntype PendingCompactionConfig struct {\n\tThreshold int `json:\"threshold,omitempty\"` // trigger compaction when entries exceed this (default 50)\n\tKeepRecent int `json:\"keep_recent,omitempty\"` // keep this many recent raw messages after compaction (default 15)\n\tMaxTokens int `json:\"max_tokens,omitempty\"` // max output tokens for LLM summarization (default 4096)\n\tProvider string `json:\"provider,omitempty\"` // LLM provider name (e.g. \"openai\"); empty = use agent's provider\n\tModel string `json:\"model,omitempty\"` // model for summarization; empty = use agent's model\n}\n\n// ChannelsConfig contains per-channel configuration.\ntype ChannelsConfig struct {\n\tTelegram TelegramConfig `json:\"telegram\"`\n\tDiscord DiscordConfig `json:\"discord\"`\n\tSlack SlackConfig `json:\"slack\"`\n\tWhatsApp WhatsAppConfig `json:\"whatsapp\"`\n\tZalo ZaloConfig `json:\"zalo\"`\n\tZaloPersonal ZaloPersonalConfig `json:\"zalo_personal\"`\n\tFeishu FeishuConfig `json:\"feishu\"`\n\tPendingCompaction *PendingCompactionConfig `json:\"pending_compaction,omitempty\"` // global pending message compaction settings\n}\n\ntype TelegramConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tToken string `json:\"token\"`\n\tProxy string `json:\"proxy,omitempty\"`\n\tAPIServer string `json:\"api_server,omitempty\"` // custom Telegram Bot API server URL (e.g. \"http://localhost:8081\")\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in groups (default true)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tDMStream *bool `json:\"dm_stream,omitempty\"` // enable streaming for DMs (default false) — edits placeholder progressively\n\tGroupStream *bool `json:\"group_stream,omitempty\"` // enable streaming for groups (default false) — sends new message, edits progressively\n\tDraftTransport *bool `json:\"draft_transport,omitempty\"` // use sendMessageDraft for DM streaming (default true) — stealth preview, no notifications per edit\n\tReasoningStream *bool `json:\"reasoning_stream,omitempty\"` // show reasoning as separate message when provider emits thinking events (default true)\n\tReactionLevel string `json:\"reaction_level,omitempty\"` // \"off\" (default), \"minimal\", \"full\" — status emoji reactions\n\tMediaMaxBytes int64 `json:\"media_max_bytes,omitempty\"` // max media download size in bytes (default 20MB)\n\tLinkPreview *bool `json:\"link_preview,omitempty\"` // enable URL previews in messages (default true)\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\n\t// Optional STT (Speech-to-Text) pipeline for voice/audio inbound messages.\n\t// When stt_proxy_url is set, audio/voice messages are transcribed before being forwarded to the agent.\n\tSTTProxyURL string `json:\"stt_proxy_url,omitempty\"` // base URL of the STT proxy service (e.g. \"https://stt.example.com\")\n\tSTTAPIKey string `json:\"stt_api_key,omitempty\"` // Bearer token for the STT proxy\n\tSTTTenantID string `json:\"stt_tenant_id,omitempty\"` // optional tenant/org identifier forwarded to the STT proxy\n\tSTTTimeoutSeconds int `json:\"stt_timeout_seconds,omitempty\"` // per-request timeout for STT calls (default 30s)\n\n\t// Optional audio-aware routing: when set, voice/audio inbound messages are routed to this\n\t// agent instead of the default channel agent. Requires the named agent to exist in the config.\n\tVoiceAgentID string `json:\"voice_agent_id,omitempty\"` // agent ID to route voice inbound to (e.g. \"speaking-agent\")\n\n\t// Per-group (and per-topic) overrides. Key is chat ID string (e.g. \"-100123456\") or \"*\" for wildcard.\n\t// TS ref: channels.telegram.groups in src/config/types.telegram.ts.\n\tGroups map[string]*TelegramGroupConfig `json:\"groups,omitempty\"`\n}\n\n// TelegramGroupConfig defines per-group overrides for a Telegram channel.\n// Matching TS TelegramGroupConfig in src/config/types.telegram.ts.\ntype TelegramGroupConfig struct {\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // override group policy for this group\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // override require_mention for this group\n\tAllowFrom FlexibleStringSlice `json:\"allow_from,omitempty\"` // override allow_from for this group\n\tEnabled *bool `json:\"enabled,omitempty\"` // disable bot for this group (default: true)\n\tSkills []string `json:\"skills,omitempty\"` // skill whitelist (nil = all, [] = none)\n\tTools []string `json:\"tools,omitempty\"` // tool allow list (nil = all, supports \"group:xxx\")\n\tSystemPrompt string `json:\"system_prompt,omitempty\"` // extra system prompt for this group\n\tTopics map[string]*TelegramTopicConfig `json:\"topics,omitempty\"` // per-topic overrides (key: thread ID string)\n\tQuota *QuotaWindow `json:\"quota,omitempty\"` // per-group quota override\n}\n\n// TelegramTopicConfig defines per-topic overrides within a Telegram group.\n// Matching TS TelegramTopicConfig in src/config/types.telegram.ts.\ntype TelegramTopicConfig struct {\n\tRequireMention *bool `json:\"require_mention,omitempty\"`\n\tGroupPolicy string `json:\"group_policy,omitempty\"`\n\tSkills []string `json:\"skills,omitempty\"`\n\tTools []string `json:\"tools,omitempty\"` // tool allow list (nil = inherit, supports \"group:xxx\")\n\tEnabled *bool `json:\"enabled,omitempty\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from,omitempty\"`\n\tSystemPrompt string `json:\"system_prompt,omitempty\"`\n}\n\ntype DiscordConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tToken string `json:\"token\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in groups (default true)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tMediaMaxBytes int64 `json:\"media_max_bytes,omitempty\"` // max media download size (default 25MB)\n\tSTTProxyURL string `json:\"stt_proxy_url,omitempty\"`\n\tSTTAPIKey string `json:\"stt_api_key,omitempty\"`\n\tSTTTenantID string `json:\"stt_tenant_id,omitempty\"`\n\tSTTTimeoutSeconds int `json:\"stt_timeout_seconds,omitempty\"`\n\tVoiceAgentID string `json:\"voice_agent_id,omitempty\"`\n}\n\ntype SlackConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tBotToken string `json:\"bot_token\"` // xoxb-... (Bot User OAuth Token)\n\tAppToken string `json:\"app_token\"` // xapp-... (App-Level Token for Socket Mode)\n\tUserToken string `json:\"user_token,omitempty\"` // xoxp-... (Optional: custom bot identity)\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"pairing\", \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in channels (default true)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tDMStream *bool `json:\"dm_stream,omitempty\"` // enable streaming for DMs (default false)\n\tGroupStream *bool `json:\"group_stream,omitempty\"` // enable streaming for groups (default false)\n\tNativeStream *bool `json:\"native_stream,omitempty\"` // use Slack ChatStreamer API if available (default false)\n\tReactionLevel string `json:\"reaction_level,omitempty\"` // \"off\" (default), \"minimal\", \"full\"\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tDebounceDelay int `json:\"debounce_delay,omitempty\"` // ms delay before dispatching rapid messages (default 300, 0=disabled)\n\tThreadTTL *int `json:\"thread_ttl,omitempty\"` // hours before thread participation expires (default 24, 0=disabled — always require @mention)\n\tMediaMaxBytes int64 `json:\"media_max_bytes,omitempty\"` // max file download size in bytes (default 20MB)\n}\n\ntype WhatsAppConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tBridgeURL string `json:\"bridge_url\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n}\n\ntype ZaloConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tToken string `json:\"token\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tWebhookURL string `json:\"webhook_url,omitempty\"`\n\tWebhookSecret string `json:\"webhook_secret,omitempty\"`\n\tMediaMaxMB int `json:\"media_max_mb,omitempty\"` // default 5\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n}\n\ntype ZaloPersonalConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in groups (default true)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tCredentialsPath string `json:\"credentials_path,omitempty\"` // path to saved cookies JSON\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n}\n\ntype FeishuConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tAppID string `json:\"app_id\"`\n\tAppSecret string `json:\"app_secret\"`\n\tEncryptKey string `json:\"encrypt_key,omitempty\"`\n\tVerificationToken string `json:\"verification_token,omitempty\"`\n\tDomain string `json:\"domain,omitempty\"` // \"lark\" (default/global), \"feishu\" (China), or custom URL\n\tConnectionMode string `json:\"connection_mode,omitempty\"` // \"websocket\" (default), \"webhook\"\n\tWebhookPort int `json:\"webhook_port,omitempty\"` // default 3000\n\tWebhookPath string `json:\"webhook_path,omitempty\"` // default \"/feishu/events\"\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default)\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default)\n\tGroupAllowFrom FlexibleStringSlice `json:\"group_allow_from,omitempty\"`\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // default true (groups)\n\tTopicSessionMode string `json:\"topic_session_mode,omitempty\"` // \"disabled\" (default)\n\tTextChunkLimit int `json:\"text_chunk_limit,omitempty\"` // default 4000\n\tMediaMaxMB int `json:\"media_max_mb,omitempty\"` // default 30\n\tRenderMode string `json:\"render_mode,omitempty\"` // \"auto\", \"raw\", \"card\"\n\tStreaming *bool `json:\"streaming,omitempty\"` // default true\n\tReactionLevel string `json:\"reaction_level,omitempty\"` // \"off\" (default), \"minimal\", \"full\" — typing emoji reactions\n\tHistoryLimit int `json:\"history_limit,omitempty\"`\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tSTTProxyURL string `json:\"stt_proxy_url,omitempty\"`\n\tSTTAPIKey string `json:\"stt_api_key,omitempty\"`\n\tSTTTenantID string `json:\"stt_tenant_id,omitempty\"`\n\tSTTTimeoutSeconds int `json:\"stt_timeout_seconds,omitempty\"`\n\tVoiceAgentID string `json:\"voice_agent_id,omitempty\"`\n}\n\n// ProvidersConfig maps provider name to its config.\ntype ProvidersConfig struct {\n\tAnthropic ProviderConfig `json:\"anthropic\"`\n\tOpenAI ProviderConfig `json:\"openai\"`\n\tOpenRouter ProviderConfig `json:\"openrouter\"`\n\tGroq ProviderConfig `json:\"groq\"`\n\tGemini ProviderConfig `json:\"gemini\"`\n\tDeepSeek ProviderConfig `json:\"deepseek\"`\n\tMistral ProviderConfig `json:\"mistral\"`\n\tXAI ProviderConfig `json:\"xai\"`\n\tMiniMax ProviderConfig `json:\"minimax\"`\n\tCohere ProviderConfig `json:\"cohere\"`\n\tPerplexity ProviderConfig `json:\"perplexity\"`\n\tDashScope ProviderConfig `json:\"dashscope\"`\n\tBailian ProviderConfig `json:\"bailian\"`\n\tZai ProviderConfig `json:\"zai\"`\n\tZaiCoding ProviderConfig `json:\"zai_coding\"`\n\tOllama OllamaConfig `json:\"ollama\"` // local Ollama instance (no API key needed)\n\tOllamaCloud ProviderConfig `json:\"ollama_cloud\"` // Ollama Cloud (API key required)\n\tClaudeCLI ClaudeCLIConfig `json:\"claude_cli\"`\n\tACP ACPConfig `json:\"acp\"`\n}\n\n// OllamaConfig configures a local (or self-hosted) Ollama instance.\n// No API key is required — Ollama accepts any Bearer token value.\ntype OllamaConfig struct {\n\tHost string `json:\"host\"` // Ollama server base URL, e.g. http://localhost:11434\n}\n\n// ClaudeCLIConfig configures the Claude CLI provider (uses subscription, not API key).\ntype ClaudeCLIConfig struct {\n\tCLIPath string `json:\"cli_path\" yaml:\"cli_path\"` // path to claude binary (default: \"claude\")\n\tModel string `json:\"model\" yaml:\"model\"` // default model alias (default: \"sonnet\")\n\tBaseWorkDir string `json:\"base_work_dir\" yaml:\"base_work_dir\"` // base dir for agent workspaces\n\tPermMode string `json:\"perm_mode\" yaml:\"perm_mode\"` // permission mode (default: \"bypassPermissions\")\n}\n\n// ACPConfig configures the ACP (Agent Client Protocol) provider.\n// Orchestrates any ACP-compatible coding agent (Claude Code, Codex CLI, Gemini CLI) as a subprocess.\ntype ACPConfig struct {\n\tBinary string `json:\"binary\"` // agent binary name or path (e.g. \"claude\", \"codex\")\n\tArgs []string `json:\"args\"` // extra spawn args\n\tModel string `json:\"model\"` // default model/agent name\n\tWorkDir string `json:\"work_dir\"` // base workspace dir\n\tIdleTTL string `json:\"idle_ttl\"` // process idle TTL (e.g. \"5m\")\n\tPermMode string `json:\"perm_mode\"` // \"approve-all\" (default), \"approve-reads\", \"deny-all\"\n}\n\ntype ProviderConfig struct {\n\tAPIKey string `json:\"api_key\"`\n\tAPIBase string `json:\"api_base,omitempty\"`\n}\n\n// APIBaseForType returns the config-level api_base for a given provider type.\n// Used as a fallback when DB providers have no api_base set.\nfunc (p *ProvidersConfig) APIBaseForType(providerType string) string {\n\tswitch providerType {\n\tcase \"anthropic_native\":\n\t\treturn p.Anthropic.APIBase\n\tcase \"openai\", \"openai_compat\":\n\t\treturn p.OpenAI.APIBase\n\tcase \"openrouter\":\n\t\treturn p.OpenRouter.APIBase\n\tcase \"groq\":\n\t\treturn p.Groq.APIBase\n\tcase \"deepseek\":\n\t\treturn p.DeepSeek.APIBase\n\tcase \"gemini_native\":\n\t\treturn p.Gemini.APIBase\n\tcase \"mistral\":\n\t\treturn p.Mistral.APIBase\n\tcase \"xai\":\n\t\treturn p.XAI.APIBase\n\tcase \"minimax_native\":\n\t\treturn p.MiniMax.APIBase\n\tcase \"cohere\":\n\t\treturn p.Cohere.APIBase\n\tcase \"perplexity\":\n\t\treturn p.Perplexity.APIBase\n\tcase \"dashscope\":\n\t\treturn p.DashScope.APIBase\n\tcase \"bailian\":\n\t\treturn p.Bailian.APIBase\n\tcase \"zai\":\n\t\treturn p.Zai.APIBase\n\tcase \"zai_coding\":\n\t\treturn p.ZaiCoding.APIBase\n\tcase \"ollama_cloud\":\n\t\treturn p.OllamaCloud.APIBase\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\n// HasAnyProvider returns true if at least one provider has an API key or CLI configured.\nfunc (c *Config) HasAnyProvider() bool {\n\tp := c.Providers\n\treturn p.Anthropic.APIKey != \"\" ||\n\t\tp.OpenAI.APIKey != \"\" ||\n\t\tp.OpenRouter.APIKey != \"\" ||\n\t\tp.Groq.APIKey != \"\" ||\n\t\tp.Gemini.APIKey != \"\" ||\n\t\tp.DeepSeek.APIKey != \"\" ||\n\t\tp.Mistral.APIKey != \"\" ||\n\t\tp.XAI.APIKey != \"\" ||\n\t\tp.MiniMax.APIKey != \"\" ||\n\t\tp.Cohere.APIKey != \"\" ||\n\t\tp.Perplexity.APIKey != \"\" ||\n\t\tp.DashScope.APIKey != \"\" ||\n\t\tp.Bailian.APIKey != \"\" ||\n\t\tp.Zai.APIKey != \"\" ||\n\t\tp.ZaiCoding.APIKey != \"\" ||\n\t\tp.Ollama.Host != \"\" ||\n\t\tp.OllamaCloud.APIKey != \"\" ||\n\t\tp.ClaudeCLI.CLIPath != \"\" ||\n\t\tp.ACP.Binary != \"\"\n}\n\n// QuotaWindow defines request limits per time window. Zero means unlimited.\ntype QuotaWindow struct {\n\tHour int `json:\"hour,omitempty\"` // max requests per hour (0 = unlimited)\n\tDay int `json:\"day,omitempty\"` // max requests per day (0 = unlimited)\n\tWeek int `json:\"week,omitempty\"` // max requests per week (0 = unlimited)\n}\n\n// IsZero returns true if no limits are set.\nfunc (w QuotaWindow) IsZero() bool { return w.Hour == 0 && w.Day == 0 && w.Week == 0 }\n\n// QuotaConfig configures per-user/group request quotas.\n// Config merge priority: Groups > Channels > Providers > Default.\ntype QuotaConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tDefault QuotaWindow `json:\"default\"`\n\tProviders map[string]QuotaWindow `json:\"providers,omitempty\"` // key = provider name (e.g. \"anthropic\")\n\tChannels map[string]QuotaWindow `json:\"channels,omitempty\"` // key = channel name (e.g. \"telegram\")\n\tGroups map[string]QuotaWindow `json:\"groups,omitempty\"` // key = userID (e.g. \"group:telegram:-100123\")\n}\n\n// GatewayConfig controls the gateway server.\ntype GatewayConfig struct {\n\tHost string `json:\"host\"`\n\tPort int `json:\"port\"`\n\tToken string `json:\"token,omitempty\"` // bearer token for WS/HTTP auth\n\tOwnerIDs []string `json:\"owner_ids,omitempty\"` // sender IDs considered \"owner\"\n\tAllowedOrigins []string `json:\"allowed_origins,omitempty\"` // WebSocket CORS whitelist (empty = allow all)\n\tMaxMessageChars int `json:\"max_message_chars,omitempty\"` // max user message characters (default 32000)\n\tRateLimitRPM int `json:\"rate_limit_rpm,omitempty\"` // rate limit: requests per minute per user (default 20, 0 = disabled)\n\tInjectionAction string `json:\"injection_action,omitempty\"` // prompt injection action: \"log\", \"warn\" (default), \"block\", \"off\"\n\tInboundDebounceMs int `json:\"inbound_debounce_ms,omitempty\"` // merge rapid messages from same sender (default 1000ms, -1 = disabled)\n\tQuota *QuotaConfig `json:\"quota,omitempty\"` // per-user/group request quotas\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // deliver intermediate text during tool iterations (default false)\n\tToolStatus *bool `json:\"tool_status,omitempty\"` // show tool name in streaming preview during tool execution (default true)\n\tTaskRecoveryIntervalSec int `json:\"task_recovery_interval_sec,omitempty\"` // team task recovery ticker interval in seconds (default 300 = 5min)\n}\n\n// ToolsConfig controls tool availability, policy, and web search.\ntype ToolsConfig struct {\n\tProfile string `json:\"profile,omitempty\"` // global profile: \"minimal\", \"coding\", \"messaging\", \"full\"\n\tAllow []string `json:\"allow,omitempty\"` // global allow list (tool names or \"group:xxx\")\n\tDeny []string `json:\"deny,omitempty\"` // global deny list\n\tAlsoAllow []string `json:\"alsoAllow,omitempty\"` // additive: adds without removing existing\n\tByProvider map[string]*ToolPolicySpec `json:\"byProvider,omitempty\"` // per-provider overrides\n\tExecApproval ExecApprovalCfg `json:\"execApproval\"` // exec command approval settings\n\tWebFetch WebFetchPolicyConfig `json:\"web_fetch\"` // domain policy for URL fetching\n\tWeb WebToolsConfig `json:\"web\"`\n\tBrowser BrowserToolConfig `json:\"browser\"`\n\tRateLimitPerHour int `json:\"rate_limit_per_hour,omitempty\"` // max tool executions per hour per session (0 = disabled)\n\tScrubCredentials *bool `json:\"scrub_credentials,omitempty\"` // auto-redact API keys/tokens in tool output (default true)\n\tMcpServers map[string]*MCPServerConfig `json:\"mcp_servers,omitempty\"` // external MCP server connections\n}\n\n// MCPServerConfig configures a single external MCP server connection.\ntype MCPServerConfig struct {\n\tTransport string `json:\"transport\"` // \"stdio\", \"sse\", \"streamable-http\"\n\tCommand string `json:\"command,omitempty\"` // stdio: command to spawn\n\tArgs []string `json:\"args,omitempty\"` // stdio: command arguments\n\tEnv map[string]string `json:\"env,omitempty\"` // stdio: extra environment variables\n\tURL string `json:\"url,omitempty\"` // sse/http: server URL\n\tHeaders map[string]string `json:\"headers,omitempty\"` // sse/http: extra HTTP headers\n\tEnabled *bool `json:\"enabled,omitempty\"` // default true\n\tToolPrefix string `json:\"tool_prefix,omitempty\"` // prefix for tool names (avoids collisions)\n\tTimeoutSec int `json:\"timeout_sec,omitempty\"` // per-tool-call timeout in seconds (default 60)\n}\n\n// IsEnabled returns whether this MCP server is enabled (default true).\nfunc (c *MCPServerConfig) IsEnabled() bool {\n\treturn c.Enabled == nil || *c.Enabled\n}\n\n// ExecApprovalCfg configures command execution approval (matching TS exec-approval.ts).\ntype ExecApprovalCfg struct {\n\tSecurity string `json:\"security,omitempty\"` // \"deny\", \"allowlist\", \"full\" (default \"full\")\n\tAsk string `json:\"ask,omitempty\"` // \"off\", \"on-miss\", \"always\" (default \"off\")\n\tAllowlist []string `json:\"allowlist,omitempty\"` // glob patterns for allowed commands\n}\n\n// WebFetchPolicyConfig controls domain filtering for the web_fetch tool.\ntype WebFetchPolicyConfig struct {\n\tPolicy string `json:\"policy,omitempty\"` // \"allow_all\" (default), \"allowlist\"\n\tAllowedDomains []string `json:\"allowed_domains,omitempty\"` // e.g. [\"github.com\", \"*.example.com\"]\n\tBlockedDomains []string `json:\"blocked_domains,omitempty\"` // always checked regardless of policy\n}\n\n// BrowserToolConfig controls the browser automation tool.\ntype BrowserToolConfig struct {\n\tEnabled bool `json:\"enabled\"` // enable the browser tool (default false)\n\tHeadless bool `json:\"headless,omitempty\"` // run Chrome in headless mode (ignored when RemoteURL is set)\n\tRemoteURL string `json:\"remote_url,omitempty\"` // CDP endpoint for remote Chrome sidecar, e.g. \"ws://chrome:9222\"\n}\n\n// ToolPolicySpec defines a tool policy at any level (global, per-agent, per-provider).\ntype ToolPolicySpec struct {\n\tProfile string `json:\"profile,omitempty\"`\n\tAllow []string `json:\"allow,omitempty\"`\n\tDeny []string `json:\"deny,omitempty\"`\n\tAlsoAllow []string `json:\"alsoAllow,omitempty\"`\n\tByProvider map[string]*ToolPolicySpec `json:\"byProvider,omitempty\"`\n}\n\ntype WebToolsConfig struct {\n\tBrave BraveConfig `json:\"brave\"`\n\tDuckDuckGo DuckDuckGoConfig `json:\"duckduckgo\"`\n}\n\ntype BraveConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tAPIKey string `json:\"api_key\"`\n\tMaxResults int `json:\"max_results\"`\n}\n\ntype DuckDuckGoConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tMaxResults int `json:\"max_results\"`\n}\n\n// SessionsConfig controls session behavior.\n// Matching TS src/config/sessions/types.ts + src/config/types.base.ts.\ntype SessionsConfig struct {\n\tScope string `json:\"scope,omitempty\"` // \"per-sender\" (default), \"global\"\n\tDmScope string `json:\"dm_scope,omitempty\"` // \"main\", \"per-peer\", \"per-channel-peer\" (default), \"per-account-channel-peer\"\n\tMainKey string `json:\"main_key,omitempty\"` // main session key suffix (default \"main\", used when dm_scope=\"main\")\n}\n\n// TtsConfig configures text-to-speech.\n// Matching TS src/config/types.tts.ts.\ntype TtsConfig struct {\n\tProvider string `json:\"provider,omitempty\"` // \"openai\", \"elevenlabs\", \"edge\", \"minimax\"\n\tAuto string `json:\"auto,omitempty\"` // \"off\" (default), \"always\", \"inbound\", \"tagged\"\n\tMode string `json:\"mode,omitempty\"` // \"final\" (default), \"all\"\n\tMaxLength int `json:\"max_length,omitempty\"` // max text length before truncation (default 1500)\n\tTimeoutMs int `json:\"timeout_ms,omitempty\"` // API timeout in ms (default 30000)\n\tOpenAI TtsOpenAIConfig `json:\"openai\"`\n\tElevenLabs TtsElevenLabsConfig `json:\"elevenlabs\"`\n\tEdge TtsEdgeConfig `json:\"edge\"`\n\tMiniMax TtsMiniMaxConfig `json:\"minimax\"`\n}\n\n// TtsOpenAIConfig configures the OpenAI TTS provider.\ntype TtsOpenAIConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"`\n\tAPIBase string `json:\"api_base,omitempty\"` // custom endpoint URL\n\tModel string `json:\"model,omitempty\"` // default \"gpt-4o-mini-tts\"\n\tVoice string `json:\"voice,omitempty\"` // default \"alloy\"\n}\n\n// TtsElevenLabsConfig configures the ElevenLabs TTS provider.\ntype TtsElevenLabsConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"`\n\tBaseURL string `json:\"base_url,omitempty\"`\n\tVoiceID string `json:\"voice_id,omitempty\"` // default \"pMsXgVXv3BLzUgSXRplE\"\n\tModelID string `json:\"model_id,omitempty\"` // default \"eleven_multilingual_v2\"\n}\n\n// TtsEdgeConfig configures the Microsoft Edge TTS provider (free, no API key).\ntype TtsEdgeConfig struct {\n\tEnabled bool `json:\"enabled,omitempty\"`\n\tVoice string `json:\"voice,omitempty\"` // default \"en-US-MichelleNeural\"\n\tRate string `json:\"rate,omitempty\"` // speech rate, e.g. \"+0%\"\n}\n\n// TtsMiniMaxConfig configures the MiniMax TTS provider.\ntype TtsMiniMaxConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"`\n\tGroupID string `json:\"group_id,omitempty\"` // MiniMax GroupId (required)\n\tAPIBase string `json:\"api_base,omitempty\"` // default \"https://api.minimax.io/v1\"\n\tModel string `json:\"model,omitempty\"` // default \"speech-02-hd\"\n\tVoiceID string `json:\"voice_id,omitempty\"` // default \"Wise_Woman\"\n}\n\n// MergeChannelGroupQuotas merges per-group quota overrides from channel configs\n// (e.g., channels.telegram.groups[chatID].quota) into gateway.quota.groups.\n// This allows per-group quotas to be set at the channel level and picked up\n// by the QuotaChecker at runtime.\nfunc MergeChannelGroupQuotas(cfg *Config) {\n\tif cfg.Gateway.Quota == nil {\n\t\treturn\n\t}\n\tif cfg.Gateway.Quota.Groups == nil {\n\t\tcfg.Gateway.Quota.Groups = make(map[string]QuotaWindow)\n\t}\n\n\t// Telegram per-group quotas\n\tfor chatID, groupCfg := range cfg.Channels.Telegram.Groups {\n\t\tif groupCfg != nil && groupCfg.Quota != nil && !groupCfg.Quota.IsZero() {\n\t\t\tkey := \"group:telegram:\" + chatID\n\t\t\tcfg.Gateway.Quota.Groups[key] = *groupCfg.Quota\n\t\t}\n\t}\n}\n"}, "84b1b07634104df4074b52750bd7b7e049499e8e": {"t": 1785759268.9068842, "n": 28397, "h": "c0e56082d9b34fed", "c": "package config\n\n// PendingCompactionConfig configures LLM-based compaction of pending group messages.\n// When a group accumulates more than Threshold pending messages, older messages are\n// summarized by an LLM and replaced with a compact summary, keeping KeepRecent raw messages.\ntype PendingCompactionConfig struct {\n\tThreshold int `json:\"threshold,omitempty\"` // trigger compaction when entries exceed this (default 50)\n\tKeepRecent int `json:\"keep_recent,omitempty\"` // keep this many recent raw messages after compaction (default 15)\n\tMaxTokens int `json:\"max_tokens,omitempty\"` // max output tokens for LLM summarization (default 4096)\n\tProvider string `json:\"provider,omitempty\"` // LLM provider name (e.g. \"openai\"); empty = use agent's provider\n\tModel string `json:\"model,omitempty\"` // model for summarization; empty = use agent's model\n}\n\n// ChannelsConfig contains per-channel configuration.\ntype ChannelsConfig struct {\n\tTelegram TelegramConfig `json:\"telegram\"`\n\tDiscord DiscordConfig `json:\"discord\"`\n\tSlack SlackConfig `json:\"slack\"`\n\tWhatsApp WhatsAppConfig `json:\"whatsapp\"`\n\tZalo ZaloConfig `json:\"zalo\"`\n\tZaloPersonal ZaloPersonalConfig `json:\"zalo_personal\"`\n\tFeishu FeishuConfig `json:\"feishu\"`\n\tPendingCompaction *PendingCompactionConfig `json:\"pending_compaction,omitempty\"` // global pending message compaction settings\n}\n\ntype TelegramConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tToken string `json:\"token\"`\n\tProxy string `json:\"proxy,omitempty\"`\n\tAPIServer string `json:\"api_server,omitempty\"` // custom Telegram Bot API server URL (e.g. \"http://localhost:8081\")\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in groups (default true)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tDMStream *bool `json:\"dm_stream,omitempty\"` // enable streaming for DMs (default false) — edits placeholder progressively\n\tGroupStream *bool `json:\"group_stream,omitempty\"` // enable streaming for groups (default false) — sends new message, edits progressively\n\tDraftTransport *bool `json:\"draft_transport,omitempty\"` // use sendMessageDraft for DM streaming (default true) — stealth preview, no notifications per edit\n\tReasoningStream *bool `json:\"reasoning_stream,omitempty\"` // show reasoning as separate message when provider emits thinking events (default true)\n\tReactionLevel string `json:\"reaction_level,omitempty\"` // \"off\" (default), \"minimal\", \"full\" — status emoji reactions\n\tMediaMaxBytes int64 `json:\"media_max_bytes,omitempty\"` // max media download size in bytes (default 20MB)\n\tLinkPreview *bool `json:\"link_preview,omitempty\"` // enable URL previews in messages (default true)\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\n\t// Optional STT (Speech-to-Text) pipeline for voice/audio inbound messages.\n\t// When stt_proxy_url is set, audio/voice messages are transcribed before being forwarded to the agent.\n\tSTTProxyURL string `json:\"stt_proxy_url,omitempty\"` // base URL of the STT proxy service (e.g. \"https://stt.example.com\")\n\tSTTAPIKey string `json:\"stt_api_key,omitempty\"` // Bearer token for the STT proxy\n\tSTTTenantID string `json:\"stt_tenant_id,omitempty\"` // optional tenant/org identifier forwarded to the STT proxy\n\tSTTTimeoutSeconds int `json:\"stt_timeout_seconds,omitempty\"` // per-request timeout for STT calls (default 30s)\n\n\t// Optional audio-aware routing: when set, voice/audio inbound messages are routed to this\n\t// agent instead of the default channel agent. Requires the named agent to exist in the config.\n\tVoiceAgentID string `json:\"voice_agent_id,omitempty\"` // agent ID to route voice inbound to (e.g. \"speaking-agent\")\n\n\t// Per-group (and per-topic) overrides. Key is chat ID string (e.g. \"-100123456\") or \"*\" for wildcard.\n\t// TS ref: channels.telegram.groups in src/config/types.telegram.ts.\n\tGroups map[string]*TelegramGroupConfig `json:\"groups,omitempty\"`\n}\n\n// TelegramGroupConfig defines per-group overrides for a Telegram channel.\n// Matching TS TelegramGroupConfig in src/config/types.telegram.ts.\ntype TelegramGroupConfig struct {\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // override group policy for this group\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // override require_mention for this group\n\tAllowFrom FlexibleStringSlice `json:\"allow_from,omitempty\"` // override allow_from for this group\n\tEnabled *bool `json:\"enabled,omitempty\"` // disable bot for this group (default: true)\n\tSkills []string `json:\"skills,omitempty\"` // skill whitelist (nil = all, [] = none)\n\tTools []string `json:\"tools,omitempty\"` // tool allow list (nil = all, supports \"group:xxx\")\n\tSystemPrompt string `json:\"system_prompt,omitempty\"` // extra system prompt for this group\n\tTopics map[string]*TelegramTopicConfig `json:\"topics,omitempty\"` // per-topic overrides (key: thread ID string)\n\tQuota *QuotaWindow `json:\"quota,omitempty\"` // per-group quota override\n}\n\n// TelegramTopicConfig defines per-topic overrides within a Telegram group.\n// Matching TS TelegramTopicConfig in src/config/types.telegram.ts.\ntype TelegramTopicConfig struct {\n\tRequireMention *bool `json:\"require_mention,omitempty\"`\n\tGroupPolicy string `json:\"group_policy,omitempty\"`\n\tSkills []string `json:\"skills,omitempty\"`\n\tTools []string `json:\"tools,omitempty\"` // tool allow list (nil = inherit, supports \"group:xxx\")\n\tEnabled *bool `json:\"enabled,omitempty\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from,omitempty\"`\n\tSystemPrompt string `json:\"system_prompt,omitempty\"`\n}\n\ntype DiscordConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tToken string `json:\"token\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in groups (default true)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tMediaMaxBytes int64 `json:\"media_max_bytes,omitempty\"` // max media download size (default 25MB)\n\tSTTProxyURL string `json:\"stt_proxy_url,omitempty\"`\n\tSTTAPIKey string `json:\"stt_api_key,omitempty\"`\n\tSTTTenantID string `json:\"stt_tenant_id,omitempty\"`\n\tSTTTimeoutSeconds int `json:\"stt_timeout_seconds,omitempty\"`\n\tVoiceAgentID string `json:\"voice_agent_id,omitempty\"`\n}\n\ntype SlackConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tBotToken string `json:\"bot_token\"` // xoxb-... (Bot User OAuth Token)\n\tAppToken string `json:\"app_token\"` // xapp-... (App-Level Token for Socket Mode)\n\tUserToken string `json:\"user_token,omitempty\"` // xoxp-... (Optional: custom bot identity)\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"pairing\", \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in channels (default true)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tDMStream *bool `json:\"dm_stream,omitempty\"` // enable streaming for DMs (default false)\n\tGroupStream *bool `json:\"group_stream,omitempty\"` // enable streaming for groups (default false)\n\tNativeStream *bool `json:\"native_stream,omitempty\"` // use Slack ChatStreamer API if available (default false)\n\tReactionLevel string `json:\"reaction_level,omitempty\"` // \"off\" (default), \"minimal\", \"full\"\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tDebounceDelay int `json:\"debounce_delay,omitempty\"` // ms delay before dispatching rapid messages (default 300, 0=disabled)\n\tThreadTTL *int `json:\"thread_ttl,omitempty\"` // hours before thread participation expires (default 24, 0=disabled — always require @mention)\n\tMediaMaxBytes int64 `json:\"media_max_bytes,omitempty\"` // max file download size in bytes (default 20MB)\n}\n\ntype WhatsAppConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tBridgeURL string `json:\"bridge_url\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n}\n\ntype ZaloConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tToken string `json:\"token\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tWebhookURL string `json:\"webhook_url,omitempty\"`\n\tWebhookSecret string `json:\"webhook_secret,omitempty\"`\n\tMediaMaxMB int `json:\"media_max_mb,omitempty\"` // default 5\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n}\n\ntype ZaloPersonalConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in groups (default true)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tCredentialsPath string `json:\"credentials_path,omitempty\"` // path to saved cookies JSON\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n}\n\ntype FeishuConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tAppID string `json:\"app_id\"`\n\tAppSecret string `json:\"app_secret\"`\n\tEncryptKey string `json:\"encrypt_key,omitempty\"`\n\tVerificationToken string `json:\"verification_token,omitempty\"`\n\tDomain string `json:\"domain,omitempty\"` // \"lark\" (default/global), \"feishu\" (China), or custom URL\n\tConnectionMode string `json:\"connection_mode,omitempty\"` // \"websocket\" (default), \"webhook\"\n\tWebhookPort int `json:\"webhook_port,omitempty\"` // default 3000\n\tWebhookPath string `json:\"webhook_path,omitempty\"` // default \"/feishu/events\"\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default)\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default)\n\tGroupAllowFrom FlexibleStringSlice `json:\"group_allow_from,omitempty\"`\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // default true (groups)\n\tTopicSessionMode string `json:\"topic_session_mode,omitempty\"` // \"disabled\" (default)\n\tTextChunkLimit int `json:\"text_chunk_limit,omitempty\"` // default 4000\n\tMediaMaxMB int `json:\"media_max_mb,omitempty\"` // default 30\n\tRenderMode string `json:\"render_mode,omitempty\"` // \"auto\", \"raw\", \"card\"\n\tStreaming *bool `json:\"streaming,omitempty\"` // default true\n\tReactionLevel string `json:\"reaction_level,omitempty\"` // \"off\" (default), \"minimal\", \"full\" — typing emoji reactions\n\tHistoryLimit int `json:\"history_limit,omitempty\"`\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tSTTProxyURL string `json:\"stt_proxy_url,omitempty\"`\n\tSTTAPIKey string `json:\"stt_api_key,omitempty\"`\n\tSTTTenantID string `json:\"stt_tenant_id,omitempty\"`\n\tSTTTimeoutSeconds int `json:\"stt_timeout_seconds,omitempty\"`\n\tVoiceAgentID string `json:\"voice_agent_id,omitempty\"`\n}\n\n// ProvidersConfig maps provider name to its config.\ntype ProvidersConfig struct {\n\tAnthropic ProviderConfig `json:\"anthropic\"`\n\tOpenAI ProviderConfig `json:\"openai\"`\n\tOpenRouter ProviderConfig `json:\"openrouter\"`\n\tGroq ProviderConfig `json:\"groq\"`\n\tGemini ProviderConfig `json:\"gemini\"`\n\tDeepSeek ProviderConfig `json:\"deepseek\"`\n\tMistral ProviderConfig `json:\"mistral\"`\n\tXAI ProviderConfig `json:\"xai\"`\n\tMiniMax ProviderConfig `json:\"minimax\"`\n\tCohere ProviderConfig `json:\"cohere\"`\n\tPerplexity ProviderConfig `json:\"perplexity\"`\n\tDashScope ProviderConfig `json:\"dashscope\"`\n\tBailian ProviderConfig `json:\"bailian\"`\n\tZai ProviderConfig `json:\"zai\"`\n\tZaiCoding ProviderConfig `json:\"zai_coding\"`\n\tOllama OllamaConfig `json:\"ollama\"` // local Ollama instance (no API key needed)\n\tOllamaCloud ProviderConfig `json:\"ollama_cloud\"` // Ollama Cloud (API key required)\n\tClaudeCLI ClaudeCLIConfig `json:\"claude_cli\"`\n\tACP ACPConfig `json:\"acp\"`\n}\n\n// OllamaConfig configures a local (or self-hosted) Ollama instance.\n// No API key is required — Ollama accepts any Bearer token value.\ntype OllamaConfig struct {\n\tHost string `json:\"host\"` // Ollama server base URL, e.g. http://localhost:11434\n}\n\n// ClaudeCLIConfig configures the Claude CLI provider (uses subscription, not API key).\ntype ClaudeCLIConfig struct {\n\tCLIPath string `json:\"cli_path\" yaml:\"cli_path\"` // path to claude binary (default: \"claude\")\n\tModel string `json:\"model\" yaml:\"model\"` // default model alias (default: \"sonnet\")\n\tBaseWorkDir string `json:\"base_work_dir\" yaml:\"base_work_dir\"` // base dir for agent workspaces\n\tPermMode string `json:\"perm_mode\" yaml:\"perm_mode\"` // permission mode (default: \"bypassPermissions\")\n}\n\n// ACPConfig configures the ACP (Agent Client Protocol) provider.\n// Orchestrates any ACP-compatible coding agent (Claude Code, Codex CLI, Gemini CLI) as a subprocess.\ntype ACPConfig struct {\n\tBinary string `json:\"binary\"` // agent binary name or path (e.g. \"claude\", \"codex\")\n\tArgs []string `json:\"args\"` // extra spawn args\n\tModel string `json:\"model\"` // default model/agent name\n\tWorkDir string `json:\"work_dir\"` // base workspace dir\n\tIdleTTL string `json:\"idle_ttl\"` // process idle TTL (e.g. \"5m\")\n\tPermMode string `json:\"perm_mode\"` // \"approve-all\" (default), \"approve-reads\", \"deny-all\"\n}\n\ntype ProviderConfig struct {\n\tAPIKey string `json:\"api_key\"`\n\tAPIBase string `json:\"api_base,omitempty\"`\n}\n\n// HasAnyProvider returns true if at least one provider has an API key or CLI configured.\nfunc (c *Config) HasAnyProvider() bool {\n\tp := c.Providers\n\treturn p.Anthropic.APIKey != \"\" ||\n\t\tp.OpenAI.APIKey != \"\" ||\n\t\tp.OpenRouter.APIKey != \"\" ||\n\t\tp.Groq.APIKey != \"\" ||\n\t\tp.Gemini.APIKey != \"\" ||\n\t\tp.DeepSeek.APIKey != \"\" ||\n\t\tp.Mistral.APIKey != \"\" ||\n\t\tp.XAI.APIKey != \"\" ||\n\t\tp.MiniMax.APIKey != \"\" ||\n\t\tp.Cohere.APIKey != \"\" ||\n\t\tp.Perplexity.APIKey != \"\" ||\n\t\tp.DashScope.APIKey != \"\" ||\n\t\tp.Bailian.APIKey != \"\" ||\n\t\tp.Zai.APIKey != \"\" ||\n\t\tp.ZaiCoding.APIKey != \"\" ||\n\t\tp.Ollama.Host != \"\" ||\n\t\tp.OllamaCloud.APIKey != \"\" ||\n\t\tp.ClaudeCLI.CLIPath != \"\" ||\n\t\tp.ACP.Binary != \"\"\n}\n\n// QuotaWindow defines request limits per time window. Zero means unlimited.\ntype QuotaWindow struct {\n\tHour int `json:\"hour,omitempty\"` // max requests per hour (0 = unlimited)\n\tDay int `json:\"day,omitempty\"` // max requests per day (0 = unlimited)\n\tWeek int `json:\"week,omitempty\"` // max requests per week (0 = unlimited)\n}\n\n// IsZero returns true if no limits are set.\nfunc (w QuotaWindow) IsZero() bool { return w.Hour == 0 && w.Day == 0 && w.Week == 0 }\n\n// QuotaConfig configures per-user/group request quotas.\n// Config merge priority: Groups > Channels > Providers > Default.\ntype QuotaConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tDefault QuotaWindow `json:\"default\"`\n\tProviders map[string]QuotaWindow `json:\"providers,omitempty\"` // key = provider name (e.g. \"anthropic\")\n\tChannels map[string]QuotaWindow `json:\"channels,omitempty\"` // key = channel name (e.g. \"telegram\")\n\tGroups map[string]QuotaWindow `json:\"groups,omitempty\"` // key = userID (e.g. \"group:telegram:-100123\")\n}\n\n// GatewayConfig controls the gateway server.\ntype GatewayConfig struct {\n\tHost string `json:\"host\"`\n\tPort int `json:\"port\"`\n\tToken string `json:\"token,omitempty\"` // bearer token for WS/HTTP auth\n\tOwnerIDs []string `json:\"owner_ids,omitempty\"` // sender IDs considered \"owner\"\n\tAllowedOrigins []string `json:\"allowed_origins,omitempty\"` // WebSocket CORS whitelist (empty = allow all)\n\tMaxMessageChars int `json:\"max_message_chars,omitempty\"` // max user message characters (default 32000)\n\tRateLimitRPM int `json:\"rate_limit_rpm,omitempty\"` // rate limit: requests per minute per user (default 20, 0 = disabled)\n\tInjectionAction string `json:\"injection_action,omitempty\"` // prompt injection action: \"log\", \"warn\" (default), \"block\", \"off\"\n\tInboundDebounceMs int `json:\"inbound_debounce_ms,omitempty\"` // merge rapid messages from same sender (default 1000ms, -1 = disabled)\n\tQuota *QuotaConfig `json:\"quota,omitempty\"` // per-user/group request quotas\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // deliver intermediate text during tool iterations (default false)\n\tToolStatus *bool `json:\"tool_status,omitempty\"` // show tool name in streaming preview during tool execution (default true)\n\tTaskRecoveryIntervalSec int `json:\"task_recovery_interval_sec,omitempty\"` // team task recovery ticker interval in seconds (default 300 = 5min)\n}\n\n// ToolsConfig controls tool availability, policy, and web search.\ntype ToolsConfig struct {\n\tProfile string `json:\"profile,omitempty\"` // global profile: \"minimal\", \"coding\", \"messaging\", \"full\"\n\tAllow []string `json:\"allow,omitempty\"` // global allow list (tool names or \"group:xxx\")\n\tDeny []string `json:\"deny,omitempty\"` // global deny list\n\tAlsoAllow []string `json:\"alsoAllow,omitempty\"` // additive: adds without removing existing\n\tByProvider map[string]*ToolPolicySpec `json:\"byProvider,omitempty\"` // per-provider overrides\n\tExecApproval ExecApprovalCfg `json:\"execApproval\"` // exec command approval settings\n\tWebFetch WebFetchPolicyConfig `json:\"web_fetch\"` // domain policy for URL fetching\n\tWeb WebToolsConfig `json:\"web\"`\n\tBrowser BrowserToolConfig `json:\"browser\"`\n\tRateLimitPerHour int `json:\"rate_limit_per_hour,omitempty\"` // max tool executions per hour per session (0 = disabled)\n\tScrubCredentials *bool `json:\"scrub_credentials,omitempty\"` // auto-redact API keys/tokens in tool output (default true)\n\tMcpServers map[string]*MCPServerConfig `json:\"mcp_servers,omitempty\"` // external MCP server connections\n}\n\n// MCPServerConfig configures a single external MCP server connection.\ntype MCPServerConfig struct {\n\tTransport string `json:\"transport\"` // \"stdio\", \"sse\", \"streamable-http\"\n\tCommand string `json:\"command,omitempty\"` // stdio: command to spawn\n\tArgs []string `json:\"args,omitempty\"` // stdio: command arguments\n\tEnv map[string]string `json:\"env,omitempty\"` // stdio: extra environment variables\n\tURL string `json:\"url,omitempty\"` // sse/http: server URL\n\tHeaders map[string]string `json:\"headers,omitempty\"` // sse/http: extra HTTP headers\n\tEnabled *bool `json:\"enabled,omitempty\"` // default true\n\tToolPrefix string `json:\"tool_prefix,omitempty\"` // prefix for tool names (avoids collisions)\n\tTimeoutSec int `json:\"timeout_sec,omitempty\"` // per-tool-call timeout in seconds (default 60)\n}\n\n// IsEnabled returns whether this MCP server is enabled (default true).\nfunc (c *MCPServerConfig) IsEnabled() bool {\n\treturn c.Enabled == nil || *c.Enabled\n}\n\n// ExecApprovalCfg configures command execution approval (matching TS exec-approval.ts).\ntype ExecApprovalCfg struct {\n\tSecurity string `json:\"security,omitempty\"` // \"deny\", \"allowlist\", \"full\" (default \"full\")\n\tAsk string `json:\"ask,omitempty\"` // \"off\", \"on-miss\", \"always\" (default \"off\")\n\tAllowlist []string `json:\"allowlist,omitempty\"` // glob patterns for allowed commands\n}\n\n// WebFetchPolicyConfig controls domain filtering for the web_fetch tool.\ntype WebFetchPolicyConfig struct {\n\tPolicy string `json:\"policy,omitempty\"` // \"allow_all\" (default), \"allowlist\"\n\tAllowedDomains []string `json:\"allowed_domains,omitempty\"` // e.g. [\"github.com\", \"*.example.com\"]\n\tBlockedDomains []string `json:\"blocked_domains,omitempty\"` // always checked regardless of policy\n}\n\n// BrowserToolConfig controls the browser automation tool.\ntype BrowserToolConfig struct {\n\tEnabled bool `json:\"enabled\"` // enable the browser tool (default false)\n\tHeadless bool `json:\"headless,omitempty\"` // run Chrome in headless mode (ignored when RemoteURL is set)\n\tRemoteURL string `json:\"remote_url,omitempty\"` // CDP endpoint for remote Chrome sidecar, e.g. \"ws://chrome:9222\"\n}\n\n// ToolPolicySpec defines a tool policy at any level (global, per-agent, per-provider).\ntype ToolPolicySpec struct {\n\tProfile string `json:\"profile,omitempty\"`\n\tAllow []string `json:\"allow,omitempty\"`\n\tDeny []string `json:\"deny,omitempty\"`\n\tAlsoAllow []string `json:\"alsoAllow,omitempty\"`\n\tByProvider map[string]*ToolPolicySpec `json:\"byProvider,omitempty\"`\n}\n\ntype WebToolsConfig struct {\n\tBrave BraveConfig `json:\"brave\"`\n\tDuckDuckGo DuckDuckGoConfig `json:\"duckduckgo\"`\n}\n\ntype BraveConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tAPIKey string `json:\"api_key\"`\n\tMaxResults int `json:\"max_results\"`\n}\n\ntype DuckDuckGoConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tMaxResults int `json:\"max_results\"`\n}\n\n// SessionsConfig controls session behavior.\n// Matching TS src/config/sessions/types.ts + src/config/types.base.ts.\ntype SessionsConfig struct {\n\tScope string `json:\"scope,omitempty\"` // \"per-sender\" (default), \"global\"\n\tDmScope string `json:\"dm_scope,omitempty\"` // \"main\", \"per-peer\", \"per-channel-peer\" (default), \"per-account-channel-peer\"\n\tMainKey string `json:\"main_key,omitempty\"` // main session key suffix (default \"main\", used when dm_scope=\"main\")\n}\n\n// TtsConfig configures text-to-speech.\n// Matching TS src/config/types.tts.ts.\ntype TtsConfig struct {\n\tProvider string `json:\"provider,omitempty\"` // \"openai\", \"elevenlabs\", \"edge\", \"minimax\"\n\tAuto string `json:\"auto,omitempty\"` // \"off\" (default), \"always\", \"inbound\", \"tagged\"\n\tMode string `json:\"mode,omitempty\"` // \"final\" (default), \"all\"\n\tMaxLength int `json:\"max_length,omitempty\"` // max text length before truncation (default 1500)\n\tTimeoutMs int `json:\"timeout_ms,omitempty\"` // API timeout in ms (default 30000)\n\tOpenAI TtsOpenAIConfig `json:\"openai\"`\n\tElevenLabs TtsElevenLabsConfig `json:\"elevenlabs\"`\n\tEdge TtsEdgeConfig `json:\"edge\"`\n\tMiniMax TtsMiniMaxConfig `json:\"minimax\"`\n}\n\n// TtsOpenAIConfig configures the OpenAI TTS provider.\ntype TtsOpenAIConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"`\n\tAPIBase string `json:\"api_base,omitempty\"` // custom endpoint URL\n\tModel string `json:\"model,omitempty\"` // default \"gpt-4o-mini-tts\"\n\tVoice string `json:\"voice,omitempty\"` // default \"alloy\"\n}\n\n// TtsElevenLabsConfig configures the ElevenLabs TTS provider.\ntype TtsElevenLabsConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"`\n\tBaseURL string `json:\"base_url,omitempty\"`\n\tVoiceID string `json:\"voice_id,omitempty\"` // default \"pMsXgVXv3BLzUgSXRplE\"\n\tModelID string `json:\"model_id,omitempty\"` // default \"eleven_multilingual_v2\"\n}\n\n// TtsEdgeConfig configures the Microsoft Edge TTS provider (free, no API key).\ntype TtsEdgeConfig struct {\n\tEnabled bool `json:\"enabled,omitempty\"`\n\tVoice string `json:\"voice,omitempty\"` // default \"en-US-MichelleNeural\"\n\tRate string `json:\"rate,omitempty\"` // speech rate, e.g. \"+0%\"\n}\n\n// TtsMiniMaxConfig configures the MiniMax TTS provider.\ntype TtsMiniMaxConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"`\n\tGroupID string `json:\"group_id,omitempty\"` // MiniMax GroupId (required)\n\tAPIBase string `json:\"api_base,omitempty\"` // default \"https://api.minimax.io/v1\"\n\tModel string `json:\"model,omitempty\"` // default \"speech-02-hd\"\n\tVoiceID string `json:\"voice_id,omitempty\"` // default \"Wise_Woman\"\n}\n\n// MergeChannelGroupQuotas merges per-group quota overrides from channel configs\n// (e.g., channels.telegram.groups[chatID].quota) into gateway.quota.groups.\n// This allows per-group quotas to be set at the channel level and picked up\n// by the QuotaChecker at runtime.\nfunc MergeChannelGroupQuotas(cfg *Config) {\n\tif cfg.Gateway.Quota == nil {\n\t\treturn\n\t}\n\tif cfg.Gateway.Quota.Groups == nil {\n\t\tcfg.Gateway.Quota.Groups = make(map[string]QuotaWindow)\n\t}\n\n\t// Telegram per-group quotas\n\tfor chatID, groupCfg := range cfg.Channels.Telegram.Groups {\n\t\tif groupCfg != nil && groupCfg.Quota != nil && !groupCfg.Quota.IsZero() {\n\t\t\tkey := \"group:telegram:\" + chatID\n\t\t\tcfg.Gateway.Quota.Groups[key] = *groupCfg.Quota\n\t\t}\n\t}\n}\n"}, "5e2fa395c7d6e5b77539d43a0ce00ef17f3cfff8": {"t": 1785759270.5154393, "n": 28472, "h": "ed9f7d6f1607dc77", "c": "package config\n\n// PendingCompactionConfig configures LLM-based compaction of pending group messages.\n// When a group accumulates more than Threshold pending messages, older messages are\n// summarized by an LLM and replaced with a compact summary, keeping KeepRecent raw messages.\ntype PendingCompactionConfig struct {\n\tThreshold int `json:\"threshold,omitempty\"` // trigger compaction when entries exceed this (default 50)\n\tKeepRecent int `json:\"keep_recent,omitempty\"` // keep this many recent raw messages after compaction (default 15)\n\tMaxTokens int `json:\"max_tokens,omitempty\"` // max output tokens for LLM summarization (default 4096)\n\tProvider string `json:\"provider,omitempty\"` // LLM provider name (e.g. \"openai\"); empty = use agent's provider\n\tModel string `json:\"model,omitempty\"` // model for summarization; empty = use agent's model\n}\n\n// ChannelsConfig contains per-channel configuration.\ntype ChannelsConfig struct {\n\tTelegram TelegramConfig `json:\"telegram\"`\n\tDiscord DiscordConfig `json:\"discord\"`\n\tSlack SlackConfig `json:\"slack\"`\n\tWhatsApp WhatsAppConfig `json:\"whatsapp\"`\n\tZalo ZaloConfig `json:\"zalo\"`\n\tZaloPersonal ZaloPersonalConfig `json:\"zalo_personal\"`\n\tFeishu FeishuConfig `json:\"feishu\"`\n\tPendingCompaction *PendingCompactionConfig `json:\"pending_compaction,omitempty\"` // global pending message compaction settings\n}\n\ntype TelegramConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tToken string `json:\"token\"`\n\tProxy string `json:\"proxy,omitempty\"`\n\tAPIServer string `json:\"api_server,omitempty\"` // custom Telegram Bot API server URL (e.g. \"http://localhost:8081\")\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in groups (default true)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tDMStream *bool `json:\"dm_stream,omitempty\"` // enable streaming for DMs (default false) — edits placeholder progressively\n\tGroupStream *bool `json:\"group_stream,omitempty\"` // enable streaming for groups (default false) — sends new message, edits progressively\n\tDraftTransport *bool `json:\"draft_transport,omitempty\"` // use sendMessageDraft for DM streaming (default true) — stealth preview, no notifications per edit\n\tReasoningStream *bool `json:\"reasoning_stream,omitempty\"` // show reasoning as separate message when provider emits thinking events (default true)\n\tReactionLevel string `json:\"reaction_level,omitempty\"` // \"off\" (default), \"minimal\", \"full\" — status emoji reactions\n\tMediaMaxBytes int64 `json:\"media_max_bytes,omitempty\"` // max media download size in bytes (default 20MB)\n\tLinkPreview *bool `json:\"link_preview,omitempty\"` // enable URL previews in messages (default true)\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\n\t// Optional STT (Speech-to-Text) pipeline for voice/audio inbound messages.\n\t// When stt_proxy_url is set, audio/voice messages are transcribed before being forwarded to the agent.\n\tSTTProxyURL string `json:\"stt_proxy_url,omitempty\"` // base URL of the STT proxy service (e.g. \"https://stt.example.com\")\n\tSTTAPIKey string `json:\"stt_api_key,omitempty\"` // Bearer token for the STT proxy\n\tSTTTenantID string `json:\"stt_tenant_id,omitempty\"` // optional tenant/org identifier forwarded to the STT proxy\n\tSTTTimeoutSeconds int `json:\"stt_timeout_seconds,omitempty\"` // per-request timeout for STT calls (default 30s)\n\n\t// Optional audio-aware routing: when set, voice/audio inbound messages are routed to this\n\t// agent instead of the default channel agent. Requires the named agent to exist in the config.\n\tVoiceAgentID string `json:\"voice_agent_id,omitempty\"` // agent ID to route voice inbound to (e.g. \"speaking-agent\")\n\n\t// Per-group (and per-topic) overrides. Key is chat ID string (e.g. \"-100123456\") or \"*\" for wildcard.\n\t// TS ref: channels.telegram.groups in src/config/types.telegram.ts.\n\tGroups map[string]*TelegramGroupConfig `json:\"groups,omitempty\"`\n}\n\n// TelegramGroupConfig defines per-group overrides for a Telegram channel.\n// Matching TS TelegramGroupConfig in src/config/types.telegram.ts.\ntype TelegramGroupConfig struct {\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // override group policy for this group\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // override require_mention for this group\n\tAllowFrom FlexibleStringSlice `json:\"allow_from,omitempty\"` // override allow_from for this group\n\tEnabled *bool `json:\"enabled,omitempty\"` // disable bot for this group (default: true)\n\tSkills []string `json:\"skills,omitempty\"` // skill whitelist (nil = all, [] = none)\n\tTools []string `json:\"tools,omitempty\"` // tool allow list (nil = all, supports \"group:xxx\")\n\tSystemPrompt string `json:\"system_prompt,omitempty\"` // extra system prompt for this group\n\tTopics map[string]*TelegramTopicConfig `json:\"topics,omitempty\"` // per-topic overrides (key: thread ID string)\n\tQuota *QuotaWindow `json:\"quota,omitempty\"` // per-group quota override\n}\n\n// TelegramTopicConfig defines per-topic overrides within a Telegram group.\n// Matching TS TelegramTopicConfig in src/config/types.telegram.ts.\ntype TelegramTopicConfig struct {\n\tRequireMention *bool `json:\"require_mention,omitempty\"`\n\tGroupPolicy string `json:\"group_policy,omitempty\"`\n\tSkills []string `json:\"skills,omitempty\"`\n\tTools []string `json:\"tools,omitempty\"` // tool allow list (nil = inherit, supports \"group:xxx\")\n\tEnabled *bool `json:\"enabled,omitempty\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from,omitempty\"`\n\tSystemPrompt string `json:\"system_prompt,omitempty\"`\n}\n\ntype DiscordConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tToken string `json:\"token\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in groups (default true)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tMediaMaxBytes int64 `json:\"media_max_bytes,omitempty\"` // max media download size (default 25MB)\n\tSTTProxyURL string `json:\"stt_proxy_url,omitempty\"`\n\tSTTAPIKey string `json:\"stt_api_key,omitempty\"`\n\tSTTTenantID string `json:\"stt_tenant_id,omitempty\"`\n\tSTTTimeoutSeconds int `json:\"stt_timeout_seconds,omitempty\"`\n\tVoiceAgentID string `json:\"voice_agent_id,omitempty\"`\n}\n\ntype SlackConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tBotToken string `json:\"bot_token\"` // xoxb-... (Bot User OAuth Token)\n\tAppToken string `json:\"app_token\"` // xapp-... (App-Level Token for Socket Mode)\n\tUserToken string `json:\"user_token,omitempty\"` // xoxp-... (Optional: custom bot identity)\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"pairing\", \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in channels (default true)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tDMStream *bool `json:\"dm_stream,omitempty\"` // enable streaming for DMs (default false)\n\tGroupStream *bool `json:\"group_stream,omitempty\"` // enable streaming for groups (default false)\n\tNativeStream *bool `json:\"native_stream,omitempty\"` // use Slack ChatStreamer API if available (default false)\n\tReactionLevel string `json:\"reaction_level,omitempty\"` // \"off\" (default), \"minimal\", \"full\"\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tDebounceDelay int `json:\"debounce_delay,omitempty\"` // ms delay before dispatching rapid messages (default 300, 0=disabled)\n\tThreadTTL *int `json:\"thread_ttl,omitempty\"` // hours before thread participation expires (default 24, 0=disabled — always require @mention)\n\tMediaMaxBytes int64 `json:\"media_max_bytes,omitempty\"` // max file download size in bytes (default 20MB)\n}\n\ntype WhatsAppConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tBridgeURL string `json:\"bridge_url\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n}\n\ntype ZaloConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tToken string `json:\"token\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tWebhookURL string `json:\"webhook_url,omitempty\"`\n\tWebhookSecret string `json:\"webhook_secret,omitempty\"`\n\tMediaMaxMB int `json:\"media_max_mb,omitempty\"` // default 5\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n}\n\ntype ZaloPersonalConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default), \"allowlist\", \"open\", \"disabled\"\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default), \"allowlist\", \"disabled\"\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // require @bot mention in groups (default true)\n\tHistoryLimit int `json:\"history_limit,omitempty\"` // max pending group messages for context (default 50, 0=disabled)\n\tCredentialsPath string `json:\"credentials_path,omitempty\"` // path to saved cookies JSON\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n}\n\ntype FeishuConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tAppID string `json:\"app_id\"`\n\tAppSecret string `json:\"app_secret\"`\n\tEncryptKey string `json:\"encrypt_key,omitempty\"`\n\tVerificationToken string `json:\"verification_token,omitempty\"`\n\tDomain string `json:\"domain,omitempty\"` // \"lark\" (default/global), \"feishu\" (China), or custom URL\n\tConnectionMode string `json:\"connection_mode,omitempty\"` // \"websocket\" (default), \"webhook\"\n\tWebhookPort int `json:\"webhook_port,omitempty\"` // default 3000\n\tWebhookPath string `json:\"webhook_path,omitempty\"` // default \"/feishu/events\"\n\tAllowFrom FlexibleStringSlice `json:\"allow_from\"`\n\tDMPolicy string `json:\"dm_policy,omitempty\"` // \"pairing\" (default)\n\tGroupPolicy string `json:\"group_policy,omitempty\"` // \"open\" (default)\n\tGroupAllowFrom FlexibleStringSlice `json:\"group_allow_from,omitempty\"`\n\tRequireMention *bool `json:\"require_mention,omitempty\"` // default true (groups)\n\tTopicSessionMode string `json:\"topic_session_mode,omitempty\"` // \"disabled\" (default)\n\tTextChunkLimit int `json:\"text_chunk_limit,omitempty\"` // default 4000\n\tMediaMaxMB int `json:\"media_max_mb,omitempty\"` // default 30\n\tRenderMode string `json:\"render_mode,omitempty\"` // \"auto\", \"raw\", \"card\"\n\tStreaming *bool `json:\"streaming,omitempty\"` // default true\n\tReactionLevel string `json:\"reaction_level,omitempty\"` // \"off\" (default), \"minimal\", \"full\" — typing emoji reactions\n\tHistoryLimit int `json:\"history_limit,omitempty\"`\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // override gateway block_reply (nil = inherit)\n\tSTTProxyURL string `json:\"stt_proxy_url,omitempty\"`\n\tSTTAPIKey string `json:\"stt_api_key,omitempty\"`\n\tSTTTenantID string `json:\"stt_tenant_id,omitempty\"`\n\tSTTTimeoutSeconds int `json:\"stt_timeout_seconds,omitempty\"`\n\tVoiceAgentID string `json:\"voice_agent_id,omitempty\"`\n}\n\n// ProvidersConfig maps provider name to its config.\ntype ProvidersConfig struct {\n\tAnthropic ProviderConfig `json:\"anthropic\"`\n\tOpenAI ProviderConfig `json:\"openai\"`\n\tOpenRouter ProviderConfig `json:\"openrouter\"`\n\tGroq ProviderConfig `json:\"groq\"`\n\tGemini ProviderConfig `json:\"gemini\"`\n\tDeepSeek ProviderConfig `json:\"deepseek\"`\n\tMistral ProviderConfig `json:\"mistral\"`\n\tXAI ProviderConfig `json:\"xai\"`\n\tMiniMax ProviderConfig `json:\"minimax\"`\n\tCohere ProviderConfig `json:\"cohere\"`\n\tPerplexity ProviderConfig `json:\"perplexity\"`\n\tDashScope ProviderConfig `json:\"dashscope\"`\n\tBailian ProviderConfig `json:\"bailian\"`\n\tZai ProviderConfig `json:\"zai\"`\n\tZaiCoding ProviderConfig `json:\"zai_coding\"`\n\tOllama OllamaConfig `json:\"ollama\"` // local Ollama instance (no API key needed)\n\tOllamaCloud ProviderConfig `json:\"ollama_cloud\"` // Ollama Cloud (API key required)\n\tClaudeCLI ClaudeCLIConfig `json:\"claude_cli\"`\n\tACP ACPConfig `json:\"acp\"`\n}\n\n// OllamaConfig configures a local (or self-hosted) Ollama instance.\n// No API key is required — Ollama accepts any Bearer token value.\ntype OllamaConfig struct {\n\tHost string `json:\"host\"` // Ollama server base URL, e.g. http://localhost:11434\n}\n\n// ClaudeCLIConfig configures the Claude CLI provider (uses subscription, not API key).\ntype ClaudeCLIConfig struct {\n\tCLIPath string `json:\"cli_path\" yaml:\"cli_path\"` // path to claude binary (default: \"claude\")\n\tModel string `json:\"model\" yaml:\"model\"` // default model alias (default: \"sonnet\")\n\tBaseWorkDir string `json:\"base_work_dir\" yaml:\"base_work_dir\"` // base dir for agent workspaces\n\tPermMode string `json:\"perm_mode\" yaml:\"perm_mode\"` // permission mode (default: \"bypassPermissions\")\n}\n\n// ACPConfig configures the ACP (Agent Client Protocol) provider.\n// Orchestrates any ACP-compatible coding agent (Claude Code, Codex CLI, Gemini CLI) as a subprocess.\ntype ACPConfig struct {\n\tBinary string `json:\"binary\"` // agent binary name or path (e.g. \"claude\", \"codex\")\n\tArgs []string `json:\"args\"` // extra spawn args\n\tModel string `json:\"model\"` // default model/agent name\n\tWorkDir string `json:\"work_dir\"` // base workspace dir\n\tIdleTTL string `json:\"idle_ttl\"` // process idle TTL (e.g. \"5m\")\n\tPermMode string `json:\"perm_mode\"` // \"approve-all\" (default), \"approve-reads\", \"deny-all\"\n}\n\ntype ProviderConfig struct {\n\tAPIKey string `json:\"api_key\"`\n\tAPIBase string `json:\"api_base,omitempty\"`\n}\n\n// HasAnyProvider returns true if at least one provider has an API key or CLI configured.\nfunc (c *Config) HasAnyProvider() bool {\n\tp := c.Providers\n\treturn p.Anthropic.APIKey != \"\" ||\n\t\tp.OpenAI.APIKey != \"\" ||\n\t\tp.OpenRouter.APIKey != \"\" ||\n\t\tp.Groq.APIKey != \"\" ||\n\t\tp.Gemini.APIKey != \"\" ||\n\t\tp.DeepSeek.APIKey != \"\" ||\n\t\tp.Mistral.APIKey != \"\" ||\n\t\tp.XAI.APIKey != \"\" ||\n\t\tp.MiniMax.APIKey != \"\" ||\n\t\tp.Cohere.APIKey != \"\" ||\n\t\tp.Perplexity.APIKey != \"\" ||\n\t\tp.DashScope.APIKey != \"\" ||\n\t\tp.Bailian.APIKey != \"\" ||\n\t\tp.Zai.APIKey != \"\" ||\n\t\tp.ZaiCoding.APIKey != \"\" ||\n\t\tp.Ollama.Host != \"\" ||\n\t\tp.OllamaCloud.APIKey != \"\" ||\n\t\tp.ClaudeCLI.CLIPath != \"\" ||\n\t\tp.ACP.Binary != \"\"\n}\n\n// QuotaWindow defines request limits per time window. Zero means unlimited.\ntype QuotaWindow struct {\n\tHour int `json:\"hour,omitempty\"` // max requests per hour (0 = unlimited)\n\tDay int `json:\"day,omitempty\"` // max requests per day (0 = unlimited)\n\tWeek int `json:\"week,omitempty\"` // max requests per week (0 = unlimited)\n}\n\n// IsZero returns true if no limits are set.\nfunc (w QuotaWindow) IsZero() bool { return w.Hour == 0 && w.Day == 0 && w.Week == 0 }\n\n// QuotaConfig configures per-user/group request quotas.\n// Config merge priority: Groups > Channels > Providers > Default.\ntype QuotaConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tDefault QuotaWindow `json:\"default\"`\n\tProviders map[string]QuotaWindow `json:\"providers,omitempty\"` // key = provider name (e.g. \"anthropic\")\n\tChannels map[string]QuotaWindow `json:\"channels,omitempty\"` // key = channel name (e.g. \"telegram\")\n\tGroups map[string]QuotaWindow `json:\"groups,omitempty\"` // key = userID (e.g. \"group:telegram:-100123\")\n}\n\n// GatewayConfig controls the gateway server.\ntype GatewayConfig struct {\n\tHost string `json:\"host\"`\n\tPort int `json:\"port\"`\n\tToken string `json:\"token,omitempty\"` // bearer token for WS/HTTP auth\n\tOwnerIDs []string `json:\"owner_ids,omitempty\"` // sender IDs considered \"owner\"\n\tAllowedOrigins []string `json:\"allowed_origins,omitempty\"` // WebSocket CORS whitelist (empty = allow all)\n\tMaxMessageChars int `json:\"max_message_chars,omitempty\"` // max user message characters (default 32000)\n\tRateLimitRPM int `json:\"rate_limit_rpm,omitempty\"` // rate limit: requests per minute per user (default 20, 0 = disabled)\n\tInjectionAction string `json:\"injection_action,omitempty\"` // prompt injection action: \"log\", \"warn\" (default), \"block\", \"off\"\n\tInboundDebounceMs int `json:\"inbound_debounce_ms,omitempty\"` // merge rapid messages from same sender (default 1000ms, -1 = disabled)\n\tQuota *QuotaConfig `json:\"quota,omitempty\"` // per-user/group request quotas\n\tBlockReply *bool `json:\"block_reply,omitempty\"` // deliver intermediate text during tool iterations (default false)\n\tToolStatus *bool `json:\"tool_status,omitempty\"` // show tool name in streaming preview during tool execution (default true)\n\tTaskRecoveryIntervalSec int `json:\"task_recovery_interval_sec,omitempty\"` // team task recovery ticker interval in seconds (default 300 = 5min)\n}\n\n// ToolsConfig controls tool availability, policy, and web search.\ntype ToolsConfig struct {\n\tProfile string `json:\"profile,omitempty\"` // global profile: \"minimal\", \"coding\", \"messaging\", \"full\"\n\tAllow []string `json:\"allow,omitempty\"` // global allow list (tool names or \"group:xxx\")\n\tDeny []string `json:\"deny,omitempty\"` // global deny list\n\tAlsoAllow []string `json:\"alsoAllow,omitempty\"` // additive: adds without removing existing\n\tByProvider map[string]*ToolPolicySpec `json:\"byProvider,omitempty\"` // per-provider overrides\n\tExecApproval ExecApprovalCfg `json:\"execApproval\"` // exec command approval settings\n\tWebFetch WebFetchPolicyConfig `json:\"web_fetch\"` // domain policy for URL fetching\n\tWeb WebToolsConfig `json:\"web\"`\n\tBrowser BrowserToolConfig `json:\"browser\"`\n\tRateLimitPerHour int `json:\"rate_limit_per_hour,omitempty\"` // max tool executions per hour per session (0 = disabled)\n\tScrubCredentials *bool `json:\"scrub_credentials,omitempty\"` // auto-redact API keys/tokens in tool output (default true)\n\tMcpServers map[string]*MCPServerConfig `json:\"mcp_servers,omitempty\"` // external MCP server connections\n}\n\n// MCPServerConfig configures a single external MCP server connection.\ntype MCPServerConfig struct {\n\tTransport string `json:\"transport\"` // \"stdio\", \"sse\", \"streamable-http\"\n\tCommand string `json:\"command,omitempty\"` // stdio: command to spawn\n\tArgs []string `json:\"args,omitempty\"` // stdio: command arguments\n\tEnv map[string]string `json:\"env,omitempty\"` // stdio: extra environment variables\n\tURL string `json:\"url,omitempty\"` // sse/http: server URL\n\tHeaders map[string]string `json:\"headers,omitempty\"` // sse/http: extra HTTP headers\n\tEnabled *bool `json:\"enabled,omitempty\"` // default true\n\tToolPrefix string `json:\"tool_prefix,omitempty\"` // prefix for tool names (avoids collisions)\n\tTimeoutSec int `json:\"timeout_sec,omitempty\"` // per-tool-call timeout in seconds (default 60)\n}\n\n// IsEnabled returns whether this MCP server is enabled (default true).\nfunc (c *MCPServerConfig) IsEnabled() bool {\n\treturn c.Enabled == nil || *c.Enabled\n}\n\n// ExecApprovalCfg configures command execution approval (matching TS exec-approval.ts).\ntype ExecApprovalCfg struct {\n\tSecurity string `json:\"security,omitempty\"` // \"deny\", \"allowlist\", \"full\" (default \"full\")\n\tAsk string `json:\"ask,omitempty\"` // \"off\", \"on-miss\", \"always\" (default \"off\")\n\tAllowlist []string `json:\"allowlist,omitempty\"` // glob patterns for allowed commands\n}\n\n// WebFetchPolicyConfig controls domain filtering for the web_fetch tool.\ntype WebFetchPolicyConfig struct {\n\tPolicy string `json:\"policy,omitempty\"` // \"allow_all\" (default), \"allowlist\"\n\tAllowedDomains []string `json:\"allowed_domains,omitempty\"` // e.g. [\"github.com\", \"*.example.com\"]\n\tBlockedDomains []string `json:\"blocked_domains,omitempty\"` // always checked regardless of policy\n}\n\n// BrowserToolConfig controls the browser automation tool.\ntype BrowserToolConfig struct {\n\tEnabled bool `json:\"enabled\"` // enable the browser tool (default false)\n\tHeadless bool `json:\"headless,omitempty\"` // run Chrome in headless mode (ignored when RemoteURL is set)\n\tRemoteURL string `json:\"remote_url,omitempty\"` // CDP endpoint for remote Chrome sidecar, e.g. \"ws://chrome:9222\"\n}\n\n// ToolPolicySpec defines a tool policy at any level (global, per-agent, per-provider).\ntype ToolPolicySpec struct {\n\tProfile string `json:\"profile,omitempty\"`\n\tAllow []string `json:\"allow,omitempty\"`\n\tDeny []string `json:\"deny,omitempty\"`\n\tAlsoAllow []string `json:\"alsoAllow,omitempty\"`\n\tByProvider map[string]*ToolPolicySpec `json:\"byProvider,omitempty\"`\n}\n\ntype WebToolsConfig struct {\n\tBrave BraveConfig `json:\"brave\"`\n\tDuckDuckGo DuckDuckGoConfig `json:\"duckduckgo\"`\n}\n\ntype BraveConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tAPIKey string `json:\"api_key\"`\n\tMaxResults int `json:\"max_results\"`\n}\n\ntype DuckDuckGoConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tMaxResults int `json:\"max_results\"`\n}\n\n// SessionsConfig controls session behavior.\n// Matching TS src/config/sessions/types.ts + src/config/types.base.ts.\ntype SessionsConfig struct {\n\tStorage string `json:\"storage\"` // directory for session files\n\tScope string `json:\"scope,omitempty\"` // \"per-sender\" (default), \"global\"\n\tDmScope string `json:\"dm_scope,omitempty\"` // \"main\", \"per-peer\", \"per-channel-peer\" (default), \"per-account-channel-peer\"\n\tMainKey string `json:\"main_key,omitempty\"` // main session key suffix (default \"main\", used when dm_scope=\"main\")\n}\n\n// TtsConfig configures text-to-speech.\n// Matching TS src/config/types.tts.ts.\ntype TtsConfig struct {\n\tProvider string `json:\"provider,omitempty\"` // \"openai\", \"elevenlabs\", \"edge\", \"minimax\"\n\tAuto string `json:\"auto,omitempty\"` // \"off\" (default), \"always\", \"inbound\", \"tagged\"\n\tMode string `json:\"mode,omitempty\"` // \"final\" (default), \"all\"\n\tMaxLength int `json:\"max_length,omitempty\"` // max text length before truncation (default 1500)\n\tTimeoutMs int `json:\"timeout_ms,omitempty\"` // API timeout in ms (default 30000)\n\tOpenAI TtsOpenAIConfig `json:\"openai\"`\n\tElevenLabs TtsElevenLabsConfig `json:\"elevenlabs\"`\n\tEdge TtsEdgeConfig `json:\"edge\"`\n\tMiniMax TtsMiniMaxConfig `json:\"minimax\"`\n}\n\n// TtsOpenAIConfig configures the OpenAI TTS provider.\ntype TtsOpenAIConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"`\n\tAPIBase string `json:\"api_base,omitempty\"` // custom endpoint URL\n\tModel string `json:\"model,omitempty\"` // default \"gpt-4o-mini-tts\"\n\tVoice string `json:\"voice,omitempty\"` // default \"alloy\"\n}\n\n// TtsElevenLabsConfig configures the ElevenLabs TTS provider.\ntype TtsElevenLabsConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"`\n\tBaseURL string `json:\"base_url,omitempty\"`\n\tVoiceID string `json:\"voice_id,omitempty\"` // default \"pMsXgVXv3BLzUgSXRplE\"\n\tModelID string `json:\"model_id,omitempty\"` // default \"eleven_multilingual_v2\"\n}\n\n// TtsEdgeConfig configures the Microsoft Edge TTS provider (free, no API key).\ntype TtsEdgeConfig struct {\n\tEnabled bool `json:\"enabled,omitempty\"`\n\tVoice string `json:\"voice,omitempty\"` // default \"en-US-MichelleNeural\"\n\tRate string `json:\"rate,omitempty\"` // speech rate, e.g. \"+0%\"\n}\n\n// TtsMiniMaxConfig configures the MiniMax TTS provider.\ntype TtsMiniMaxConfig struct {\n\tAPIKey string `json:\"api_key,omitempty\"`\n\tGroupID string `json:\"group_id,omitempty\"` // MiniMax GroupId (required)\n\tAPIBase string `json:\"api_base,omitempty\"` // default \"https://api.minimax.io/v1\"\n\tModel string `json:\"model,omitempty\"` // default \"speech-02-hd\"\n\tVoiceID string `json:\"voice_id,omitempty\"` // default \"Wise_Woman\"\n}\n\n// MergeChannelGroupQuotas merges per-group quota overrides from channel configs\n// (e.g., channels.telegram.groups[chatID].quota) into gateway.quota.groups.\n// This allows per-group quotas to be set at the channel level and picked up\n// by the QuotaChecker at runtime.\nfunc MergeChannelGroupQuotas(cfg *Config) {\n\tif cfg.Gateway.Quota == nil {\n\t\treturn\n\t}\n\tif cfg.Gateway.Quota.Groups == nil {\n\t\tcfg.Gateway.Quota.Groups = make(map[string]QuotaWindow)\n\t}\n\n\t// Telegram per-group quotas\n\tfor chatID, groupCfg := range cfg.Channels.Telegram.Groups {\n\t\tif groupCfg != nil && groupCfg.Quota != nil && !groupCfg.Quota.IsZero() {\n\t\t\tkey := \"group:telegram:\" + chatID\n\t\t\tcfg.Gateway.Quota.Groups[key] = *groupCfg.Quota\n\t\t}\n\t}\n}\n"}}}}