diff --git a/astrbot/core/platform/manager.py b/astrbot/core/platform/manager.py index 9ff892025..18a2def74 100644 --- a/astrbot/core/platform/manager.py +++ b/astrbot/core/platform/manager.py @@ -6,7 +6,7 @@ from astrbot.core import logger from astrbot.core.config.astrbot_config import AstrBotConfig from astrbot.core.star.star_handler import EventType, star_handlers_registry, star_map -from .platform import Platform +from .platform import Platform, PlatformStatus from .register import platform_cls_map from .sources.webchat.webchat_adapter import WebChatAdapter @@ -16,7 +16,7 @@ class PlatformManager: self.platform_insts: list[Platform] = [] """加载的 Platform 的实例""" - self._inst_map = {} + self._inst_map: dict[str, dict] = {} self.platforms_config = config["platform"] self.settings = config["platform_settings"] @@ -37,7 +37,10 @@ class PlatformManager: webchat_inst = WebChatAdapter({}, self.settings, self.event_queue) self.platform_insts.append(webchat_inst) asyncio.create_task( - self._task_wrapper(asyncio.create_task(webchat_inst.run(), name="webchat")), + self._task_wrapper( + asyncio.create_task(webchat_inst.run(), name="webchat"), + platform=webchat_inst, + ), ) async def load_platform(self, platform_config: dict): @@ -131,6 +134,7 @@ class PlatformManager: inst.run(), name=f"platform_{platform_config['type']}_{platform_config['id']}", ), + platform=inst, ), ) handlers = star_handlers_registry.get_handlers_by_event_type( @@ -145,17 +149,28 @@ class PlatformManager: except Exception: logger.error(traceback.format_exc()) - async def _task_wrapper(self, task: asyncio.Task): + async def _task_wrapper(self, task: asyncio.Task, platform: Platform | None = None): + # 设置平台状态为运行中 + if platform: + platform.status = PlatformStatus.RUNNING + try: await task except asyncio.CancelledError: - pass + if platform: + platform.status = PlatformStatus.STOPPED except Exception as e: + error_msg = str(e) + tb_str = traceback.format_exc() logger.error(f"------- 任务 {task.get_name()} 发生错误: {e}") - for line in traceback.format_exc().split("\n"): + for line in tb_str.split("\n"): logger.error(f"| {line}") logger.error("-------") + # 记录错误到平台实例 + if platform: + platform.record_error(error_msg, tb_str) + async def reload(self, platform_config: dict): await self.terminate_platform(platform_config["id"]) if platform_config["enable"]: @@ -172,9 +187,9 @@ class PlatformManager: logger.info(f"正在尝试终止 {platform_id} 平台适配器 ...") # client_id = self._inst_map.pop(platform_id, None) - info = self._inst_map.pop(platform_id, None) + info = self._inst_map.pop(platform_id) client_id = info["client_id"] - inst = info["inst"] + inst: Platform = info["inst"] try: self.platform_insts.remove( next( @@ -196,3 +211,46 @@ class PlatformManager: def get_insts(self): return self.platform_insts + + def get_all_stats(self) -> dict: + """获取所有平台的统计信息 + + Returns: + 包含所有平台统计信息的字典 + """ + stats_list = [] + total_errors = 0 + running_count = 0 + error_count = 0 + + for inst in self.platform_insts: + try: + stat = inst.get_stats() + stats_list.append(stat) + total_errors += stat.get("error_count", 0) + if stat.get("status") == PlatformStatus.RUNNING.value: + running_count += 1 + elif stat.get("status") == PlatformStatus.ERROR.value: + error_count += 1 + except Exception as e: + # 如果获取统计信息失败,记录基本信息 + logger.warning(f"获取平台统计信息失败: {e}") + stats_list.append( + { + "id": getattr(inst, "config", {}).get("id", "unknown"), + "type": "unknown", + "status": "unknown", + "error_count": 0, + "last_error": None, + } + ) + + return { + "platforms": stats_list, + "summary": { + "total": len(stats_list), + "running": running_count, + "error": error_count, + "total_errors": total_errors, + }, + } diff --git a/astrbot/core/platform/platform.py b/astrbot/core/platform/platform.py index ef5a0c07e..2b3a87d8b 100644 --- a/astrbot/core/platform/platform.py +++ b/astrbot/core/platform/platform.py @@ -2,6 +2,9 @@ import abc import uuid from asyncio import Queue from collections.abc import Awaitable +from dataclasses import dataclass, field +from datetime import datetime +from enum import Enum from typing import Any from astrbot.core.message.message_event_result import MessageChain @@ -12,6 +15,24 @@ from .message_session import MessageSesion from .platform_metadata import PlatformMetadata +class PlatformStatus(Enum): + """平台运行状态""" + + PENDING = "pending" # 待启动 + RUNNING = "running" # 运行中 + ERROR = "error" # 发生错误 + STOPPED = "stopped" # 已停止 + + +@dataclass +class PlatformError: + """平台错误信息""" + + message: str + timestamp: datetime = field(default_factory=datetime.now) + traceback: str | None = None + + class Platform(abc.ABC): def __init__(self, config: dict, event_queue: Queue): super().__init__() @@ -21,6 +42,63 @@ class Platform(abc.ABC): self._event_queue = event_queue self.client_self_id = uuid.uuid4().hex + # 平台运行状态 + self._status: PlatformStatus = PlatformStatus.PENDING + self._errors: list[PlatformError] = [] + self._started_at: datetime | None = None + + @property + def status(self) -> PlatformStatus: + """获取平台运行状态""" + return self._status + + @status.setter + def status(self, value: PlatformStatus): + """设置平台运行状态""" + self._status = value + if value == PlatformStatus.RUNNING and self._started_at is None: + self._started_at = datetime.now() + + @property + def errors(self) -> list[PlatformError]: + """获取错误列表""" + return self._errors + + @property + def last_error(self) -> PlatformError | None: + """获取最近的错误""" + return self._errors[-1] if self._errors else None + + def record_error(self, message: str, traceback_str: str | None = None): + """记录一个错误""" + self._errors.append(PlatformError(message=message, traceback=traceback_str)) + self._status = PlatformStatus.ERROR + + def clear_errors(self): + """清除错误记录""" + self._errors.clear() + if self._status == PlatformStatus.ERROR: + self._status = PlatformStatus.RUNNING + + def get_stats(self) -> dict: + """获取平台统计信息""" + meta = self.meta() + return { + "id": meta.id or self.config.get("id"), + "type": meta.name, + "display_name": meta.adapter_display_name or meta.name, + "status": self._status.value, + "started_at": self._started_at.isoformat() if self._started_at else None, + "error_count": len(self._errors), + "last_error": { + "message": self.last_error.message, + "timestamp": self.last_error.timestamp.isoformat(), + "traceback": self.last_error.traceback, + } + if self.last_error + else None, + } + @abc.abstractmethod def run(self) -> Awaitable[Any]: """得到一个平台的运行实例,需要返回一个协程对象。""" diff --git a/astrbot/dashboard/routes/platform.py b/astrbot/dashboard/routes/platform.py index 1a7394187..5b709a628 100644 --- a/astrbot/dashboard/routes/platform.py +++ b/astrbot/dashboard/routes/platform.py @@ -24,8 +24,6 @@ class PlatformRoute(Route): self.core_lifecycle = core_lifecycle self.platform_manager = core_lifecycle.platform_manager - # 路由不使用标准的 /api 前缀,因为 webhook 回调需要直接访问 - # 所以我们手动注册路由 self._register_webhook_routes() def _register_webhook_routes(self): @@ -37,6 +35,13 @@ class PlatformRoute(Route): methods=["GET", "POST"], ) + # 平台统计信息接口 + self.app.add_url_rule( + "/api/platform/stats", + view_func=self.get_platform_stats, + methods=["GET"], + ) + async def unified_webhook_callback(self, webhook_uuid: str): """统一 webhook 回调入口 @@ -80,3 +85,16 @@ class PlatformRoute(Route): if platform.config.get("unified_webhook_mode", False): return platform return None + + async def get_platform_stats(self): + """获取所有平台的统计信息 + + Returns: + 包含平台统计信息的响应 + """ + try: + stats = self.platform_manager.get_all_stats() + return Response().ok(stats).__dict__ + except Exception as e: + logger.error(f"获取平台统计信息失败: {e}", exc_info=True) + return Response().error(f"获取统计信息失败: {e}").__dict__, 500 diff --git a/dashboard/src/i18n/locales/en-US/features/platform.json b/dashboard/src/i18n/locales/en-US/features/platform.json index 20ea1d0ff..96a2696aa 100644 --- a/dashboard/src/i18n/locales/en-US/features/platform.json +++ b/dashboard/src/i18n/locales/en-US/features/platform.json @@ -58,5 +58,22 @@ "connected": "Connected", "disconnected": "Disconnected", "error": "Error" + }, + "runtimeStatus": { + "running": "Running", + "error": "Error", + "pending": "Pending", + "stopped": "Stopped", + "unknown": "Unknown", + "errors": "error(s)" + }, + "errorDialog": { + "title": "Error Details", + "platformId": "Platform ID", + "errorCount": "Error Count", + "lastError": "Last Error", + "occurredAt": "Occurred At", + "traceback": "Traceback", + "close": "Close" } } \ No newline at end of file diff --git a/dashboard/src/i18n/locales/zh-CN/features/platform.json b/dashboard/src/i18n/locales/zh-CN/features/platform.json index 3ffcbb7a1..7f8ef7c0d 100644 --- a/dashboard/src/i18n/locales/zh-CN/features/platform.json +++ b/dashboard/src/i18n/locales/zh-CN/features/platform.json @@ -58,5 +58,22 @@ "connected": "已连接", "disconnected": "已断开", "error": "错误" + }, + "runtimeStatus": { + "running": "运行中", + "error": "发生错误", + "pending": "等待启动", + "stopped": "已停止", + "unknown": "未知", + "errors": "个错误" + }, + "errorDialog": { + "title": "错误详情", + "platformId": "平台 ID", + "errorCount": "错误数量", + "lastError": "最近错误", + "occurredAt": "发生时间", + "traceback": "错误堆栈", + "close": "关闭" } } \ No newline at end of file diff --git a/dashboard/src/views/PlatformPage.vue b/dashboard/src/views/PlatformPage.vue index 821e74626..c4beb40f1 100644 --- a/dashboard/src/views/PlatformPage.vue +++ b/dashboard/src/views/PlatformPage.vue @@ -30,6 +30,33 @@ :bglogo="getPlatformIcon(platform.type || platform.id)" @toggle-enabled="platformStatusChange" @delete="deletePlatform" @edit="editPlatform">