Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,11 @@ parser_custom_font_weight: int = 700
# [可选] 是否需要转发媒体内容(超过 4 项时始终使用合并转发)
parser_need_forward_contents=True

# [可选] 是否将一次解析产生的所有消息放入同一条合并转发
# 默认关闭以保持原有发送行为;启用后外部只发送一条消息,插件不会主动限制内部节点数量
# 实际可发送数量仍受适配器及平台限制
parser_forward_all_messages=False

# [可选] emoji 渲染 CDN
# 例如 ELK_SH_CDN = "https://emojicdn.elk.sh", MQRIO_DEV_CDN = "https://emoji-cdn.mqrio.dev"
parser_emoji_cdn="https://emojicdn.elk.sh"
Expand Down
7 changes: 7 additions & 0 deletions src/nonebot_plugin_parser/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ class Config(BaseModel):
"""字体粗细程度"""
parser_need_forward_contents: bool = True
"""是否需要转发媒体内容"""
parser_forward_all_messages: bool = False
"""是否将一次解析的所有消息放入同一条合并转发中"""
parser_emoji_cdn: str = ELK_SH_CDN
"""Pilmoji 表情 CDN"""
parser_emoji_style: EmojiStyle = EmojiStyle.FACEBOOK
Expand Down Expand Up @@ -174,6 +176,11 @@ def need_forward_contents(self) -> bool:
"""是否需要转发媒体内容"""
return self.parser_need_forward_contents

@property
def forward_all_messages(self) -> bool:
"""是否将一次解析的所有消息放入同一条合并转发中"""
return self.parser_forward_all_messages

