WechatOnCloud/bridge/woc_bridge/ui/orchestrator.py
Kris 15fc62d478 feat: 完成微信UI自动化新架构全量开发与集成
- 新增六层UI自动化架构:从Backend到Capabilities的完整分层实现
- 添加WeChat 4.0分辨率适配Profile与图像模板资源
- 实现幂等缓存、熔断器、重试策略、链路追踪与监控指标
- 新增头像下载安全校验、发布朋友圈路径白名单防护
- 优化密钥缓存、DB校验逻辑与初始化流程
- 补充完整错误码体系与启动清场机制
2026-07-16 01:19:47 +08:00

245 lines
8.1 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.

"""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