import os from typing import Any, Dict, List, Optional from datetime import datetime, timezone from zoneinfo import ZoneInfo from sqlmodel import Session from core.settings import DEFAULT_BOT_SYSTEM_TIMEZONE from models.bot import BotInstance from services.bot_storage_service import ( _bot_data_root, _clear_bot_dashboard_direct_session, _clear_bot_sessions, _migrate_bot_resources_store, _normalize_env_params, _normalize_resource_limits, _read_bot_config, _read_bot_resources, _read_cron_store, _read_env_store, _safe_float, _safe_int, _workspace_root, _write_bot_config, _write_bot_resources, _write_cron_store, _write_env_store, ) from services.bot_channel_service import ( _channel_api_to_cfg, _get_bot_channels_from_config, _normalize_channel_extra, _normalize_initial_channels, _read_global_delivery_flags, _sync_workspace_channels_impl, ) from services.bot_mcp_service import ( _merge_mcp_servers_preserving_extras, _normalize_mcp_servers, _sanitize_mcp_servers_in_config_data, ) from services.template_service import get_agent_md_templates __all__ = [ "_bot_data_root", "_channel_api_to_cfg", "_clear_bot_dashboard_direct_session", "_clear_bot_sessions", "_get_bot_channels_from_config", "_migrate_bot_resources_store", "_normalize_channel_extra", "_normalize_env_params", "_normalize_initial_channels", "_normalize_mcp_servers", "_normalize_resource_limits", "_normalize_system_timezone", "_provider_defaults", "_read_bot_config", "_read_bot_resources", "_read_bot_runtime_snapshot", "_read_cron_store", "_read_env_store", "_read_global_delivery_flags", "_resolve_bot_env_params", "_safe_float", "_safe_int", "_sanitize_mcp_servers_in_config_data", "_serialize_bot", "_serialize_bot_list_item", "_sync_workspace_channels", "_workspace_root", "_write_bot_config", "_write_bot_resources", "_write_cron_store", "_write_env_store", "_merge_mcp_servers_preserving_extras", ] def _get_default_system_timezone() -> str: value = str(DEFAULT_BOT_SYSTEM_TIMEZONE or "").strip() or "Asia/Shanghai" try: ZoneInfo(value) return value except Exception: return "Asia/Shanghai" def _normalize_system_timezone(raw: Any) -> str: value = str(raw or "").strip() if not value: return _get_default_system_timezone() try: ZoneInfo(value) except Exception as exc: raise ValueError("Invalid system timezone. Use an IANA timezone such as Asia/Shanghai.") from exc return value def _resolve_bot_env_params(bot_id: str, raw: Optional[Dict[str, str]] = None) -> Dict[str, str]: env_params = _normalize_env_params(raw if isinstance(raw, dict) else _read_env_store(bot_id)) try: env_params["TZ"] = _normalize_system_timezone(env_params.get("TZ")) except ValueError: env_params["TZ"] = _get_default_system_timezone() return env_params def _provider_defaults(provider: str) -> tuple[str, str]: normalized = provider.lower().strip() if normalized in {"openai"}: return "openai", "https://api.openai.com/v1" if normalized in {"openrouter"}: return "openrouter", "https://openrouter.ai/api/v1" if normalized in {"dashscope", "aliyun", "qwen", "aliyun-qwen"}: return "dashscope", "https://dashscope.aliyuncs.com/compatible-mode/v1" if normalized in {"deepseek"}: return "deepseek", "https://api.deepseek.com/v1" if normalized in {"xunfei", "iflytek", "xfyun"}: return "openai", "https://spark-api-open.xf-yun.com/v1" if normalized in {"vllm"}: return "openai", "" if normalized in {"kimi", "moonshot"}: return "kimi", "https://api.moonshot.cn/v1" if normalized in {"minimax"}: return "minimax", "https://api.minimax.chat/v1" return normalized, "" def _read_workspace_md(bot_id: str, filename: str, default_value: str) -> str: path = os.path.join(_workspace_root(bot_id), filename) if not os.path.isfile(path): return default_value try: with open(path, "r", encoding="utf-8") as f: return f.read().strip() except Exception: return default_value def _read_bot_runtime_snapshot(bot: BotInstance) -> Dict[str, Any]: config_data = _read_bot_config(bot.id) env_params = _resolve_bot_env_params(bot.id) template_defaults = get_agent_md_templates() provider_name = "" provider_cfg: Dict[str, Any] = {} providers_cfg = config_data.get("providers") if isinstance(providers_cfg, dict): for p_name, p_cfg in providers_cfg.items(): provider_name = str(p_name or "").strip() if isinstance(p_cfg, dict): provider_cfg = p_cfg break agents_defaults: Dict[str, Any] = {} agents_cfg = config_data.get("agents") if isinstance(agents_cfg, dict): defaults = agents_cfg.get("defaults") if isinstance(defaults, dict): agents_defaults = defaults channels_cfg = config_data.get("channels") send_progress, send_tool_hints = _read_global_delivery_flags(channels_cfg) llm_provider = provider_name or "" llm_model = str(agents_defaults.get("model") or "") api_key = str(provider_cfg.get("apiKey") or "").strip() api_base = str(provider_cfg.get("apiBase") or "").strip() api_base_lower = api_base.lower() provider_alias = str(provider_cfg.get("dashboardProviderAlias") or "").strip().lower() if llm_provider == "openai" and provider_alias in {"xunfei", "iflytek", "xfyun", "vllm"}: llm_provider = "xunfei" if provider_alias in {"iflytek", "xfyun"} else provider_alias elif llm_provider == "openai" and ("spark-api-open.xf-yun.com" in api_base_lower or "xf-yun.com" in api_base_lower): llm_provider = "xunfei" soul_md = _read_workspace_md(bot.id, "SOUL.md", template_defaults.get("soul_md", "")) resources = _read_bot_resources(bot.id, config_data=config_data) return { "llm_provider": llm_provider, "llm_model": llm_model, "api_key": api_key, "api_base": api_base, "temperature": _safe_float(agents_defaults.get("temperature"), 0.2), "top_p": _safe_float(agents_defaults.get("topP"), 1.0), "max_tokens": _safe_int(agents_defaults.get("maxTokens"), 8192), "cpu_cores": resources["cpu_cores"], "memory_mb": resources["memory_mb"], "storage_gb": resources["storage_gb"], "system_timezone": env_params.get("TZ") or _get_default_system_timezone(), "send_progress": send_progress, "send_tool_hints": send_tool_hints, "soul_md": soul_md, "agents_md": _read_workspace_md(bot.id, "AGENTS.md", template_defaults.get("agents_md", "")), "user_md": _read_workspace_md(bot.id, "USER.md", template_defaults.get("user_md", "")), "tools_md": _read_workspace_md(bot.id, "TOOLS.md", template_defaults.get("tools_md", "")), "identity_md": _read_workspace_md(bot.id, "IDENTITY.md", template_defaults.get("identity_md", "")), "system_prompt": soul_md, } def _serialize_bot(bot: BotInstance) -> Dict[str, Any]: runtime = _read_bot_runtime_snapshot(bot) created_at = bot.created_at.isoformat() + "Z" if bot.created_at else None updated_at = bot.updated_at.isoformat() + "Z" if bot.updated_at else None return { "id": bot.id, "name": bot.name, "enabled": bool(getattr(bot, "enabled", True)), "access_password": bot.access_password or "", "has_access_password": bool(str(bot.access_password or "").strip()), "avatar_model": "base", "avatar_skin": "blue_suit", "image_tag": bot.image_tag, "llm_provider": runtime.get("llm_provider") or "", "llm_model": runtime.get("llm_model") or "", "system_prompt": runtime.get("system_prompt") or "", "api_base": runtime.get("api_base") or "", "temperature": _safe_float(runtime.get("temperature"), 0.2), "top_p": _safe_float(runtime.get("top_p"), 1.0), "max_tokens": _safe_int(runtime.get("max_tokens"), 8192), "cpu_cores": _safe_float(runtime.get("cpu_cores"), 1.0), "memory_mb": _safe_int(runtime.get("memory_mb"), 1024), "storage_gb": _safe_int(runtime.get("storage_gb"), 10), "system_timezone": str(runtime.get("system_timezone") or _get_default_system_timezone()), "send_progress": bool(runtime.get("send_progress")), "send_tool_hints": bool(runtime.get("send_tool_hints")), "soul_md": runtime.get("soul_md") or "", "agents_md": runtime.get("agents_md") or "", "user_md": runtime.get("user_md") or "", "tools_md": runtime.get("tools_md") or "", "identity_md": runtime.get("identity_md") or "", "workspace_dir": bot.workspace_dir, "docker_status": bot.docker_status, "current_state": bot.current_state, "last_action": bot.last_action, "created_at": created_at, "updated_at": updated_at, } def _serialize_bot_list_item(bot: BotInstance) -> Dict[str, Any]: created_at = bot.created_at.isoformat() + "Z" if bot.created_at else None updated_at = bot.updated_at.isoformat() + "Z" if bot.updated_at else None return { "id": bot.id, "name": bot.name, "enabled": bool(getattr(bot, "enabled", True)), "has_access_password": bool(str(bot.access_password or "").strip()), "image_tag": bot.image_tag, "docker_status": bot.docker_status, "current_state": bot.current_state, "last_action": bot.last_action, "created_at": created_at, "updated_at": updated_at, } def _sync_workspace_channels( session: Session, bot_id: str, 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 snapshot = _read_bot_runtime_snapshot(bot) _sync_workspace_channels_impl( session, bot_id, snapshot, channels_override=channels_override, global_delivery_override=global_delivery_override, runtime_overrides=runtime_overrides, )