mirror of
https://github.com/AstrBotDevs/AstrBot
synced 2026-07-17 01:49:15 +08:00
feat: enhance platform management with status tracking and error handling
- Introduced PlatformStatus enum to manage platform states (pending, running, error, stopped). - Added error recording and retrieval functionality in the Platform class. - Implemented a new method in PlatformManager to gather statistics for all platforms. - Updated the dashboard to display platform statuses and error details, including a dialog for error insights. - Enhanced localization for runtime statuses and error dialogs in both English and Chinese.
This commit is contained in:
@@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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]:
|
||||
"""得到一个平台的运行实例,需要返回一个协程对象。"""
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -58,5 +58,22 @@
|
||||
"connected": "已连接",
|
||||
"disconnected": "已断开",
|
||||
"error": "错误"
|
||||
},
|
||||
"runtimeStatus": {
|
||||
"running": "运行中",
|
||||
"error": "发生错误",
|
||||
"pending": "等待启动",
|
||||
"stopped": "已停止",
|
||||
"unknown": "未知",
|
||||
"errors": "个错误"
|
||||
},
|
||||
"errorDialog": {
|
||||
"title": "错误详情",
|
||||
"platformId": "平台 ID",
|
||||
"errorCount": "错误数量",
|
||||
"lastError": "最近错误",
|
||||
"occurredAt": "发生时间",
|
||||
"traceback": "错误堆栈",
|
||||
"close": "关闭"
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,33 @@
|
||||
:bglogo="getPlatformIcon(platform.type || platform.id)" @toggle-enabled="platformStatusChange"
|
||||
@delete="deletePlatform" @edit="editPlatform">
|
||||
<template #item-details="{ item }">
|
||||
<!-- 平台运行状态 - 只在非运行状态或有错误时显示 -->
|
||||
<div class="platform-status-row mb-2" v-if="getPlatformStat(item.id) && (getPlatformStat(item.id)?.status !== 'running' || getPlatformStat(item.id)?.error_count > 0)">
|
||||
<!-- 状态 chip - 只在非 running 状态时显示 -->
|
||||
<v-chip
|
||||
v-if="getPlatformStat(item.id)?.status !== 'running'"
|
||||
size="small"
|
||||
:color="getStatusColor(getPlatformStat(item.id)?.status)"
|
||||
variant="tonal"
|
||||
class="status-chip"
|
||||
>
|
||||
<v-icon size="small" start>{{ getStatusIcon(getPlatformStat(item.id)?.status) }}</v-icon>
|
||||
{{ tm('runtimeStatus.' + (getPlatformStat(item.id)?.status || 'unknown')) }}
|
||||
</v-chip>
|
||||
<!-- 错误数量提示 -->
|
||||
<v-chip
|
||||
v-if="getPlatformStat(item.id)?.error_count > 0"
|
||||
size="small"
|
||||
color="error"
|
||||
variant="tonal"
|
||||
class="error-chip"
|
||||
:class="{ 'ms-2': getPlatformStat(item.id)?.status !== 'running' }"
|
||||
@click.stop="showErrorDetails(item)"
|
||||
>
|
||||
<v-icon size="small" start>mdi-bug</v-icon>
|
||||
{{ getPlatformStat(item.id)?.error_count }} {{ tm('runtimeStatus.errors') }}
|
||||
</v-chip>
|
||||
</div>
|
||||
<div v-if="item.unified_webhook_mode && item.webhook_uuid" class="webhook-info">
|
||||
<v-chip
|
||||
size="small"
|
||||
@@ -111,6 +138,47 @@
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- 错误详情对话框 -->
|
||||
<v-dialog v-model="showErrorDialog" max-width="700">
|
||||
<v-card>
|
||||
<v-card-title class="d-flex align-center pa-4">
|
||||
<v-icon class="me-2" color="error">mdi-alert-circle</v-icon>
|
||||
{{ tm('errorDialog.title') }}
|
||||
</v-card-title>
|
||||
<v-card-text class="px-4 pb-4" v-if="currentErrorPlatform">
|
||||
<div class="mb-3">
|
||||
<strong>{{ tm('errorDialog.platformId') }}:</strong> {{ currentErrorPlatform.id }}
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<strong>{{ tm('errorDialog.errorCount') }}:</strong> {{ currentErrorPlatform.error_count }}
|
||||
</div>
|
||||
<div v-if="currentErrorPlatform.last_error" class="error-details">
|
||||
<div class="mb-2">
|
||||
<strong>{{ tm('errorDialog.lastError') }}:</strong>
|
||||
</div>
|
||||
<v-alert type="error" variant="tonal" class="mb-3">
|
||||
<div class="error-message">{{ currentErrorPlatform.last_error.message }}</div>
|
||||
<div class="error-time text-caption text-medium-emphasis mt-1">
|
||||
{{ tm('errorDialog.occurredAt') }}: {{ new Date(currentErrorPlatform.last_error.timestamp).toLocaleString() }}
|
||||
</div>
|
||||
</v-alert>
|
||||
<div v-if="currentErrorPlatform.last_error.traceback">
|
||||
<div class="mb-2">
|
||||
<strong>{{ tm('errorDialog.traceback') }}:</strong>
|
||||
</div>
|
||||
<pre class="traceback-box">{{ currentErrorPlatform.last_error.traceback }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</v-card-text>
|
||||
<v-card-actions class="pa-4 pt-0">
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn variant="tonal" color="primary" @click="showErrorDialog = false">
|
||||
{{ tm('errorDialog.close') }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- 消息提示 -->
|
||||
<v-snackbar :timeout="3000" elevation="24" :color="save_message_success" v-model="save_message_snack"
|
||||
location="top">
|
||||
@@ -167,6 +235,14 @@ export default {
|
||||
showWebhookDialog: false,
|
||||
currentWebhookUuid: '',
|
||||
|
||||
// 平台统计信息
|
||||
platformStats: {},
|
||||
statsRefreshInterval: null,
|
||||
|
||||
// 错误详情对话框
|
||||
showErrorDialog: false,
|
||||
currentErrorPlatform: null,
|
||||
|
||||
store: useCommonStore()
|
||||
}
|
||||
},
|
||||
@@ -189,6 +265,17 @@ export default {
|
||||
|
||||
mounted() {
|
||||
this.getConfig();
|
||||
this.getPlatformStats();
|
||||
// 每 10 秒刷新一次平台状态
|
||||
this.statsRefreshInterval = setInterval(() => {
|
||||
this.getPlatformStats();
|
||||
}, 10000);
|
||||
},
|
||||
|
||||
beforeUnmount() {
|
||||
if (this.statsRefreshInterval) {
|
||||
clearInterval(this.statsRefreshInterval);
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
@@ -213,6 +300,53 @@ export default {
|
||||
});
|
||||
},
|
||||
|
||||
getPlatformStats() {
|
||||
axios.get('/api/platform/stats').then((res) => {
|
||||
if (res.data.status === 'ok') {
|
||||
// 将数组转换为以 id 为 key 的对象,方便查找
|
||||
const stats = {};
|
||||
for (const platform of res.data.data.platforms || []) {
|
||||
stats[platform.id] = platform;
|
||||
}
|
||||
this.platformStats = stats;
|
||||
}
|
||||
}).catch((err) => {
|
||||
console.warn('获取平台统计信息失败:', err);
|
||||
});
|
||||
},
|
||||
|
||||
getPlatformStat(platformId) {
|
||||
return this.platformStats[platformId] || null;
|
||||
},
|
||||
|
||||
getStatusColor(status) {
|
||||
switch (status) {
|
||||
case 'running': return 'success';
|
||||
case 'error': return 'error';
|
||||
case 'pending': return 'warning';
|
||||
case 'stopped': return 'grey';
|
||||
default: return 'grey';
|
||||
}
|
||||
},
|
||||
|
||||
getStatusIcon(status) {
|
||||
switch (status) {
|
||||
case 'running': return 'mdi-check-circle';
|
||||
case 'error': return 'mdi-alert-circle';
|
||||
case 'pending': return 'mdi-clock-outline';
|
||||
case 'stopped': return 'mdi-stop-circle';
|
||||
default: return 'mdi-help-circle';
|
||||
}
|
||||
},
|
||||
|
||||
showErrorDetails(platform) {
|
||||
const stat = this.getPlatformStat(platform.id);
|
||||
if (stat && stat.error_count > 0) {
|
||||
this.currentErrorPlatform = stat;
|
||||
this.showErrorDialog = true;
|
||||
}
|
||||
},
|
||||
|
||||
editPlatform(platform) {
|
||||
this.updatingPlatformConfig = JSON.parse(JSON.stringify(platform));
|
||||
this.updatingMode = true;
|
||||
@@ -326,4 +460,42 @@ export default {
|
||||
.webhook-chip {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.platform-status-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.status-chip {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.error-chip {
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.error-details {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.traceback-box {
|
||||
background-color: #1e1e1e;
|
||||
color: #d4d4d4;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
overflow-x: auto;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user