ForcePilot/backend/package/yuxi/channel/extensions/freshdesk/gateway.py
Kris 3e861d6cd1 feat(freshdesk): 新增Freshdesk/Freshchat渠道插件
实现完整的Freshdesk和Freshchat集成支持,包含会话守卫、错误定义、消息去重、配置管理、webhook处理、出站消息发送、状态监控、安全校验、配对功能和流式回复支持
2026-05-21 10:47:20 +08:00

142 lines
5.2 KiB
Python

import hashlib
import hmac
import logging
from yuxi.channel.extensions.freshdesk.client import FreshdeskClient
from yuxi.channel.extensions.freshdesk.config import FreshdeskConfigAdapter
from yuxi.channel.extensions.freshdesk.dedupe import MessageDeduplicator
from yuxi.channel.extensions.freshdesk.errors import FreshdeskError, FreshdeskErrorCode
from yuxi.channel.extensions.freshdesk.reconciler import FreshdeskReconciler
from yuxi.channel.extensions.freshdesk.types import FreshdeskIdentity, FreshdeskMode
logger = logging.getLogger(__name__)
class FreshdeskGateway:
def __init__(self):
self._accounts: dict[str, dict] = {}
self._dedupe = MessageDeduplicator()
self._reconcilers: dict[str, FreshdeskReconciler] = {}
self._queue = None
@property
def accounts(self) -> dict[str, dict]:
return self._accounts
@property
def dedupe(self) -> MessageDeduplicator:
return self._dedupe
@property
def queue(self):
return self._queue
def resolve_account_by_hint(self, domain_hint: str | None = None, api_key_hint: str | None = None) -> dict | None:
if not self._accounts:
return None
if len(self._accounts) == 1:
return next(iter(self._accounts.values()))
for entry in self._accounts.values():
account = entry["account"]
if domain_hint and account.freshdesk_domain == domain_hint:
return entry
if domain_hint and account.freshchat_domain == domain_hint:
return entry
if api_key_hint and account.freshdesk_api_key.startswith(api_key_hint):
return entry
if api_key_hint and account.freshchat_api_key.startswith(api_key_hint):
return entry
return None
async def start(self, ctx) -> object:
adapter = FreshdeskConfigAdapter()
config = getattr(ctx, "raw_config", None) or {}
account = await self._resolve_account(ctx.account_id, adapter, config)
if not adapter.is_configured(account):
raise FreshdeskError(FreshdeskErrorCode.CONFIG_ERROR, "凭证配置不完整")
client = FreshdeskClient(
freshdesk_domain=account.freshdesk_domain,
freshdesk_api_key=account.freshdesk_api_key,
freshchat_domain=account.freshchat_domain,
freshchat_api_key=account.freshchat_api_key,
)
identity = await self._auth_test(client, account)
self._queue = getattr(ctx, "queue", None)
self._accounts[ctx.account_id] = {
"account": account,
"client": client,
"identity": identity,
}
reconciler = FreshdeskReconciler(client, account, self._dedupe, self._queue)
await reconciler.start()
self._reconcilers[ctx.account_id] = reconciler
logger.info("Freshdesk gateway started for account %s, mode=%s", account.account_id, account.mode)
return {
"running": True,
"account": account,
"identity": identity,
}
async def stop(self, ctx) -> None:
reconciler = self._reconcilers.pop(ctx.account_id, None)
if reconciler:
await reconciler.stop()
entry = self._accounts.pop(ctx.account_id, None)
if entry is None:
return
await entry["client"].close()
self._queue = None
logger.info("Freshdesk gateway stopped for account %s", ctx.account_id)
async def _resolve_account(self, account_id: str, adapter, config: dict):
return adapter.resolve_account(account_id, config)
async def _auth_test(self, client: FreshdeskClient, account) -> FreshdeskIdentity:
fc_ok = False
fd_ok = False
fc_identity = {}
fd_identity = {}
if account.mode in (FreshdeskMode.FRESHCHAT, FreshdeskMode.BOTH) and account.freshchat_api_key:
try:
fc_identity = await client.fc_get_me()
fc_ok = True
except Exception as e:
logger.warning("Freshchat Token 验证失败: %s", e)
if account.mode in (FreshdeskMode.FRESHDESK, FreshdeskMode.BOTH) and account.freshdesk_api_key:
try:
fd_identity = await client.fd_get_me()
fd_ok = True
except Exception as e:
logger.warning("Freshdesk API Key 验证失败: %s", e)
if not fc_ok and not fd_ok:
raise FreshdeskError(FreshdeskErrorCode.AUTH_ERROR, "所有凭证验证失败")
return FreshdeskIdentity(
freshdesk_agent_id=str(fd_identity.get("id", "")),
freshdesk_agent_name=fd_identity.get("contact", {}).get("name", "") if fd_ok else "",
freshchat_account_id=str(fc_identity.get("id", "")) if fc_ok else "",
freshchat_account_name=fc_identity.get("name", "") if fc_ok else "",
)
def verify_webhook_signature(self, body: bytes, x_freshchat_hmac: str, webhook_secret: str) -> bool:
if not webhook_secret or not x_freshchat_hmac:
return False
computed = hmac.new(
webhook_secret.encode("utf-8"),
body,
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(computed, x_freshchat_hmac)