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"{message}", }, "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": "正在为您查询,请稍候。", }, } 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"}