ForcePilot/backend/package/yuxi/channels/infrastructure/host_bootstrap.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

1020 lines
47 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.

"""宿主启动编排器。
本模块是编排层的启动编排入口,按顺序装配渠道网关的所有
组件。关键步骤失败必须终止启动并报告原因INV-I5
插件加载失败不得终止启动,必须触发优雅降级。
启动顺序不可颠倒:
1. 加载渠道配置INF-009配置缺失或类型不符必须抛异常
2. ``ensure_channel_schema()`` 初始化渠道 schemaINV-I5失败必须抛异常
3. 初始化注册中心 + EventBus + 领域服务(注册到 DI 容器)
4. 加载并初始化被驱动适配器INF-010应用级适配器连通性检查
5. 加载并初始化渠道插件(按依赖拓扑,失败记录警告继续)
6. 装配管道INF-011注入锚点格式校验
7. 注册驱动适配器(开始接受流量)
8. 标记宿主为 readyINF-012与步骤 7 紧邻,对齐规范 §15.1
后台任务启动置于 ready 之后)
9. 启动 Outbox 恢复扫描
10. 启动配对过期扫描FR-33
11. 启动审计日志保留期清理FR-34
12. 启动失败插件自动重载FR-36
13. 启动传输引擎管理器入站Worker
"""
from __future__ import annotations
import asyncio
from collections.abc import Awaitable, Callable, Coroutine, Sequence
from typing import Any, Protocol
from yuxi.channels.application.extension.config_source_registry import (
ConfigSourceRegistry,
)
from yuxi.channels.application.extension.event_bus import EventBus
from yuxi.channels.application.extension.event_subscription_registry import (
EventSubscriptionRegistry,
)
from yuxi.channels.application.extension.stage_slot_registry import StageSlotRegistry
from yuxi.channels.application.health.health_aggregator import HealthAggregator
from yuxi.channels.application.lifecycle.plugin_dependency_resolver import (
PluginDependencyResolver,
)
from yuxi.channels.application.lifecycle.plugin_lifecycle_manager import (
PluginLifecycleManager,
)
from yuxi.channels.application.outbox.audit_log_retention_scanner import (
AuditLogRetentionScanner,
)
from yuxi.channels.application.outbox.outbox_recovery_scanner import (
OutboxRecoveryScanner,
)
from yuxi.channels.application.outbox.pairing_expiration_scanner import (
PairingExpirationScanner,
)
from yuxi.channels.application.pipeline.stage_slot_injector import StageSlotInjector
from yuxi.channels.application.transport import TransportManager
from yuxi.channels.contract.dtos.lifecycle import LifecycleResult
from yuxi.channels.contract.errors import (
ConfigValidationError,
DependencyError,
Error,
InternalError,
)
from yuxi.channels.contract.errors.server import OperationTimeoutError
from yuxi.channels.contract.plugin.manifest import ChannelManifest
from yuxi.channels.contract.ports.driven.config_port import ConfigPort
from yuxi.channels.contract.ports.driven.event_publisher_port import EventPublisherPort
from yuxi.channels.contract.ports.driven.logger_port import LoggerPort
from yuxi.channels.contract.ports.driven.pingable_port import PingablePort
from yuxi.channels.core.registry.command_registry import CommandRegistryTable
from yuxi.channels.core.registry.plugin_registry import PluginRegistry
from yuxi.channels.core.service.common_command_adapter import registerCommonCommands
from yuxi.channels.infrastructure.dependency_injection import (
DependencyInjectionContainer,
)
__all__ = [
"HostBootstrap",
]
class _EnsureSchemaPort(Protocol):
"""渠道 schema 初始化端口(局部 Protocol
``PostgresManager`` 满足此 Protocol提供 ``ensure_channel_schema`` 方法)。
使用局部 Protocol 避免在 contract 层新增端口;``PostgresManager`` 位于
storage 层,不属于 channels 四层架构,直接依赖会破坏分层边界。
"""
async def ensure_channel_schema(self) -> None:
"""确保渠道网关 schema 就绪。"""
...
class HostBootstrap:
"""宿主启动编排器。
按顺序装配渠道网关所有组件INV-I5
启动顺序不可颠倒:
1. 加载配置
2. ``ensure_channel_schema()`` 初始化渠道 schema
3. 初始化注册中心 + EventBus + 领域服务(注册到 DI 容器)
4. 加载并初始化被驱动适配器
5. 加载并初始化渠道插件(按依赖拓扑)
6. 装配管道(注入插件阶段)
7. 注册驱动适配器(开始接受流量)
8. 标记宿主为 readyINF-012与步骤 7 紧邻,对齐规范 §15.1
9. 启动 Outbox 恢复扫描
10. 启动配对过期扫描FR-33
11. 启动审计日志保留期清理FR-34
12. 启动失败插件自动重载FR-36
13. 启动传输引擎管理器
关键约束:
- 关键步骤配置加载、Schema 初始化、核心初始化、被驱动适配器初始化)
失败必须终止启动并报告原因INV-I5
- 插件加载失败不得终止启动,记录警告并继续,触发优雅降级。
- 每步有超时保护,关键步骤超时抛 ``TimeoutError`` 终止启动。
- 异常处理保留原始错误上下文INV-7使用 ``raise ... from e``。
"""
#: Schema 初始化超时(秒)
_SCHEMA_INIT_TIMEOUT: float = 60.0
#: 被驱动适配器初始化超时(秒)
_DRIVEN_ADAPTER_INIT_TIMEOUT: float = 30.0
#: 配置加载超时INF-009
_CONFIG_LOAD_TIMEOUT: float = 30.0
#: 插件发现超时(秒)
_PLUGIN_DISCOVER_TIMEOUT: float = 60.0
#: 单个插件加载超时(秒)
# 注:必须大于内部 onInit 60s + onStart 60s = 120s 之和,否则外部超时
# 会先于内部 hook 超时触发,取消 ``_handle_failure`` 导致插件状态
# 不一致P1-5
_PLUGIN_LOAD_TIMEOUT: float = 180.0
#: 驱动适配器注册超时(秒)
_DRIVING_ADAPTER_REGISTER_TIMEOUT: float = 30.0
#: 后台任务启动探测窗口create_task 后短暂等待以捕获启动即失败
_TASK_STARTUP_PROBE_TIMEOUT: float = 0.5
def __init__(
self,
ensure_schema: _EnsureSchemaPort,
di_container: DependencyInjectionContainer,
plugin_dir: str,
event_bus: EventBus,
stage_slot_registry: StageSlotRegistry,
event_subscription_registry: EventSubscriptionRegistry,
config_source_registry: ConfigSourceRegistry,
plugin_lifecycle_manager: PluginLifecycleManager,
plugin_dependency_resolver: PluginDependencyResolver,
stage_slot_injector: StageSlotInjector,
outbox_recovery_scanner: OutboxRecoveryScanner,
pairing_expiration_scanner: PairingExpirationScanner,
audit_log_retention_scanner: AuditLogRetentionScanner,
health_aggregator: HealthAggregator,
transport_manager: TransportManager,
logger: LoggerPort,
config_port: ConfigPort,
driving_adapter_registrar: Callable[[], Awaitable[None]] | None = None,
driving_adapter_unregistrar: Callable[[], Awaitable[None]] | None = None,
app_driven_adapters: Sequence[Any] | None = None,
) -> None:
"""初始化宿主启动编排器。
Args:
ensure_schema: 渠道 schema 初始化端口(``PostgresManager`` 满足
此 Protocol提供 ``ensure_channel_schema`` 方法)。
di_container: 依赖注入容器,用于注册应用级单例。
plugin_dir: 插件目录路径,供 ``discover`` 扫描。
event_bus: 事件总线,注册到 DI 容器供领域事件分发。
stage_slot_registry: 阶段槽位注册中心。
event_subscription_registry: 事件订阅注册中心。
config_source_registry: 配置源注册中心。
plugin_lifecycle_manager: 插件生命周期管理器,编排 discover/load。
注册到 DI 容器供 ``PluginLifecycleService`` 委托调用。
plugin_dependency_resolver: 插件依赖解析器Kahn 拓扑排序。
stage_slot_injector: 管道阶段插槽注入器。
outbox_recovery_scanner: Outbox 恢复扫描器。
pairing_expiration_scanner: 配对过期扫描器FR-33
audit_log_retention_scanner: 审计日志保留期清理扫描器FR-34
health_aggregator: 健康状态聚合器,注册到 DI 容器供
``HealthCheckService`` 委托调用。
logger: 日志端口,记录启动过程。
config_port: 配置被驱动端口INF-009启动期 ``_loadConfig``
调用 ``getSchema`` 验证配置端口可用与 schema 已加载,配置
缺失或不可用必须终止启动INV-I5 / §13.1)。
driving_adapter_registrar: 驱动适配器注册回调(可选),由调用方
提供,在步骤 7 调用以开始接受流量。为 ``None`` 时步骤 7 记录
告警并跳过(驱动适配器尚未接线)。
driving_adapter_unregistrar: 驱动适配器注销回调(可选),与
``driving_adapter_registrar`` 对称,由调用方提供。当启动失败
回滚且 ``_driving_registered`` 为 ``True`` 时调用,以注销已
注册的驱动适配器并停止接收新流量。为 ``None`` 时回滚跳过
驱动适配器注销步骤(仅当调用方未提供注销能力时使用)。
app_driven_adapters: 应用级被驱动适配器列表可选INF-010
包含 ``RedisCacheAdapter`` / ``ChannelPersistenceAdapter`` /
``ARQQueueAdapter`` / ``RedisConfigAdapter`` 等跨请求共享的
应用级适配器实例。``_initDrivenAdapters`` 步骤对每个适配器
执行连通性检查(``ping()`` 或等效),失败抛 ``DependencyError``
终止启动。为 ``None`` 或空时跳过连通性检查(向后兼容)。
"""
self._ensure_schema = ensure_schema
self._di = di_container
self._plugin_dir = plugin_dir
self._event_bus = event_bus
self._stage_slot_registry = stage_slot_registry
self._event_subscription_registry = event_subscription_registry
self._config_source_registry = config_source_registry
self._plugin_lifecycle = plugin_lifecycle_manager
self._resolver = plugin_dependency_resolver
self._stage_slot_injector = stage_slot_injector
self._outbox_scanner = outbox_recovery_scanner
self._pairing_scanner = pairing_expiration_scanner
self._audit_log_retention_scanner = audit_log_retention_scanner
self._health_aggregator = health_aggregator
self._transport_manager = transport_manager
self._logger = logger
self._config_port = config_port
self._driving_adapter_registrar = driving_adapter_registrar
self._driving_adapter_unregistrar = driving_adapter_unregistrar
self._app_driven_adapters: tuple[Any, ...] = tuple(app_driven_adapters) if app_driven_adapters else ()
self._ready = False
# 启动进行中标志:防止 ``bootstrap`` 并发或重复调用导致状态混乱
# 如后台任务引用被覆盖产生孤儿任务、DI 容器重复注册等)。
self._bootstrapping = False
self._outbox_scan_task: asyncio.Task[None] | None = None
self._pairing_scan_task: asyncio.Task[None] | None = None
self._audit_log_retention_scan_task: asyncio.Task[None] | None = None
self._plugin_reload_task: asyncio.Task[None] | None = None
self._transport_manager_task: asyncio.Task[None] | None = None
# 启动步骤完成跟踪标志INF-013用于 ``_rollback`` 判断哪些步骤
# 已成功执行需逆序回滚。仅在对应步骤成功路径末尾置 ``True``,失败
# 路径保持 ``False`` 以跳过该步回滚。
self._driving_registered: bool = False
self._plugins_loaded: bool = False
@property
def isReady(self) -> bool:
"""宿主是否已就绪。"""
return self._ready
@property
def outboxScanTask(self) -> asyncio.Task[None] | None:
"""Outbox 恢复扫描后台任务(供 ``HostShutdown`` 取消)。"""
return self._outbox_scan_task
@property
def pairingScanTask(self) -> asyncio.Task[None] | None:
"""配对过期扫描后台任务(供 ``HostShutdown`` 取消)。"""
return self._pairing_scan_task
@property
def auditLogRetentionScanTask(self) -> asyncio.Task[None] | None:
"""审计日志保留期清理后台任务(供 ``HostShutdown`` 取消)。"""
return self._audit_log_retention_scan_task
@property
def pluginReloadTask(self) -> asyncio.Task[None] | None:
"""失败插件自动重载后台任务(供 ``HostShutdown`` 取消)。"""
return self._plugin_reload_task
@property
def transportManagerTask(self) -> asyncio.Task[None] | None:
"""传输引擎管理器后台任务(供 ``HostShutdown`` 取消)。"""
return self._transport_manager_task
async def bootstrap(self) -> None:
"""执行启动编排。
关键步骤Schema 初始化、核心初始化、被驱动适配器初始化)
失败必须抛异常导致宿主启动失败INV-I5。插件加载失败记录
警告并继续,触发优雅降级。每步有超时保护,关键步骤超时抛
``TimeoutError`` 终止启动。
启动序列INF-012步骤 7 与步骤 8 紧邻,对齐规范 §15.1
后台任务启动置于 ``_markReady`` 之后)::
0. _loadConfig 加载渠道配置INF-009
1. _initSchema 初始化渠道 schema
2. _initCore 注册中心 + EventBus + 领域服务
3. _initDrivenAdapters 被驱动适配器(含连通性检查 INF-010
4. _loadPlugins 渠道插件(按依赖拓扑)
5. _assemblePipelines 装配管道(含锚点校验 INF-011
6. _registerDrivingAdapters 注册驱动适配器(开始接受流量)
7. _markReady 标记宿主为 ready
8. _startOutboxRecovery Outbox 恢复扫描
9. _startPairingExpiration 配对过期扫描
10. _startAuditLogRetention 审计日志保留期清理
11. _startPluginReload 失败插件自动重载
Raises:
Exception: 关键步骤失败时向上抛出导致宿主启动失败INV-I5
TimeoutError: 任一关键步骤超时。
InternalError: ``bootstrap`` 重复或并发调用。
"""
# 入口守卫:防止重复/并发调用导致状态混乱孤儿后台任务、DI 重复
# 注册、驱动适配器重复接线等)。启动失败后允许调用方重试,故仅在
# 进行中或已就绪时拒绝。
if self._ready:
raise InternalError(message="host already bootstrapped")
if self._bootstrapping:
raise InternalError(message="bootstrap already in progress")
self._bootstrapping = True
try:
await self._logger.info("宿主启动开始")
# 0. 加载渠道配置INF-009
# §13.1:配置缺失或类型不符必须在启动期失败,不得静默使用默认值。
await self._loadConfig()
# 1. ensure_channel_schema() 初始化渠道 schema
# INV-I5 / FF-SCHEMA-02失败必须抛异常导致宿主启动失败
await self._initSchema()
# 2. 初始化注册中心 + EventBus + 领域服务(注册到 DI 容器)
await self._initCore()
# 3. 加载并初始化被驱动适配器INF-010应用级适配器连通性检查
await self._initDrivenAdapters()
# 4. 加载并初始化渠道插件(按依赖拓扑)
# 插件失败标记 failed触发优雅降级不阻塞其他插件
await self._loadPlugins()
# 4.5 注册通用命令(依赖已加载插件的 channel_type 列表)
await self._registerCommonCommands()
# 5. 装配管道(注入插件阶段)
await self._assemblePipelines()
# 6. 注册驱动适配器(开始接受流量)
await self._registerDrivingAdapters()
# 7. 标记宿主为 readyINF-012与步骤 6 紧邻,对齐规范 §15.1
# 要求"注册驱动适配器 → 标记 ready"不可被后台任务启动插入)
await self._markReady()
# 8. 启动 Outbox 恢复扫描
await self._startOutboxRecovery()
# 9. 启动配对过期扫描FR-33
await self._startPairingExpiration()
# 10. 启动审计日志保留期清理FR-34
await self._startAuditLogRetention()
# 11. 启动失败插件自动重载FR-36
await self._startPluginReload()
# 12. 启动传输引擎管理器
await self._startTransportManager()
await self._logger.info("宿主启动完成")
except Exception as e:
# 关键步骤失败INV-I5 / INF-013按 §15.2 逆序回滚已启动的
# 组件(注销驱动适配器 → 停止插件 → 取消后台任务),避免资源
# 泄漏与孤儿状态。随后重新抛出原始异常保留错误上下文INV-7
# 不掩盖根因。
await self._logger.error(f"宿主启动失败,执行回滚: {e}")
await self._rollback()
raise
finally:
# 无论成功或失败,重置启动进行中标志。成功后 ``_ready=True``
# 后续调用将被入口守卫拒绝;失败后允许调用方重试。
self._bootstrapping = False
def _cancelBackgroundTasks(self) -> None:
"""取消已启动的后台任务bootstrap 失败时回滚)。
使用 ``getattr`` 安全访问可能尚未创建的任务属性(失败可能发生在
任一后台任务启动之前),仅对未完成的任务触发取消。实际的任务等待
与异常记录由 ``HostShutdown._releaseCoreResourcesInternal`` 在关停
流程中完成,此处仅触发取消(``Task.cancel`` 为同步调度)以尽快
释放资源。
"""
for task_attr in (
"outboxScanTask",
"pairingScanTask",
"auditLogRetentionScanTask",
"pluginReloadTask",
"transportManagerTask",
):
task = getattr(self, task_attr, None)
if task is not None and not task.done():
task.cancel()
async def _startBackgroundTask(
self,
coro: Coroutine[Any, Any, None],
name: str,
log_message: str,
) -> asyncio.Task[None]:
"""启动后台任务并探测启动即失败P0-2
创建后台任务后,通过 ``asyncio.wait`` 在短探测窗口内观察任务是否
立即失败(未捕获异常在首次 ``await`` 前抛出)。正常运行的任务
(进入循环体)不触发探测失败,返回 Task 对象供调用方持有。
任务注册 ``done_callback`` 以捕获探测窗口之外的意外结束(如
``scan_loop`` 内部 ``while True`` 因未预期 bug 退出),避免任务
静默死亡无人感知。取消触发的结束不在 callback 中记录(属正常关停)。
Args:
coro: 后台任务协程(如 ``scanner.scan_loop(interval=60.0)``)。
name: 任务名称,用于日志与 ``DependencyError`` 标识。
log_message: 任务启动成功后的日志消息。
Returns:
已启动的 ``asyncio.Task`` 对象。
Raises:
DependencyError: 任务在探测窗口内完成且携带异常(启动即失败)。
"""
task = asyncio.create_task(coro)
task.add_done_callback(self._makeBackgroundTaskDoneCallback(name))
done, _ = await asyncio.wait({task}, timeout=self._TASK_STARTUP_PROBE_TIMEOUT)
if task in done and task.exception() is not None:
exc = task.exception()
await self._logger.error(
f"后台任务启动即失败: task={name}, error={exc}",
task=name,
error_type=type(exc).__name__,
error=str(exc),
)
raise DependencyError(name, exc) from exc
await self._logger.info(log_message)
return task
def _makeBackgroundTaskDoneCallback(
self,
name: str,
) -> Callable[[asyncio.Task[None]], None]:
"""构造后台任务结束回调。
返回的回调在任务结束时被调用:
- 任务被取消(``cancelled()``):静默返回,属正常关停流程。
- 任务正常完成(无异常):静默返回。
- 任务携带异常:调度异步 ERROR 日志记录,避免 ``scan_loop`` 内部
未预期 bug 导致任务静默死亡无人感知。
日志通过 ``asyncio.create_task`` 调度(回调为同步函数,无法直接
``await``)。事件循环已关闭时忽略日志(关停期边缘场景)。
Args:
name: 任务名称,用于日志标识。
Returns:
可注册给 ``Task.add_done_callback`` 的同步回调函数。
"""
def _on_done(task: asyncio.Task[None]) -> None:
if task.cancelled():
return
exc = task.exception()
if exc is None:
return
try:
asyncio.create_task(
self._logger.error(
f"后台任务意外结束: task={name}, error={exc}",
task=name,
error_type=type(exc).__name__,
error=str(exc),
)
)
except RuntimeError:
# 事件循环已关闭(关停期),无法调度日志,忽略
pass
return _on_done
async def _rollback(self) -> None:
"""启动失败时按 §15.2 逆序回滚已启动的组件INF-013
回滚顺序与启动顺序相反,确保已启动的组件被正确清理:
1. **注销驱动适配器**:若 ``_driving_registered`` 为 ``True`` 且
``driving_adapter_unregistrar`` 回调非 ``None``,调用之以停止
接收新流量。回调为 ``None`` 时跳过(调用方未提供注销能力)。
2. **停止已加载插件**:若 ``_plugins_loaded`` 为 ``True``,调用
``PluginLifecycleManager.stopAllStartedPlugins`` 释放所有
``STARTED`` / ``PAUSED`` 状态插件资源(封装内部迭代逻辑,
避免访问 ``_plugin_registry`` 私有属性)。
3. **取消后台任务**:复用 ``_cancelBackgroundTasks`` 取消 Outbox
扫描等已启动的后台任务。
单步回滚异常记录告警并继续确保后续步骤仍能执行INV-9 回退路径
完整性)。触发回滚的原始异常已由调用方 ``bootstrap`` 记录,此处
不重复记录,仅清理资源。
"""
# 1. 注销驱动适配器(停止接收新流量)
if self._driving_registered and self._driving_adapter_unregistrar is not None:
try:
await self._driving_adapter_unregistrar()
except Exception as exc:
await self._logger.error(
"rollback: driving_adapter_unregistrar failed",
error_type=type(exc).__name__,
error=str(exc),
)
# 2. 停止已加载的 STARTED/PAUSED 状态插件
# 通过 ``PluginLifecycleManager.stopAllStartedPlugins`` public API
# 释放插件资源,避免访问其内部 ``_plugin_registry`` 私有属性
# (封装原则)。单插件停止异常已由 ``stopAllStartedPlugins``
# 内部记录告警并继续,此处仅需捕获整体异常以确保后续回滚步骤执行。
if self._plugins_loaded:
try:
await self._plugin_lifecycle.stopAllStartedPlugins()
except Exception as exc:
await self._logger.error(
"rollback: stop plugins failed",
error_type=type(exc).__name__,
error=str(exc),
)
# 3. 取消已启动的后台任务(``_cancelBackgroundTasks`` 为同步方法,
# ``Task.cancel`` 仅调度取消不等待完成)
try:
self._cancelBackgroundTasks()
except Exception as exc:
await self._logger.error(
"rollback: cancel background tasks failed",
error_type=type(exc).__name__,
error=str(exc),
)
async def _initSchema(self) -> None:
"""初始化渠道 schema。
调用 ``ensure_channel_schema()`` 创建渠道网关表与索引。失败必须抛异常
导致宿主启动失败INV-I5 / FF-SCHEMA-02
Raises:
Exception: ``ensure_channel_schema`` 失败时向上抛出。
TimeoutError: schema 初始化超时。
"""
try:
await asyncio.wait_for(
self._ensure_schema.ensure_channel_schema(),
timeout=self._SCHEMA_INIT_TIMEOUT,
)
except TimeoutError as e:
await self._logger.error(
f"渠道 schema 初始化超时: timeout={self._SCHEMA_INIT_TIMEOUT}s",
)
raise OperationTimeoutError(
timeout_ms=int(self._SCHEMA_INIT_TIMEOUT * 1000),
message=f"渠道 schema 初始化超时: timeout={self._SCHEMA_INIT_TIMEOUT}s",
) from e
except Exception as e:
await self._logger.error(f"渠道 schema 初始化失败: {e}")
raise # 显式失败不掩盖根因INV-I5
async def _loadConfig(self) -> None:
"""加载渠道配置INF-009
调用 ``ConfigPort.getSchema`` 验证配置端口可用与 schema 已加载。
§13.1 要求"配置缺失或类型不匹配必须在启动期失败,不得静默使用
默认值"。本步骤在 schema 初始化前执行,确保后续 schema 初始化与
管道装配可基于已知有效的配置进行。
失败语义:
- ``getSchema`` 返回空元组:视为配置未初始化,抛
``ConfigValidationError`` 终止启动。
- ``getSchema`` 抛 ``DependencyError``(如 Redis 不可达):
向上抛出原始异常INV-I5由 ``bootstrap`` 的统一异常处理
路径记录并触发回滚。
- ``getSchema`` 抛其他异常:包装为 ``ConfigValidationError``
并通过 ``raise ... from e`` 保留根因INV-7
- 超时:抛 ``OperationTimeoutError`` 终止启动。
Raises:
ConfigValidationError: 配置 schema 为空或加载失败。
OperationTimeoutError: 配置加载超时。
"""
try:
schema = await asyncio.wait_for(
self._config_port.getSchema(),
timeout=self._CONFIG_LOAD_TIMEOUT,
)
except TimeoutError as e:
await self._logger.error(
f"渠道配置加载超时: timeout={self._CONFIG_LOAD_TIMEOUT}s",
)
raise OperationTimeoutError(
timeout_ms=int(self._CONFIG_LOAD_TIMEOUT * 1000),
message=f"渠道配置加载超时: timeout={self._CONFIG_LOAD_TIMEOUT}s",
) from e
except DependencyError:
# 依赖故障(如 Redis 不可达)已含 dep 信息,向上抛出保留根因
await self._logger.error("渠道配置加载失败:依赖故障")
raise
except Exception as e:
await self._logger.error(f"渠道配置加载失败: {e}")
raise ConfigValidationError(
[f"load config schema failed: {e}"],
) from e
if not schema:
await self._logger.error("渠道配置 schema 为空,配置未初始化")
raise ConfigValidationError(
["config schema is empty, config not initialized"],
)
await self._logger.info(
"渠道配置已加载",
schema_field_count=len(schema),
)
async def _initCore(self) -> None:
"""初始化注册中心 + EventBus + 领域服务。
将 framework 层应用级单例EventBus、4 个扩展点注册中心)注册到 DI
容器,供请求级 ``create_channel_use_cases`` 解析。核心注册为同步操作
(仅注册单例到 DI 容器,无 I/O无超时保护需求。
"""
await self._registerCoreSingletons()
await self._logger.info("核心组件已注册到 DI 容器")
async def _registerCoreSingletons(self) -> None:
"""将核心组件注册到 DI 容器。
注册两类应用级单例:
- framework 扩展点EventBus、StageSlotRegistry、
EventSubscriptionRegistry、ConfigSourceRegistry
- framework 编排组件PluginLifecycleManager、HealthAggregator
(供 ``create_channel_use_cases`` resolve 并注入用例服务)
同时将 ``EventPublisherPort`` 绑定到同一 ``EventBus`` 实例,使应用层
通过端口契约获取事件发布能力满足依赖方向铁律INV-1与端口实现
命名空间分离§6.3)。
核心注册表PluginRegistry、CapabilityRegistry 等)由调用方在
构造 DI 容器时注册,此处不重复注册。
"""
self._di.registerSingleton(EventBus, self._event_bus)
# EventPublisherPort 绑定到 EventBus 实例EventBus 结构化满足
# EventPublisherPort Protocol无需显式继承使应用层通过端口
# 契约依赖,不直接 import framework 层 EventBus。
self._di.registerSingleton(EventPublisherPort, self._event_bus)
self._di.registerSingleton(StageSlotRegistry, self._stage_slot_registry)
self._di.registerSingleton(EventSubscriptionRegistry, self._event_subscription_registry)
self._di.registerSingleton(ConfigSourceRegistry, self._config_source_registry)
self._di.registerSingleton(PluginLifecycleManager, self._plugin_lifecycle)
self._di.registerSingleton(HealthAggregator, self._health_aggregator)
self._di.registerSingleton(TransportManager, self._transport_manager)
async def _initDrivenAdapters(self) -> None:
"""加载并初始化被驱动适配器INF-010应用级适配器连通性检查
被驱动适配器为请求级组件,由 ``create_driven_adapters(db)`` 按请求创建,
共享同一 ``db`` session。本步骤对 ``HostBootstrap`` 构造时传入的应用级
被驱动适配器(``cache_port`` / ``persistence_port`` / ``queue_port`` /
``config_port`` 等跨请求共享实例)执行连通性检查,确保下游依赖可达:
任一失败必须抛 ``DependencyError`` 终止启动INV-I5避免启动后请求期
才发现依赖故障。
连通性检查策略(``_pingAdapter``
- 通过 ``PingablePort`` 显式契约判断适配器是否具备连通性探测能力
INV-3 契约显式化)。
- 满足契约的适配器(``RedisCacheAdapter`` /
``ChannelPersistenceAdapter`` / ``RedisConfigAdapter`` /
``ARQQueueAdapter``)调用其 ``ping()`` 方法。
- 不满足契约的适配器(如 ``AgentRunAdapter``)显式跳过。
Raises:
DependencyError: 任一应用级适配器连通性检查失败。
OperationTimeoutError: 被驱动适配器初始化超时。
"""
try:
await asyncio.wait_for(
self._checkDrivenAdaptersConnectivity(),
timeout=self._DRIVEN_ADAPTER_INIT_TIMEOUT,
)
except TimeoutError as e:
raise OperationTimeoutError(
timeout_ms=int(self._DRIVEN_ADAPTER_INIT_TIMEOUT * 1000),
message=f"被驱动适配器初始化超时: timeout={self._DRIVEN_ADAPTER_INIT_TIMEOUT}s",
) from e
await self._logger.info(
"应用级被驱动适配器连通性检查通过",
adapter_count=len(self._app_driven_adapters),
)
async def _checkDrivenAdaptersConnectivity(self) -> None:
"""对应用级被驱动适配器执行连通性检查INF-010
遍历 ``_app_driven_adapters``,逐个调用 ``_pingAdapter``。任一适配器
连通性失败抛 ``DependencyError`` 终止启动。``_app_driven_adapters`` 为
空时仅记录请求级 factory 模式说明并返回(向后兼容未传入场景)。
"""
if not self._app_driven_adapters:
await self._logger.debug(
"DI 容器就绪,被驱动适配器工厂可用(请求级,由 create_driven_adapters 按请求创建)",
)
return
for adapter in self._app_driven_adapters:
await self._pingAdapter(adapter)
async def _pingAdapter(self, adapter: Any) -> None:
"""对单个应用级被驱动适配器执行连通性检查。
通过 ``PingablePort`` 显式契约判断适配器是否具备连通性探测能力。
满足契约的适配器(``RedisCacheAdapter`` / ``ChannelPersistenceAdapter``
/ ``RedisConfigAdapter`` / ``ARQQueueAdapter``)调用其 ``ping()``
方法;不满足契约的适配器(如 ``AgentRunAdapter``)显式跳过。
``ping()`` 返回 ``False`` 或抛异常时,抛 ``DependencyError`` 附上
适配器类型名,由 ``bootstrap`` 的统一异常处理路径记录并触发回滚。
Raises:
DependencyError: 适配器 ping 返回 False 或抛异常。
"""
adapter_name = type(adapter).__name__
if not isinstance(adapter, PingablePort):
await self._logger.debug(
f"被驱动适配器未实现 PingablePort 契约,跳过连通性检查: adapter={adapter_name}",
)
return
try:
ok = await adapter.ping()
except Exception as exc:
await self._logger.error(
f"被驱动适配器连通性检查异常: adapter={adapter_name}, error={exc}",
adapter=adapter_name,
error_type=type(exc).__name__,
error=str(exc),
)
raise DependencyError(adapter_name, exc) from exc
if ok is False:
await self._logger.error(
f"被驱动适配器连通性检查失败: adapter={adapter_name}",
adapter=adapter_name,
)
raise DependencyError(
adapter_name,
Error(f"ping returned False: {adapter_name}"),
)
async def _loadPlugins(self) -> None:
"""加载并初始化渠道插件(按依赖拓扑)。
执行 ``discover`` 扫描插件目录 → ``resolve`` 拓扑排序 → 按序 ``load``。
discover/resolve 失败记录警告并跳过插件加载;单个插件 load 失败记录
警告并继续,触发优雅降级,不阻塞宿主启动。
Raises:
TimeoutError: discover 阶段超时。
"""
# discover 扫描插件目录
try:
manifests = await asyncio.wait_for(
self._plugin_lifecycle.discover(self._plugin_dir),
timeout=self._PLUGIN_DISCOVER_TIMEOUT,
)
except TimeoutError as e:
raise OperationTimeoutError(
timeout_ms=int(self._PLUGIN_DISCOVER_TIMEOUT * 1000),
message=f"插件发现超时: timeout={self._PLUGIN_DISCOVER_TIMEOUT}s",
) from e
except Exception as e:
await self._logger.warn(f"插件发现失败,跳过插件加载: {e}")
return
if not manifests:
await self._logger.info("无插件可加载")
return
await self._logger.info(f"发现 {len(manifests)} 个插件")
# resolve 拓扑排序(失败则使用未排序顺序)
try:
sorted_manifests = self._resolver.resolve(manifests)
except Exception as e:
await self._logger.warn(
f"插件依赖解析失败,使用未排序顺序加载: {e}",
)
sorted_manifests = manifests
# 按拓扑序加载
for manifest in sorted_manifests:
await self._loadSinglePlugin(manifest)
# INF-013插件加载步骤完成含优雅降级场景置位跟踪标志。
# ``_rollback`` 据此判断是否需要停止 STARTED/PAUSED 状态插件;
# 单个插件 load 失败已由 ``_loadSinglePlugin`` 记录告警并继续,
# 不影响本步骤的完成状态。
self._plugins_loaded = True
async def _registerCommonCommands(self) -> None:
"""注册通用命令到命令注册表。
从 DI 容器解析 ``PluginRegistry`` 与 ``CommandRegistryTable``
遍历已加载插件的 channel_type 列表,为每个渠道类型注册 6 个通用
命令(/help、/model、/reset、/about、/allowlist、/approve
渠道特定命令优先保留,通用命令仅填充未占用的命令名。
本步骤必须在 ``_loadPlugins`` 之后执行,因为 channel_type 列表
由插件 manifest 声明,只有在插件加载后才可知。
"""
plugin_registry = self._di.resolve(PluginRegistry)
command_registry_table = self._di.resolve(CommandRegistryTable)
channel_types = plugin_registry.listChannelTypes()
registerCommonCommands(command_registry_table, channel_types)
await self._logger.info(
f"通用命令注册完成: {len(channel_types)} 个渠道类型",
channel_count=len(channel_types),
)
async def _loadSinglePlugin(self, manifest: ChannelManifest) -> None:
"""加载单个插件,失败记录警告并继续。
插件加载失败不阻塞宿主启动,记录警告并触发优雅降级。
Args:
manifest: 渠道清单。
"""
try:
result: LifecycleResult = await asyncio.wait_for(
self._plugin_lifecycle.load(manifest.id),
timeout=self._PLUGIN_LOAD_TIMEOUT,
)
except TimeoutError:
await self._logger.warn(
f"插件加载超时,跳过: plugin_id={manifest.id}, timeout={self._PLUGIN_LOAD_TIMEOUT}s",
plugin_id=manifest.id,
)
return
except Exception as e:
await self._logger.warn(
f"插件加载异常,跳过: plugin_id={manifest.id}, error={e}",
plugin_id=manifest.id,
error=str(e),
)
return
if result.state == "failed":
await self._logger.warn(
f"插件加载失败,触发优雅降级: plugin_id={manifest.id}, error={result.error}",
plugin_id=manifest.id,
error=result.error or "",
)
else:
await self._logger.info(
f"插件加载成功: plugin_id={manifest.id}",
plugin_id=manifest.id,
)
async def _assemblePipelines(self) -> None:
"""装配管道注入插件阶段INF-011注入锚点格式校验
管道为请求级组件,由 ``create_channel_use_cases`` 按请求创建。阶段插槽
注入(``StageSlotInjector.inject``)在请求级管道装配时执行。本步骤在
启动期对注册中心所有声明式槽位执行一次注入验证(锚点格式合法、
``replace`` 白名单约束、目标 ID 非空),提前发现非法锚点,避免请求期
``inject`` 才抛 ``RuleViolationError`` 影响可用性。槽位校验为同步操作
(无 I/O无超时保护需求。
验证失败必须抛 ``ValidationError`` 终止启动INV-I5
Raises:
ValidationError: 槽位锚点格式非法或 ``replace`` 用于非白名单阶段。
"""
await self._validateAndLogPipelines()
async def _validateAndLogPipelines(self) -> None:
"""校验所有声明式管道槽位锚点并记录就绪状态INF-011
先调用 ``StageSlotInjector.validateAnchors`` 对所有管道的槽位锚点
做格式与白名单校验,失败抛 ``ValidationError``;通过后再记录各管道
槽位数量,供运维定位。
"""
# 1. 启动期锚点注入验证(不依赖请求级 stages 列表)
self._stage_slot_injector.validateAnchors()
# 2. 记录已注册槽位数量
inbound_slots = self._stage_slot_registry.findByPipeline("inbound")
outbound_slots = self._stage_slot_registry.findByPipeline("outbound")
control_slots = self._stage_slot_registry.findByPipeline("control")
await self._logger.info(
"管道阶段槽位就绪(请求级注入,锚点校验通过)",
inbound_slot_count=len(inbound_slots),
outbound_slot_count=len(outbound_slots),
control_slot_count=len(control_slots),
)
async def _registerDrivingAdapters(self) -> None:
"""注册驱动适配器(开始接受流量)。
若调用方提供了 ``driving_adapter_registrar`` 回调,则调用之以注册
各驱动适配器(如 Webhook / Admin API / 内置 Web 渠道等,具体由
宿主接线决定)。为 ``None`` 时记录告警,宿主不开始接受流量
(驱动适配器尚未接线)。
Raises:
TimeoutError: 驱动适配器注册超时。
"""
if self._driving_adapter_registrar is None:
await self._logger.warn(
"驱动适配器注册回调未提供宿主不开始接受流量driving_adapter_registrar=None",
)
return
try:
await asyncio.wait_for(
self._driving_adapter_registrar(),
timeout=self._DRIVING_ADAPTER_REGISTER_TIMEOUT,
)
except TimeoutError as e:
raise OperationTimeoutError(
timeout_ms=int(self._DRIVING_ADAPTER_REGISTER_TIMEOUT * 1000),
message=f"驱动适配器注册超时: timeout={self._DRIVING_ADAPTER_REGISTER_TIMEOUT}s",
) from e
await self._logger.info("驱动适配器已注册,开始接受流量")
# INF-013驱动适配器注册成功置位跟踪标志。``_rollback`` 据此
# 判断是否需要调用 ``driving_adapter_unregistrar`` 注销驱动适配器。
self._driving_registered = True
async def _startOutboxRecovery(self) -> None:
"""启动 Outbox 恢复扫描。
以后台任务方式启动 ``OutboxRecoveryScanner.scan_loop``,定时(默认
60s 间隔)扫描恢复重启前未完成的 Outbox 条目。``scan_loop`` 内部已
捕获并记录单轮扫描异常,不阻塞宿主启动,也不会因单轮失败而退出循环。
通过 ``_startBackgroundTask`` 启动探测捕获启动即失败。
Raises:
DependencyError: 后台任务启动即失败。
"""
self._outbox_scan_task = await self._startBackgroundTask(
self._outbox_scanner.scan_loop(interval=60.0),
name="outbox_scan",
log_message="Outbox 恢复定时扫描已启动(后台任务,间隔 60s",
)
async def _startPairingExpiration(self) -> None:
"""启动配对过期扫描后台任务FR-33
以后台任务方式启动 ``PairingExpirationScanner.scan_loop``,定时
(默认 300s 间隔)扫描过期的 PENDING 配对并通过聚合根
``expireIfOverdue`` 标记为 EXPIRED。通过 ``_startBackgroundTask``
启动探测捕获启动即失败。
Raises:
DependencyError: 后台任务启动即失败。
"""
self._pairing_scan_task = await self._startBackgroundTask(
self._pairing_scanner.scan_loop(interval=300.0),
name="pairing_scan",
log_message="配对过期定时扫描已启动(后台任务,间隔 300s",
)
async def _startAuditLogRetention(self) -> None:
"""启动审计日志保留期清理后台任务FR-34
以后台任务方式启动 ``AuditLogRetentionScanner.scan_loop``,定时
(默认 86400s 间隔)清理超过保留期(默认 90 天)的审计日志。通过
``_startBackgroundTask`` 启动探测捕获启动即失败。
Raises:
DependencyError: 后台任务启动即失败。
"""
self._audit_log_retention_scan_task = await self._startBackgroundTask(
self._audit_log_retention_scanner.scan_loop(interval=86400.0),
name="audit_log_retention_scan",
log_message="审计日志保留期定时清理已启动(后台任务,间隔 86400s保留 90 天)",
)
async def _startPluginReload(self) -> None:
"""启动失败插件自动重载后台任务FR-36
以后台任务方式启动 ``PluginLifecycleManager.reloadLoop``,定时
(默认 60s 间隔)扫描 ``DegradationManager`` 中到达 ``next_retry_at``
的失败插件并触发重载。``reloadLoop`` 内部已捕获并记录单轮扫描异常,
不阻塞宿主启动。通过 ``_startBackgroundTask`` 启动探测捕获启动即失败。
Raises:
DependencyError: 后台任务启动即失败。
"""
self._plugin_reload_task = await self._startBackgroundTask(
self._plugin_lifecycle.reloadLoop(interval=60.0),
name="plugin_reload",
log_message="失败插件自动重载已启动(后台任务,间隔 60s",
)
async def _startTransportManager(self) -> None:
"""启动传输引擎管理器。
以后台任务方式启动 ``TransportManager.start()``,统一管理所有
渠道账户的入站传输 WorkerPuller/Stream。通过 ``_startBackgroundTask``
启动探测捕获启动即失败。
Raises:
DependencyError: 后台任务启动即失败。
"""
self._transport_manager_task = await self._startBackgroundTask(
self._transport_manager.start(),
name="transport_manager",
log_message="传输引擎管理器已启动",
)
async def _markReady(self) -> None:
"""标记宿主为 ready。
``_setReadyState`` 为同步操作(仅置位标志与记录日志,无 I/O
无超时保护需求。
"""
await self._setReadyState()
async def _setReadyState(self) -> None:
"""设置宿主就绪状态。"""
self._ready = True
await self._logger.info("宿主已标记为 ready")