dashboard-nanobot/backend/services/bot_service.py

270 lines
10 KiB
Python

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,
)