Files
hack/tools/scripts/llm-key-hunter/results/_cache/content/bbgiangpp-cpu_goclaw-daugiatem.json

1 line
1.1 MiB
Plaintext
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{"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"}}}}