ForcePilot/backend/package/yuxi/channels/application/health/channel_probe.py
Kris 8eead29de0 refactor: 批量清理冗余空行,优化部分枚举使用方式
1.  移除所有适配器文件中多余的空导入行
2.  调整ValidationError继承,移除不必要的ValueError继承
3.  修正多处ChannelType使用方式,从.value改为直接使用枚举实例
4.  优化飞书插件部分硬编码渠道类型为枚举实例
5.  更新wechat_ilink插件清单与适配器配置
6.  新增飞书目录适配器缓存清理支持判断与iLink生命周期适配器凭据轮换支持判断
7.  优化配置处理器历史查询逻辑,区分键不存在与无历史记录场景
2026-07-04 00:14:56 +08:00

249 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""渠道主动探测器。
实现 ``ChannelProbe``,主动调用渠道 API 探测可用性,不得仅返回缓存状态。
探测通过可探测适配器(``ProbeableAdapter``)的 ``probe()`` 方法执行,
超时 30s结果映射为契约层 ``ProbeResult``。
"""
from __future__ import annotations
import asyncio
import time
from yuxi.channels.contract.dtos.channel import ChannelType
from yuxi.channels.contract.dtos.health import AdapterProbeOutcome, ProbeResult
from yuxi.channels.contract.errors.server import OperationTimeoutError
from yuxi.channels.contract.plugin.adapters import ProbeableAdapter
from yuxi.channels.contract.plugin.manifest import PluginManifest
from yuxi.channels.contract.ports.driven.cache_port import CachePort
from yuxi.channels.contract.ports.driven.logger_port import LoggerPort
from yuxi.channels.contract.ports.driven.persistence_health_port import PersistenceHealthPort
from yuxi.channels.contract.ports.driven.queue_port import QueuePort
from yuxi.channels.core.registry.plugin_registry import PluginRegistry
__all__ = ["ChannelProbe", "ProbeableAdapter", "AdapterProbeOutcome"]
class ChannelProbe:
"""渠道主动探测器。
FR-35。主动调用渠道 API 探测可用性,
不得仅返回缓存状态。探测可触发副作用(需明确标识)。
探测流程:
1. 检查下游依赖DB / Redis / Worker任一异常返回 ``healthy=False``。
下游检查与适配器探测均受 ``PROBE_TIMEOUT`` 超时保护。
2. 按清单的渠道类型从 ``PluginRegistry`` 动态查找可探测适配器
``ProbeableAdapter``)。插件未注册可探测适配器时返回
``healthy=False`` 且 message 标注"渠道未实现主动探测"
以区分"不健康""不支持探测"
3. 通过 ``asyncio.wait_for`` 调用适配器 ``probe()``,超时 30s。
4. 将适配器探测结果映射为契约层 ``ProbeResult``。
关键约束:
- **不得** 仅返回缓存状态,必须主动调用渠道 API。
- 探测超时抛出 ``TimeoutError``HTTP 504FR-35由上层
``unified_error_handler`` 统一处理。
- 探测异常返回 ``healthy=False`` 的 ``ProbeResult``,不抛异常。
- 下游依赖DB / Redis / Worker异常时返回 ``healthy=False``
避免下游故障被适配器探测掩盖。
"""
PROBE_TIMEOUT = 30.0
"""探测超时时间(秒)。"""
def __init__(
self,
plugin_registry: PluginRegistry,
persistence_port: PersistenceHealthPort,
cache_port: CachePort,
queue_port: QueuePort,
logger: LoggerPort,
) -> None:
"""初始化渠道主动探测器。
参数:
plugin_registry: 插件注册表,用于动态查找渠道的可探测适配器。
适配器由插件通过 ``PluginHost.registerAdapter("probeable", ...)``
注册,``ChannelProbe`` 每次探测时从注册表动态查找,保证插件
加载/卸载后适配器索引自动更新。
persistence_port: 持久化被驱动端口,用于探测前检查数据库可用性。
cache_port: 缓存被驱动端口,用于探测前检查缓存可用性。
queue_port: 队列被驱动端口,用于探测前检查 Worker 状态。
logger: 日志被驱动端口,用于记录探测过程。
"""
self._registry = plugin_registry
self._persistence_port = persistence_port
self._cache_port = cache_port
self._queue_port = queue_port
self._logger = logger
async def probe(self, manifest: PluginManifest) -> ProbeResult:
"""主动探测渠道 API 可用性。
参数:
manifest: 插件清单,提供渠道类型信息。
返回:
探针结果。``account_id`` 为空字符串(探测为渠道类型级别,
不绑定具体账户);``healthy`` 反映探测时刻的渠道真实状态。
"""
channel_type = manifest.manifest.channel_type
start = time.monotonic()
# 下游依赖检查 + 适配器探测均受 PROBE_TIMEOUT 超时保护,
# 避免下游 ping/worker_status 挂起导致探测请求无限期阻塞。
try:
return await asyncio.wait_for(
self._probeInternal(channel_type, start),
timeout=self.PROBE_TIMEOUT,
)
except TimeoutError as exc:
latency_ms = int((time.monotonic() - start) * 1000)
await self._logger.warn(
"渠道探测超时",
channel_type=channel_type,
timeout=str(self.PROBE_TIMEOUT),
latency_ms=str(latency_ms),
)
raise OperationTimeoutError(
int(self.PROBE_TIMEOUT * 1000),
message=f"probe timed out after {self.PROBE_TIMEOUT}s",
) from exc
async def _probeInternal(self, channel_type: ChannelType, start: float) -> ProbeResult:
"""执行探测内部逻辑(下游检查 + 适配器探测)。
参数:
channel_type: 渠道类型。
start: 探测开始时间(``time.monotonic()``),用于计算延迟。
返回:
探针结果。
"""
# 下游依赖检查DB / Redis / Worker队列。任一异常时返回不健康
# 避免下游故障被适配器探测掩盖。
try:
db_ok = await self._persistence_port.ping()
except Exception as exc:
await self._logger.warn(
"渠道探测下游检查DB ping 异常",
channel_type=channel_type,
error=str(exc),
)
db_ok = False
if not db_ok:
return ProbeResult(
channel_type=channel_type,
account_id="",
healthy=False,
latency_ms=0,
message="下游依赖异常:数据库不可用",
)
# CachePort.ping 契约保证故障时返回 False 不抛异常,但实现层
# RedisCacheAdapter依赖 Redis 连接,连接级故障仍可能抛异常。
# 此处捕获兜底,与 DB ping 处理一致,避免探测被缓存故障穿透。
try:
redis_ok = await self._cache_port.ping()
except Exception as exc:
await self._logger.warn(
"渠道探测下游检查Redis ping 异常",
channel_type=channel_type,
error=str(exc),
)
redis_ok = False
if not redis_ok:
return ProbeResult(
channel_type=channel_type,
account_id="",
healthy=False,
latency_ms=0,
message="下游依赖异常:缓存不可用",
)
try:
worker_status = await self._queue_port.getWorkerStatus()
except Exception as exc:
await self._logger.warn(
"渠道探测下游检查Worker 状态查询异常",
channel_type=channel_type,
error=str(exc),
)
worker_status = None
# getWorkerStatus 返回 WorkerStatus DTO查询异常或 available 非 True
# 均视为 Worker 停摆(与 HealthAggregator._aggregate 判定一致)。
if not (worker_status is not None and worker_status.available is True):
return ProbeResult(
channel_type=channel_type,
account_id="",
healthy=False,
latency_ms=0,
message="下游依赖异常Worker 停摆",
)
# 从 PluginRegistry 动态查找可探测适配器:插件通过
# registerAdapter("probeable", ...) 注册 ProbeableAdapter
# 此处按 channel_type 索引。未注册时返回"渠道未实现主动探测"
# 以区分"渠道不健康"与"渠道不支持探测"。
adapter = self._findProbeableAdapter(channel_type)
if adapter is None:
return ProbeResult(
channel_type=channel_type,
account_id="",
healthy=False,
latency_ms=int((time.monotonic() - start) * 1000),
message="渠道未实现主动探测,状态基于插件生命周期",
)
try:
outcome = await adapter.probe()
except Exception as e:
latency_ms = int((time.monotonic() - start) * 1000)
await self._logger.warn(
"渠道探测异常",
channel_type=channel_type,
error=str(e),
latency_ms=str(latency_ms),
)
return ProbeResult(
channel_type=channel_type,
account_id="",
healthy=False,
latency_ms=latency_ms,
message=f"探测异常: {e}",
)
latency_ms = outcome.latency_ms if outcome.latency_ms > 0 else int((time.monotonic() - start) * 1000)
await self._logger.info(
"渠道探测完成",
channel_type=channel_type,
healthy=str(outcome.is_available),
latency_ms=str(latency_ms),
)
return ProbeResult(
channel_type=channel_type,
account_id="",
healthy=outcome.is_available,
latency_ms=latency_ms,
message=outcome.reason,
)
def _findProbeableAdapter(self, channel_type: ChannelType) -> ProbeableAdapter | None:
"""从插件注册表查找指定渠道类型的可探测适配器。
遍历 ``PluginRegistry.listPluginAdapters()`` 返回的
``(channel_type, DrivenAdapters)`` 列表,匹配渠道类型后从
``DrivenAdapters.probeable_adapters`` 取首个适配器。
参数:
channel_type: 渠道类型。
返回:
可探测适配器实例,未注册时返回 ``None``。
"""
for ct, adapters in self._registry.listPluginAdapters():
if ct == channel_type and adapters.probeable_adapters:
return adapters.probeable_adapters[0]
return None