feat(webchat): refactor message parsing logic and integrate new parsing function
This commit is contained in:
@@ -4,6 +4,7 @@ import shutil
|
|||||||
import uuid
|
import uuid
|
||||||
from collections.abc import Awaitable, Callable, Sequence
|
from collections.abc import Awaitable, Callable, Sequence
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from astrbot.core.db.po import Attachment
|
from astrbot.core.db.po import Attachment
|
||||||
from astrbot.core.message.components import (
|
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]]
|
AttachmentGetter = Callable[[str], Awaitable[Attachment | None]]
|
||||||
AttachmentInserter = Callable[[str, str, 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"}
|
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(
|
async def build_webchat_message_parts(
|
||||||
message_payload: str | list,
|
message_payload: str | list,
|
||||||
*,
|
*,
|
||||||
@@ -192,7 +318,13 @@ async def build_message_chain_from_payload(
|
|||||||
get_attachment_by_id=get_attachment_by_id,
|
get_attachment_by_id=get_attachment_by_id,
|
||||||
strict=strict,
|
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(
|
async def create_attachment_part_from_existing_file(
|
||||||
|
|||||||
@@ -9,14 +9,6 @@ from typing import Any
|
|||||||
from astrbot import logger
|
from astrbot import logger
|
||||||
from astrbot.core import db_helper
|
from astrbot.core import db_helper
|
||||||
from astrbot.core.db.po import PlatformMessageHistory
|
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.message.message_event_result import MessageChain
|
||||||
from astrbot.core.platform import (
|
from astrbot.core.platform import (
|
||||||
AstrBotMessage,
|
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 astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
||||||
|
|
||||||
from ...register import register_platform_adapter
|
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_event import WebChatMessageEvent
|
||||||
from .webchat_queue_mgr import WebChatQueueMgr, webchat_queue_mgr
|
from .webchat_queue_mgr import WebChatQueueMgr, webchat_queue_mgr
|
||||||
|
|
||||||
@@ -175,72 +170,30 @@ class WebChatAdapter(Platform):
|
|||||||
Returns:
|
Returns:
|
||||||
tuple[list, list[str]]: (消息组件列表, 纯文本列表)
|
tuple[list, list[str]]: (消息组件列表, 纯文本列表)
|
||||||
"""
|
"""
|
||||||
components = []
|
|
||||||
text_parts = []
|
|
||||||
|
|
||||||
for part in message_parts:
|
async def get_reply_parts(
|
||||||
part_type = part.get("type")
|
message_id: Any,
|
||||||
if part_type == "plain":
|
) -> tuple[list[dict], str | None, str | None] | None:
|
||||||
text = part.get("text", "")
|
history = await self._get_message_history(message_id)
|
||||||
components.append(Plain(text=text))
|
if not history or not history.content:
|
||||||
text_parts.append(text)
|
return None
|
||||||
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
|
|
||||||
|
|
||||||
if reply_message_str:
|
reply_parts = history.content.get("message", [])
|
||||||
reply_chain = [Plain(text=reply_message_str)]
|
if not isinstance(reply_parts, list):
|
||||||
|
return None
|
||||||
|
|
||||||
# recursively get the content of the referenced message, if selected_text is empty
|
return reply_parts, history.sender_id, history.sender_name
|
||||||
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))
|
|
||||||
|
|
||||||
|
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
|
return components, text_parts
|
||||||
|
|
||||||
async def convert_message(self, data: tuple) -> AstrBotMessage:
|
async def convert_message(self, data: tuple) -> AstrBotMessage:
|
||||||
|
|||||||
Reference in New Issue
Block a user