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

131 lines
4.4 KiB
Python

import asyncio
import json
import logging
from fastapi import APIRouter, Request
from fastapi.responses import JSONResponse, PlainTextResponse, Response
import httpx
from ask_sdk_webservice_support.webservice_handler import WebserviceSkillHandler
from yuxi.channel.extensions.alexa.config import AlexaConfig
from yuxi.channel.extensions.alexa.skill import (
create_skill_handler,
extract_query_from_envelope,
set_cached_agent_response,
dispatch_to_agent,
)
from yuxi.channel.plugins.registry import ChannelPluginRegistry
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/channel/alexa", tags=["alexa"])
_skill_handler = WebserviceSkillHandler(skill=create_skill_handler())
_config = AlexaConfig()
def _build_error_response(message: str = "抱歉,服务暂时不可用,请稍后再试。") -> dict:
return {
"version": "1.0",
"response": {
"outputSpeech": {
"type": "SSML",
"ssml": f"<speak>{message}</speak>",
},
"shouldEndSession": True,
},
}
def _extract_progressive_credentials(body: dict) -> tuple[str, str]:
context = body.get("context", {})
system = context.get("System", context.get("system", {}))
api_access_token = system.get("apiAccessToken", "")
api_endpoint = system.get("apiEndpoint", "")
return api_access_token, api_endpoint
async def _send_progressive_response(request_id: str, api_access_token: str, api_endpoint: str):
if not api_access_token or not api_endpoint:
return
directive_url = f"{api_endpoint}/v1/directives"
directive_body = {
"header": {
"requestId": str(request_id),
},
"directive": {
"type": "VoicePlayer.Speak",
"speech": "<speak>正在为您查询,请稍候。</speak>",
},
}
async with httpx.AsyncClient(timeout=httpx.Timeout(2.0)) as client:
try:
await client.post(
directive_url,
json=directive_body,
headers={"Authorization": f"Bearer {api_access_token}"},
)
except Exception:
logger.debug("Progressive response send failed (non-critical)")
@router.post("/skill")
async def alexa_skill_endpoint(request: Request):
body_bytes = await request.body()
headers = dict(request.headers)
try:
body = json.loads(body_bytes)
except json.JSONDecodeError:
logger.warning("Alexa webhook: invalid JSON body")
return PlainTextResponse("", status_code=400)
request_id = body.get("request", {}).get("requestId", "")
if request_id:
plugin = ChannelPluginRegistry.get("alexa")
if plugin and plugin.is_duplicate(request_id):
return PlainTextResponse("")
request_type = body.get("request", {}).get("type", "")
skip_agent_types = {"LaunchRequest", "SessionEndedRequest"}
if request_type not in skip_agent_types:
query_info = extract_query_from_envelope(body)
account = _config.resolve_account()
if account.progressive_response_enabled:
api_token, api_endpoint = _extract_progressive_credentials(body)
asyncio.create_task(_send_progressive_response(query_info["request_id"], api_token, api_endpoint))
agent_response = await dispatch_to_agent(query_info)
set_cached_agent_response(query_info["request_id"], agent_response)
try:
response = _skill_handler.verify_request_and_dispatch(
http_request_body=body_bytes,
http_request_headers=headers,
)
except Exception:
logger.exception("Alexa skill handler error")
return JSONResponse(_build_error_response(), status_code=200)
if isinstance(response, tuple):
status_code = response[1] if len(response) > 1 else 200
headers_out = response[2] if len(response) > 2 else {}
content = response[0]
if isinstance(content, bytes):
content = content.decode("utf-8")
try:
data = json.loads(content) if isinstance(content, str) else content
except json.JSONDecodeError:
return Response(content=content, status_code=status_code)
return JSONResponse(content=data, status_code=status_code, headers=headers_out)
return response
@router.get("/health")
async def alexa_health():
return {"status": "ok", "channel": "alexa"}