ForcePilot/backend/package/yuxi/channel/extensions/alexa/proactive.py
Kris 37493b6da6 feat(alexa): 新增Amazon Alexa渠道插件完整实现
新增了Alexa渠道的所有核心功能模块,包括配置管理、请求校验、会话管理、安全验证、去重、响应格式化、渐进式响应、主动推送、提醒功能、配对绑定以及Webhook端点,并完成了插件注册和配置项定义。
2026-05-21 10:38:31 +08:00

144 lines
5.3 KiB
Python

import logging
import time
import httpx
logger = logging.getLogger(__name__)
LWA_TOKEN_URL = "https://api.amazon.com/auth/o2/token"
PROACTIVE_EVENTS_URL = "https://api.amazonalexa.com/v1/proactiveEvents"
PROACTIVE_SCOPE = "alexa::proactive_events"
LWA_TOKEN_TTL_MARGIN = 60
class ProactiveEventsService:
def __init__(self, account=None):
self._account = account
self._enabled = getattr(account, "proactive_events_enabled", False) if account else False
self._client_id = getattr(account, "oauth_client_id", "") if account else ""
self._client_secret = getattr(account, "oauth_client_secret", "") if account else ""
self._lwa_token: str | None = None
self._lwa_token_expires_at: float = 0
@property
def enabled(self) -> bool:
return self._enabled
async def send_message_alert(self, user_id: str, message: str) -> bool:
return await self._send_event(user_id=user_id, audience_type="Unicast")
async def send_broadcast_alert(self, message: str) -> bool:
return await self._send_event(user_id=None, audience_type="Broadcast")
async def _send_event(self, user_id: str | None, audience_type: str) -> bool:
if not self._enabled:
logger.debug("Proactive events disabled, skipping send")
return False
token = await self._get_lwa_token()
if not token:
logger.warning("Failed to acquire LWA token for proactive events")
return False
user_label = user_id[:12] if user_id else "broadcast"
event_body = {
"timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
"referenceId": f"fp-msg-{user_label}-{int(time.time())}",
"expiryTime": time.strftime(
"%Y-%m-%dT%H:%M:%SZ", time.gmtime(time.time() + 86400)
),
"event": {
"name": "AMAZON.MessageAlert.Activated",
"payload": {
"state": {
"status": "UNREAD",
"freshness": "NEW",
},
"messageGroup": {
"creator": {
"name": "ForcePilot",
},
"count": 1,
"urgency": "URGENT",
},
},
},
"localizedAttributes": [
{
"locale": "zh-CN",
"sellerName": "ForcePilot",
}
],
"relevantAudience": self._build_audience(user_id, audience_type),
}
async with httpx.AsyncClient(timeout=httpx.Timeout(10.0)) as client:
try:
resp = await client.post(
PROACTIVE_EVENTS_URL,
json=event_body,
headers={
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
},
)
if resp.status_code == 202:
logger.info(
"Proactive event (%s) sent to %s",
audience_type,
user_label,
)
return True
logger.warning(
"Proactive event API returned %d: %s",
resp.status_code,
resp.text[:200],
)
except Exception:
logger.exception("Proactive event send failed")
return False
@staticmethod
def _build_audience(user_id: str | None, audience_type: str) -> dict:
if audience_type == "Unicast" and user_id:
return {
"type": "Unicast",
"payload": {
"user": user_id,
},
}
return {"type": "Multicast"}
async def _get_lwa_token(self) -> str | None:
now = time.time()
if self._lwa_token and now < self._lwa_token_expires_at - LWA_TOKEN_TTL_MARGIN:
return self._lwa_token
if not self._client_id or not self._client_secret:
logger.warning("LWA credentials not configured for proactive events")
return None
async with httpx.AsyncClient(timeout=httpx.Timeout(10.0)) as client:
try:
resp = await client.post(
LWA_TOKEN_URL,
data={
"grant_type": "client_credentials",
"client_id": self._client_id,
"client_secret": self._client_secret,
"scope": PROACTIVE_SCOPE,
},
headers={"Content-Type": "application/x-www-form-urlencoded"},
)
if resp.status_code == 200:
data = resp.json()
self._lwa_token = data.get("access_token")
expires_in = data.get("expires_in", 3600)
self._lwa_token_expires_at = now + expires_in
return self._lwa_token
logger.warning("LWA token request returned %d", resp.status_code)
except Exception:
logger.exception("LWA token acquisition failed")
return None