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