feat(webchat): refactor message parsing logic and integrate new parsing function
This commit is contained in:
@@ -4,6 +4,7 @@ import shutil
|
||||
import uuid
|
||||
from collections.abc import Awaitable, Callable, Sequence
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from astrbot.core.db.po import Attachment
|
||||
from astrbot.core.message.components import (
|
||||
@@ -19,6 +20,10 @@ from astrbot.core.message.message_event_result import MessageChain
|
||||
|
||||
AttachmentGetter = Callable[[str], Awaitable[Attachment | None]]
|
||||
AttachmentInserter = Callable[[str, str, str], Awaitable[Attachment | None]]
|
||||
ReplyHistoryGetter = Callable[
|
||||
[Any],
|
||||
Awaitable[tuple[list[dict], str | None, str | None] | None],
|
||||
]
|
||||
|
||||
MEDIA_PART_TYPES = {"image", "record", "file", "video"}
|
||||
|
||||
@@ -35,6 +40,127 @@ def webchat_message_parts_have_content(message_parts: list[dict]) -> bool:
|
||||
)
|
||||
|
||||
|
||||
async def parse_webchat_message_parts(
|
||||
message_parts: list,
|
||||
*,
|
||||
strict: bool = False,
|
||||
include_empty_plain: bool = False,
|
||||
verify_media_path_exists: bool = True,
|
||||
reply_history_getter: ReplyHistoryGetter | None = None,
|
||||
current_depth: int = 0,
|
||||
max_reply_depth: int = 0,
|
||||
cast_reply_id_to_str: bool = True,
|
||||
) -> tuple[list, list[str], bool]:
|
||||
"""Parse webchat message parts into components/text parts.
|
||||
|
||||
Returns:
|
||||
tuple[list, list[str], bool]:
|
||||
(components, plain_text_parts, has_non_reply_content)
|
||||
"""
|
||||
components = []
|
||||
text_parts: list[str] = []
|
||||
has_content = False
|
||||
|
||||
for part in message_parts:
|
||||
if not isinstance(part, dict):
|
||||
if strict:
|
||||
raise ValueError("message part must be an object")
|
||||
continue
|
||||
|
||||
part_type = str(part.get("type", "")).strip()
|
||||
if part_type == "plain":
|
||||
text = str(part.get("text", ""))
|
||||
if text or include_empty_plain:
|
||||
components.append(Plain(text=text))
|
||||
text_parts.append(text)
|
||||
if text:
|
||||
has_content = True
|
||||
continue
|
||||
|
||||
if part_type == "reply":
|
||||
message_id = part.get("message_id")
|
||||
if message_id is None:
|
||||
if strict:
|
||||
raise ValueError("reply part missing message_id")
|
||||
continue
|
||||
|
||||
reply_chain = []
|
||||
reply_message_str = str(part.get("selected_text", ""))
|
||||
sender_id = None
|
||||
sender_name = None
|
||||
|
||||
if reply_message_str:
|
||||
reply_chain = [Plain(text=reply_message_str)]
|
||||
elif (
|
||||
reply_history_getter
|
||||
and current_depth < max_reply_depth
|
||||
and message_id is not None
|
||||
):
|
||||
reply_info = await reply_history_getter(message_id)
|
||||
if reply_info:
|
||||
reply_parts, sender_id, sender_name = reply_info
|
||||
(
|
||||
reply_chain,
|
||||
reply_text_parts,
|
||||
_,
|
||||
) = await parse_webchat_message_parts(
|
||||
reply_parts,
|
||||
strict=strict,
|
||||
include_empty_plain=include_empty_plain,
|
||||
verify_media_path_exists=verify_media_path_exists,
|
||||
reply_history_getter=reply_history_getter,
|
||||
current_depth=current_depth + 1,
|
||||
max_reply_depth=max_reply_depth,
|
||||
cast_reply_id_to_str=cast_reply_id_to_str,
|
||||
)
|
||||
reply_message_str = "".join(reply_text_parts)
|
||||
|
||||
reply_id = str(message_id) if cast_reply_id_to_str else message_id
|
||||
components.append(
|
||||
Reply(
|
||||
id=reply_id,
|
||||
message_str=reply_message_str,
|
||||
chain=reply_chain,
|
||||
sender_id=sender_id,
|
||||
sender_nickname=sender_name,
|
||||
)
|
||||
)
|
||||
continue
|
||||
|
||||
if part_type not in MEDIA_PART_TYPES:
|
||||
if strict:
|
||||
raise ValueError(f"unsupported message part type: {part_type}")
|
||||
continue
|
||||
|
||||
path = part.get("path")
|
||||
if not path:
|
||||
if strict:
|
||||
raise ValueError(f"{part_type} part missing path")
|
||||
continue
|
||||
|
||||
file_path = Path(str(path))
|
||||
if verify_media_path_exists and not file_path.exists():
|
||||
if strict:
|
||||
raise ValueError(f"file not found: {file_path!s}")
|
||||
continue
|
||||
|
||||
file_path_str = (
|
||||
str(file_path.resolve()) if verify_media_path_exists else str(file_path)
|
||||
)
|
||||
has_content = True
|
||||
if part_type == "image":
|
||||
components.append(Image.fromFileSystem(file_path_str))
|
||||
elif part_type == "record":
|
||||
components.append(Record.fromFileSystem(file_path_str))
|
||||
elif part_type == "video":
|
||||
components.append(Video.fromFileSystem(file_path_str))
|
||||
else:
|
||||
filename = str(part.get("filename", "")).strip() or file_path.name
|
||||
components.append(File(name=filename, file=file_path_str))
|
||||
|
||||
return components, text_parts, has_content
|
||||
|
||||
|
||||
async def build_webchat_message_parts(
|
||||
message_payload: str | list,
|
||||
*,
|
||||
@@ -192,7 +318,13 @@ async def build_message_chain_from_payload(
|
||||
get_attachment_by_id=get_attachment_by_id,
|
||||
strict=strict,
|
||||
)
|
||||
return webchat_message_parts_to_message_chain(message_parts, strict=strict)
|
||||
components, _, has_content = await parse_webchat_message_parts(
|
||||
message_parts,
|
||||
strict=strict,
|
||||
)
|
||||
if strict and (not components or not has_content):
|
||||
raise ValueError("Message content is empty (reply only is not allowed)")
|
||||
return MessageChain(chain=components)
|
||||
|
||||
|
||||
async def create_attachment_part_from_existing_file(
|
||||
|
||||
@@ -9,14 +9,6 @@ from typing import Any
|
||||
from astrbot import logger
|
||||
from astrbot.core import db_helper
|
||||
from astrbot.core.db.po import PlatformMessageHistory
|
||||
from astrbot.core.message.components import (
|
||||
File,
|
||||
Image,
|
||||
Plain,
|
||||
Record,
|
||||
Reply,
|
||||
Video,
|
||||
)
|
||||
from astrbot.core.message.message_event_result import MessageChain
|
||||
from astrbot.core.platform import (
|
||||
AstrBotMessage,
|
||||
@@ -29,7 +21,10 @@ from astrbot.core.platform.astr_message_event import MessageSesion
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
||||
|
||||
from ...register import register_platform_adapter
|
||||
from .message_parts_helper import message_chain_to_storage_message_parts
|
||||
from .message_parts_helper import (
|
||||
message_chain_to_storage_message_parts,
|
||||
parse_webchat_message_parts,
|
||||
)
|
||||
from .webchat_event import WebChatMessageEvent
|
||||
from .webchat_queue_mgr import WebChatQueueMgr, webchat_queue_mgr
|
||||
|
||||
@@ -175,72 +170,30 @@ class WebChatAdapter(Platform):
|
||||
Returns:
|
||||
tuple[list, list[str]]: (消息组件列表, 纯文本列表)
|
||||
"""
|
||||
components = []
|
||||
text_parts = []
|
||||
|
||||
for part in message_parts:
|
||||
part_type = part.get("type")
|
||||
if part_type == "plain":
|
||||
text = part.get("text", "")
|
||||
components.append(Plain(text=text))
|
||||
text_parts.append(text)
|
||||
elif part_type == "reply":
|
||||
message_id = part.get("message_id")
|
||||
reply_chain = []
|
||||
reply_message_str = part.get("selected_text", "")
|
||||
sender_id = None
|
||||
sender_name = None
|
||||
async def get_reply_parts(
|
||||
message_id: Any,
|
||||
) -> tuple[list[dict], str | None, str | None] | None:
|
||||
history = await self._get_message_history(message_id)
|
||||
if not history or not history.content:
|
||||
return None
|
||||
|
||||
if reply_message_str:
|
||||
reply_chain = [Plain(text=reply_message_str)]
|
||||
reply_parts = history.content.get("message", [])
|
||||
if not isinstance(reply_parts, list):
|
||||
return None
|
||||
|
||||
# recursively get the content of the referenced message, if selected_text is empty
|
||||
if not reply_message_str and depth < max_depth and message_id:
|
||||
history = await self._get_message_history(message_id)
|
||||
if history and history.content:
|
||||
reply_parts = history.content.get("message", [])
|
||||
if isinstance(reply_parts, list):
|
||||
(
|
||||
reply_chain,
|
||||
reply_text_parts,
|
||||
) = await self._parse_message_parts(
|
||||
reply_parts,
|
||||
depth=depth + 1,
|
||||
max_depth=max_depth,
|
||||
)
|
||||
reply_message_str = "".join(reply_text_parts)
|
||||
sender_id = history.sender_id
|
||||
sender_name = history.sender_name
|
||||
|
||||
components.append(
|
||||
Reply(
|
||||
id=message_id,
|
||||
chain=reply_chain,
|
||||
message_str=reply_message_str,
|
||||
sender_id=sender_id,
|
||||
sender_nickname=sender_name,
|
||||
)
|
||||
)
|
||||
elif part_type == "image":
|
||||
path = part.get("path")
|
||||
if path:
|
||||
components.append(Image.fromFileSystem(path))
|
||||
elif part_type == "record":
|
||||
path = part.get("path")
|
||||
if path:
|
||||
components.append(Record.fromFileSystem(path))
|
||||
elif part_type == "file":
|
||||
path = part.get("path")
|
||||
if path:
|
||||
filename = part.get("filename") or (
|
||||
os.path.basename(path) if path else "file"
|
||||
)
|
||||
components.append(File(name=filename, file=path))
|
||||
elif part_type == "video":
|
||||
path = part.get("path")
|
||||
if path:
|
||||
components.append(Video.fromFileSystem(path))
|
||||
return reply_parts, history.sender_id, history.sender_name
|
||||
|
||||
components, text_parts, _ = await parse_webchat_message_parts(
|
||||
message_parts,
|
||||
strict=False,
|
||||
include_empty_plain=True,
|
||||
verify_media_path_exists=False,
|
||||
reply_history_getter=get_reply_parts,
|
||||
current_depth=depth,
|
||||
max_reply_depth=max_depth,
|
||||
cast_reply_id_to_str=False,
|
||||
)
|
||||
return components, text_parts
|
||||
|
||||
async def convert_message(self, data: tuple) -> AstrBotMessage:
|
||||
|
||||
Reference in New Issue
Block a user