ForcePilot/backend/package/yuxi/channels/adapters/zalo_user/tool.py
Kris 9285da5c55 feat(zalo-user): 实现完整的Zalo用户频道适配器
新增了Zalo用户频道的完整适配器实现,包括:
- 基础的适配器初始化与导出结构
- 群组同步与成员获取功能
- 请求限流与退避重试机制
- 健康检查与状态探针
- 消息反应/表情处理工具
- 贴纸缓存与消息去重功能
- 消息ID格式化与追踪
- TTS语音合成支持
- 消息发送权限校验
- 长文本分块发送
- 操作审批流程
- 常量配置与国际化支持
- 图像视觉分析功能
- 贴纸消息处理
- 登录与配置向导
- 群组上下文缓存
- 网关连接管理
- 配置Schema校验
- 状态问题与安全审计
- 内联按钮与交互组件
- 交互式回调分发
- 联系人与群组目录管理
- 富媒体卡片消息支持
2026-05-12 00:53:13 +08:00

214 lines
7.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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