"""L5 FlowOrchestrator:编排幂等/熔断/会话缓存/队列/Flow。 send_text 流程: 1. 幂等检查(命中返回 skipped=true) 2. DB 校验熔断检查(OPEN 时返回 BRIDGE_CIRCUITED) 3. 入队(自适应 delay_ms:同联系人 1000 / 不同 config.send_delay_ms) 4. 成功且 verified 才写幂等缓存 5. DB 校验失败计数 + 熔断 6. 失败时 session_cache.invalidate() """ from __future__ import annotations import asyncio import logging import time from typing import Optional from woc_bridge.ui.circuit_breaker import CircuitBreaker, CircuitState from woc_bridge.ui.errors import FlowError, is_retryable from woc_bridge.ui.flow import FlowContext, FlowResult, FlowState from woc_bridge.ui.idem_cache import IdemCache logger = logging.getLogger("woc-bridge") # 同联系人发送间隔(毫秒),比默认 send_delay_ms 短 _SAME_CONTACT_DELAY_MS = 1000 class SessionCache: """会话缓存:记录最近打开的会话名,30s TTL。 命中时 SendTextFlow 跳过 activate/click_search_box/type_query/open_session 四步, 直接校验会话仍打开后进入 focus_input。 """ def __init__(self, ttl: float = 30.0) -> None: self.ttl = ttl self._current: Optional[tuple[str, float]] = None # (to_wxid, expire_at) def get(self) -> Optional[str]: """返回当前缓存的 to_wxid,过期或无缓存返回 None。""" if self._current is None: return None to_wxid, expire_at = self._current if time.monotonic() >= expire_at: self._current = None return None return to_wxid def set(self, to_wxid: str) -> None: """更新缓存。""" self._current = (to_wxid, time.monotonic() + self.ttl) def invalidate(self) -> None: """失效缓存。""" self._current = None class FlowOrchestrator: """L5 Flow 编排器。 整合幂等缓存、熔断器、会话缓存、发送队列、Flow 执行。 """ def __init__( self, send_queue, idem_cache: IdemCache, actions, db_reader, config, send_text_flow: Optional[SendTextFlow] = None, ) -> None: """初始化编排器。 Args: send_queue: SendQueue 实例 idem_cache: IdemCache 实例 actions: L3 Actions 实例 db_reader: DbReader 实例 config: BridgeConfig 实例(含 send_delay_ms 等) send_text_flow: 可选的 SendTextFlow 实例(用于测试注入) """ self.send_queue = send_queue self.idem_cache = idem_cache self.actions = actions self.db_reader = db_reader self.config = config self.session_cache = SessionCache(ttl=30.0) # DB 校验熔断器:5 次失败 / 30s 恢复 self.db_verify_breaker = CircuitBreaker( "db_verify", failure_threshold=5, recovery_timeout=30.0 ) self._send_text_flow = send_text_flow def _get_send_text_flow(self): """获取 SendTextFlow 实例(惰性创建或使用注入的)。""" if self._send_text_flow is not None: return self._send_text_flow # 惰性导入避免循环依赖 from woc_bridge.ui.flows.send_text import SendTextFlow return SendTextFlow( actions=self.actions, db_reader=self.db_reader, session_cache=self.session_cache, ) async def send_text( self, to_wxid: str, content: str, display_name: str = "", client_request_id: str = "", ) -> FlowResult: """发送文本消息。 流程: 1. 幂等检查 2. DB 校验熔断检查 3. 自适应延时入队 4. 执行 SendTextFlow 5. 成功且 verified 写幂等缓存 6. 失败时失效会话缓存 Args: to_wxid: 目标 wxid content: 消息内容 display_name: 显示名(搜索用) client_request_id: 客户端幂等 ID Returns: FlowResult """ started_at = time.monotonic() # 1. 幂等检查 cached = self.idem_cache.get( "send_text", to_wxid, content, client_request_id ) if cached is not None: logger.info( "[orchestrator] idem cache hit, skip send (to_wxid=%s)", to_wxid, ) # 返回缓存的 FlowResult,标记 skipped if isinstance(cached, FlowResult): return FlowResult( success=True, local_id=cached.local_id, verified=cached.verified, skipped=True, state=cached.state, duration_ms=(time.monotonic() - started_at) * 1000, ) # 兜底:构造一个 skipped 结果 return FlowResult( success=True, skipped=True, duration_ms=(time.monotonic() - started_at) * 1000, ) # 2. DB 校验熔断检查 if not self.db_verify_breaker.allow(): logger.warning( "[orchestrator] db_verify breaker OPEN, reject send" ) return FlowResult( success=False, error="db_verify circuit breaker open", error_code="BRIDGE_CIRCUITED", state=FlowState.FAILED, duration_ms=(time.monotonic() - started_at) * 1000, ) # 3. 自适应延时:同联系人用短延时 cached_session = self.session_cache.get() is_same_contact = ( cached_session is not None and cached_session == to_wxid ) delay_ms = _SAME_CONTACT_DELAY_MS if is_same_contact else None # None 表示用 send_queue 的默认 send_delay_ms # 4. 构造 FlowContext ctx = FlowContext( to_wxid=to_wxid, content=content, display_name=display_name, client_request_id=client_request_id, ) # 5. 入队执行 flow = self._get_send_text_flow() try: result: FlowResult = await self.send_queue.enqueue( lambda: flow.run(ctx), delay_ms=delay_ms, ) except Exception as exc: logger.error( "[orchestrator] send_queue exception: %s", exc ) # 失败时失效会话缓存 self.session_cache.invalidate() # 保留 BridgeError 的原始错误码(如 RATE_LIMITED), # 避免限流/超时等可重试错误被吞成 SEND_FAILED error_code = "SEND_FAILED" if hasattr(exc, "code") and exc.code: error_code = exc.code # 传递 details(如 retry_after)供路由层设置 Retry-After 头 exc_details = None if hasattr(exc, "details") and exc.details: exc_details = exc.details return FlowResult( success=False, error=str(exc), error_code=error_code, state=FlowState.FAILED, duration_ms=(time.monotonic() - started_at) * 1000, details=exc_details, ) # 6. 结果处理 if result.success and result.verified: # 成功且 verified:写幂等缓存 self.idem_cache.set( "send_text", to_wxid, content, result, client_request_id ) self.db_verify_breaker.record_success() elif result.success and not result.verified: # 发送成功但校验失败:DB 校验熔断计数 self.db_verify_breaker.record_failure() logger.warning( "[orchestrator] send success but verify failed, breaker fail_count+1" ) else: # 发送失败:失效会话缓存 self.session_cache.invalidate() logger.warning( "[orchestrator] send failed: %s (code=%s)", result.error, result.error_code, ) return result