新增 KakaoTalk 渠道扩展,支持在 Yuxi 平台中集成 KakaoTalk 即时通讯渠道。 包含以下功能模块: - bot: Bot 客户端封装 - config: 渠道配置管理 - gateway: SSE/WebSocket 网关接入 - webhook: Webhook 事件处理 - outbound: 外发消息管理 - streaming: 流式消息处理 - pairing: 用户配对与绑定 - security: 安全校验 - dedupe: 消息去重 - monitor: 渠道状态监控 - status: 会话状态管理 - card_builder: KakaoTalk 卡片消息构建 - quick_reply: 快捷回复处理 - types: 类型定义
116 lines
3.5 KiB
Python
116 lines
3.5 KiB
Python
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import logging
|
|
|
|
from yuxi.channel.extensions.kakaotalk.bot import KakaoTalkBotClient
|
|
from yuxi.channel.extensions.kakaotalk.config import KakaoTalkConfigAdapter
|
|
from yuxi.channel.extensions.kakaotalk.format import chunk_text, strip_markdown
|
|
from yuxi.channel.protocols import OutboundDeliveryMode
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class KakaoTalkOutboundAdapter:
|
|
delivery_mode = OutboundDeliveryMode.HYBRID
|
|
chunker_mode = "newline"
|
|
text_chunk_limit: int = 1000
|
|
|
|
def __init__(self):
|
|
self._config = KakaoTalkConfigAdapter()
|
|
|
|
async def send_text(
|
|
self,
|
|
target_id: str,
|
|
content: str,
|
|
*,
|
|
reply_to_id: str | None = None,
|
|
thread_id: str | None = None,
|
|
account_id: str | None = None,
|
|
) -> None:
|
|
account = await self._resolve_account(account_id)
|
|
if not account:
|
|
logger.error("KakaoTalk send_text: account not resolved")
|
|
return
|
|
|
|
admin_key = account.get("admin_key", "")
|
|
bot_user_key = target_id
|
|
|
|
if not admin_key or not bot_user_key:
|
|
return
|
|
|
|
client = KakaoTalkBotClient(admin_key=admin_key)
|
|
chunks = chunk_text(content, account.get("text_chunk_limit", 1000))
|
|
|
|
for chunk in chunks:
|
|
try:
|
|
await client.send_text(bot_user_key, chunk)
|
|
except Exception:
|
|
logger.exception("KakaoTalk send_text error for user %s", bot_user_key)
|
|
await asyncio.sleep(0.5)
|
|
|
|
async def send_media(
|
|
self,
|
|
target_id: str,
|
|
media_url: str,
|
|
media_type: str,
|
|
reply_to_id: str | None = None,
|
|
thread_id: str | None = None,
|
|
account_id: str | None = None,
|
|
) -> None:
|
|
account = await self._resolve_account(account_id)
|
|
if not account:
|
|
return
|
|
|
|
admin_key = account.get("admin_key", "")
|
|
bot_user_key = target_id
|
|
|
|
if not admin_key or not bot_user_key:
|
|
return
|
|
|
|
client = KakaoTalkBotClient(admin_key=admin_key)
|
|
try:
|
|
await client.send_photo(bot_user_key, media_url)
|
|
except Exception:
|
|
logger.exception("KakaoTalk send_media error for user %s", bot_user_key)
|
|
|
|
def build_skill_response(
|
|
self,
|
|
content: str,
|
|
quick_replies: list[dict] | None = None,
|
|
) -> dict:
|
|
from yuxi.channel.extensions.kakaotalk.card_builder import CardBuilder
|
|
|
|
output = CardBuilder.simple_text(content)
|
|
if quick_replies:
|
|
return CardBuilder.build_skill_response([output], quick_replies)
|
|
return CardBuilder.build_skill_response([output])
|
|
|
|
def chunker(self, text: str, limit: int, ctx: object | None = None) -> list[str]:
|
|
return chunk_text(text, limit)
|
|
|
|
def sanitize_text(self, text: str, payload: object) -> str:
|
|
return strip_markdown(text)
|
|
|
|
def resolve_target(
|
|
self,
|
|
to: str | None = None,
|
|
*,
|
|
config: dict | None = None,
|
|
allow_from: list[str] | None = None,
|
|
account_id: str | None = None,
|
|
mode: str | None = None,
|
|
) -> tuple[bool, str]:
|
|
if not to:
|
|
return False, "target required"
|
|
return True, to
|
|
|
|
async def _resolve_account(self, account_id: str | None) -> dict | None:
|
|
if not account_id:
|
|
account_id = "default"
|
|
try:
|
|
return await self._config.resolve_account(account_id)
|
|
except Exception:
|
|
logger.exception("KakaoTalk resolve_account error")
|
|
return None
|