fix(plugin-runtime): surface detailed send failures for plugin tools
This commit is contained in:
@@ -284,6 +284,22 @@ async def test_text_to_stream_returns_false_when_platform_io_fails(monkeypatch:
|
|||||||
assert len(fake_manager.sent_messages) == 1
|
assert len(fake_manager.sent_messages) == 1
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_custom_to_stream_detailed_raises_stream_not_found(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
monkeypatch.setattr(
|
||||||
|
send_service._chat_manager,
|
||||||
|
"get_session_by_session_id",
|
||||||
|
lambda stream_id: None,
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(send_service.SendServiceError, match="未找到聊天流: group_chat"):
|
||||||
|
await send_service.custom_to_stream_detailed(
|
||||||
|
message_type="poke",
|
||||||
|
content={"qq_id": "2810873701"},
|
||||||
|
stream_id="group_chat",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_private_outbound_message_preserves_bot_sender_and_receiver_user(
|
async def test_private_outbound_message_preserves_bot_sender_and_receiver_user(
|
||||||
monkeypatch: pytest.MonkeyPatch,
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ class RuntimeCoreCapabilityMixin:
|
|||||||
return {"success": False, "error": "缺少必要参数 text 或 stream_id"}
|
return {"success": False, "error": "缺少必要参数 text 或 stream_id"}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = await send_api.text_to_stream(
|
await send_api.text_to_stream_with_message_detailed(
|
||||||
text=text,
|
text=text,
|
||||||
stream_id=stream_id,
|
stream_id=stream_id,
|
||||||
typing=bool(args.get("typing", False)),
|
typing=bool(args.get("typing", False)),
|
||||||
@@ -90,7 +90,7 @@ class RuntimeCoreCapabilityMixin:
|
|||||||
sync_to_maisaka_history=sync_to_maisaka_history,
|
sync_to_maisaka_history=sync_to_maisaka_history,
|
||||||
maisaka_source_kind=maisaka_source_kind,
|
maisaka_source_kind=maisaka_source_kind,
|
||||||
)
|
)
|
||||||
return {"success": result}
|
return {"success": True}
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.error(f"[cap.send.text] 执行失败: {exc}", exc_info=True)
|
logger.error(f"[cap.send.text] 执行失败: {exc}", exc_info=True)
|
||||||
return {"success": False, "error": str(exc)}
|
return {"success": False, "error": str(exc)}
|
||||||
@@ -117,14 +117,14 @@ class RuntimeCoreCapabilityMixin:
|
|||||||
return {"success": False, "error": "缺少必要参数 emoji_base64 或 stream_id"}
|
return {"success": False, "error": "缺少必要参数 emoji_base64 或 stream_id"}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = await send_api.emoji_to_stream(
|
await send_api.emoji_to_stream_with_message_detailed(
|
||||||
emoji_base64=emoji_base64,
|
emoji_base64=emoji_base64,
|
||||||
stream_id=stream_id,
|
stream_id=stream_id,
|
||||||
storage_message=bool(args.get("storage_message", True)),
|
storage_message=bool(args.get("storage_message", True)),
|
||||||
sync_to_maisaka_history=sync_to_maisaka_history,
|
sync_to_maisaka_history=sync_to_maisaka_history,
|
||||||
maisaka_source_kind=maisaka_source_kind,
|
maisaka_source_kind=maisaka_source_kind,
|
||||||
)
|
)
|
||||||
return {"success": result}
|
return {"success": True}
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.error(f"[cap.send.emoji] 执行失败: {exc}", exc_info=True)
|
logger.error(f"[cap.send.emoji] 执行失败: {exc}", exc_info=True)
|
||||||
return {"success": False, "error": str(exc)}
|
return {"success": False, "error": str(exc)}
|
||||||
@@ -185,7 +185,7 @@ class RuntimeCoreCapabilityMixin:
|
|||||||
return {"success": False, "error": "缺少必要参数 command 或 stream_id"}
|
return {"success": False, "error": "缺少必要参数 command 或 stream_id"}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = await send_api.custom_to_stream(
|
await send_api.custom_to_stream_detailed(
|
||||||
message_type="command",
|
message_type="command",
|
||||||
content=command,
|
content=command,
|
||||||
stream_id=stream_id,
|
stream_id=stream_id,
|
||||||
@@ -194,7 +194,7 @@ class RuntimeCoreCapabilityMixin:
|
|||||||
sync_to_maisaka_history=sync_to_maisaka_history,
|
sync_to_maisaka_history=sync_to_maisaka_history,
|
||||||
maisaka_source_kind=maisaka_source_kind,
|
maisaka_source_kind=maisaka_source_kind,
|
||||||
)
|
)
|
||||||
return {"success": result}
|
return {"success": True}
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.error(f"[cap.send.command] 执行失败: {exc}", exc_info=True)
|
logger.error(f"[cap.send.command] 执行失败: {exc}", exc_info=True)
|
||||||
return {"success": False, "error": str(exc)}
|
return {"success": False, "error": str(exc)}
|
||||||
@@ -224,7 +224,7 @@ class RuntimeCoreCapabilityMixin:
|
|||||||
return {"success": False, "error": "缺少必要参数 message_type 或 stream_id"}
|
return {"success": False, "error": "缺少必要参数 message_type 或 stream_id"}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = await send_api.custom_to_stream(
|
await send_api.custom_to_stream_detailed(
|
||||||
message_type=message_type,
|
message_type=message_type,
|
||||||
content=content,
|
content=content,
|
||||||
stream_id=stream_id,
|
stream_id=stream_id,
|
||||||
@@ -234,7 +234,7 @@ class RuntimeCoreCapabilityMixin:
|
|||||||
sync_to_maisaka_history=sync_to_maisaka_history,
|
sync_to_maisaka_history=sync_to_maisaka_history,
|
||||||
maisaka_source_kind=maisaka_source_kind,
|
maisaka_source_kind=maisaka_source_kind,
|
||||||
)
|
)
|
||||||
return {"success": result}
|
return {"success": True}
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.error(f"[cap.send.custom] 执行失败: {exc}", exc_info=True)
|
logger.error(f"[cap.send.custom] 执行失败: {exc}", exc_info=True)
|
||||||
return {"success": False, "error": str(exc)}
|
return {"success": False, "error": str(exc)}
|
||||||
|
|||||||
@@ -88,6 +88,22 @@ _RAISE_ON_FAILED_CAPABILITIES = frozenset(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class _PluginLogCaptureHandler(stdlib_logging.Handler):
|
||||||
|
"""捕获插件调用期间的 warning/error 日志。"""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
super().__init__(level=stdlib_logging.WARNING)
|
||||||
|
self.messages: List[str] = []
|
||||||
|
|
||||||
|
def emit(self, record: stdlib_logging.LogRecord) -> None:
|
||||||
|
try:
|
||||||
|
message = record.getMessage().strip()
|
||||||
|
except Exception:
|
||||||
|
message = ""
|
||||||
|
if message:
|
||||||
|
self.messages.append(message)
|
||||||
|
|
||||||
|
|
||||||
class _ContextAwarePlugin(Protocol):
|
class _ContextAwarePlugin(Protocol):
|
||||||
"""支持注入运行时上下文的插件协议。
|
"""支持注入运行时上下文的插件协议。
|
||||||
|
|
||||||
@@ -548,6 +564,36 @@ class PluginRunner:
|
|||||||
cast(_ContextAwarePlugin, instance)._set_context(ctx)
|
cast(_ContextAwarePlugin, instance)._set_context(ctx)
|
||||||
logger.debug(f"已为插件 {plugin_id} 注入 PluginContext")
|
logger.debug(f"已为插件 {plugin_id} 注入 PluginContext")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _enrich_failed_invoke_result(result: Any, log_messages: List[str]) -> Any:
|
||||||
|
"""将插件工具失败结果补充为包含最近错误详情的结构。"""
|
||||||
|
if not isinstance(result, dict) or result.get("success", True) is not False:
|
||||||
|
return result
|
||||||
|
|
||||||
|
detail = ""
|
||||||
|
for item in reversed(log_messages):
|
||||||
|
normalized = str(item or "").strip()
|
||||||
|
if normalized:
|
||||||
|
detail = normalized
|
||||||
|
break
|
||||||
|
if not detail:
|
||||||
|
return result
|
||||||
|
|
||||||
|
current_error = str(result.get("error") or "").strip()
|
||||||
|
current_message = str(result.get("message") or result.get("content") or "").strip()
|
||||||
|
|
||||||
|
if not current_error:
|
||||||
|
result["error"] = detail
|
||||||
|
elif detail not in current_error:
|
||||||
|
result["error"] = f"{current_error};{detail}"
|
||||||
|
|
||||||
|
if current_message:
|
||||||
|
if detail not in current_message:
|
||||||
|
result["message"] = f"{current_message}(原因:{detail})"
|
||||||
|
else:
|
||||||
|
result["message"] = detail
|
||||||
|
return result
|
||||||
|
|
||||||
def _apply_plugin_config(self, meta: PluginMeta, config_data: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
def _apply_plugin_config(self, meta: PluginMeta, config_data: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
||||||
"""在 Runner 侧为插件实例注入当前插件配置。
|
"""在 Runner 侧为插件实例注入当前插件配置。
|
||||||
|
|
||||||
@@ -1659,6 +1705,9 @@ class PluginRunner:
|
|||||||
|
|
||||||
component_name = invoke.component_name
|
component_name = invoke.component_name
|
||||||
handler_method = self._resolve_component_handler(meta, component_name)
|
handler_method = self._resolve_component_handler(meta, component_name)
|
||||||
|
plugin_logger = stdlib_logging.getLogger(f"plugin.{plugin_id}")
|
||||||
|
capture_handler = _PluginLogCaptureHandler()
|
||||||
|
plugin_logger.addHandler(capture_handler)
|
||||||
|
|
||||||
# 回退: 旧版 LegacyPluginAdapter 通过 invoke_component 统一桥接
|
# 回退: 旧版 LegacyPluginAdapter 通过 invoke_component 统一桥接
|
||||||
if (handler_method is None or not callable(handler_method)) and hasattr(meta.instance, "invoke_component"):
|
if (handler_method is None or not callable(handler_method)) and hasattr(meta.instance, "invoke_component"):
|
||||||
@@ -1683,12 +1732,18 @@ class PluginRunner:
|
|||||||
if inspect.iscoroutinefunction(handler_method)
|
if inspect.iscoroutinefunction(handler_method)
|
||||||
else handler_method(**invoke.args)
|
else handler_method(**invoke.args)
|
||||||
)
|
)
|
||||||
|
result = self._enrich_failed_invoke_result(result, capture_handler.messages)
|
||||||
resp_payload = InvokeResultPayload(success=True, result=result)
|
resp_payload = InvokeResultPayload(success=True, result=result)
|
||||||
return envelope.make_response(payload=resp_payload.model_dump())
|
return envelope.make_response(payload=resp_payload.model_dump())
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"插件 {plugin_id} 组件 {component_name} 执行异常: {e}", exc_info=True)
|
logger.error(f"插件 {plugin_id} 组件 {component_name} 执行异常: {e}", exc_info=True)
|
||||||
resp_payload = InvokeResultPayload(success=False, result=str(e))
|
error_text = str(e).strip()
|
||||||
|
if not error_text and capture_handler.messages:
|
||||||
|
error_text = capture_handler.messages[-1]
|
||||||
|
resp_payload = InvokeResultPayload(success=False, result=error_text)
|
||||||
return envelope.make_response(payload=resp_payload.model_dump())
|
return envelope.make_response(payload=resp_payload.model_dump())
|
||||||
|
finally:
|
||||||
|
plugin_logger.removeHandler(capture_handler)
|
||||||
|
|
||||||
async def _handle_llm_provider_invoke(self, envelope: Envelope) -> Envelope:
|
async def _handle_llm_provider_invoke(self, envelope: Envelope) -> Envelope:
|
||||||
"""处理 LLM Provider 调用请求。
|
"""处理 LLM Provider 调用请求。
|
||||||
|
|||||||
@@ -48,6 +48,10 @@ from src.plugin_runtime.host.hook_spec_registry import HookSpec, HookSpecRegistr
|
|||||||
logger = get_logger("send_service")
|
logger = get_logger("send_service")
|
||||||
|
|
||||||
|
|
||||||
|
class SendServiceError(RuntimeError):
|
||||||
|
"""发送服务错误。"""
|
||||||
|
|
||||||
|
|
||||||
def register_send_service_hook_specs(registry: HookSpecRegistry) -> List[HookSpec]:
|
def register_send_service_hook_specs(registry: HookSpecRegistry) -> List[HookSpec]:
|
||||||
"""注册发送服务内置 Hook 规格。
|
"""注册发送服务内置 Hook 规格。
|
||||||
|
|
||||||
@@ -581,6 +585,34 @@ def _build_outbound_session_message(
|
|||||||
return outbound_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:
|
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}")
|
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(
|
async def _send_via_platform_io(
|
||||||
message: SessionMessage,
|
message: SessionMessage,
|
||||||
*,
|
*,
|
||||||
@@ -884,6 +928,117 @@ async def send_session_message_with_message(
|
|||||||
return sent_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(
|
async def send_session_message(
|
||||||
message: SessionMessage,
|
message: SessionMessage,
|
||||||
*,
|
*,
|
||||||
@@ -1054,6 +1209,76 @@ async def _send_to_target_with_message(
|
|||||||
return None
|
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(
|
async def text_to_stream_with_message(
|
||||||
text: str,
|
text: str,
|
||||||
stream_id: 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(
|
async def text_to_stream(
|
||||||
text: str,
|
text: str,
|
||||||
stream_id: 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(
|
async def emoji_to_stream(
|
||||||
emoji_base64: str,
|
emoji_base64: str,
|
||||||
stream_id: 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(
|
async def custom_reply_set_to_stream(
|
||||||
reply_set: MessageSequence,
|
reply_set: MessageSequence,
|
||||||
stream_id: str,
|
stream_id: str,
|
||||||
|
|||||||
Reference in New Issue
Block a user