feat: publish workphone SDK deployment and API docs
This commit is contained in:
428
sdk/agent/skills/connection_guard.py
Normal file
428
sdk/agent/skills/connection_guard.py
Normal file
@@ -0,0 +1,428 @@
|
||||
"""
|
||||
连接守护技能 - 确保设备与服务器始终保持连接
|
||||
|
||||
职责:
|
||||
1. 自动处理系统弹窗(USB调试授权、权限请求、系统更新提示等)
|
||||
2. 监控 ADB / WiFi / 网络连通性
|
||||
3. 检测并恢复 uiautomator2 服务
|
||||
4. 检测屏幕状态,必要时亮屏
|
||||
5. 与 Hawk 网络层协同做断网恢复
|
||||
|
||||
这是一个「常驻」技能,Agent 启动后以后台循环方式持续运行。
|
||||
"""
|
||||
|
||||
import logging
|
||||
import time
|
||||
import re
|
||||
from typing import Dict, Any, List, Optional
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
_agent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
if _agent_dir not in sys.path:
|
||||
sys.path.insert(0, _agent_dir)
|
||||
|
||||
try:
|
||||
from skills.base import BaseSkill
|
||||
except ImportError:
|
||||
from ..base import BaseSkill
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# 弹窗规则库 — 覆盖主流安卓品牌:小米/华为/OPPO/vivo/三星/一加/荣耀/realme
|
||||
#
|
||||
# 每条规则: detect = 界面关键词(正则), click = 要点击的按钮文案
|
||||
# 规则按优先级排列,匹配到即执行,不会重复点击
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
_POPUP_DISMISS_RULES: List[Dict[str, Any]] = [
|
||||
|
||||
# ─── 1. USB 调试授权(所有品牌) ───
|
||||
{"detect": [
|
||||
"USB调试", "USB debugging", "允许USB调试", "Allow USB debugging",
|
||||
"USB 偵錯", # 繁体
|
||||
"USB调试(安全设置)", # MIUI 特有
|
||||
"RSA.*密钥指纹", "RSA.*key fingerprint", # 弹窗正文
|
||||
"允许通过USB调试", # 华为
|
||||
"이 컴퓨터에서 USB 디버깅", # 三星韩语
|
||||
],
|
||||
"click": [
|
||||
"确定", "允许", "OK", "Allow", "始终允许", "Always allow",
|
||||
"확인", # 三星韩语
|
||||
"允許", # 繁体
|
||||
]},
|
||||
|
||||
# ─── 1b. USB 连接模式(仅充电 → 文件传输,防 MIUI 间歇掉 ADB) ───
|
||||
{"detect": [
|
||||
"USB 用于", "USB用途", "USB 用途", "连接方式",
|
||||
"仅充电", "充电模式", "Charging only", "Charge only",
|
||||
"USB 偏好设置", "Use USB for",
|
||||
"传输文件", "文件传输", "Transfer files",
|
||||
],
|
||||
"click": [
|
||||
"传输文件", "文件传输", "Transfer files", "File transfer",
|
||||
"传输照片", "Photo transfer", "MTP",
|
||||
]},
|
||||
|
||||
# ─── 2. MIUI 安全弹窗(小米/红米特有) ───
|
||||
{"detect": [
|
||||
"安全警告", "此操作具有风险",
|
||||
"模拟点击", "模拟输入", # MIUI「检测到模拟操作」
|
||||
"检测到.*模拟", "监控.*辅助",
|
||||
"开发者选项.*安全", "安全设置已关闭",
|
||||
],
|
||||
"click": ["确定", "继续", "我已知晓", "允许", "仍然允许"]},
|
||||
|
||||
# ─── 3. 华为/荣耀 特有弹窗 ───
|
||||
{"detect": [
|
||||
"允许HiSuite通过", "HiSuite",
|
||||
"仅充电", "传输文件", "传输照片", # USB 模式选择
|
||||
"华为手机助手",
|
||||
"应用.*未通过安全检测", # 华为安全中心
|
||||
"可能会损坏您的设备",
|
||||
],
|
||||
"click": ["允许", "继续安装", "我已知晓", "确定", "仍然安装"]},
|
||||
|
||||
# ─── 4. OPPO/realme/一加 特有弹窗 ───
|
||||
{"detect": [
|
||||
"ColorOS", "USB 传输",
|
||||
"允许.*调试", "您要允许.*USB调试",
|
||||
"开发者模式.*已开启",
|
||||
"USB用途", "连接方式",
|
||||
"充电模式.*传输", # OPPO USB 模式选择
|
||||
],
|
||||
"click": ["确定", "允许", "OK", "传输文件"]},
|
||||
|
||||
# ─── 5. vivo/iQOO 特有弹窗 ───
|
||||
{"detect": [
|
||||
"Funtouch", "OriginOS",
|
||||
"是否允许USB调试",
|
||||
"开启USB调试.*安全风险",
|
||||
"vivo.*安全",
|
||||
],
|
||||
"click": ["确定", "允许", "OK", "已了解风险"]},
|
||||
|
||||
# ─── 6. 三星 特有弹窗 ───
|
||||
{"detect": [
|
||||
"One UI", "Samsung",
|
||||
"您是否要允许USB调试", "Allow USB debugging",
|
||||
"Knox.*安全",
|
||||
"设备保护",
|
||||
],
|
||||
"click": ["确定", "允许", "OK", "Allow"]},
|
||||
|
||||
# ─── 7. ADB 通用权限 ───
|
||||
{"detect": ["允许通过", "ADB", "adb"],
|
||||
"click": ["确定", "允许", "OK"]},
|
||||
|
||||
# ─── 8. 安装应用(所有品牌) ───
|
||||
{"detect": [
|
||||
"要安装", "允许安装", "安装应用",
|
||||
"安装未知应用", "Install unknown apps",
|
||||
"来源不明", "Unknown sources",
|
||||
"是否安装该应用", "Install this app",
|
||||
"外部来源", "第三方来源",
|
||||
"继续安装.*可能有风险", # 各品牌安全提示
|
||||
"仍要安装",
|
||||
],
|
||||
"click": [
|
||||
"继续安装", "允许", "安装", "Install",
|
||||
"仍然安装", "仍要安装", "我已了解",
|
||||
]},
|
||||
|
||||
# ─── 9. 权限请求弹窗(通用) ───
|
||||
{"detect": [
|
||||
"允许.*访问", "允许.*使用", "Allow.*access",
|
||||
"请求.*权限", "needs.*permission",
|
||||
"允许.*读取", "允许.*写入",
|
||||
"允许.*拍照", "允许.*录音",
|
||||
"允许.*定位", "Allow.*location",
|
||||
"允许.*通知", "Allow.*notification",
|
||||
"允许.*悬浮窗", "允许.*显示在其他应用",
|
||||
"Display over other apps",
|
||||
"允许.*存储", "Allow.*storage",
|
||||
],
|
||||
"click": [
|
||||
"允许", "始终允许", "Allow", "While using the app",
|
||||
"仅在使用时允许", "仅使用期间",
|
||||
"始终", "Always",
|
||||
]},
|
||||
|
||||
# ─── 10. 系统更新/升级 ───
|
||||
{"detect": [
|
||||
"系统更新", "System update", "升级",
|
||||
"发现新版本", "New version",
|
||||
"现在更新", "Update now",
|
||||
"MIUI.*更新", "EMUI.*更新", "ColorOS.*更新",
|
||||
"OTA.*更新",
|
||||
],
|
||||
"click": ["稍后", "以后", "取消", "Later", "Cancel", "Not now", "稍后再说"]},
|
||||
|
||||
# ─── 11. 电池优化(各品牌措辞不同) ───
|
||||
{"detect": [
|
||||
"电池优化", "Battery optimization", "忽略电池优化",
|
||||
"省电策略", "后台耗电", "电量管理",
|
||||
"自启动管理", "自启动",
|
||||
"是否允许.*后台运行",
|
||||
"关闭.*省电",
|
||||
"Ignore battery optimizations",
|
||||
"不再受限", # 华为
|
||||
],
|
||||
"click": ["允许", "是", "Yes", "Allow", "不受限制", "确定"]},
|
||||
|
||||
# ─── 12. 后台运行保活 ───
|
||||
{"detect": [
|
||||
"后台运行", "后台活动", "Background",
|
||||
"锁定.*后台", "保持运行",
|
||||
"允许.*自启动", "允许.*后台",
|
||||
"关闭后台清理",
|
||||
"电池管理.*允许",
|
||||
],
|
||||
"click": ["允许", "Allow", "确定", "开启"]},
|
||||
|
||||
# ─── 13. 通知权限 ───
|
||||
{"detect": [
|
||||
"通知权限", "发送通知", "Post notifications",
|
||||
"允许.*推送", "开启通知",
|
||||
"Notification access",
|
||||
],
|
||||
"click": ["允许", "Allow", "确定"]},
|
||||
|
||||
# ─── 14. 悬浮窗/无障碍 ───
|
||||
{"detect": [
|
||||
"悬浮窗", "Display over", "draw over",
|
||||
"无障碍", "Accessibility",
|
||||
"辅助功能",
|
||||
],
|
||||
"click": ["允许", "Allow", "确定", "开启"]},
|
||||
|
||||
# ─── 15. Termux / 终端相关 ───
|
||||
{"detect": ["Termux", "终端", "com.termux"],
|
||||
"click": ["允许", "Allow", "确定"]},
|
||||
|
||||
# ─── 16. 应用崩溃/ANR ───
|
||||
{"detect": [
|
||||
"ANR", "没有响应", "not responding", "停止运行",
|
||||
"已停止", "has stopped", "keeps stopping",
|
||||
"无响应.*关闭", "close app",
|
||||
],
|
||||
"click": ["等待", "Wait", "再次打开", "Reopen"]},
|
||||
|
||||
# ─── 17. 隐私合规弹窗(国内APP常见) ───
|
||||
{"detect": [
|
||||
"隐私政策", "用户协议", "Privacy policy",
|
||||
"个人信息保护", "我已阅读",
|
||||
"同意并继续", "Agree.*continue",
|
||||
],
|
||||
"click": ["同意", "同意并继续", "Agree", "我已阅读并同意", "确定"]},
|
||||
|
||||
# ─── 18. 广告/推广弹窗 ───
|
||||
{"detect": [
|
||||
"领取.*红包", "签到.*奖励",
|
||||
"今日推荐", "猜你喜欢",
|
||||
"开通.*会员", "免费试用",
|
||||
"跳过", "Skip",
|
||||
],
|
||||
"click": ["跳过", "关闭", "Skip", "Close", "×", "✕"]},
|
||||
|
||||
# ─── 19. SIM卡/运营商弹窗 ───
|
||||
{"detect": [
|
||||
"SIM.*未插入", "SIM.*not inserted",
|
||||
"无SIM卡", "No SIM",
|
||||
"紧急呼叫", "Emergency",
|
||||
],
|
||||
"click": ["确定", "OK", "关闭"]},
|
||||
]
|
||||
|
||||
|
||||
class ConnectionGuardSkill(BaseSkill):
|
||||
"""
|
||||
连接守护技能 — 常驻后台,确保设备与服务器始终连接
|
||||
|
||||
核心能力:
|
||||
1. dismiss_popups() — 自动关闭系统弹窗(USB授权、权限请求等)
|
||||
2. ensure_screen_on() — 确保屏幕亮起
|
||||
3. check_u2_alive() — 检测 uiautomator2 是否存活
|
||||
4. check_network() — 检测网络连通性(委托 Hawk)
|
||||
5. full_guard_cycle() — 执行一轮完整守护(上述全部)
|
||||
"""
|
||||
|
||||
PACKAGE = ""
|
||||
NAME = "连接守护"
|
||||
|
||||
def _shell(self, cmd: str) -> str:
|
||||
"""安全执行 shell 并返回输出"""
|
||||
try:
|
||||
r = self.d.shell(cmd)
|
||||
return (getattr(r, "output", None) or str(r) or "").strip()
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
# ========== 1. 弹窗自动处理 ==========
|
||||
|
||||
def dismiss_popups(self) -> Dict[str, Any]:
|
||||
"""
|
||||
扫描当前界面,自动点击 USB 授权/权限请求/系统弹窗的确认按钮。
|
||||
返回本轮处理了哪些弹窗。
|
||||
"""
|
||||
dismissed = []
|
||||
if not self.d:
|
||||
return {"dismissed": dismissed, "count": 0}
|
||||
|
||||
try:
|
||||
xml = self.d.dump_hierarchy()
|
||||
except Exception as e:
|
||||
logger.debug(f"dump_hierarchy 失败: {e}")
|
||||
return {"dismissed": dismissed, "count": 0, "error": str(e)}
|
||||
|
||||
for rule in _POPUP_DISMISS_RULES:
|
||||
detected = False
|
||||
for keyword in rule["detect"]:
|
||||
if re.search(keyword, xml, re.IGNORECASE):
|
||||
detected = True
|
||||
break
|
||||
|
||||
if not detected:
|
||||
continue
|
||||
|
||||
for btn_text in rule["click"]:
|
||||
try:
|
||||
if self.d(text=btn_text).exists(timeout=0.5):
|
||||
self.d(text=btn_text).click()
|
||||
dismissed.append({"popup": rule["detect"][0], "clicked": btn_text})
|
||||
logger.info(f"🛡️ 自动处理弹窗: 检测到「{rule['detect'][0]}」→ 点击「{btn_text}」")
|
||||
time.sleep(0.5)
|
||||
break
|
||||
except Exception:
|
||||
continue
|
||||
# textContains 兜底
|
||||
try:
|
||||
if self.d(textContains=btn_text).exists(timeout=0.3):
|
||||
self.d(textContains=btn_text).click()
|
||||
dismissed.append({"popup": rule["detect"][0], "clicked": btn_text})
|
||||
logger.info(f"🛡️ 自动处理弹窗: 检测到「{rule['detect'][0]}」→ 点击「{btn_text}」(contains)")
|
||||
time.sleep(0.5)
|
||||
break
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
return {"dismissed": dismissed, "count": len(dismissed)}
|
||||
|
||||
# ========== 2. 屏幕状态 ==========
|
||||
|
||||
def ensure_screen_on(self) -> Dict[str, Any]:
|
||||
"""确保屏幕亮起(某些 ADB 操作需要屏幕点亮)"""
|
||||
if not self.d:
|
||||
return {"screen_on": False, "action": "no_device"}
|
||||
|
||||
try:
|
||||
info = self.d.info
|
||||
if info.get("screenOn"):
|
||||
return {"screen_on": True, "action": "already_on"}
|
||||
self.d.press("power")
|
||||
time.sleep(0.5)
|
||||
# 如果有锁屏,向上滑动解锁
|
||||
self.d.swipe_ext("up", scale=0.8)
|
||||
time.sleep(0.3)
|
||||
return {"screen_on": True, "action": "woke_up"}
|
||||
except Exception as e:
|
||||
logger.debug(f"ensure_screen_on 异常: {e}")
|
||||
return {"screen_on": False, "action": "error", "error": str(e)}
|
||||
|
||||
# ========== 3. uiautomator2 存活检测 ==========
|
||||
|
||||
def check_u2_alive(self) -> Dict[str, Any]:
|
||||
"""检测 uiautomator2 是否正常工作,异常时尝试重启"""
|
||||
if not self.d:
|
||||
return {"alive": False, "action": "no_device"}
|
||||
|
||||
try:
|
||||
info = self.d.info
|
||||
return {"alive": True, "package": info.get("currentPackageName", ""), "action": "ok"}
|
||||
except Exception as e:
|
||||
logger.warning(f"u2 异常: {e},尝试重启服务")
|
||||
try:
|
||||
self._shell("am instrument -w -r -e debug false "
|
||||
"-e class androidx.test.runner.AndroidJUnitRunner "
|
||||
"io.appium.uiautomator2.server.test/"
|
||||
"androidx.test.runner.AndroidJUnitRunner 2>/dev/null &")
|
||||
time.sleep(3)
|
||||
info = self.d.info
|
||||
return {"alive": True, "action": "restarted"}
|
||||
except Exception as e2:
|
||||
return {"alive": False, "action": "restart_failed", "error": str(e2)}
|
||||
|
||||
# ========== 4. 网络检测(委托 Hawk) ==========
|
||||
|
||||
def check_network(self) -> Dict[str, Any]:
|
||||
"""检测网络连通性,断网时委托 Hawk 尝试恢复"""
|
||||
try:
|
||||
from hawk import is_network_available, try_reconnect_network
|
||||
|
||||
if is_network_available(self.d):
|
||||
return {"network": True, "action": "ok"}
|
||||
|
||||
logger.info("🌐 检测到断网,启动 Hawk 网络恢复...")
|
||||
result = try_reconnect_network(self.d)
|
||||
return {
|
||||
"network": result.get("success", False),
|
||||
"action": "hawk_reconnect",
|
||||
"detail": result,
|
||||
}
|
||||
except ImportError:
|
||||
# Hawk 不可用时仅做 ping 检测
|
||||
out = self._shell("ping -c 1 -W 2 8.8.8.8 2>/dev/null && echo ok || echo fail")
|
||||
return {"network": "ok" in out, "action": "ping_only"}
|
||||
except Exception as e:
|
||||
return {"network": False, "action": "error", "error": str(e)}
|
||||
|
||||
# ========== 5. ADB 连接检测 ==========
|
||||
|
||||
def check_adb_connection(self) -> Dict[str, Any]:
|
||||
"""检测 ADB 连接状态"""
|
||||
out = self._shell("getprop ro.serialno 2>/dev/null || echo unknown")
|
||||
if out and out != "unknown":
|
||||
return {"adb": True, "serial": out}
|
||||
return {"adb": False, "serial": ""}
|
||||
|
||||
# ========== 6. 完整守护周期 ==========
|
||||
|
||||
def full_guard_cycle(self) -> Dict[str, Any]:
|
||||
"""
|
||||
执行一轮完整守护检查(Agent 心跳循环中调用):
|
||||
1. 自动处理弹窗(USB授权等)
|
||||
2. 确保屏幕亮起
|
||||
3. 检测 u2 存活
|
||||
4. 检测网络
|
||||
5. 检测 ADB
|
||||
"""
|
||||
results = {}
|
||||
|
||||
results["popups"] = self.dismiss_popups()
|
||||
results["screen"] = self.ensure_screen_on()
|
||||
results["u2"] = self.check_u2_alive()
|
||||
results["network"] = self.check_network()
|
||||
results["adb"] = self.check_adb_connection()
|
||||
|
||||
all_ok = (
|
||||
results["u2"].get("alive", False)
|
||||
and results["network"].get("network", False)
|
||||
)
|
||||
results["all_ok"] = all_ok
|
||||
results["timestamp"] = int(time.time())
|
||||
|
||||
if not all_ok:
|
||||
issues = []
|
||||
if not results["u2"].get("alive"):
|
||||
issues.append("u2不可用")
|
||||
if not results["network"].get("network"):
|
||||
issues.append("网络断开")
|
||||
logger.warning(f"⚠️ 守护检查发现问题: {', '.join(issues)}")
|
||||
else:
|
||||
popup_count = results["popups"].get("count", 0)
|
||||
if popup_count > 0:
|
||||
logger.info(f"🛡️ 守护周期完成: 处理了 {popup_count} 个弹窗,连接正常")
|
||||
|
||||
return results
|
||||
Reference in New Issue
Block a user