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

212 lines
7.3 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.

"""L4 Flow 基类与状态机基础设施。
Flow 是 L4 层的核心抽象:编排 L3 Actions 完成完整业务流程(如发送文本、
发送文件、发布朋友圈)。每个 Flow 由若干 transition 组成,每个 transition
对应一个状态转换。
状态机9 个状态):
INITIAL → WINDOW_ACTIVATED → SEARCH_BOX_CLICKED → QUERY_TYPED →
SESSION_OPENED → INPUT_FOCUSED → TEXT_TYPED → SENT → CLEANED_UP
任何状态失败 → FAILED
异常处理FlowError / TimeoutError 时调 _reset_to_idle 恢复 UI 状态,
避免残留脏状态影响下次流程。
"""
from __future__ import annotations
import asyncio
import enum
import logging
import time
from dataclasses import dataclass, field
from typing import Optional
from woc_bridge.ui.backends.base import WindowGeometry
from woc_bridge.ui.errors import FlowError
logger = logging.getLogger("woc-bridge")
# 整流程超时(与 backends/xdotool.py 的 _FLOW_TIMEOUT_SEC 对齐)
_FLOW_TIMEOUT_SEC = 30.0
class FlowState(enum.Enum):
"""Flow 状态机 9 个状态。"""
INITIAL = "initial"
WINDOW_ACTIVATED = "window_activated"
SEARCH_BOX_CLICKED = "search_box_clicked"
QUERY_TYPED = "query_typed"
SESSION_OPENED = "session_opened"
INPUT_FOCUSED = "input_focused"
TEXT_TYPED = "text_typed"
SENT = "sent"
CLEANED_UP = "cleaned_up"
FAILED = "failed"
@dataclass
class FlowContext:
"""Flow 执行上下文(贯穿整个流程,各 transition 共享)。
所有字段均有默认值,避免构造时必填字段顺序问题。
"""
to_wxid: str = ""
content: str = ""
display_name: str = ""
client_request_id: str = ""
win_geom: Optional[WindowGeometry] = None
before_msg_id: int = 0
local_send_id: str = ""
verified: bool = False
image_confidence: float = 0.0
# 内部状态
current_state: FlowState = FlowState.INITIAL
started_at: float = field(default_factory=time.monotonic)
@dataclass
class FlowResult:
"""Flow 执行结果。"""
success: bool = False
local_id: str = ""
verified: bool = False
skipped: bool = False
error: str = ""
error_code: str = ""
state: FlowState = FlowState.INITIAL
image_confidence: float = 0.0
duration_ms: float = 0.0
details: Optional[dict] = None
class Flow:
"""L4 Flow 基类。
子类需实现 run(ctx) 方法,编排各 transition。
基类提供 _reset_to_idle / _verify_main_view / _full_cleanup_on_startup
等通用恢复与校验方法。
"""
def __init__(self, actions, db_reader=None) -> None:
"""初始化 Flow。
Args:
actions: L3 Actions 实例
db_reader: DB 读取器(可选,用于校验)
"""
self.actions = actions
self.db_reader = db_reader
# ------------------------------------------------------------------
# 状态恢复
# ------------------------------------------------------------------
async def _reset_to_idle(self, win_geom: Optional[WindowGeometry] = None) -> bool:
"""多策略清场Esc×3 + 点击空白 + BackSpace×30 + Esc×2 + 校验。
Flow 失败或启动时调用,确保 UI 回到干净状态。
不依赖 win_geom点击空白用固定坐标避免 win_geom=None 时崩溃)。
Args:
win_geom: 窗口几何(可选,用于 post_verify
Returns:
True 表示清场成功post_verify 通过False 表示可能仍残留脏状态
"""
try:
# 1. Esc×3 关闭可能的弹窗/搜索框
await self.actions.backend.key_press("Escape", repeat=3)
await asyncio.sleep(0.3)
# 2. 点击主视图空白区域(避免焦点在搜索框/输入框)
if win_geom is not None:
# 点击主视图中心区域
blank_x = win_geom.x + win_geom.width // 2
blank_y = win_geom.y + win_geom.height // 2
await self.actions.backend.click(blank_x, blank_y)
await asyncio.sleep(0.3)
# 3. BackSpace×30 清空可能残留的输入框内容
await self.actions.backend.key_press("BackSpace", repeat=30)
await asyncio.sleep(0.2)
# 4. Esc×2 再次关闭
await self.actions.backend.key_press("Escape", repeat=2)
await asyncio.sleep(0.2)
# 5. post_verify尝试用 main_view 模板校验
if win_geom is not None:
try:
elem = await self.actions.wait_for(
"main_view", win_geom, timeout=2.0, interval=0.3
)
if elem is not None:
logger.info("[flow] _reset_to_idle post_verify OK")
return True
logger.warning("[flow] _reset_to_idle post_verify: main_view not found")
return False
except Exception as exc:
logger.warning(
"[flow] _reset_to_idle post_verify exception: %s", exc
)
return False
logger.info("[flow] _reset_to_idle done (no post_verify, win_geom=None)")
return True
except Exception as exc:
logger.warning("[flow] _reset_to_idle exception: %s", exc)
return False
async def _verify_main_view(self, win_geom: WindowGeometry) -> bool:
"""校验当前是否在主视图(用 main_view 模板)。
Args:
win_geom: 窗口几何
Returns:
True 表示在主视图
"""
try:
elem = await self.actions.wait_for(
"main_view", win_geom, timeout=3.0, interval=0.3
)
return elem is not None
except Exception:
return False
async def _full_cleanup_on_startup(self) -> bool:
"""启动时全量清场3 次重试 _reset_to_idle。
上次崩溃可能残留脏状态(搜索框打开/输入框有内容),
启动时调用此方法确保干净起点。
Returns:
True 表示清场成功False 表示 3 次都失败(可能需人工 VNC 接入)
"""
for attempt in range(3):
logger.info(
"[flow] startup cleanup attempt %d/3", attempt + 1
)
try:
# 启动时无 win_geom用 None跳过 post_verify
ok = await self._reset_to_idle(win_geom=None)
if ok:
logger.info(
"[flow] startup cleanup OK at attempt %d", attempt + 1
)
return True
except Exception as exc:
logger.warning(
"[flow] startup cleanup attempt %d exception: %s",
attempt + 1, exc,
)
await asyncio.sleep(1.0)
logger.warning("[flow] startup cleanup failed 3 attempts")
return False
# ------------------------------------------------------------------
# 子类实现
# ------------------------------------------------------------------
async def run(self, ctx: FlowContext) -> FlowResult:
"""子类实现:编排各 transition返回 FlowResult。"""
raise NotImplementedError("subclass must implement run()")