import html import logging logger = logging.getLogger(__name__) APL_DOCUMENT_TEMPLATE = { "type": "APL", "version": "1.8", "theme": "dark", "import": [], "resources": [], "styles": {}, "layouts": {}, "mainTemplate": { "parameters": ["payload"], "items": [ { "type": "Container", "width": "100vw", "height": "100vh", "paddingTop": "8vh", "paddingLeft": "8vw", "paddingRight": "8vw", "items": [ { "type": "Text", "text": "${payload.title}", "fontSize": "36dp", "fontWeight": "700", "color": "#FFFFFF", "paddingBottom": "24dp", }, { "type": "ScrollView", "height": "60vh", "item": { "type": "Text", "text": "${payload.content}", "fontSize": "28dp", "color": "#CCCCCC", "lineHeight": "1.4", }, }, ], } ], }, } APL_LIST_TEMPLATE = { "type": "APL", "version": "1.8", "theme": "dark", "import": [], "resources": [], "styles": {}, "layouts": {}, "mainTemplate": { "parameters": ["payload"], "items": [ { "type": "Container", "width": "100vw", "height": "100vh", "paddingTop": "4vh", "paddingLeft": "6vw", "paddingRight": "6vw", "items": [ { "type": "Text", "text": "${payload.title}", "fontSize": "32dp", "fontWeight": "700", "color": "#FFFFFF", "paddingBottom": "20dp", }, { "type": "ScrollView", "height": "72vh", "item": { "type": "Sequence", "data": "${payload.items}", "numbered": False, "items": [ { "type": "TouchWrapper", "onPress": { "type": "SendEvent", "arguments": ["selectItem", "${data.index}"] }, "item": { "type": "Container", "borderRadius": "12dp", "backgroundColor": "#2A2A3A", "padding": "16dp", "spacing": "6dp", "items": [ { "type": "Text", "text": "${data.title}", "fontSize": "26dp", "fontWeight": "600", "color": "#FFFFFF", }, { "type": "Text", "text": "${data.summary}", "fontSize": "20dp", "color": "#AAAAAA", "maxLines": 3, }, ], }, }, ], }, }, ], } ], }, } APL_IMAGE_TEMPLATE = { "type": "APL", "version": "1.8", "theme": "dark", "import": [], "resources": [], "styles": {}, "layouts": {}, "mainTemplate": { "parameters": ["payload"], "items": [ { "type": "Container", "width": "100vw", "height": "100vh", "paddingTop": "6vh", "paddingLeft": "6vw", "paddingRight": "6vw", "alignItems": "center", "items": [ { "type": "Text", "text": "${payload.title}", "fontSize": "28dp", "fontWeight": "700", "color": "#FFFFFF", "paddingBottom": "16dp", }, { "type": "Image", "source": "${payload.imageUrl}", "width": "80vw", "height": "50vh", "scale": "best-fit", "align": "center", }, { "type": "Text", "text": "${payload.caption}", "fontSize": "22dp", "color": "#AAAAAA", "paddingTop": "16dp", "maxLines": 2, }, ], } ], }, } class SSMLResponseBuilder: @staticmethod def build_response( ssml: str, should_end_session: bool = False, session_attributes: dict | None = None, card_title: str | None = None, card_content: str | None = None, ) -> dict: resp = { "version": "1.0", "sessionAttributes": session_attributes or {}, "response": { "outputSpeech": { "type": "SSML", "ssml": ssml, }, "shouldEndSession": should_end_session, }, } if card_title and card_content: resp["response"]["card"] = { "type": "Simple", "title": card_title, "content": card_content, } return resp @staticmethod def build_error_response(message: str = "抱歉,我遇到了一些问题。") -> dict: return SSMLResponseBuilder.build_response( ssml=f"{message}", should_end_session=True, ) @staticmethod def build_apl_directive(title: str, content: str) -> dict: safe_title = html.escape(title, quote=False) safe_content = html.escape(content, quote=False) return { "type": "Alexa.Presentation.APL.RenderDocument", "token": "forcepilot-response-token", "document": APL_DOCUMENT_TEMPLATE, "datasources": { "payload": { "title": safe_title, "content": safe_content, } }, } @staticmethod def build_apl_list_directive(title: str, items: list[dict]) -> dict: safe_title = html.escape(title, quote=False) safe_items = [] for idx, item in enumerate(items): safe_items.append({ "index": idx, "title": html.escape(str(item.get("title", "")), quote=False), "summary": html.escape(str(item.get("summary", "")), quote=False), }) return { "type": "Alexa.Presentation.APL.RenderDocument", "token": "forcepilot-list-token", "document": APL_LIST_TEMPLATE, "datasources": { "payload": { "title": safe_title, "items": safe_items, } }, } @staticmethod def build_apl_image_directive(title: str, image_url: str, caption: str = "") -> dict: safe_title = html.escape(title, quote=False) safe_caption = html.escape(caption, quote=False) return { "type": "Alexa.Presentation.APL.RenderDocument", "token": "forcepilot-image-token", "document": APL_IMAGE_TEMPLATE, "datasources": { "payload": { "title": safe_title, "imageUrl": image_url, "caption": safe_caption, } }, } @staticmethod def supports_apl(handler_input) -> bool: try: envelope = handler_input.request_envelope device = envelope.context.system.device if device and device.supported_interfaces: return "Alexa.Presentation.APL" in device.supported_interfaces except Exception: pass return False @staticmethod def build_execute_commands_directive( token: str, commands: list[dict], ) -> dict: return { "type": "Alexa.Presentation.APL.ExecuteCommands", "token": token, "commands": commands, } @staticmethod def build_set_value_command( component_id: str, property_name: str, value: str, ) -> dict: return { "type": "SetValue", "componentId": component_id, "property": property_name, "value": value, } @staticmethod def build_speak_item_command(ssml: str) -> dict: return { "type": "SpeakItem", "componentId": "", "highlightMode": "line", "minimumDwellTime": 1000, } @staticmethod def build_scroll_command(distance: int = 200) -> dict: return { "type": "Scroll", "distance": distance, } @staticmethod def build_update_content_commands( token: str, title: str | None = None, content: str | None = None, ) -> dict: commands = [] if title is not None: commands.append( SSMLResponseBuilder.build_set_value_command( "contentTitle", "text", html.escape(title, quote=False) ) ) if content is not None: commands.append( SSMLResponseBuilder.build_set_value_command( "contentBody", "text", html.escape(content, quote=False) ) ) return SSMLResponseBuilder.build_execute_commands_directive(token, commands)