mirror of
https://github.com/AstrBotDevs/AstrBot
synced 2026-07-07 21:40:14 +08:00
Compare commits
7 Commits
dependabot
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
331444b41f | ||
|
|
39090a74ba | ||
|
|
bf18fadbe6 | ||
|
|
1124dfe295 | ||
|
|
510e833695 | ||
|
|
0a5d6485a1 | ||
|
|
0dee40bd9d |
20
.github/workflows/docker-image.yml
vendored
20
.github/workflows/docker-image.yml
vendored
@@ -71,20 +71,20 @@ jobs:
|
||||
echo "build_date=$build_date" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Set QEMU
|
||||
uses: docker/setup-qemu-action@v4.2.0
|
||||
uses: docker/setup-qemu-action@v4.1.0
|
||||
|
||||
- name: Set Docker Buildx
|
||||
uses: docker/setup-buildx-action@v4.2.0
|
||||
uses: docker/setup-buildx-action@v4.1.0
|
||||
|
||||
- name: Log in to DockerHub
|
||||
uses: docker/login-action@v4.4.0
|
||||
uses: docker/login-action@v4.2.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
if: env.HAS_GHCR_TOKEN == 'true'
|
||||
uses: docker/login-action@v4.4.0
|
||||
uses: docker/login-action@v4.2.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ env.GHCR_OWNER }}
|
||||
@@ -105,7 +105,7 @@ jobs:
|
||||
echo "EOF" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Build and Push Nightly Image
|
||||
uses: docker/build-push-action@v7.3.0
|
||||
uses: docker/build-push-action@v7.2.0
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
@@ -171,27 +171,27 @@ jobs:
|
||||
cp -r dashboard/dist astrbot/dashboard/dist
|
||||
|
||||
- name: Set QEMU
|
||||
uses: docker/setup-qemu-action@v4.2.0
|
||||
uses: docker/setup-qemu-action@v4.1.0
|
||||
|
||||
- name: Set Docker Buildx
|
||||
uses: docker/setup-buildx-action@v4.2.0
|
||||
uses: docker/setup-buildx-action@v4.1.0
|
||||
|
||||
- name: Log in to DockerHub
|
||||
uses: docker/login-action@v4.4.0
|
||||
uses: docker/login-action@v4.2.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
if: env.HAS_GHCR_TOKEN == 'true'
|
||||
uses: docker/login-action@v4.4.0
|
||||
uses: docker/login-action@v4.2.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ env.GHCR_OWNER }}
|
||||
password: ${{ secrets.GHCR_GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and Push Release Image
|
||||
uses: docker/build-push-action@v7.3.0
|
||||
uses: docker/build-push-action@v7.2.0
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import logging
|
||||
|
||||
__version__ = "4.26.4"
|
||||
__version__ = "4.26.5"
|
||||
logger = logging.getLogger("astrbot")
|
||||
|
||||
@@ -35,6 +35,9 @@ from astrbot.core.star.star_manager import PluginManager
|
||||
from astrbot.core.subagent_orchestrator import SubAgentOrchestrator
|
||||
from astrbot.core.umop_config_router import UmopConfigRouter
|
||||
from astrbot.core.updator import AstrBotUpdator
|
||||
from astrbot.core.utils.event_loop_diagnostics import (
|
||||
create_event_loop_diagnostic_tasks,
|
||||
)
|
||||
from astrbot.core.utils.llm_metadata import update_llm_metadata
|
||||
from astrbot.core.utils.migra_helper import migra
|
||||
from astrbot.core.utils.temp_dir_cleaner import TempDirCleaner
|
||||
@@ -296,13 +299,18 @@ class AstrBotCoreLifecycle:
|
||||
self.temp_dir_cleaner.run(),
|
||||
name="temp_dir_cleaner",
|
||||
)
|
||||
diagnostic_tasks = create_event_loop_diagnostic_tasks()
|
||||
|
||||
# 把插件中注册的所有协程函数注册到事件总线中并执行
|
||||
extra_tasks = []
|
||||
for task in self.star_context._register_tasks:
|
||||
extra_tasks.append(asyncio.create_task(task, name=task.__name__)) # type: ignore
|
||||
|
||||
tasks_ = [event_bus_task, *(extra_tasks if extra_tasks else [])]
|
||||
tasks_ = [
|
||||
event_bus_task,
|
||||
*diagnostic_tasks,
|
||||
*(extra_tasks if extra_tasks else []),
|
||||
]
|
||||
if cron_task:
|
||||
tasks_.append(cron_task)
|
||||
if temp_dir_cleaner_task:
|
||||
|
||||
216
astrbot/core/utils/event_loop_diagnostics.py
Normal file
216
astrbot/core/utils/event_loop_diagnostics.py
Normal file
@@ -0,0 +1,216 @@
|
||||
import asyncio
|
||||
import faulthandler
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import TextIO
|
||||
|
||||
from astrbot import logger
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
||||
|
||||
DEFAULT_LAG_MONITOR_ENABLED = True
|
||||
DEFAULT_LAG_MONITOR_INTERVAL = 5.0
|
||||
DEFAULT_LAG_MONITOR_THRESHOLD = 15.0
|
||||
DEFAULT_WATCHDOG_ENABLED = True
|
||||
DEFAULT_WATCHDOG_INTERVAL = 5.0
|
||||
DEFAULT_WATCHDOG_TIMEOUT = 30.0
|
||||
DEFAULT_WATCHDOG_LOG_RELATIVE_PATH = Path("logs") / "event_loop_watchdog.log"
|
||||
DEFAULT_WATCHDOG_LOG_MAX_BYTES = 1024 * 1024
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class EventLoopDiagnosticSettings:
|
||||
"""Settings for event loop lag and blockage diagnostics.
|
||||
|
||||
Args:
|
||||
lag_monitor_enabled: Whether to log event loop scheduling lag.
|
||||
lag_monitor_interval: Seconds between lag monitor wakeups.
|
||||
lag_monitor_threshold: Minimum lag seconds before logging a warning.
|
||||
watchdog_enabled: Whether to arm the faulthandler watchdog.
|
||||
watchdog_interval: Seconds between faulthandler watchdog refreshes.
|
||||
watchdog_timeout: Seconds without event loop progress before dumping stacks.
|
||||
watchdog_log_path: File that receives faulthandler watchdog output.
|
||||
watchdog_log_max_bytes: Maximum watchdog log bytes before rotation.
|
||||
"""
|
||||
|
||||
lag_monitor_enabled: bool
|
||||
lag_monitor_interval: float
|
||||
lag_monitor_threshold: float
|
||||
watchdog_enabled: bool
|
||||
watchdog_interval: float
|
||||
watchdog_timeout: float
|
||||
watchdog_log_path: Path
|
||||
watchdog_log_max_bytes: int
|
||||
|
||||
|
||||
def _watchdog_log_path() -> Path:
|
||||
"""Resolve the watchdog stack dump log path.
|
||||
|
||||
Returns:
|
||||
Absolute path for watchdog stack dump output.
|
||||
"""
|
||||
return Path(get_astrbot_data_path()) / DEFAULT_WATCHDOG_LOG_RELATIVE_PATH
|
||||
|
||||
|
||||
def load_event_loop_diagnostic_settings() -> EventLoopDiagnosticSettings:
|
||||
"""Load fixed event loop diagnostic settings.
|
||||
|
||||
Returns:
|
||||
Event loop diagnostic settings.
|
||||
"""
|
||||
return EventLoopDiagnosticSettings(
|
||||
lag_monitor_enabled=DEFAULT_LAG_MONITOR_ENABLED,
|
||||
lag_monitor_interval=DEFAULT_LAG_MONITOR_INTERVAL,
|
||||
lag_monitor_threshold=DEFAULT_LAG_MONITOR_THRESHOLD,
|
||||
watchdog_enabled=DEFAULT_WATCHDOG_ENABLED,
|
||||
watchdog_interval=DEFAULT_WATCHDOG_INTERVAL,
|
||||
watchdog_timeout=DEFAULT_WATCHDOG_TIMEOUT,
|
||||
watchdog_log_path=_watchdog_log_path(),
|
||||
watchdog_log_max_bytes=DEFAULT_WATCHDOG_LOG_MAX_BYTES,
|
||||
)
|
||||
|
||||
|
||||
async def monitor_event_loop_lag(
|
||||
*,
|
||||
interval: float = DEFAULT_LAG_MONITOR_INTERVAL,
|
||||
warn_after: float = DEFAULT_LAG_MONITOR_THRESHOLD,
|
||||
) -> None:
|
||||
"""Log a warning when the event loop wakes significantly later than expected.
|
||||
|
||||
Args:
|
||||
interval: Seconds between monitor wakeups.
|
||||
warn_after: Minimum lag seconds before logging a warning.
|
||||
"""
|
||||
loop = asyncio.get_running_loop()
|
||||
expected = loop.time() + interval
|
||||
while True:
|
||||
await asyncio.sleep(interval)
|
||||
now = loop.time()
|
||||
lag = now - expected
|
||||
if lag > warn_after:
|
||||
logger.warning(
|
||||
"Event loop lag detected: %.3fs (threshold %.3fs).",
|
||||
lag,
|
||||
warn_after,
|
||||
)
|
||||
expected = now + interval
|
||||
|
||||
|
||||
def _rotate_watchdog_log_file(log_path: Path, max_bytes: int) -> None:
|
||||
"""Rotate the watchdog log when it reaches the configured size limit.
|
||||
|
||||
Args:
|
||||
log_path: Current watchdog log path.
|
||||
max_bytes: Maximum current log size before rotation.
|
||||
"""
|
||||
try:
|
||||
if not log_path.exists() or log_path.stat().st_size < max_bytes:
|
||||
return
|
||||
rotated_path = log_path.with_name(f"{log_path.name}.1")
|
||||
if rotated_path.exists():
|
||||
rotated_path.unlink()
|
||||
log_path.replace(rotated_path)
|
||||
except OSError as e:
|
||||
logger.warning("Failed to rotate event loop watchdog log %s: %s", log_path, e)
|
||||
|
||||
|
||||
def _open_watchdog_log_file(log_path: Path, max_bytes: int) -> TextIO:
|
||||
"""Open the watchdog log file after applying size-based rotation.
|
||||
|
||||
Args:
|
||||
log_path: Current watchdog log path.
|
||||
max_bytes: Maximum current log size before rotation.
|
||||
|
||||
Returns:
|
||||
Writable text file object for faulthandler output.
|
||||
"""
|
||||
log_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
_rotate_watchdog_log_file(log_path, max_bytes)
|
||||
return log_path.open("a", encoding="utf-8")
|
||||
|
||||
|
||||
async def faulthandler_event_loop_watchdog(
|
||||
*,
|
||||
timeout: float = DEFAULT_WATCHDOG_TIMEOUT,
|
||||
interval: float = DEFAULT_WATCHDOG_INTERVAL,
|
||||
dump_file: TextIO | None = None,
|
||||
dump_path: Path | None = None,
|
||||
max_bytes: int = DEFAULT_WATCHDOG_LOG_MAX_BYTES,
|
||||
) -> None:
|
||||
"""Dump all thread stacks if the event loop is blocked for too long.
|
||||
|
||||
Args:
|
||||
timeout: Seconds without watchdog refresh before faulthandler dumps stacks.
|
||||
interval: Seconds between watchdog refreshes while the event loop is healthy.
|
||||
dump_file: File object that receives faulthandler output.
|
||||
dump_path: Path that receives faulthandler output when dump_file is unset.
|
||||
max_bytes: Maximum current log size before rotation.
|
||||
"""
|
||||
log_path = dump_path or _watchdog_log_path()
|
||||
try:
|
||||
while True:
|
||||
faulthandler.cancel_dump_traceback_later()
|
||||
output: TextIO | None = None
|
||||
should_close = False
|
||||
try:
|
||||
output = dump_file or _open_watchdog_log_file(log_path, max_bytes)
|
||||
should_close = dump_file is None
|
||||
faulthandler.dump_traceback_later(
|
||||
timeout,
|
||||
repeat=False,
|
||||
file=output,
|
||||
)
|
||||
await asyncio.sleep(interval)
|
||||
except Exception as e:
|
||||
logger.warning("Event loop faulthandler watchdog failed: %s", e)
|
||||
await asyncio.sleep(interval)
|
||||
finally:
|
||||
faulthandler.cancel_dump_traceback_later()
|
||||
if should_close and output is not None:
|
||||
output.close()
|
||||
finally:
|
||||
faulthandler.cancel_dump_traceback_later()
|
||||
|
||||
|
||||
def create_event_loop_diagnostic_tasks() -> list[asyncio.Task]:
|
||||
"""Create enabled event loop diagnostic tasks for the current loop.
|
||||
|
||||
Returns:
|
||||
A list of created asyncio tasks.
|
||||
"""
|
||||
settings = load_event_loop_diagnostic_settings()
|
||||
tasks: list[asyncio.Task] = []
|
||||
|
||||
if settings.lag_monitor_enabled:
|
||||
tasks.append(
|
||||
asyncio.create_task(
|
||||
monitor_event_loop_lag(
|
||||
interval=settings.lag_monitor_interval,
|
||||
warn_after=settings.lag_monitor_threshold,
|
||||
),
|
||||
name="event_loop_lag_monitor",
|
||||
)
|
||||
)
|
||||
|
||||
if settings.watchdog_enabled:
|
||||
logger.info(
|
||||
"Event loop faulthandler watchdog enabled: timeout=%.3fs interval=%.3fs. "
|
||||
"If the loop is blocked, Python thread stacks will be written to %s "
|
||||
"(rotates at %d bytes).",
|
||||
settings.watchdog_timeout,
|
||||
settings.watchdog_interval,
|
||||
settings.watchdog_log_path,
|
||||
settings.watchdog_log_max_bytes,
|
||||
)
|
||||
tasks.append(
|
||||
asyncio.create_task(
|
||||
faulthandler_event_loop_watchdog(
|
||||
timeout=settings.watchdog_timeout,
|
||||
interval=settings.watchdog_interval,
|
||||
dump_path=settings.watchdog_log_path,
|
||||
max_bytes=settings.watchdog_log_max_bytes,
|
||||
),
|
||||
name="event_loop_faulthandler_watchdog",
|
||||
)
|
||||
)
|
||||
|
||||
return tasks
|
||||
41
changelogs/v4.26.5.md
Normal file
41
changelogs/v4.26.5.md
Normal file
@@ -0,0 +1,41 @@
|
||||
## [4.26.5] - 2026-07-07
|
||||
|
||||
### Added
|
||||
|
||||
- Added support for setting a custom bot ID while creating a platform by scanning a QR code. (#9067)
|
||||
- Added ChatUI project workspaces. (#9066)
|
||||
- Added improved ChatUI attachment display. (#9134)
|
||||
- Added malformed tool-call name sanitation in `ToolLoopAgentRunner`. (#9144)
|
||||
- Added model metadata in selectors and a chat token usage indicator. (#9161)
|
||||
- Added event-loop diagnostics and expanded diagnostics documentation. (#9168)
|
||||
- Added documentation for cross-platform compatibility and Python version support.
|
||||
- Added test coverage for POSIX file URI root preservation. (#8906)
|
||||
|
||||
### Changed
|
||||
|
||||
- Log event-loop watchdog startup at the info level.
|
||||
- Keep model metadata separate from providers. (#9161)
|
||||
- Polish snackbar styling in the WebUI. (#9173)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Enabled only synced ModelScope MCP servers. (#9084)
|
||||
- Improved MCP client startup and shutdown handling to avoid idle spinning and clean up on errors, timeouts, and cancellation. (#9070)
|
||||
- Replaced fragile knowledge-base duplicate-name error matching with a pre-check. (#9121)
|
||||
- Improved knowledge-base retrieval resilience on per-knowledge-base failures and rerank selection. (#9122)
|
||||
- Cleaned up `KBMedia` records on document deletion and fixed knowledge-base stats updates. (#9120)
|
||||
- Aligned the knowledge-base CRUD API contract with `kb_name`, canonical payload fields, and OpenAPI types. (#9000)
|
||||
- Constrained custom workspace paths and preserved absolute custom workspace paths.
|
||||
- Improved web search API key failover.
|
||||
- Enforced ownership when reading ChatUI sessions. (#9141)
|
||||
- Secured project update temporary staging. (#9083)
|
||||
- Improved QQ Official retry behavior when websocket send returns `None`. (#8977)
|
||||
- Preserved QQ Official quoted message context. (#9164)
|
||||
- Removed QQ Official reply IDs on proactive fallback so oversized passive replies can succeed via proactive sending. (#9169)
|
||||
- Adapted MiMo STT to V2.5 models and rejected non-WAV audio payloads. (#9118)
|
||||
- Serialized webchat history timestamps as UTC-aware ISO strings. (#9159)
|
||||
- Synced plugin activation status on enable in the WebUI. (#9156)
|
||||
- Exited MCP `anyio` contexts in the lifecycle task on disable. (#9132)
|
||||
- Fixed leaked SSE exit stack handling that could cause MCP epoll busy-wait. (#8307)
|
||||
|
||||
[4.26.5]: https://github.com/AstrBotDevs/AstrBot/compare/v4.26.4...v4.26.5
|
||||
@@ -7,9 +7,12 @@
|
||||
<v-snackbar v-if="toastStore.current" v-model="snackbarShow" :color="toastStore.current.color"
|
||||
:timeout="toastStore.current.timeout" :multi-line="toastStore.current.multiLine"
|
||||
:location="toastStore.current.location" close-on-back>
|
||||
{{ toastStore.current.message }}
|
||||
<div class="app-snackbar-message">
|
||||
<v-icon v-if="toastIcon" :icon="toastIcon" size="24" />
|
||||
<span>{{ toastStore.current.message }}</span>
|
||||
</div>
|
||||
<template #actions v-if="toastStore.current.closable">
|
||||
<v-btn variant="text" @click="snackbarShow = false">关闭</v-btn>
|
||||
<v-btn icon="mdi-close" variant="text" size="small" aria-label="Close notification" @click="snackbarShow = false" />
|
||||
</template>
|
||||
</v-snackbar>
|
||||
</template>
|
||||
@@ -32,6 +35,14 @@ const snackbarShow = computed({
|
||||
}
|
||||
})
|
||||
|
||||
const toastIcon = computed(() => ({
|
||||
success: 'mdi-check-circle-outline',
|
||||
error: 'mdi-alert-circle-outline',
|
||||
warning: 'mdi-alert-outline',
|
||||
info: 'mdi-information-outline',
|
||||
primary: 'mdi-information-outline'
|
||||
})[toastStore.current?.color] || '')
|
||||
|
||||
onMounted(() => {
|
||||
const desktopBridge = window.astrbotDesktop
|
||||
if (!desktopBridge?.onTrayRestartBackend) {
|
||||
|
||||
@@ -292,7 +292,7 @@
|
||||
</v-dialog>
|
||||
|
||||
<!-- 消息提示 -->
|
||||
<v-snackbar :timeout="3000" elevation="24" :color="save_message_success" v-model="save_message_snack" location="top">
|
||||
<v-snackbar :timeout="3000" elevation="6" :color="save_message_success" v-model="save_message_snack" location="top">
|
||||
{{ save_message }}
|
||||
</v-snackbar>
|
||||
</div>
|
||||
|
||||
@@ -746,7 +746,7 @@
|
||||
v-model="snackbar.show"
|
||||
:timeout="3500"
|
||||
:color="snackbar.color"
|
||||
elevation="24"
|
||||
elevation="6"
|
||||
>
|
||||
{{ snackbar.message }}
|
||||
</v-snackbar>
|
||||
|
||||
@@ -314,7 +314,7 @@ watch(viewMode, async (mode) => {
|
||||
/>
|
||||
|
||||
<!-- Snackbar -->
|
||||
<v-snackbar :timeout="2000" elevation="24" :color="snackbar.color" v-model="snackbar.show">
|
||||
<v-snackbar :timeout="2000" elevation="6" :color="snackbar.color" v-model="snackbar.show">
|
||||
{{ snackbar.message }}
|
||||
</v-snackbar>
|
||||
</template>
|
||||
|
||||
@@ -21,6 +21,10 @@ export default createVuetify({
|
||||
VCard: {
|
||||
rounded: 'lg'
|
||||
},
|
||||
VSnackbar: {
|
||||
elevation: 6,
|
||||
rounded: 'lg'
|
||||
},
|
||||
VTextField: {
|
||||
rounded: 'lg'
|
||||
},
|
||||
|
||||
@@ -20,6 +20,42 @@ html {
|
||||
.v-overlay.v-snackbar {
|
||||
--v-layout-left: 0px !important;
|
||||
--v-layout-right: 0px !important;
|
||||
|
||||
.v-snackbar__wrapper {
|
||||
min-height: 52px;
|
||||
border-radius: 8px !important;
|
||||
box-shadow: 0 10px 24px rgba(16, 24, 40, 0.16) !important;
|
||||
}
|
||||
|
||||
.v-snackbar__content {
|
||||
padding: 14px 18px;
|
||||
line-height: 1.35;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.v-snackbar__actions {
|
||||
margin-inline-end: 6px;
|
||||
}
|
||||
|
||||
.v-snackbar__actions .v-btn {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.v-snackbar__wrapper.bg-success {
|
||||
color: #ffffff !important;
|
||||
background-color: #457c3c !important;
|
||||
}
|
||||
}
|
||||
|
||||
.app-snackbar-message {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
|
||||
.v-icon {
|
||||
flex: 0 0 auto;
|
||||
opacity: 0.95;
|
||||
}
|
||||
}
|
||||
|
||||
.customizer-btn .icon {
|
||||
|
||||
@@ -166,7 +166,7 @@
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<v-snackbar :timeout="3000" elevation="24" :color="save_message_success" v-model="save_message_snack">
|
||||
<v-snackbar :timeout="3000" elevation="6" :color="save_message_success" v-model="save_message_snack">
|
||||
{{ save_message }}
|
||||
</v-snackbar>
|
||||
|
||||
|
||||
@@ -377,7 +377,7 @@
|
||||
</v-dialog>
|
||||
|
||||
<!-- 消息提示 -->
|
||||
<v-snackbar :timeout="3000" elevation="24" :color="messageType" v-model="showMessage" location="top">
|
||||
<v-snackbar :timeout="3000" elevation="6" :color="messageType" v-model="showMessage" location="top">
|
||||
{{ message }}
|
||||
</v-snackbar>
|
||||
</div>
|
||||
|
||||
@@ -510,7 +510,7 @@ const updateDialogPluginLogo = computed(() => {
|
||||
|
||||
<v-snackbar
|
||||
:timeout="2000"
|
||||
elevation="24"
|
||||
elevation="6"
|
||||
:color="snack_success"
|
||||
v-model="snack_show"
|
||||
location="bottom center"
|
||||
|
||||
@@ -220,7 +220,7 @@
|
||||
</v-dialog>
|
||||
|
||||
<!-- 消息提示 -->
|
||||
<v-snackbar :timeout="3000" elevation="24" :color="save_message_success" v-model="save_message_snack"
|
||||
<v-snackbar :timeout="3000" elevation="6" :color="save_message_success" v-model="save_message_snack"
|
||||
location="top">
|
||||
{{ save_message }}
|
||||
</v-snackbar>
|
||||
|
||||
@@ -650,7 +650,7 @@
|
||||
</v-dialog>
|
||||
|
||||
<!-- 提示信息 -->
|
||||
<v-snackbar v-model="snackbar" :timeout="3000" elevation="24" :color="snackbarColor" location="top">
|
||||
<v-snackbar v-model="snackbar" :timeout="3000" elevation="6" :color="snackbarColor" location="top">
|
||||
{{ snackbarText }}
|
||||
</v-snackbar>
|
||||
|
||||
|
||||
@@ -257,7 +257,7 @@
|
||||
</v-dialog>
|
||||
|
||||
<!-- 消息提示 -->
|
||||
<v-snackbar :timeout="3000" elevation="24" :color="messageType" v-model="showMessage" location="top">
|
||||
<v-snackbar :timeout="3000" elevation="6" :color="messageType" v-model="showMessage" location="top">
|
||||
{{ message }}
|
||||
</v-snackbar>
|
||||
</div>
|
||||
|
||||
@@ -231,6 +231,7 @@ export default defineConfig({
|
||||
base: "/others",
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: "异常诊断", link: "/diagnostics" },
|
||||
{ text: "自部署文转图", link: "/self-host-t2i" },
|
||||
{ text: "插件下载不了?试试自建 GitHub 加速服务", link: "/github-proxy" },
|
||||
],
|
||||
@@ -484,6 +485,7 @@ export default defineConfig({
|
||||
base: "/en/others",
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: "Diagnostics", link: "/diagnostics" },
|
||||
{ text: "Self-hosted HTML to Image", link: "/self-host-t2i" },
|
||||
],
|
||||
},
|
||||
|
||||
83
docs/en/others/diagnostics.md
Normal file
83
docs/en/others/diagnostics.md
Normal file
@@ -0,0 +1,83 @@
|
||||
# Diagnostics
|
||||
|
||||
This page provides a general checklist for diagnosing AstrBot issues. When something goes wrong, first identify which stage is affected, then collect the relevant logs. This makes issue reports easier to reproduce and investigate.
|
||||
|
||||
## Common Issue Types
|
||||
|
||||
- Startup failures: the process exits after startup, WebUI is unavailable, or database/config loading fails.
|
||||
- Platform connection issues: QQ, OneBot, Telegram, WeCom, or other platforms cannot connect, receive messages, or send messages.
|
||||
- Model request issues: replies take too long, requests time out, 429/5xx errors appear, or the proxy/API base is unavailable.
|
||||
- Plugin or MCP issues: enabling fails, tool calls fail, dependencies fail to install, or an MCP service does not respond.
|
||||
- Slow tasks or abnormal CPU usage: messages, proactive tasks, or scheduled tasks start but continue much later; or the process keeps unusually high CPU usage.
|
||||
|
||||
## Logs to Check First
|
||||
|
||||
Start with the main AstrBot log:
|
||||
|
||||
```text
|
||||
data/logs/astrbot.log
|
||||
```
|
||||
|
||||
For Docker deployments, also check container logs:
|
||||
|
||||
```bash
|
||||
docker logs <container-name>
|
||||
```
|
||||
|
||||
If the issue involves slow tasks, abnormal CPU usage, or multiple sessions becoming slow at the same time, also check the event loop diagnostic logs:
|
||||
|
||||
```text
|
||||
data/logs/event_loop_watchdog.log
|
||||
data/logs/event_loop_watchdog.log.1
|
||||
```
|
||||
|
||||
`event_loop_watchdog.log` rotates to `.1` after it exceeds 1 MB.
|
||||
|
||||
## General Checklist
|
||||
|
||||
1. Identify the time of the incident, then collect logs from 1 to 3 minutes before and after it.
|
||||
2. Check the scope: all platforms, one platform, one group, one user, or one plugin.
|
||||
3. For slow or missing model replies, check the model provider status, API key, API base, proxy, network, request timeout, and retry logs.
|
||||
4. For plugin or MCP issues, disable recently installed or updated plugins first, then check plugin dependencies and MCP service logs.
|
||||
5. For platform messaging issues, check whether the adapter is connected, platform-side settings are correct, and callback/WebSocket URLs are reachable.
|
||||
6. For slow tasks or abnormal CPU usage, see "Event Loop Lag Diagnostics" below.
|
||||
|
||||
## Event Loop Lag Diagnostics
|
||||
|
||||
The event loop schedules messages, plugins, scheduled jobs, model requests, and tool calls. If it is blocked by synchronous code, many features can look delayed at once.
|
||||
|
||||
Common symptoms:
|
||||
|
||||
- Logs stop after `ready to request llm provider`, `acquired session lock for llm request`, or a tool result, then continue much later.
|
||||
- A proactive or scheduled task starts, but one step in the middle takes a long time to continue.
|
||||
- Multiple platforms or sessions become slow at the same time.
|
||||
- CPU usage stays at 100%, or CPU usage is low but requests keep waiting for an external service.
|
||||
|
||||
If the main log contains the following entry, the event loop experienced visible scheduling delay:
|
||||
|
||||
```text
|
||||
Event loop lag detected: 18.432s (threshold 15.000s).
|
||||
```
|
||||
|
||||
If the event loop does not resume for a long time, AstrBot writes Python thread stacks to:
|
||||
|
||||
```text
|
||||
data/logs/event_loop_watchdog.log
|
||||
```
|
||||
|
||||
When reading this file, focus on the top frames. Useful clues often include plugin functions, platform adapters, MCP tools, synchronous network requests, `time.sleep()`, `subprocess.run()`, or CPU-heavy loops.
|
||||
|
||||
## What to Include in an Issue
|
||||
|
||||
When filing an issue, include as much of the following as possible:
|
||||
|
||||
- Approximate time of the incident and timezone.
|
||||
- AstrBot version, deployment method (Docker, manual deployment, desktop client, etc.), and operating system.
|
||||
- Trigger path: startup, normal chat, group chat, platform callback, scheduled task, MCP tool, plugin feature, etc.
|
||||
- Scope: all sessions, one platform, one group, one user, or one plugin.
|
||||
- Logs from `data/logs/astrbot.log` for 1 to 3 minutes around the incident.
|
||||
- If the issue involves lag or abnormal CPU usage, include `data/logs/event_loop_watchdog.log` and `data/logs/event_loop_watchdog.log.1`.
|
||||
- For Docker deployments, include the matching `docker logs` output.
|
||||
- Installed plugin list, and whether the issue still happens after disabling third-party plugins.
|
||||
|
||||
Before sharing logs, redact API keys, tokens, cookies, private chat content, and other sensitive information.
|
||||
83
docs/zh/others/diagnostics.md
Normal file
83
docs/zh/others/diagnostics.md
Normal file
@@ -0,0 +1,83 @@
|
||||
# 异常诊断
|
||||
|
||||
本文用于整理 AstrBot 出现异常时的通用排查方法。遇到问题时,先确定问题发生在哪个阶段,再收集对应日志;这样提交 Issue 时更容易复现和定位。
|
||||
|
||||
## 常见问题类型
|
||||
|
||||
- 启动失败:进程启动后退出、WebUI 打不开、数据库或配置加载失败。
|
||||
- 平台连接异常:QQ、OneBot、Telegram、企业微信等平台无法连接、收不到消息或无法发送消息。
|
||||
- 模型请求异常:长时间无回复、频繁超时、报 429/5xx、代理或 API Base 不可用。
|
||||
- 插件或 MCP 异常:启用失败、工具调用失败、依赖安装失败、MCP 服务无响应。
|
||||
- 任务卡顿或 CPU 异常:消息、主动任务、定时任务已经触发,但中间步骤很久才继续;或进程 CPU 长时间异常升高。
|
||||
|
||||
## 先看哪些日志
|
||||
|
||||
优先查看 AstrBot 主日志:
|
||||
|
||||
```text
|
||||
data/logs/astrbot.log
|
||||
```
|
||||
|
||||
如果使用 Docker 部署,也可以查看容器日志:
|
||||
|
||||
```bash
|
||||
docker logs <container-name>
|
||||
```
|
||||
|
||||
如果问题和任务卡顿、CPU 异常、多个会话同时变慢有关,还要查看事件循环诊断日志:
|
||||
|
||||
```text
|
||||
data/logs/event_loop_watchdog.log
|
||||
data/logs/event_loop_watchdog.log.1
|
||||
```
|
||||
|
||||
`event_loop_watchdog.log` 超过 1MB 后会轮转为 `.1`。
|
||||
|
||||
## 通用排查步骤
|
||||
|
||||
1. 确认问题发生的时间点,并截取该时间前后 1 到 3 分钟的日志。
|
||||
2. 确认问题范围:是所有平台都异常,还是只有某个平台、某个群、某个用户、某个插件异常。
|
||||
3. 如果是模型无回复或很慢,检查模型服务商状态、API Key、API Base、代理、网络、请求超时和重试日志。
|
||||
4. 如果是插件或 MCP 问题,先禁用最近安装或更新的插件,观察问题是否消失;同时检查插件依赖和 MCP 服务日志。
|
||||
5. 如果是平台收发消息异常,检查平台适配器是否已连接、平台后台配置是否正确、回调地址或 WebSocket 地址是否可访问。
|
||||
6. 如果是卡顿或 CPU 异常,参考下方“事件循环卡顿诊断”。
|
||||
|
||||
## 事件循环卡顿诊断
|
||||
|
||||
事件循环负责调度消息、插件、定时任务、模型请求和工具调用。如果它被同步代码阻塞,很多功能都会表现为延迟。
|
||||
|
||||
常见现象:
|
||||
|
||||
- 日志停在 `ready to request llm provider`、`acquired session lock for llm request`、工具调用结果之后,很久才继续。
|
||||
- 主动任务或定时任务已经触发,但中间某一步迟迟不继续。
|
||||
- 多个平台或多个会话同时变慢。
|
||||
- CPU 长时间 100%,或 CPU 不高但请求一直等待外部服务返回。
|
||||
|
||||
如果主日志出现以下内容,说明事件循环经历了明显延迟:
|
||||
|
||||
```text
|
||||
Event loop lag detected: 18.432s (threshold 15.000s).
|
||||
```
|
||||
|
||||
如果事件循环长时间没有恢复,AstrBot 会把 Python 线程栈写入:
|
||||
|
||||
```text
|
||||
data/logs/event_loop_watchdog.log
|
||||
```
|
||||
|
||||
查看该文件时,重点关注栈顶附近正在执行的代码。常见线索包括插件函数、平台适配器、MCP 工具、同步网络请求、`time.sleep()`、`subprocess.run()`、CPU 密集循环等。
|
||||
|
||||
## 提交 Issue 时请附带
|
||||
|
||||
提交问题时,请尽量提供以下信息:
|
||||
|
||||
- 问题发生的大致时间点和时区。
|
||||
- AstrBot 版本、部署方式(Docker、手动部署、桌面客户端等)、操作系统。
|
||||
- 触发方式:启动、普通聊天、群聊、平台回调、定时任务、MCP 工具、插件功能等。
|
||||
- 影响范围:所有会话、某个平台、某个群、某个用户,还是某个插件。
|
||||
- `data/logs/astrbot.log` 中问题发生前后 1 到 3 分钟的日志。
|
||||
- 如果存在卡顿或 CPU 异常,请附带 `data/logs/event_loop_watchdog.log` 和 `data/logs/event_loop_watchdog.log.1`。
|
||||
- 如果使用 Docker,请附带对应时间段的 `docker logs`。
|
||||
- 已安装插件列表,以及问题是否在禁用第三方插件后仍然出现。
|
||||
|
||||
提交日志前请先检查并遮盖 API Key、Token、Cookie、私聊内容等敏感信息。
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "AstrBot"
|
||||
version = "4.26.4"
|
||||
version = "4.26.5"
|
||||
description = "Easy-to-use multi-platform LLM chatbot and development framework"
|
||||
readme = "README.md"
|
||||
license = { text = "AGPL-3.0-or-later" }
|
||||
|
||||
134
tests/unit/test_event_loop_diagnostics.py
Normal file
134
tests/unit/test_event_loop_diagnostics.py
Normal file
@@ -0,0 +1,134 @@
|
||||
import asyncio
|
||||
|
||||
import pytest
|
||||
|
||||
from astrbot.core.utils import event_loop_diagnostics as diagnostics
|
||||
|
||||
|
||||
def test_load_event_loop_diagnostic_settings_defaults():
|
||||
"""Default settings enable lag monitoring and stack dump watchdog."""
|
||||
settings = diagnostics.load_event_loop_diagnostic_settings()
|
||||
|
||||
assert settings.lag_monitor_enabled is True
|
||||
assert settings.lag_monitor_interval == diagnostics.DEFAULT_LAG_MONITOR_INTERVAL
|
||||
assert settings.lag_monitor_threshold == diagnostics.DEFAULT_LAG_MONITOR_THRESHOLD
|
||||
assert settings.watchdog_enabled is True
|
||||
assert settings.watchdog_interval == diagnostics.DEFAULT_WATCHDOG_INTERVAL
|
||||
assert settings.watchdog_timeout == diagnostics.DEFAULT_WATCHDOG_TIMEOUT
|
||||
assert settings.watchdog_log_max_bytes == diagnostics.DEFAULT_WATCHDOG_LOG_MAX_BYTES
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_event_loop_diagnostic_tasks_defaults():
|
||||
"""Default diagnostics should create both event loop diagnostic tasks."""
|
||||
tasks = diagnostics.create_event_loop_diagnostic_tasks()
|
||||
|
||||
try:
|
||||
assert [task.get_name() for task in tasks] == [
|
||||
"event_loop_lag_monitor",
|
||||
"event_loop_faulthandler_watchdog",
|
||||
]
|
||||
finally:
|
||||
for task in tasks:
|
||||
task.cancel()
|
||||
await asyncio.gather(*tasks, return_exceptions=True)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_faulthandler_watchdog_cancels_pending_dump(monkeypatch):
|
||||
"""The faulthandler watchdog should cancel its pending dump on shutdown."""
|
||||
calls = []
|
||||
|
||||
class FakeFaultHandler:
|
||||
def cancel_dump_traceback_later(self):
|
||||
calls.append("cancel")
|
||||
|
||||
def dump_traceback_later(self, timeout, repeat, file):
|
||||
calls.append(("dump", timeout, repeat, file))
|
||||
|
||||
fake_faulthandler = FakeFaultHandler()
|
||||
monkeypatch.setattr(diagnostics, "faulthandler", fake_faulthandler)
|
||||
|
||||
task = asyncio.create_task(
|
||||
diagnostics.faulthandler_event_loop_watchdog(timeout=10, interval=1)
|
||||
)
|
||||
await asyncio.sleep(0)
|
||||
task.cancel()
|
||||
await asyncio.gather(task, return_exceptions=True)
|
||||
|
||||
assert any(isinstance(call, tuple) and call[0] == "dump" for call in calls)
|
||||
assert calls[-1] == "cancel"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_faulthandler_watchdog_writes_rotating_log(tmp_path, monkeypatch):
|
||||
"""The faulthandler watchdog should write to and rotate its log file."""
|
||||
log_path = tmp_path / "logs" / "event_loop_watchdog.log"
|
||||
log_path.parent.mkdir()
|
||||
log_path.write_text("x" * 8, encoding="utf-8")
|
||||
calls = []
|
||||
|
||||
class FakeFaultHandler:
|
||||
def cancel_dump_traceback_later(self):
|
||||
calls.append("cancel")
|
||||
|
||||
def dump_traceback_later(self, timeout, repeat, file):
|
||||
calls.append(("dump", timeout, repeat, file.name))
|
||||
file.write("watchdog dump\n")
|
||||
file.flush()
|
||||
|
||||
fake_faulthandler = FakeFaultHandler()
|
||||
monkeypatch.setattr(diagnostics, "faulthandler", fake_faulthandler)
|
||||
|
||||
task = asyncio.create_task(
|
||||
diagnostics.faulthandler_event_loop_watchdog(
|
||||
timeout=10,
|
||||
interval=1,
|
||||
dump_path=log_path,
|
||||
max_bytes=4,
|
||||
)
|
||||
)
|
||||
await asyncio.sleep(0)
|
||||
task.cancel()
|
||||
await asyncio.gather(task, return_exceptions=True)
|
||||
|
||||
assert log_path.read_text(encoding="utf-8") == "watchdog dump\n"
|
||||
assert log_path.with_name("event_loop_watchdog.log.1").read_text(
|
||||
encoding="utf-8"
|
||||
) == "x" * 8
|
||||
assert any(isinstance(call, tuple) and call[0] == "dump" for call in calls)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_faulthandler_watchdog_survives_dump_failure(tmp_path, monkeypatch):
|
||||
"""The watchdog should keep running after faulthandler arm failures."""
|
||||
log_path = tmp_path / "event_loop_watchdog.log"
|
||||
armed_again = asyncio.Event()
|
||||
calls = []
|
||||
|
||||
class FakeFaultHandler:
|
||||
def cancel_dump_traceback_later(self):
|
||||
calls.append("cancel")
|
||||
|
||||
def dump_traceback_later(self, timeout, repeat, file):
|
||||
calls.append(("dump", timeout, repeat, file.name))
|
||||
if len([call for call in calls if isinstance(call, tuple)]) == 1:
|
||||
raise RuntimeError("boom")
|
||||
armed_again.set()
|
||||
|
||||
fake_faulthandler = FakeFaultHandler()
|
||||
monkeypatch.setattr(diagnostics, "faulthandler", fake_faulthandler)
|
||||
|
||||
task = asyncio.create_task(
|
||||
diagnostics.faulthandler_event_loop_watchdog(
|
||||
timeout=10,
|
||||
interval=0.01,
|
||||
dump_path=log_path,
|
||||
)
|
||||
)
|
||||
await asyncio.wait_for(armed_again.wait(), timeout=1)
|
||||
task.cancel()
|
||||
await asyncio.gather(task, return_exceptions=True)
|
||||
|
||||
dump_calls = [call for call in calls if isinstance(call, tuple)]
|
||||
assert len(dump_calls) >= 2
|
||||
Reference in New Issue
Block a user