mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-10-29 11:27:43 +08:00
严格检查全部宿主源码,清零类型错误预算并移除按模块类型豁免。 按原因、所属方法、错误码及语法指纹锁定必要兼容例外和站点类型桩, CI 阻断新增忽略、检查范围退化、本地桩替代真实接口及新的类型错误。 修复空值、异步和供应方类型边界,保持插件与 API 兼容合同, 并补充存储、订阅、通知、Agent 和工作流边界的回归覆盖。
159 lines
5.3 KiB
Python
159 lines
5.3 KiB
Python
"""
|
||
发送消息工具
|
||
"""
|
||
|
||
from typing import Any, Optional, Type
|
||
|
||
from pydantic import BaseModel, Field, model_validator
|
||
|
||
from app.agent.tools.base import MoviePilotTool
|
||
from app.agent.tools.tags import ToolTag
|
||
from app.runtime.log import logger
|
||
from app.schemas.message import Message
|
||
from app.schemas.types import MessageType
|
||
|
||
|
||
class SendMessageInput(BaseModel):
|
||
"""
|
||
发送消息工具的输入参数模型
|
||
"""
|
||
|
||
message: Optional[str] = Field(
|
||
None,
|
||
description="发送给用户的清晰完整消息正文",
|
||
)
|
||
title: Optional[str] = Field(
|
||
None,
|
||
description="简短概括正文的消息标题",
|
||
)
|
||
image_url: Optional[str] = Field(
|
||
None,
|
||
description="支持图片的渠道中随消息发送的可选图片 URL,例如 Telegram 或 Slack",
|
||
)
|
||
rich_message: Optional[str] = Field(
|
||
None,
|
||
description=(
|
||
"采用 GitHub 风格 Markdown 的完整 Telegram 富文本正文。"
|
||
"Telegram 用它替代 message、title 和 image_url;"
|
||
"其他渠道在 message 为空时用作纯文本回退"
|
||
),
|
||
)
|
||
|
||
@model_validator(mode="after")
|
||
def validate_payload(self) -> "SendMessageInput":
|
||
"""
|
||
校验消息内容和可选格式参数。
|
||
"""
|
||
if (
|
||
not self.message
|
||
and not self.title
|
||
and not self.image_url
|
||
and not self.rich_message
|
||
):
|
||
raise ValueError("message、title、image_url、rich_message 至少需要提供一个")
|
||
return self
|
||
|
||
|
||
class SendMessageTool(MoviePilotTool):
|
||
"""
|
||
发送普通通知消息给当前用户。
|
||
"""
|
||
|
||
name: str = Field(
|
||
default="send_message",
|
||
description="供模型调用的稳定工具名称",
|
||
)
|
||
tags: list[str] = Field(
|
||
default=[ToolTag.Write, ToolTag.Message, ToolTag.Admin, ToolTag.TerminalResponse],
|
||
description="工具的能力和权限分类标签,初始化时会统一归一化",
|
||
)
|
||
sends_message: bool = Field(
|
||
default=True,
|
||
description="工具是否直接向当前用户发送消息",
|
||
)
|
||
return_direct: bool = Field(
|
||
default=True,
|
||
description="是否直接返回工具结果并结束本轮智能体响应",
|
||
)
|
||
description: str = Field(
|
||
default=(
|
||
"通过已配置的通知渠道(Telegram、Slack、微信等)向用户发送消息。"
|
||
"支持图片的渠道可传 image_url。"
|
||
"Telegram 的结构化最终回复优先使用 rich_message,并以 GitHub 风格 Markdown 提供完整内容。"
|
||
"本工具直接完成用户回复;"
|
||
"发送后不要再发送内容相同的最终文本。"
|
||
),
|
||
description="供模型理解工具用途、输入约束和调用边界的说明",
|
||
)
|
||
args_schema: Type[BaseModel] = Field(
|
||
default=SendMessageInput,
|
||
description="工具输入参数的验证模型或 JSON Schema",
|
||
)
|
||
require_admin: bool = Field(
|
||
default=True,
|
||
description="是否仅允许系统管理员调用",
|
||
)
|
||
|
||
def get_tool_message(self, **kwargs: Any) -> Optional[str]:
|
||
"""
|
||
根据消息参数生成友好的提示消息
|
||
"""
|
||
rich_message = kwargs.get("rich_message") or ""
|
||
message = kwargs.get("message", "") or rich_message
|
||
title = kwargs.get("title") or ""
|
||
image_url = kwargs.get("image_url")
|
||
|
||
# 截断过长的消息
|
||
if len(message) > 50:
|
||
message = message[:50] + "..."
|
||
|
||
if rich_message:
|
||
return f"发送富文本消息: {message}"
|
||
if title and image_url:
|
||
return f"发送图文消息: [{title}] {message}"
|
||
if title:
|
||
return f"发送消息: [{title}] {message}"
|
||
if image_url:
|
||
return f"发送图片消息: {message}"
|
||
return f"发送消息: {message}"
|
||
|
||
async def run( # type: ignore[override]
|
||
self,
|
||
message: Optional[str] = None,
|
||
title: Optional[str] = None,
|
||
image_url: Optional[str] = None,
|
||
rich_message: Optional[str] = None,
|
||
**kwargs: object,
|
||
) -> str:
|
||
"""
|
||
发送消息到当前会话渠道。
|
||
"""
|
||
title = title or ("图片" if image_url and not message and not rich_message else "")
|
||
text = message or rich_message or ""
|
||
|
||
logger.info(
|
||
f"执行工具: {self.name}, 参数: title={title}, message={text}, "
|
||
f"image_url={image_url}, rich_message={bool(rich_message)}"
|
||
)
|
||
try:
|
||
await self.send_message(
|
||
Message.model_validate({
|
||
"channel": self._channel,
|
||
"source": self._source,
|
||
"mtype": MessageType.Other,
|
||
"userid": self._user_id,
|
||
"username": self._username,
|
||
"title": title,
|
||
"text": text,
|
||
"image": image_url,
|
||
"rich_message": rich_message,
|
||
"save_history": False,
|
||
})
|
||
)
|
||
self._agent_context["user_reply_sent"] = True
|
||
self._agent_context["reply_mode"] = "send_message"
|
||
return "消息已发送"
|
||
except Exception as e:
|
||
logger.error(f"发送消息失败: {e}")
|
||
return f"发送消息时发生错误: {str(e)}"
|