fix(plugin-runtime): surface detailed send failures for plugin tools

This commit is contained in:
Losita
2026-05-12 22:02:15 +08:00
parent fd50f33662
commit 702316ae57
4 changed files with 380 additions and 9 deletions

View File

@@ -48,6 +48,10 @@ from src.plugin_runtime.host.hook_spec_registry import HookSpec, HookSpecRegistr
logger = get_logger("send_service")
class SendServiceError(RuntimeError):
"""发送服务错误。"""
def register_send_service_hook_specs(registry: HookSpecRegistry) -> List[HookSpec]:
"""注册发送服务内置 Hook 规格。
@@ -581,6 +585,34 @@ def _build_outbound_session_message(
return outbound_message
def _build_outbound_session_message_detailed(
message_sequence: MessageSequence,
stream_id: str,
processed_plain_text: str = "",
reply_message: Optional[MaiMessage] = None,
selected_expressions: Optional[List[int]] = None,
) -> SessionMessage:
"""根据目标会话构建待发送消息,失败时抛出带详情的异常。"""
outbound_message = _build_outbound_session_message(
message_sequence=message_sequence,
stream_id=stream_id,
processed_plain_text=processed_plain_text,
reply_message=reply_message,
selected_expressions=selected_expressions,
)
if outbound_message is not None:
return outbound_message
target_stream = _chat_manager.get_session_by_session_id(stream_id)
if target_stream is None:
raise SendServiceError(f"未找到聊天流: {stream_id}")
if not get_bot_account(target_stream.platform):
raise SendServiceError(f"平台 {target_stream.platform} 未配置机器人账号,无法发送消息")
raise SendServiceError("构建出站消息失败")
def _ensure_reply_component(message: SessionMessage, reply_message_id: str) -> None:
"""为消息补充回复组件。
@@ -743,6 +775,18 @@ def _log_platform_io_failures(delivery_batch: DeliveryBatch) -> None:
logger.warning(f"[SendService] Platform IO 发送失败: platform={delivery_batch.route_key.platform} {failed_details}")
def _build_platform_io_failure_message(delivery_batch: DeliveryBatch) -> str:
"""构造 Platform IO 发送失败的紧凑错误信息。"""
failed_details = [
str(receipt.error or "").strip()
for receipt in delivery_batch.failed_receipts
if str(receipt.error or "").strip()
]
if failed_details:
return "; ".join(failed_details)
return "未命中任何发送路由"
async def _send_via_platform_io(
message: SessionMessage,
*,
@@ -884,6 +928,117 @@ async def send_session_message_with_message(
return sent_message
async def send_session_message_with_message_detailed(
message: SessionMessage,
*,
typing: bool = False,
set_reply: bool = False,
reply_message_id: Optional[str] = None,
storage_message: bool = True,
show_log: bool = True,
sync_to_maisaka_history: bool = False,
maisaka_source_kind: str = "outbound_send",
) -> SessionMessage:
"""统一发送一条内部消息,失败时抛出带详情的异常。"""
if not message.message_id:
logger.error("[SendService] 消息缺少 message_id无法发送")
raise SendServiceError("消息缺少 message_id无法发送")
try:
before_send_result, message = await _invoke_send_hook(
"send_service.before_send",
message,
typing=typing,
set_reply=set_reply,
reply_message_id=reply_message_id,
storage_message=storage_message,
show_log=show_log,
)
if before_send_result.aborted:
logger.info(f"[SendService] 消息 {message.message_id} 在发送前被 Hook 中止")
raise SendServiceError("消息在发送前被 Hook 中止")
before_kwargs = before_send_result.kwargs
typing = _coerce_bool(before_kwargs.get("typing"), typing)
set_reply = _coerce_bool(before_kwargs.get("set_reply"), set_reply)
storage_message = _coerce_bool(before_kwargs.get("storage_message"), storage_message)
show_log = _coerce_bool(before_kwargs.get("show_log"), show_log)
raw_reply_message_id = before_kwargs.get("reply_message_id", reply_message_id)
reply_message_id = None if raw_reply_message_id in {None, ""} else str(raw_reply_message_id)
platform_io_manager = get_platform_io_manager()
try:
await platform_io_manager.ensure_send_pipeline_ready()
except Exception as exc:
logger.error(f"[SendService] 准备 Platform IO 发送管线失败: {exc}")
logger.debug(traceback.format_exc())
raise SendServiceError(f"准备 Platform IO 发送管线失败: {exc}") from exc
try:
route_key = platform_io_manager.build_route_key_from_message(message)
except Exception as exc:
logger.warning(f"[SendService] 根据消息构造 Platform IO 路由键失败: {exc}")
raise SendServiceError(f"根据消息构造 Platform IO 路由键失败: {exc}") from exc
try:
await _prepare_message_for_platform_io(
message,
typing=typing,
set_reply=set_reply,
reply_message_id=reply_message_id,
)
delivery_batch = await platform_io_manager.send_message(
message,
route_key,
metadata={"show_log": False},
)
except Exception as exc:
logger.error(f"[SendService] Platform IO 发送异常: {exc}")
logger.debug(traceback.format_exc())
raise SendServiceError(f"Platform IO 发送异常: {exc}") from exc
sent = bool(delivery_batch.has_success)
if sent:
await _apply_successful_delivery_receipt(message, delivery_batch)
await _dispatch_adapter_callbacks(delivery_batch)
await _invoke_send_hook(
"send_service.after_send",
message,
sent=sent,
typing=typing,
set_reply=set_reply,
reply_message_id=reply_message_id,
storage_message=storage_message,
show_log=show_log,
)
if delivery_batch.has_success:
if storage_message:
_store_sent_message(message)
await _notify_memory_automation_on_message_sent(message)
if show_log:
successful_driver_ids = [
receipt.driver_id or "unknown"
for receipt in delivery_batch.sent_receipts
]
logger.info(
f"[SendService] 已通过 Platform IO 将消息发往平台 '{route_key.platform}' "
f"(drivers: {', '.join(successful_driver_ids)}) "
f"message={_build_outbound_log_preview(message)}"
)
if sync_to_maisaka_history:
_sync_sent_message_to_maisaka_history(
message,
source_kind=str(maisaka_source_kind or "outbound_send"),
)
return message
_log_platform_io_failures(delivery_batch)
raise SendServiceError(_build_platform_io_failure_message(delivery_batch))
except SendServiceError:
raise
async def send_session_message(
message: SessionMessage,
*,
@@ -1054,6 +1209,76 @@ async def _send_to_target_with_message(
return None
async def _send_to_target_with_message_detailed(
message_sequence: MessageSequence,
stream_id: str,
processed_plain_text: str = "",
typing: bool = False,
set_reply: bool = False,
reply_message: Optional[MaiMessage] = None,
storage_message: bool = True,
show_log: bool = True,
selected_expressions: Optional[List[int]] = None,
sync_to_maisaka_history: bool = False,
maisaka_source_kind: str = "outbound_send",
) -> SessionMessage:
"""向指定目标构建并发送消息,失败时抛出带详情的异常。"""
try:
if set_reply and reply_message is None:
logger.warning("[SendService] 使用引用回复,但未提供回复消息")
raise SendServiceError("使用引用回复,但未提供回复消息")
if show_log:
logger.debug(f"[SendService] 发送{_describe_message_sequence(message_sequence)}消息到 {stream_id}")
outbound_message = _build_outbound_session_message_detailed(
message_sequence=message_sequence,
stream_id=stream_id,
processed_plain_text=processed_plain_text,
reply_message=reply_message,
selected_expressions=selected_expressions,
)
after_build_result, outbound_message = await _invoke_send_hook(
"send_service.after_build_message",
outbound_message,
stream_id=stream_id,
processed_plain_text=processed_plain_text,
typing=typing,
set_reply=set_reply,
storage_message=storage_message,
show_log=show_log,
)
if after_build_result.aborted:
logger.info(f"[SendService] 消息 {outbound_message.message_id} 在构建后被 Hook 中止")
raise SendServiceError(f"消息 {outbound_message.message_id} 在构建后被 Hook 中止")
after_build_kwargs = after_build_result.kwargs
typing = _coerce_bool(after_build_kwargs.get("typing"), typing)
set_reply = _coerce_bool(after_build_kwargs.get("set_reply"), set_reply)
storage_message = _coerce_bool(after_build_kwargs.get("storage_message"), storage_message)
show_log = _coerce_bool(after_build_kwargs.get("show_log"), show_log)
sent_message = await send_session_message_with_message_detailed(
outbound_message,
typing=typing,
set_reply=set_reply,
reply_message_id=reply_message.message_id if reply_message is not None else None,
storage_message=storage_message,
show_log=show_log,
sync_to_maisaka_history=sync_to_maisaka_history,
maisaka_source_kind=maisaka_source_kind,
)
logger.debug(f"[SendService] 成功发送消息到 {stream_id}")
return sent_message
except SendServiceError:
raise
except Exception as exc:
logger.error(f"[SendService] 发送消息时出错: {exc}")
logger.debug(traceback.format_exc())
raise SendServiceError(f"发送消息时出错: {exc}") from exc
async def text_to_stream_with_message(
text: str,
stream_id: str,
@@ -1079,6 +1304,31 @@ async def text_to_stream_with_message(
)
async def text_to_stream_with_message_detailed(
text: str,
stream_id: str,
typing: bool = False,
set_reply: bool = False,
reply_message: Optional[MaiMessage] = None,
storage_message: bool = True,
selected_expressions: Optional[List[int]] = None,
sync_to_maisaka_history: bool = False,
maisaka_source_kind: str = "outbound_send",
) -> SessionMessage:
"""向指定流发送文本消息,失败时抛出带详情的异常。"""
return await _send_to_target_with_message_detailed(
message_sequence=MessageSequence(components=[TextComponent(text=text)]),
stream_id=stream_id,
typing=typing,
set_reply=set_reply,
reply_message=reply_message,
storage_message=storage_message,
selected_expressions=selected_expressions,
sync_to_maisaka_history=sync_to_maisaka_history,
maisaka_source_kind=maisaka_source_kind,
)
async def text_to_stream(
text: str,
stream_id: str,
@@ -1142,6 +1392,28 @@ async def emoji_to_stream_with_message(
)
async def emoji_to_stream_with_message_detailed(
emoji_base64: str,
stream_id: str,
storage_message: bool = True,
set_reply: bool = False,
reply_message: Optional[MaiMessage] = None,
sync_to_maisaka_history: bool = False,
maisaka_source_kind: str = "outbound_send",
) -> SessionMessage:
"""向指定流发送表情消息,失败时抛出带详情的异常。"""
return await _send_to_target_with_message_detailed(
message_sequence=_build_message_sequence_from_custom_message("emoji", emoji_base64),
stream_id=stream_id,
typing=False,
storage_message=storage_message,
set_reply=set_reply,
reply_message=reply_message,
sync_to_maisaka_history=sync_to_maisaka_history,
maisaka_source_kind=maisaka_source_kind,
)
async def emoji_to_stream(
emoji_base64: str,
stream_id: str,
@@ -1253,6 +1525,34 @@ async def custom_to_stream(
)
async def custom_to_stream_detailed(
message_type: str,
content: str | Dict[str, Any],
stream_id: str,
processed_plain_text: str = "",
typing: bool = False,
reply_message: Optional[MaiMessage] = None,
set_reply: bool = False,
storage_message: bool = True,
show_log: bool = True,
sync_to_maisaka_history: bool = False,
maisaka_source_kind: str = "outbound_send",
) -> SessionMessage:
"""向指定流发送自定义类型消息,失败时抛出带详情的异常。"""
return await _send_to_target_with_message_detailed(
message_sequence=_build_message_sequence_from_custom_message(message_type, content),
stream_id=stream_id,
processed_plain_text=processed_plain_text,
typing=typing,
reply_message=reply_message,
set_reply=set_reply,
storage_message=storage_message,
show_log=show_log,
sync_to_maisaka_history=sync_to_maisaka_history,
maisaka_source_kind=maisaka_source_kind,
)
async def custom_reply_set_to_stream(
reply_set: MessageSequence,
stream_id: str,