ForcePilot/backend/package/yuxi/channel/extensions/kakaotalk/outbound.py
Kris 8b66218d9b feat(channel): 添加 KakaoTalk 渠道扩展
新增 KakaoTalk 渠道扩展,支持在 Yuxi 平台中集成 KakaoTalk 即时通讯渠道。

包含以下功能模块:
- bot: Bot 客户端封装
- config: 渠道配置管理
- gateway: SSE/WebSocket 网关接入
- webhook: Webhook 事件处理
- outbound: 外发消息管理
- streaming: 流式消息处理
- pairing: 用户配对与绑定
- security: 安全校验
- dedupe: 消息去重
- monitor: 渠道状态监控
- status: 会话状态管理
- card_builder: KakaoTalk 卡片消息构建
- quick_reply: 快捷回复处理
- types: 类型定义
2026-05-21 11:12:31 +08:00

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