dashboard-nanobot/backend/services/bot_service.py

270 lines
10 KiB
Python
Raw Normal View History

2026-03-31 04:31:47 +00:00
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,
2026-03-31 06:04:34 +00:00
_sync_workspace_channels_impl,
2026-03-31 04:31:47 +00:00
)
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)
2026-03-31 06:56:31 +00:00
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
2026-03-31 04:31:47 +00:00
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,
2026-03-31 06:56:31 +00:00
"created_at": created_at,
"updated_at": updated_at,
2026-03-31 04:31:47 +00:00
}
def _serialize_bot_list_item(bot: BotInstance) -> Dict[str, Any]:
2026-03-31 06:56:31 +00:00
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
2026-03-31 04:31:47 +00:00
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,
2026-03-31 06:56:31 +00:00
"created_at": created_at,
"updated_at": updated_at,
2026-03-31 04:31:47 +00:00
}
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,
)