新增 Intercom 渠道扩展,支持在 Yuxi 平台中集成 Intercom 客服渠道。 包含以下功能模块: - client: Intercom API 客户端封装 - config: 渠道配置管理 - gateway: SSE/WebSocket 网关接入 - webhook: Webhook 事件处理 - streaming: 流式消息处理 - format: 消息格式转换 - outbound: 外发消息管理 - handoff: 转人工会话切换 - pairing: 用户配对与绑定 - security: 安全签名校验 - dedupe: 消息去重 - monitor: 渠道状态监控 - status: 会话状态管理 - session_guard: 会话防护 - types: 类型定义
193 lines
8.5 KiB
Python
193 lines
8.5 KiB
Python
import os
|
||
import logging
|
||
|
||
from yuxi.channel.extensions.intercom.types import IntercomAccount
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
class IntercomConfigAdapter:
|
||
def list_account_ids(self, config: dict) -> list[str]:
|
||
accounts = config.get("channels", {}).get("intercom", {}).get("accounts", {})
|
||
if accounts:
|
||
return list(accounts.keys())
|
||
if os.getenv("INTERCOM_ACCESS_TOKEN"):
|
||
return ["default"]
|
||
return []
|
||
|
||
async def resolve_account(self, account_id: str, config: dict | None = None) -> dict:
|
||
account = self._resolve_account_dataclass(account_id, config or {})
|
||
return {
|
||
"account_id": account.account_id,
|
||
"access_token": account.access_token,
|
||
"webhook_secret": account.webhook_secret,
|
||
"admin_id": account.admin_id,
|
||
"dm_policy": account.dm_policy,
|
||
"allow_from": account.allow_from,
|
||
"auto_skip_closed": account.auto_skip_closed,
|
||
"auto_skip_snoozed": account.auto_skip_snoozed,
|
||
"handoff_policy": account.handoff_policy,
|
||
"datacenter": account.datacenter,
|
||
}
|
||
|
||
def _resolve_account_dataclass(self, account_id: str, config: dict) -> IntercomAccount:
|
||
channel_cfg = config.get("channels", {}).get("intercom", {})
|
||
accounts = channel_cfg.get("accounts", {})
|
||
account_cfg = accounts.get(account_id, {}) if accounts else channel_cfg
|
||
|
||
access_token = self._resolve_token(account_cfg.get("accessToken"), "INTERCOM_ACCESS_TOKEN")
|
||
webhook_secret = self._resolve_token(account_cfg.get("webhookSecret"), "INTERCOM_WEBHOOK_SECRET")
|
||
admin_id = account_cfg.get("adminId") or os.getenv("INTERCOM_ADMIN_ID", "")
|
||
|
||
return IntercomAccount(
|
||
account_id=account_id,
|
||
access_token=access_token,
|
||
webhook_secret=webhook_secret,
|
||
admin_id=admin_id,
|
||
dm_policy=account_cfg.get("dmPolicy", "open"),
|
||
allow_from=account_cfg.get("allowFrom", []),
|
||
auto_skip_closed=account_cfg.get("autoSkipClosed", True),
|
||
auto_skip_snoozed=account_cfg.get("autoSkipSnoozed", True),
|
||
handoff_policy=account_cfg.get("handoffPolicy", "ignore"),
|
||
datacenter=account_cfg.get("datacenter", "us"),
|
||
)
|
||
|
||
@staticmethod
|
||
def _resolve_token(account_value: str | None, env_key: str) -> str:
|
||
if account_value is not None:
|
||
if account_value == "none":
|
||
return ""
|
||
if account_value.startswith("$"):
|
||
env_val = os.environ.get(account_value.strip("${}"), "")
|
||
return env_val
|
||
return account_value
|
||
return os.environ.get(env_key, "")
|
||
|
||
def is_configured(self, account: dict) -> bool:
|
||
return bool(account.get("access_token"))
|
||
|
||
def is_enabled(self, account: dict, config: dict) -> bool:
|
||
return True
|
||
|
||
def disabled_reason(self, account: dict, config: dict) -> str:
|
||
return ""
|
||
|
||
def unconfigured_reason(self, account: dict, config: dict) -> str:
|
||
if self.is_configured(account):
|
||
return ""
|
||
return "Missing required credential: accessToken"
|
||
|
||
def describe_account(self, account: dict, config: dict) -> dict:
|
||
return {
|
||
"account_id": account.get("account_id", ""),
|
||
"dm_policy": account.get("dm_policy", "open"),
|
||
"handoff_policy": account.get("handoff_policy", "ignore"),
|
||
}
|
||
|
||
def default_account_id(self, config: dict) -> str:
|
||
return config.get("channels", {}).get("intercom", {}).get("default_account", "default")
|
||
|
||
def resolve_allow_from(self, config: dict, account_id: str | None = None) -> list[str] | None:
|
||
channel_cfg = config.get("channels", {}).get("intercom", {})
|
||
accounts = channel_cfg.get("accounts", {})
|
||
if account_id and account_id in accounts:
|
||
return accounts[account_id].get("allowFrom")
|
||
return channel_cfg.get("allowFrom")
|
||
|
||
def format_allow_from(self, config: dict, account_id: str | None, allow_from: list[str | int]) -> list[str]:
|
||
return [str(entry) for entry in allow_from]
|
||
|
||
def has_configured_state(self, config: dict) -> bool:
|
||
accounts = config.get("channels", {}).get("intercom", {}).get("accounts", {})
|
||
if accounts:
|
||
for account_cfg in accounts.values():
|
||
if account_cfg.get("accessToken") or account_cfg.get("accessToken") is None:
|
||
return True
|
||
return bool(os.getenv("INTERCOM_ACCESS_TOKEN"))
|
||
|
||
def has_persisted_auth_state(self, config: dict) -> bool:
|
||
accounts = config.get("channels", {}).get("intercom", {}).get("accounts", {})
|
||
if accounts:
|
||
for account_cfg in accounts.values():
|
||
if account_cfg.get("accessToken"):
|
||
return True
|
||
return False
|
||
|
||
def config_schema(self) -> dict:
|
||
return {
|
||
"$schema": "https://json-schema.org/draft-07/schema#",
|
||
"type": "object",
|
||
"title": "Intercom 渠道配置",
|
||
"properties": {
|
||
"accounts": {
|
||
"type": "object",
|
||
"title": "账户列表",
|
||
"description": "Intercom 账户配置,key 为账户 ID",
|
||
"additionalProperties": {
|
||
"type": "object",
|
||
"properties": {
|
||
"accessToken": {
|
||
"type": "string",
|
||
"title": "Access Token",
|
||
"description": "Intercom Access Token (Settings > Developer Hub > Access Tokens)",
|
||
"x-ui-password": True,
|
||
},
|
||
"webhookSecret": {
|
||
"type": "string",
|
||
"title": "Webhook Secret",
|
||
"description": "用于验证 Webhook 请求的 HMAC 密钥",
|
||
"x-ui-password": True,
|
||
},
|
||
"adminId": {
|
||
"type": "string",
|
||
"title": "Admin ID",
|
||
"description": "Bot 的管理员 ID(留空则自动从 API 获取)",
|
||
},
|
||
"dmPolicy": {
|
||
"type": "string",
|
||
"title": "DM 安全策略",
|
||
"enum": ["open", "pairing", "allowlist", "disabled"],
|
||
"default": "open",
|
||
},
|
||
"allowFrom": {
|
||
"type": "array",
|
||
"title": "白名单 Contact ID",
|
||
"items": {"type": "string"},
|
||
"description": "仅 dmPolicy=allowlist 时生效,格式: contact_xxx 或 lead_xxx",
|
||
},
|
||
"autoSkipClosed": {
|
||
"type": "boolean",
|
||
"title": "跳过已关闭会话",
|
||
"default": True,
|
||
},
|
||
"autoSkipSnoozed": {
|
||
"type": "boolean",
|
||
"title": "跳过已暂停会话",
|
||
"default": True,
|
||
},
|
||
"handoffPolicy": {
|
||
"type": "string",
|
||
"title": "人工接管策略",
|
||
"enum": ["ignore", "note_only", "continue"],
|
||
"default": "ignore",
|
||
"description": "检测到人工接管后的行为",
|
||
},
|
||
"datacenter": {
|
||
"type": "string",
|
||
"title": "数据中心",
|
||
"enum": ["us", "eu", "au"],
|
||
"default": "us",
|
||
"description": "Intercom 数据中心区域",
|
||
},
|
||
},
|
||
"required": ["accessToken"],
|
||
},
|
||
},
|
||
"default_account": {
|
||
"type": "string",
|
||
"title": "默认账户",
|
||
"default": "default",
|
||
},
|
||
},
|
||
}
|