From 2a7c02af8ea4ede41fea3039f1ca97db821abe56 Mon Sep 17 00:00:00 2001 From: Weilong Liao <37870767+Soulter@users.noreply.github.com> Date: Mon, 6 Jul 2026 23:48:30 +0800 Subject: [PATCH 01/36] fix: remove qq official reply id on proactive fallback so oversized passive replies can succeed via proactive sending (#9169) --- .../core/platform/sources/qqofficial/qqofficial_message_event.py | 1 + 1 file changed, 1 insertion(+) diff --git a/astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py b/astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py index 1e7d4f96e..e80c8296a 100644 --- a/astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py +++ b/astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py @@ -502,6 +502,7 @@ class QQOfficialMessageEvent(AstrMessageEvent): logger.info("[QQOfficial] 回复消息失败: %s, 尝试使用主动发送接口。", err) if payload.get("msg_id"): fallback_payload = payload.copy() + fallback_payload.pop("msg_id", None) try: ret = await send_func(fallback_payload) logger.info("[QQOfficial] 使用主动发送接口发送成功。") From 0dee40bd9dbbce6d58d6297b02fba59e12be9be9 Mon Sep 17 00:00:00 2001 From: Weilong Liao <37870767+Soulter@users.noreply.github.com> Date: Tue, 7 Jul 2026 00:14:03 +0800 Subject: [PATCH 02/36] feat: add event loop diagnostics (#9168) * feat: add event loop diagnostics * fix: write event loop watchdog dumps to rotating log * chore: use fixed event loop diagnostic defaults * fix: keep event loop watchdog alive after dump failures --- astrbot/core/core_lifecycle.py | 10 +- astrbot/core/utils/event_loop_diagnostics.py | 216 +++++++++++++++++++ tests/unit/test_event_loop_diagnostics.py | 134 ++++++++++++ 3 files changed, 359 insertions(+), 1 deletion(-) create mode 100644 astrbot/core/utils/event_loop_diagnostics.py create mode 100644 tests/unit/test_event_loop_diagnostics.py diff --git a/astrbot/core/core_lifecycle.py b/astrbot/core/core_lifecycle.py index c325a2ea3..215b81754 100644 --- a/astrbot/core/core_lifecycle.py +++ b/astrbot/core/core_lifecycle.py @@ -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: diff --git a/astrbot/core/utils/event_loop_diagnostics.py b/astrbot/core/utils/event_loop_diagnostics.py new file mode 100644 index 000000000..50eabea5d --- /dev/null +++ b/astrbot/core/utils/event_loop_diagnostics.py @@ -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.warning( + "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 diff --git a/tests/unit/test_event_loop_diagnostics.py b/tests/unit/test_event_loop_diagnostics.py new file mode 100644 index 000000000..0737478f9 --- /dev/null +++ b/tests/unit/test_event_loop_diagnostics.py @@ -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 From 0a5d6485a1afc563e888fe1a17254228ff1b2baa Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Tue, 7 Jul 2026 00:19:40 +0800 Subject: [PATCH 03/36] docs: add diagnostics guide --- docs/.vitepress/config.mjs | 2 ++ docs/en/others/diagnostics.md | 65 +++++++++++++++++++++++++++++++++++ docs/zh/others/diagnostics.md | 65 +++++++++++++++++++++++++++++++++++ 3 files changed, 132 insertions(+) create mode 100644 docs/en/others/diagnostics.md create mode 100644 docs/zh/others/diagnostics.md diff --git a/docs/.vitepress/config.mjs b/docs/.vitepress/config.mjs index a0b14bff4..a2b8062c5 100644 --- a/docs/.vitepress/config.mjs +++ b/docs/.vitepress/config.mjs @@ -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" }, ], }, diff --git a/docs/en/others/diagnostics.md b/docs/en/others/diagnostics.md new file mode 100644 index 000000000..f922e5e18 --- /dev/null +++ b/docs/en/others/diagnostics.md @@ -0,0 +1,65 @@ +# Diagnostics + +When AstrBot appears to slow down or get stuck without an obvious error, start with the event loop diagnostics. The event loop schedules messages, plugins, scheduled jobs, model requests, and tool calls. If it is blocked by synchronous code or cannot resume for a long time, many features can look delayed at once. + +## Common Symptoms + +- A group or private message reaches AstrBot, but processing continues tens of seconds or minutes later. +- 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, instead of only one user's request being slow. +- CPU usage stays unusually high, or CPU usage is low but requests wait a long time for an external service. + +## Diagnostic Logs + +AstrBot logs event loop lag. If the lag exceeds the threshold, the main log contains an entry like: + +```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 the file exceeds 1 MB, it rotates to: + +```text +data/logs/event_loop_watchdog.log.1 +``` + +Also check the main log: + +```text +data/logs/astrbot.log +``` + +For Docker deployments, you can also run: + +```bash +docker logs +``` + +## How to Investigate + +1. First check whether `Event loop lag detected` appears in the logs. If it does, AstrBot's main event loop experienced visible scheduling delay. +2. Open `data/logs/event_loop_watchdog.log` and inspect the top frames. Useful clues often include plugin functions, platform adapters, MCP tools, synchronous network requests, `time.sleep()`, `subprocess.run()`, or CPU-heavy loops. +3. If there is no event loop lag log but one conversation still waits for a long time, check the model provider, proxy, network, tool timeout, or MCP server response time first. +4. If only one session is stuck, a previous request in that session may still be running. If all sessions are stuck, the event loop is more likely blocked. +5. If CPU stays at 100%, focus on the watchdog stack and MCP/plugin calls. If CPU is low, the process is more likely waiting for an external network or model service. + +## 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: normal chat, group chat, scheduled task, MCP tool, plugin feature, etc. +- Logs from `data/logs/astrbot.log` for 1 to 3 minutes around the incident. +- `data/logs/event_loop_watchdog.log` and `data/logs/event_loop_watchdog.log.1` if they exist. +- For Docker deployments, 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. diff --git a/docs/zh/others/diagnostics.md b/docs/zh/others/diagnostics.md new file mode 100644 index 000000000..28bc4ee71 --- /dev/null +++ b/docs/zh/others/diagnostics.md @@ -0,0 +1,65 @@ +# 异常诊断 + +当 AstrBot 出现“看起来没有报错,但明显变慢或卡住”的情况时,可以先从事件循环诊断日志入手。事件循环是 AstrBot 调度消息、插件、定时任务、模型请求和工具调用的核心;如果它被同步阻塞或长期无法恢复,很多功能都会表现为延迟。 + +## 常见现象 + +- 群聊或私聊消息已经进入 AstrBot,但过了几十秒甚至几分钟才继续处理。 +- 日志停在 `ready to request llm provider`、`acquired session lock for llm request`、工具调用结果之后,很久才出现下一条 Agent 日志。 +- 主动任务、定时任务已经触发,但中间某一步迟迟不继续。 +- 多个平台或多个会话同时变慢,而不是只有单个用户的请求慢。 +- 进程 CPU 异常升高,或 CPU 不高但请求长时间等待外部服务返回。 + +## 查看诊断日志 + +AstrBot 会记录事件循环延迟。如果延迟超过阈值,主日志中会出现类似日志: + +```text +Event loop lag detected: 18.432s (threshold 15.000s). +``` + +如果事件循环长时间没有恢复,AstrBot 会把 Python 线程栈写入: + +```text +data/logs/event_loop_watchdog.log +``` + +该文件超过 1MB 后会轮转为: + +```text +data/logs/event_loop_watchdog.log.1 +``` + +你也可以同时查看主日志: + +```text +data/logs/astrbot.log +``` + +如果使用 Docker 部署,也可以用: + +```bash +docker logs +``` + +## 排查思路 + +1. 先确认是否有 `Event loop lag detected` 日志。如果有,说明 AstrBot 主事件循环确实经历了明显延迟。 +2. 查看 `data/logs/event_loop_watchdog.log`,关注栈顶附近正在执行的代码。常见线索包括插件函数、平台适配器、MCP 工具、同步网络请求、`time.sleep()`、`subprocess.run()`、CPU 密集循环等。 +3. 如果没有事件循环延迟日志,但某次对话仍然卡很久,优先检查模型服务商、代理、网络、工具调用超时、MCP 服务响应时间。 +4. 如果只有某个会话卡住,可能是该会话内前一个请求还没有结束;如果所有会话都卡住,更可能是事件循环被阻塞。 +5. 如果 CPU 长时间 100%,优先关注 watchdog 栈和 MCP/插件相关调用;如果 CPU 很低,更常见的是等待外部网络或模型服务返回。 + +## 提交 Issue 时请附带 + +提交问题时,请尽量提供以下信息,方便定位: + +- 问题发生的大致时间点和时区。 +- AstrBot 版本、部署方式(Docker、手动部署、桌面客户端等)、操作系统。 +- 触发方式:普通聊天、群聊、定时任务、MCP 工具、插件功能等。 +- `data/logs/astrbot.log` 中问题发生前后 1 到 3 分钟的日志。 +- `data/logs/event_loop_watchdog.log` 和 `data/logs/event_loop_watchdog.log.1`(如果存在)。 +- 如果使用 Docker,请附带对应时间段的 `docker logs`。 +- 已安装插件列表,以及问题是否在禁用第三方插件后仍然出现。 + +提交日志前请先检查并遮盖 API Key、Token、Cookie、私聊内容等敏感信息。 From 510e833695ff3bd954ae9a642f49cbd34966a452 Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Tue, 7 Jul 2026 00:20:17 +0800 Subject: [PATCH 04/36] chore: log event loop watchdog startup at info --- astrbot/core/utils/event_loop_diagnostics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/astrbot/core/utils/event_loop_diagnostics.py b/astrbot/core/utils/event_loop_diagnostics.py index 50eabea5d..cf2892643 100644 --- a/astrbot/core/utils/event_loop_diagnostics.py +++ b/astrbot/core/utils/event_loop_diagnostics.py @@ -192,7 +192,7 @@ def create_event_loop_diagnostic_tasks() -> list[asyncio.Task]: ) if settings.watchdog_enabled: - logger.warning( + 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).", From 1124dfe2953b1fdd79eea370f449c5244bfca31f Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Tue, 7 Jul 2026 00:26:03 +0800 Subject: [PATCH 05/36] docs: broaden diagnostics guide --- docs/en/others/diagnostics.md | 90 ++++++++++++++++++++-------------- docs/zh/others/diagnostics.md | 92 +++++++++++++++++++++-------------- 2 files changed, 109 insertions(+), 73 deletions(-) diff --git a/docs/en/others/diagnostics.md b/docs/en/others/diagnostics.md index f922e5e18..c4b34f463 100644 --- a/docs/en/others/diagnostics.md +++ b/docs/en/others/diagnostics.md @@ -1,18 +1,59 @@ # Diagnostics -When AstrBot appears to slow down or get stuck without an obvious error, start with the event loop diagnostics. The event loop schedules messages, plugins, scheduled jobs, model requests, and tool calls. If it is blocked by synchronous code or cannot resume for a long time, many features can look delayed at once. +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 Symptoms +## 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 +``` + +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: -- A group or private message reaches AstrBot, but processing continues tens of seconds or minutes later. - 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, instead of only one user's request being slow. -- CPU usage stays unusually high, or CPU usage is low but requests wait a long time for an external service. +- 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. -## Diagnostic Logs - -AstrBot logs event loop lag. If the lag exceeds the threshold, the main log contains an entry like: +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). @@ -24,31 +65,7 @@ If the event loop does not resume for a long time, AstrBot writes Python thread data/logs/event_loop_watchdog.log ``` -When the file exceeds 1 MB, it rotates to: - -```text -data/logs/event_loop_watchdog.log.1 -``` - -Also check the main log: - -```text -data/logs/astrbot.log -``` - -For Docker deployments, you can also run: - -```bash -docker logs -``` - -## How to Investigate - -1. First check whether `Event loop lag detected` appears in the logs. If it does, AstrBot's main event loop experienced visible scheduling delay. -2. Open `data/logs/event_loop_watchdog.log` and inspect the top frames. Useful clues often include plugin functions, platform adapters, MCP tools, synchronous network requests, `time.sleep()`, `subprocess.run()`, or CPU-heavy loops. -3. If there is no event loop lag log but one conversation still waits for a long time, check the model provider, proxy, network, tool timeout, or MCP server response time first. -4. If only one session is stuck, a previous request in that session may still be running. If all sessions are stuck, the event loop is more likely blocked. -5. If CPU stays at 100%, focus on the watchdog stack and MCP/plugin calls. If CPU is low, the process is more likely waiting for an external network or model service. +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 @@ -56,10 +73,11 @@ 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: normal chat, group chat, scheduled task, MCP tool, plugin feature, etc. +- 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. -- `data/logs/event_loop_watchdog.log` and `data/logs/event_loop_watchdog.log.1` if they exist. -- For Docker deployments, the matching `docker logs` output. +- 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. diff --git a/docs/zh/others/diagnostics.md b/docs/zh/others/diagnostics.md index 28bc4ee71..ab9a2c65b 100644 --- a/docs/zh/others/diagnostics.md +++ b/docs/zh/others/diagnostics.md @@ -1,18 +1,59 @@ # 异常诊断 -当 AstrBot 出现“看起来没有报错,但明显变慢或卡住”的情况时,可以先从事件循环诊断日志入手。事件循环是 AstrBot 调度消息、插件、定时任务、模型请求和工具调用的核心;如果它被同步阻塞或长期无法恢复,很多功能都会表现为延迟。 +本文用于整理 AstrBot 出现异常时的通用排查方法。遇到问题时,先确定问题发生在哪个阶段,再收集对应日志;这样提交 Issue 时更容易复现和定位。 -## 常见现象 +## 常见问题类型 -- 群聊或私聊消息已经进入 AstrBot,但过了几十秒甚至几分钟才继续处理。 -- 日志停在 `ready to request llm provider`、`acquired session lock for llm request`、工具调用结果之后,很久才出现下一条 Agent 日志。 -- 主动任务、定时任务已经触发,但中间某一步迟迟不继续。 -- 多个平台或多个会话同时变慢,而不是只有单个用户的请求慢。 -- 进程 CPU 异常升高,或 CPU 不高但请求长时间等待外部服务返回。 +- 启动失败:进程启动后退出、WebUI 打不开、数据库或配置加载失败。 +- 平台连接异常:QQ、OneBot、Telegram、企业微信等平台无法连接、收不到消息或无法发送消息。 +- 模型请求异常:长时间无回复、频繁超时、报 429/5xx、代理或 API Base 不可用。 +- 插件或 MCP 异常:启用失败、工具调用失败、依赖安装失败、MCP 服务无响应。 +- 任务卡顿或 CPU 异常:消息、主动任务、定时任务已经触发,但中间步骤很久才继续;或进程 CPU 长时间异常升高。 -## 查看诊断日志 +## 先看哪些日志 -AstrBot 会记录事件循环延迟。如果延迟超过阈值,主日志中会出现类似日志: +优先查看 AstrBot 主日志: + +```text +data/logs/astrbot.log +``` + +如果使用 Docker 部署,也可以查看容器日志: + +```bash +docker logs +``` + +如果问题和任务卡顿、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). @@ -24,41 +65,18 @@ Event loop lag detected: 18.432s (threshold 15.000s). data/logs/event_loop_watchdog.log ``` -该文件超过 1MB 后会轮转为: - -```text -data/logs/event_loop_watchdog.log.1 -``` - -你也可以同时查看主日志: - -```text -data/logs/astrbot.log -``` - -如果使用 Docker 部署,也可以用: - -```bash -docker logs -``` - -## 排查思路 - -1. 先确认是否有 `Event loop lag detected` 日志。如果有,说明 AstrBot 主事件循环确实经历了明显延迟。 -2. 查看 `data/logs/event_loop_watchdog.log`,关注栈顶附近正在执行的代码。常见线索包括插件函数、平台适配器、MCP 工具、同步网络请求、`time.sleep()`、`subprocess.run()`、CPU 密集循环等。 -3. 如果没有事件循环延迟日志,但某次对话仍然卡很久,优先检查模型服务商、代理、网络、工具调用超时、MCP 服务响应时间。 -4. 如果只有某个会话卡住,可能是该会话内前一个请求还没有结束;如果所有会话都卡住,更可能是事件循环被阻塞。 -5. 如果 CPU 长时间 100%,优先关注 watchdog 栈和 MCP/插件相关调用;如果 CPU 很低,更常见的是等待外部网络或模型服务返回。 +查看该文件时,重点关注栈顶附近正在执行的代码。常见线索包括插件函数、平台适配器、MCP 工具、同步网络请求、`time.sleep()`、`subprocess.run()`、CPU 密集循环等。 ## 提交 Issue 时请附带 -提交问题时,请尽量提供以下信息,方便定位: +提交问题时,请尽量提供以下信息: - 问题发生的大致时间点和时区。 - AstrBot 版本、部署方式(Docker、手动部署、桌面客户端等)、操作系统。 -- 触发方式:普通聊天、群聊、定时任务、MCP 工具、插件功能等。 +- 触发方式:启动、普通聊天、群聊、平台回调、定时任务、MCP 工具、插件功能等。 +- 影响范围:所有会话、某个平台、某个群、某个用户,还是某个插件。 - `data/logs/astrbot.log` 中问题发生前后 1 到 3 分钟的日志。 -- `data/logs/event_loop_watchdog.log` 和 `data/logs/event_loop_watchdog.log.1`(如果存在)。 +- 如果存在卡顿或 CPU 异常,请附带 `data/logs/event_loop_watchdog.log` 和 `data/logs/event_loop_watchdog.log.1`。 - 如果使用 Docker,请附带对应时间段的 `docker logs`。 - 已安装插件列表,以及问题是否在禁用第三方插件后仍然出现。 From bf18fadbe623476b79bfd6bcfec3c0ff47d4efc2 Mon Sep 17 00:00:00 2001 From: Weilong Liao <37870767+Soulter@users.noreply.github.com> Date: Tue, 7 Jul 2026 09:43:59 +0800 Subject: [PATCH 06/36] style: polish snackbar appearance (#9173) --- dashboard/src/App.vue | 15 ++++++-- .../extension/McpServersSection.vue | 2 +- .../components/extension/SkillsSection.vue | 2 +- .../extension/componentPanel/index.vue | 2 +- dashboard/src/plugins/vuetify.ts | 4 +++ dashboard/src/scss/_override.scss | 36 +++++++++++++++++++ dashboard/src/views/ConfigPage.vue | 2 +- dashboard/src/views/ConversationPage.vue | 2 +- dashboard/src/views/ExtensionPage.vue | 2 +- dashboard/src/views/PlatformPage.vue | 2 +- dashboard/src/views/SessionManagementPage.vue | 2 +- .../src/views/persona/PersonaManager.vue | 2 +- 12 files changed, 62 insertions(+), 11 deletions(-) diff --git a/dashboard/src/App.vue b/dashboard/src/App.vue index 6ce7b460a..579fec28a 100644 --- a/dashboard/src/App.vue +++ b/dashboard/src/App.vue @@ -7,9 +7,12 @@ - {{ toastStore.current.message }} +
+ + {{ toastStore.current.message }} +
@@ -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) { diff --git a/dashboard/src/components/extension/McpServersSection.vue b/dashboard/src/components/extension/McpServersSection.vue index 52d3591bf..3b35e21e5 100644 --- a/dashboard/src/components/extension/McpServersSection.vue +++ b/dashboard/src/components/extension/McpServersSection.vue @@ -292,7 +292,7 @@ - + {{ save_message }} diff --git a/dashboard/src/components/extension/SkillsSection.vue b/dashboard/src/components/extension/SkillsSection.vue index 77f1f5f7c..926e11736 100644 --- a/dashboard/src/components/extension/SkillsSection.vue +++ b/dashboard/src/components/extension/SkillsSection.vue @@ -746,7 +746,7 @@ v-model="snackbar.show" :timeout="3500" :color="snackbar.color" - elevation="24" + elevation="6" > {{ snackbar.message }} diff --git a/dashboard/src/components/extension/componentPanel/index.vue b/dashboard/src/components/extension/componentPanel/index.vue index 15d911a25..97e7cab8f 100644 --- a/dashboard/src/components/extension/componentPanel/index.vue +++ b/dashboard/src/components/extension/componentPanel/index.vue @@ -314,7 +314,7 @@ watch(viewMode, async (mode) => { /> - + {{ snackbar.message }} diff --git a/dashboard/src/plugins/vuetify.ts b/dashboard/src/plugins/vuetify.ts index e38fd388e..37b56f3d9 100644 --- a/dashboard/src/plugins/vuetify.ts +++ b/dashboard/src/plugins/vuetify.ts @@ -21,6 +21,10 @@ export default createVuetify({ VCard: { rounded: 'lg' }, + VSnackbar: { + elevation: 6, + rounded: 'lg' + }, VTextField: { rounded: 'lg' }, diff --git a/dashboard/src/scss/_override.scss b/dashboard/src/scss/_override.scss index be16623a0..79204dce4 100644 --- a/dashboard/src/scss/_override.scss +++ b/dashboard/src/scss/_override.scss @@ -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 { diff --git a/dashboard/src/views/ConfigPage.vue b/dashboard/src/views/ConfigPage.vue index 4f57eaf8e..2d1dc78ef 100644 --- a/dashboard/src/views/ConfigPage.vue +++ b/dashboard/src/views/ConfigPage.vue @@ -166,7 +166,7 @@ - + {{ save_message }} diff --git a/dashboard/src/views/ConversationPage.vue b/dashboard/src/views/ConversationPage.vue index 35c0c205a..41e633913 100644 --- a/dashboard/src/views/ConversationPage.vue +++ b/dashboard/src/views/ConversationPage.vue @@ -377,7 +377,7 @@ - + {{ message }} diff --git a/dashboard/src/views/ExtensionPage.vue b/dashboard/src/views/ExtensionPage.vue index 983c5e03a..df88f3df0 100644 --- a/dashboard/src/views/ExtensionPage.vue +++ b/dashboard/src/views/ExtensionPage.vue @@ -510,7 +510,7 @@ const updateDialogPluginLogo = computed(() => { - {{ save_message }} diff --git a/dashboard/src/views/SessionManagementPage.vue b/dashboard/src/views/SessionManagementPage.vue index 5c71b1b18..f7392dc0e 100644 --- a/dashboard/src/views/SessionManagementPage.vue +++ b/dashboard/src/views/SessionManagementPage.vue @@ -650,7 +650,7 @@ - + {{ snackbarText }} diff --git a/dashboard/src/views/persona/PersonaManager.vue b/dashboard/src/views/persona/PersonaManager.vue index 164640516..a241dbe1f 100644 --- a/dashboard/src/views/persona/PersonaManager.vue +++ b/dashboard/src/views/persona/PersonaManager.vue @@ -257,7 +257,7 @@ - + {{ message }} From 39090a74bae58608c7f0b758e75b4c0cd71898e8 Mon Sep 17 00:00:00 2001 From: Weilong Liao <37870767+Soulter@users.noreply.github.com> Date: Tue, 7 Jul 2026 09:54:43 +0800 Subject: [PATCH 07/36] chore: bump version to 4.26.5 (#9174) --- astrbot/__init__.py | 2 +- changelogs/v4.26.5.md | 35 +++++++++++++++++++++++++++++++++++ pyproject.toml | 2 +- 3 files changed, 37 insertions(+), 2 deletions(-) create mode 100644 changelogs/v4.26.5.md diff --git a/astrbot/__init__.py b/astrbot/__init__.py index a3e6bd5e6..c410be033 100644 --- a/astrbot/__init__.py +++ b/astrbot/__init__.py @@ -1,4 +1,4 @@ import logging -__version__ = "4.26.4" +__version__ = "4.26.5" logger = logging.getLogger("astrbot") diff --git a/changelogs/v4.26.5.md b/changelogs/v4.26.5.md new file mode 100644 index 000000000..4249b9ef0 --- /dev/null +++ b/changelogs/v4.26.5.md @@ -0,0 +1,35 @@ +## What's Changed + +- feat(platform): 支持扫码创建时自定义机器人ID (#9067) (9f50c900b) +- fix: enable only synced ModelScope MCP servers (#9084) (89b80a6ca) +- feat: add ChatUI project workspaces (#9066) (e7d5be632) +- fix: Improve MCP client startup and shutdown handling to prevent idle spinning when a server is disabled and ensure proper cleanup on errors, timeouts, and cancellation (#9070) (3ce66576f) +- fix(kb): replace fragile error string matching with pre-check for duplicate kb_name (#9121) (aecee6fc9) +- fix(kb): improve retrieval resilience on per-KB failure and rerank selection (#9122) (468eea99c) +- fix(kb): clean up KBMedia records on document delete, fix update_kb_stats (#9120) (e2f3b0008) +- fix: align knowledge base CRUD API contract with kb_name, canonical payload fields, and matching OpenAPI types (#9000) (ea9e3421d) +- fix: constrain custom workspace paths (6d798908a) +- fix(core): improve web search API key failover (8a31cf01f) +- fix: preserve absolute custom workspace paths (20008f179) +- feat: improve ChatUI attachment display (#9134) (b43cc6dee) +- fix: enforce ownership when reading ChatUI sessions (#9141) (041fba4df) +- fix: secure project update temp staging (#9083) (30426c4f6) +- fix: qq_official websocket适配器发送消息收到None时现在会重试了 (#8977) (#8979) (cc0b34750) +- fix: adapt MiMo STT to V2.5 models and reject non-WAV audio payloads (#9118) (c9eed7b65) +- feat: add note on cross-platform compatibility and Python version support (85b653b6f) +- test: cover POSIX file URI root preservation (#8906) (3b8caf37e) +- feat: add sanitation for malformed tool call names in ToolLoopAgentRunner (#9144) (25cbd41e0) +- feat: show model metadata in selectors (6627fd53c) +- fix: keep model metadata separate from providers (#9161) (18e067fab) +- fix: serialize webchat history timestamps as UTC-aware ISO strings (#9159) (56326c755) +- feat: add chat token usage indicator (ab2502c17) +- fix(webui): sync plugin activation status on enable (#9156) (ba7f8ebfb) +- fix: preserve qq official quoted message context (#9164) (4166d9ab5) +- fix: exit MCP anyio contexts in the lifecycle task on disable (#9132) (afb007911) +- fix(mcp): attempt to fix epoll busy-wait caused by leaked SSE exit stack (#8307) (56f253395) +- fix: remove qq official reply id on proactive fallback so oversized passive replies can succeed via proactive sending (#9169) (2a7c02af8) +- feat: add event loop diagnostics (#9168) (0dee40bd9) +- docs: add diagnostics guide (0a5d6485a) +- chore: log event loop watchdog startup at info (510e83369) +- docs: broaden diagnostics guide (1124dfe29) +- style: polish snackbar appearance (#9173) (bf18fadbe) diff --git a/pyproject.toml b/pyproject.toml index 983d131ef..d80dadc48 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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" } From 331444b41f77f6d364c215cff3266b1e9233928f Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Tue, 7 Jul 2026 10:12:25 +0800 Subject: [PATCH 08/36] docs: update v4.26.5 changelog format --- changelogs/v4.26.5.md | 74 +++++++++++++++++++++++-------------------- 1 file changed, 40 insertions(+), 34 deletions(-) diff --git a/changelogs/v4.26.5.md b/changelogs/v4.26.5.md index 4249b9ef0..c04d01bac 100644 --- a/changelogs/v4.26.5.md +++ b/changelogs/v4.26.5.md @@ -1,35 +1,41 @@ -## What's Changed +## [4.26.5] - 2026-07-07 -- feat(platform): 支持扫码创建时自定义机器人ID (#9067) (9f50c900b) -- fix: enable only synced ModelScope MCP servers (#9084) (89b80a6ca) -- feat: add ChatUI project workspaces (#9066) (e7d5be632) -- fix: Improve MCP client startup and shutdown handling to prevent idle spinning when a server is disabled and ensure proper cleanup on errors, timeouts, and cancellation (#9070) (3ce66576f) -- fix(kb): replace fragile error string matching with pre-check for duplicate kb_name (#9121) (aecee6fc9) -- fix(kb): improve retrieval resilience on per-KB failure and rerank selection (#9122) (468eea99c) -- fix(kb): clean up KBMedia records on document delete, fix update_kb_stats (#9120) (e2f3b0008) -- fix: align knowledge base CRUD API contract with kb_name, canonical payload fields, and matching OpenAPI types (#9000) (ea9e3421d) -- fix: constrain custom workspace paths (6d798908a) -- fix(core): improve web search API key failover (8a31cf01f) -- fix: preserve absolute custom workspace paths (20008f179) -- feat: improve ChatUI attachment display (#9134) (b43cc6dee) -- fix: enforce ownership when reading ChatUI sessions (#9141) (041fba4df) -- fix: secure project update temp staging (#9083) (30426c4f6) -- fix: qq_official websocket适配器发送消息收到None时现在会重试了 (#8977) (#8979) (cc0b34750) -- fix: adapt MiMo STT to V2.5 models and reject non-WAV audio payloads (#9118) (c9eed7b65) -- feat: add note on cross-platform compatibility and Python version support (85b653b6f) -- test: cover POSIX file URI root preservation (#8906) (3b8caf37e) -- feat: add sanitation for malformed tool call names in ToolLoopAgentRunner (#9144) (25cbd41e0) -- feat: show model metadata in selectors (6627fd53c) -- fix: keep model metadata separate from providers (#9161) (18e067fab) -- fix: serialize webchat history timestamps as UTC-aware ISO strings (#9159) (56326c755) -- feat: add chat token usage indicator (ab2502c17) -- fix(webui): sync plugin activation status on enable (#9156) (ba7f8ebfb) -- fix: preserve qq official quoted message context (#9164) (4166d9ab5) -- fix: exit MCP anyio contexts in the lifecycle task on disable (#9132) (afb007911) -- fix(mcp): attempt to fix epoll busy-wait caused by leaked SSE exit stack (#8307) (56f253395) -- fix: remove qq official reply id on proactive fallback so oversized passive replies can succeed via proactive sending (#9169) (2a7c02af8) -- feat: add event loop diagnostics (#9168) (0dee40bd9) -- docs: add diagnostics guide (0a5d6485a) -- chore: log event loop watchdog startup at info (510e83369) -- docs: broaden diagnostics guide (1124dfe29) -- style: polish snackbar appearance (#9173) (bf18fadbe) +### 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 From adebd2958ed885548107cd69552757f76197da66 Mon Sep 17 00:00:00 2001 From: Soulter <37870767+Soulter@users.noreply.github.com> Date: Tue, 7 Jul 2026 22:00:19 +0800 Subject: [PATCH 09/36] chore: refine README MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SengokuCola 必定女装 --- README.md | 60 ++++++++++++++++++++----------------------------------- 1 file changed, 22 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index 7f0b89017..8e5fab740 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,10 @@ AstrBot is an open-source all-in-one Agent chatbot platform that integrates with ### One-Click Deployment +> [!NOTE] +> Requires [uv](https://docs.astral.sh/uv/) to be installed. +> For macOS users: due to macOS security checks, the first run of the `astrbot` command may take longer (about 10-20s). + For users who want to quickly experience AstrBot, are familiar with command-line usage, and can install a `uv` environment on their own, we recommend the `uv` one-click deployment method ⚡️: ```bash @@ -83,20 +87,17 @@ astrbot init # Only execute this command for the first time to initialize the en astrbot run ``` -> Requires [uv](https://docs.astral.sh/uv/) to be installed. -> AstrBot requires Python 3.12 or later. The `--python 3.12` option ensures that `uv` creates the tool environment with Python 3.12. - -> [!NOTE] -> For macOS users: due to macOS security checks, the first run of the `astrbot` command may take longer (about 10-20s). - Update `astrbot`: ```bash uv tool upgrade astrbot --python 3.12 ``` -> [!WARNING] -> AstrBot deployed via `uv` **does not support upgrading through the WebUI**. To update, please run the command above from the command line. +### One-Click Cloud Deployment (RainYun) + +For users who want one-click deployment and do not want to manage servers themselves, we recommend RainYun's one-click cloud deployment service ☁️: + +[![Deploy on RainYun](https://rainyun-apps.cn-nb1.rains3.com/materials/deploy-on-rainyun-en.svg)](https://app.rainyun.com/apps/rca/store/5994?ref=NjU1ODg0) ### Docker Deployment @@ -104,12 +105,6 @@ For users familiar with containers and looking for a more stable, production-rea Please refer to the official documentation: [Deploy AstrBot with Docker](https://docs.astrbot.app/deploy/astrbot/docker.html#%E4%BD%BF%E7%94%A8-docker-%E9%83%A8%E7%BD%B2-astrbot). -### Deploy on RainYun - -For users who want one-click deployment and do not want to manage servers themselves, we recommend RainYun's one-click cloud deployment service ☁️: - -[![Deploy on RainYun](https://rainyun-apps.cn-nb1.rains3.com/materials/deploy-on-rainyun-en.svg)](https://app.rainyun.com/apps/rca/store/5994?ref=NjU1ODg0) - ### Desktop Application Deployment For users who want to use AstrBot on desktop and mainly use ChatUI, we recommend AstrBot App. @@ -142,6 +137,16 @@ yay -S astrbot-git If you need panel-based management or deeper customization, see [BT-Panel Deployment](https://docs.astrbot.app/deploy/astrbot/btpanel.html) for BT Panel app-store setup, [1Panel Deployment](https://docs.astrbot.app/deploy/astrbot/1panel.html) for 1Panel app-market deployment, [CasaOS Deployment](https://docs.astrbot.app/deploy/astrbot/casaos.html) for NAS/home-server visual deployment, and [Manual Deployment](https://docs.astrbot.app/deploy/astrbot/cli.html) for fully custom source-based installation with `uv`. + +## ❤️ Sponsors + +[Want to be here?](mailto://community@astrbot.app) + +

+ sponsors +

+ + ## Supported Messaging Platforms Connect AstrBot to your favorite chat platform. @@ -205,13 +210,6 @@ Connect AstrBot to your favorite chat platform. | Xiaomi MiMo TTS | Text-to-Speech Services | | Volcano Engine TTS | Text-to-Speech Services | -## ❤️ Sponsors - -

- sponsors -

- - ## ❤️ Contributing Issues and Pull Requests are always welcome! Feel free to submit your changes to this project :) @@ -235,22 +233,7 @@ pre-commit install ### QQ Groups -- Group 1: 322154837 (Full) -- Group 3: 630166526 (Full) -- Group 4: 1077826412 (Full) -- Group 5: 822130018 (Full) -- Group 6: 753075035 (Full) -- Group 7: 743746109 (Full) -- Group 8: 1030353265 (Full) -- Group 9: 1076659624 (Full) -- Group 10: 1078079676 (Full) -- Group 11: 704659519 (Full) -- Group 12: 916228568 (Full) -- Group 13: 1092185289 -- Group 14: 1103419483 - -- Developer Group(Chit-chat): 975206796 -- Developer Group(Formal): 1039761811 +We have 15+ chat groups, please see: [Community](https://docs.astrbot.app/community.html) for details. ### Discord Server @@ -264,9 +247,10 @@ Special thanks to all Contributors and plugin developers for their contributions -Additionally, the birth of this project would not have been possible without the help of the following open-source projects: +Open Source Friends ❤️ - [NapNeko/NapCatQQ](https://github.com/NapNeko/NapCatQQ) - The amazing cat framework +- [Mai-with-u/MaiBot](https://github.com/Mai-with-u/MaiBot) - The powerful "digital life" in your QQ! ## ⭐ Star History From dcc40ede1decb1f62b950291c02b413a7b33a3e9 Mon Sep 17 00:00:00 2001 From: VectorPeak Date: Wed, 8 Jul 2026 00:27:52 +0800 Subject: [PATCH 10/36] fix: validate dashboard account username updates (#9175) Co-authored-by: chatgpt-codex-connector[bot] <199175422+chatgpt-codex-connector[bot]@users.noreply.github.com> --- astrbot/dashboard/services/auth_service.py | 10 ++- tests/test_dashboard.py | 99 ++++++++++++++++++++++ 2 files changed, 107 insertions(+), 2 deletions(-) diff --git a/astrbot/dashboard/services/auth_service.py b/astrbot/dashboard/services/auth_service.py index bc6cbde50..856662acb 100644 --- a/astrbot/dashboard/services/auth_service.py +++ b/astrbot/dashboard/services/auth_service.py @@ -369,6 +369,12 @@ class AuthService: if not new_pwd and not new_username: return self.error("新用户名和新密码不能同时为空") + username_to_save = None + if new_username is not None and new_username != "": + if not isinstance(new_username, str) or len(new_username.strip()) < 3: + return self.error("用户名长度至少3位") + username_to_save = new_username.strip() + if new_pwd: if not isinstance(new_pwd, str): return self.error("新密码无效") @@ -384,8 +390,8 @@ class AuthService: await set_password_change_required(self.db, self.config, False) if is_totp_enabled(self.config): await revoke_user_trusted_devices(self.db) - if new_username: - self.config["dashboard"]["username"] = new_username + if username_to_save: + self.config["dashboard"]["username"] = username_to_save self.config.save_config() diff --git a/tests/test_dashboard.py b/tests/test_dashboard.py index 16356fbab..87d4ce006 100644 --- a/tests/test_dashboard.py +++ b/tests/test_dashboard.py @@ -1360,6 +1360,105 @@ async def test_generated_password_requires_password_change_until_changed( ) +@pytest.mark.asyncio +@pytest.mark.parametrize( + ("endpoint", "method"), + [ + ("/api/auth/account/edit", "post"), + ("/api/v1/auth/account", "patch"), + ], +) +@pytest.mark.parametrize("new_username", ["ab", " "]) +async def test_account_edit_rejects_invalid_username( + app: FastAPIAppAdapter, + core_lifecycle_td: AstrBotCoreLifecycle, + endpoint: str, + method: str, + new_username: str, +): + original_dashboard_config = copy.deepcopy( + core_lifecycle_td.astrbot_config["dashboard"] + ) + test_client = app.test_client() + current_username = core_lifecycle_td.astrbot_config["dashboard"]["username"] + current_password = _resolve_dashboard_password(core_lifecycle_td) + + try: + login_response = await test_client.post( + "/api/auth/login", + json={"username": current_username, "password": current_password}, + ) + login_data = await login_response.get_json() + assert login_data["status"] == "ok" + headers = {"Authorization": f"Bearer {login_data['data']['token']}"} + + payload = { + "password": current_password, + "new_password": "", + "confirm_password": "", + "new_username": new_username, + } + request = getattr(test_client, method) + response = await request(endpoint, headers=headers, json=payload) + data = await response.get_json() + + assert data["status"] == "error" + assert data["message"] == "用户名长度至少3位" + assert ( + core_lifecycle_td.astrbot_config["dashboard"]["username"] + == (original_dashboard_config["username"]) + ) + finally: + await _restore_dashboard_password_state( + core_lifecycle_td, + original_dashboard_config, + ) + + +@pytest.mark.asyncio +async def test_account_edit_trims_valid_username( + app: FastAPIAppAdapter, + core_lifecycle_td: AstrBotCoreLifecycle, +): + original_dashboard_config = copy.deepcopy( + core_lifecycle_td.astrbot_config["dashboard"] + ) + test_client = app.test_client() + current_username = core_lifecycle_td.astrbot_config["dashboard"]["username"] + current_password = _resolve_dashboard_password(core_lifecycle_td) + + try: + login_response = await test_client.post( + "/api/auth/login", + json={"username": current_username, "password": current_password}, + ) + login_data = await login_response.get_json() + assert login_data["status"] == "ok" + headers = {"Authorization": f"Bearer {login_data['data']['token']}"} + + response = await test_client.post( + "/api/auth/account/edit", + headers=headers, + json={ + "password": current_password, + "new_password": "", + "confirm_password": "", + "new_username": " astrbot-admin ", + }, + ) + data = await response.get_json() + + assert data["status"] == "ok" + assert core_lifecycle_td.astrbot_config["dashboard"]["username"] == ( + "astrbot-admin" + ) + finally: + await _restore_dashboard_password_state( + core_lifecycle_td, + original_dashboard_config, + ) + + @pytest.mark.asyncio async def test_local_setup_can_skip_default_password_auth( app: FastAPIAppAdapter, From 0be821afc600a8b47d583d7952a1d1b1d605e534 Mon Sep 17 00:00:00 2001 From: Soulter <37870767+Soulter@users.noreply.github.com> Date: Wed, 8 Jul 2026 00:39:31 +0800 Subject: [PATCH 11/36] chore: update readme --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8e5fab740..aca631384 100644 --- a/README.md +++ b/README.md @@ -140,10 +140,12 @@ If you need panel-based management or deeper customization, see [BT-Panel Deploy ## ❤️ Sponsors -[Want to be here?](mailto://community@astrbot.app) +Welcome to sponsor us via [Afdian](https://afdian.com/a/astrbot_team) or [contact us](mailto:community@astrbot.app).

- sponsors + + sponsors +

From afb898e61fc7e8ba8cbe38f4858cde6bd3191914 Mon Sep 17 00:00:00 2001 From: SpencerFang0617 Date: Thu, 9 Jul 2026 15:47:57 +0800 Subject: [PATCH 12/36] fix(line): remove unexpected force argument from get_json (#9187) --- astrbot/core/platform/sources/line/line_adapter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/astrbot/core/platform/sources/line/line_adapter.py b/astrbot/core/platform/sources/line/line_adapter.py index 33959f6f7..06daee652 100644 --- a/astrbot/core/platform/sources/line/line_adapter.py +++ b/astrbot/core/platform/sources/line/line_adapter.py @@ -133,7 +133,7 @@ class LinePlatformAdapter(Platform): return "invalid signature", 400 try: - payload = await request.get_json(force=True, silent=False) + payload = await request.get_json(silent=False) except Exception as e: logger.warning("[LINE] invalid webhook body: %s", e) return "bad request", 400 From 12f2f5a09ce0e7b0535f5d0754567151b88942a7 Mon Sep 17 00:00:00 2001 From: Cooper <90701697+CooperWang0912@users.noreply.github.com> Date: Thu, 9 Jul 2026 15:48:52 +0800 Subject: [PATCH 13/36] fix: Added Try Except to Filter Handling (#9183) * Added Try Except to Filter Handling * Avoid Redundant Error Processing --- astrbot/core/pipeline/result_decorate/stage.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/astrbot/core/pipeline/result_decorate/stage.py b/astrbot/core/pipeline/result_decorate/stage.py index 5956c8b5e..681befc41 100644 --- a/astrbot/core/pipeline/result_decorate/stage.py +++ b/astrbot/core/pipeline/result_decorate/stage.py @@ -243,7 +243,13 @@ class ResultDecorateStage(Stage): continue for seg in split_response: if self.content_cleanup_rule: - seg = re.sub(self.content_cleanup_rule, "", seg) + try: + seg = re.sub(self.content_cleanup_rule, "", seg) + except re.error: + logger.error( + f"分段回复过滤表达式失败,无法成功过滤:{traceback.format_exc()}" + ) + self.content_cleanup_rule = None seg = seg.strip() if seg: new_chain.append(Plain(seg)) From 7422fb075c51f0b0432cc4be20de2e3c794c24a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8F=B9=E5=8F=B7=E5=A4=A7=E5=B8=9D?= <162813557+th-dd@users.noreply.github.com> Date: Sat, 11 Jul 2026 22:46:16 +0800 Subject: [PATCH 14/36] fix(platform): correct tutorial links for satori and mattermost (#9205) --- dashboard/src/utils/platformUtils.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dashboard/src/utils/platformUtils.js b/dashboard/src/utils/platformUtils.js index d69af9237..1b64311b1 100644 --- a/dashboard/src/utils/platformUtils.js +++ b/dashboard/src/utils/platformUtils.js @@ -66,10 +66,11 @@ export function getTutorialLink(platformType) { "slack": "https://docs.astrbot.app/platform/slack.html", "kook": "https://docs.astrbot.app/platform/kook.html", "vocechat": "https://docs.astrbot.app/platform/vocechat.html", - "satori": "https://docs.astrbot.app/platform/satori/llonebot.html", + "satori": "https://docs.astrbot.app/platform/satori/guide.html", "misskey": "https://docs.astrbot.app/platform/misskey.html", "line": "https://docs.astrbot.app/platform/line.html", "matrix": "https://docs.astrbot.app/platform/matrix.html", + "mattermost": "https://docs.astrbot.app/platform/mattermost.html", } return tutorialMap[platformType] || "https://docs.astrbot.app"; } From 4c6ef8a492b8c11d14189a82559a4edf5b9106d4 Mon Sep 17 00:00:00 2001 From: Fiber <48828021+leafliber@users.noreply.github.com> Date: Sat, 11 Jul 2026 22:46:55 +0800 Subject: [PATCH 15/36] fix: resolve mobile chat input freeze and multiline attachment button alignment (#9202) * fix: resolve mobile chat input freeze caused by autoResize oscillation The autoResize() function in ChatInput.vue used minHeight + 8 as the threshold to decide whether to switch from multi-line (textarea) back to single-line (input). On mobile viewports (max-width: 768px), the textarea's 2-line scrollHeight is below minHeight + 8 due to smaller line-height and padding, causing an infinite oscillation loop between and