@property
def emoji_cdn(self) -> str:
"""Pilmoji 表情 CDN"""
Expand Down
19 changes: 19 additions & 0 deletions src/nonebot_plugin_parser/helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
Image,
Video,
Voice,
RefNode,
Segment,
Reference,
CustomNode,
Expand Down Expand Up @@ -64,6 +65,24 @@ def construct_forward_message(

return Reference(nodes=nodes)

@staticmethod
def construct_forward_messages(
messages: Sequence[UniMessage],
user_id: str | None = None,
) -> Reference:
"""将多条消息合并为一条转发消息,并展开已有的转发节点。"""
if user_id is None:
user_id = current_bot.get().self_id

nodes: list[RefNode | CustomNode] = []
for message in messages:
if len(message) == 1 and isinstance(message[0], Reference):
nodes.extend(message[0].children)
else:
nodes.append(CustomNode(uid=user_id, name=pconfig.nickname, content=message))

return Reference(nodes=nodes)

@staticmethod
def img_seg(
file: Path | bytes,
Expand Down
31 changes: 28 additions & 3 deletions src/nonebot_plugin_parser/matchers/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import re
from typing import TypeVar
from typing import Any, TypeVar, Protocol
from collections.abc import AsyncIterator

from nonebot import logger, get_driver, on_command
from nonebot.params import CommandArg
Expand All @@ -13,6 +14,10 @@
from ..renders import get_renderer


class _MessageRenderer(Protocol):
def render_messages(self) -> AsyncIterator[UniMessage[Any]]: ...


def _get_enabled_parser_classes() -> list[type[BaseParser]]:
disabled_platforms = set(pconfig.disabled_platforms)
all_subclass = BaseParser.get_all_subclass()
Expand Down Expand Up @@ -60,6 +65,27 @@ def clear_result_cache():
_RESULT_CACHE.clear()


async def send_rendered_messages(renderer: _MessageRenderer) -> None:
"""发送渲染结果,可选将所有输出聚合为单条合并转发。"""
if pconfig.forward_all_messages:
messages: list[UniMessage[Any]] = []
render_error: Exception | None = None
try:
async for message in renderer.render_messages():
messages.append(message)
except Exception as e:
render_error = e

if messages:
forward = UniHelper.construct_forward_messages(messages)
await UniMessage(forward).send()
if render_error is not None:
raise render_error
else:
async for message in renderer.render_messages():
await message.send()


@UniHelper.with_reaction
async def parser_handler(
sr: SearchResult = Searched(),
Expand All @@ -79,8 +105,7 @@ async def parser_handler(

# 3. 渲染内容消息并发送
renderer = get_renderer(result.platform.name)(result)
async for message in renderer.render_messages():
await message.send()
await send_rendered_messages(renderer)

# 4. 缓存解析结果
_RESULT_CACHE[cache_key] = result
Expand Down
134 changes: 134 additions & 0 deletions tests/others/test_forward_messages.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
def test_construct_forward_messages_flattens_existing_references():
from nonebot_plugin_alconna.uniseg import Text, Image, Video, Reference, CustomNode, UniMessage

from nonebot_plugin_parser.helper import UniHelper

existing = Reference(
nodes=[
CustomNode(uid="1", name="parser", content=UniMessage(Image(url="https://example.com/1.jpg"))),
CustomNode(uid="1", name="parser", content=UniMessage(Image(url="https://example.com/2.jpg"))),
]
)
messages = [
UniMessage(Text("preview")),
UniMessage(existing),
UniMessage(Video(url="https://example.com/1.mp4")),
UniMessage(Video(url="https://example.com/2.mp4")),
]

forward = UniHelper.construct_forward_messages(messages, user_id="1")

assert len(forward.children) == 5
node_types = []
for node in forward.children:
assert isinstance(node, CustomNode)
assert isinstance(node.content, UniMessage)
node_types.append(node.content[0].type)
assert node_types == ["text", "image", "image", "video", "video"]


async def test_send_rendered_messages_sends_once(monkeypatch):
from nonebot_plugin_alconna.uniseg import Text, Image, Video, Reference, CustomNode, UniMessage

from nonebot_plugin_parser.config import pconfig
from nonebot_plugin_parser.matchers import send_rendered_messages

class Renderer:
async def render_messages(self):
yield UniMessage(Text("preview"))
yield UniMessage(
Reference(
nodes=[
CustomNode(uid="1", name="parser", content=UniMessage(Image(url="https://example.com/1.jpg"))),
CustomNode(uid="1", name="parser", content=UniMessage(Image(url="https://example.com/2.jpg"))),
]
)
)
yield UniMessage(Video(url="https://example.com/1.mp4"))
yield UniMessage(Video(url="https://example.com/2.mp4"))

sent: list[UniMessage] = []

async def send(message, *args, **kwargs):
sent.append(message)

monkeypatch.setattr(pconfig, "parser_forward_all_messages", True)
bot_context = type("BotContext", (), {"get": lambda self: type("Bot", (), {"self_id": "1"})()})()
monkeypatch.setattr("nonebot_plugin_parser.helper.current_bot", bot_context)
monkeypatch.setattr(UniMessage, "send", send)

await send_rendered_messages(Renderer())

assert len(sent) == 1
assert len(sent[0]) == 1
assert isinstance(sent[0][0], Reference)
assert len(sent[0][0].children) == 5


async def test_send_rendered_messages_preserves_original_behavior_when_disabled(monkeypatch):
from nonebot_plugin_alconna.uniseg import Text, UniMessage

from nonebot_plugin_parser.config import pconfig
from nonebot_plugin_parser.matchers import send_rendered_messages

class Renderer:
async def render_messages(self):
yield UniMessage(Text("first"))
yield UniMessage(Text("second"))

sent: list[UniMessage] = []

async def send(message, *args, **kwargs):
sent.append(message)

monkeypatch.setattr(pconfig, "parser_forward_all_messages", False)
monkeypatch.setattr(UniMessage, "send", send)

await send_rendered_messages(Renderer())

assert [message.extract_plain_text() for message in sent] == ["first", "second"]


async def test_send_rendered_messages_sends_partial_results_before_reraising(monkeypatch):
import pytest
from nonebot_plugin_alconna.uniseg import Text, Reference, UniMessage

from nonebot_plugin_parser.config import pconfig
from nonebot_plugin_parser.matchers import send_rendered_messages

class RenderError(Exception):
pass

class Renderer:
async def render_messages(self):
yield UniMessage(Text("preview"))
raise RenderError

sent: list[UniMessage] = []

async def send(message, *args, **kwargs):
sent.append(message)

monkeypatch.setattr(pconfig, "parser_forward_all_messages", True)
bot_context = type("BotContext", (), {"get": lambda self: type("Bot", (), {"self_id": "1"})()})()
monkeypatch.setattr("nonebot_plugin_parser.helper.current_bot", bot_context)
monkeypatch.setattr(UniMessage, "send", send)

with pytest.raises(RenderError):
await send_rendered_messages(Renderer())

assert len(sent) == 1
assert isinstance(sent[0][0], Reference)
assert len(sent[0][0].children) == 1


def test_construct_forward_messages_does_not_limit_nodes():
from nonebot_plugin_alconna.uniseg import Text, UniMessage

from nonebot_plugin_parser.helper import UniHelper

messages = [UniMessage(Text(str(index))) for index in range(128)]

forward = UniHelper.construct_forward_messages(messages, user_id="1")

assert len(forward.children) == len(messages)
Loading