新增 LINE 官方账号对接的全套功能,包括: 1. 基础的 Bot 探测、会话解析、消息格式化能力 2. 富媒体消息模板、快速回复、卡片指令支持 3. Webhook 签名验证、重放防护、多账户路由管理 4. 消息发送、回复、分块传输、用户绑定管理 5. 交互式配置向导与诊断工具
179 lines
7.3 KiB
Python
179 lines
7.3 KiB
Python
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
from yuxi.channels.adapters.line.card_command import resolve_card_command
|
|
from yuxi.channels.models import DeliveryResult
|
|
|
|
|
|
class LINEActionsAdapter:
|
|
"""LINE Message Actions 适配器,提供 send/reply/broadcast/react/poll 等 actions 注册。"""
|
|
|
|
def __init__(self, adapter):
|
|
self._adapter = adapter
|
|
|
|
def available_actions(self) -> list[str]:
|
|
return [
|
|
"send",
|
|
"reply",
|
|
"broadcast",
|
|
"multicast",
|
|
"send_attachment",
|
|
"sticker",
|
|
"react",
|
|
"poll",
|
|
"download_file",
|
|
"mark_as_read",
|
|
"card",
|
|
]
|
|
|
|
async def execute(self, action_name: str, params: dict[str, Any]) -> DeliveryResult:
|
|
actions_map = {
|
|
"send": self._action_send,
|
|
"reply": self._action_reply,
|
|
"broadcast": self._action_broadcast,
|
|
"multicast": self._action_multicast,
|
|
"send_attachment": self._action_send_attachment,
|
|
"sticker": self._action_sticker,
|
|
"react": self._action_react,
|
|
"poll": self._action_poll,
|
|
"download_file": self._action_download_file,
|
|
"mark_as_read": self._action_mark_as_read,
|
|
"card": self._action_card,
|
|
}
|
|
handler = actions_map.get(action_name)
|
|
if handler is None:
|
|
return DeliveryResult(success=False, error=f"Unknown action: {action_name}")
|
|
return await handler(params)
|
|
|
|
async def _action_send(self, params: dict) -> DeliveryResult:
|
|
from yuxi.channels.models import ChannelIdentity, ChannelResponse
|
|
|
|
chat_id = params.get("chat_id", params.get("to", ""))
|
|
content = params.get("content", params.get("text", ""))
|
|
identity = ChannelIdentity(
|
|
channel_id=self._adapter.channel_id,
|
|
channel_type=self._adapter.channel_type,
|
|
channel_user_id=chat_id,
|
|
channel_chat_id=chat_id,
|
|
)
|
|
response = ChannelResponse(identity=identity, content=content)
|
|
return await self._adapter.send(response)
|
|
|
|
async def _action_reply(self, params: dict) -> DeliveryResult:
|
|
from yuxi.channels.models import ChannelIdentity, ChannelResponse
|
|
|
|
chat_id = params.get("chat_id", params.get("to", ""))
|
|
content = params.get("content", params.get("text", ""))
|
|
reply_token = params.get("reply_token", "")
|
|
identity = ChannelIdentity(
|
|
channel_id=self._adapter.channel_id,
|
|
channel_type=self._adapter.channel_type,
|
|
channel_user_id=chat_id,
|
|
channel_chat_id=chat_id,
|
|
)
|
|
response = ChannelResponse(identity=identity, content=content, metadata={"reply_token": reply_token})
|
|
return await self._adapter.send(response)
|
|
|
|
async def _action_broadcast(self, params: dict) -> DeliveryResult:
|
|
from yuxi.channels.models import ChannelIdentity, ChannelResponse
|
|
|
|
content = params.get("content", params.get("text", ""))
|
|
identity = ChannelIdentity(
|
|
channel_id=self._adapter.channel_id,
|
|
channel_type=self._adapter.channel_type,
|
|
channel_user_id="broadcast",
|
|
channel_chat_id="broadcast",
|
|
)
|
|
response = ChannelResponse(identity=identity, content=content)
|
|
return await self._adapter.send_broadcast(response)
|
|
|
|
async def _action_multicast(self, params: dict) -> DeliveryResult:
|
|
from yuxi.channels.models import ChannelIdentity, ChannelResponse
|
|
|
|
user_ids = params.get("user_ids", params.get("to", []))
|
|
content = params.get("content", params.get("text", ""))
|
|
identity = ChannelIdentity(
|
|
channel_id=self._adapter.channel_id,
|
|
channel_type=self._adapter.channel_type,
|
|
channel_user_id="multicast",
|
|
channel_chat_id="multicast",
|
|
)
|
|
response = ChannelResponse(identity=identity, content=content)
|
|
return await self._adapter.send_multicast(user_ids, response)
|
|
|
|
async def _action_send_attachment(self, params: dict) -> DeliveryResult:
|
|
chat_id = params.get("chat_id", params.get("to", ""))
|
|
media_type = params.get("media_type", params.get("type", "image"))
|
|
url = params.get("url", params.get("data", ""))
|
|
return await self._adapter.send_media(chat_id, media_type, url, **params)
|
|
|
|
async def _action_sticker(self, params: dict) -> DeliveryResult:
|
|
chat_id = params.get("chat_id", params.get("to", ""))
|
|
package_id = str(params.get("package_id", "1"))
|
|
sticker_id = str(params.get("sticker_id", "1"))
|
|
reply_token = params.get("reply_token")
|
|
return await self._adapter.send_sticker(chat_id, package_id, sticker_id, reply_token)
|
|
|
|
async def _action_react(self, params: dict) -> DeliveryResult:
|
|
chat_id = params.get("chat_id", params.get("to", ""))
|
|
msg_id = params.get("msg_id", params.get("message_id", ""))
|
|
emoji = params.get("emoji", "👍")
|
|
return await self._adapter.send_reaction(chat_id, msg_id, emoji)
|
|
|
|
async def _action_poll(self, params: dict) -> DeliveryResult:
|
|
chat_id = params.get("chat_id", params.get("to", ""))
|
|
question = params.get("question", "")
|
|
options = params.get("options", [])
|
|
anonymous = params.get("anonymous", False)
|
|
duration = params.get("duration_seconds", 0)
|
|
allow_multiple = params.get("allow_multiple", False)
|
|
return await self._adapter.create_poll(chat_id, question, options, anonymous, duration, allow_multiple)
|
|
|
|
async def _action_download_file(self, params: dict) -> DeliveryResult:
|
|
file_id = params.get("file_id", params.get("message_id", ""))
|
|
try:
|
|
await self._adapter.download_media(file_id)
|
|
return DeliveryResult(success=True, message_id=file_id)
|
|
except Exception as e:
|
|
return DeliveryResult(success=False, error=str(e))
|
|
|
|
async def _action_mark_as_read(self, params: dict) -> DeliveryResult:
|
|
chat_id = params.get("chat_id", params.get("to", ""))
|
|
ok = await self._adapter.mark_as_read(chat_id)
|
|
return DeliveryResult(success=ok)
|
|
|
|
async def _action_card(self, params: dict) -> DeliveryResult:
|
|
from yuxi.channels.models import ChannelIdentity, ChannelResponse
|
|
|
|
chat_id = params.get("chat_id", params.get("to", ""))
|
|
card_type = params.get("card_type", "")
|
|
card_data = params.get("card_data", {})
|
|
card_msg = resolve_card_command(f"[[card:{card_type}:{import_json_dumps(card_data)}]]")
|
|
|
|
if card_msg is None:
|
|
return DeliveryResult(success=False, error=f"Failed to build card: {card_type}")
|
|
|
|
identity = ChannelIdentity(
|
|
channel_id=self._adapter.channel_id,
|
|
channel_type=self._adapter.channel_type,
|
|
channel_user_id=chat_id,
|
|
channel_chat_id=chat_id,
|
|
)
|
|
response = ChannelResponse(
|
|
identity=identity,
|
|
content="",
|
|
metadata={
|
|
"line_message_type": "flex",
|
|
"flex_contents": card_msg.get("contents", {}),
|
|
"alt_text": card_msg.get("altText", "Card"),
|
|
},
|
|
)
|
|
return await self._adapter.send(response)
|
|
|
|
|
|
def import_json_dumps(obj):
|
|
import json
|
|
|
|
return json.dumps(obj, ensure_ascii=False)
|