ForcePilot/backend/package/yuxi/channels/adapters/zalo_user/tool.py

214 lines
7.8 KiB
Python
Raw Normal View History

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