新增了Alexa渠道的所有核心功能模块,包括配置管理、请求校验、会话管理、安全验证、去重、响应格式化、渐进式响应、主动推送、提醒功能、配对绑定以及Webhook端点,并完成了插件注册和配置项定义。
131 lines
4.4 KiB
Python
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"}
|