新增了完整的Signal渠道适配器实现,包含RPC客户端、守护进程管理、安全策略、消息处理、安装配置工具等全套功能,支持通过signal-cli与Signal网络进行通信,包含账户管理、消息收发、反应处理、媒体分析、健康检查等能力。
138 lines
4.2 KiB
Python
138 lines
4.2 KiB
Python
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import logging
|
|
from dataclasses import dataclass, field
|
|
|
|
import aiohttp
|
|
|
|
from yuxi.channels.models import HealthStatus
|
|
from yuxi.channels.adapters.signal.client import RpcClient, RpcError
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
PROBE_TIMEOUT_MS = 7500
|
|
|
|
|
|
class ProbeError(Exception):
|
|
def __init__(self, message: str, error_type: str = "UNKNOWN"):
|
|
super().__init__(message)
|
|
self.error_type = error_type
|
|
|
|
|
|
PROBE_ERROR_UNAUTHORIZED = "UNAUTHORIZED"
|
|
PROBE_ERROR_NOT_FOUND = "NOT_FOUND"
|
|
PROBE_ERROR_DAEMON_UNREACHABLE = "SIGNAL_DAEMON_UNREACHABLE"
|
|
PROBE_ERROR_TIMEOUT = "TIMEOUT"
|
|
PROBE_ERROR_UNKNOWN = "UNKNOWN"
|
|
|
|
|
|
@dataclass
|
|
class SignalProbeResult:
|
|
status: str = "unknown"
|
|
version: str | None = None
|
|
error_type: str | None = None
|
|
latency_ms: float = 0.0
|
|
metadata: dict = field(default_factory=dict)
|
|
|
|
@property
|
|
def success(self) -> bool:
|
|
return self.status == "healthy" and self.version is not None
|
|
|
|
|
|
async def probe_signal_daemon(rpc_client: RpcClient) -> SignalProbeResult:
|
|
import time as _time
|
|
|
|
start = _time.monotonic()
|
|
base_url = rpc_client.base_url
|
|
|
|
try:
|
|
about_result = await _probe_about_endpoint(base_url)
|
|
if about_result:
|
|
elapsed = (_time.monotonic() - start) * 1000
|
|
return SignalProbeResult(
|
|
status="healthy",
|
|
version=about_result.get("version", "unknown"),
|
|
latency_ms=elapsed,
|
|
metadata={"arm": "about", "about": about_result},
|
|
)
|
|
except Exception:
|
|
logger.debug("About endpoint probe failed, falling back to version RPC")
|
|
|
|
try:
|
|
version_result = await asyncio.wait_for(
|
|
rpc_client.call("version"),
|
|
timeout=PROBE_TIMEOUT_MS / 1000.0,
|
|
)
|
|
elapsed = (_time.monotonic() - start) * 1000
|
|
return SignalProbeResult(
|
|
status="healthy",
|
|
version=version_result.get("version", "unknown"),
|
|
latency_ms=elapsed,
|
|
metadata={"arm": "rpc_version"},
|
|
)
|
|
except TimeoutError:
|
|
return SignalProbeResult(
|
|
status="unhealthy",
|
|
error_type=PROBE_ERROR_TIMEOUT,
|
|
latency_ms=PROBE_TIMEOUT_MS,
|
|
)
|
|
except RpcError as e:
|
|
return SignalProbeResult(
|
|
status="unhealthy",
|
|
error_type=_classify_rpc_error(e),
|
|
metadata={"error": str(e)},
|
|
)
|
|
except Exception as e:
|
|
return SignalProbeResult(
|
|
status="unhealthy",
|
|
error_type=PROBE_ERROR_DAEMON_UNREACHABLE,
|
|
metadata={"error": str(e)},
|
|
)
|
|
|
|
|
|
async def _probe_about_endpoint(base_url: str) -> dict | None:
|
|
url = f"{base_url}/api/v1/about"
|
|
try:
|
|
async with aiohttp.ClientSession() as session:
|
|
async with session.get(url) as resp:
|
|
if resp.status == 200:
|
|
return await resp.json()
|
|
except Exception:
|
|
pass
|
|
return None
|
|
|
|
|
|
async def health_check_signal(rpc_client: RpcClient) -> HealthStatus:
|
|
try:
|
|
probe_result = await probe_signal_daemon(rpc_client)
|
|
if not probe_result.success:
|
|
return HealthStatus(
|
|
status="unhealthy",
|
|
last_error=probe_result.error_type or "unknown error",
|
|
metadata=probe_result.metadata,
|
|
)
|
|
return HealthStatus(
|
|
status="healthy",
|
|
metadata={
|
|
"version": probe_result.version or "unknown",
|
|
"latency_ms": probe_result.latency_ms,
|
|
**probe_result.metadata,
|
|
},
|
|
)
|
|
except Exception as e:
|
|
return HealthStatus(status="unhealthy", last_error=str(e))
|
|
|
|
|
|
def _classify_rpc_error(error: RpcError) -> str:
|
|
msg = str(error).lower()
|
|
if error.code == -32602:
|
|
return PROBE_ERROR_NOT_FOUND
|
|
if "unauthorized" in msg or "authorization" in msg or "forbidden" in msg:
|
|
return PROBE_ERROR_UNAUTHORIZED
|
|
if "not found" in msg or "missing" in msg:
|
|
return PROBE_ERROR_NOT_FOUND
|
|
if "refused" in msg or "unreachable" in msg or "connect" in msg:
|
|
return PROBE_ERROR_DAEMON_UNREACHABLE
|
|
return PROBE_ERROR_UNKNOWN
|