214 lines
7.8 KiB
Python
214 lines
7.8 KiB
Python
|
|
from __future__ import annotations
|
|||
|
|
|
|||
|
|
from typing import Annotated, Any
|
|||
|
|
|
|||
|
|
from pydantic import BaseModel, Field
|
|||
|
|
|
|||
|
|
from yuxi.utils.logging_config import logger
|
|||
|
|
|
|||
|
|
ZALOUSER_TOOL_DESCRIPTION = """
|
|||
|
|
在 Zalo 个人账户上执行操作。支持 7 种动作(action):
|
|||
|
|
|
|||
|
|
- send: 发送 Markdown 文本消息到指定会话
|
|||
|
|
- image: 发送图片到指定会话
|
|||
|
|
- link: 发送链接到指定会话
|
|||
|
|
- friends: 列出好友列表
|
|||
|
|
- groups: 列出群组列表
|
|||
|
|
- me: 获取当前登录账户信息
|
|||
|
|
- status: 检查 Zalo 账户认证状态
|
|||
|
|
|
|||
|
|
使用场景:
|
|||
|
|
1. 向 Zalo 联系人发送消息通知
|
|||
|
|
2. 查看好友或群组列表
|
|||
|
|
3. 检查 Zalo 账户状态
|
|||
|
|
|
|||
|
|
上下文自动推导:
|
|||
|
|
如果未显式提供 chat_id,工具会自动从 deliveryContext 推导 threadId 和 isGroup。
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
|
|||
|
|
class ZaloUserToolInput(BaseModel):
|
|||
|
|
action: str = Field(
|
|||
|
|
description="操作类型: send, image, link, friends, groups, me, status",
|
|||
|
|
)
|
|||
|
|
chat_id: str = Field(
|
|||
|
|
default="",
|
|||
|
|
description="目标会话 ID。对于 send/image/link 必须指定;留空时自动从上下文推导",
|
|||
|
|
)
|
|||
|
|
content: str = Field(
|
|||
|
|
default="",
|
|||
|
|
description="对于 send: Markdown 文本内容;对于 image: 图片 URL;对于 link: 链接 URL",
|
|||
|
|
)
|
|||
|
|
caption: str = Field(
|
|||
|
|
default="",
|
|||
|
|
description="对于 link: 链接的标题文本;对于 image: 图片描述",
|
|||
|
|
)
|
|||
|
|
query: str = Field(
|
|||
|
|
default="",
|
|||
|
|
description="对于 friends/groups: 搜索过滤关键词",
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
|
|||
|
|
class ZaloUserToolAdapter:
|
|||
|
|
"""Zalo User 工具适配器:将 7 种动作桥接到 Adapter 能力"""
|
|||
|
|
|
|||
|
|
def __init__(self, adapter: Any):
|
|||
|
|
self._adapter = adapter
|
|||
|
|
|
|||
|
|
async def execute(
|
|||
|
|
self,
|
|||
|
|
action: str,
|
|||
|
|
chat_id: str = "",
|
|||
|
|
content: str = "",
|
|||
|
|
caption: str = "",
|
|||
|
|
query: str = "",
|
|||
|
|
delivery_context: dict[str, Any] | None = None,
|
|||
|
|
) -> dict[str, Any]:
|
|||
|
|
resolved_chat_id = chat_id or self._resolve_ambient_target(delivery_context)
|
|||
|
|
|
|||
|
|
if action == "send":
|
|||
|
|
return await self._do_send(resolved_chat_id, content)
|
|||
|
|
elif action == "image":
|
|||
|
|
return await self._do_image(resolved_chat_id, content, caption)
|
|||
|
|
elif action == "link":
|
|||
|
|
return await self._do_link(resolved_chat_id, content, caption)
|
|||
|
|
elif action == "friends":
|
|||
|
|
return await self._do_friends(query)
|
|||
|
|
elif action == "groups":
|
|||
|
|
return await self._do_groups(query)
|
|||
|
|
elif action == "me":
|
|||
|
|
return await self._do_me()
|
|||
|
|
elif action == "status":
|
|||
|
|
return await self._do_status()
|
|||
|
|
else:
|
|||
|
|
return {
|
|||
|
|
"error": f"Unknown action: {action}",
|
|||
|
|
"valid_actions": ["send", "image", "link", "friends", "groups", "me", "status"],
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
def _resolve_ambient_target(self, delivery_context: dict[str, Any] | None) -> str:
|
|||
|
|
if not delivery_context:
|
|||
|
|
return ""
|
|||
|
|
thread_id = delivery_context.get("threadId") or delivery_context.get("thread_id") or ""
|
|||
|
|
if thread_id:
|
|||
|
|
return str(thread_id)
|
|||
|
|
return ""
|
|||
|
|
|
|||
|
|
async def _do_send(self, chat_id: str, content: str) -> dict[str, Any]:
|
|||
|
|
if not chat_id or not content:
|
|||
|
|
return {"error": "chat_id and content are required for send action"}
|
|||
|
|
try:
|
|||
|
|
identity = self._adapter._build_stream_identity(chat_id, "")
|
|||
|
|
from yuxi.channels.models import ChannelResponse, MessageType
|
|||
|
|
|
|||
|
|
response = ChannelResponse(
|
|||
|
|
identity=identity,
|
|||
|
|
message_type=MessageType.TEXT,
|
|||
|
|
content=content,
|
|||
|
|
)
|
|||
|
|
result = await self._adapter.send(response)
|
|||
|
|
if result.success:
|
|||
|
|
return {"status": "sent", "message_id": result.message_id}
|
|||
|
|
return {"status": "failed", "error": result.error}
|
|||
|
|
except Exception as e:
|
|||
|
|
logger.exception(f"[ZaloUserTool] send failed: {e}")
|
|||
|
|
return {"error": str(e)}
|
|||
|
|
|
|||
|
|
async def _do_image(self, chat_id: str, image_url: str, caption: str) -> dict[str, Any]:
|
|||
|
|
if not chat_id or not image_url:
|
|||
|
|
return {"error": "chat_id and content (image URL) are required for image action"}
|
|||
|
|
try:
|
|||
|
|
result = await self._adapter.send_media(chat_id, "image", image_url)
|
|||
|
|
if result.success:
|
|||
|
|
return {"status": "sent", "message_id": result.message_id}
|
|||
|
|
return {"status": "failed", "error": result.error}
|
|||
|
|
except Exception as e:
|
|||
|
|
logger.exception(f"[ZaloUserTool] image failed: {e}")
|
|||
|
|
return {"error": str(e)}
|
|||
|
|
|
|||
|
|
async def _do_link(self, chat_id: str, url: str, caption: str) -> dict[str, Any]:
|
|||
|
|
if not chat_id or not url:
|
|||
|
|
return {"error": "chat_id and content (link URL) are required for link action"}
|
|||
|
|
try:
|
|||
|
|
from .send import send_link
|
|||
|
|
|
|||
|
|
result = await send_link(self._adapter._bridge, chat_id, url, caption)
|
|||
|
|
if result.success:
|
|||
|
|
return {"status": "sent", "message_id": result.message_id}
|
|||
|
|
return {"status": "failed", "error": result.error}
|
|||
|
|
except Exception as e:
|
|||
|
|
logger.exception(f"[ZaloUserTool] link failed: {e}")
|
|||
|
|
return {"error": str(e)}
|
|||
|
|
|
|||
|
|
async def _do_friends(self, query: str) -> dict[str, Any]:
|
|||
|
|
try:
|
|||
|
|
friends = await self._adapter.list_friends(query)
|
|||
|
|
return {"friends": friends, "count": len(friends)}
|
|||
|
|
except Exception as e:
|
|||
|
|
logger.exception(f"[ZaloUserTool] friends failed: {e}")
|
|||
|
|
return {"error": str(e)}
|
|||
|
|
|
|||
|
|
async def _do_groups(self, query: str) -> dict[str, Any]:
|
|||
|
|
try:
|
|||
|
|
groups = await self._adapter.list_groups(query)
|
|||
|
|
return {"groups": groups, "count": len(groups)}
|
|||
|
|
except Exception as e:
|
|||
|
|
logger.exception(f"[ZaloUserTool] groups failed: {e}")
|
|||
|
|
return {"error": str(e)}
|
|||
|
|
|
|||
|
|
async def _do_me(self) -> dict[str, Any]:
|
|||
|
|
try:
|
|||
|
|
info = await self._adapter.get_account_info()
|
|||
|
|
return {"account": info}
|
|||
|
|
except Exception as e:
|
|||
|
|
logger.exception(f"[ZaloUserTool] me failed: {e}")
|
|||
|
|
return {"error": str(e)}
|
|||
|
|
|
|||
|
|
async def _do_status(self) -> dict[str, Any]:
|
|||
|
|
try:
|
|||
|
|
health = await self._adapter.health_check()
|
|||
|
|
return {
|
|||
|
|
"status": health.status,
|
|||
|
|
"credential_stage": health.metadata.get("credential_stage", "unknown"),
|
|||
|
|
"adapter_status": health.metadata.get("adapter_status", "unknown"),
|
|||
|
|
"account": health.metadata.get("account", "unknown"),
|
|||
|
|
}
|
|||
|
|
except Exception as e:
|
|||
|
|
logger.exception(f"[ZaloUserTool] status failed: {e}")
|
|||
|
|
return {"error": str(e)}
|
|||
|
|
|
|||
|
|
|
|||
|
|
def create_zalouser_tool(adapter: Any):
|
|||
|
|
"""创建注册到 LangChain 的 zalouser 工具
|
|||
|
|
|
|||
|
|
使用方式:
|
|||
|
|
tool = create_zalouser_tool(adapter_instance)
|
|||
|
|
"""
|
|||
|
|
tool_adapter = ZaloUserToolAdapter(adapter)
|
|||
|
|
|
|||
|
|
from yuxi.agents.toolkits.registry import tool as registry_tool
|
|||
|
|
|
|||
|
|
@registry_tool(
|
|||
|
|
category="channel",
|
|||
|
|
tags=["zalo", "消息"],
|
|||
|
|
display_name="Zalo User",
|
|||
|
|
description=ZALOUSER_TOOL_DESCRIPTION,
|
|||
|
|
args_schema=ZaloUserToolInput,
|
|||
|
|
)
|
|||
|
|
async def zalouser_tool(
|
|||
|
|
action: Annotated[str, "操作类型: send, image, link, friends, groups, me, status"],
|
|||
|
|
chat_id: Annotated[str, "目标会话 ID(可选,自动从上下文推导)"] = "",
|
|||
|
|
content: Annotated[str, "消息内容/图片URL/链接URL"] = "",
|
|||
|
|
caption: Annotated[str, "标题/描述"] = "",
|
|||
|
|
query: Annotated[str, "搜索关键词"] = "",
|
|||
|
|
) -> dict:
|
|||
|
|
return await tool_adapter.execute(
|
|||
|
|
action=action,
|
|||
|
|
chat_id=chat_id,
|
|||
|
|
content=content,
|
|||
|
|
caption=caption,
|
|||
|
|
query=query,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
return zalouser_tool
|