新增 Intercom 渠道扩展,支持在 Yuxi 平台中集成 Intercom 客服渠道。 包含以下功能模块: - client: Intercom API 客户端封装 - config: 渠道配置管理 - gateway: SSE/WebSocket 网关接入 - webhook: Webhook 事件处理 - streaming: 流式消息处理 - format: 消息格式转换 - outbound: 外发消息管理 - handoff: 转人工会话切换 - pairing: 用户配对与绑定 - security: 安全签名校验 - dedupe: 消息去重 - monitor: 渠道状态监控 - status: 会话状态管理 - session_guard: 会话防护 - types: 类型定义
188 lines
6.3 KiB
Python
188 lines
6.3 KiB
Python
import time
|
|
import logging
|
|
|
|
from yuxi.channel.extensions.intercom.client import IntercomClient
|
|
from yuxi.channel.extensions.intercom.types import IntercomProbeResult
|
|
from yuxi.channel.protocols import ChannelAccountSnapshot, ChannelStatusIssue
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class IntercomStatus:
|
|
default_runtime: ChannelAccountSnapshot | None = None
|
|
|
|
async def probe(self, account: dict | None = None) -> bool:
|
|
if not account:
|
|
return False
|
|
access_token = account.get("access_token", "")
|
|
if not access_token:
|
|
return False
|
|
probe_result = await self._probe_intercom(access_token)
|
|
return probe_result.ok
|
|
|
|
async def _probe_intercom(self, access_token: str, timeout: float = 10.0) -> IntercomProbeResult:
|
|
client = IntercomClient(access_token, timeout=timeout)
|
|
start = time.monotonic()
|
|
|
|
try:
|
|
me = await client.get_me()
|
|
latency_ms = (time.monotonic() - start) * 1000
|
|
return IntercomProbeResult(
|
|
ok=bool(me.get("id")),
|
|
admin_id=str(me.get("id", "")),
|
|
app_id=str(me.get("app", {}).get("id_code", "")),
|
|
name=me.get("name", ""),
|
|
email=me.get("email", ""),
|
|
latency_ms=latency_ms,
|
|
)
|
|
except Exception as e:
|
|
latency_ms = (time.monotonic() - start) * 1000
|
|
return IntercomProbeResult(ok=False, error=str(e), latency_ms=latency_ms)
|
|
finally:
|
|
await client.close()
|
|
|
|
def build_summary(self, snapshot: object) -> dict:
|
|
return {
|
|
"channel": "intercom",
|
|
"account_id": getattr(snapshot, "account_id", ""),
|
|
"status": getattr(snapshot, "status_state", "unknown"),
|
|
}
|
|
|
|
def build_channel_summary(
|
|
self,
|
|
account: dict,
|
|
config: dict,
|
|
default_account_id: str,
|
|
snapshot: ChannelAccountSnapshot,
|
|
) -> dict:
|
|
account_id = account.get("account_id", "")
|
|
return {
|
|
"channel": "intercom",
|
|
"account_id": account_id,
|
|
"is_default": account_id == default_account_id,
|
|
"configured": snapshot.configured,
|
|
"enabled": snapshot.enabled,
|
|
"connected": snapshot.connected,
|
|
"running": snapshot.running,
|
|
"status": snapshot.status_state,
|
|
"health_state": snapshot.health_state,
|
|
"dm_policy": snapshot.dm_policy,
|
|
"last_error": snapshot.last_error,
|
|
"probe": snapshot.probe,
|
|
}
|
|
|
|
def build_account_snapshot(
|
|
self,
|
|
account: dict,
|
|
config: dict,
|
|
runtime: ChannelAccountSnapshot | None = None,
|
|
probe: object | None = None,
|
|
audit: object | None = None,
|
|
) -> ChannelAccountSnapshot:
|
|
from yuxi.channel.extensions.intercom.config import IntercomConfigAdapter
|
|
|
|
adapter = IntercomConfigAdapter()
|
|
account_id = account.get("account_id", "")
|
|
configured = adapter.is_configured(account)
|
|
enabled = adapter.is_enabled(account, config)
|
|
status_state = self.resolve_account_state(account, config, configured, enabled)
|
|
|
|
return ChannelAccountSnapshot(
|
|
account_id=account_id,
|
|
enabled=enabled,
|
|
configured=configured,
|
|
status_state=status_state,
|
|
dm_policy=account.get("dm_policy", "open"),
|
|
allow_from=account.get("allow_from", []),
|
|
probe=probe,
|
|
audit=audit,
|
|
)
|
|
|
|
def format_capabilities_probe(self, probe: object) -> list[dict]:
|
|
if probe is None:
|
|
return []
|
|
return [
|
|
{
|
|
"label": "Intercom 连接",
|
|
"ok": getattr(probe, "ok", False),
|
|
"detail": f"{getattr(probe, 'name', '')} / {getattr(probe, 'email', '')}",
|
|
"latency_ms": getattr(probe, "latency_ms", None),
|
|
}
|
|
]
|
|
|
|
async def audit_account(
|
|
self,
|
|
account: dict,
|
|
timeout_ms: int,
|
|
config: dict,
|
|
probe: object | None = None,
|
|
) -> object:
|
|
access_token = account.get("access_token", "")
|
|
if not access_token:
|
|
return {"ok": False, "error": "No access token configured"}
|
|
|
|
probe_result = await self._probe_intercom(access_token)
|
|
return {
|
|
"ok": probe_result.ok,
|
|
"admin_id": probe_result.admin_id,
|
|
"app_id": probe_result.app_id,
|
|
"name": probe_result.name,
|
|
"email": probe_result.email,
|
|
"latency_ms": probe_result.latency_ms,
|
|
"error": probe_result.error,
|
|
}
|
|
|
|
async def build_capabilities_diagnostics(
|
|
self,
|
|
account: dict,
|
|
timeout_ms: int,
|
|
config: dict,
|
|
probe: object | None = None,
|
|
audit: object | None = None,
|
|
target: str | None = None,
|
|
) -> dict | None:
|
|
return {
|
|
"channel": "intercom",
|
|
"probe": self.format_capabilities_probe(probe),
|
|
"audit": audit,
|
|
}
|
|
|
|
def log_self_id(
|
|
self,
|
|
account: dict,
|
|
config: dict,
|
|
runtime: object,
|
|
include_channel_prefix: bool = False,
|
|
) -> None:
|
|
prefix = "Intercom " if include_channel_prefix else ""
|
|
account_id = account.get("account_id", "default")
|
|
logger.info("%sBot account loaded: id=%s", prefix, account_id)
|
|
|
|
def resolve_account_state(
|
|
self,
|
|
account: dict,
|
|
config: dict,
|
|
configured: bool,
|
|
enabled: bool,
|
|
) -> str:
|
|
if not configured:
|
|
return "unconfigured"
|
|
if not enabled:
|
|
return "disabled"
|
|
return "enabled"
|
|
|
|
def collect_status_issues(self, accounts: list[ChannelAccountSnapshot]) -> list[ChannelStatusIssue]:
|
|
issues: list[ChannelStatusIssue] = []
|
|
for snapshot in accounts:
|
|
if snapshot.status_state == "unconfigured":
|
|
issues.append(
|
|
ChannelStatusIssue(
|
|
channel="intercom",
|
|
account_id=snapshot.account_id,
|
|
kind="unconfigured",
|
|
message=f"Intercom 账户 {snapshot.account_id} 未配置凭证",
|
|
fix="在管理面板中配置 Intercom Access Token",
|
|
)
|
|
)
|
|
return issues
|