ForcePilot/backend/package/yuxi/channel/extensions/kakaotalk/bot.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

148 lines
4.3 KiB
Python

from __future__ import annotations
import json
import logging
import httpx
logger = logging.getLogger(__name__)
KAKAO_API_BASE = "https://kapi.kakao.com"
class KakaoTalkBotClient:
def __init__(self, admin_key: str):
self._admin_key = admin_key
self._http = httpx.AsyncClient(
base_url=KAKAO_API_BASE,
headers={
"Authorization": f"KakaoAK {admin_key}",
"Content-Type": "application/x-www-form-urlencoded",
},
timeout=httpx.Timeout(10.0),
)
async def probe(self) -> bool:
try:
resp = await self._http.get("/v2/user/me")
return resp.status_code == 200
except Exception:
return False
async def send_text(self, user_key: str, text: str) -> dict:
template = {
"object_type": "text",
"text": text,
}
resp = await self._http.post(
"/v1/api/talk/friends/message/send",
data={
"receiver_uuids": json.dumps([user_key]),
"template_object": json.dumps(template),
},
)
resp.raise_for_status()
return resp.json()
async def send_photo(self, user_key: str, image_url: str) -> dict:
template = {
"object_type": "feed",
"content": {
"title": "",
"image_url": image_url,
"image_width": 800,
"image_height": 800,
"link": {"web_url": image_url},
},
}
resp = await self._http.post(
"/v1/api/talk/friends/message/send",
data={
"receiver_uuids": json.dumps([user_key]),
"template_object": json.dumps(template),
},
)
resp.raise_for_status()
return resp.json()
async def send_list(
self, user_key: str,
header_title: str,
items: list[dict],
buttons: list[dict] | None = None,
) -> dict:
template = {
"object_type": "list",
"header_title": header_title,
"header_link": {"web_url": ""},
"contents": [
{
"title": item.get("title", ""),
"description": item.get("description", ""),
"image_url": item.get("image_url", ""),
"link": item.get("link", {"web_url": ""}),
}
for item in items[:5]
],
}
if buttons:
template["buttons"] = buttons[:2]
return await self.send_template(user_key, template)
async def send_commerce(
self, user_key: str,
title: str,
price: int,
currency: str = "won",
product_url: str = "",
image_url: str = "",
) -> dict:
template = {
"object_type": "commerce",
"content": {
"title": title,
"image_url": image_url,
"link": {"web_url": product_url},
"regular_price": price,
"currency_unit": currency,
"currency_unit_position": 0,
},
}
return await self.send_template(user_key, template)
async def send_location(
self, user_key: str,
address: str,
latitude: float,
longitude: float,
content_title: str = "",
) -> dict:
template = {
"object_type": "location",
"content": {
"title": content_title or address,
"address": address,
},
"address": address,
"address_title": address,
"social": {},
}
return await self.send_template(user_key, template)
async def send_template(self, user_key: str, template: dict) -> dict:
return await self.send_template_multi([user_key], template)
async def send_template_multi(self, user_keys: list[str], template: dict) -> dict:
resp = await self._http.post(
"/v1/api/talk/friends/message/send",
data={
"receiver_uuids": json.dumps(user_keys[:5]),
"template_object": json.dumps(template),
},
)
resp.raise_for_status()
return resp.json()
async def close(self) -> None:
await self._http.aclose()