Files
workphone-sdk/sdk/app/services/wechat_nav_cache.py
Manus AI 5919659930 feat: 控制台 WS 全量检测 · APK 桌面「工作手机」入口
nav_cache 模块单例;index 增加 Hub 链接与 hook 探测;SetupActivity 注册 Launcher。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-24 09:19:12 +08:00

443 lines
17 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.

"""
微信 UI 导航缓存系统 v1.0
—— 记录所有页面路径、元素坐标,避免重复探索,支持自动纠错
核心理念:
1. 首次探索时录制页面结构到 JSON持久化
2. 后续操作直接查缓存,跳过 uiautomator dump
3. 缓存失效(点击后页面不符预期)自动触发重新探索
4. 每个 action 的操作步骤作为 "路径模板" 存储
"""
import json
import time
import os
import re
import logging
from typing import Dict, Any, List, Optional, Tuple
from pathlib import Path
logger = logging.getLogger(__name__)
CACHE_DIR = Path(__file__).parent.parent.parent / "data" / "nav_cache"
LOG_DIR = Path(__file__).parent.parent.parent / "data" / "operation_logs"
class PageSignature:
"""页面特征签名——用于快速验证当前在哪个页面"""
def __init__(self, name: str, must_have: List[str] = None,
must_not_have: List[str] = None, title: str = None):
self.name = name
self.must_have = must_have or []
self.must_not_have = must_not_have or []
self.title = title
def match(self, texts: List[str]) -> bool:
text_set = set(texts)
for t in self.must_have:
if t not in text_set:
return False
for t in self.must_not_have:
if t in text_set:
return False
if self.title and self.title not in text_set:
return False
return True
def to_dict(self):
return {
"name": self.name,
"must_have": self.must_have,
"must_not_have": self.must_not_have,
"title": self.title,
}
@classmethod
def from_dict(cls, d):
return cls(d["name"], d.get("must_have", []),
d.get("must_not_have", []), d.get("title"))
class NavStep:
"""单步导航操作"""
def __init__(self, action: str, target: str = None,
coords: List[int] = None, wait: float = 1.0,
verify_page: str = None, fallback: str = None):
self.action = action # tap_text / tap_coord / tap_tab / scroll_up / scroll_down / back / swipe
self.target = target # 文本目标 or tab名
self.coords = coords # [x, y] 精确坐标(缓存命中时用)
self.wait = wait
self.verify_page = verify_page # 期望到达的页面签名名
self.fallback = fallback # 失败后备用动作
def to_dict(self):
return {k: v for k, v in {
"action": self.action, "target": self.target,
"coords": self.coords, "wait": self.wait,
"verify_page": self.verify_page, "fallback": self.fallback,
}.items() if v is not None}
@classmethod
def from_dict(cls, d):
return cls(**d)
class NavPath:
"""一条完整的导航路径(从微信首页到目标页面)"""
def __init__(self, name: str, steps: List[NavStep] = None,
target_page: str = None):
self.name = name
self.steps = steps or []
self.target_page = target_page
self.success_count = 0
self.fail_count = 0
self.last_success = None
def to_dict(self):
return {
"name": self.name,
"steps": [s.to_dict() for s in self.steps],
"target_page": self.target_page,
"success_count": self.success_count,
"fail_count": self.fail_count,
"last_success": self.last_success,
}
@classmethod
def from_dict(cls, d):
p = cls(d["name"], [NavStep.from_dict(s) for s in d.get("steps", [])],
d.get("target_page"))
p.success_count = d.get("success_count", 0)
p.fail_count = d.get("fail_count", 0)
p.last_success = d.get("last_success")
return p
class WeChatNavCache:
"""导航缓存管理器"""
def __init__(self, wechat_version: str = "8.0.56",
screen_w: int = 1080, screen_h: int = 2400):
self.version = wechat_version
self.screen_w = screen_w
self.screen_h = screen_h
CACHE_DIR.mkdir(parents=True, exist_ok=True)
self.cache_file = CACHE_DIR / f"nav_v{wechat_version}_{screen_w}x{screen_h}.json"
self.pages: Dict[str, Dict] = {}
self.paths: Dict[str, NavPath] = {}
self.signatures: Dict[str, PageSignature] = {}
self.element_cache: Dict[str, Dict[str, Any]] = {}
self._load()
if not self.pages:
self._init_builtin()
def _load(self):
if self.cache_file.exists():
try:
data = json.loads(self.cache_file.read_text(encoding="utf-8"))
for name, page in data.get("pages", {}).items():
self.pages[name] = page
for name, path in data.get("paths", {}).items():
self.paths[name] = NavPath.from_dict(path)
for name, sig in data.get("signatures", {}).items():
self.signatures[name] = PageSignature.from_dict(sig)
self.element_cache = data.get("element_cache", {})
logger.info(f"导航缓存已加载: {len(self.pages)} 页面, "
f"{len(self.paths)} 路径, {len(self.element_cache)} 元素")
except Exception as e:
logger.warning(f"加载缓存失败: {e}")
def save(self):
data = {
"version": self.version,
"screen": f"{self.screen_w}x{self.screen_h}",
"updated": time.strftime("%Y-%m-%d %H:%M:%S"),
"pages": self.pages,
"paths": {k: v.to_dict() for k, v in self.paths.items()},
"signatures": {k: v.to_dict() for k, v in self.signatures.items()},
"element_cache": self.element_cache,
}
self.cache_file.write_text(
json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
def _init_builtin(self):
"""初始化内置的已知页面数据(基于真机录制 2026-03-14"""
# ─── 底部 Tab 坐标(精确录制) ───
tab_y = 2148
self.element_cache["tabs"] = {
"微信": {"center": [135, tab_y], "bounds": [0, 2119, 270, 2176]},
"通讯录": {"center": [405, tab_y], "bounds": [270, 2119, 540, 2176]},
"发现": {"center": [675, tab_y], "bounds": [540, 2119, 810, 2176]},
"": {"center": [945, tab_y], "bounds": [810, 2119, 1080, 2176]},
}
# ─── 页面特征签名 ───
sigs = {
"home": PageSignature("home", must_have=[], title=None),
"me": PageSignature("me", must_have=["服务", "收藏", "设置"]),
"settings": PageSignature("settings", must_have=["账号与安全"],
title="设置"),
"account_security": PageSignature(
"account_security", must_have=["微信号", "手机号", "微信密码"],
title="账号与安全"),
"safety_center": PageSignature(
"safety_center", must_have=["微信安全中心"]),
"contacts": PageSignature("contacts", must_have=["新的朋友"]),
"discover": PageSignature("discover", must_have=["朋友圈", "视频号"]),
}
for name, sig in sigs.items():
self.signatures[name] = sig
# ─── "我"页面元素坐标(精确录制) ───
self.pages["me"] = {
"recorded_at": "2026-03-14 16:32",
"elements": {
"头像区": {"center": [608, 298]},
"微信号": {"center": [643, 398], "text": "微信号Lytiao1"},
"状态": {"center": [379, 507]},
"3个朋友": {"center": [676, 507]},
"服务": {"center": [197, 706]},
"收藏": {"center": [197, 883]},
"朋友圈": {"center": [220, 1037]},
"视频号": {"center": [220, 1191]},
"订单与卡包": {"center": [266, 1345]},
"表情": {"center": [197, 1499]},
"设置": {"center": [197, 1676]},
},
}
# ─── 设置页面元素坐标 ───
self.pages["settings"] = {
"recorded_at": "2026-03-14 16:32",
"needs_scroll_to_top": True,
"elements": {
"账号与安全": {"center": [250, 300], "note": "顶部滑后第一项"},
"青少年模式": {"center": [250, 479]},
"关怀模式": {"center": [158, 631]},
"新消息通知": {"center": [204, 800]},
"聊天": {"center": [112, 958]},
"通用": {"center": [158, 1112]},
"隐私": {"center": [89, 1257], "section_header": True},
"朋友权限": {"center": [112, 1423]},
"个人信息与权限": {"center": [135, 1574]},
"个人信息收集清单": {"center": [158, 1725]},
"第三方信息共享清单": {"center": [181, 1876]},
"插件": {"center": [89, 2018]},
"关于微信": {"center": [89, 2168]},
},
}
# ─── 账号与安全页面 ───
self.pages["account_security"] = {
"recorded_at": "2026-03-14 16:33",
"elements": {
"微信号": {"center": [112, 300], "value_center": [603, 300]},
"手机号": {"center": [112, 451], "value_center": [603, 451]},
"微信密码": {"center": [135, 625]},
"声音锁": {"center": [112, 778]},
"应急联系人": {"center": [158, 953]},
"登录过的设备": {"center": [181, 1104]},
"更多安全设置": {"center": [181, 1257]},
"微信安全中心": {"center": [513, 1423]},
},
}
# ─── 常用导航路径 ───
self.paths["to_me"] = NavPath("to_me", [
NavStep("tap_tab", "", [945, tab_y], 1.5, "me"),
], "me")
self.paths["to_settings"] = NavPath("to_settings", [
NavStep("tap_tab", "", [945, tab_y], 1.5, "me"),
NavStep("scroll_up", None, None, 0.5),
NavStep("tap_text", "设置", [197, 1676], 2.0, "settings"),
NavStep("scroll_to_top", None, None, 0.5),
], "settings")
self.paths["to_account_security"] = NavPath("to_account_security", [
NavStep("tap_tab", "", [945, tab_y], 1.5, "me"),
NavStep("scroll_up", None, None, 0.5),
NavStep("tap_text", "设置", [197, 1676], 2.0, "settings"),
NavStep("scroll_to_top", None, None, 0.5),
NavStep("tap_text", "账号与安全", [250, 300], 2.0, "account_security"),
], "account_security")
self.paths["to_safety_center"] = NavPath("to_safety_center", [
NavStep("tap_tab", "", [945, tab_y], 1.5, "me"),
NavStep("scroll_up", None, None, 0.5),
NavStep("tap_text", "设置", [197, 1676], 2.0, "settings"),
NavStep("scroll_to_top", None, None, 0.5),
NavStep("tap_text", "账号与安全", [250, 300], 2.0, "account_security"),
NavStep("tap_text", "微信安全中心", [513, 1423], 3.0, "safety_center"),
], "safety_center")
self.paths["to_discover"] = NavPath("to_discover", [
NavStep("tap_tab", "发现", [675, tab_y], 1.5, "discover"),
], "discover")
self.paths["to_contacts"] = NavPath("to_contacts", [
NavStep("tap_tab", "通讯录", [405, tab_y], 1.5, "contacts"),
], "contacts")
self.save()
logger.info("内置导航缓存已初始化并保存")
def get_tab_coord(self, tab_name: str) -> Optional[List[int]]:
tabs = self.element_cache.get("tabs", {})
if tab_name in tabs:
return tabs[tab_name]["center"]
return None
def get_element_coord(self, page: str, element: str) -> Optional[List[int]]:
page_data = self.pages.get(page, {})
elements = page_data.get("elements", {})
if element in elements:
return elements[element].get("center")
return None
def get_path(self, path_name: str) -> Optional[NavPath]:
return self.paths.get(path_name)
def record_page(self, page_name: str, elements: Dict[str, Dict]):
"""录制/更新一个页面的元素数据"""
self.pages[page_name] = {
"recorded_at": time.strftime("%Y-%m-%d %H:%M"),
"elements": elements,
}
self.save()
logger.info(f"页面 [{page_name}] 已录制: {len(elements)} 个元素")
def record_path_result(self, path_name: str, success: bool):
"""记录路径执行结果"""
path = self.paths.get(path_name)
if path:
if success:
path.success_count += 1
path.last_success = time.strftime("%Y-%m-%d %H:%M:%S")
else:
path.fail_count += 1
self.save()
def update_element_coord(self, page: str, element: str,
new_center: List[int]):
"""自动纠错:更新元素坐标"""
if page not in self.pages:
self.pages[page] = {"elements": {}}
if "elements" not in self.pages[page]:
self.pages[page]["elements"] = {}
if element not in self.pages[page]["elements"]:
self.pages[page]["elements"][element] = {}
old = self.pages[page]["elements"][element].get("center")
self.pages[page]["elements"][element]["center"] = new_center
self.pages[page]["elements"][element]["updated_at"] = \
time.strftime("%Y-%m-%d %H:%M")
self.save()
logger.info(f"坐标纠错: [{page}].{element} {old} -> {new_center}")
class OperationLogger:
"""操作日志记录器——每次操作自动记录步骤/耗时/结果"""
def __init__(self, device_id: str = ""):
LOG_DIR.mkdir(parents=True, exist_ok=True)
self.device_id = device_id
self.log_file = LOG_DIR / f"ops_{time.strftime('%Y%m%d')}.jsonl"
self._current_op = None
def start_operation(self, action: str, params: dict):
self._current_op = {
"action": action,
"params": {k: v for k, v in params.items()
if k not in ("password", "pwd", "new_pwd", "old_pwd")},
"device": self.device_id,
"started_at": time.strftime("%Y-%m-%d %H:%M:%S"),
"start_ts": time.time(),
"steps": [],
}
def log_step(self, step_type: str, detail: str = "",
coords: List[int] = None, success: bool = True):
if not self._current_op:
return
self._current_op["steps"].append({
"type": step_type,
"detail": detail,
"coords": coords,
"success": success,
"ts": round(time.time() - self._current_op["start_ts"], 2),
})
def end_operation(self, success: bool, result: dict = None,
error: str = None):
if not self._current_op:
return
self._current_op["success"] = success
self._current_op["duration_ms"] = round(
(time.time() - self._current_op["start_ts"]) * 1000)
self._current_op["ended_at"] = time.strftime("%Y-%m-%d %H:%M:%S")
if result:
self._current_op["result_summary"] = str(result)[:200]
if error:
self._current_op["error"] = error
del self._current_op["start_ts"]
try:
with open(self.log_file, "a", encoding="utf-8") as f:
f.write(json.dumps(self._current_op, ensure_ascii=False) + "\n")
except Exception as e:
logger.warning(f"写入操作日志失败: {e}")
op = self._current_op
self._current_op = None
return op
def get_recent_ops(self, limit: int = 20) -> List[dict]:
"""读取最近的操作记录"""
if not self.log_file.exists():
return []
ops = []
try:
with open(self.log_file, "r", encoding="utf-8") as f:
for line in f:
line = line.strip()
if line:
ops.append(json.loads(line))
except Exception:
pass
return ops[-limit:]
def get_action_stats(self) -> Dict[str, Dict]:
"""统计每个 action 的成功率和平均耗时"""
ops = self.get_recent_ops(1000)
stats: Dict[str, Dict] = {}
for op in ops:
action = op.get("action", "unknown")
if action not in stats:
stats[action] = {"total": 0, "success": 0,
"total_ms": 0, "errors": []}
stats[action]["total"] += 1
if op.get("success"):
stats[action]["success"] += 1
stats[action]["total_ms"] += op.get("duration_ms", 0)
if op.get("error"):
stats[action]["errors"].append(op["error"][:50])
for action, s in stats.items():
s["success_rate"] = (
f"{s['success']/s['total']*100:.0f}%" if s["total"] else "N/A")
s["avg_ms"] = (
s["total_ms"] // s["total"] if s["total"] else 0)
return stats
# 模块级单例,供 wechat_adb_engine 等导入
nav_cache = WeChatNavCache()