新增 KakaoTalk 渠道扩展,支持在 Yuxi 平台中集成 KakaoTalk 即时通讯渠道。 包含以下功能模块: - bot: Bot 客户端封装 - config: 渠道配置管理 - gateway: SSE/WebSocket 网关接入 - webhook: Webhook 事件处理 - outbound: 外发消息管理 - streaming: 流式消息处理 - pairing: 用户配对与绑定 - security: 安全校验 - dedupe: 消息去重 - monitor: 渠道状态监控 - status: 会话状态管理 - card_builder: KakaoTalk 卡片消息构建 - quick_reply: 快捷回复处理 - types: 类型定义
156 lines
6.6 KiB
Python
156 lines
6.6 KiB
Python
from __future__ import annotations
|
|
|
|
import logging
|
|
import os
|
|
|
|
from yuxi.channel.extensions.kakaotalk.types import KakaoTalkAccount, KakaoTalkTokenSource
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
ENV_KAKAO_REST_API_KEY = "KAKAO_REST_API_KEY"
|
|
ENV_KAKAO_ADMIN_KEY = "KAKAO_ADMIN_KEY"
|
|
ENV_KAKAO_BOT_ID = "KAKAO_BOT_ID"
|
|
|
|
|
|
class KakaoTalkConfigAdapter:
|
|
|
|
def __init__(self):
|
|
self._config: dict = {}
|
|
|
|
def list_account_ids(self, config: dict) -> list[str]:
|
|
self._config = config
|
|
kt_cfg = self._get_kakaotalk_config()
|
|
accounts = kt_cfg.get("accounts", {}) if isinstance(kt_cfg, dict) else {}
|
|
if accounts:
|
|
return list(accounts.keys())
|
|
return ["default"]
|
|
|
|
async def resolve_account(self, account_id: str) -> dict:
|
|
raw = self._load_raw_config(account_id)
|
|
account = self._build_account(account_id, raw)
|
|
return self._to_dict(account)
|
|
|
|
def _get_kakaotalk_config(self) -> dict:
|
|
channels = self._config.get("channels", {})
|
|
return channels.get("kakaotalk", {}) if channels else self._config
|
|
|
|
def is_configured(self, account: dict) -> bool:
|
|
return bool(account.get("rest_api_key") and account.get("admin_key") and account.get("bot_id"))
|
|
|
|
def is_enabled(self, account: dict) -> bool:
|
|
return account.get("enabled", True)
|
|
|
|
def disabled_reason(self, account: dict) -> str:
|
|
missing = []
|
|
if not account.get("rest_api_key"):
|
|
missing.append("REST API Key")
|
|
if not account.get("admin_key"):
|
|
missing.append("Admin Key")
|
|
if not account.get("bot_id"):
|
|
missing.append("Bot ID")
|
|
if missing:
|
|
return f"Missing: {', '.join(missing)}"
|
|
return ""
|
|
|
|
def describe_account(self, account: dict) -> dict:
|
|
return {
|
|
"account_id": account.get("account_id", ""),
|
|
"name": account.get("name", ""),
|
|
"bot_id": account.get("bot_id", ""),
|
|
"channel_name": account.get("channel_name", ""),
|
|
"configured": self.is_configured(account),
|
|
}
|
|
|
|
def config_schema(self) -> dict:
|
|
return {
|
|
"type": "object",
|
|
"properties": {
|
|
"enabled": {"type": "boolean", "default": True},
|
|
"rest_api_key": {"type": "string", "title": "REST API Key"},
|
|
"admin_key": {"type": "string", "title": "Admin Key"},
|
|
"bot_id": {"type": "string", "title": "OpenBuilder Bot ID"},
|
|
"channel_name": {"type": "string", "title": "KakaoTalk Channel Name"},
|
|
"dm_policy": {
|
|
"type": "string",
|
|
"enum": ["pairing", "allowlist", "open", "disabled"],
|
|
"default": "pairing",
|
|
},
|
|
"allow_from": {"type": "array", "items": {"type": "string"}},
|
|
"skill_server_path": {"type": "string", "default": "/kakaotalk/skill"},
|
|
"text_chunk_limit": {"type": "integer", "default": 1000},
|
|
"media_max_mb": {"type": "integer", "default": 5},
|
|
"accounts": {
|
|
"type": "object",
|
|
"additionalProperties": {
|
|
"type": "object",
|
|
"properties": {
|
|
"name": {"type": "string"},
|
|
"rest_api_key": {"type": "string"},
|
|
"admin_key": {"type": "string"},
|
|
"bot_id": {"type": "string"},
|
|
"channel_name": {"type": "string"},
|
|
"dm_policy": {"type": "string", "enum": ["pairing", "allowlist", "open", "disabled"]},
|
|
"allow_from": {"type": "array", "items": {"type": "string"}},
|
|
"skill_server_path": {"type": "string"},
|
|
},
|
|
},
|
|
},
|
|
"default_account": {"type": "string", "default": "default"},
|
|
},
|
|
}
|
|
|
|
def _load_raw_config(self, account_id: str) -> dict:
|
|
kt_cfg = self._get_kakaotalk_config()
|
|
if account_id == "__base__":
|
|
return kt_cfg if isinstance(kt_cfg, dict) else {}
|
|
accounts = kt_cfg.get("accounts", {}) if isinstance(kt_cfg, dict) else {}
|
|
return accounts.get(account_id, {}) if isinstance(accounts, dict) else {}
|
|
|
|
def _build_account(self, account_id: str, raw: dict) -> KakaoTalkAccount:
|
|
base_raw = self._load_raw_config("__base__")
|
|
admin_key, admin_key_source = self._resolve_admin_key(account_id, raw, base_raw)
|
|
|
|
return KakaoTalkAccount(
|
|
account_id=account_id,
|
|
rest_api_key=raw.get("rest_api_key", base_raw.get("rest_api_key", "")),
|
|
admin_key=admin_key,
|
|
admin_key_source=admin_key_source,
|
|
bot_id=raw.get("bot_id", base_raw.get("bot_id", "")),
|
|
channel_name=raw.get("channel_name", base_raw.get("channel_name", "")),
|
|
name=raw.get("name", account_id),
|
|
dm_policy=raw.get("dm_policy", base_raw.get("dm_policy", "pairing")),
|
|
allow_from=raw.get("allow_from", base_raw.get("allow_from", [])),
|
|
skill_server_path=raw.get("skill_server_path", base_raw.get("skill_server_path", "/kakaotalk/skill")),
|
|
text_chunk_limit=raw.get("text_chunk_limit", base_raw.get("text_chunk_limit", 1000)),
|
|
media_max_mb=raw.get("media_max_mb", base_raw.get("media_max_mb", 5)),
|
|
)
|
|
|
|
@staticmethod
|
|
def _resolve_admin_key(account_id: str, raw: dict, base_raw: dict) -> tuple[str, KakaoTalkTokenSource]:
|
|
if raw.get("admin_key"):
|
|
return raw["admin_key"], KakaoTalkTokenSource.ACCOUNT
|
|
if account_id == "default":
|
|
if base_raw.get("admin_key"):
|
|
return base_raw["admin_key"], KakaoTalkTokenSource.BASE
|
|
env_key = os.environ.get(ENV_KAKAO_ADMIN_KEY, "")
|
|
if env_key:
|
|
return env_key, KakaoTalkTokenSource.ENV
|
|
return "", KakaoTalkTokenSource.NONE
|
|
|
|
@staticmethod
|
|
def _to_dict(account: KakaoTalkAccount) -> dict:
|
|
return {
|
|
"account_id": account.account_id,
|
|
"rest_api_key": account.rest_api_key,
|
|
"admin_key": account.admin_key,
|
|
"admin_key_source": account.admin_key_source.value,
|
|
"bot_id": account.bot_id,
|
|
"channel_name": account.channel_name,
|
|
"name": account.name,
|
|
"dm_policy": account.dm_policy,
|
|
"allow_from": account.allow_from,
|
|
"skill_server_path": account.skill_server_path,
|
|
"text_chunk_limit": account.text_chunk_limit,
|
|
"media_max_mb": account.media_max_mb,
|
|
"enabled": True,
|
|
} |