from pathlib import Path from typing import Any, Dict, List, Optional from sqlmodel import Session from core.config_manager import BotConfigManager from core.settings import BOTS_WORKSPACE_ROOT from models.bot import BotInstance from schemas.bot import ChannelConfigRequest from services.bot_storage_service import ( _normalize_resource_limits, _read_bot_config, _write_bot_resources, ) from services.template_service import get_agent_md_templates config_manager = BotConfigManager(host_data_root=BOTS_WORKSPACE_ROOT) def _normalize_channel_extra(raw: Any) -> Dict[str, Any]: if not isinstance(raw, dict): return {} return raw def _normalize_allow_from(raw: Any) -> List[str]: rows: List[str] = [] if isinstance(raw, list): for item in raw: text = str(item or "").strip() if text and text not in rows: rows.append(text) return rows or ["*"] def _read_global_delivery_flags(channels_cfg: Any) -> tuple[bool, bool]: if not isinstance(channels_cfg, dict): return False, False send_progress = channels_cfg.get("sendProgress") send_tool_hints = channels_cfg.get("sendToolHints") dashboard_cfg = channels_cfg.get("dashboard") if isinstance(dashboard_cfg, dict): if send_progress is None and "sendProgress" in dashboard_cfg: send_progress = dashboard_cfg.get("sendProgress") if send_tool_hints is None and "sendToolHints" in dashboard_cfg: send_tool_hints = dashboard_cfg.get("sendToolHints") return bool(send_progress), bool(send_tool_hints) def _channel_cfg_to_api_dict(bot_id: str, ctype: str, cfg: Dict[str, Any]) -> Dict[str, Any]: ctype = str(ctype or "").strip().lower() enabled = bool(cfg.get("enabled", True)) port = max(1, min(int(cfg.get("port", 8080) or 8080), 65535)) extra: Dict[str, Any] = {} external_app_id = "" app_secret = "" if ctype == "feishu": external_app_id = str(cfg.get("appId") or "") app_secret = str(cfg.get("appSecret") or "") extra = { "encryptKey": cfg.get("encryptKey", ""), "verificationToken": cfg.get("verificationToken", ""), "allowFrom": _normalize_allow_from(cfg.get("allowFrom", [])), } elif ctype == "dingtalk": external_app_id = str(cfg.get("clientId") or "") app_secret = str(cfg.get("clientSecret") or "") extra = {"allowFrom": _normalize_allow_from(cfg.get("allowFrom", []))} elif ctype == "telegram": app_secret = str(cfg.get("token") or "") extra = { "proxy": cfg.get("proxy", ""), "replyToMessage": bool(cfg.get("replyToMessage", False)), "allowFrom": _normalize_allow_from(cfg.get("allowFrom", [])), } elif ctype == "slack": external_app_id = str(cfg.get("botToken") or "") app_secret = str(cfg.get("appToken") or "") extra = { "mode": cfg.get("mode", "socket"), "replyInThread": bool(cfg.get("replyInThread", True)), "groupPolicy": cfg.get("groupPolicy", "mention"), "groupAllowFrom": cfg.get("groupAllowFrom", []), "reactEmoji": cfg.get("reactEmoji", "eyes"), } elif ctype == "qq": external_app_id = str(cfg.get("appId") or "") app_secret = str(cfg.get("secret") or "") extra = {"allowFrom": _normalize_allow_from(cfg.get("allowFrom", []))} elif ctype == "weixin": app_secret = "" extra = { "hasSavedState": (Path(BOTS_WORKSPACE_ROOT) / bot_id / ".nanobot" / "weixin" / "account.json").is_file(), } elif ctype == "email": extra = { "consentGranted": bool(cfg.get("consentGranted", False)), "imapHost": str(cfg.get("imapHost") or ""), "imapPort": int(cfg.get("imapPort") or 993), "imapUsername": str(cfg.get("imapUsername") or ""), "imapPassword": str(cfg.get("imapPassword") or ""), "imapMailbox": str(cfg.get("imapMailbox") or "INBOX"), "imapUseSsl": bool(cfg.get("imapUseSsl", True)), "smtpHost": str(cfg.get("smtpHost") or ""), "smtpPort": int(cfg.get("smtpPort") or 587), "smtpUsername": str(cfg.get("smtpUsername") or ""), "smtpPassword": str(cfg.get("smtpPassword") or ""), "smtpUseTls": bool(cfg.get("smtpUseTls", True)), "smtpUseSsl": bool(cfg.get("smtpUseSsl", False)), "fromAddress": str(cfg.get("fromAddress") or ""), "autoReplyEnabled": bool(cfg.get("autoReplyEnabled", True)), "pollIntervalSeconds": int(cfg.get("pollIntervalSeconds") or 30), "markSeen": bool(cfg.get("markSeen", True)), "maxBodyChars": int(cfg.get("maxBodyChars") or 12000), "subjectPrefix": str(cfg.get("subjectPrefix") or "Re: "), "allowFrom": _normalize_allow_from(cfg.get("allowFrom", [])), } else: external_app_id = str(cfg.get("appId") or cfg.get("clientId") or cfg.get("botToken") or cfg.get("externalAppId") or "") app_secret = str(cfg.get("appSecret") or cfg.get("clientSecret") or cfg.get("secret") or cfg.get("token") or cfg.get("appToken") or "") extra = { key: value for key, value in cfg.items() if key not in {"enabled", "port", "appId", "clientId", "botToken", "externalAppId", "appSecret", "clientSecret", "secret", "token", "appToken"} } return { "id": ctype, "bot_id": bot_id, "channel_type": ctype, "external_app_id": external_app_id, "app_secret": app_secret, "internal_port": port, "is_active": enabled, "extra_config": extra, "locked": ctype == "dashboard", } def _channel_api_to_cfg(row: Dict[str, Any]) -> Dict[str, Any]: ctype = str(row.get("channel_type") or "").strip().lower() enabled = bool(row.get("is_active", True)) extra = _normalize_channel_extra(row.get("extra_config")) external_app_id = str(row.get("external_app_id") or "") app_secret = str(row.get("app_secret") or "") port = max(1, min(int(row.get("internal_port") or 8080), 65535)) if ctype == "feishu": return { "enabled": enabled, "appId": external_app_id, "appSecret": app_secret, "encryptKey": extra.get("encryptKey", ""), "verificationToken": extra.get("verificationToken", ""), "allowFrom": _normalize_allow_from(extra.get("allowFrom", [])), } if ctype == "dingtalk": return { "enabled": enabled, "clientId": external_app_id, "clientSecret": app_secret, "allowFrom": _normalize_allow_from(extra.get("allowFrom", [])), } if ctype == "telegram": return { "enabled": enabled, "token": app_secret, "proxy": extra.get("proxy", ""), "replyToMessage": bool(extra.get("replyToMessage", False)), "allowFrom": _normalize_allow_from(extra.get("allowFrom", [])), } if ctype == "slack": return { "enabled": enabled, "mode": extra.get("mode", "socket"), "botToken": external_app_id, "appToken": app_secret, "replyInThread": bool(extra.get("replyInThread", True)), "groupPolicy": extra.get("groupPolicy", "mention"), "groupAllowFrom": extra.get("groupAllowFrom", []), "reactEmoji": extra.get("reactEmoji", "eyes"), } if ctype == "qq": return { "enabled": enabled, "appId": external_app_id, "secret": app_secret, "allowFrom": _normalize_allow_from(extra.get("allowFrom", [])), } if ctype == "weixin": return { "enabled": enabled, "token": app_secret, } if ctype == "email": return { "enabled": enabled, "consentGranted": bool(extra.get("consentGranted", False)), "imapHost": str(extra.get("imapHost") or ""), "imapPort": max(1, min(int(extra.get("imapPort") or 993), 65535)), "imapUsername": str(extra.get("imapUsername") or ""), "imapPassword": str(extra.get("imapPassword") or ""), "imapMailbox": str(extra.get("imapMailbox") or "INBOX"), "imapUseSsl": bool(extra.get("imapUseSsl", True)), "smtpHost": str(extra.get("smtpHost") or ""), "smtpPort": max(1, min(int(extra.get("smtpPort") or 587), 65535)), "smtpUsername": str(extra.get("smtpUsername") or ""), "smtpPassword": str(extra.get("smtpPassword") or ""), "smtpUseTls": bool(extra.get("smtpUseTls", True)), "smtpUseSsl": bool(extra.get("smtpUseSsl", False)), "fromAddress": str(extra.get("fromAddress") or ""), "autoReplyEnabled": bool(extra.get("autoReplyEnabled", True)), "pollIntervalSeconds": max(5, int(extra.get("pollIntervalSeconds") or 30)), "markSeen": bool(extra.get("markSeen", True)), "maxBodyChars": max(1, int(extra.get("maxBodyChars") or 12000)), "subjectPrefix": str(extra.get("subjectPrefix") or "Re: "), "allowFrom": _normalize_allow_from(extra.get("allowFrom", [])), } merged = dict(extra) merged.update( { "enabled": enabled, "appId": external_app_id, "appSecret": app_secret, "port": port, } ) return merged def _get_bot_channels_from_config(bot: BotInstance) -> List[Dict[str, Any]]: config_data = _read_bot_config(bot.id) channels_cfg = config_data.get("channels") if not isinstance(channels_cfg, dict): channels_cfg = {} send_progress, send_tool_hints = _read_global_delivery_flags(channels_cfg) rows: List[Dict[str, Any]] = [ { "id": "dashboard", "bot_id": bot.id, "channel_type": "dashboard", "external_app_id": f"dashboard-{bot.id}", "app_secret": "", "internal_port": 9000, "is_active": True, "extra_config": { "sendProgress": send_progress, "sendToolHints": send_tool_hints, }, "locked": True, } ] for ctype, cfg in channels_cfg.items(): if ctype in {"sendProgress", "sendToolHints", "dashboard"} or not isinstance(cfg, dict): continue rows.append(_channel_cfg_to_api_dict(bot.id, ctype, cfg)) return rows def _normalize_initial_channels(bot_id: str, channels: Optional[List[ChannelConfigRequest]]) -> List[Dict[str, Any]]: rows: List[Dict[str, Any]] = [] seen_types: set[str] = set() for channel in channels or []: ctype = (channel.channel_type or "").strip().lower() if not ctype or ctype == "dashboard" or ctype in seen_types: continue seen_types.add(ctype) rows.append( { "id": ctype, "bot_id": bot_id, "channel_type": ctype, "external_app_id": (channel.external_app_id or "").strip() or f"{ctype}-{bot_id}", "app_secret": (channel.app_secret or "").strip(), "internal_port": max(1, min(int(channel.internal_port or 8080), 65535)), "is_active": bool(channel.is_active), "extra_config": _normalize_channel_extra(channel.extra_config), "locked": False, } ) return rows def _sync_workspace_channels_impl( session: Session, bot_id: str, snapshot: Dict[str, Any], *, channels_override: Optional[List[Dict[str, Any]]] = None, global_delivery_override: Optional[Dict[str, Any]] = None, runtime_overrides: Optional[Dict[str, Any]] = None, ) -> None: bot = session.get(BotInstance, bot_id) if not bot: return template_defaults = get_agent_md_templates() bot_data: Dict[str, Any] = { "name": bot.name, "system_prompt": snapshot.get("system_prompt") or template_defaults.get("soul_md", ""), "soul_md": snapshot.get("soul_md") or template_defaults.get("soul_md", ""), "agents_md": snapshot.get("agents_md") or template_defaults.get("agents_md", ""), "user_md": snapshot.get("user_md") or template_defaults.get("user_md", ""), "tools_md": snapshot.get("tools_md") or template_defaults.get("tools_md", ""), "identity_md": snapshot.get("identity_md") or template_defaults.get("identity_md", ""), "llm_provider": snapshot.get("llm_provider") or "", "llm_model": snapshot.get("llm_model") or "", "api_key": snapshot.get("api_key") or "", "api_base": snapshot.get("api_base") or "", "temperature": snapshot.get("temperature"), "top_p": snapshot.get("top_p"), "max_tokens": snapshot.get("max_tokens"), "cpu_cores": snapshot.get("cpu_cores"), "memory_mb": snapshot.get("memory_mb"), "storage_gb": snapshot.get("storage_gb"), "send_progress": bool(snapshot.get("send_progress")), "send_tool_hints": bool(snapshot.get("send_tool_hints")), } if isinstance(runtime_overrides, dict): for key, value in runtime_overrides.items(): if key in {"api_key", "llm_provider", "llm_model"}: text = str(value or "").strip() if not text: continue bot_data[key] = text continue if key == "api_base": bot_data[key] = str(value or "").strip() continue bot_data[key] = value resources = _normalize_resource_limits( bot_data.get("cpu_cores"), bot_data.get("memory_mb"), bot_data.get("storage_gb"), ) bot_data.update(resources) send_progress = bool(bot_data.get("send_progress", False)) send_tool_hints = bool(bot_data.get("send_tool_hints", False)) if isinstance(global_delivery_override, dict): if "sendProgress" in global_delivery_override: send_progress = bool(global_delivery_override.get("sendProgress")) if "sendToolHints" in global_delivery_override: send_tool_hints = bool(global_delivery_override.get("sendToolHints")) channels_data = channels_override if channels_override is not None else _get_bot_channels_from_config(bot) bot_data["send_progress"] = send_progress bot_data["send_tool_hints"] = send_tool_hints normalized_channels: List[Dict[str, Any]] = [] for row in channels_data: ctype = str(row.get("channel_type") or "").strip().lower() if not ctype or ctype == "dashboard": continue normalized_channels.append( { "channel_type": ctype, "external_app_id": str(row.get("external_app_id") or ""), "app_secret": str(row.get("app_secret") or ""), "internal_port": max(1, min(int(row.get("internal_port") or 8080), 65535)), "is_active": bool(row.get("is_active", True)), "extra_config": _normalize_channel_extra(row.get("extra_config")), } ) config_manager.update_workspace(bot_id=bot_id, bot_data=bot_data, channels=normalized_channels) _write_bot_resources(bot_id, bot_data.get("cpu_cores"), bot_data.get("memory_mb"), bot_data.get("storage_gb"))