2026-07-16 16:53:42 +08:00
|
|
|
|
"""窗口管理子 driver:窗口查找 / 激活 / 几何 / 截图 / 启动清场。
|
|
|
|
|
|
|
|
|
|
|
|
从 xdotool_driver.py 单类中拆出的窗口域方法,继承 BaseDriver 复用
|
|
|
|
|
|
_step / _parse_window_geometry / _click_at 公共能力。
|
|
|
|
|
|
|
|
|
|
|
|
方法清单(与原 XdotoolDriver 签名一致):
|
|
|
|
|
|
- find_wechat_window() -> Optional[int]
|
|
|
|
|
|
- activate_window(window_id=None) -> None
|
|
|
|
|
|
- get_window_geometry(window_id=None) -> WindowGeom (新增公开方法)
|
|
|
|
|
|
- screenshot() -> bytes (新增,对齐 backends 模式)
|
|
|
|
|
|
- _full_cleanup_on_startup() -> bool
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
|
|
import asyncio
|
|
|
|
|
|
import logging
|
2026-07-17 18:10:31 +08:00
|
|
|
|
import time
|
2026-07-16 16:53:42 +08:00
|
|
|
|
from typing import Optional
|
|
|
|
|
|
|
|
|
|
|
|
from woc_bridge.models import BridgeError
|
|
|
|
|
|
from woc_bridge.ui.drivers.base import BaseDriver, WindowGeom
|
|
|
|
|
|
|
2026-07-17 18:10:31 +08:00
|
|
|
|
|
2026-07-16 16:53:42 +08:00
|
|
|
|
logger = logging.getLogger("woc-bridge")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class WindowDriver(BaseDriver):
|
|
|
|
|
|
"""窗口管理子 driver。"""
|
|
|
|
|
|
|
|
|
|
|
|
def __init__(self, display: str, backend=None) -> None:
|
|
|
|
|
|
super().__init__(display, backend)
|
2026-07-17 18:10:31 +08:00
|
|
|
|
# 缓存最近一次找到的主窗口信息,用于抑制连续重复的 info 日志
|
|
|
|
|
|
self._last_window_key: Optional[tuple[int, int, int, int, int, int]] = None
|
2026-07-16 16:53:42 +08:00
|
|
|
|
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
|
|
|
|
# 命令执行包装(BaseDriver._run_managed 返回 tuple 且抛 TimeoutError,
|
|
|
|
|
|
# 这里包装为 BridgeError 以保持与原 XdotoolDriver._run 语义一致)
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
|
|
|
|
async def _run(
|
|
|
|
|
|
self,
|
|
|
|
|
|
args: list[str],
|
|
|
|
|
|
*,
|
|
|
|
|
|
input_bytes: Optional[bytes] = None,
|
|
|
|
|
|
) -> tuple[int, bytes, bytes]:
|
|
|
|
|
|
"""执行一条命令并返回 (returncode, stdout, stderr)。
|
|
|
|
|
|
|
|
|
|
|
|
委托 BaseDriver._run_managed(持 _ui_lock 串行 + L1 超时兜底),
|
|
|
|
|
|
TimeoutError 转 BridgeError(code=SEND_FAILED),保持原 _run 语义。
|
|
|
|
|
|
input_bytes 参数保留以兼容原签名,当前实现未使用(无方法依赖)。
|
|
|
|
|
|
"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
return await self._run_managed(args)
|
|
|
|
|
|
except asyncio.TimeoutError as exc:
|
|
|
|
|
|
logger.error("[ui] command timeout: %s", args[0] if args else "?")
|
|
|
|
|
|
raise BridgeError(
|
|
|
|
|
|
code="SEND_FAILED",
|
|
|
|
|
|
message=f"命令超时: {args[0] if args else '?'}",
|
|
|
|
|
|
) from exc
|
|
|
|
|
|
|
|
|
|
|
|
async def _key(self, key: str, repeat: int = 1) -> None:
|
|
|
|
|
|
"""执行 xdotool key [--repeat N] <key>。"""
|
|
|
|
|
|
logger.info("[ui] key: %s repeat=%d", key, repeat)
|
|
|
|
|
|
args = ["xdotool", "key"]
|
|
|
|
|
|
if repeat > 1:
|
|
|
|
|
|
args.extend(["--repeat", str(repeat)])
|
|
|
|
|
|
args.append(key)
|
|
|
|
|
|
rc, _, _ = await self._run(args)
|
|
|
|
|
|
logger.info("[ui] key %s -> rc=%s", key, rc)
|
|
|
|
|
|
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
|
|
|
|
# 窗口与进程检测
|
|
|
|
|
|
# ------------------------------------------------------------------
|
2026-07-17 18:10:31 +08:00
|
|
|
|
async def _find_wechat_pids(self) -> list[int]:
|
|
|
|
|
|
"""查找微信主进程 PID 列表。
|
|
|
|
|
|
|
|
|
|
|
|
微信可执行文件常见路径:
|
|
|
|
|
|
- /config/wechat/opt/wechat/wechat(安装后的实际路径)
|
|
|
|
|
|
- /opt/wechat/wechat
|
|
|
|
|
|
通过 pgrep 按命令行匹配,避免依赖固定路径。
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
PID 列表(可能为空)
|
|
|
|
|
|
"""
|
|
|
|
|
|
pids: list[int] = []
|
|
|
|
|
|
for pattern in ["wechat", "/config/wechat/opt/wechat"]:
|
|
|
|
|
|
rc, stdout, _ = await self._run(["pgrep", "-f", pattern])
|
|
|
|
|
|
if rc != 0:
|
|
|
|
|
|
continue
|
|
|
|
|
|
for line in stdout.decode(errors="ignore").splitlines():
|
|
|
|
|
|
line = line.strip()
|
|
|
|
|
|
if not line:
|
|
|
|
|
|
continue
|
|
|
|
|
|
try:
|
|
|
|
|
|
pids.append(int(line))
|
|
|
|
|
|
except ValueError:
|
|
|
|
|
|
continue
|
|
|
|
|
|
# 去重保持顺序
|
|
|
|
|
|
seen = set()
|
|
|
|
|
|
unique: list[int] = []
|
|
|
|
|
|
for pid in pids:
|
|
|
|
|
|
if pid not in seen:
|
|
|
|
|
|
seen.add(pid)
|
|
|
|
|
|
unique.append(pid)
|
|
|
|
|
|
return unique
|
|
|
|
|
|
|
2026-07-16 16:53:42 +08:00
|
|
|
|
async def find_wechat_window(self) -> Optional[int]:
|
2026-07-17 18:10:31 +08:00
|
|
|
|
"""查找微信主窗口 ID。
|
|
|
|
|
|
|
|
|
|
|
|
策略:
|
|
|
|
|
|
1. 优先通过 wechat 进程 PID 搜索窗口(最可靠,微信主窗口 name/class 可能为空);
|
|
|
|
|
|
2. 再用 name/class 搜索兜底;
|
|
|
|
|
|
3. 过滤掉面积极小的隐藏/装饰/加载窗口;
|
|
|
|
|
|
4. 多个候选时取面积最大者。
|
2026-07-16 16:53:42 +08:00
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
窗口 ID(int),找不到返回 None
|
|
|
|
|
|
"""
|
2026-07-17 18:10:31 +08:00
|
|
|
|
candidates: list[tuple[int, int, WindowGeom]] = []
|
|
|
|
|
|
seen_wids: set[int] = set()
|
|
|
|
|
|
|
|
|
|
|
|
async def add_candidate(wid: int) -> None:
|
|
|
|
|
|
"""获取窗口几何并加入候选(去重)。"""
|
|
|
|
|
|
if wid in seen_wids:
|
|
|
|
|
|
return
|
|
|
|
|
|
seen_wids.add(wid)
|
|
|
|
|
|
rc, gstdout, _ = await self._run(
|
|
|
|
|
|
["xdotool", "getwindowgeometry", "--shell", str(wid)]
|
|
|
|
|
|
)
|
|
|
|
|
|
if rc != 0:
|
|
|
|
|
|
return
|
|
|
|
|
|
geom = self._parse_window_geometry(gstdout.decode(errors="ignore"))
|
|
|
|
|
|
if geom is None:
|
|
|
|
|
|
return
|
|
|
|
|
|
area = geom.width * geom.height
|
|
|
|
|
|
# 过滤极小/无效的窗口
|
|
|
|
|
|
if area < 10000 or geom.width < 100 or geom.height < 100:
|
|
|
|
|
|
return
|
|
|
|
|
|
candidates.append((area, wid, geom))
|
|
|
|
|
|
|
|
|
|
|
|
# 1. 通过 wechat 进程 PID 搜索窗口(优先)
|
|
|
|
|
|
for pid in await self._find_wechat_pids():
|
|
|
|
|
|
rc, stdout, _ = await self._run(
|
|
|
|
|
|
["xdotool", "search", "--pid", str(pid)]
|
|
|
|
|
|
)
|
|
|
|
|
|
if rc != 0:
|
|
|
|
|
|
continue
|
|
|
|
|
|
for line in stdout.decode(errors="ignore").splitlines():
|
|
|
|
|
|
wid_str = line.strip()
|
|
|
|
|
|
if not wid_str:
|
|
|
|
|
|
continue
|
|
|
|
|
|
try:
|
|
|
|
|
|
await add_candidate(int(wid_str))
|
|
|
|
|
|
except ValueError:
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
# 2. name/class 兜底
|
|
|
|
|
|
searches = [
|
|
|
|
|
|
["xdotool", "search", "--onlyvisible", "--name", "微信"],
|
|
|
|
|
|
["xdotool", "search", "--onlyvisible", "--class", "wechat"],
|
|
|
|
|
|
["xdotool", "search", "--name", "微信"],
|
|
|
|
|
|
["xdotool", "search", "--class", "wechat"],
|
|
|
|
|
|
]
|
|
|
|
|
|
for args in searches:
|
|
|
|
|
|
returncode, stdout, _ = await self._run(args)
|
|
|
|
|
|
if returncode != 0:
|
|
|
|
|
|
continue
|
|
|
|
|
|
for line in stdout.decode(errors="ignore").splitlines():
|
|
|
|
|
|
wid_str = line.strip()
|
|
|
|
|
|
if not wid_str:
|
|
|
|
|
|
continue
|
|
|
|
|
|
try:
|
|
|
|
|
|
await add_candidate(int(wid_str))
|
|
|
|
|
|
except ValueError:
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
if not candidates:
|
|
|
|
|
|
if self._last_window_key is not None:
|
|
|
|
|
|
logger.warning("[window] 未找到有效微信主窗口")
|
|
|
|
|
|
self._last_window_key = None
|
|
|
|
|
|
else:
|
|
|
|
|
|
logger.debug("[window] 仍未找到有效微信主窗口")
|
2026-07-16 16:53:42 +08:00
|
|
|
|
return None
|
|
|
|
|
|
|
2026-07-17 18:10:31 +08:00
|
|
|
|
# 取面积最大者(微信主窗口通常远大于通知/托盘窗口)
|
|
|
|
|
|
candidates.sort(key=lambda x: x[0], reverse=True)
|
|
|
|
|
|
area, wid, geom = candidates[0]
|
|
|
|
|
|
key = (wid, geom.x, geom.y, geom.width, geom.height, area)
|
|
|
|
|
|
if key == self._last_window_key:
|
|
|
|
|
|
logger.debug(
|
|
|
|
|
|
"[window] 主窗口未变化 id=%d area=%d geom=(%d,%d %dx%d)",
|
|
|
|
|
|
wid, area, geom.x, geom.y, geom.width, geom.height,
|
|
|
|
|
|
)
|
|
|
|
|
|
else:
|
|
|
|
|
|
logger.info(
|
|
|
|
|
|
"[window] 选中微信主窗口 id=%d area=%d geom=(%d,%d %dx%d)",
|
|
|
|
|
|
wid, area, geom.x, geom.y, geom.width, geom.height,
|
|
|
|
|
|
)
|
|
|
|
|
|
self._last_window_key = key
|
|
|
|
|
|
return wid
|
|
|
|
|
|
|
2026-07-16 16:53:42 +08:00
|
|
|
|
async def activate_window(self, window_id: Optional[int] = None) -> None:
|
|
|
|
|
|
"""激活微信窗口。
|
|
|
|
|
|
|
2026-07-17 18:10:31 +08:00
|
|
|
|
使用非阻塞 windowactivate 避免 --sync 在 VNC 无人操作时死等,
|
|
|
|
|
|
之后轮询 getactivewindow 确认焦点是否真正落到目标窗口。
|
|
|
|
|
|
|
2026-07-16 16:53:42 +08:00
|
|
|
|
Args:
|
|
|
|
|
|
window_id: 指定窗口 ID;为 None 时自动查找
|
|
|
|
|
|
"""
|
|
|
|
|
|
if window_id is None:
|
|
|
|
|
|
window_id = await self.find_wechat_window()
|
|
|
|
|
|
if window_id is None:
|
|
|
|
|
|
raise BridgeError(
|
|
|
|
|
|
code="WINDOW_NOT_FOUND",
|
|
|
|
|
|
message="未找到微信窗口,无法激活",
|
|
|
|
|
|
)
|
2026-07-17 18:10:31 +08:00
|
|
|
|
await self._run(["xdotool", "windowactivate", str(window_id)])
|
|
|
|
|
|
|
|
|
|
|
|
deadline = time.monotonic() + 2.0
|
|
|
|
|
|
while time.monotonic() < deadline:
|
|
|
|
|
|
rc, stdout, _ = await self._run(["xdotool", "getactivewindow"])
|
|
|
|
|
|
if rc == 0:
|
|
|
|
|
|
active_text = stdout.decode(errors="ignore").strip()
|
|
|
|
|
|
try:
|
|
|
|
|
|
if int(active_text) == window_id:
|
|
|
|
|
|
return
|
|
|
|
|
|
except ValueError:
|
|
|
|
|
|
pass
|
|
|
|
|
|
await asyncio.sleep(0.1)
|
|
|
|
|
|
logger.warning(
|
|
|
|
|
|
"[window] activate_window: focus verify timeout id=%d, continue",
|
|
|
|
|
|
window_id,
|
|
|
|
|
|
)
|
2026-07-16 16:53:42 +08:00
|
|
|
|
|
|
|
|
|
|
async def get_window_geometry(
|
|
|
|
|
|
self, window_id: Optional[int] = None
|
|
|
|
|
|
) -> WindowGeom:
|
|
|
|
|
|
"""获取微信窗口几何信息,返回 WindowGeom。
|
|
|
|
|
|
|
|
|
|
|
|
新增公开方法:原 XdotoolDriver 仅有私有 _get_window_geometry(返回
|
|
|
|
|
|
tuple),本方法对外暴露 WindowGeom 并复用 BaseDriver._parse_window_geometry
|
|
|
|
|
|
消除内联解析重复。
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
window_id: 指定窗口 ID;为 None 时自动查找
|
|
|
|
|
|
|
|
|
|
|
|
Raises:
|
|
|
|
|
|
BridgeError(WINDOW_NOT_FOUND): 找不到微信窗口
|
|
|
|
|
|
BridgeError(SEND_FAILED): 获取/解析几何信息失败
|
|
|
|
|
|
"""
|
|
|
|
|
|
if window_id is None:
|
|
|
|
|
|
window_id = await self.find_wechat_window()
|
|
|
|
|
|
if window_id is None:
|
|
|
|
|
|
raise BridgeError(
|
|
|
|
|
|
code="WINDOW_NOT_FOUND",
|
|
|
|
|
|
message="未找到微信窗口",
|
|
|
|
|
|
)
|
|
|
|
|
|
returncode, stdout, _ = await self._run(
|
|
|
|
|
|
["xdotool", "getwindowgeometry", "--shell", str(window_id)]
|
|
|
|
|
|
)
|
|
|
|
|
|
if returncode != 0:
|
|
|
|
|
|
raise BridgeError(
|
|
|
|
|
|
code="SEND_FAILED",
|
|
|
|
|
|
message="无法获取微信窗口几何信息",
|
|
|
|
|
|
)
|
|
|
|
|
|
geom = self._parse_window_geometry(stdout.decode(errors="ignore"))
|
|
|
|
|
|
if geom is None:
|
|
|
|
|
|
raise BridgeError(
|
|
|
|
|
|
code="SEND_FAILED",
|
|
|
|
|
|
message="解析窗口几何信息失败",
|
|
|
|
|
|
)
|
|
|
|
|
|
return geom
|
|
|
|
|
|
|
|
|
|
|
|
async def screenshot(self) -> bytes:
|
|
|
|
|
|
"""截取整个 X 屏幕为 PNG bytes。
|
|
|
|
|
|
|
|
|
|
|
|
新增方法:原 XdotoolDriver 无 screenshot,仅 _debug_screenshot 写盘。
|
|
|
|
|
|
本方法对齐 woc_bridge.ui.backends.xdotool.XdotoolBackend.screenshot 的
|
|
|
|
|
|
scrot -o - 模式,输出 PNG bytes 到内存,不落盘。
|
|
|
|
|
|
"""
|
|
|
|
|
|
async with self._ui_lock:
|
|
|
|
|
|
proc = await asyncio.create_subprocess_exec(
|
|
|
|
|
|
"scrot",
|
|
|
|
|
|
"-o",
|
|
|
|
|
|
"-",
|
|
|
|
|
|
stdout=asyncio.subprocess.PIPE,
|
|
|
|
|
|
stderr=asyncio.subprocess.PIPE,
|
|
|
|
|
|
env=self._env(),
|
|
|
|
|
|
)
|
|
|
|
|
|
try:
|
|
|
|
|
|
stdout, stderr = await asyncio.wait_for(
|
|
|
|
|
|
proc.communicate(), timeout=5.0
|
|
|
|
|
|
)
|
|
|
|
|
|
except (asyncio.TimeoutError, asyncio.CancelledError):
|
|
|
|
|
|
proc.kill()
|
|
|
|
|
|
try:
|
|
|
|
|
|
await asyncio.wait_for(proc.wait(), timeout=5.0)
|
|
|
|
|
|
except asyncio.TimeoutError:
|
|
|
|
|
|
logger.error("[ui-driver] scrot proc.wait() timeout after kill")
|
|
|
|
|
|
raise
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
if proc.returncode is None:
|
|
|
|
|
|
proc.kill()
|
|
|
|
|
|
try:
|
|
|
|
|
|
await asyncio.wait_for(proc.wait(), timeout=5.0)
|
|
|
|
|
|
except asyncio.TimeoutError:
|
|
|
|
|
|
logger.error("[ui-driver] scrot proc.wait() timeout after kill")
|
|
|
|
|
|
raise
|
|
|
|
|
|
if proc.returncode != 0:
|
|
|
|
|
|
err = stderr.decode(errors="ignore").strip() if stderr else "unknown"
|
|
|
|
|
|
raise BridgeError(
|
|
|
|
|
|
code="SEND_FAILED",
|
|
|
|
|
|
message=f"截图失败: {err}",
|
|
|
|
|
|
)
|
|
|
|
|
|
return stdout
|
|
|
|
|
|
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
|
|
|
|
# 内部几何工具
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
|
|
|
|
async def _get_window_geometry(self) -> tuple[int, int, int, int]:
|
|
|
|
|
|
"""获取微信窗口几何信息,返回 (win_x, win_y, win_w, win_h)。
|
|
|
|
|
|
|
|
|
|
|
|
统一封装 getwindowgeometry --shell 解析逻辑,复用
|
|
|
|
|
|
BaseDriver._parse_window_geometry 消除重复代码。
|
|
|
|
|
|
"""
|
|
|
|
|
|
window_id = await self.find_wechat_window()
|
|
|
|
|
|
if window_id is None:
|
|
|
|
|
|
raise BridgeError(
|
|
|
|
|
|
code="WINDOW_NOT_FOUND",
|
|
|
|
|
|
message="未找到微信窗口",
|
|
|
|
|
|
)
|
|
|
|
|
|
returncode, stdout, _ = await self._run(
|
|
|
|
|
|
["xdotool", "getwindowgeometry", "--shell", str(window_id)]
|
|
|
|
|
|
)
|
|
|
|
|
|
if returncode != 0:
|
|
|
|
|
|
raise BridgeError(
|
|
|
|
|
|
code="SEND_FAILED",
|
|
|
|
|
|
message="无法获取微信窗口几何信息",
|
|
|
|
|
|
)
|
|
|
|
|
|
geom = self._parse_window_geometry(stdout.decode(errors="ignore"))
|
|
|
|
|
|
if geom is None:
|
|
|
|
|
|
raise BridgeError(
|
|
|
|
|
|
code="SEND_FAILED",
|
|
|
|
|
|
message="解析窗口几何信息失败",
|
|
|
|
|
|
)
|
|
|
|
|
|
return (geom.x, geom.y, geom.width, geom.height)
|
|
|
|
|
|
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
|
|
|
|
# 启动清场(P1 高可用修复)
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
|
|
|
|
async def _full_cleanup_on_startup(self) -> bool:
|
|
|
|
|
|
"""启动时全量清场(3 次重试)。
|
|
|
|
|
|
|
|
|
|
|
|
上次崩溃可能残留脏状态(搜索框未关、弹窗未处理、焦点错位等),
|
|
|
|
|
|
直接进入发送流程会放大故障。连续重置 3 次,仍失败则告警但不阻塞
|
|
|
|
|
|
启动(返回 False,由调用方决定是否告警)。
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
True 表示清场成功(或窗口未找到直接跳过);False 表示 3 次重置均失败,
|
|
|
|
|
|
可能需要人工 VNC 接入排查。
|
|
|
|
|
|
"""
|
|
|
|
|
|
# 1. 获取窗口几何;窗口未找到时跳过清场(不阻塞启动)
|
|
|
|
|
|
try:
|
|
|
|
|
|
win_geom = await self._get_window_geometry()
|
|
|
|
|
|
except BridgeError as e:
|
|
|
|
|
|
logger.warning("启动清场: 微信窗口未找到,跳过 (%s)", e.message)
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
# 2. 连续重置 3 次
|
|
|
|
|
|
for attempt in range(3):
|
|
|
|
|
|
if await self._reset_to_idle(win_geom):
|
|
|
|
|
|
logger.info("启动清场成功 (attempt=%d)", attempt + 1)
|
|
|
|
|
|
return True
|
|
|
|
|
|
logger.warning("启动清场失败 (attempt=%d)", attempt + 1)
|
|
|
|
|
|
|
|
|
|
|
|
# 3. 3 次均失败,告警但不抛异常(不阻塞启动)
|
|
|
|
|
|
logger.error("启动清场 3 次失败,可能需要人工 VNC 接入")
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
async def _reset_to_idle(self, win_geom: tuple[int, int, int, int]) -> bool:
|
|
|
|
|
|
"""安全重置到 IDLE 状态(幂等,失败不抛异常)。
|
|
|
|
|
|
|
|
|
|
|
|
多策略清场,逐层剥离可能残留的脏状态:
|
|
|
|
|
|
1. Esc×3:关闭弹窗/搜索覆盖层
|
|
|
|
|
|
2. 点击窗口左上角空白区域:退 webview 焦点,避免焦点停在输入框
|
|
|
|
|
|
3. BackSpace×30:清空可能残留的输入框内容
|
|
|
|
|
|
4. Esc×2:兜底关闭剩余弹窗
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
win_geom: _get_window_geometry 返回的 (win_x, win_y, win_w, win_h)
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
True 表示重置流程执行完成;False 表示中途异常(需重试或人工介入)
|
|
|
|
|
|
"""
|
|
|
|
|
|
win_x, win_y, _win_w, _win_h = win_geom
|
|
|
|
|
|
try:
|
|
|
|
|
|
# 1. Esc×3 关闭弹窗/搜索覆盖层
|
|
|
|
|
|
for _ in range(3):
|
|
|
|
|
|
await self._key("Escape")
|
|
|
|
|
|
await asyncio.sleep(0.3)
|
|
|
|
|
|
# 2. 点击窗口左上角空白区域,退出 webview 焦点(链式命令点击)
|
|
|
|
|
|
await self._click_at(win_x + 10, win_y + 10)
|
|
|
|
|
|
await asyncio.sleep(0.3)
|
|
|
|
|
|
# 3. BackSpace×30 清空可能残留的输入框内容
|
|
|
|
|
|
await self._key("BackSpace", repeat=30)
|
|
|
|
|
|
await asyncio.sleep(0.3)
|
|
|
|
|
|
# 4. Esc×2 兜底关闭剩余弹窗
|
|
|
|
|
|
for _ in range(2):
|
|
|
|
|
|
await self._key("Escape")
|
|
|
|
|
|
await asyncio.sleep(0.3)
|
|
|
|
|
|
return True
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error("_reset_to_idle 异常: %r", e)
|
|
|
|
|
|
return False
|