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

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,
}