367 lines
15 KiB
Python
367 lines
15 KiB
Python
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"))
|