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

105 lines
3.4 KiB
Python

from __future__ import annotations
import asyncio
from dataclasses import dataclass
from typing import Any
from yuxi.utils.logging_config import logger
from .bridge import BridgeClient
from .login import LoginFlow
@dataclass
class GatewayContext:
bridge_url: str
config: dict[str, Any]
bridge: BridgeClient | None = None
login_flow: LoginFlow | None = None
class ZaloGateway:
def __init__(self):
self._accounts: dict[str, GatewayContext] = {}
self._lock = asyncio.Lock()
async def start_account(self, profile: str, config: dict[str, Any]) -> GatewayContext:
async with self._lock:
if profile in self._accounts:
logger.info(f"[ZaloUser] Gateway account '{profile}' already started")
return self._accounts[profile]
bridge_url = config.get("bridge_url", "http://localhost:5556")
timeout = config.get("network", {}).get("connect_timeout", 10)
bridge = BridgeClient(bridge_url, timeout)
await bridge.start()
login_flow = LoginFlow(bridge, config)
ctx = GatewayContext(
bridge_url=bridge_url,
config=config,
bridge=bridge,
login_flow=login_flow,
)
self._accounts[profile] = ctx
logger.info(f"[ZaloUser] Gateway account '{profile}' started")
return ctx
async def login_with_qr_start(self, profile: str = "default") -> dict[str, Any]:
if profile not in self._accounts:
raise ValueError(f"Account '{profile}' not started. Call start_account first.")
ctx = self._accounts[profile]
if ctx.login_flow is None:
ctx.login_flow = LoginFlow(ctx.bridge, ctx.config)
result = await ctx.login_flow.start_qr_login()
return result
async def login_with_qr_wait(
self,
profile: str = "default",
poll_interval: float = 2.0,
timeout: float = 120.0,
) -> dict[str, Any]:
if profile not in self._accounts:
raise ValueError(f"Account '{profile}' not started.")
ctx = self._accounts[profile]
if ctx.login_flow is None:
ctx.login_flow = LoginFlow(ctx.bridge, ctx.config)
try:
logged_in = await ctx.login_flow.wait_for_login(poll_interval, timeout)
return {"status": "success", "logged_in": True}
except Exception as e:
return {"status": "error", "logged_in": False, "error": str(e)}
async def logout_account(self, profile: str = "default") -> dict[str, Any]:
if profile not in self._accounts:
return {"status": "not_found"}
ctx = self._accounts[profile]
if ctx.login_flow:
await ctx.login_flow.logout()
if ctx.bridge:
await ctx.bridge.stop()
del self._accounts[profile]
logger.info(f"[ZaloUser] Gateway account '{profile}' logged out")
return {"status": "logged_out"}
async def shutdown(self) -> None:
for profile in list(self._accounts.keys()):
await self.logout_account(profile)
def list_accounts(self) -> list[str]:
return list(self._accounts.keys())
_zalo_gateway: ZaloGateway | None = None
def get_zalo_gateway() -> ZaloGateway:
global _zalo_gateway
if _zalo_gateway is None:
_zalo_gateway = ZaloGateway()
return _zalo_gateway