From 2743c4d0f31f08361d7518b6b573a57782031f7f Mon Sep 17 00:00:00 2001 From: "mula.liu" Date: Mon, 2 Mar 2026 00:38:24 +0800 Subject: [PATCH] add dashboard.py --- bot-images/dashboard.py | 94 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 bot-images/dashboard.py diff --git a/bot-images/dashboard.py b/bot-images/dashboard.py new file mode 100644 index 0000000..ae0c3a2 --- /dev/null +++ b/bot-images/dashboard.py @@ -0,0 +1,94 @@ +from __future__ import annotations +import asyncio +from aiohttp import web +from loguru import logger +from typing import Any +import json + +from nanobot.channels.base import BaseChannel +from nanobot.bus.events import InboundMessage, OutboundMessage + +class DashboardChannel(BaseChannel): + """ + 专门为管理面板设计的渠道。 + 它充当机器人内部总线 (Bus) 与宿主机面板之间的桥梁。 + """ + + def __init__(self, config: Any, bus: Any, host: str = "0.0.0.0", port: int = 9000): + super().__init__(config, bus) + self.host = host + self.port = port + self.runner = None + + async def start(self) -> None: + """启动 Dashboard HTTP 服务""" + app = web.Application() + app.router.add_post("/chat", self._handle_chat) + + self.runner = web.AppRunner(app) + await self.runner.setup() + site = web.TCPSite(self.runner, self.host, self.port) + + await site.start() + self._is_running = True + logger.info(f"🚀 Dashboard Channel 代理已上线,监听端口: {self.port}") + + async def stop(self) -> None: + """停止服务""" + if self.runner: + await self.runner.cleanup() + self._is_running = False + logger.info("Dashboard Channel 已下线") + + async def send(self, message: OutboundMessage) -> None: + """ + 从总线 (Bus) 接收机器人发出的所有消息,并结构化输出到 stdout。 + """ + media = [str(v).strip().replace("\\", "/") for v in (message.media or []) if str(v).strip()] + if not message.content and not media: + return + + # 核心:从元数据识别消息类型(进度更新 vs 最终回复) + is_progress = message.metadata.get("_progress", False) + is_tool_hint = message.metadata.get("_tool_hint", False) + + payload = { + "type": "BUS_EVENT", + "source": "dashboard_channel", + "is_progress": is_progress, + "is_tool": is_tool_hint, + "content": message.content, + "media": media, + } + + # 使用 JSON 格式输出,方便面板后端精准解析,告别正则 + print(f"\n__DASHBOARD_DATA_START__{json.dumps(payload, ensure_ascii=False)}__DASHBOARD_DATA_END__\n", flush=True) + + async def _handle_chat(self, request: web.Request) -> web.Response: + """处理来自面板的指令入站""" + try: + data = await request.json() + user_message = data.get("message", "").strip() + media = [str(v).strip().replace("\\", "/") for v in (data.get("media") or []) if str(v).strip()] + + if not user_message and not media: + return web.json_response({"status": "error", "reason": "empty message and media"}, status=400) + if not user_message: + user_message = "[attachment message]" + + # 调试日志:打印收到的原始消息长度和前 20 个字符,确保中文未乱码 + logger.info(f"📥 [Dashboard Channel] 收到指令 (len={len(user_message)}): {user_message[:20]}...") + + # 将消息塞入总线 + await self.bus.publish_inbound(InboundMessage( + channel="dashboard", + sender_id="user", + chat_id="direct", + content=user_message, + media=media, + )) + + return web.json_response({"status": "ok"}) + except Exception as e: + logger.error(f"❌ Dashboard Channel 接收指令失败: {e}") + return web.json_response({"status": "error", "reason": str(e)}, status=500)