diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d5e5e2..301482e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ - 新增引用解析开关 - 新增 allcpp 解析器 - 修复更新插件时字体占用导致无法更新的问题 +- 修复引用解析时 `"".join` 遇到 `None` 崩溃(引用合并转发 / 附件 / 空文本卡片时) +- 引用链抽不到文本时保留当前消息正文,不再用空串覆盖 +- `on_message` 异常只写日志,不再把堆栈发到群里 ## v1.5.6 diff --git a/core/utils.py b/core/utils.py index 759e9ee..656a413 100644 --- a/core/utils.py +++ b/core/utils.py @@ -228,6 +228,16 @@ def _clean_embedded_url(value: str) -> str: return unquote(value.strip().replace("\\/", "/")) +def join_nonempty_texts(parts: list[object]) -> str: + """拼接文本,跳过 None 和空串,避免 str.join 遇到空值崩溃。""" + texts: list[str] = [] + for part in parts: + if part is None or part == "": + continue + texts.append(str(part)) + return "".join(texts) + + def extract_json_url(data: dict | str) -> str | None: """处理 JSON 类型消息段,提取可交给解析器处理的 URL。""" if isinstance(data, str): diff --git a/main.py b/main.py index 06a49ca..0cdd7a8 100644 --- a/main.py +++ b/main.py @@ -22,7 +22,7 @@ from .core.parsers import BaseParser, BilibiliParser from .core.render import Renderer from .core.sender import MessageSender -from .core.utils import extract_json_url +from .core.utils import extract_json_url, join_nonempty_texts class ParserPlugin(Star): @@ -113,7 +113,14 @@ def _get_parser_by_type(self, parser_type): @filter.event_message_type(filter.EventMessageType.ALL) async def on_message(self, event: AstrMessageEvent): - """消息的统一入口""" + """消息的统一入口。异常只写日志,避免堆栈发到群里。""" + try: + await self._handle_message(event) + except Exception: + logger.exception("解析消息失败,已跳过") + + async def _handle_message(self, event: AstrMessageEvent): + """实际解析流程。""" umo = event.unified_msg_origin # 白名单 @@ -138,7 +145,7 @@ async def on_message(self, event: AstrMessageEvent): for seg in chain: if isinstance(seg, At): mentioned_ids.add(str(seg.qq)) - elif isinstance(seg, Plain): + elif isinstance(seg, Plain) and seg.text: mentioned_ids.update(re.findall(r"<@!?([^>\s]+)>", seg.text)) if ( self.cfg.require_at_in_group @@ -169,8 +176,9 @@ async def on_message(self, event: AstrMessageEvent): reply_texts.append(seg.text) elif isinstance(seg, Json): reply_texts.append(extract_json_url(seg.data)) - if reply_texts: - text = "".join(reply_texts) + reply_text = join_nonempty_texts(reply_texts) + if reply_text: + text = reply_text if not text: return diff --git a/tests/test_reply_text.py b/tests/test_reply_text.py new file mode 100644 index 0000000..f7ceb8d --- /dev/null +++ b/tests/test_reply_text.py @@ -0,0 +1,32 @@ +from __future__ import annotations + +import importlib +import sys +import types +from types import SimpleNamespace + +import pytest + + +@pytest.fixture +def utils_module(monkeypatch: pytest.MonkeyPatch): + logger = SimpleNamespace(info=lambda *a, **k: None, warning=lambda *a, **k: None) + astrbot_pkg = types.ModuleType("astrbot") + astrbot_pkg.__path__ = [] + api_module = types.ModuleType("astrbot.api") + api_module.logger = logger + monkeypatch.setitem(sys.modules, "astrbot", astrbot_pkg) + monkeypatch.setitem(sys.modules, "astrbot.api", api_module) + monkeypatch.delitem(sys.modules, "core.utils", raising=False) + return importlib.import_module("core.utils") + + +def test_join_skips_none(utils_module): + assert utils_module.join_nonempty_texts([None]) == "" + assert utils_module.join_nonempty_texts([None, "https://b23.tv/x"]) == ( + "https://b23.tv/x" + ) + + +def test_join_skips_empty_and_keeps_order(utils_module): + assert utils_module.join_nonempty_texts(["a", None, "", "b"]) == "ab"