- 新增六层UI自动化架构:从Backend到Capabilities的完整分层实现 - 添加WeChat 4.0分辨率适配Profile与图像模板资源 - 实现幂等缓存、熔断器、重试策略、链路追踪与监控指标 - 新增头像下载安全校验、发布朋友圈路径白名单防护 - 优化密钥缓存、DB校验逻辑与初始化流程 - 补充完整错误码体系与启动清场机制
245 lines
8.1 KiB
Python
245 lines
8.1 KiB
Python
"""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
|