1. 新增WeChatWocStreamConnectorAdapter,实现SSE长连接接收消息,支持热更新配置 2. 升级wechat_woc插件依赖bridge版本至1.5.0,开启both传输模式 3. 新增SSE相关常量与配置项,完善woc_bridge_client SSE实现 4. 重构RouteStage,复用会话缓存避免重复查询 5. 新增入站/出站全链路日志打点,优化异常场景日志输出 6. 实现入站幂等记录状态回写机制,独立事务避免死锁 7. 新增TransportManager重启恢复逻辑,自动恢复在线账号传输任务 8. 修复inbound_pipeline移除冗余persistence_port参数,注入idempotency仓储 9. 新增InboundContext.idempotency_record_id字段,传递幂等记录主键
1041 lines
49 KiB
Python
1041 lines
49 KiB
Python
"""HostBootstrap / HostShutdown 工厂。
|
||
|
||
本模块提供 ``create_host_bootstrap`` 与 ``create_host_shutdown`` 两个工厂函数,
|
||
集中装配渠道网关编排层所需的全量依赖,避免在 lifespan 中散落构造逻辑。
|
||
工厂按依赖拓扑顺序构造组件,并将共享依赖注册到 ``DependencyInjectionContainer``
|
||
供 ``create_host_shutdown`` 复用(INF-006:消除模块级可变状态)。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from collections.abc import Awaitable, Callable, Mapping
|
||
from dataclasses import dataclass
|
||
from typing import Any, Protocol
|
||
|
||
from arq import ArqRedis
|
||
from redis.asyncio import Redis
|
||
from sqlalchemy.ext.asyncio import AsyncSession
|
||
|
||
from yuxi.channels.adapters import DrivenAdapters, create_driven_adapters
|
||
from yuxi.channels.adapters.agent_run_execution_adapter import (
|
||
AgentRunExecutionAdapter,
|
||
)
|
||
from yuxi.channels.adapters.arq_queue_adapter import ARQQueueAdapter
|
||
from yuxi.channels.adapters.channel_persistence_adapter import (
|
||
ChannelPersistenceAdapter,
|
||
)
|
||
from yuxi.channels.adapters.content_review_repository_adapter import (
|
||
ContentReviewRepositoryAdapter,
|
||
)
|
||
from yuxi.channels.adapters.default_content_moderation_adapter import (
|
||
DefaultContentModerationAdapter,
|
||
)
|
||
from yuxi.channels.adapters.masking_adapter import MaskingAdapter
|
||
from yuxi.channels.adapters.opentelemetry_tracer_adapter import (
|
||
OpenTelemetryTracerAdapter,
|
||
)
|
||
from yuxi.channels.adapters.redis_cache_adapter import RedisCacheAdapter
|
||
from yuxi.channels.adapters.redis_config_adapter import RedisConfigAdapter
|
||
from yuxi.channels.application.circuit_breaker.channel_circuit_breaker import (
|
||
ChannelCircuitBreaker,
|
||
)
|
||
from yuxi.channels.application.extension.channel_event_broadcaster import (
|
||
ChannelEventBroadcaster,
|
||
)
|
||
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.handlers.channel_degraded_handler import (
|
||
ChannelDegradedHandler,
|
||
)
|
||
from yuxi.channels.application.extension.handlers.channel_recovered_handler import (
|
||
ChannelRecoveredHandler,
|
||
)
|
||
from yuxi.channels.application.extension.handlers.outbox_state_audit_handler import (
|
||
OutboxStateAuditHandler,
|
||
)
|
||
from yuxi.channels.application.extension.handlers.pairing_approved_handler import (
|
||
PairingApprovedHandler,
|
||
)
|
||
from yuxi.channels.application.extension.handlers.plugin_lifecycle_audit_handler import (
|
||
PluginLifecycleAuditHandler,
|
||
)
|
||
from yuxi.channels.application.extension.handlers.route_match_cache_handler import (
|
||
RouteMatchCacheHandler,
|
||
)
|
||
from yuxi.channels.application.extension.handlers.whitelist_config_handler import (
|
||
WhitelistConfigHandler,
|
||
)
|
||
from yuxi.channels.application.extension.stage_slot_registry import StageSlotRegistry
|
||
from yuxi.channels.application.health.channel_probe import ChannelProbe
|
||
from yuxi.channels.application.health.diagnostics_exporter import DiagnosticsExporter
|
||
from yuxi.channels.application.health.health_aggregator import HealthAggregator
|
||
from yuxi.channels.application.lifecycle.config_scope_registry import (
|
||
ConfigScopeRegistry,
|
||
)
|
||
from yuxi.channels.application.lifecycle.plugin_capability_checker import (
|
||
PluginCapabilityChecker,
|
||
)
|
||
from yuxi.channels.application.lifecycle.plugin_dependency_resolver import (
|
||
PluginDependencyResolver,
|
||
)
|
||
from yuxi.channels.application.lifecycle.plugin_lifecycle_manager import (
|
||
PluginLifecycleManager,
|
||
)
|
||
from yuxi.channels.application.lifecycle.plugin_loader import PluginLoader
|
||
from yuxi.channels.application.lifecycle.sensitive_field_registry import (
|
||
SensitiveFieldRegistry,
|
||
)
|
||
from yuxi.channels.application.pipeline.stage_slot_injector import StageSlotInjector
|
||
from yuxi.channels.application.transport import TransportManager
|
||
from yuxi.channels.contract.dtos.channel import ChannelType
|
||
from yuxi.channels.contract.dtos.config import ConfigScope
|
||
from yuxi.channels.contract.dtos.inbound import InboundMessageCmd
|
||
from yuxi.channels.contract.dtos.outbox import OutboxConfig
|
||
from yuxi.channels.contract.dtos.route import MatchTier
|
||
from yuxi.channels.contract.plugin.adapters.outbound_adapter import OutboundAdapter
|
||
from yuxi.channels.contract.plugin.extension_point import EventSubscription
|
||
from yuxi.channels.contract.ports.driven.agent_run_execution_port import (
|
||
AgentRunExecutionPort,
|
||
)
|
||
from yuxi.channels.contract.ports.driven.cache_port import CachePort
|
||
from yuxi.channels.contract.ports.driven.config_port import ConfigPort
|
||
from yuxi.channels.contract.ports.driven.content_review_repository_port import (
|
||
ContentReviewRepositoryPort,
|
||
)
|
||
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.persistence_port import PersistencePort
|
||
from yuxi.channels.contract.ports.driven.queue_port import QueuePort
|
||
from yuxi.channels.core.event.config import configureSensitiveFieldRegistry
|
||
from yuxi.channels.core.registry.capability_registry import CapabilityRegistry
|
||
from yuxi.channels.core.registry.command_registry import CommandRegistryTable
|
||
from yuxi.channels.core.registry.identity_resolver_registry import (
|
||
IdentityResolverEntry,
|
||
IdentityResolverRegistry,
|
||
)
|
||
from yuxi.channels.core.registry.plugin_registry import PluginRegistry
|
||
from yuxi.channels.core.registry.route_match_registry import RouteMatchRegistry
|
||
from yuxi.channels.core.registry.whitelist_registry import WhitelistRegistry
|
||
from yuxi.channels.core.service.capability_verifier import CapabilityVerifier
|
||
from yuxi.channels.core.service.degradation_manager import DegradationManager
|
||
from yuxi.channels.core.service.explicit_mapping_resolver import ExplicitMappingResolver
|
||
from yuxi.channels.core.service.phone_number_resolver import PhoneNumberResolver
|
||
from yuxi.channels.infrastructure.channel_use_cases import create_channel_use_cases
|
||
from yuxi.channels.infrastructure.dependency_injection import (
|
||
DependencyInjectionContainer,
|
||
)
|
||
from yuxi.channels.infrastructure.host_bootstrap import HostBootstrap
|
||
from yuxi.channels.infrastructure.host_shutdown import HostShutdown
|
||
from yuxi.config.app import config as app_config
|
||
from yuxi.services.run_queue_service import get_arq_pool, get_redis_client
|
||
from yuxi.utils.guard import ContentGuard
|
||
|
||
__all__ = ["create_host_bootstrap", "create_host_shutdown", "load_channel_plugins"]
|
||
|
||
#: 默认匹配层级(FR-04):按优先级降序排列,session_key 最高(800),
|
||
#: default 最低(100)。``registerTier`` 内部按升序排序,``RouteResolver``
|
||
#: 遍历时反转得到降序。
|
||
_DEFAULT_MATCH_TIERS: tuple[MatchTier, ...] = (
|
||
MatchTier(name="session_key", priority=800, match_method="exact"),
|
||
MatchTier(name="identity_id", priority=700, match_method="exact"),
|
||
MatchTier(name="peer_id", priority=600, match_method="exact"),
|
||
MatchTier(name="chat_type", priority=500, match_method="exact"),
|
||
MatchTier(name="channel_session", priority=400, match_method="exact"),
|
||
MatchTier(name="channel_type", priority=300, match_method="exact"),
|
||
MatchTier(name="account", priority=200, match_method="exact"),
|
||
MatchTier(name="default", priority=100, match_method="exact"),
|
||
)
|
||
|
||
|
||
def _register_default_match_tiers(registry: RouteMatchRegistry) -> None:
|
||
"""向路由匹配注册表注册 8 个默认层级与 6 个内置 matcher(FR-04)。
|
||
|
||
层级优先级:session_key(800) → identity_id(700) → peer_id(600) →
|
||
chat_type(500) → channel_session(400) → channel_type(300) →
|
||
account(200) → default(100)。
|
||
|
||
内置 matcher 注册 6 个(session_key/identity_id/peer_id/chat_type/account/default),
|
||
其余 2 个层级(channel_session/channel_type)暂未实现匹配逻辑,
|
||
``RouteResolver`` 遍历时会记录告警并跳过(AC-25)。插件可通过
|
||
``registerMatcher`` 为这些层级注入自定义 matcher。
|
||
"""
|
||
for tier in _DEFAULT_MATCH_TIERS:
|
||
registry.registerTier(tier)
|
||
# 注册内置 matcher(FR-04):通过闭包捕获 registry 查找方法
|
||
registry.registerMatcher("session_key", lambda ctx: registry.findBySession(ctx.session_key))
|
||
registry.registerMatcher(
|
||
"identity_id",
|
||
lambda ctx: registry.findByIdentity(ctx.unified_identity_id) if ctx.unified_identity_id else None,
|
||
)
|
||
registry.registerMatcher("peer_id", lambda ctx: registry.findExplicit(ctx.account_id, ctx.peer_id))
|
||
registry.registerMatcher("chat_type", lambda ctx: registry.findByChatType(ctx.account_id, ctx.chat_type))
|
||
registry.registerMatcher("account", lambda ctx: registry.findDefault(ctx.account_id))
|
||
registry.registerMatcher("default", lambda ctx: registry.findDefault(ctx.account_id))
|
||
|
||
|
||
class _EnsureSchemaPort(Protocol):
|
||
"""渠道 schema 初始化端口(工厂模块级 Protocol)。
|
||
|
||
``PostgresManager`` 满足此 Protocol(提供 ``ensure_channel_schema`` 与
|
||
``AsyncSession`` 方法)。与 ``host_bootstrap._EnsureSchemaPort`` 一致,
|
||
此处独立定义避免跨模块导入私有名,同时让工厂签名显式契约化(消除 ``Any``
|
||
弱类型,符合项目硬约束)。
|
||
"""
|
||
|
||
async def ensure_channel_schema(self) -> None:
|
||
"""确保渠道网关 schema 就绪。"""
|
||
...
|
||
|
||
def AsyncSession(self) -> AsyncSession: # noqa: N802 - 与 PostgresManager API 一致
|
||
"""创建异步数据库会话。"""
|
||
...
|
||
|
||
|
||
@dataclass
|
||
class _PluginLoadingCore:
|
||
"""插件加载核心组件聚合(供两工厂入口共享装配)。
|
||
|
||
``create_host_bootstrap`` 与 ``load_channel_plugins`` 共享同一装配拓扑:
|
||
6 个注册中心、共享 Redis/ARQ 客户端、EventBus、PluginLifecycleManager 及
|
||
其子依赖。聚合为单一 dataclass 消除 ~130 行重复构造代码,保证两入口
|
||
装配逻辑一致(AGENTS.md:复用场景允许抽象,禁止复制粘贴)。
|
||
"""
|
||
|
||
plugin_registry: PluginRegistry
|
||
stage_slot_registry: StageSlotRegistry
|
||
event_subscription_registry: EventSubscriptionRegistry
|
||
config_source_registry: ConfigSourceRegistry
|
||
route_match_registry: RouteMatchRegistry
|
||
whitelist_registry: WhitelistRegistry
|
||
capability_registry: CapabilityRegistry
|
||
event_bus: EventBus
|
||
cache_port: CachePort
|
||
degradation_manager: DegradationManager
|
||
plugin_lifecycle_manager: PluginLifecycleManager
|
||
plugin_dependency_resolver: PluginDependencyResolver
|
||
plugin_loader: PluginLoader
|
||
outbox_config: OutboxConfig
|
||
redis_client: Redis
|
||
arq_pool: ArqRedis
|
||
execution_port_impl: AgentRunExecutionAdapter
|
||
# F-01:敏感字段注册表,从 CONFIG_SCHEMA 预合并,HostBootstrap._loadPlugins
|
||
# discover 后调用 register(manifests) 合并插件声明的敏感字段。
|
||
sensitive_registry: SensitiveFieldRegistry
|
||
# F-02:配置作用域注册表,从 CONFIG_SCHEMA 预合并,HostBootstrap._loadPlugins
|
||
# discover 后调用 register(manifests) 合并插件声明的作用域。注入到
|
||
# RedisConfigAdapter 供 _build_key 校验 key→scope 一致性。
|
||
config_scope_registry: ConfigScopeRegistry
|
||
|
||
|
||
async def _assemble_plugin_loading_core(
|
||
logger: LoggerPort,
|
||
ensure_schema: _EnsureSchemaPort,
|
||
plugin_dir: str | None = None,
|
||
) -> _PluginLoadingCore:
|
||
"""装配插件加载核心组件(``create_host_bootstrap`` 与 ``load_channel_plugins`` 共享)。
|
||
|
||
按依赖拓扑顺序构造 6 个注册中心、共享 Redis/ARQ 客户端、EventBus、
|
||
PluginLifecycleManager 及其子依赖。``driven_adapters_factory`` 作为请求级
|
||
工厂闭包捕获 ``ensure_schema.AsyncSession()``,供 PluginHostImpl 获取端口。
|
||
|
||
Args:
|
||
logger: 日志端口,供所有 framework 层组件注入。
|
||
ensure_schema: 数据库会话工厂来源(``PostgresManager`` 满足此 Protocol)。
|
||
plugin_dir: 插件目录路径(P1 PLG-INSTALL/UNINSTALL 文件级操作注入),
|
||
传入则注入 ``PluginLoader.plugin_dir``,为 ``None`` 时 PluginLoader
|
||
仅支持 ``load``,文件级安装/卸载抛 ``ValidationError``。
|
||
|
||
Returns:
|
||
装配完成的 ``_PluginLoadingCore`` 聚合实例。
|
||
"""
|
||
# 1. 构造无依赖的注册中心
|
||
plugin_registry = PluginRegistry()
|
||
stage_slot_registry = StageSlotRegistry()
|
||
event_subscription_registry = EventSubscriptionRegistry()
|
||
config_source_registry = ConfigSourceRegistry(logger=logger)
|
||
route_match_registry = RouteMatchRegistry()
|
||
whitelist_registry = WhitelistRegistry()
|
||
_register_default_match_tiers(route_match_registry)
|
||
|
||
# 2. 构造共享 Redis 客户端与 ARQ 连接池(构造注入,INV-5),
|
||
# 供 RedisConfigAdapter / RedisCacheAdapter / ARQQueueAdapter 与请求级
|
||
# create_driven_adapters 复用。获取方式与 services/run_queue_service.py
|
||
# 保持一致(单例 + ping 校验),HostShutdown 关停时由适配器 close() 释放。
|
||
redis_client = await get_redis_client()
|
||
arq_pool = await get_arq_pool()
|
||
# 构造 AgentRunExecutionPort 实现,委托 yuxi.services 技术操作(ADP-001)
|
||
execution_port_impl = AgentRunExecutionAdapter()
|
||
|
||
# 3. 构造 cache_port / degradation_manager / EventBus
|
||
# EventBus 依赖 degradation_manager 在 FR-36 降级事件分发时联动
|
||
cache_port: CachePort = RedisCacheAdapter(redis_client=redis_client, logger=logger)
|
||
degradation_manager = DegradationManager(
|
||
cache_port=cache_port,
|
||
plugin_registry=plugin_registry,
|
||
logger=logger,
|
||
)
|
||
event_bus = EventBus(logger=logger, degradation_manager=degradation_manager)
|
||
|
||
# 4. 构造 OutboxConfig
|
||
# 组合根从配置源构造注入应用层,消除应用层对 yuxi.config.app 的直接依赖
|
||
# (§6.1 应用服务层禁止依赖具体技术适配器)。
|
||
outbox_config = OutboxConfig(
|
||
ttl_seconds=app_config.channel_outbox_ttl_seconds,
|
||
retry_backoff_schedule=tuple(app_config.channel_outbox_retry_backoff_schedule),
|
||
max_retry=app_config.channel_outbox_max_retry,
|
||
)
|
||
|
||
# 5. 构造 PluginLifecycleManager 的子依赖
|
||
plugin_loader = PluginLoader(logger=logger, plugin_dir=plugin_dir)
|
||
plugin_dependency_resolver = PluginDependencyResolver(logger=logger)
|
||
plugin_capability_checker = PluginCapabilityChecker(logger=logger)
|
||
capability_registry = CapabilityRegistry()
|
||
# F-01:敏感字段注册表,构造时预合并 CONFIG_SCHEMA 中的敏感字段。
|
||
# HostBootstrap._loadPlugins discover 后调用 register(manifests) 合并
|
||
# 插件 manifest 声明的敏感字段,并通过 configureSensitiveFieldRegistry
|
||
# 注入到 core/event/config.py 模块级全局变量。
|
||
sensitive_registry = SensitiveFieldRegistry()
|
||
# F-02:配置作用域注册表,构造时预合并 CONFIG_SCHEMA 中的作用域声明。
|
||
# HostBootstrap._loadPlugins discover 后调用 register(manifests) 合并
|
||
# 插件 manifest 声明的作用域。registry.key_to_scope_map 注入到
|
||
# RedisConfigAdapter 供 _build_key 校验 key→scope 一致性。
|
||
config_scope_registry = ConfigScopeRegistry()
|
||
|
||
# driven_adapters_factory 为请求级工厂,每次调用创建独立 AsyncSession
|
||
# 与 DrivenAdapters 实例,供 PluginHostImpl 获取端口。
|
||
# F-02:传入 config_scope_registry.key_to_scope_map 供 RedisConfigAdapter
|
||
# 校验作用域一致性。registry 通过 register() 原地更新内部 dict,adapter
|
||
# 持有的只读视图在 discover 后可见插件声明的作用域。
|
||
def driven_adapters_factory() -> DrivenAdapters:
|
||
db = ensure_schema.AsyncSession()
|
||
return create_driven_adapters(
|
||
db,
|
||
outbox_config=outbox_config,
|
||
redis_client=redis_client,
|
||
arq_pool=arq_pool,
|
||
execution_port=execution_port_impl,
|
||
key_to_scope_map=config_scope_registry.key_to_scope_map,
|
||
)
|
||
|
||
# 6. 构造 PluginLifecycleManager
|
||
plugin_lifecycle_manager = PluginLifecycleManager(
|
||
loader=plugin_loader,
|
||
resolver=plugin_dependency_resolver,
|
||
capability_checker=plugin_capability_checker,
|
||
plugin_registry=plugin_registry,
|
||
stage_slot_registry=stage_slot_registry,
|
||
event_subscription_registry=event_subscription_registry,
|
||
config_source_registry=config_source_registry,
|
||
event_bus=event_bus,
|
||
driven_adapters_factory=driven_adapters_factory,
|
||
degradation_manager=degradation_manager,
|
||
cache_port=cache_port,
|
||
logger=logger,
|
||
route_match_registry=route_match_registry,
|
||
capability_registry=capability_registry,
|
||
)
|
||
|
||
return _PluginLoadingCore(
|
||
plugin_registry=plugin_registry,
|
||
stage_slot_registry=stage_slot_registry,
|
||
event_subscription_registry=event_subscription_registry,
|
||
config_source_registry=config_source_registry,
|
||
route_match_registry=route_match_registry,
|
||
whitelist_registry=whitelist_registry,
|
||
capability_registry=capability_registry,
|
||
event_bus=event_bus,
|
||
cache_port=cache_port,
|
||
degradation_manager=degradation_manager,
|
||
plugin_lifecycle_manager=plugin_lifecycle_manager,
|
||
plugin_dependency_resolver=plugin_dependency_resolver,
|
||
plugin_loader=plugin_loader,
|
||
outbox_config=outbox_config,
|
||
redis_client=redis_client,
|
||
arq_pool=arq_pool,
|
||
execution_port_impl=execution_port_impl,
|
||
sensitive_registry=sensitive_registry,
|
||
config_scope_registry=config_scope_registry,
|
||
)
|
||
|
||
|
||
def _register_builtin_event_subscribers(
|
||
event_bus: EventBus,
|
||
persistence_port: PersistencePort,
|
||
queue_port: QueuePort,
|
||
whitelist_registry: WhitelistRegistry,
|
||
cache_port: CachePort,
|
||
plugin_registry: PluginRegistry,
|
||
logger: LoggerPort,
|
||
di_container: DependencyInjectionContainer | None = None,
|
||
) -> None:
|
||
"""注册内置事件订阅者(FR-18/22/30/32/33/34/36)。
|
||
|
||
订阅映射:
|
||
- ``OutboxStateChanged`` → 审计日志(FR-22 / FR-34)
|
||
- 8 类插件生命周期事件 → 审计日志(FR-32 / FR-34),共用同一
|
||
``PluginLifecycleAuditHandler`` 实例,handler 内部按事件类型
|
||
映射审计操作类型
|
||
- ``ConfigChanged`` / ``ConfigRollback`` → 白名单实时刷新(FR-18)
|
||
+ 路由匹配缓存失效(FR-30),复用同一对 handler,保证配置回滚
|
||
后下游缓存立即刷新
|
||
- ``PairingApproved`` → 入站管道重试(FR-33)
|
||
- ``ChannelDegraded`` / ``ChannelRecovered`` → 审计日志(FR-36),
|
||
形成降级-恢复闭环,满足「降级期间审计日志必须继续记录」要求
|
||
- ``ChannelSessionUpdated`` / ``ChannelMessageReceived`` /
|
||
``ChannelMessageSent`` → SSE 广播器(渠道实时事件)
|
||
"""
|
||
# OutboxStateChanged → 审计日志
|
||
event_bus.register(
|
||
"host",
|
||
EventSubscription(
|
||
event_type="OutboxStateChanged",
|
||
handler=OutboxStateAuditHandler(persistence_port),
|
||
),
|
||
)
|
||
# 8 类插件生命周期事件 → 审计日志
|
||
plugin_audit_handler = PluginLifecycleAuditHandler(
|
||
persistence_port=persistence_port,
|
||
plugin_registry=plugin_registry,
|
||
)
|
||
for plugin_event_type in (
|
||
"PluginDiscovered",
|
||
"PluginLoaded",
|
||
"PluginStarted",
|
||
"PluginPaused",
|
||
"PluginResumed",
|
||
"PluginStopped",
|
||
"PluginUnloaded",
|
||
"PluginFailed",
|
||
):
|
||
event_bus.register(
|
||
"host",
|
||
EventSubscription(
|
||
event_type=plugin_event_type,
|
||
handler=plugin_audit_handler,
|
||
),
|
||
)
|
||
# ConfigChanged / ConfigRollback → 白名单刷新 + 路由匹配缓存失效
|
||
# handler 复用同一对实例(WhitelistConfigHandler 内部按 event_type
|
||
# 区分 new_value / rollback_value),消除重复构造。
|
||
whitelist_handler = WhitelistConfigHandler(whitelist_registry, logger=logger)
|
||
route_match_handler = RouteMatchCacheHandler(cache_port)
|
||
for config_event in ("ConfigChanged", "ConfigRollback"):
|
||
event_bus.register(
|
||
"host",
|
||
EventSubscription(event_type=config_event, handler=whitelist_handler),
|
||
)
|
||
event_bus.register(
|
||
"host",
|
||
EventSubscription(event_type=config_event, handler=route_match_handler),
|
||
)
|
||
# PairingApproved → 入站管道重试
|
||
event_bus.register(
|
||
"host",
|
||
EventSubscription(
|
||
event_type="PairingApproved",
|
||
handler=PairingApprovedHandler(queue_port),
|
||
),
|
||
)
|
||
# ChannelDegraded / ChannelRecovered → 审计日志(降级-恢复闭环)
|
||
event_bus.register(
|
||
"host",
|
||
EventSubscription(
|
||
event_type="ChannelDegraded",
|
||
handler=ChannelDegradedHandler(persistence_port),
|
||
),
|
||
)
|
||
event_bus.register(
|
||
"host",
|
||
EventSubscription(
|
||
event_type="ChannelRecovered",
|
||
handler=ChannelRecoveredHandler(persistence_port),
|
||
),
|
||
)
|
||
|
||
# 渠道会话/消息事件 → SSE 广播器(BE-3)
|
||
channel_event_broadcaster = ChannelEventBroadcaster(logger=logger)
|
||
if di_container is not None:
|
||
di_container.registerSingleton(ChannelEventBroadcaster, channel_event_broadcaster)
|
||
for channel_event_type in (
|
||
"ChannelSessionUpdated",
|
||
"ChannelMessageReceived",
|
||
"ChannelMessageSent",
|
||
):
|
||
event_bus.register(
|
||
"host",
|
||
EventSubscription(
|
||
event_type=channel_event_type,
|
||
handler=channel_event_broadcaster,
|
||
),
|
||
)
|
||
|
||
|
||
def _construct_health_dependencies(
|
||
plugin_registry: PluginRegistry,
|
||
persistence_port: PersistencePort,
|
||
cache_port: CachePort,
|
||
queue_port: QueuePort,
|
||
capability_registry: CapabilityRegistry,
|
||
event_publisher: EventBus,
|
||
redis_client: Redis,
|
||
logger: LoggerPort,
|
||
key_to_scope_map: Mapping[str, ConfigScope] | None = None,
|
||
) -> tuple[ConfigPort, ChannelCircuitBreaker, ChannelProbe, OpenTelemetryTracerAdapter, DiagnosticsExporter]:
|
||
"""构造健康检查子依赖(FR-35,6-P0-07 装配)。
|
||
|
||
拆分为独立函数以解耦装配顺序:``TransportManager`` 需要
|
||
``channel_circuit_breaker``,而 ``HealthAggregator`` 需要
|
||
``TransportManager``(作为 ``transport_health_port``)。本函数先构造
|
||
circuit_breaker 等共享依赖,供后续 ``TransportManager`` 与
|
||
``HealthAggregator`` 复用,避免循环依赖。
|
||
|
||
``ChannelProbe`` 持有 ``PluginRegistry`` 引用,运行时动态查找插件注册的
|
||
``ProbeableAdapter``,无需静态 adapter_registry。``config_port`` /
|
||
``capability_verifier`` / ``channel_circuit_breaker`` 供
|
||
``HealthAggregator`` 读取各渠道熔断器状态并填充
|
||
``ChannelHealth.circuit_breaker_state``。
|
||
|
||
``tracer_port`` 供 ``DiagnosticsExporter`` / ``HealthAggregator`` 解析
|
||
真实 trace_id(INV-10)。``OpenTelemetryTracerAdapter`` 无状态(仅持
|
||
logger 引用 + 读 ContextVar)。
|
||
|
||
Args:
|
||
key_to_scope_map: F-02 配置作用域映射,注入到 ``RedisConfigAdapter``
|
||
供 ``_build_key`` 校验 key→scope 一致性。为 ``None`` 时不校验
|
||
(向后兼容)。
|
||
|
||
Returns:
|
||
``(config_port, channel_circuit_breaker, channel_probe, tracer_port,
|
||
diagnostics_exporter)`` 五元组,供后续 TransportManager 与
|
||
HealthAggregator 构造使用。
|
||
"""
|
||
config_port: ConfigPort = RedisConfigAdapter(
|
||
redis_client=redis_client,
|
||
logger=logger,
|
||
key_to_scope_map=key_to_scope_map,
|
||
)
|
||
capability_verifier = CapabilityVerifier(
|
||
cache_port=cache_port,
|
||
registry=capability_registry,
|
||
logger=logger,
|
||
)
|
||
channel_circuit_breaker = ChannelCircuitBreaker(
|
||
cache_port=cache_port,
|
||
capability_verifier=capability_verifier,
|
||
event_publisher=event_publisher,
|
||
config_port=config_port,
|
||
logger=logger,
|
||
)
|
||
channel_probe = ChannelProbe(
|
||
plugin_registry=plugin_registry,
|
||
persistence_port=persistence_port,
|
||
cache_port=cache_port,
|
||
queue_port=queue_port,
|
||
logger=logger,
|
||
)
|
||
tracer_port = OpenTelemetryTracerAdapter(logger)
|
||
masking_port = MaskingAdapter()
|
||
diagnostics_exporter = DiagnosticsExporter(
|
||
persistence_port=persistence_port,
|
||
logger=logger,
|
||
masking_port=masking_port,
|
||
tracer_port=tracer_port,
|
||
)
|
||
return config_port, channel_circuit_breaker, channel_probe, tracer_port, diagnostics_exporter
|
||
|
||
|
||
def _register_di_singletons(
|
||
di_container: DependencyInjectionContainer,
|
||
core: _PluginLoadingCore,
|
||
*,
|
||
persistence_port: PersistencePort,
|
||
persistence_db: AsyncSession,
|
||
queue_port: QueuePort,
|
||
config_port: ConfigPort,
|
||
stage_slot_injector: StageSlotInjector,
|
||
health_aggregator: HealthAggregator,
|
||
channel_circuit_breaker: ChannelCircuitBreaker,
|
||
transport_manager: TransportManager,
|
||
logger: LoggerPort,
|
||
) -> None:
|
||
"""注册全量 DI 单例到容器(INF-006:消除模块级可变状态)。
|
||
|
||
``HostBootstrap._registerCoreSingletons`` 也会注册部分单例,此处预注册
|
||
保证 ``bootstrap()`` 失败后 DI 容器状态完整,``create_channel_use_cases``
|
||
不会因漏注册抛 InternalError(``registerSingleton`` 幂等,后续覆盖为同一实例)。
|
||
|
||
注册内容分 4 类:
|
||
1. 从 ``core`` 直接注册的注册中心与共享依赖(Redis/ARQ/EventBus 等)
|
||
2. 命令注册表(FR-15,6 个跨渠道通用命令)与身份解析器注册表
|
||
(FR-05,explicit_mapping priority=200 + phone_number priority=300)
|
||
3. 应用级端口与编排组件(persistence/queue/cache/config +
|
||
HealthAggregator + CircuitBreaker + StageSlotInjector)
|
||
4. 内容审核域单例(CR-01~CR-03):
|
||
- ``ContentGuard``:封装关键词 + 可选 LLM 二审能力(不 import
|
||
全局单例 ``content_guard``,§9.5 插件隔离)
|
||
- ``DefaultContentModerationAdapter``:未注册插件渠道时的兜底
|
||
审核适配器,供 ``create_channel_use_cases`` resolve 并注入
|
||
DispatchStage
|
||
- ``ContentReviewRepositoryAdapter``:审核历史仓储适配器,复用
|
||
``persistence_db`` 共享 AsyncSession(与 ChannelPersistenceAdapter
|
||
同一会话,§10.1 共享会话约定),``tx`` 参数仅作为"是否自主提交"
|
||
标志位。同时注册为 ``ContentReviewRepositoryPort`` 端口绑定
|
||
(INV-3 契约显式化)
|
||
|
||
注:``persistence_db`` 的生命周期由 ``HostShutdown`` 通过
|
||
``persistence_port.aclose()`` → ``self._db.close()`` 统一管理
|
||
(CloseablePort 契约),不注册为 ``AsyncSession`` 单例(请求级资源,
|
||
注册为单例存在并发污染隐患)。
|
||
"""
|
||
# 1. 注册中心与共享依赖
|
||
di_container.registerSingleton(PluginRegistry, core.plugin_registry)
|
||
di_container.registerSingleton(CapabilityRegistry, core.capability_registry)
|
||
di_container.registerSingleton(RouteMatchRegistry, core.route_match_registry)
|
||
di_container.registerSingleton(WhitelistRegistry, core.whitelist_registry)
|
||
# 共享 Redis 客户端单例,供 create_channel_use_cases 解析并注入请求级
|
||
# RedisConfigAdapter(构造注入,INV-5)。
|
||
di_container.registerSingleton(Redis, core.redis_client)
|
||
# 共享 ARQ 连接池单例,供 create_channel_use_cases 解析并注入请求级
|
||
# ARQQueueAdapter(构造注入,INV-5 / ADP-005)。
|
||
di_container.registerSingleton(ArqRedis, core.arq_pool)
|
||
# AgentRunExecutionPort 单例,供 create_channel_use_cases 解析并注入
|
||
# 请求级 AgentRunAdapter(构造注入,ADP-001)。
|
||
di_container.registerSingleton(AgentRunExecutionPort, core.execution_port_impl)
|
||
di_container.registerSingleton(EventBus, core.event_bus)
|
||
# EventPublisherPort 绑定到 EventBus 实例(EventBus 结构化满足
|
||
# EventPublisherPort Protocol),使应用层通过端口契约依赖。
|
||
di_container.registerSingleton(EventPublisherPort, core.event_bus)
|
||
di_container.registerSingleton(StageSlotRegistry, core.stage_slot_registry)
|
||
di_container.registerSingleton(EventSubscriptionRegistry, core.event_subscription_registry)
|
||
di_container.registerSingleton(ConfigSourceRegistry, core.config_source_registry)
|
||
di_container.registerSingleton(PluginLifecycleManager, core.plugin_lifecycle_manager)
|
||
di_container.registerSingleton(PluginDependencyResolver, core.plugin_dependency_resolver)
|
||
# PluginLoader 单例(P1 PLG-INSTALL/UNINSTALL 文件操作),供
|
||
# create_channel_use_cases resolve 并注入 DispatchStage。
|
||
di_container.registerSingleton(PluginLoader, core.plugin_loader)
|
||
# F-01:敏感字段注册表单例,供 create_channel_use_cases resolve 并注入
|
||
# ControlPlanePipeline.create(ConfigHandler + AuditContextBuilder)与
|
||
# 外部 AuditContextBuilder。HostBootstrap._loadPlugins discover 后调用
|
||
# register(manifests) 合并插件声明的敏感字段。
|
||
di_container.registerSingleton(SensitiveFieldRegistry, core.sensitive_registry)
|
||
# F-02:配置作用域注册表单例,供 create_channel_use_cases resolve 并提取
|
||
# key_to_scope_map 注入到请求级 RedisConfigAdapter。HostBootstrap._loadPlugins
|
||
# discover 后调用 register(manifests) 合并插件声明的作用域。
|
||
di_container.registerSingleton(ConfigScopeRegistry, core.config_scope_registry)
|
||
|
||
# 2. 命令注册表(FR-15)与身份解析器注册表(FR-05)
|
||
# 由 bootstrap 一次性注册,避免每个请求重复构造解析器实例。
|
||
# 通用命令注册推迟到插件加载完成后由 HostBootstrap 触发
|
||
# (依赖已加载插件的 channel_type 列表)。
|
||
command_registry_table = CommandRegistryTable()
|
||
di_container.registerSingleton(CommandRegistryTable, command_registry_table)
|
||
|
||
identity_resolver_registry = IdentityResolverRegistry()
|
||
# explicit_mapping(priority=200):管理员配置的显式映射,置信度 1.0
|
||
identity_resolver_registry.registerResolver(
|
||
IdentityResolverEntry(
|
||
name="explicit_mapping",
|
||
priority=200,
|
||
resolver=ExplicitMappingResolver(config_port=config_port),
|
||
)
|
||
)
|
||
# phone_number(priority=300):手机号启发式匹配,置信度 0.7
|
||
identity_resolver_registry.registerResolver(
|
||
IdentityResolverEntry(
|
||
name="phone_number",
|
||
priority=300,
|
||
resolver=PhoneNumberResolver(persistence_port=persistence_port),
|
||
)
|
||
)
|
||
di_container.registerSingleton(IdentityResolverRegistry, identity_resolver_registry)
|
||
|
||
# 3. 应用级端口与编排组件
|
||
di_container.registerSingleton(StageSlotInjector, stage_slot_injector)
|
||
# HealthAggregator 预注册:HostBootstrap._registerCoreSingletons 也会注册,
|
||
# 此处预注册保证 bootstrap 失败后 DI 容器状态完整(与 EventBus 等同理)。
|
||
di_container.registerSingleton(HealthAggregator, health_aggregator)
|
||
# ChannelCircuitBreaker 注册为单例,供 create_channel_use_cases 解析并注入
|
||
# 控制面 DispatchStage(FR-35 熔断器状态查询,AC-32 运行时状态聚合)。
|
||
di_container.registerSingleton(ChannelCircuitBreaker, channel_circuit_breaker)
|
||
di_container.registerSingleton(LoggerPort, logger)
|
||
di_container.registerSingleton(CachePort, core.cache_port)
|
||
di_container.registerSingleton(PersistencePort, persistence_port)
|
||
di_container.registerSingleton(QueuePort, queue_port)
|
||
di_container.registerSingleton(ConfigPort, config_port)
|
||
di_container.registerSingleton(TransportManager, transport_manager)
|
||
|
||
# 4. 内容审核域单例(CR-01~CR-03)
|
||
content_guard = ContentGuard()
|
||
default_moderation_adapter = DefaultContentModerationAdapter(
|
||
content_guard=content_guard,
|
||
logger=logger,
|
||
)
|
||
review_repository = ContentReviewRepositoryAdapter(
|
||
db=persistence_db,
|
||
logger=logger,
|
||
)
|
||
di_container.registerSingleton(DefaultContentModerationAdapter, default_moderation_adapter)
|
||
di_container.registerSingleton(ContentReviewRepositoryAdapter, review_repository)
|
||
di_container.registerSingleton(ContentReviewRepositoryPort, review_repository)
|
||
|
||
|
||
async def create_host_bootstrap(
|
||
ensure_schema: _EnsureSchemaPort,
|
||
logger: LoggerPort,
|
||
plugin_dir: str,
|
||
di_container: DependencyInjectionContainer,
|
||
driving_adapter_registrar: Callable[[], Awaitable[None]] | None = None,
|
||
driving_adapter_unregistrar: Callable[[], Awaitable[None]] | None = None,
|
||
) -> HostBootstrap:
|
||
"""构造完整的 HostBootstrap 实例。
|
||
|
||
按依赖拓扑顺序装配渠道网关编排层的全量组件:注册中心 → EventBus →
|
||
端口适配器 → 插件生命周期管理器 → 管道注入器 → 健康聚合器 →
|
||
DI 容器 → HostBootstrap。构造完成后将共享依赖注册到
|
||
``DependencyInjectionContainer``,供 ``create_host_shutdown`` 复用
|
||
(INF-006:消除模块级可变状态)。
|
||
|
||
注:Outbox 恢复扫描(FR-22)、配对过期扫描(FR-33)与审计日志保留期
|
||
清理(FR-34)已迁移至 scheduler worker handler,不再由 api 进程构造与
|
||
启动,避免共享 ``AsyncSession`` 并发使用与锁 TTL 不匹配问题。
|
||
|
||
Args:
|
||
ensure_schema: 渠道 schema 初始化端口(``PostgresManager`` 满足此
|
||
Protocol,提供 ``ensure_channel_schema`` 方法)。同时作为数据库
|
||
会话工厂来源,通过 ``ensure_schema.AsyncSession()`` 创建应用级
|
||
与请求级 ``AsyncSession``。
|
||
logger: 日志被驱动端口,供所有 framework 层组件注入。
|
||
plugin_dir: 插件目录路径,供 ``PluginLifecycleManager.discover`` 扫描。
|
||
di_container: 依赖注入容器,由调用方创建并传入。HostBootstrap 的
|
||
``_registerCoreSingletons`` 会将 framework 层应用级单例
|
||
(EventBus、StageSlotRegistry、PluginLifecycleManager、
|
||
HealthAggregator 等)注册到该容器,供路由层通过
|
||
``app.state.channel_di_container`` 解析。调用方需将同一容器
|
||
赋值给 ``app.state.channel_di_container`` 以保证路由层可访问。
|
||
driving_adapter_registrar: 驱动适配器注册回调(可选),由调用方
|
||
提供,在 HostBootstrap 步骤 7 调用以开始接受流量。
|
||
driving_adapter_unregistrar: 驱动适配器注销回调(可选,INF-013),
|
||
与 ``driving_adapter_registrar`` 对称,由调用方提供。当
|
||
``bootstrap()`` 启动失败回滚且驱动适配器已注册时调用,以注销
|
||
已注册的驱动适配器并停止接收新流量。为 ``None`` 时回滚跳过
|
||
驱动适配器注销步骤。
|
||
|
||
Returns:
|
||
装配完成的 HostBootstrap 实例,调用方需进一步调用 ``bootstrap()``
|
||
执行启动编排。
|
||
"""
|
||
# 1. 装配插件加载核心组件(与 load_channel_plugins 共享)
|
||
core = await _assemble_plugin_loading_core(logger, ensure_schema, plugin_dir=plugin_dir)
|
||
|
||
# 2. 构造应用级 persistence_port 与 queue_port
|
||
# persistence_port 需要 AsyncSession,创建应用级会话供 TransportManager /
|
||
# HealthAggregator / DiagnosticsExporter 共用;queue_port 无需会话。
|
||
persistence_db = ensure_schema.AsyncSession()
|
||
persistence_port: PersistencePort = ChannelPersistenceAdapter(
|
||
persistence_db, outbox_config=core.outbox_config, logger=logger
|
||
)
|
||
queue_port: QueuePort = ARQQueueAdapter(arq_pool=core.arq_pool, logger=logger)
|
||
|
||
# 3. 注册内置事件订阅者(FR-18/22/30/32/33/34/36)
|
||
_register_builtin_event_subscribers(
|
||
event_bus=core.event_bus,
|
||
persistence_port=persistence_port,
|
||
queue_port=queue_port,
|
||
whitelist_registry=core.whitelist_registry,
|
||
cache_port=core.cache_port,
|
||
plugin_registry=core.plugin_registry,
|
||
logger=logger,
|
||
di_container=di_container,
|
||
)
|
||
|
||
# 4. 构造 StageSlotInjector
|
||
stage_slot_injector = StageSlotInjector(
|
||
stage_slot_registry=core.stage_slot_registry,
|
||
logger=logger,
|
||
)
|
||
|
||
# 6. 构造健康检查子依赖(circuit_breaker 等,FR-35,6-P0-07 装配)
|
||
# 拆分为独立步骤以解耦装配顺序:TransportManager 需要 circuit_breaker,
|
||
# HealthAggregator 需要 TransportManager(作为 transport_health_port)
|
||
(
|
||
config_port,
|
||
channel_circuit_breaker,
|
||
channel_probe,
|
||
tracer_port,
|
||
diagnostics_exporter,
|
||
) = _construct_health_dependencies(
|
||
plugin_registry=core.plugin_registry,
|
||
persistence_port=persistence_port,
|
||
cache_port=core.cache_port,
|
||
queue_port=queue_port,
|
||
capability_registry=core.capability_registry,
|
||
event_publisher=core.event_bus,
|
||
redis_client=core.redis_client,
|
||
logger=logger,
|
||
key_to_scope_map=core.config_scope_registry.key_to_scope_map,
|
||
)
|
||
|
||
# 7. 构造 TransportManager(需要 circuit_breaker,供 HealthAggregator 作为
|
||
# transport_health_port 注入,FR-18 传输健康聚合)
|
||
async def _deliver_inbound_message(cmd: InboundMessageCmd) -> Any:
|
||
db = ensure_schema.AsyncSession()
|
||
try:
|
||
use_cases = create_channel_use_cases(db, di_container)
|
||
result = await use_cases.inbound_message.receiveInbound(cmd)
|
||
await logger.debug(
|
||
"inbound message delivered",
|
||
trace_id=cmd.trace_id,
|
||
channel_type=str(cmd.channel_type),
|
||
account_id=cmd.account_id,
|
||
ack_decision=result.ack_decision,
|
||
)
|
||
return result
|
||
finally:
|
||
# SQLAlchemy AsyncSession.close() 会自动 rollback 未提交的事务,
|
||
# 无需显式 rollback(L-12 修复:删除冗余 rollback)。close 失败
|
||
# 时记录 error,避免连接泄漏被静默吞掉。
|
||
try:
|
||
await db.close()
|
||
except Exception as exc:
|
||
await logger.error(
|
||
"inbound_message db close failed, connection may leak",
|
||
error=str(exc),
|
||
)
|
||
|
||
transport_manager = TransportManager(
|
||
plugin_registry=core.plugin_registry,
|
||
persistence_port=persistence_port,
|
||
config_port=config_port,
|
||
event_bus=core.event_bus,
|
||
circuit_breaker=channel_circuit_breaker,
|
||
logger=logger,
|
||
message_deliverer=_deliver_inbound_message,
|
||
)
|
||
|
||
# 8. 构造 HealthAggregator(注入 transport_manager 作为 transport_health_port)
|
||
health_aggregator = HealthAggregator(
|
||
plugin_registry=core.plugin_registry,
|
||
persistence_port=persistence_port,
|
||
cache_port=core.cache_port,
|
||
queue_port=queue_port,
|
||
circuit_breaker=channel_circuit_breaker,
|
||
channel_probe=channel_probe,
|
||
diagnostics_exporter=diagnostics_exporter,
|
||
logger=logger,
|
||
tracer_port=tracer_port,
|
||
transport_health_port=transport_manager,
|
||
)
|
||
|
||
# 9. 注册全量 DI 单例(INF-006:消除模块级可变状态)
|
||
_register_di_singletons(
|
||
di_container=di_container,
|
||
core=core,
|
||
persistence_port=persistence_port,
|
||
persistence_db=persistence_db,
|
||
queue_port=queue_port,
|
||
config_port=config_port,
|
||
stage_slot_injector=stage_slot_injector,
|
||
health_aggregator=health_aggregator,
|
||
channel_circuit_breaker=channel_circuit_breaker,
|
||
transport_manager=transport_manager,
|
||
logger=logger,
|
||
)
|
||
|
||
# 10. 构造 HostBootstrap(INF-009/010:传入 config_port 与 app_driven_adapters,
|
||
# 供 ``_loadConfig`` 加载渠道配置、``_initDrivenAdapters`` 对应用级
|
||
# 被驱动适配器执行连通性检查;driving_adapter_unregistrar 用于启动
|
||
# 失败回滚时注销驱动适配器)
|
||
app_driven_adapters = (
|
||
core.cache_port,
|
||
persistence_port,
|
||
queue_port,
|
||
config_port,
|
||
)
|
||
return HostBootstrap(
|
||
ensure_schema=ensure_schema,
|
||
di_container=di_container,
|
||
plugin_dir=plugin_dir,
|
||
event_bus=core.event_bus,
|
||
stage_slot_registry=core.stage_slot_registry,
|
||
event_subscription_registry=core.event_subscription_registry,
|
||
config_source_registry=core.config_source_registry,
|
||
plugin_lifecycle_manager=core.plugin_lifecycle_manager,
|
||
plugin_dependency_resolver=core.plugin_dependency_resolver,
|
||
stage_slot_injector=stage_slot_injector,
|
||
health_aggregator=health_aggregator,
|
||
transport_manager=transport_manager,
|
||
logger=logger,
|
||
config_port=config_port,
|
||
driving_adapter_registrar=driving_adapter_registrar,
|
||
driving_adapter_unregistrar=driving_adapter_unregistrar,
|
||
app_driven_adapters=app_driven_adapters,
|
||
)
|
||
|
||
|
||
def create_host_shutdown(
|
||
host_bootstrap: HostBootstrap,
|
||
di_container: DependencyInjectionContainer,
|
||
inflight_drain_waiter: Callable[[float], Awaitable[None]] | None = None,
|
||
) -> HostShutdown:
|
||
"""构造 HostShutdown 实例,复用 ``create_host_bootstrap`` 注册到 DI 容器的共享依赖。
|
||
|
||
从 ``DependencyInjectionContainer`` 解析必填依赖,并传入
|
||
``host_bootstrap`` 实例本身(而非其后台任务引用),由 ``HostShutdown``
|
||
在关停时惰性读取后台任务属性。这样允许 ``HostShutdown``
|
||
在 ``bootstrap()`` 执行之前创建,避免任务引用在构造时为 ``None`` 导致
|
||
关停时取消逻辑被跳过。
|
||
|
||
INF-017:同时从 DI 容器解析 4 个应用级被驱动适配器
|
||
(``cache_port`` / ``persistence_port`` / ``queue_port`` / ``config_port``)
|
||
传入 ``app_driven_adapters`` 参数,供 ``HostShutdown`` 在关停步骤 6
|
||
显式调用 ``close()`` 释放资源。
|
||
|
||
INF-006:依赖来源由模块级全局状态改为 DI 容器,
|
||
消除模块级可变状态(INV-5)。
|
||
|
||
注:``HostShutdown`` 不再接受 ``driving_adapter_unregistrar`` 参数。
|
||
当前系统未接入驱动适配器机制,关停步骤已省略"注销驱动适配器"。
|
||
当驱动适配器实现就绪后,需在 ``HostShutdown`` 与本工厂同步恢复该参数。
|
||
|
||
Args:
|
||
host_bootstrap: HostBootstrap 实例。无需已执行 ``bootstrap()``,
|
||
``HostShutdown`` 会在关停时惰性读取其后台任务属性。
|
||
di_container: 依赖注入容器,由 ``create_host_bootstrap`` 注册共享
|
||
依赖。需与传入 ``host_bootstrap`` 的容器为同一实例。
|
||
inflight_drain_waiter: 在途请求排空回调(可选),接受超时参数,
|
||
在关停步骤 2 调用以等待在途请求完成。由 ``InflightRequestTracker.wait_drained``
|
||
提供。为 ``None`` 时步骤 2 直接等待固定超时。
|
||
|
||
Returns:
|
||
装配完成的 HostShutdown 实例,调用方需进一步调用 ``shutdown()``
|
||
执行关停编排。
|
||
"""
|
||
# 解析应用级被驱动适配器(HostBootstrap 构造时注册到 DI 容器)
|
||
app_driven_adapters = (
|
||
di_container.resolve(CachePort),
|
||
di_container.resolve(PersistencePort),
|
||
di_container.resolve(QueuePort),
|
||
di_container.resolve(ConfigPort),
|
||
)
|
||
|
||
return HostShutdown(
|
||
plugin_lifecycle_manager=di_container.resolve(PluginLifecycleManager),
|
||
plugin_registry=di_container.resolve(PluginRegistry),
|
||
plugin_dependency_resolver=di_container.resolve(PluginDependencyResolver),
|
||
stage_slot_injector=di_container.resolve(StageSlotInjector),
|
||
stage_slot_registry=di_container.resolve(StageSlotRegistry),
|
||
event_subscription_registry=di_container.resolve(EventSubscriptionRegistry),
|
||
config_source_registry=di_container.resolve(ConfigSourceRegistry),
|
||
event_bus=di_container.resolve(EventBus),
|
||
transport_manager=di_container.resolve(TransportManager),
|
||
logger=di_container.resolve(LoggerPort),
|
||
host_bootstrap=host_bootstrap,
|
||
inflight_drain_waiter=inflight_drain_waiter,
|
||
app_driven_adapters=app_driven_adapters,
|
||
)
|
||
|
||
|
||
async def load_channel_plugins(
|
||
plugin_dir: str,
|
||
logger: LoggerPort,
|
||
ensure_schema: _EnsureSchemaPort,
|
||
) -> tuple[PluginRegistry, dict[ChannelType, OutboundAdapter], EventBus]:
|
||
"""加载渠道插件,返回 PluginRegistry、outbound_adapter_registry 与 EventBus。
|
||
|
||
通过 ``_assemble_plugin_loading_core`` 装配核心组件(与
|
||
``create_host_bootstrap`` 共享同一装配拓扑),执行 discover + resolve +
|
||
load 全流程,从加载的插件适配器构建 outbound_adapter_registry。供 ARQ
|
||
Worker 等非 FastAPI 进程共享调用。
|
||
|
||
与 ``create_host_bootstrap`` 的行为差异(调用方需知悉):
|
||
- **不执行** schema 初始化(``ensure_channel_schema``)
|
||
- **不执行** 配置加载校验(``_loadConfig`` / ``config_port.getSchema``)
|
||
- **不注册** 内置事件订阅者(OutboxStateAudit / PluginLifecycleAudit /
|
||
WhitelistConfig / RouteMatchCache / PairingApproved / ChannelDegraded /
|
||
ChannelRecovered 等均缺失)
|
||
- **不注册** 通用命令(``registerCommonCommands``)
|
||
- **不注册** 内置身份解析器(explicit_mapping / phone_number)
|
||
- **不构造** HealthAggregator 等编排组件
|
||
- **不注册** 任何 DI 容器单例(调用方需自行管理返回的三个实例)
|
||
|
||
失败处理对齐 ``HostBootstrap._loadPlugins``:
|
||
- discover 失败:记录警告并跳过插件加载(warn-and-continue,对齐
|
||
§11.9 优雅降级)。注意:仅 ``TimeoutError`` 在 ``HostBootstrap``
|
||
中会终止启动,本函数不实现超时保护,discover 异统一律 warn-and-continue。
|
||
- resolve 失败:记录警告并使用未排序顺序继续。
|
||
- 单个插件 load 失败:记录警告并继续(触发优雅降级),不阻塞其他插件。
|
||
|
||
Args:
|
||
plugin_dir: 插件目录路径,供 ``PluginLifecycleManager.discover`` 扫描。
|
||
logger: 日志端口,供所有 framework 层组件注入。
|
||
ensure_schema: 数据库会话工厂来源(``PostgresManager`` 满足此
|
||
Protocol,提供 ``AsyncSession`` 方法),供 ``driven_adapters_factory``
|
||
创建请求级 ``AsyncSession``。
|
||
|
||
Returns:
|
||
``(plugin_registry, outbound_adapter_registry, event_bus)`` 三元组。
|
||
``plugin_registry`` 已填充已加载插件的清单与适配器。
|
||
``outbound_adapter_registry`` 按渠道类型索引 ``OutboundAdapter``。
|
||
``event_bus`` 为插件加载过程中使用的 ``EventBus``,供调用方复用。
|
||
"""
|
||
# 1. 装配插件加载核心组件(与 create_host_bootstrap 共享)
|
||
core = await _assemble_plugin_loading_core(logger, ensure_schema, plugin_dir=plugin_dir)
|
||
|
||
# 2. discover + resolve + load(与 HostBootstrap._loadPlugins 一致)
|
||
# discover 失败记录警告并跳过插件加载(对齐 §11.9 优雅降级),
|
||
# 不静默吞掉错误,保证调用方知晓插件未加载。
|
||
try:
|
||
manifests = await core.plugin_lifecycle_manager.discover(plugin_dir)
|
||
except Exception as e:
|
||
await logger.warn(f"插件发现失败,跳过插件加载: {e}")
|
||
manifests = []
|
||
|
||
if not manifests:
|
||
await logger.info("无插件可加载")
|
||
else:
|
||
await logger.info(f"发现 {len(manifests)} 个插件")
|
||
# F-01:合并插件 manifest 声明的敏感字段到 SensitiveFieldRegistry,
|
||
# 并通过 configureSensitiveFieldRegistry 注入到 core/event/config.py
|
||
# 模块级全局变量。worker 进程(ARQ 等)发布 ConfigChanged/
|
||
# ConfigRollback 事件时,_sanitize_config_value 能识别插件声明的
|
||
# 敏感字段(如 bridge_url/bot_token/app_secret 等),避免泄露到事件
|
||
# 订阅方。对齐 HostBootstrap._loadPlugins 的调用顺序(register +
|
||
# configure 在 resolve + load 之前)。
|
||
core.sensitive_registry.register(manifests)
|
||
configureSensitiveFieldRegistry(core.sensitive_registry)
|
||
# resolve 拓扑排序(失败则使用未排序顺序)
|
||
try:
|
||
sorted_manifests = core.plugin_dependency_resolver.resolve(manifests)
|
||
except Exception as e:
|
||
await logger.warn(f"插件依赖解析失败,使用未排序顺序加载: {e}")
|
||
sorted_manifests = manifests
|
||
# 按拓扑序加载(单个插件失败不阻塞其他插件,触发优雅降级)
|
||
for manifest in sorted_manifests:
|
||
result = await core.plugin_lifecycle_manager.load(manifest.id)
|
||
if result.state == "failed":
|
||
await logger.warn(
|
||
f"插件加载失败,触发优雅降级: plugin_id={manifest.id}, error={result.error}",
|
||
plugin_id=manifest.id,
|
||
error=result.error or "",
|
||
)
|
||
else:
|
||
await logger.info(
|
||
f"插件加载成功: plugin_id={manifest.id}",
|
||
plugin_id=manifest.id,
|
||
)
|
||
|
||
# 3. 构建 outbound_adapter_registry(与 create_channel_use_cases 一致)
|
||
outbound_adapter_registry: dict[ChannelType, OutboundAdapter] = {}
|
||
for channel_type, plugin_da in core.plugin_registry.listPluginAdapters():
|
||
for adapter in plugin_da.outbound_adapters:
|
||
outbound_adapter_registry[channel_type] = adapter
|
||
|
||
return core.plugin_registry, outbound_adapter_registry, core.event_bus
|