ForcePilot/backend/package/yuxi/channel/extensions/whatsapp/gateway.py
Kris 7215418610 feat(channel): 添加企业微信、微博、WhatsApp 和 Workplace 渠道扩展
新增企业微信、微博、WhatsApp、Workplace 四个渠道扩展。

企业微信渠道扩展主要模块:config, gateway, webhook, webhook_bot, outbound, streaming, pairing, security, crypto, dedupe, persistent_dedupe, card, directory, events, externalcontact, media, mentions, menu, message, oauth, status

微博渠道扩展主要模块:config, gateway, webhook, outbound, streaming, pairing, security, dedupe, passive_reply, broadcast, message, menu, media, subscription, template, status

WhatsApp 渠道扩展主要模块:config, gateway, webhook, outbound, streaming, pairing, security, dedupe, actions, monitor, status

Workplace 渠道扩展主要模块:config, gateway, webhook, outbound, streaming, pairing, security, dedupe, actions, challenge, groups, media, mentions, menu, monitor, persona, quick_reply, signature, subscriptions, template, threading, users, status
2026-05-21 12:01:56 +08:00

123 lines
4.6 KiB
Python

import asyncio
import time
import logging
from yuxi.channel.extensions.whatsapp.monitor import parse_webhook_to_unified, parse_statuses
from yuxi.channel.extensions.whatsapp.types import WhatsAppAccount
logger = logging.getLogger(__name__)
class WhatsAppGateway:
def __init__(self, config_adapter, deduplicator):
self._config_adapter = config_adapter
self._deduplicator = deduplicator
self._running = False
self._last_message_at: float | None = None
self._tasks: list[asyncio.Task] = []
self._queue: asyncio.Queue | None = None
@property
def last_message_at(self) -> float | None:
return self._last_message_at
def get_queue(self) -> asyncio.Queue | None:
return self._queue
async def start(self, ctx) -> object:
account = self._resolve_account(ctx)
if not account.is_configured():
logger.warning("whatsapp account %s not configured, skipping start", account.account_id)
return {"running": False, "reason": "not-configured"}
self._queue = asyncio.Queue(maxsize=1000)
self._running = True
task = asyncio.create_task(self._webhook_processing_loop(account))
self._tasks.append(task)
logger.info("whatsapp gateway started for account %s", account.account_id)
return {"running": True, "account_id": account.account_id}
async def stop(self, ctx) -> None:
self._running = False
for task in self._tasks:
task.cancel()
self._tasks.clear()
self._queue = None
logger.info("whatsapp gateway stopped")
async def enqueue_webhook(self, payload: dict) -> None:
if self._queue is not None:
await self._queue.put(payload)
async def _webhook_processing_loop(self, account: WhatsAppAccount):
while self._running and self._queue is not None:
try:
payload = await asyncio.wait_for(self._queue.get(), timeout=1.0)
await self._process_payload(payload, account)
except TimeoutError:
continue
except asyncio.CancelledError:
break
except Exception:
logger.exception("Error processing webhook payload")
async def _process_payload(self, payload: dict, account: WhatsAppAccount):
messages = parse_webhook_to_unified(payload, account)
status_events = parse_statuses(payload, account)
for status in status_events:
logger.info(
"WhatsApp status: msg_id=%s status=%s recipient=%s",
status["msg_id"], status["status"], status["recipient_id"],
)
if status["errors"]:
logger.warning("WhatsApp message error: %s", status["errors"])
entries = payload.get("entry", [])
for entry in entries:
for change in entry.get("changes", []):
value = change.get("value", {})
for err in value.get("errors", []):
logger.error(
"WhatsApp webhook error: code=%s title=%s",
err.get("code"), err.get("title"),
)
for unified_msg in messages:
if self._is_echo(unified_msg, account):
continue
if self._deduplicator.is_duplicate(unified_msg.get("msg_id", "")):
logger.debug("Duplicate message ignored: %s", unified_msg.get("msg_id"))
continue
target_queue = self._resolve_target_queue(unified_msg)
if target_queue:
self._last_message_at = time.monotonic()
await target_queue.put(unified_msg)
logger.debug("WhatsApp message enqueued: sender=%s", unified_msg.get("sender", {}).get("id"))
@staticmethod
def _is_echo(msg: dict, account: WhatsAppAccount) -> bool:
sender = msg.get("sender", {})
sender_id = sender.get("id", "")
return sender_id == account.phone_number_id
def _resolve_target_queue(self, _msg: dict) -> asyncio.Queue | None:
return self._queue
def _resolve_account(self, ctx) -> WhatsAppAccount:
config = getattr(ctx, "config", {}) if ctx else {}
accounts = config.get("accounts", {})
account_id = getattr(ctx, "account_id", "default")
raw = accounts.get(account_id, {})
account_dict = self._config_adapter._build_account(account_id, raw)
field_names = {f.name for f in WhatsAppAccount.__dataclass_fields__.values()}
return WhatsAppAccount(**{k: v for k, v in account_dict.items() if k in field_names})