本次提交包含多项核心功能迭代与优化: 1. 新增KF客服会话类型,完善聊天类型枚举 2. 新增消息撤回操作类型与身份置信度排序方法 3. 新增控制面结果DTO与敏感字段注册表端口 4. 新增身份合并回滚、重试失败投递目标等业务能力 5. 优化Outbox投递逻辑与熔断器状态判断 6. 修复部分代码冗余与类型不匹配问题 7. 新增数据库索引并发创建与路由绑定清理逻辑 8. 优化会话关闭服务与插件重载并发控制
740 lines
28 KiB
Python
740 lines
28 KiB
Python
"""入站传输管理器。
|
||
|
||
统一管理 PullerWorker 和 StreamWorker 的生命周期,订阅账号上下线事件,
|
||
动态启动/停止账号的传输任务。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import asyncio
|
||
import uuid
|
||
from collections.abc import Awaitable, Callable
|
||
from typing import Any
|
||
|
||
from yuxi.channels.application.transport.base_worker import (
|
||
TransportConfig,
|
||
)
|
||
from yuxi.channels.application.transport.puller_worker import PullerWorker
|
||
from yuxi.channels.application.transport.stream_worker import StreamWorker
|
||
from yuxi.channels.contract.dtos.channel import AccountFilter, AccountStatus, ChannelType
|
||
from yuxi.channels.contract.dtos.health import TransportHealthSnapshot
|
||
from yuxi.channels.contract.dtos.plugin import DomainEvent, EventHandler
|
||
from yuxi.channels.contract.plugin.extension_point import EventSubscription
|
||
from yuxi.channels.contract.plugin.manifest import FailurePolicy
|
||
from yuxi.channels.contract.ports.driven import (
|
||
ConfigPort,
|
||
LoggerPort,
|
||
PersistencePort,
|
||
)
|
||
from yuxi.channels.core.registry import PluginRegistry
|
||
|
||
__all__ = ["TransportManager"]
|
||
|
||
# 管理循环的轮询间隔(秒):仅用于等待取消信号,不参与业务逻辑
|
||
_MANAGER_LOOP_INTERVAL_S: float = 1.0
|
||
|
||
|
||
class TransportManager:
|
||
"""入站传输全局管理器。
|
||
|
||
单例模式管理 PullerWorker 和 StreamWorker,订阅 ChannelAccountOnline/Offline
|
||
事件,根据 transport_mode 与适配器能力动态启动/停止对应账号的传输任务
|
||
(Task 11:pull 仅 Puller、stream 仅 Stream、both 优先 Stream 降级 Puller)。
|
||
|
||
职责:
|
||
- 从 PluginRegistry 动态构建 puller/stream 适配器注册表
|
||
- 创建并管理 PullerWorker、StreamWorker 单例
|
||
- 订阅账号上下线事件,驱动 Worker 启动/停止账号任务
|
||
- 提供全局健康状态查询
|
||
- 优雅关停所有传输任务
|
||
"""
|
||
|
||
def __init__(
|
||
self,
|
||
*,
|
||
plugin_registry: PluginRegistry,
|
||
persistence_port: PersistencePort,
|
||
config_port: ConfigPort,
|
||
event_bus: Any,
|
||
circuit_breaker: Any,
|
||
logger: LoggerPort,
|
||
message_deliverer: Callable[[Any], Awaitable[Any]],
|
||
transport_config: TransportConfig | None = None,
|
||
) -> None:
|
||
"""初始化 TransportManager。
|
||
|
||
参数:
|
||
plugin_registry: 插件注册表,用于动态获取渠道适配器。
|
||
persistence_port: 持久化端口,用于游标持久化等。
|
||
config_port: 配置端口。
|
||
event_bus: 事件总线,用于订阅/发布事件。
|
||
circuit_breaker: 熔断器实例。
|
||
logger: 日志端口。
|
||
message_deliverer: 入站消息投递回调。
|
||
transport_config: 传输配置,为 None 时使用默认配置。
|
||
"""
|
||
self._plugin_registry = plugin_registry
|
||
self._persistence_port = persistence_port
|
||
self._config_port = config_port
|
||
self._event_bus = event_bus
|
||
self._circuit_breaker = circuit_breaker
|
||
self._logger = logger
|
||
self._message_deliverer = message_deliverer
|
||
self._config = transport_config or TransportConfig()
|
||
|
||
self._puller_registry: dict[ChannelType, Any] = {}
|
||
self._stream_connector_registry: dict[ChannelType, Any] = {}
|
||
self._puller_worker: PullerWorker | None = None
|
||
self._stream_worker: StreamWorker | None = None
|
||
self._manager_task: asyncio.Task[None] | None = None
|
||
self._running: bool = False
|
||
self._plugin_id = "transport-manager"
|
||
# H-14:恢复扫描与配置热更新互斥锁(进程内,防同进程竞态)
|
||
self._restore_lock = asyncio.Lock()
|
||
|
||
async def start(self) -> None:
|
||
"""启动传输管理器。
|
||
|
||
- 构建 puller_registry 和 stream_connector_registry
|
||
- 创建 PullerWorker 和 StreamWorker 实例
|
||
- 启动 Worker
|
||
- 订阅账号上下线事件
|
||
- 启动管理循环任务
|
||
"""
|
||
if self._running:
|
||
return
|
||
|
||
trace_id = str(uuid.uuid4())
|
||
|
||
puller_registry: dict[ChannelType, Any] = {}
|
||
stream_connector_registry: dict[ChannelType, Any] = {}
|
||
|
||
for channel_type, plugin_da in self._plugin_registry.listPluginAdapters():
|
||
if plugin_da.puller_adapters:
|
||
puller_registry[channel_type] = plugin_da.puller_adapters[0]
|
||
if plugin_da.stream_connector_adapters:
|
||
stream_connector_registry[channel_type] = plugin_da.stream_connector_adapters[0]
|
||
|
||
self._puller_registry = puller_registry
|
||
self._stream_connector_registry = stream_connector_registry
|
||
|
||
self._puller_worker = PullerWorker(
|
||
message_deliverer=self._message_deliverer,
|
||
logger=self._logger,
|
||
config_port=self._config_port,
|
||
event_publisher=self._event_bus,
|
||
circuit_breaker=self._circuit_breaker,
|
||
persistence_port=self._persistence_port,
|
||
config=self._config,
|
||
)
|
||
|
||
self._stream_worker = StreamWorker(
|
||
message_deliverer=self._message_deliverer,
|
||
logger=self._logger,
|
||
config_port=self._config_port,
|
||
event_publisher=self._event_bus,
|
||
circuit_breaker=self._circuit_breaker,
|
||
config=self._config,
|
||
)
|
||
|
||
await self._puller_worker.start()
|
||
await self._stream_worker.start()
|
||
|
||
self._registerEventHandlers()
|
||
|
||
self._running = True
|
||
self._manager_task = asyncio.create_task(
|
||
self._manager_loop(),
|
||
name="transport-manager-loop",
|
||
)
|
||
|
||
await self._logger.info(
|
||
"transport manager started",
|
||
trace_id=trace_id,
|
||
puller_channels=[ct for ct in puller_registry.keys()],
|
||
stream_channels=[ct for ct in stream_connector_registry.keys()],
|
||
)
|
||
|
||
# 重启恢复:扫描 DB 中 ACTIVE 状态账号,为每个账号启动传输任务。
|
||
# 系统重启后不会收到 ChannelAccountOnline 事件,必须主动恢复,
|
||
# 否则已上线账号的传输任务(SSE/短轮询)不会启动。
|
||
await self._restoreOnlineAccounts(trace_id)
|
||
|
||
async def stop(self, timeout: float = 5.0) -> None:
|
||
"""停止传输管理器。
|
||
|
||
- 取消事件订阅
|
||
- 取消管理循环任务
|
||
- 停止所有 Worker
|
||
- 等待所有任务完成
|
||
|
||
参数:
|
||
timeout: 等待 Worker 停止的超时时间(秒)。
|
||
"""
|
||
if not self._running:
|
||
return
|
||
|
||
trace_id = str(uuid.uuid4())
|
||
self._running = False
|
||
|
||
self._unregisterEventHandlers()
|
||
|
||
if self._manager_task is not None and not self._manager_task.done():
|
||
self._manager_task.cancel()
|
||
try:
|
||
await self._manager_task
|
||
except asyncio.CancelledError:
|
||
pass
|
||
self._manager_task = None
|
||
|
||
if self._puller_worker is not None:
|
||
await self._puller_worker.stop(timeout)
|
||
self._puller_worker = None
|
||
|
||
if self._stream_worker is not None:
|
||
await self._stream_worker.stop(timeout)
|
||
self._stream_worker = None
|
||
|
||
await self._logger.info(
|
||
"transport manager stopped",
|
||
trace_id=trace_id,
|
||
)
|
||
|
||
def getHealth(self) -> TransportHealthSnapshot:
|
||
"""返回传输引擎健康状态(同步,向后兼容)。
|
||
|
||
返回:
|
||
``TransportHealthSnapshot``,包含 running 状态和各 Worker 健康
|
||
子状态(``WorkerHealthSnapshot``)。
|
||
"""
|
||
return TransportHealthSnapshot(
|
||
running=self._running,
|
||
puller=self._puller_worker.getHealth() if self._puller_worker else None,
|
||
stream=self._stream_worker.getHealth() if self._stream_worker else None,
|
||
)
|
||
|
||
async def getTransportHealth(self) -> TransportHealthSnapshot:
|
||
"""返回传输引擎健康状态(实现 TransportHealthPort,FR-18)。
|
||
|
||
供 ``HealthAggregator`` 通过 ``TransportHealthPort`` 聚合,暴露每个
|
||
账号的运行状态(running/backoff/stopped/error)。实现幂等、无副作用,
|
||
不抛异常阻塞调用方。
|
||
|
||
返回:
|
||
``TransportHealthSnapshot``,结构见 ``TransportHealthPort`` 协议
|
||
文档。异常时降级返回 ``puller`` / ``stream`` 为 ``None`` 的快照。
|
||
"""
|
||
try:
|
||
return self.getHealth()
|
||
except Exception as exc:
|
||
# 健康检查不得抛异常阻塞调用方(与 HealthAggregator 超时降级策略一致)
|
||
await self._logger.warn(
|
||
"transport health query failed, returning empty state",
|
||
error=str(exc),
|
||
)
|
||
return TransportHealthSnapshot(
|
||
running=self._running,
|
||
puller=None,
|
||
stream=None,
|
||
)
|
||
|
||
async def _manager_loop(self) -> None:
|
||
"""管理循环,等待取消信号。"""
|
||
try:
|
||
while self._running:
|
||
await asyncio.sleep(_MANAGER_LOOP_INTERVAL_S)
|
||
except asyncio.CancelledError:
|
||
pass
|
||
|
||
def _registerEventHandlers(self) -> None:
|
||
"""注册事件处理器。"""
|
||
self._event_bus.register(
|
||
self._plugin_id,
|
||
EventSubscription(
|
||
event_type="ChannelAccountOnline",
|
||
handler=_AccountEventHandler(self._on_account_online),
|
||
priority=100,
|
||
failure_policy=FailurePolicy.DEGRADE,
|
||
),
|
||
)
|
||
self._event_bus.register(
|
||
self._plugin_id,
|
||
EventSubscription(
|
||
event_type="ChannelAccountOffline",
|
||
handler=_AccountEventHandler(self._on_account_offline),
|
||
priority=100,
|
||
failure_policy=FailurePolicy.DEGRADE,
|
||
),
|
||
)
|
||
self._event_bus.register(
|
||
self._plugin_id,
|
||
EventSubscription(
|
||
event_type="ChannelDegraded",
|
||
handler=_AccountEventHandler(self._on_channel_degraded),
|
||
priority=100,
|
||
failure_policy=FailurePolicy.DEGRADE,
|
||
),
|
||
)
|
||
self._event_bus.register(
|
||
self._plugin_id,
|
||
EventSubscription(
|
||
event_type="ChannelRecovered",
|
||
handler=_AccountEventHandler(self._on_channel_recovered),
|
||
priority=100,
|
||
failure_policy=FailurePolicy.DEGRADE,
|
||
),
|
||
)
|
||
self._event_bus.register(
|
||
self._plugin_id,
|
||
EventSubscription(
|
||
event_type="TransportErrorOccurred",
|
||
handler=_AccountEventHandler(self._on_transport_error),
|
||
priority=100,
|
||
failure_policy=FailurePolicy.DEGRADE,
|
||
),
|
||
)
|
||
# 配置热更新订阅(FR-04):transport.* 配置变更时应用 hot/restart 策略
|
||
for config_event in ("ConfigChanged", "ConfigRollback"):
|
||
self._event_bus.register(
|
||
self._plugin_id,
|
||
EventSubscription(
|
||
event_type=config_event,
|
||
handler=_AccountEventHandler(self._on_config_changed),
|
||
priority=100,
|
||
failure_policy=FailurePolicy.DEGRADE,
|
||
),
|
||
)
|
||
|
||
def _unregisterEventHandlers(self) -> None:
|
||
"""取消事件订阅。"""
|
||
self._event_bus.unregister(self._plugin_id)
|
||
|
||
async def _on_account_online(self, event: DomainEvent) -> None:
|
||
"""处理账号上线事件。
|
||
|
||
从事件 payload 获取 channel_type 和 account_id,根据 transport_mode
|
||
与适配器能力选择启动 Puller 或 Stream 任务(Task 11.1 + 11.2):
|
||
- ``pull``: 仅启动 Puller
|
||
- ``stream``: 仅启动 Stream
|
||
- ``both``: 优先 Stream,Puller 作为降级(Stream 适配器不可用时启动)
|
||
|
||
参数:
|
||
event: ChannelAccountOnline 事件。
|
||
"""
|
||
trace_id = event.trace_id or str(uuid.uuid4())
|
||
channel_type_raw = event.payload.get("channel_type")
|
||
account_id = event.payload.get("account_id")
|
||
if channel_type_raw is None or account_id is None:
|
||
await self._logger.warn(
|
||
"account online event missing required fields, skip",
|
||
trace_id=trace_id,
|
||
channel_type=channel_type_raw,
|
||
account_id=account_id,
|
||
)
|
||
return
|
||
channel_type = ChannelType(channel_type_raw)
|
||
|
||
await self._logger.info(
|
||
"received account online event",
|
||
trace_id=trace_id,
|
||
channel_type=channel_type,
|
||
account_id=account_id,
|
||
)
|
||
await self._startTransportForAccount(channel_type, account_id, trace_id, source="event")
|
||
|
||
async def _startTransportForAccount(
|
||
self,
|
||
channel_type: ChannelType,
|
||
account_id: str,
|
||
trace_id: str,
|
||
source: str = "event",
|
||
) -> None:
|
||
"""为单个账号启动传输任务(事件驱动与重启恢复共用)。
|
||
|
||
根据 transport_mode 与适配器能力选择启动 Puller 或 Stream 任务:
|
||
- ``pull``: 仅启动 Puller
|
||
- ``stream``: 仅启动 Stream
|
||
- ``both``: 优先 Stream,Puller 作为降级(Stream 适配器不可用时启动)
|
||
|
||
参数:
|
||
channel_type: 渠道类型。
|
||
account_id: 账号 ID。
|
||
trace_id: 链路追踪 ID。
|
||
source: 启动来源(``event`` 事件驱动 / ``restore`` 重启恢复),
|
||
仅用于日志区分。
|
||
"""
|
||
transport_mode = await self._resolveTransportMode(channel_type, account_id)
|
||
puller_adapter = self._puller_registry.get(channel_type)
|
||
stream_adapter = self._stream_connector_registry.get(channel_type)
|
||
|
||
await self._logger.info(
|
||
"starting transport for account",
|
||
trace_id=trace_id,
|
||
channel_type=channel_type,
|
||
account_id=account_id,
|
||
transport_mode=transport_mode,
|
||
has_puller_adapter=puller_adapter is not None,
|
||
has_stream_adapter=stream_adapter is not None,
|
||
source=source,
|
||
)
|
||
|
||
started = False
|
||
if transport_mode == "pull":
|
||
if puller_adapter is not None and self._puller_worker is not None:
|
||
await self._puller_worker.start_account(channel_type, account_id, puller_adapter)
|
||
started = True
|
||
elif transport_mode == "stream":
|
||
if stream_adapter is not None and self._stream_worker is not None:
|
||
await self._stream_worker.start_account(channel_type, account_id, stream_adapter)
|
||
started = True
|
||
else:
|
||
# Task 11.2: both 模式优先 Stream,Puller 作为降级。
|
||
# Stream 适配器可用时仅启动 Stream(Stream 健康时不 poll);
|
||
# Stream 适配器不可用时降级启动 Puller。
|
||
if stream_adapter is not None and self._stream_worker is not None:
|
||
await self._stream_worker.start_account(channel_type, account_id, stream_adapter)
|
||
started = True
|
||
elif puller_adapter is not None and self._puller_worker is not None:
|
||
await self._puller_worker.start_account(channel_type, account_id, puller_adapter)
|
||
started = True
|
||
|
||
# 传输任务成功启动后持久化插件运行态(FR-32)。诊断字段,写入失败
|
||
# 仅告警不中止(与 last_health_check_at 一致的 best-effort 语义)。
|
||
if started:
|
||
await self._touchPluginStatus(channel_type, account_id, "running", trace_id)
|
||
|
||
async def _restoreOnlineAccounts(self, trace_id: str) -> None:
|
||
"""重启恢复:扫描 DB 中 ACTIVE 状态账号,启动传输任务。
|
||
|
||
系统重启后 TransportManager 不会收到 ChannelAccountOnline 事件,
|
||
本方法在 ``start()`` 中被调用,为所有已注册渠道的 ACTIVE 状态账号
|
||
启动传输任务(SSE 或短轮询),实现重启后自动恢复。
|
||
|
||
错误隔离:单个账号查询/启动失败不阻塞其他账号,仅记录 warn 日志。
|
||
仅恢复注册表中有 puller 或 stream 适配器的渠道(未注册适配器的
|
||
渠道跳过)。
|
||
|
||
参数:
|
||
trace_id: 启动链路追踪 ID。
|
||
"""
|
||
# H-14:持有恢复锁,与 _on_config_changed 的 reloadConfig 互斥,
|
||
# 防止恢复扫描与配置热更新竞态。异常时由 async with 保证锁释放。
|
||
async with self._restore_lock:
|
||
restored = 0
|
||
failed = 0
|
||
# 合并 puller 和 stream 注册表的渠道类型,避免遗漏
|
||
channel_types = set(self._puller_registry.keys()) | set(self._stream_connector_registry.keys())
|
||
|
||
for channel_type in channel_types:
|
||
try:
|
||
accounts = await self._persistence_port.findAccountsByFilter(
|
||
AccountFilter(channel_type=channel_type, status=AccountStatus.ACTIVE)
|
||
)
|
||
except Exception as exc:
|
||
await self._logger.warn(
|
||
"transport restore: failed to query accounts for channel",
|
||
trace_id=trace_id,
|
||
channel_type=channel_type,
|
||
error=str(exc),
|
||
)
|
||
continue
|
||
|
||
for account in accounts:
|
||
try:
|
||
await self._startTransportForAccount(
|
||
channel_type=channel_type,
|
||
account_id=account.account_id,
|
||
trace_id=trace_id,
|
||
source="restore",
|
||
)
|
||
restored += 1
|
||
except Exception as exc:
|
||
failed += 1
|
||
await self._logger.warn(
|
||
"transport restore: failed to start account",
|
||
trace_id=trace_id,
|
||
channel_type=channel_type,
|
||
account_id=account.account_id,
|
||
error=str(exc),
|
||
)
|
||
|
||
await self._logger.info(
|
||
"transport restore completed",
|
||
trace_id=trace_id,
|
||
restored_accounts=restored,
|
||
failed_accounts=failed,
|
||
channel_count=len(channel_types),
|
||
)
|
||
|
||
async def _resolveTransportMode(
|
||
self,
|
||
channel_type: ChannelType,
|
||
account_id: str,
|
||
) -> str:
|
||
"""解析账号的传输模式(Task 11.1)。
|
||
|
||
优先使用账号级 ``transport_mode``(``ChannelAccount.transport_mode``),
|
||
账号不存在或读取失败时回退到 manifest 声明的渠道级 ``transport_mode``,
|
||
均不可用时回退到 ``both``。
|
||
|
||
参数:
|
||
channel_type: 渠道类型。
|
||
account_id: 渠道账号ID。
|
||
|
||
返回:
|
||
传输模式字符串(``pull`` / ``stream`` / ``both``)。
|
||
"""
|
||
try:
|
||
account = await self._persistence_port.getChannelAccount(channel_type, account_id)
|
||
except Exception as exc:
|
||
await self._logger.warn(
|
||
"failed to get channel account for transport_mode resolution, fallback to manifest default",
|
||
channel_type=channel_type,
|
||
account_id=account_id,
|
||
error=str(exc),
|
||
)
|
||
account = None
|
||
|
||
if account is not None:
|
||
return account.transport_mode
|
||
|
||
plugin = self._plugin_registry.getPluginByChannelType(channel_type)
|
||
if plugin is not None:
|
||
return plugin.manifest.transport_mode
|
||
return "both"
|
||
|
||
async def _on_account_offline(self, event: DomainEvent) -> None:
|
||
"""处理账号下线事件。
|
||
|
||
停止对应账号的所有传输任务。
|
||
|
||
参数:
|
||
event: ChannelAccountOffline 事件。
|
||
"""
|
||
trace_id = event.trace_id or str(uuid.uuid4())
|
||
channel_type_raw = event.payload.get("channel_type")
|
||
account_id = event.payload.get("account_id")
|
||
if channel_type_raw is None or account_id is None:
|
||
await self._logger.warn(
|
||
"account offline event missing required fields, skip",
|
||
trace_id=trace_id,
|
||
channel_type=channel_type_raw,
|
||
account_id=account_id,
|
||
)
|
||
return
|
||
channel_type = ChannelType(channel_type_raw)
|
||
reason = event.payload.get("reason", "offline")
|
||
|
||
await self._logger.info(
|
||
"received account offline event, stopping transport",
|
||
trace_id=trace_id,
|
||
channel_type=channel_type,
|
||
account_id=account_id,
|
||
reason=reason,
|
||
)
|
||
|
||
if self._puller_worker is not None:
|
||
await self._puller_worker.stop_account(channel_type, account_id, reason)
|
||
|
||
if self._stream_worker is not None:
|
||
await self._stream_worker.stop_account(channel_type, account_id, reason)
|
||
|
||
# 传输任务停止后持久化插件运行态(FR-32)。
|
||
await self._touchPluginStatus(channel_type, account_id, "stopped", trace_id)
|
||
|
||
async def _on_channel_degraded(self, event: DomainEvent) -> None:
|
||
"""处理渠道降级事件。
|
||
|
||
P0 阶段留空,P1 阶段实现降级逻辑(如暂停非关键任务、调整轮询间隔等)。
|
||
|
||
参数:
|
||
event: ChannelDegraded 事件。
|
||
"""
|
||
pass
|
||
|
||
async def _on_channel_recovered(self, event: DomainEvent) -> None:
|
||
"""处理渠道恢复事件。
|
||
|
||
P0 阶段留空,P1 阶段实现恢复逻辑。
|
||
|
||
参数:
|
||
event: ChannelRecovered 事件。
|
||
"""
|
||
pass
|
||
|
||
async def _on_transport_error(self, event: DomainEvent) -> None:
|
||
"""处理传输错误事件。
|
||
|
||
记录日志并将账户插件运行态置为 ``error``(FR-32)。P1 阶段可用于
|
||
监控告警。
|
||
|
||
参数:
|
||
event: TransportErrorOccurred 事件。
|
||
"""
|
||
trace_id = event.trace_id or str(uuid.uuid4())
|
||
channel_type_raw = event.payload.get("channel_type")
|
||
account_id = event.payload.get("account_id")
|
||
await self._logger.warn(
|
||
"transport error occurred",
|
||
trace_id=trace_id,
|
||
channel_type=channel_type_raw,
|
||
account_id=account_id,
|
||
error_category=event.payload.get("error_category"),
|
||
error_code=event.payload.get("error_code"),
|
||
)
|
||
# 持久化插件运行态为 error(FR-32)。仅当事件携带账户信息时写入。
|
||
if channel_type_raw is not None and account_id is not None:
|
||
await self._touchPluginStatus(ChannelType(channel_type_raw), account_id, "error", trace_id)
|
||
|
||
async def _touchPluginStatus(
|
||
self,
|
||
channel_type: ChannelType,
|
||
account_id: str,
|
||
plugin_status: str,
|
||
trace_id: str,
|
||
) -> None:
|
||
"""更新账户插件运行态(FR-32)。
|
||
|
||
传输任务 start/stop/error 后调用 ``PersistencePort.updatePluginStatus``
|
||
持久化插件运行态(running / stopped / error)。诊断字段,best-effort
|
||
写入:失败时仅记录告警,不中止传输主流程(与 ``last_health_check_at``
|
||
一致)。
|
||
"""
|
||
try:
|
||
await self._persistence_port.updatePluginStatus(channel_type, account_id, plugin_status)
|
||
except Exception as exc:
|
||
await self._logger.warn(
|
||
"failed to update plugin_status",
|
||
trace_id=trace_id,
|
||
channel_type=channel_type,
|
||
account_id=account_id,
|
||
plugin_status=plugin_status,
|
||
error=str(exc),
|
||
)
|
||
|
||
async def _on_config_changed(self, event: DomainEvent) -> None:
|
||
"""处理配置变更事件(FR-04 配置热更新)。
|
||
|
||
监听 ``transport.*`` 前缀的全局配置变更,按 hot/restart 模式应用:
|
||
- hot 模式(``stall_timeout_ms`` / ``backoff_schedule`` /
|
||
``backoff_jitter``):重新加载 TransportConfig 并更新 Worker 配置,
|
||
当前轮次结束后生效。
|
||
- restart 模式(``max_restart_attempts`` /
|
||
``graceful_shutdown_timeout_s``):记录警告日志,需手动重启
|
||
TransportManager 生效(P1 阶段不实现自动重启,避免账号连接中断)。
|
||
|
||
参数:
|
||
event: ConfigChanged / ConfigRollback 事件,payload 包含 key。
|
||
"""
|
||
key = event.payload.get("key", "")
|
||
if not key.startswith("transport."):
|
||
return
|
||
# 仅处理全局配置(target 为 None 或空),忽略账户级配置
|
||
target = event.payload.get("target")
|
||
if target:
|
||
return
|
||
|
||
trace_id = event.trace_id or str(uuid.uuid4())
|
||
if key in _RESTART_REQUIRED_CONFIG_KEYS:
|
||
await self._logger.warn(
|
||
"transport config change requires restart to take effect",
|
||
trace_id=trace_id,
|
||
key=key,
|
||
event_type=event.event_type,
|
||
)
|
||
return
|
||
|
||
# 仅对 hot-reloadable 键触发重新加载,忽略未知 transport.* 键
|
||
if key not in _RELOADABLE_CONFIG_KEYS:
|
||
return
|
||
|
||
# hot 模式:重新加载配置并更新 Worker
|
||
await self._logger.info(
|
||
"transport config hot reload triggered",
|
||
trace_id=trace_id,
|
||
key=key,
|
||
event_type=event.event_type,
|
||
)
|
||
# H-14:与 _restoreOnlineAccounts 互斥,等待恢复扫描完成后再重载配置
|
||
async with self._restore_lock:
|
||
await self.reloadConfig()
|
||
|
||
async def reloadConfig(self) -> None:
|
||
"""重新加载传输配置并更新 Worker(FR-04 hot 模式)。
|
||
|
||
从 ``ConfigPort`` 读取最新 ``transport.*`` 配置,更新
|
||
``TransportManager._config`` 及两个 Worker 的 ``_config``。
|
||
Worker 在下一轮循环(退避等待、轮询间隔、心跳间隔)时自动应用新配置。
|
||
|
||
幂等:多次调用安全,仅更新配置不重启任务。
|
||
"""
|
||
trace_id = str(uuid.uuid4())
|
||
try:
|
||
new_config = await self._loadTransportConfig()
|
||
except Exception as exc:
|
||
await self._logger.error(
|
||
"transport config reload failed, keeping old config",
|
||
trace_id=trace_id,
|
||
error=str(exc),
|
||
)
|
||
return
|
||
|
||
self._config = new_config
|
||
if self._puller_worker is not None:
|
||
self._puller_worker._config = new_config
|
||
if self._stream_worker is not None:
|
||
self._stream_worker._config = new_config
|
||
|
||
await self._logger.info(
|
||
"transport config reloaded",
|
||
trace_id=trace_id,
|
||
stall_timeout_ms=new_config.stall_timeout_ms,
|
||
backoff_schedule=list(new_config.backoff_schedule),
|
||
backoff_jitter=new_config.backoff_jitter,
|
||
)
|
||
|
||
async def _loadTransportConfig(self) -> TransportConfig:
|
||
"""从 ConfigPort 加载 TransportConfig。
|
||
|
||
复用 BaseTransportWorker._loadConfig 的读取逻辑,集中配置加载
|
||
避免逻辑重复。
|
||
"""
|
||
# 委托任一 Worker 的 _loadConfig(两者配置相同),无 Worker 时
|
||
# 直接构造默认配置
|
||
if self._puller_worker is not None:
|
||
return await self._puller_worker._loadConfig()
|
||
if self._stream_worker is not None:
|
||
return await self._stream_worker._loadConfig()
|
||
return TransportConfig()
|
||
|
||
|
||
# transport.* 配置键的热更新模式分类(FR-04)
|
||
_RELOADABLE_CONFIG_KEYS: frozenset[str] = frozenset(
|
||
{
|
||
"transport.stall_timeout_ms",
|
||
"transport.backoff_schedule",
|
||
"transport.backoff_jitter",
|
||
"transport.stream_reconnect_backoff_ms",
|
||
"transport.stream_reconnect_max_backoff_ms",
|
||
}
|
||
)
|
||
_RESTART_REQUIRED_CONFIG_KEYS: frozenset[str] = frozenset(
|
||
{
|
||
"transport.max_restart_attempts",
|
||
"transport.graceful_shutdown_timeout_s",
|
||
}
|
||
)
|
||
|
||
|
||
class _AccountEventHandler(EventHandler):
|
||
"""账号事件处理器适配器。
|
||
|
||
将 EventHandler 接口适配到 TransportManager 的异步处理方法。
|
||
"""
|
||
|
||
def __init__(self, handler: Callable[[DomainEvent], Awaitable[None]]) -> None:
|
||
self._handler = handler
|
||
|
||
async def handle(self, event: DomainEvent) -> None:
|
||
await self._handler(event)
|