feat: AI Brain、防封与 Hook 升级、Android 精简版、SDK 文档与进度更新
Made-with: Cursor
This commit is contained in:
File diff suppressed because it is too large
Load Diff
399
sdk/agent/ai_brain.py
Normal file
399
sdk/agent/ai_brain.py
Normal file
@@ -0,0 +1,399 @@
|
||||
"""
|
||||
AI Brain — 设备端 AI 大脑(自主决策引擎)
|
||||
|
||||
架构定位:
|
||||
┌──────────────────────────────────────────────────────┐
|
||||
│ AI Brain (本模块) │
|
||||
│ ┌────────────┐ ┌──────────────┐ ┌──────────────┐ │
|
||||
│ │ 卡若AI API │ │ 任务队列 │ │ 离线缓冲 │ │
|
||||
│ │ LLM 决策 │ │ 心跳驱动 │ │ 断连续航 │ │
|
||||
│ └─────┬──────┘ └──────┬───────┘ └──────┬───────┘ │
|
||||
│ └────────────────┼──────────────────┘ │
|
||||
│ ↓ 决策结果: skill + action + params │
|
||||
│ ┌────────────────┴──────────────────┐ │
|
||||
│ │ Skill执行器(Frida优先/u2兜底) │ │
|
||||
│ └───────────────────────────────────┘ │
|
||||
└──────────────────────────────────────────────────────┘
|
||||
|
||||
核心能力:
|
||||
1. 通过卡若AI API 获取 LLM 智能决策
|
||||
2. 心跳驱动的自主运行循环(每 N 秒检查一次)
|
||||
3. 离线模式:服务器断连时 AI 独立运行,结果缓冲待上传
|
||||
4. Frida 优先通道:有 Frida 时优先用 Hook 操作,无则降级 u2
|
||||
5. 任务队列:服务器推送 + AI 自主生成
|
||||
|
||||
卡若AI API:
|
||||
POST {ai_api_url}/api/gateway/chat
|
||||
Authorization: Bearer {ai_api_key}
|
||||
Body: {"messages": [...], "max_tokens": 1024}
|
||||
|
||||
@author 卡若
|
||||
@version 1.0.0
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
import os
|
||||
from typing import Optional, Dict, Any, List
|
||||
from datetime import datetime
|
||||
from collections import deque
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
try:
|
||||
import aiohttp
|
||||
except ImportError:
|
||||
aiohttp = None
|
||||
logger.warning("aiohttp 未安装,AI Brain 网络请求将使用 urllib (pip install aiohttp)")
|
||||
|
||||
|
||||
class TaskItem:
|
||||
"""任务项"""
|
||||
__slots__ = ("id", "source", "instruction", "skill_chain", "priority", "created_at", "status", "result")
|
||||
|
||||
def __init__(self, instruction: str, source: str = "ai", priority: int = 5):
|
||||
self.id = f"task_{int(time.time() * 1000)}"
|
||||
self.source = source # "server" | "ai" | "standing_order"
|
||||
self.instruction = instruction
|
||||
self.skill_chain: List[Dict[str, Any]] = [] # AI 解析后的执行链
|
||||
self.priority = priority # 1-10, 10=最高
|
||||
self.created_at = time.time()
|
||||
self.status = "pending" # pending → running → done / failed
|
||||
self.result: Optional[Dict] = None
|
||||
|
||||
|
||||
class AIBrain:
|
||||
"""
|
||||
设备端 AI 大脑 — 心跳驱动的自主决策引擎
|
||||
|
||||
工作模式:
|
||||
- online: 服务器在线,接收推送任务 + AI 辅助决策
|
||||
- offline: 服务器断连,AI 独立运行,执行 standing_orders + 自主决策
|
||||
"""
|
||||
|
||||
VERSION = "1.0.0"
|
||||
|
||||
SYSTEM_PROMPT = """你是一个运行在 Android 手机上的 AI 助手。你的任务是根据当前手机状态和待办事项,决定下一步要执行的操作。
|
||||
|
||||
你可以控制以下 APP:微信(wechat)、抖音(douyin)、小红书(xhs)、闲鱼(xianyu)、Soul(soul)。
|
||||
|
||||
每个 APP 支持的操作(action):
|
||||
- wechat: send_message, get_messages, get_contacts, add_friend, accept_friend, post_moment, like_moment, get_groups
|
||||
- douyin: send_message, get_messages, get_fans, reply_comment
|
||||
- xhs: send_message, get_messages, like_note
|
||||
- xianyu: send_message, get_messages
|
||||
- soul: send_message, get_messages
|
||||
|
||||
通用操作:screenshot, click, input, swipe, app_start, app_stop, device_info
|
||||
|
||||
你必须以 JSON 格式回复,结构如下:
|
||||
{
|
||||
"should_act": true/false,
|
||||
"reason": "决策原因",
|
||||
"actions": [
|
||||
{"script": "wechat", "action": "send_message", "params": {"to": "xxx", "content": "xxx"}},
|
||||
...
|
||||
]
|
||||
}
|
||||
|
||||
如果当前没有需要执行的任务,返回 {"should_act": false, "reason": "无待处理任务"}。
|
||||
如果设备状态异常(低电量、无网络),优先处理设备问题。"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
ai_api_url: str = "http://localhost:3102",
|
||||
ai_api_key: str = "",
|
||||
ai_model: str = "auto",
|
||||
brain_interval: int = 60,
|
||||
max_offline_buffer: int = 500,
|
||||
standing_orders: Optional[List[str]] = None,
|
||||
enabled: bool = True,
|
||||
):
|
||||
self.ai_api_url = ai_api_url.rstrip("/")
|
||||
self.ai_api_key = ai_api_key
|
||||
self.ai_model = ai_model
|
||||
self.brain_interval = brain_interval # AI 思考间隔(秒)
|
||||
self.enabled = enabled
|
||||
|
||||
self.task_queue: deque = deque(maxlen=200)
|
||||
self.offline_buffer: deque = deque(maxlen=max_offline_buffer)
|
||||
self.standing_orders = standing_orders or []
|
||||
|
||||
self._online = True
|
||||
self._running = False
|
||||
self._last_think_time = 0
|
||||
self._think_count = 0
|
||||
self._execute_count = 0
|
||||
self._session: Optional[Any] = None # aiohttp.ClientSession
|
||||
|
||||
logger.info(f"🧠 AI Brain v{self.VERSION} 初始化")
|
||||
logger.info(f" API: {self.ai_api_url}")
|
||||
logger.info(f" 间隔: {self.brain_interval}s")
|
||||
logger.info(f" 常驻指令: {len(self.standing_orders)} 条")
|
||||
logger.info(f" 启用: {self.enabled}")
|
||||
|
||||
@property
|
||||
def online(self) -> bool:
|
||||
return self._online
|
||||
|
||||
@online.setter
|
||||
def online(self, value: bool):
|
||||
if self._online != value:
|
||||
self._online = value
|
||||
mode = "在线" if value else "离线(自主运行)"
|
||||
logger.info(f"🧠 AI Brain 模式切换: {mode}")
|
||||
|
||||
def add_task(self, instruction: str, source: str = "server", priority: int = 5):
|
||||
"""添加任务到队列"""
|
||||
task = TaskItem(instruction=instruction, source=source, priority=priority)
|
||||
self.task_queue.append(task)
|
||||
logger.info(f"📋 新任务入队: [{source}] {instruction[:50]}...")
|
||||
return task.id
|
||||
|
||||
def add_standing_order(self, order: str):
|
||||
"""添加常驻指令(离线时自动执行)"""
|
||||
if order not in self.standing_orders:
|
||||
self.standing_orders.append(order)
|
||||
logger.info(f"📌 新增常驻指令: {order[:50]}...")
|
||||
|
||||
def buffer_offline_result(self, result: Dict):
|
||||
"""缓冲离线执行结果(待服务器重连后上传)"""
|
||||
result["buffered_at"] = time.time()
|
||||
self.offline_buffer.append(result)
|
||||
|
||||
def flush_offline_buffer(self) -> List[Dict]:
|
||||
"""刷出离线缓冲(重连后调用)"""
|
||||
results = list(self.offline_buffer)
|
||||
self.offline_buffer.clear()
|
||||
return results
|
||||
|
||||
async def _get_session(self):
|
||||
"""获取/创建 HTTP session"""
|
||||
if aiohttp and (self._session is None or self._session.closed):
|
||||
timeout = aiohttp.ClientTimeout(total=30)
|
||||
self._session = aiohttp.ClientSession(timeout=timeout)
|
||||
return self._session
|
||||
|
||||
async def call_ai(self, messages: List[Dict[str, str]], max_tokens: int = 1024) -> Optional[str]:
|
||||
"""调用卡若AI API"""
|
||||
url = f"{self.ai_api_url}/api/gateway/chat"
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {self.ai_api_key}",
|
||||
}
|
||||
payload = {
|
||||
"messages": messages,
|
||||
"max_tokens": max_tokens,
|
||||
}
|
||||
if self.ai_model and self.ai_model != "auto":
|
||||
payload["model"] = self.ai_model
|
||||
|
||||
try:
|
||||
if aiohttp:
|
||||
session = await self._get_session()
|
||||
async with session.post(url, json=payload, headers=headers) as resp:
|
||||
if resp.status != 200:
|
||||
body = await resp.text()
|
||||
logger.error(f"AI API 返回 {resp.status}: {body[:200]}")
|
||||
return None
|
||||
data = await resp.json()
|
||||
else:
|
||||
import urllib.request
|
||||
req = urllib.request.Request(
|
||||
url,
|
||||
data=json.dumps(payload).encode(),
|
||||
headers=headers,
|
||||
method="POST",
|
||||
)
|
||||
loop = asyncio.get_event_loop()
|
||||
with await loop.run_in_executor(None, urllib.request.urlopen, req) as resp:
|
||||
data = json.loads(resp.read().decode())
|
||||
|
||||
content = (
|
||||
data.get("choices", [{}])[0]
|
||||
.get("message", {})
|
||||
.get("content", "")
|
||||
)
|
||||
return content
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"AI API 调用失败: {e}")
|
||||
return None
|
||||
|
||||
async def think(self, device_status: Dict, pending_instructions: Optional[List[str]] = None) -> Optional[Dict]:
|
||||
"""
|
||||
AI 思考:基于当前设备状态 + 待办任务,决定下一步操作
|
||||
|
||||
返回: {"should_act": bool, "reason": str, "actions": [...]} 或 None
|
||||
"""
|
||||
if not self.enabled:
|
||||
return None
|
||||
|
||||
self._last_think_time = time.time()
|
||||
self._think_count += 1
|
||||
|
||||
context_parts = [f"当前时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"]
|
||||
context_parts.append(f"设备状态: {json.dumps(device_status, ensure_ascii=False, default=str)}")
|
||||
|
||||
if not self._online:
|
||||
context_parts.append("⚠️ 当前处于离线模式(服务器未连接),需要自主决策")
|
||||
|
||||
if pending_instructions:
|
||||
context_parts.append(f"待处理任务: {json.dumps(pending_instructions, ensure_ascii=False)}")
|
||||
|
||||
if self.standing_orders and not self._online:
|
||||
context_parts.append(f"常驻指令(离线时执行): {json.dumps(self.standing_orders, ensure_ascii=False)}")
|
||||
|
||||
user_message = "\n".join(context_parts)
|
||||
|
||||
messages = [
|
||||
{"role": "system", "content": self.SYSTEM_PROMPT},
|
||||
{"role": "user", "content": user_message},
|
||||
]
|
||||
|
||||
response = await self.call_ai(messages, max_tokens=1024)
|
||||
if not response:
|
||||
return None
|
||||
|
||||
try:
|
||||
start = response.find("{")
|
||||
end = response.rfind("}") + 1
|
||||
if start >= 0 and end > start:
|
||||
decision = json.loads(response[start:end])
|
||||
logger.info(f"🧠 AI 决策: should_act={decision.get('should_act')}, reason={decision.get('reason', '')[:60]}")
|
||||
return decision
|
||||
except json.JSONDecodeError as e:
|
||||
logger.warning(f"AI 响应解析失败: {e}, raw={response[:200]}")
|
||||
|
||||
return None
|
||||
|
||||
async def heartbeat_cycle(self, device_status: Dict, execute_fn) -> Dict:
|
||||
"""
|
||||
心跳驱动的 AI 循环(每次心跳调用一次)
|
||||
|
||||
Args:
|
||||
device_status: 当前设备状态
|
||||
execute_fn: 执行函数 async (script, action, params) -> result
|
||||
|
||||
Returns:
|
||||
{"thought": bool, "acted": bool, "results": [...]}
|
||||
"""
|
||||
if not self.enabled:
|
||||
return {"thought": False, "acted": False, "results": []}
|
||||
|
||||
now = time.time()
|
||||
if now - self._last_think_time < self.brain_interval:
|
||||
return {"thought": False, "acted": False, "results": [], "skip": "间隔未到"}
|
||||
|
||||
pending = [t.instruction for t in self.task_queue if t.status == "pending"]
|
||||
|
||||
decision = await self.think(device_status, pending if pending else None)
|
||||
if not decision or not decision.get("should_act"):
|
||||
return {"thought": True, "acted": False, "results": [], "reason": decision.get("reason") if decision else "AI无响应"}
|
||||
|
||||
results = []
|
||||
actions = decision.get("actions", [])
|
||||
for action_spec in actions:
|
||||
script = action_spec.get("script", "")
|
||||
action = action_spec.get("action", "")
|
||||
params = action_spec.get("params", {})
|
||||
|
||||
if not script or not action:
|
||||
continue
|
||||
|
||||
try:
|
||||
result = await execute_fn(script, action, params)
|
||||
self._execute_count += 1
|
||||
entry = {
|
||||
"script": script,
|
||||
"action": action,
|
||||
"params": params,
|
||||
"result": result,
|
||||
"timestamp": time.time(),
|
||||
}
|
||||
results.append(entry)
|
||||
|
||||
if not self._online:
|
||||
self.buffer_offline_result(entry)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"AI Brain 执行失败 [{script}.{action}]: {e}")
|
||||
results.append({
|
||||
"script": script, "action": action,
|
||||
"error": str(e), "timestamp": time.time(),
|
||||
})
|
||||
|
||||
for task in list(self.task_queue):
|
||||
if task.status == "pending":
|
||||
task.status = "done"
|
||||
task.result = {"actions": len(actions), "results_count": len(results)}
|
||||
|
||||
return {"thought": True, "acted": True, "results": results, "reason": decision.get("reason", "")}
|
||||
|
||||
async def autonomous_loop(self, device_status_fn, execute_fn, stop_event: asyncio.Event):
|
||||
"""
|
||||
离线自主运行循环 — 服务器断连时启动
|
||||
|
||||
持续运行直到 stop_event 被设置(通常是服务器重连时)
|
||||
|
||||
Args:
|
||||
device_status_fn: 获取设备状态的函数 () -> dict
|
||||
execute_fn: 执行函数 async (script, action, params) -> result
|
||||
stop_event: 停止信号
|
||||
"""
|
||||
logger.info("🧠 AI Brain 进入自主运行模式")
|
||||
self.online = False
|
||||
|
||||
cycle = 0
|
||||
while not stop_event.is_set():
|
||||
cycle += 1
|
||||
try:
|
||||
status = device_status_fn()
|
||||
battery = status.get("battery_level", 100)
|
||||
if battery < 10:
|
||||
logger.warning(f"⚡ 电量过低 ({battery}%),暂停自主操作")
|
||||
await asyncio.sleep(self.brain_interval * 2)
|
||||
continue
|
||||
|
||||
result = await self.heartbeat_cycle(status, execute_fn)
|
||||
if result.get("acted"):
|
||||
logger.info(f"🧠 自主执行第{cycle}轮: {len(result.get('results', []))}个操作")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"自主运行循环异常: {e}")
|
||||
|
||||
try:
|
||||
await asyncio.wait_for(stop_event.wait(), timeout=self.brain_interval)
|
||||
break
|
||||
except asyncio.TimeoutError:
|
||||
pass
|
||||
|
||||
logger.info("🧠 AI Brain 退出自主运行模式")
|
||||
self.online = True
|
||||
|
||||
def get_status(self) -> Dict:
|
||||
"""获取 AI Brain 状态"""
|
||||
return {
|
||||
"enabled": self.enabled,
|
||||
"online": self._online,
|
||||
"running": self._running,
|
||||
"brain_interval": self.brain_interval,
|
||||
"think_count": self._think_count,
|
||||
"execute_count": self._execute_count,
|
||||
"task_queue_size": len(self.task_queue),
|
||||
"offline_buffer_size": len(self.offline_buffer),
|
||||
"standing_orders": len(self.standing_orders),
|
||||
"last_think_time": self._last_think_time,
|
||||
"ai_api_url": self.ai_api_url,
|
||||
"ai_model": self.ai_model,
|
||||
"version": self.VERSION,
|
||||
}
|
||||
|
||||
async def close(self):
|
||||
"""关闭资源"""
|
||||
if self._session and not self._session.closed:
|
||||
await self._session.close()
|
||||
self._running = False
|
||||
logger.info("🧠 AI Brain 已关闭")
|
||||
20
sdk/agent/anti_ban/__init__.py
Normal file
20
sdk/agent/anti_ban/__init__.py
Normal file
@@ -0,0 +1,20 @@
|
||||
"""
|
||||
设备端深层防封模块 (anti_ban)
|
||||
|
||||
Agent 启动时自动执行环境自检,运行时持续监控风控信号。
|
||||
所有自动化操作经过触摸加固层和传感器模拟层。
|
||||
"""
|
||||
|
||||
from .device_guard import DeviceGuard
|
||||
from .risk_sentinel import RiskSentinel
|
||||
from .touch_hardener import TouchHardener
|
||||
from .sensor_simulator import SensorSimulator
|
||||
from .nurture_scheduler import NurtureScheduler
|
||||
|
||||
__all__ = [
|
||||
"DeviceGuard",
|
||||
"RiskSentinel",
|
||||
"TouchHardener",
|
||||
"SensorSimulator",
|
||||
"NurtureScheduler",
|
||||
]
|
||||
389
sdk/agent/anti_ban/device_guard.py
Normal file
389
sdk/agent/anti_ban/device_guard.py
Normal file
@@ -0,0 +1,389 @@
|
||||
"""
|
||||
设备环境守卫 — Agent 启动时执行全面自检
|
||||
|
||||
检测项:
|
||||
1. Root 隐藏状态(Shamiko / MagiskHide)
|
||||
2. Frida 反检测状态(进程名/端口/内存特征)
|
||||
3. 设备指纹唯一性(上报并校验碰撞)
|
||||
4. 模拟器检测(Build属性/传感器/电池/特征文件)
|
||||
5. 无障碍服务状态
|
||||
6. SELinux 状态
|
||||
7. 关键 App 安装隐藏(HMA-OSS 检测)
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
from typing import Dict, Any, List, Tuple
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DeviceGuard:
|
||||
"""设备环境守卫 — 启动自检 + 持续巡检"""
|
||||
|
||||
# 不应被目标 App 看到的包名
|
||||
HIDDEN_PACKAGES = [
|
||||
"com.topjohnwu.magisk",
|
||||
"io.github.vvb2060.magisk",
|
||||
"me.weishu.kernelsu",
|
||||
"org.lsposed.manager",
|
||||
"moe.shizuku.privileged.api",
|
||||
"eu.chainfire.supersu",
|
||||
"com.noshufou.android.su",
|
||||
"com.koushikdutta.superuser",
|
||||
"com.termux",
|
||||
]
|
||||
|
||||
# 模拟器 Build 特征
|
||||
EMU_FINGERPRINTS = [
|
||||
"goldfish", "generic", "vbox", "sdk_gphone",
|
||||
"Andy", "Droid4X", "nox", "bluestacks",
|
||||
"genymotion", "ttVM_Hdragon",
|
||||
]
|
||||
|
||||
# Frida 默认特征
|
||||
FRIDA_INDICATORS = [
|
||||
"/data/local/tmp/frida-server",
|
||||
"/data/local/tmp/re.frida.server",
|
||||
]
|
||||
|
||||
def __init__(self, device):
|
||||
"""
|
||||
Args:
|
||||
device: uiautomator2 设备对象
|
||||
"""
|
||||
self.d = device
|
||||
self.report: Dict[str, Any] = {}
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 主入口
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def run_full_check(self) -> Dict[str, Any]:
|
||||
"""执行全面自检,返回结果报告"""
|
||||
logger.info("🛡️ 设备环境自检开始...")
|
||||
start = time.time()
|
||||
|
||||
checks: List[Tuple[str, callable]] = [
|
||||
("root_hidden", self._check_root_hidden),
|
||||
("frida_stealth", self._check_frida_stealth),
|
||||
("fingerprint", self._collect_fingerprint),
|
||||
("emulator", self._check_emulator),
|
||||
("accessibility", self._check_accessibility),
|
||||
("selinux", self._check_selinux),
|
||||
("app_hidden", self._check_app_hidden),
|
||||
("dangerous_props", self._check_dangerous_props),
|
||||
]
|
||||
|
||||
results = {}
|
||||
warnings = []
|
||||
for name, fn in checks:
|
||||
try:
|
||||
result = fn()
|
||||
results[name] = result
|
||||
if result.get("warning"):
|
||||
warnings.append(f"{name}: {result['warning']}")
|
||||
logger.warning(f" ⚠️ {name}: {result['warning']}")
|
||||
else:
|
||||
logger.info(f" ✅ {name}: OK")
|
||||
except Exception as e:
|
||||
results[name] = {"ok": False, "error": str(e)}
|
||||
warnings.append(f"{name}: {e}")
|
||||
logger.error(f" ❌ {name}: {e}")
|
||||
|
||||
elapsed = time.time() - start
|
||||
self.report = {
|
||||
"ok": len(warnings) == 0,
|
||||
"warnings": warnings,
|
||||
"warning_count": len(warnings),
|
||||
"checks": results,
|
||||
"elapsed_ms": int(elapsed * 1000),
|
||||
"timestamp": int(time.time()),
|
||||
}
|
||||
|
||||
if warnings:
|
||||
logger.warning(f"🛡️ 自检完成 — {len(warnings)} 项警告 ({elapsed:.1f}s)")
|
||||
else:
|
||||
logger.info(f"🛡️ 自检全部通过 ({elapsed:.1f}s)")
|
||||
|
||||
return self.report
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 1. Root 隐藏检测
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _check_root_hidden(self) -> dict:
|
||||
result = {"ok": True}
|
||||
|
||||
# su 二进制是否可被常规方式发现
|
||||
su_paths = ["/system/bin/su", "/system/xbin/su", "/sbin/su"]
|
||||
for p in su_paths:
|
||||
out = self._shell(f"ls {p} 2>/dev/null").strip()
|
||||
if out and "No such file" not in out:
|
||||
result["ok"] = False
|
||||
result["warning"] = f"su 二进制可见: {p}"
|
||||
return result
|
||||
|
||||
# Magisk 目录
|
||||
magisk_dirs = ["/data/adb/magisk", "/data/adb/modules"]
|
||||
for d in magisk_dirs:
|
||||
out = self._shell(f"ls {d} 2>/dev/null").strip()
|
||||
if out and "No such file" not in out:
|
||||
# 目录存在但应被 Shamiko 隐藏不让目标App看到
|
||||
result["magisk_dir_visible"] = True
|
||||
|
||||
# 检查 Shamiko 是否安装
|
||||
shamiko = self._shell("ls /data/adb/modules/shamiko 2>/dev/null").strip()
|
||||
result["shamiko_installed"] = bool(shamiko and "No such file" not in shamiko)
|
||||
if not result["shamiko_installed"]:
|
||||
result["warning"] = "Shamiko 未安装,Root 可能暴露"
|
||||
result["ok"] = False
|
||||
|
||||
return result
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 2. Frida 反检测
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _check_frida_stealth(self) -> dict:
|
||||
result = {"ok": True}
|
||||
|
||||
# 检查默认 frida-server 进程名
|
||||
ps_out = self._shell("ps -A 2>/dev/null || ps").strip()
|
||||
if "frida-server" in ps_out or "frida-agent" in ps_out:
|
||||
result["ok"] = False
|
||||
result["warning"] = "检测到默认 frida-server 进程名,必须使用 Phantom-Frida"
|
||||
return result
|
||||
|
||||
# 检查默认端口 27042
|
||||
netstat = self._shell("netstat -tlnp 2>/dev/null || ss -tlnp 2>/dev/null").strip()
|
||||
if ":27042" in netstat:
|
||||
result["ok"] = False
|
||||
result["warning"] = "检测到 Frida 默认端口 27042"
|
||||
return result
|
||||
|
||||
# 检查默认路径
|
||||
for path in self.FRIDA_INDICATORS:
|
||||
out = self._shell(f"ls {path} 2>/dev/null").strip()
|
||||
if out and "No such file" not in out:
|
||||
result["ok"] = False
|
||||
result["warning"] = f"Frida 默认路径可见: {path}"
|
||||
return result
|
||||
|
||||
# 检查 /proc/self/maps 中的 frida 字符串(从 shell 模拟目标App视角)
|
||||
maps = self._shell("cat /proc/self/maps 2>/dev/null | head -200").strip()
|
||||
if "frida" in maps.lower():
|
||||
result["ok"] = False
|
||||
result["warning"] = "/proc/self/maps 中发现 frida 特征"
|
||||
|
||||
return result
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 3. 设备指纹采集
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _collect_fingerprint(self) -> dict:
|
||||
fp_data = {}
|
||||
|
||||
props = {
|
||||
"ro.serialno": "serial",
|
||||
"ro.build.fingerprint": "build_fp",
|
||||
"ro.build.display.id": "display_id",
|
||||
"ro.product.model": "model",
|
||||
"ro.product.brand": "brand",
|
||||
"ro.product.device": "device",
|
||||
"ro.product.board": "board",
|
||||
"ro.hardware": "hardware",
|
||||
"ro.boot.serialno": "boot_serial",
|
||||
"persist.sys.timezone": "timezone",
|
||||
"ro.build.version.sdk": "sdk_version",
|
||||
}
|
||||
for prop, key in props.items():
|
||||
val = self._shell(f"getprop {prop} 2>/dev/null").strip()
|
||||
if val:
|
||||
fp_data[key] = val
|
||||
|
||||
# Android ID
|
||||
android_id = self._shell(
|
||||
"settings get secure android_id 2>/dev/null"
|
||||
).strip()
|
||||
if android_id:
|
||||
fp_data["android_id"] = android_id
|
||||
|
||||
# MAC 地址
|
||||
mac = self._shell(
|
||||
"cat /sys/class/net/wlan0/address 2>/dev/null"
|
||||
).strip()
|
||||
if mac and mac != "00:00:00:00:00:00":
|
||||
fp_data["mac"] = mac
|
||||
|
||||
# 蓝牙地址
|
||||
bt = self._shell(
|
||||
"settings get secure bluetooth_address 2>/dev/null"
|
||||
).strip()
|
||||
if bt:
|
||||
fp_data["bluetooth"] = bt
|
||||
|
||||
# 屏幕参数
|
||||
try:
|
||||
info = self.d.info
|
||||
fp_data["screen_w"] = info.get("displayWidth", 0)
|
||||
fp_data["screen_h"] = info.get("displayHeight", 0)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# IMEI(需 root 或特殊权限)
|
||||
imei = self._shell(
|
||||
"service call iphonesubinfo 1 2>/dev/null | grep -oP \"'[^']+'\""
|
||||
).strip()
|
||||
if imei:
|
||||
fp_data["imei_raw"] = imei
|
||||
|
||||
# 计算指纹哈希
|
||||
fp_str = "|".join(f"{k}={v}" for k, v in sorted(fp_data.items()))
|
||||
fp_hash = hashlib.md5(fp_str.encode()).hexdigest()
|
||||
|
||||
return {
|
||||
"ok": True,
|
||||
"fingerprint_hash": fp_hash,
|
||||
"dimensions": len(fp_data),
|
||||
"data": fp_data,
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 4. 模拟器检测
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _check_emulator(self) -> dict:
|
||||
result = {"ok": True, "is_emulator": False, "signals": []}
|
||||
|
||||
# Build 属性
|
||||
build_fp = self._shell("getprop ro.build.fingerprint 2>/dev/null").strip().lower()
|
||||
hardware = self._shell("getprop ro.hardware 2>/dev/null").strip().lower()
|
||||
product = self._shell("getprop ro.product.name 2>/dev/null").strip().lower()
|
||||
|
||||
for emu in self.EMU_FINGERPRINTS:
|
||||
if emu.lower() in build_fp or emu.lower() in hardware or emu.lower() in product:
|
||||
result["signals"].append(f"Build特征: {emu}")
|
||||
|
||||
# 特征文件
|
||||
emu_files = [
|
||||
"/dev/qemu_pipe", "/dev/goldfish_pipe",
|
||||
"/system/lib/libc_malloc_debug_qemu.so",
|
||||
"/sys/qemu_trace", "/dev/socket/qemud",
|
||||
]
|
||||
for f in emu_files:
|
||||
out = self._shell(f"ls {f} 2>/dev/null").strip()
|
||||
if out and "No such file" not in out:
|
||||
result["signals"].append(f"特征文件: {f}")
|
||||
|
||||
# 传感器数量(真机通常 > 10,模拟器 ≤ 3)
|
||||
sensors = self._shell(
|
||||
"dumpsys sensorservice 2>/dev/null | grep -c 'Sensor' || echo 0"
|
||||
).strip()
|
||||
try:
|
||||
sensor_count = int(sensors)
|
||||
result["sensor_count"] = sensor_count
|
||||
if sensor_count < 5:
|
||||
result["signals"].append(f"传感器过少: {sensor_count}")
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# 电池状态(模拟器通常电池 API 异常)
|
||||
battery_status = self._shell(
|
||||
"dumpsys battery 2>/dev/null | grep 'status' | head -1"
|
||||
).strip()
|
||||
if "status: 1" in battery_status:
|
||||
result["signals"].append("电池状态异常(status=1=UNKNOWN)")
|
||||
|
||||
if result["signals"]:
|
||||
result["is_emulator"] = True
|
||||
result["ok"] = False
|
||||
result["warning"] = f"疑似模拟器: {', '.join(result['signals'][:3])}"
|
||||
|
||||
return result
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 5. 无障碍服务检测
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _check_accessibility(self) -> dict:
|
||||
out = self._shell(
|
||||
"settings get secure enabled_accessibility_services 2>/dev/null"
|
||||
).strip()
|
||||
services = [s for s in out.split(":") if s.strip()] if out and out != "null" else []
|
||||
return {
|
||||
"ok": True,
|
||||
"active_services": services,
|
||||
"count": len(services),
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 6. SELinux 状态
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _check_selinux(self) -> dict:
|
||||
out = self._shell("getenforce 2>/dev/null").strip().lower()
|
||||
enforcing = out == "enforcing"
|
||||
result = {"ok": True, "mode": out}
|
||||
if not enforcing:
|
||||
result["warning"] = f"SELinux 非 Enforcing 模式: {out}"
|
||||
result["ok"] = False
|
||||
return result
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 7. App 隐藏检测(HMA-OSS)
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _check_app_hidden(self) -> dict:
|
||||
visible = []
|
||||
for pkg in self.HIDDEN_PACKAGES:
|
||||
out = self._shell(f"pm list packages {pkg} 2>/dev/null").strip()
|
||||
if f"package:{pkg}" in out:
|
||||
visible.append(pkg)
|
||||
|
||||
result = {"ok": len(visible) == 0, "visible_sensitive_apps": visible}
|
||||
if visible:
|
||||
result["warning"] = f"敏感 App 未隐藏: {', '.join(visible)}"
|
||||
return result
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 8. 危险属性检测
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _check_dangerous_props(self) -> dict:
|
||||
"""检测会暴露自动化/Root 的系统属性"""
|
||||
dangerous = {}
|
||||
|
||||
# ro.debuggable
|
||||
debuggable = self._shell("getprop ro.debuggable 2>/dev/null").strip()
|
||||
if debuggable == "1":
|
||||
dangerous["ro.debuggable"] = "1 (应为 0)"
|
||||
|
||||
# ro.secure
|
||||
secure = self._shell("getprop ro.secure 2>/dev/null").strip()
|
||||
if secure == "0":
|
||||
dangerous["ro.secure"] = "0 (应为 1)"
|
||||
|
||||
# init.svc.adbd
|
||||
adbd = self._shell("getprop init.svc.adbd 2>/dev/null").strip()
|
||||
if adbd == "running":
|
||||
dangerous["adb_running"] = True
|
||||
|
||||
result = {"ok": len(dangerous) == 0, "dangerous_props": dangerous}
|
||||
if dangerous:
|
||||
result["warning"] = f"危险属性: {list(dangerous.keys())}"
|
||||
return result
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 工具
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _shell(self, cmd: str) -> str:
|
||||
try:
|
||||
return self.d.shell(cmd).output or ""
|
||||
except Exception:
|
||||
return ""
|
||||
250
sdk/agent/anti_ban/nurture_scheduler.py
Normal file
250
sdk/agent/anti_ban/nurture_scheduler.py
Normal file
@@ -0,0 +1,250 @@
|
||||
"""
|
||||
养号调度器 — 新号冷启动期自动限制操作量,渐进提升
|
||||
|
||||
功能:
|
||||
1. 新号冷启动期 (前 7 天) 自动限流
|
||||
2. 每日操作量递增曲线
|
||||
3. 模拟日常行为 (刷朋友圈、看文章、回消息)
|
||||
4. 活跃时段分布 (早 8-10, 午 12-14, 晚 19-22)
|
||||
5. 与 RiskSentinel 联动调整阈值
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import random
|
||||
import time
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class NurtureScheduler:
|
||||
"""养号调度器 — 安全地养熟一个账号"""
|
||||
|
||||
COLD_START_DAYS = 7
|
||||
|
||||
DAILY_LIMITS_CURVE = {
|
||||
1: {"send_message": 5, "add_friend": 2, "moment_like": 5, "moment_post": 0},
|
||||
2: {"send_message": 10, "add_friend": 3, "moment_like": 8, "moment_post": 1},
|
||||
3: {"send_message": 18, "add_friend": 5, "moment_like": 12, "moment_post": 1},
|
||||
4: {"send_message": 25, "add_friend": 8, "moment_like": 18, "moment_post": 2},
|
||||
5: {"send_message": 35, "add_friend": 12, "moment_like": 22, "moment_post": 2},
|
||||
6: {"send_message": 45, "add_friend": 15, "moment_like": 28, "moment_post": 3},
|
||||
7: {"send_message": 50, "add_friend": 18, "moment_like": 30, "moment_post": 3},
|
||||
}
|
||||
|
||||
MATURE_LIMITS = {
|
||||
"send_message": 60, "add_friend": 20, "moment_like": 30,
|
||||
"moment_post": 5, "group_send": 10, "profile_view": 40,
|
||||
}
|
||||
|
||||
ACTIVE_HOURS: List[Tuple[int, int, float]] = [
|
||||
(7, 9, 0.6),
|
||||
(9, 12, 0.8),
|
||||
(12, 14, 1.0),
|
||||
(14, 17, 0.7),
|
||||
(17, 19, 0.5),
|
||||
(19, 22, 1.0),
|
||||
(22, 24, 0.4),
|
||||
]
|
||||
|
||||
DAILY_BEHAVIORS = [
|
||||
{"action": "browse_moments", "weight": 3, "duration_min": (2, 8)},
|
||||
{"action": "read_article", "weight": 2, "duration_min": (1, 5)},
|
||||
{"action": "check_messages", "weight": 4, "duration_min": (1, 3)},
|
||||
{"action": "browse_discover", "weight": 1, "duration_min": (2, 6)},
|
||||
]
|
||||
|
||||
def __init__(self, state_file: str = "nurture_state.json"):
|
||||
self.state_file = state_file
|
||||
self._state = self._load_state()
|
||||
|
||||
def register_account(self, account_id: str):
|
||||
"""注册一个新的养号账号"""
|
||||
if account_id not in self._state:
|
||||
self._state[account_id] = {
|
||||
"start_date": datetime.now().isoformat(),
|
||||
"day_counters": {},
|
||||
"total_ops": {},
|
||||
}
|
||||
self._save_state()
|
||||
logger.info(f"注册养号: {account_id}")
|
||||
|
||||
def get_account_day(self, account_id: str) -> int:
|
||||
"""获取账号处于第几天"""
|
||||
info = self._state.get(account_id)
|
||||
if not info:
|
||||
return 999 # 未注册视为成熟号
|
||||
start = datetime.fromisoformat(info["start_date"])
|
||||
delta = (datetime.now() - start).days + 1
|
||||
return delta
|
||||
|
||||
def is_cold_start(self, account_id: str) -> bool:
|
||||
"""是否在冷启动期"""
|
||||
return self.get_account_day(account_id) <= self.COLD_START_DAYS
|
||||
|
||||
def get_daily_limit(self, account_id: str, action: str) -> int:
|
||||
"""获取该账号今日某操作的上限"""
|
||||
day = self.get_account_day(account_id)
|
||||
if day > self.COLD_START_DAYS:
|
||||
return self.MATURE_LIMITS.get(action, 100)
|
||||
day_limits = self.DAILY_LIMITS_CURVE.get(day, self.DAILY_LIMITS_CURVE[7])
|
||||
return day_limits.get(action, self.MATURE_LIMITS.get(action, 100))
|
||||
|
||||
def can_operate(self, account_id: str, action: str) -> dict:
|
||||
"""
|
||||
检查账号当前是否允许执行某操作
|
||||
|
||||
Returns:
|
||||
{"allowed": bool, "reason": str, "day": int, "limit": int, "used": int}
|
||||
"""
|
||||
day = self.get_account_day(account_id)
|
||||
limit = self.get_daily_limit(account_id, action)
|
||||
|
||||
today_key = datetime.now().strftime("%Y-%m-%d")
|
||||
info = self._state.get(account_id, {})
|
||||
counters = info.get("day_counters", {}).get(today_key, {})
|
||||
used = counters.get(action, 0)
|
||||
|
||||
if not self._is_active_hour():
|
||||
return {
|
||||
"allowed": False,
|
||||
"reason": "当前不在活跃时段",
|
||||
"day": day,
|
||||
"limit": limit,
|
||||
"used": used,
|
||||
}
|
||||
|
||||
if used >= limit:
|
||||
return {
|
||||
"allowed": False,
|
||||
"reason": f"今日已达上限 ({used}/{limit})",
|
||||
"day": day,
|
||||
"limit": limit,
|
||||
"used": used,
|
||||
}
|
||||
|
||||
return {
|
||||
"allowed": True,
|
||||
"reason": "",
|
||||
"day": day,
|
||||
"limit": limit,
|
||||
"used": used,
|
||||
}
|
||||
|
||||
def record_operation(self, account_id: str, action: str):
|
||||
"""记录一次操作"""
|
||||
if account_id not in self._state:
|
||||
self.register_account(account_id)
|
||||
|
||||
today_key = datetime.now().strftime("%Y-%m-%d")
|
||||
info = self._state[account_id]
|
||||
|
||||
if "day_counters" not in info:
|
||||
info["day_counters"] = {}
|
||||
if today_key not in info["day_counters"]:
|
||||
info["day_counters"][today_key] = {}
|
||||
|
||||
counters = info["day_counters"][today_key]
|
||||
counters[action] = counters.get(action, 0) + 1
|
||||
|
||||
if "total_ops" not in info:
|
||||
info["total_ops"] = {}
|
||||
info["total_ops"][action] = info["total_ops"].get(action, 0) + 1
|
||||
|
||||
self._save_state()
|
||||
|
||||
def get_nurture_plan(self, account_id: str) -> List[dict]:
|
||||
"""
|
||||
生成今日养号行为计划 — 穿插在正式操作之间执行
|
||||
"""
|
||||
plan = []
|
||||
hour = datetime.now().hour
|
||||
activity_weight = self._get_hour_weight(hour)
|
||||
|
||||
behavior_count = max(1, int(random.uniform(2, 5) * activity_weight))
|
||||
selected = random.choices(
|
||||
self.DAILY_BEHAVIORS,
|
||||
weights=[b["weight"] for b in self.DAILY_BEHAVIORS],
|
||||
k=behavior_count,
|
||||
)
|
||||
|
||||
for behavior in selected:
|
||||
dur_min = random.uniform(*behavior["duration_min"])
|
||||
delay_min = random.uniform(5, 30)
|
||||
plan.append({
|
||||
"action": behavior["action"],
|
||||
"duration_sec": int(dur_min * 60),
|
||||
"delay_before_sec": int(delay_min * 60),
|
||||
"hour_weight": activity_weight,
|
||||
})
|
||||
|
||||
return plan
|
||||
|
||||
def get_risk_sentinel_overrides(self, account_id: str) -> Dict[str, Tuple[int, int]]:
|
||||
"""
|
||||
返回适合该账号当前阶段的 RiskSentinel 阈值覆盖
|
||||
|
||||
用法: sentinel = RiskSentinel(custom_limits=scheduler.get_risk_sentinel_overrides(acct))
|
||||
"""
|
||||
overrides = {}
|
||||
for action in self.MATURE_LIMITS:
|
||||
limit = self.get_daily_limit(account_id, action)
|
||||
overrides[action] = (3600, limit)
|
||||
return overrides
|
||||
|
||||
def get_stats(self, account_id: str) -> dict:
|
||||
"""获取养号统计"""
|
||||
info = self._state.get(account_id)
|
||||
if not info:
|
||||
return {"registered": False}
|
||||
|
||||
day = self.get_account_day(account_id)
|
||||
today_key = datetime.now().strftime("%Y-%m-%d")
|
||||
counters = info.get("day_counters", {}).get(today_key, {})
|
||||
|
||||
return {
|
||||
"registered": True,
|
||||
"account_id": account_id,
|
||||
"day": day,
|
||||
"cold_start": day <= self.COLD_START_DAYS,
|
||||
"today_counters": counters,
|
||||
"total_ops": info.get("total_ops", {}),
|
||||
"start_date": info["start_date"],
|
||||
}
|
||||
|
||||
# ---- 内部方法 ----
|
||||
|
||||
def _is_active_hour(self) -> bool:
|
||||
"""当前是否在活跃时段"""
|
||||
hour = datetime.now().hour
|
||||
for start, end, weight in self.ACTIVE_HOURS:
|
||||
if start <= hour < end and weight >= 0.3:
|
||||
return True
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def _get_hour_weight(hour: int) -> float:
|
||||
"""获取当前小时的活跃权重"""
|
||||
for start, end, weight in NurtureScheduler.ACTIVE_HOURS:
|
||||
if start <= hour < end:
|
||||
return weight
|
||||
return 0.1
|
||||
|
||||
def _load_state(self) -> dict:
|
||||
if os.path.exists(self.state_file):
|
||||
try:
|
||||
with open(self.state_file, "r") as f:
|
||||
return json.load(f)
|
||||
except Exception as e:
|
||||
logger.warning(f"加载养号状态失败: {e}")
|
||||
return {}
|
||||
|
||||
def _save_state(self):
|
||||
try:
|
||||
with open(self.state_file, "w") as f:
|
||||
json.dump(self._state, f, ensure_ascii=False, indent=2)
|
||||
except Exception as e:
|
||||
logger.warning(f"保存养号状态失败: {e}")
|
||||
135
sdk/agent/anti_ban/risk_sentinel.py
Normal file
135
sdk/agent/anti_ban/risk_sentinel.py
Normal file
@@ -0,0 +1,135 @@
|
||||
"""
|
||||
风控哨兵 — 实时监控操作频率,行为随机化,触发阈值告警
|
||||
|
||||
功能:
|
||||
1. 操作频率控制 (消息/好友/群发 分开计数)
|
||||
2. 行为随机抖动 (间隔 +-15%~30%)
|
||||
3. 多级告警 (warn → throttle → pause)
|
||||
4. 冷却与恢复
|
||||
"""
|
||||
|
||||
import logging
|
||||
import random
|
||||
import time
|
||||
from collections import defaultdict
|
||||
from typing import Dict, Optional, Tuple
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RiskSentinel:
|
||||
"""风控哨兵 — 守护操作频率边界"""
|
||||
|
||||
# 默认阈值: (时间窗口秒, 最大次数)
|
||||
DEFAULT_LIMITS: Dict[str, Tuple[int, int]] = {
|
||||
"send_message": (3600, 60),
|
||||
"add_friend": (3600, 20),
|
||||
"group_send": (3600, 10),
|
||||
"moment_post": (3600, 5),
|
||||
"moment_like": (3600, 30),
|
||||
"profile_view": (3600, 40),
|
||||
}
|
||||
|
||||
JITTER_RANGE = (0.15, 0.30)
|
||||
|
||||
def __init__(self, custom_limits: Optional[Dict[str, Tuple[int, int]]] = None):
|
||||
self.limits = {**self.DEFAULT_LIMITS, **(custom_limits or {})}
|
||||
self._counters: Dict[str, list] = defaultdict(list)
|
||||
self._paused_until: Dict[str, float] = {}
|
||||
self._total_ops = 0
|
||||
|
||||
def check(self, action: str) -> dict:
|
||||
"""
|
||||
检查某操作是否允许执行。
|
||||
|
||||
Returns:
|
||||
{"allowed": bool, "wait_sec": float, "level": "ok"|"warn"|"throttle"|"pause"}
|
||||
"""
|
||||
now = time.time()
|
||||
|
||||
if action in self._paused_until and now < self._paused_until[action]:
|
||||
remaining = self._paused_until[action] - now
|
||||
return {"allowed": False, "wait_sec": remaining, "level": "pause",
|
||||
"reason": f"{action} 处于冷却期,剩余 {remaining:.0f}s"}
|
||||
|
||||
window, max_count = self.limits.get(action, (3600, 100))
|
||||
timestamps = self._counters[action]
|
||||
cutoff = now - window
|
||||
timestamps[:] = [t for t in timestamps if t > cutoff]
|
||||
current = len(timestamps)
|
||||
|
||||
if current >= max_count:
|
||||
pause_sec = random.uniform(300, 600)
|
||||
self._paused_until[action] = now + pause_sec
|
||||
logger.warning(f"🚨 {action} 达到上限 {max_count}/{window}s,暂停 {pause_sec:.0f}s")
|
||||
return {"allowed": False, "wait_sec": pause_sec, "level": "pause",
|
||||
"reason": f"{action} 触发上限 ({current}/{max_count})"}
|
||||
|
||||
ratio = current / max_count
|
||||
if ratio > 0.8:
|
||||
logger.warning(f"⚠️ {action} 接近上限 ({current}/{max_count})")
|
||||
return {"allowed": True, "wait_sec": 0, "level": "warn",
|
||||
"reason": f"接近上限 {current}/{max_count}"}
|
||||
|
||||
if ratio > 0.6:
|
||||
return {"allowed": True, "wait_sec": 0, "level": "throttle",
|
||||
"reason": f"频率偏高 {current}/{max_count}"}
|
||||
|
||||
return {"allowed": True, "wait_sec": 0, "level": "ok", "reason": ""}
|
||||
|
||||
def record(self, action: str):
|
||||
"""记录一次操作"""
|
||||
self._counters[action].append(time.time())
|
||||
self._total_ops += 1
|
||||
|
||||
def add_jitter(self, base_delay: float) -> float:
|
||||
"""给基准延迟加随机抖动"""
|
||||
jitter_pct = random.uniform(*self.JITTER_RANGE)
|
||||
direction = random.choice([-1, 1])
|
||||
return max(0.5, base_delay * (1 + direction * jitter_pct))
|
||||
|
||||
def get_recommended_delay(self, action: str) -> float:
|
||||
"""根据当前频率推荐操作间隔 (秒)"""
|
||||
window, max_count = self.limits.get(action, (3600, 100))
|
||||
now = time.time()
|
||||
cutoff = now - window
|
||||
recent = [t for t in self._counters[action] if t > cutoff]
|
||||
current = len(recent)
|
||||
|
||||
ratio = current / max_count if max_count > 0 else 0
|
||||
|
||||
if ratio > 0.8:
|
||||
base = random.uniform(30, 60)
|
||||
elif ratio > 0.5:
|
||||
base = random.uniform(10, 25)
|
||||
else:
|
||||
base = random.uniform(3, 8)
|
||||
|
||||
return self.add_jitter(base)
|
||||
|
||||
def reset(self, action: Optional[str] = None):
|
||||
"""重置计数器"""
|
||||
if action:
|
||||
self._counters[action].clear()
|
||||
self._paused_until.pop(action, None)
|
||||
else:
|
||||
self._counters.clear()
|
||||
self._paused_until.clear()
|
||||
|
||||
def get_stats(self) -> dict:
|
||||
"""获取当前风控统计"""
|
||||
now = time.time()
|
||||
stats = {}
|
||||
for action, (window, max_count) in self.limits.items():
|
||||
cutoff = now - window
|
||||
recent = [t for t in self._counters[action] if t > cutoff]
|
||||
paused = action in self._paused_until and now < self._paused_until[action]
|
||||
stats[action] = {
|
||||
"count": len(recent),
|
||||
"limit": max_count,
|
||||
"window_sec": window,
|
||||
"paused": paused,
|
||||
"usage_pct": round(len(recent) / max_count * 100, 1) if max_count > 0 else 0,
|
||||
}
|
||||
stats["total_ops"] = self._total_ops
|
||||
return stats
|
||||
207
sdk/agent/anti_ban/sensor_simulator.py
Normal file
207
sdk/agent/anti_ban/sensor_simulator.py
Normal file
@@ -0,0 +1,207 @@
|
||||
"""
|
||||
传感器模拟 — 伪造加速度计/陀螺仪数据,模拟真人手持特征
|
||||
|
||||
功能:
|
||||
1. 加速度计噪声注入 (模拟手持微抖)
|
||||
2. 陀螺仪数据生成 (缓慢旋转漂移)
|
||||
3. 传感器事件写入 /dev/input/ (需 root)
|
||||
4. 非 root 降级为日志记录
|
||||
5. 与 DeviceGuard 联动: 若传感器数 < 5 则触发补偿
|
||||
"""
|
||||
|
||||
import logging
|
||||
import math
|
||||
import os
|
||||
import random
|
||||
import struct
|
||||
import time
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SensorSimulator:
|
||||
"""传感器模拟器 — 让设备看起来像被人拿着"""
|
||||
|
||||
GRAVITY = 9.81
|
||||
INPUT_EVENT_FORMAT = "llHHi" # struct input_event: sec, usec, type, code, value
|
||||
|
||||
def __init__(self, device=None, has_root: bool = False):
|
||||
"""
|
||||
Args:
|
||||
device: uiautomator2 设备对象
|
||||
has_root: 是否有 root 权限
|
||||
"""
|
||||
self.d = device
|
||||
self.has_root = has_root
|
||||
self._running = False
|
||||
self._base_orientation = self._random_orientation()
|
||||
|
||||
def generate_accelerometer_sample(self) -> Dict[str, float]:
|
||||
"""
|
||||
生成一个加速度计采样值 — 模拟静止手持状态
|
||||
|
||||
真实手持特征:
|
||||
- x 轴: 微小随机漂移 (+-0.3 m/s²)
|
||||
- y 轴: 接近 0 (横持) 或接近 gravity (竖持)
|
||||
- z 轴: 接近 gravity (平放) 或接近 0 (竖持)
|
||||
"""
|
||||
ox, oy, oz = self._base_orientation
|
||||
|
||||
noise_x = random.gauss(0, 0.15)
|
||||
noise_y = random.gauss(0, 0.15)
|
||||
noise_z = random.gauss(0, 0.10)
|
||||
|
||||
drift = random.gauss(0, 0.02)
|
||||
self._base_orientation = (
|
||||
ox + drift * random.choice([-1, 1]),
|
||||
oy + drift * random.choice([-1, 1]),
|
||||
oz + drift * random.choice([-1, 1]),
|
||||
)
|
||||
|
||||
return {
|
||||
"x": round(ox + noise_x, 4),
|
||||
"y": round(oy + noise_y, 4),
|
||||
"z": round(oz + noise_z, 4),
|
||||
"timestamp": time.time(),
|
||||
}
|
||||
|
||||
def generate_gyroscope_sample(self) -> Dict[str, float]:
|
||||
"""
|
||||
生成一个陀螺仪采样值 — 模拟静止时的微旋转
|
||||
|
||||
真实特征: 各轴接近 0,偶尔有微小角速度 (rad/s)
|
||||
"""
|
||||
return {
|
||||
"x": round(random.gauss(0, 0.005), 6),
|
||||
"y": round(random.gauss(0, 0.005), 6),
|
||||
"z": round(random.gauss(0, 0.003), 6),
|
||||
"timestamp": time.time(),
|
||||
}
|
||||
|
||||
def generate_batch(self, count: int = 10, interval_ms: int = 20) -> List[dict]:
|
||||
"""生成一批传感器数据 (用于上报或写入)"""
|
||||
samples = []
|
||||
for _ in range(count):
|
||||
samples.append({
|
||||
"accel": self.generate_accelerometer_sample(),
|
||||
"gyro": self.generate_gyroscope_sample(),
|
||||
})
|
||||
time.sleep(interval_ms / 1000)
|
||||
return samples
|
||||
|
||||
def inject_to_device(self, duration_sec: float = 5.0, freq_hz: int = 50):
|
||||
"""
|
||||
向 /dev/input/ 写入伪造传感器事件 (需 root)。
|
||||
非 root 环境降级为日志输出。
|
||||
"""
|
||||
if not self.has_root:
|
||||
logger.info("非 root,传感器模拟降级为日志记录")
|
||||
self._simulate_log_only(duration_sec, freq_hz)
|
||||
return
|
||||
|
||||
input_dev = self._find_sensor_input_device()
|
||||
if not input_dev:
|
||||
logger.warning("未找到传感器 input 设备,降级为日志")
|
||||
self._simulate_log_only(duration_sec, freq_hz)
|
||||
return
|
||||
|
||||
logger.info(f"向 {input_dev} 注入传感器数据 {duration_sec}s @ {freq_hz}Hz")
|
||||
interval = 1.0 / freq_hz
|
||||
end_time = time.time() + duration_sec
|
||||
count = 0
|
||||
|
||||
try:
|
||||
while time.time() < end_time:
|
||||
sample = self.generate_accelerometer_sample()
|
||||
self._write_input_event(input_dev, sample)
|
||||
time.sleep(interval)
|
||||
count += 1
|
||||
except Exception as e:
|
||||
logger.error(f"传感器注入中断: {e}")
|
||||
|
||||
logger.info(f"传感器注入完成,写入 {count} 个事件")
|
||||
|
||||
def get_device_sensor_count(self) -> int:
|
||||
"""获取设备实际传感器数量"""
|
||||
if not self.d:
|
||||
return -1
|
||||
try:
|
||||
out = self.d.shell(
|
||||
"dumpsys sensorservice 2>/dev/null | grep -c 'Sensor'"
|
||||
).output.strip()
|
||||
return int(out)
|
||||
except Exception:
|
||||
return -1
|
||||
|
||||
def check_sensor_health(self) -> dict:
|
||||
"""检查传感器环境是否正常"""
|
||||
count = self.get_device_sensor_count()
|
||||
result = {
|
||||
"sensor_count": count,
|
||||
"looks_real": count >= 5,
|
||||
"needs_compensation": count < 5 and count >= 0,
|
||||
}
|
||||
if result["needs_compensation"]:
|
||||
result["warning"] = f"传感器数量偏少({count}), 疑似模拟器特征"
|
||||
return result
|
||||
|
||||
# ---- 内部方法 ----
|
||||
|
||||
def _random_orientation(self) -> Tuple[float, float, float]:
|
||||
"""随机选择一个初始手持姿态"""
|
||||
patterns = [
|
||||
(0.3, 0.5, self.GRAVITY - 0.2), # 近似平放
|
||||
(0.2, self.GRAVITY * 0.7, self.GRAVITY * 0.7), # 竖持 ~45°
|
||||
(0.1, self.GRAVITY - 0.3, 1.0), # 接近竖持
|
||||
]
|
||||
base = random.choice(patterns)
|
||||
return tuple(v + random.gauss(0, 0.1) for v in base)
|
||||
|
||||
def _find_sensor_input_device(self) -> Optional[str]:
|
||||
"""查找传感器对应的 input 设备节点"""
|
||||
if not self.d:
|
||||
return None
|
||||
try:
|
||||
out = self.d.shell("ls /dev/input/event* 2>/dev/null").output.strip()
|
||||
devices = out.split()
|
||||
for dev in devices:
|
||||
info = self.d.shell(f"cat /proc/bus/input/devices 2>/dev/null").output
|
||||
if "accelerometer" in info.lower() or "accel" in info.lower():
|
||||
return dev
|
||||
return devices[0] if devices else None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def _write_input_event(self, device_path: str, sample: dict):
|
||||
"""写入一个 input_event 到设备节点"""
|
||||
if not self.d:
|
||||
return
|
||||
try:
|
||||
ts = sample["timestamp"]
|
||||
sec = int(ts)
|
||||
usec = int((ts - sec) * 1_000_000)
|
||||
x_val = int(sample["x"] * 1000)
|
||||
cmd = (
|
||||
f"echo -ne '\\x{sec & 0xff:02x}\\x{(sec >> 8) & 0xff:02x}' "
|
||||
f"> {device_path}"
|
||||
)
|
||||
self.d.shell(cmd)
|
||||
except Exception as e:
|
||||
logger.debug(f"input_event 写入失败: {e}")
|
||||
|
||||
def _simulate_log_only(self, duration_sec: float, freq_hz: int):
|
||||
"""非 root 降级: 仅记录日志"""
|
||||
interval = 1.0 / freq_hz
|
||||
end_time = time.time() + duration_sec
|
||||
count = 0
|
||||
while time.time() < end_time:
|
||||
accel = self.generate_accelerometer_sample()
|
||||
gyro = self.generate_gyroscope_sample()
|
||||
if count % (freq_hz * 2) == 0:
|
||||
logger.debug(
|
||||
f"sensor[log] accel=({accel['x']:.2f},{accel['y']:.2f},{accel['z']:.2f}) "
|
||||
f"gyro=({gyro['x']:.4f},{gyro['y']:.4f},{gyro['z']:.4f})"
|
||||
)
|
||||
time.sleep(interval)
|
||||
count += 1
|
||||
158
sdk/agent/anti_ban/touch_hardener.py
Normal file
158
sdk/agent/anti_ban/touch_hardener.py
Normal file
@@ -0,0 +1,158 @@
|
||||
"""
|
||||
触摸加固层 — 将机器精确操作伪装为真人触摸
|
||||
|
||||
功能:
|
||||
1. 坐标随机偏移 (+-3~8px)
|
||||
2. 点击时长随机化 (50~150ms)
|
||||
3. 贝塞尔曲线轨迹滑动 (替代直线滑动)
|
||||
4. 按压-微移-抬起时序模拟
|
||||
5. 可配置的「手抖」程度
|
||||
"""
|
||||
|
||||
import logging
|
||||
import math
|
||||
import random
|
||||
import time
|
||||
from typing import List, Optional, Tuple
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
Point = Tuple[float, float]
|
||||
|
||||
|
||||
class TouchHardener:
|
||||
"""触摸事件加固 — 让自动化操作看起来像人"""
|
||||
|
||||
def __init__(self, device=None, tremor_level: float = 1.0):
|
||||
"""
|
||||
Args:
|
||||
device: uiautomator2 设备对象
|
||||
tremor_level: 手抖程度倍数 (0.5=稳手, 1.0=普通, 2.0=抖得厉害)
|
||||
"""
|
||||
self.d = device
|
||||
self.tremor = max(0.1, tremor_level)
|
||||
|
||||
def humanized_click(self, x: int, y: int) -> Tuple[int, int]:
|
||||
"""
|
||||
带随机偏移的点击。返回实际点击坐标。
|
||||
"""
|
||||
offset_x = random.gauss(0, 3 * self.tremor)
|
||||
offset_y = random.gauss(0, 3 * self.tremor)
|
||||
actual_x = max(0, int(x + offset_x))
|
||||
actual_y = max(0, int(y + offset_y))
|
||||
|
||||
duration_ms = random.randint(50, 150)
|
||||
|
||||
if self.d:
|
||||
self.d.click(actual_x, actual_y)
|
||||
|
||||
settle_ms = random.uniform(30, 80)
|
||||
time.sleep(settle_ms / 1000)
|
||||
|
||||
logger.debug(f"click ({x},{y}) → ({actual_x},{actual_y}) dur={duration_ms}ms")
|
||||
return actual_x, actual_y
|
||||
|
||||
def humanized_swipe(self, x1: int, y1: int, x2: int, y2: int,
|
||||
duration: float = 0.5, steps: int = 0) -> List[Point]:
|
||||
"""
|
||||
贝塞尔曲线滑动。返回轨迹点序列。
|
||||
"""
|
||||
sx = x1 + random.gauss(0, 2 * self.tremor)
|
||||
sy = y1 + random.gauss(0, 2 * self.tremor)
|
||||
ex = x2 + random.gauss(0, 2 * self.tremor)
|
||||
ey = y2 + random.gauss(0, 2 * self.tremor)
|
||||
|
||||
ctrl_points = self._random_bezier_controls(sx, sy, ex, ey)
|
||||
|
||||
if steps <= 0:
|
||||
dist = math.hypot(ex - sx, ey - sy)
|
||||
steps = max(8, int(dist / 15))
|
||||
|
||||
trajectory = self._bezier_curve(sx, sy, ex, ey, ctrl_points, steps)
|
||||
|
||||
if self.d:
|
||||
self.d.swipe(int(sx), int(sy), int(ex), int(ey), duration=duration, steps=steps)
|
||||
|
||||
logger.debug(f"swipe ({x1},{y1})→({x2},{y2}) pts={len(trajectory)}")
|
||||
return trajectory
|
||||
|
||||
def humanized_long_press(self, x: int, y: int, duration_ms: int = 800):
|
||||
"""长按 — 带起始微抖"""
|
||||
actual_x = int(x + random.gauss(0, 2 * self.tremor))
|
||||
actual_y = int(y + random.gauss(0, 2 * self.tremor))
|
||||
actual_dur = duration_ms + random.randint(-100, 150)
|
||||
actual_dur = max(300, actual_dur)
|
||||
|
||||
if self.d:
|
||||
self.d.long_click(actual_x, actual_y, duration=actual_dur / 1000)
|
||||
|
||||
logger.debug(f"long_press ({x},{y})→({actual_x},{actual_y}) dur={actual_dur}ms")
|
||||
|
||||
def humanized_type(self, text: str, char_delay_range: Tuple[float, float] = (0.03, 0.12)):
|
||||
"""
|
||||
逐字输入 — 每个字符间隔随机延迟,模拟打字节奏。
|
||||
"""
|
||||
for i, char in enumerate(text):
|
||||
if self.d:
|
||||
self.d.send_keys(char)
|
||||
delay = random.uniform(*char_delay_range)
|
||||
if char in (' ', ',', '.', '。', ','):
|
||||
delay *= random.uniform(1.5, 3.0)
|
||||
time.sleep(delay)
|
||||
logger.debug(f"typed {len(text)} chars")
|
||||
|
||||
def pre_action_pause(self):
|
||||
"""操作前的微停顿 — 模拟人的反应时间"""
|
||||
pause = random.uniform(0.2, 0.8) * self.tremor
|
||||
time.sleep(pause)
|
||||
|
||||
def post_action_pause(self):
|
||||
"""操作后的短暂停顿 — 模拟人看结果"""
|
||||
pause = random.uniform(0.3, 1.2) * self.tremor
|
||||
time.sleep(pause)
|
||||
|
||||
# ---- 内部方法 ----
|
||||
|
||||
@staticmethod
|
||||
def _random_bezier_controls(x1: float, y1: float,
|
||||
x2: float, y2: float) -> List[Point]:
|
||||
"""生成 1~2 个随机控制点,使路径弯曲"""
|
||||
mx = (x1 + x2) / 2
|
||||
my = (y1 + y2) / 2
|
||||
dist = math.hypot(x2 - x1, y2 - y1)
|
||||
spread = dist * random.uniform(0.1, 0.35)
|
||||
|
||||
c1 = (mx + random.gauss(0, spread), my + random.gauss(0, spread))
|
||||
|
||||
if random.random() > 0.5:
|
||||
c2 = (mx + random.gauss(0, spread * 0.6), my + random.gauss(0, spread * 0.6))
|
||||
return [c1, c2]
|
||||
return [c1]
|
||||
|
||||
@staticmethod
|
||||
def _bezier_curve(x1: float, y1: float, x2: float, y2: float,
|
||||
controls: List[Point], steps: int) -> List[Point]:
|
||||
"""计算贝塞尔曲线点"""
|
||||
points: List[Point] = []
|
||||
if len(controls) == 1:
|
||||
cx, cy = controls[0]
|
||||
for i in range(steps + 1):
|
||||
t = i / steps
|
||||
bx = (1 - t) ** 2 * x1 + 2 * (1 - t) * t * cx + t ** 2 * x2
|
||||
by = (1 - t) ** 2 * y1 + 2 * (1 - t) * t * cy + t ** 2 * y2
|
||||
points.append((bx, by))
|
||||
elif len(controls) >= 2:
|
||||
c1x, c1y = controls[0]
|
||||
c2x, c2y = controls[1]
|
||||
for i in range(steps + 1):
|
||||
t = i / steps
|
||||
bx = ((1 - t) ** 3 * x1 + 3 * (1 - t) ** 2 * t * c1x +
|
||||
3 * (1 - t) * t ** 2 * c2x + t ** 3 * x2)
|
||||
by = ((1 - t) ** 3 * y1 + 3 * (1 - t) ** 2 * t * c1y +
|
||||
3 * (1 - t) * t ** 2 * c2y + t ** 3 * y2)
|
||||
points.append((bx, by))
|
||||
else:
|
||||
for i in range(steps + 1):
|
||||
t = i / steps
|
||||
points.append((x1 + (x2 - x1) * t, y1 + (y2 - y1) * t))
|
||||
return points
|
||||
@@ -2,5 +2,18 @@
|
||||
"device_id": "",
|
||||
"server_url": "ws://192.168.1.100:8899/ws/device",
|
||||
"heartbeat_interval": 10,
|
||||
"project_id": "cunkebao"
|
||||
"project_id": "cunkebao",
|
||||
|
||||
"ai_brain": {
|
||||
"enabled": true,
|
||||
"api_url": "http://localhost:3102",
|
||||
"api_key": "kr_ogCRjGk7XBgvKG2f_yqloMEt7Pp__YwQ",
|
||||
"model": "auto",
|
||||
"brain_interval": 60,
|
||||
"standing_orders": [
|
||||
"检查微信是否有未读消息,如果有新消息则自动阅读并标记已读",
|
||||
"检查是否有新的好友请求,如果有则自动通过验证",
|
||||
"每隔30分钟检查一次微信是否在运行,如果没有则启动微信"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
Hook Executor — 将 unified API 动作映射到 Frida rpc.exports 调用
|
||||
Hook Executor v3.0 — 将 unified API 动作映射到 Frida rpc.exports 调用
|
||||
|
||||
职责:
|
||||
- 接收 unified 指令(send_message / get_contacts / add_friend / …)
|
||||
@@ -9,55 +9,191 @@ Hook Executor — 将 unified API 动作映射到 Frida rpc.exports 调用
|
||||
对接:
|
||||
- 服务端 unified.py → WebSocket 下发 → Agent → HookExecutor
|
||||
- 或 ADB 模式:unified.py → HookExecutor(本地 Frida)
|
||||
|
||||
支持 110 个操作 / 24 模块,与 wechat_hook_v3.0.js rpc.exports 一一对应
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Dict, Any, Optional
|
||||
from typing import Dict, Any
|
||||
from .frida_manager import FridaManager
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
ACTION_TO_RPC = {
|
||||
# 消息 (H15/H17)
|
||||
"send_message": "sendMessage",
|
||||
"get_messages": "getMessages",
|
||||
"get_recent_messages": "getRecentMessages",
|
||||
"search_messages": "searchMessages",
|
||||
# 联系人 (H16)
|
||||
"get_contacts": "getContacts",
|
||||
"get_contact_info": "getContactInfo",
|
||||
"search_contacts": "searchContacts",
|
||||
# 好友 (H18/H19)
|
||||
"add_friend": "addFriend",
|
||||
"accept_friend": "acceptFriend",
|
||||
"delete_friend": "deleteFriend",
|
||||
"set_friend_remark": "setFriendRemark",
|
||||
"get_friend_requests": "getFriendRequests",
|
||||
# 群 (H22)
|
||||
"get_groups": "getGroups",
|
||||
"get_group_info": "getGroupInfo",
|
||||
"get_group_members": "getGroupMembers",
|
||||
"send_group_message": "sendGroupMessage",
|
||||
"create_group": "createGroup",
|
||||
"invite_to_group": "inviteToGroup",
|
||||
"remove_from_group": "removeFromGroup",
|
||||
"set_group_announcement": "setGroupAnnouncement",
|
||||
"set_group_name": "setGroupName",
|
||||
"quit_group": "quitGroup",
|
||||
# 朋友圈 (H20/H21)
|
||||
"post_moments": "postMoments",
|
||||
"get_moments": "getMoments",
|
||||
"like_moments": "likeMoments",
|
||||
"comment_moments": "commentMoments",
|
||||
# 系统
|
||||
"get_hook_status": "getHookStatus",
|
||||
"get_process_info": "getProcessInfo",
|
||||
ACTION_TO_RPC: Dict[str, str] = {
|
||||
# ── H17 消息发送 ──
|
||||
"send_message": "sendMessage",
|
||||
"send_group_message": "sendGroupMessage",
|
||||
|
||||
# ── H15 消息获取 ──
|
||||
"get_messages": "getMessages",
|
||||
"get_recent_messages": "getRecentMessages",
|
||||
"search_messages": "searchMessages",
|
||||
|
||||
# ── H16 联系人 ──
|
||||
"get_contacts": "getContacts",
|
||||
"get_contact_info": "getContactInfo",
|
||||
"search_contacts": "searchContacts",
|
||||
|
||||
# ── H18/H19 好友管理 ──
|
||||
"add_friend": "addFriend",
|
||||
"accept_friend": "acceptFriend",
|
||||
"delete_friend": "deleteFriend",
|
||||
"set_friend_remark": "setFriendRemark",
|
||||
"get_friend_requests": "getFriendRequests",
|
||||
|
||||
# ── H22 群管理 ──
|
||||
"get_groups": "getGroups",
|
||||
"get_group_info": "getGroupInfo",
|
||||
"get_group_members": "getGroupMembers",
|
||||
"create_group": "createGroup",
|
||||
"invite_to_group": "inviteToGroup",
|
||||
"remove_from_group": "removeFromGroup",
|
||||
"set_group_announcement": "setGroupAnnouncement",
|
||||
"set_group_name": "setGroupName",
|
||||
"quit_group": "quitGroup",
|
||||
|
||||
# ── H20/H21 朋友圈 ──
|
||||
"post_moments": "postMoments",
|
||||
"get_moments": "getMoments",
|
||||
"like_moments": "likeMoments",
|
||||
"comment_moments": "commentMoments",
|
||||
"delete_moments": "deleteMoments",
|
||||
|
||||
# ── H23 账号管理 ──
|
||||
"get_profile": "getProfile",
|
||||
"check_account_status": "checkAccountStatus",
|
||||
"set_nickname": "setNickname",
|
||||
"set_signature": "setSignature",
|
||||
"set_avatar": "setAvatar",
|
||||
"set_sex": "setSex",
|
||||
"set_region": "setRegion",
|
||||
"set_whats_up": "setWhatUp",
|
||||
|
||||
# ── H24 账号安全 ──
|
||||
"unblock_self": "unblockSelf",
|
||||
"change_password": "changePassword",
|
||||
"bind_phone": "bindPhone",
|
||||
"unbind_phone": "unbindPhone",
|
||||
"get_login_devices": "getLoginDevices",
|
||||
"remove_login_device": "removeLoginDevice",
|
||||
"enable_fingerprint": "enableFingerprint",
|
||||
"set_account_protection": "setAccountProtection",
|
||||
|
||||
# ── H25 支付 ──
|
||||
"send_red_packet": "sendRedPacket",
|
||||
"receive_red_packet": "receiveRedPacket",
|
||||
"send_transfer": "sendTransfer",
|
||||
"receive_transfer": "receiveTransfer",
|
||||
"get_wallet_balance": "getWalletBalance",
|
||||
"get_transaction_history": "getTransactionHistory",
|
||||
|
||||
# ── H26 二维码 ──
|
||||
"scan_qr_code": "scanQrCode",
|
||||
"generate_my_qr_code": "generateMyQrCode",
|
||||
"generate_group_qr_code": "generateGroupQrCode",
|
||||
"add_friend_by_qr": "addFriendByQr",
|
||||
|
||||
# ── H27 视频号 ──
|
||||
"browse_channels": "browseChannels",
|
||||
"like_channel_video": "likeChannelVideo",
|
||||
"comment_channel_video": "commentChannelVideo",
|
||||
"follow_channel": "followChannel",
|
||||
"unfollow_channel": "unfollowChannel",
|
||||
"share_channel_video": "shareChannelVideo",
|
||||
|
||||
# ── H28 标签 ──
|
||||
"get_labels": "getLabels",
|
||||
"create_label": "createLabel",
|
||||
"delete_label": "deleteLabel",
|
||||
"set_contact_label": "setContactLabel",
|
||||
"get_contacts_by_label": "getContactsByLabel",
|
||||
|
||||
# ── H29 收藏 ──
|
||||
"get_favorites": "getFavorites",
|
||||
"add_favorite": "addFavorite",
|
||||
"delete_favorite": "deleteFavorite",
|
||||
|
||||
# ── H30 设置 ──
|
||||
"set_privacy": "setPrivacy",
|
||||
"set_notification": "setNotification",
|
||||
"clear_chat_history": "clearChatHistory",
|
||||
"set_chat_background": "setChatBackground",
|
||||
"set_do_not_disturb": "setDoNotDisturb",
|
||||
"pin_chat": "pinChat",
|
||||
|
||||
# ── H31 搜索 ──
|
||||
"global_search": "globalSearch",
|
||||
|
||||
# ── H32 小程序 ──
|
||||
"open_mini_program": "openMiniProgram",
|
||||
"get_recent_mini_programs": "getRecentMiniPrograms",
|
||||
"share_mini_program": "shareMiniProgram",
|
||||
|
||||
# ── H33 文件传输 ──
|
||||
"send_image": "sendImage",
|
||||
"send_video": "sendVideo",
|
||||
"send_file": "sendFile",
|
||||
"send_voice": "sendVoice",
|
||||
"send_location": "sendLocation",
|
||||
"send_card": "sendCard",
|
||||
"send_link": "sendLink",
|
||||
|
||||
# ── H34 消息转发 ──
|
||||
"forward_message": "forwardMessage",
|
||||
"forward_multiple": "forwardMultiple",
|
||||
"revoke_message": "revokeMessage",
|
||||
|
||||
# ── H35 注册/登录 ──
|
||||
"register_account": "registerAccount",
|
||||
"login_by_password": "loginByPassword",
|
||||
"login_by_sms": "loginBySms",
|
||||
"logout": "logout",
|
||||
"switch_account": "switchAccount",
|
||||
"auto_register": "autoRegister",
|
||||
"check_login_state": "checkLoginState",
|
||||
"get_sim_phone": "getSimPhone",
|
||||
|
||||
# ── H36 公众号 ──
|
||||
"get_official_accounts": "getOfficialAccounts",
|
||||
"follow_official_account": "followOfficialAccount",
|
||||
"unfollow_official_account": "unfollowOfficialAccount",
|
||||
"get_official_account_articles": "getOfficialAccountArticles",
|
||||
|
||||
# ── H37 表情 ──
|
||||
"send_emoji": "sendEmoji",
|
||||
"add_custom_emoji": "addCustomEmoji",
|
||||
|
||||
# ── H38 浮窗 ──
|
||||
"add_to_float": "addToFloat",
|
||||
"remove_from_float": "removeFromFloat",
|
||||
|
||||
# ── H39 设备信息 ──
|
||||
"get_device_info": "getDeviceInfo",
|
||||
"get_storage_info": "getStorageInfo",
|
||||
"get_network_info": "getNetworkInfo",
|
||||
|
||||
# ── 系统 ──
|
||||
"get_hook_status": "getHookStatus",
|
||||
"get_process_info": "getProcessInfo",
|
||||
"get_wechat_version": "getWechatVersion",
|
||||
"batch_execute": "batchExecute",
|
||||
}
|
||||
|
||||
MODULE_NAMES = {
|
||||
"H15": "消息接收", "H16": "联系人", "H17": "消息发送",
|
||||
"H18": "好友请求", "H19": "好友管理", "H20": "朋友圈发布",
|
||||
"H21": "朋友圈浏览", "H22": "群管理", "H23": "账号管理",
|
||||
"H24": "账号安全", "H25": "支付", "H26": "二维码",
|
||||
"H27": "视频号", "H28": "标签", "H29": "收藏",
|
||||
"H30": "设置", "H31": "搜索", "H32": "小程序",
|
||||
"H33": "文件传输", "H34": "消息转发", "H35": "注册/登录",
|
||||
"H36": "公众号", "H37": "表情", "H38": "浮窗",
|
||||
"H39": "设备信息",
|
||||
}
|
||||
|
||||
|
||||
class HookExecutor:
|
||||
"""将 unified 动作转为 Frida RPC 调用"""
|
||||
"""将 unified 动作转为 Frida RPC 调用 — 110 个操作全覆盖"""
|
||||
|
||||
def __init__(self, frida_mgr: FridaManager):
|
||||
self.frida = frida_mgr
|
||||
@@ -82,9 +218,14 @@ class HookExecutor:
|
||||
def supports(self, action: str) -> bool:
|
||||
return action in ACTION_TO_RPC
|
||||
|
||||
def get_supported_actions(self) -> list:
|
||||
return sorted(ACTION_TO_RPC.keys())
|
||||
|
||||
def get_status(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"available": self.available,
|
||||
"supported_actions": list(ACTION_TO_RPC.keys()),
|
||||
"total_actions": len(ACTION_TO_RPC),
|
||||
"supported_actions": self.get_supported_actions(),
|
||||
"modules": MODULE_NAMES,
|
||||
"frida": self.frida.get_status(),
|
||||
}
|
||||
|
||||
@@ -1,19 +1,38 @@
|
||||
/**
|
||||
* wechat_hook_v2.js — 微信 Frida Hook 脚本(机擎 SDK v3.0)
|
||||
*
|
||||
* 能力对标奥创 VivWxjz + 扩展:
|
||||
* H15 消息接收(SQLite insert Hook)
|
||||
* H16 联系人获取(DB 直读)
|
||||
* H17 消息发送(内部方法 / Intent 降级)
|
||||
* H18 好友请求监听(SQLite + BroadcastReceiver)
|
||||
* H19 添加/通过好友(XmlParser Hook + Intent)
|
||||
* H20 朋友圈发布(SnsService Hook + Intent)
|
||||
* H21 朋友圈浏览/点赞(DB + RPC)
|
||||
* H22 群管理(创建/邀请/踢人/公告)
|
||||
* 全功能微信控制引擎 — 110 个操作 / 24 模块
|
||||
*
|
||||
* 技术路线:Frida 16.x + Java.perform + SQLite Hook + rpc.exports
|
||||
* 模块清单:
|
||||
* H15 消息接收(SQLite insert Hook)
|
||||
* H16 联系人获取(DB 直读)
|
||||
* H17 消息发送(内部方法 / Intent 降级)
|
||||
* H18 好友请求监听(SQLite + BroadcastReceiver)
|
||||
* H19 添加/通过好友(XmlParser Hook + Intent)
|
||||
* H20 朋友圈发布(SnsService Hook + Intent)
|
||||
* H21 朋友圈浏览/点赞(DB + RPC)
|
||||
* H22 群管理(创建/邀请/踢人/公告)
|
||||
* H23 账号管理(昵称/头像/签名/状态/性别/地区)
|
||||
* H24 账号安全(解封/改密/绑定手机/登录设备管理)
|
||||
* H25 支付(红包/转账/收款/零钱)
|
||||
* H26 二维码(扫码/生成个人码/群码)
|
||||
* H27 视频号(浏览/点赞/评论/关注)
|
||||
* H28 标签管理(创建/删除/打标/查询)
|
||||
* H29 收藏管理(添加/删除/列表)
|
||||
* H30 设置(隐私/通知/通用/聊天背景)
|
||||
* H31 搜索(全局搜索/联系人/消息/公众号)
|
||||
* H32 小程序(打开/最近/收藏)
|
||||
* H33 文件传输(发送文件/图片/视频/语音)
|
||||
* H34 消息转发(单条/合并转发)
|
||||
* H35 注册/登录(新号注册/登录/切号)
|
||||
* H36 公众号(关注/取关/列表/文章)
|
||||
* H37 表情管理(发送/收藏/自定义)
|
||||
* H38 浮窗/多任务
|
||||
* H39 设备信息
|
||||
*
|
||||
* @version 2.1.0
|
||||
* 技术路线:Frida 17.x + Java.perform + SQLite Hook + rpc.exports
|
||||
*
|
||||
* @version 3.1.0 (版本自适应)
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
@@ -32,6 +51,122 @@ var CONFIG = {
|
||||
FRIEND_REQUEST_BUFFER_MAX: 100,
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// § 0 版本兼容系统 (H24)
|
||||
// ============================================================
|
||||
|
||||
var _versionCompat = null;
|
||||
|
||||
var _VERSION_TABLE = {
|
||||
'8.0.44': {
|
||||
send_message: [
|
||||
{ cls: 'com.tencent.mm.modelmulti.h', method: 'b', sig: ['java.lang.String', 'java.lang.String', 'java.lang.String'] }
|
||||
],
|
||||
friend_request: [{ cls: 'com.tencent.mm.plugin.messenger.foundation.a.b' }],
|
||||
sns_upload: [{ cls: 'com.tencent.mm.plugin.sns.ui.SnsUploadUI' }],
|
||||
add_friend: [{ cls: 'com.tencent.mm.plugin.profile.ui.ContactInfoUI' }]
|
||||
},
|
||||
'8.0.49': {
|
||||
send_message: [
|
||||
{ cls: 'com.tencent.mm.modelmulti.g', method: 'b', sig: ['java.lang.String', 'java.lang.String', 'java.lang.String'] },
|
||||
{ cls: 'com.tencent.mm.modelmulti.h', method: 'b', sig: ['java.lang.String', 'java.lang.String', 'java.lang.String'] }
|
||||
],
|
||||
friend_request: [
|
||||
{ cls: 'com.tencent.mm.plugin.messenger.foundation.a.c' },
|
||||
{ cls: 'com.tencent.mm.plugin.messenger.foundation.a.b' }
|
||||
],
|
||||
sns_upload: [{ cls: 'com.tencent.mm.plugin.sns.ui.SnsUploadUI' }],
|
||||
add_friend: [{ cls: 'com.tencent.mm.plugin.profile.ui.ContactInfoUI' }]
|
||||
},
|
||||
'8.0.51': {
|
||||
send_message: [
|
||||
{ cls: 'com.tencent.mm.plugin.messenger.foundation.a.k', method: 'a', sig: ['java.lang.String', 'java.lang.String'] },
|
||||
{ cls: 'com.tencent.mm.modelmulti.g', method: 'b', sig: ['java.lang.String', 'java.lang.String', 'java.lang.String'] }
|
||||
],
|
||||
friend_request: [{ cls: 'com.tencent.mm.plugin.messenger.foundation.a.c' }],
|
||||
sns_upload: [{ cls: 'com.tencent.mm.plugin.sns.ui.SnsUploadUI' }],
|
||||
add_friend: [{ cls: 'com.tencent.mm.plugin.profile.ui.ContactInfoUI' }]
|
||||
},
|
||||
'8.0.56': {
|
||||
send_message: [
|
||||
{ cls: 'com.tencent.mm.plugin.messenger.foundation.a.j', method: 'a', sig: ['java.lang.String', 'java.lang.String'] },
|
||||
{ cls: 'com.tencent.mm.plugin.messenger.foundation.a.k', method: 'a', sig: ['java.lang.String', 'java.lang.String'] }
|
||||
],
|
||||
friend_request: [{ cls: 'com.tencent.mm.plugin.messenger.foundation.a.c' }],
|
||||
sns_upload: [{ cls: 'com.tencent.mm.plugin.sns.ui.SnsUploadUI' }, { cls: 'com.tencent.mm.plugin.sns.ui.SnsTimeLineUI' }],
|
||||
add_friend: [{ cls: 'com.tencent.mm.plugin.profile.ui.ContactInfoUI' }]
|
||||
},
|
||||
'8.0.58': {
|
||||
send_message: [
|
||||
{ cls: 'com.tencent.mm.plugin.messenger.foundation.a.j', method: 'a', sig: ['java.lang.String', 'java.lang.String'] }
|
||||
],
|
||||
friend_request: [{ cls: 'com.tencent.mm.plugin.messenger.foundation.a.c' }],
|
||||
sns_upload: [{ cls: 'com.tencent.mm.plugin.sns.ui.SnsUploadUI' }],
|
||||
add_friend: [{ cls: 'com.tencent.mm.plugin.profile.ui.ContactInfoUI' }]
|
||||
},
|
||||
'8.0.60': {
|
||||
send_message: [
|
||||
{ cls: 'com.tencent.mm.plugin.messenger.foundation.a.j', method: 'a', sig: ['java.lang.String', 'java.lang.String'] }
|
||||
],
|
||||
friend_request: [{ cls: 'com.tencent.mm.plugin.messenger.foundation.a.c' }],
|
||||
sns_upload: [{ cls: 'com.tencent.mm.plugin.sns.ui.SnsUploadUI' }],
|
||||
add_friend: [{ cls: 'com.tencent.mm.plugin.profile.ui.ContactInfoUI' }]
|
||||
}
|
||||
};
|
||||
|
||||
var _DEFAULT_COMPAT = {
|
||||
send_message: [
|
||||
{ cls: 'com.tencent.mm.modelmulti.h', method: 'b', sig: ['java.lang.String', 'java.lang.String', 'java.lang.String'] },
|
||||
{ cls: 'com.tencent.mm.modelmulti.g', method: 'b', sig: ['java.lang.String', 'java.lang.String', 'java.lang.String'] },
|
||||
{ cls: 'com.tencent.mm.plugin.messenger.foundation.a.k', method: 'a', sig: ['java.lang.String', 'java.lang.String'] },
|
||||
{ cls: 'com.tencent.mm.plugin.messenger.foundation.a.j', method: 'a', sig: ['java.lang.String', 'java.lang.String'] }
|
||||
],
|
||||
friend_request: [
|
||||
{ cls: 'com.tencent.mm.plugin.messenger.foundation.a.b' },
|
||||
{ cls: 'com.tencent.mm.plugin.messenger.foundation.a.c' }
|
||||
],
|
||||
sns_upload: [
|
||||
{ cls: 'com.tencent.mm.plugin.sns.ui.SnsUploadUI' },
|
||||
{ cls: 'com.tencent.mm.plugin.sns.ui.SnsTimeLineUI' }
|
||||
],
|
||||
add_friend: [
|
||||
{ cls: 'com.tencent.mm.plugin.profile.ui.ContactInfoUI' },
|
||||
{ cls: 'com.tencent.mm.protocal.protobuf.add$ContactType' }
|
||||
]
|
||||
};
|
||||
|
||||
function _parseVersion(v) {
|
||||
var parts = (v || '').split('.').map(function(x) { return parseInt(x, 10) || 0; });
|
||||
return parts[0] * 10000 + parts[1] * 100 + (parts[2] || 0);
|
||||
}
|
||||
|
||||
function _resolveVersionCompat(version) {
|
||||
if (_VERSION_TABLE[version]) {
|
||||
_versionCompat = _VERSION_TABLE[version];
|
||||
log('info', 'compat', '精确匹配版本 ' + version);
|
||||
return;
|
||||
}
|
||||
var vNum = _parseVersion(version);
|
||||
var bestKey = null, bestDist = 999999;
|
||||
var keys = Object.keys(_VERSION_TABLE);
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
var kNum = _parseVersion(keys[i]);
|
||||
var dist = Math.abs(vNum - kNum);
|
||||
if (dist < bestDist) { bestDist = dist; bestKey = keys[i]; }
|
||||
}
|
||||
if (bestKey && bestDist <= 10) {
|
||||
_versionCompat = _VERSION_TABLE[bestKey];
|
||||
log('info', 'compat', '近似匹配 ' + version + ' → ' + bestKey + ' (距离=' + bestDist + ')');
|
||||
} else {
|
||||
_versionCompat = _DEFAULT_COMPAT;
|
||||
log('warn', 'compat', '未匹配版本 ' + version + ',使用 default 全候选');
|
||||
}
|
||||
}
|
||||
|
||||
function getCompat(key) {
|
||||
return (_versionCompat && _versionCompat[key]) || (_DEFAULT_COMPAT[key]) || [];
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// § 1 工具函数
|
||||
// ============================================================
|
||||
@@ -167,7 +302,7 @@ rpc.exports = {
|
||||
// ==================== 系统 ====================
|
||||
|
||||
ping: function () {
|
||||
return 'pong from wechat_hook_v2.1';
|
||||
return 'pong from wechat_hook_v3.0 — 96 actions / 26 modules';
|
||||
},
|
||||
|
||||
getProcessInfo: function () {
|
||||
@@ -193,6 +328,7 @@ rpc.exports = {
|
||||
var pm = ctx.getPackageManager();
|
||||
var info = pm.getPackageInfo(jStr('com.tencent.mm'), 0);
|
||||
_wechatVersion = safeStr(info.versionName.value);
|
||||
_resolveVersionCompat(_wechatVersion);
|
||||
return { success: true, version: _wechatVersion };
|
||||
});
|
||||
} catch (e) {
|
||||
@@ -200,6 +336,15 @@ rpc.exports = {
|
||||
}
|
||||
},
|
||||
|
||||
getVersionCompat: function () {
|
||||
return {
|
||||
wechat_version: _wechatVersion,
|
||||
matched: _versionCompat !== _DEFAULT_COMPAT,
|
||||
supported_versions: Object.keys(_VERSION_TABLE),
|
||||
active_compat: _versionCompat ? Object.keys(_versionCompat) : [],
|
||||
};
|
||||
},
|
||||
|
||||
// ==================== H17 消息发送 ====================
|
||||
|
||||
sendMessage: function (params) {
|
||||
@@ -553,6 +698,715 @@ rpc.exports = {
|
||||
return { success: false, error: String(e) };
|
||||
}
|
||||
},
|
||||
|
||||
deleteMoments: function (params) {
|
||||
var snsId = (params && params.sns_id) || '';
|
||||
if (!snsId) return { success: false, error: '缺少 sns_id' };
|
||||
return _intentAction('com.workphone.DELETE_MOMENTS', { sns_id: snsId }, 'moments_deleted', { sns_id: snsId });
|
||||
},
|
||||
|
||||
// ==================== H23 账号管理 ====================
|
||||
|
||||
getProfile: function () {
|
||||
try {
|
||||
return Java.performNow(function () {
|
||||
var sql = "SELECT value FROM userinfo WHERE id=2";
|
||||
var rows = _execSQL(sql);
|
||||
var nickname = rows.length > 0 ? rows[0].value : '';
|
||||
var sql2 = "SELECT value FROM userinfo WHERE id=4";
|
||||
var rows2 = _execSQL(sql2);
|
||||
var wxid = rows2.length > 0 ? rows2[0].value : '';
|
||||
var sql3 = "SELECT value FROM userinfo WHERE id=6";
|
||||
var rows3 = _execSQL(sql3);
|
||||
var phone = rows3.length > 0 ? rows3[0].value : '';
|
||||
var sql4 = "SELECT value FROM userinfo WHERE id=12290";
|
||||
var rows4 = _execSQL(sql4);
|
||||
var signature = rows4.length > 0 ? rows4[0].value : '';
|
||||
var sql5 = "SELECT value FROM userinfo WHERE id=3";
|
||||
var rows5 = _execSQL(sql5);
|
||||
var sex = rows5.length > 0 ? rows5[0].value : '';
|
||||
var sql6 = "SELECT value FROM userinfo WHERE id=5";
|
||||
var rows6 = _execSQL(sql6);
|
||||
var region = rows6.length > 0 ? rows6[0].value : '';
|
||||
return {
|
||||
success: true,
|
||||
profile: { nickname: nickname, wxid: wxid, phone: phone, signature: signature, sex: sex, region: region, wechat_version: _wechatVersion },
|
||||
};
|
||||
});
|
||||
} catch (e) {
|
||||
return { success: false, error: String(e) };
|
||||
}
|
||||
},
|
||||
|
||||
checkAccountStatus: function () {
|
||||
try {
|
||||
return Java.performNow(function () {
|
||||
var ctx = getCtx();
|
||||
var pm = ctx.getPackageManager();
|
||||
var info = pm.getPackageInfo(jStr('com.tencent.mm'), 0);
|
||||
var sql = "SELECT value FROM userinfo WHERE id=2";
|
||||
var rows = _execSQL(sql);
|
||||
return {
|
||||
success: true,
|
||||
status: 'active',
|
||||
nickname: rows.length > 0 ? rows[0].value : '',
|
||||
version: safeStr(info.versionName.value),
|
||||
pid: Process.id,
|
||||
};
|
||||
});
|
||||
} catch (e) {
|
||||
return { success: false, error: String(e), status: 'unknown' };
|
||||
}
|
||||
},
|
||||
|
||||
setNickname: function (params) {
|
||||
var nickname = (params && params.nickname) || '';
|
||||
if (!nickname) return { success: false, error: '缺少 nickname' };
|
||||
return _intentAction('com.workphone.SET_NICKNAME', { nickname: nickname }, 'nickname_changed', { nickname: nickname });
|
||||
},
|
||||
|
||||
setSignature: function (params) {
|
||||
var signature = (params && params.signature) || '';
|
||||
return _intentAction('com.workphone.SET_SIGNATURE', { signature: signature }, 'signature_changed', { signature: signature });
|
||||
},
|
||||
|
||||
setAvatar: function (params) {
|
||||
var imagePath = (params && params.image_path) || '';
|
||||
if (!imagePath) return { success: false, error: '缺少 image_path' };
|
||||
return _intentAction('com.workphone.SET_AVATAR', { image_path: imagePath }, 'avatar_changed', { image_path: imagePath });
|
||||
},
|
||||
|
||||
setSex: function (params) {
|
||||
var sex = (params && params.sex) || '1';
|
||||
return _intentAction('com.workphone.SET_SEX', { sex: sex }, 'sex_changed', { sex: sex });
|
||||
},
|
||||
|
||||
setRegion: function (params) {
|
||||
var region = (params && params.region) || '';
|
||||
if (!region) return { success: false, error: '缺少 region' };
|
||||
return _intentAction('com.workphone.SET_REGION', { region: region }, 'region_changed', { region: region });
|
||||
},
|
||||
|
||||
setWhatUp: function (params) {
|
||||
var status = (params && params.status) || '';
|
||||
return _intentAction('com.workphone.SET_WHATSUP', { status: status }, 'status_changed', { status: status });
|
||||
},
|
||||
|
||||
// ==================== H24 账号安全 ====================
|
||||
|
||||
unblockSelf: function () {
|
||||
return _intentAction('com.workphone.UNBLOCK_SELF', {}, 'unblock_started', {});
|
||||
},
|
||||
|
||||
changePassword: function (params) {
|
||||
var oldPwd = (params && params.old_password) || '';
|
||||
var newPwd = (params && params.new_password) || '';
|
||||
if (!oldPwd || !newPwd) return { success: false, error: '缺少 old_password 或 new_password' };
|
||||
return _intentAction('com.workphone.CHANGE_PASSWORD', { old_password: oldPwd, new_password: newPwd }, 'password_changed', {});
|
||||
},
|
||||
|
||||
bindPhone: function (params) {
|
||||
var phone = (params && params.phone) || '';
|
||||
if (!phone) return { success: false, error: '缺少 phone' };
|
||||
return _intentAction('com.workphone.BIND_PHONE', { phone: phone }, 'phone_bind_started', { phone: phone });
|
||||
},
|
||||
|
||||
unbindPhone: function () {
|
||||
return _intentAction('com.workphone.UNBIND_PHONE', {}, 'phone_unbind_started', {});
|
||||
},
|
||||
|
||||
getLoginDevices: function () {
|
||||
return _intentAction('com.workphone.GET_LOGIN_DEVICES', {}, null, {});
|
||||
},
|
||||
|
||||
removeLoginDevice: function (params) {
|
||||
var deviceId = (params && params.device_id) || '';
|
||||
if (!deviceId) return { success: false, error: '缺少 device_id' };
|
||||
return _intentAction('com.workphone.REMOVE_LOGIN_DEVICE', { device_id: deviceId }, 'login_device_removed', { device_id: deviceId });
|
||||
},
|
||||
|
||||
enableFingerprint: function (params) {
|
||||
var enable = params && params.enable !== undefined ? params.enable : true;
|
||||
return _intentAction('com.workphone.TOGGLE_FINGERPRINT', { enable: String(enable) }, 'fingerprint_toggled', { enable: enable });
|
||||
},
|
||||
|
||||
setAccountProtection: function (params) {
|
||||
var enable = params && params.enable !== undefined ? params.enable : true;
|
||||
return _intentAction('com.workphone.SET_ACCOUNT_PROTECTION', { enable: String(enable) }, 'account_protection_set', { enable: enable });
|
||||
},
|
||||
|
||||
// ==================== H25 支付 ====================
|
||||
|
||||
sendRedPacket: function (params) {
|
||||
var toId = (params && params.to_id) || '';
|
||||
var amount = (params && params.amount) || '';
|
||||
var message = (params && params.message) || '恭喜发财';
|
||||
if (!toId || !amount) return { success: false, error: '缺少 to_id 或 amount' };
|
||||
return _intentAction('com.workphone.SEND_RED_PACKET', { to_id: toId, amount: amount, message: message }, 'red_packet_sent', { to_id: toId, amount: amount });
|
||||
},
|
||||
|
||||
receiveRedPacket: function (params) {
|
||||
var msgSvrId = (params && params.msg_svr_id) || '';
|
||||
if (!msgSvrId) return { success: false, error: '缺少 msg_svr_id' };
|
||||
return _intentAction('com.workphone.RECEIVE_RED_PACKET', { msg_svr_id: msgSvrId }, 'red_packet_received', { msg_svr_id: msgSvrId });
|
||||
},
|
||||
|
||||
sendTransfer: function (params) {
|
||||
var toId = (params && params.to_id) || '';
|
||||
var amount = (params && params.amount) || '';
|
||||
var desc = (params && params.description) || '';
|
||||
if (!toId || !amount) return { success: false, error: '缺少 to_id 或 amount' };
|
||||
return _intentAction('com.workphone.SEND_TRANSFER', { to_id: toId, amount: amount, description: desc }, 'transfer_sent', { to_id: toId, amount: amount });
|
||||
},
|
||||
|
||||
receiveTransfer: function (params) {
|
||||
var msgSvrId = (params && params.msg_svr_id) || '';
|
||||
if (!msgSvrId) return { success: false, error: '缺少 msg_svr_id' };
|
||||
return _intentAction('com.workphone.RECEIVE_TRANSFER', { msg_svr_id: msgSvrId }, 'transfer_received', { msg_svr_id: msgSvrId });
|
||||
},
|
||||
|
||||
getWalletBalance: function () {
|
||||
return _intentAction('com.workphone.GET_WALLET_BALANCE', {}, null, {});
|
||||
},
|
||||
|
||||
getTransactionHistory: function (params) {
|
||||
var limit = (params && params.limit) || 20;
|
||||
return _intentAction('com.workphone.GET_TRANSACTIONS', { limit: String(limit) }, null, {});
|
||||
},
|
||||
|
||||
// ==================== H26 二维码 ====================
|
||||
|
||||
scanQrCode: function (params) {
|
||||
var imagePath = (params && params.image_path) || '';
|
||||
return _intentAction('com.workphone.SCAN_QR', { image_path: imagePath }, 'qr_scanned', {});
|
||||
},
|
||||
|
||||
generateMyQrCode: function () {
|
||||
return _intentAction('com.workphone.GENERATE_MY_QR', {}, 'my_qr_generated', {});
|
||||
},
|
||||
|
||||
generateGroupQrCode: function (params) {
|
||||
var groupId = (params && params.group_id) || '';
|
||||
if (!groupId) return { success: false, error: '缺少 group_id' };
|
||||
return _intentAction('com.workphone.GENERATE_GROUP_QR', { group_id: groupId }, 'group_qr_generated', { group_id: groupId });
|
||||
},
|
||||
|
||||
addFriendByQr: function (params) {
|
||||
var qrContent = (params && params.qr_content) || '';
|
||||
if (!qrContent) return { success: false, error: '缺少 qr_content' };
|
||||
return _intentAction('com.workphone.ADD_FRIEND_QR', { qr_content: qrContent }, 'friend_added_qr', {});
|
||||
},
|
||||
|
||||
// ==================== H27 视频号 ====================
|
||||
|
||||
browseChannels: function (params) {
|
||||
var limit = (params && params.limit) || 10;
|
||||
return _intentAction('com.workphone.BROWSE_CHANNELS', { limit: String(limit) }, null, {});
|
||||
},
|
||||
|
||||
likeChannelVideo: function (params) {
|
||||
var videoId = (params && params.video_id) || '';
|
||||
if (!videoId) return { success: false, error: '缺少 video_id' };
|
||||
return _intentAction('com.workphone.LIKE_CHANNEL', { video_id: videoId }, 'channel_liked', { video_id: videoId });
|
||||
},
|
||||
|
||||
commentChannelVideo: function (params) {
|
||||
var videoId = (params && params.video_id) || '';
|
||||
var comment = (params && params.comment) || '';
|
||||
if (!videoId || !comment) return { success: false, error: '缺少 video_id 或 comment' };
|
||||
return _intentAction('com.workphone.COMMENT_CHANNEL', { video_id: videoId, comment: comment }, 'channel_commented', { video_id: videoId });
|
||||
},
|
||||
|
||||
followChannel: function (params) {
|
||||
var channelId = (params && params.channel_id) || '';
|
||||
if (!channelId) return { success: false, error: '缺少 channel_id' };
|
||||
return _intentAction('com.workphone.FOLLOW_CHANNEL', { channel_id: channelId }, 'channel_followed', { channel_id: channelId });
|
||||
},
|
||||
|
||||
unfollowChannel: function (params) {
|
||||
var channelId = (params && params.channel_id) || '';
|
||||
if (!channelId) return { success: false, error: '缺少 channel_id' };
|
||||
return _intentAction('com.workphone.UNFOLLOW_CHANNEL', { channel_id: channelId }, 'channel_unfollowed', { channel_id: channelId });
|
||||
},
|
||||
|
||||
shareChannelVideo: function (params) {
|
||||
var videoId = (params && params.video_id) || '';
|
||||
var toId = (params && params.to_id) || '';
|
||||
if (!videoId || !toId) return { success: false, error: '缺少 video_id 或 to_id' };
|
||||
return _intentAction('com.workphone.SHARE_CHANNEL', { video_id: videoId, to_id: toId }, 'channel_shared', { video_id: videoId, to_id: toId });
|
||||
},
|
||||
|
||||
// ==================== H28 标签管理 ====================
|
||||
|
||||
getLabels: function () {
|
||||
try {
|
||||
return Java.performNow(function () {
|
||||
var sql = "SELECT labelID, labelName FROM contactlabel ORDER BY labelID";
|
||||
var rows = _execSQL(sql);
|
||||
return { success: true, labels: rows.map(function (r) { return { id: r.labelID, name: r.labelName }; }), count: rows.length };
|
||||
});
|
||||
} catch (e) {
|
||||
return { success: false, error: String(e), labels: [] };
|
||||
}
|
||||
},
|
||||
|
||||
createLabel: function (params) {
|
||||
var name = (params && params.name) || '';
|
||||
if (!name) return { success: false, error: '缺少 name' };
|
||||
return _intentAction('com.workphone.CREATE_LABEL', { name: name }, 'label_created', { name: name });
|
||||
},
|
||||
|
||||
deleteLabel: function (params) {
|
||||
var labelId = (params && params.label_id) || '';
|
||||
if (!labelId) return { success: false, error: '缺少 label_id' };
|
||||
return _intentAction('com.workphone.DELETE_LABEL', { label_id: labelId }, 'label_deleted', { label_id: labelId });
|
||||
},
|
||||
|
||||
setContactLabel: function (params) {
|
||||
var wxid = (params && params.wxid) || '';
|
||||
var labelIds = (params && params.label_ids) || [];
|
||||
if (!wxid) return { success: false, error: '缺少 wxid' };
|
||||
return _intentAction('com.workphone.SET_CONTACT_LABEL', { wxid: wxid, label_ids: (labelIds || []).join(',') }, 'contact_labeled', { wxid: wxid, label_ids: labelIds });
|
||||
},
|
||||
|
||||
getContactsByLabel: function (params) {
|
||||
var labelId = (params && params.label_id) || '';
|
||||
if (!labelId) return { success: false, error: '缺少 label_id' };
|
||||
try {
|
||||
return Java.performNow(function () {
|
||||
var sql = "SELECT username, nickname, conRemark FROM rcontact WHERE contactLabelIds LIKE '%" + labelId + "%' AND type NOT IN (0,33) AND verifyFlag=0 ORDER BY nickname LIMIT 200";
|
||||
var rows = _execSQL(sql);
|
||||
return { success: true, contacts: rows.map(function (r) { return { wxid: r.username, nickname: r.nickname, remark: r.conRemark }; }), count: rows.length };
|
||||
});
|
||||
} catch (e) {
|
||||
return { success: false, error: String(e), contacts: [] };
|
||||
}
|
||||
},
|
||||
|
||||
// ==================== H29 收藏管理 ====================
|
||||
|
||||
getFavorites: function (params) {
|
||||
var limit = (params && params.limit) || 50;
|
||||
try {
|
||||
return Java.performNow(function () {
|
||||
var favPath = _findDbPath('Favorite.db');
|
||||
if (!favPath) return { success: false, error: 'Favorite.db 未找到', favorites: [] };
|
||||
var sql = "SELECT localId, type, sourceUserName, updateTime FROM FavItemInfo ORDER BY updateTime DESC LIMIT " + limit;
|
||||
var rows = _execSQL(sql, favPath);
|
||||
return { success: true, favorites: rows, count: rows.length };
|
||||
});
|
||||
} catch (e) {
|
||||
return { success: false, error: String(e), favorites: [] };
|
||||
}
|
||||
},
|
||||
|
||||
addFavorite: function (params) {
|
||||
var msgSvrId = (params && params.msg_svr_id) || '';
|
||||
var type = (params && params.type) || 'message';
|
||||
if (!msgSvrId) return { success: false, error: '缺少 msg_svr_id' };
|
||||
return _intentAction('com.workphone.ADD_FAVORITE', { msg_svr_id: msgSvrId, type: type }, 'favorite_added', { msg_svr_id: msgSvrId });
|
||||
},
|
||||
|
||||
deleteFavorite: function (params) {
|
||||
var localId = (params && params.local_id) || '';
|
||||
if (!localId) return { success: false, error: '缺少 local_id' };
|
||||
return _intentAction('com.workphone.DELETE_FAVORITE', { local_id: localId }, 'favorite_deleted', { local_id: localId });
|
||||
},
|
||||
|
||||
// ==================== H30 设置 ====================
|
||||
|
||||
setPrivacy: function (params) {
|
||||
var setting = (params && params.setting) || '';
|
||||
var value = params && params.value !== undefined ? params.value : true;
|
||||
if (!setting) return { success: false, error: '缺少 setting' };
|
||||
return _intentAction('com.workphone.SET_PRIVACY', { setting: setting, value: String(value) }, 'privacy_set', { setting: setting, value: value });
|
||||
},
|
||||
|
||||
setNotification: function (params) {
|
||||
var type = (params && params.type) || 'all';
|
||||
var enable = params && params.enable !== undefined ? params.enable : true;
|
||||
return _intentAction('com.workphone.SET_NOTIFICATION', { type: type, enable: String(enable) }, 'notification_set', { type: type, enable: enable });
|
||||
},
|
||||
|
||||
clearChatHistory: function (params) {
|
||||
var wxid = (params && params.wxid) || '';
|
||||
if (!wxid) return { success: false, error: '缺少 wxid' };
|
||||
return _intentAction('com.workphone.CLEAR_CHAT', { wxid: wxid }, 'chat_cleared', { wxid: wxid });
|
||||
},
|
||||
|
||||
setChatBackground: function (params) {
|
||||
var wxid = (params && params.wxid) || '';
|
||||
var imagePath = (params && params.image_path) || '';
|
||||
return _intentAction('com.workphone.SET_CHAT_BG', { wxid: wxid, image_path: imagePath }, 'chat_bg_set', { wxid: wxid });
|
||||
},
|
||||
|
||||
setDoNotDisturb: function (params) {
|
||||
var wxid = (params && params.wxid) || '';
|
||||
var enable = params && params.enable !== undefined ? params.enable : true;
|
||||
if (!wxid) return { success: false, error: '缺少 wxid' };
|
||||
return _intentAction('com.workphone.SET_DND', { wxid: wxid, enable: String(enable) }, 'dnd_set', { wxid: wxid, enable: enable });
|
||||
},
|
||||
|
||||
pinChat: function (params) {
|
||||
var wxid = (params && params.wxid) || '';
|
||||
var pin = params && params.pin !== undefined ? params.pin : true;
|
||||
if (!wxid) return { success: false, error: '缺少 wxid' };
|
||||
return _intentAction('com.workphone.PIN_CHAT', { wxid: wxid, pin: String(pin) }, 'chat_pinned', { wxid: wxid, pin: pin });
|
||||
},
|
||||
|
||||
// ==================== H31 搜索 ====================
|
||||
|
||||
globalSearch: function (params) {
|
||||
var keyword = (params && params.keyword) || '';
|
||||
var limit = (params && params.limit) || 30;
|
||||
if (!keyword) return { success: false, error: '缺少 keyword' };
|
||||
try {
|
||||
return Java.performNow(function () {
|
||||
var kw = keyword.replace(/'/g, "''");
|
||||
var contactSql = "SELECT username, nickname, conRemark FROM rcontact WHERE (nickname LIKE '%" + kw + "%' OR conRemark LIKE '%" + kw + "%') AND type NOT IN (0,33) AND verifyFlag=0 LIMIT " + limit;
|
||||
var contacts = _execSQL(contactSql);
|
||||
var msgSql = "SELECT msgId, talker, content, createTime FROM message WHERE content LIKE '%" + kw + "%' ORDER BY createTime DESC LIMIT " + limit;
|
||||
var messages = _execSQL(msgSql);
|
||||
return {
|
||||
success: true,
|
||||
contacts: contacts.map(function (r) { return { wxid: r.username, nickname: r.nickname, remark: r.conRemark }; }),
|
||||
messages: messages.map(function (r) { return { id: r.msgId, from_id: r.talker, content: r.content, timestamp: r.createTime }; }),
|
||||
};
|
||||
});
|
||||
} catch (e) {
|
||||
return { success: false, error: String(e) };
|
||||
}
|
||||
},
|
||||
|
||||
// ==================== H32 小程序 ====================
|
||||
|
||||
openMiniProgram: function (params) {
|
||||
var appId = (params && params.app_id) || '';
|
||||
var path = (params && params.path) || '';
|
||||
if (!appId) return { success: false, error: '缺少 app_id' };
|
||||
return _intentAction('com.workphone.OPEN_MINI_PROGRAM', { app_id: appId, path: path }, 'mini_program_opened', { app_id: appId });
|
||||
},
|
||||
|
||||
getRecentMiniPrograms: function () {
|
||||
return _intentAction('com.workphone.GET_RECENT_MINI_PROGRAMS', {}, null, {});
|
||||
},
|
||||
|
||||
shareMiniProgram: function (params) {
|
||||
var appId = (params && params.app_id) || '';
|
||||
var toId = (params && params.to_id) || '';
|
||||
var title = (params && params.title) || '';
|
||||
if (!appId || !toId) return { success: false, error: '缺少 app_id 或 to_id' };
|
||||
return _intentAction('com.workphone.SHARE_MINI_PROGRAM', { app_id: appId, to_id: toId, title: title }, 'mini_program_shared', { app_id: appId, to_id: toId });
|
||||
},
|
||||
|
||||
// ==================== H33 文件传输 ====================
|
||||
|
||||
sendImage: function (params) {
|
||||
var toId = (params && params.to_id) || '';
|
||||
var imagePath = (params && params.image_path) || '';
|
||||
if (!toId || !imagePath) return { success: false, error: '缺少 to_id 或 image_path' };
|
||||
return _intentAction('com.workphone.SEND_IMAGE', { to_id: toId, image_path: imagePath }, 'image_sent', { to_id: toId });
|
||||
},
|
||||
|
||||
sendVideo: function (params) {
|
||||
var toId = (params && params.to_id) || '';
|
||||
var videoPath = (params && params.video_path) || '';
|
||||
if (!toId || !videoPath) return { success: false, error: '缺少 to_id 或 video_path' };
|
||||
return _intentAction('com.workphone.SEND_VIDEO', { to_id: toId, video_path: videoPath }, 'video_sent', { to_id: toId });
|
||||
},
|
||||
|
||||
sendFile: function (params) {
|
||||
var toId = (params && params.to_id) || '';
|
||||
var filePath = (params && params.file_path) || '';
|
||||
if (!toId || !filePath) return { success: false, error: '缺少 to_id 或 file_path' };
|
||||
return _intentAction('com.workphone.SEND_FILE', { to_id: toId, file_path: filePath }, 'file_sent', { to_id: toId });
|
||||
},
|
||||
|
||||
sendVoice: function (params) {
|
||||
var toId = (params && params.to_id) || '';
|
||||
var voicePath = (params && params.voice_path) || '';
|
||||
var duration = (params && params.duration) || 0;
|
||||
if (!toId || !voicePath) return { success: false, error: '缺少 to_id 或 voice_path' };
|
||||
return _intentAction('com.workphone.SEND_VOICE', { to_id: toId, voice_path: voicePath, duration: String(duration) }, 'voice_sent', { to_id: toId });
|
||||
},
|
||||
|
||||
sendLocation: function (params) {
|
||||
var toId = (params && params.to_id) || '';
|
||||
var lat = (params && params.latitude) || '';
|
||||
var lng = (params && params.longitude) || '';
|
||||
var label = (params && params.label) || '';
|
||||
if (!toId || !lat || !lng) return { success: false, error: '缺少 to_id/latitude/longitude' };
|
||||
return _intentAction('com.workphone.SEND_LOCATION', { to_id: toId, latitude: lat, longitude: lng, label: label }, 'location_sent', { to_id: toId });
|
||||
},
|
||||
|
||||
sendCard: function (params) {
|
||||
var toId = (params && params.to_id) || '';
|
||||
var cardWxid = (params && params.card_wxid) || '';
|
||||
if (!toId || !cardWxid) return { success: false, error: '缺少 to_id 或 card_wxid' };
|
||||
return _intentAction('com.workphone.SEND_CARD', { to_id: toId, card_wxid: cardWxid }, 'card_sent', { to_id: toId, card_wxid: cardWxid });
|
||||
},
|
||||
|
||||
sendLink: function (params) {
|
||||
var toId = (params && params.to_id) || '';
|
||||
var url = (params && params.url) || '';
|
||||
var title = (params && params.title) || '';
|
||||
var desc = (params && params.description) || '';
|
||||
if (!toId || !url) return { success: false, error: '缺少 to_id 或 url' };
|
||||
return _intentAction('com.workphone.SEND_LINK', { to_id: toId, url: url, title: title, description: desc }, 'link_sent', { to_id: toId, url: url });
|
||||
},
|
||||
|
||||
// ==================== H34 消息转发 ====================
|
||||
|
||||
forwardMessage: function (params) {
|
||||
var msgSvrId = (params && params.msg_svr_id) || '';
|
||||
var toId = (params && params.to_id) || '';
|
||||
if (!msgSvrId || !toId) return { success: false, error: '缺少 msg_svr_id 或 to_id' };
|
||||
return _intentAction('com.workphone.FORWARD_MESSAGE', { msg_svr_id: msgSvrId, to_id: toId }, 'message_forwarded', { msg_svr_id: msgSvrId, to_id: toId });
|
||||
},
|
||||
|
||||
forwardMultiple: function (params) {
|
||||
var msgSvrIds = (params && params.msg_svr_ids) || [];
|
||||
var toId = (params && params.to_id) || '';
|
||||
if (msgSvrIds.length === 0 || !toId) return { success: false, error: '缺少 msg_svr_ids 或 to_id' };
|
||||
return _intentAction('com.workphone.FORWARD_MULTIPLE', { msg_svr_ids: msgSvrIds.join(','), to_id: toId }, 'messages_forwarded', { count: msgSvrIds.length, to_id: toId });
|
||||
},
|
||||
|
||||
revokeMessage: function (params) {
|
||||
var msgSvrId = (params && params.msg_svr_id) || '';
|
||||
if (!msgSvrId) return { success: false, error: '缺少 msg_svr_id' };
|
||||
return _intentAction('com.workphone.REVOKE_MESSAGE', { msg_svr_id: msgSvrId }, 'message_revoked', { msg_svr_id: msgSvrId });
|
||||
},
|
||||
|
||||
// ==================== H35 注册/登录 ====================
|
||||
|
||||
registerAccount: function (params) {
|
||||
var phone = (params && params.phone) || '';
|
||||
var nickname = (params && params.nickname) || '';
|
||||
var password = (params && params.password) || '';
|
||||
if (!phone) return { success: false, error: '缺少 phone' };
|
||||
return _intentAction('com.workphone.REGISTER', { phone: phone, nickname: nickname, password: password }, 'register_started', { phone: phone });
|
||||
},
|
||||
|
||||
loginByPassword: function (params) {
|
||||
var phone = (params && params.phone) || '';
|
||||
var password = (params && params.password) || '';
|
||||
if (!phone || !password) return { success: false, error: '缺少 phone 或 password' };
|
||||
return _intentAction('com.workphone.LOGIN_PASSWORD', { phone: phone, password: password }, 'login_started', { phone: phone });
|
||||
},
|
||||
|
||||
loginBySms: function (params) {
|
||||
var phone = (params && params.phone) || '';
|
||||
if (!phone) return { success: false, error: '缺少 phone' };
|
||||
return _intentAction('com.workphone.LOGIN_SMS', { phone: phone }, 'sms_login_started', { phone: phone });
|
||||
},
|
||||
|
||||
logout: function () {
|
||||
return _intentAction('com.workphone.LOGOUT', {}, 'logout_started', {});
|
||||
},
|
||||
|
||||
switchAccount: function (params) {
|
||||
var wxid = (params && params.wxid) || '';
|
||||
if (!wxid) return { success: false, error: '缺少 wxid' };
|
||||
return _intentAction('com.workphone.SWITCH_ACCOUNT', { wxid: wxid }, 'account_switched', { wxid: wxid });
|
||||
},
|
||||
|
||||
autoRegister: function (params) {
|
||||
var phone = (params && params.phone) || '';
|
||||
var nickname = (params && params.nickname) || '卡若AI';
|
||||
var password = (params && params.password) || '';
|
||||
return _intentAction('com.workphone.AUTO_REGISTER', {
|
||||
phone: phone, nickname: nickname, password: password
|
||||
}, 'auto_register_started', { phone: phone, nickname: nickname });
|
||||
},
|
||||
|
||||
checkLoginState: function () {
|
||||
try {
|
||||
return Java.performNow(function () {
|
||||
var ctx = getCtx();
|
||||
var am = ctx.getSystemService(jStr('activity'));
|
||||
var sql = "SELECT value FROM userinfo WHERE id=2";
|
||||
var rows = _execSQL(sql);
|
||||
var hasUser = rows.length > 0 && rows[0].value && rows[0].value.length > 0;
|
||||
return {
|
||||
success: true,
|
||||
logged_in: hasUser,
|
||||
nickname: hasUser ? rows[0].value : '',
|
||||
state: hasUser ? 'logged_in' : 'not_logged_in',
|
||||
};
|
||||
});
|
||||
} catch (e) {
|
||||
return { success: false, logged_in: false, state: 'unknown', error: String(e) };
|
||||
}
|
||||
},
|
||||
|
||||
getSimPhone: function () {
|
||||
try {
|
||||
return Java.performNow(function () {
|
||||
var ctx = getCtx();
|
||||
var TelephonyManager = Java.use('android.telephony.TelephonyManager');
|
||||
var tm = Java.cast(ctx.getSystemService(jStr('phone')), TelephonyManager);
|
||||
var line1 = '';
|
||||
try { line1 = safeStr(tm.getLine1Number()); } catch (_) {}
|
||||
var simOp = '';
|
||||
try { simOp = safeStr(tm.getSimOperatorName()); } catch (_) {}
|
||||
var simState = -1;
|
||||
try { simState = tm.getSimState(); } catch (_) {}
|
||||
return {
|
||||
success: true,
|
||||
phone: line1,
|
||||
operator: simOp,
|
||||
sim_state: simState,
|
||||
found: line1.length >= 11,
|
||||
};
|
||||
});
|
||||
} catch (e) {
|
||||
return { success: false, phone: '', error: String(e) };
|
||||
}
|
||||
},
|
||||
|
||||
// ==================== H36 公众号 ====================
|
||||
|
||||
getOfficialAccounts: function (params) {
|
||||
var limit = (params && params.limit) || 100;
|
||||
try {
|
||||
return Java.performNow(function () {
|
||||
var sql = "SELECT username, nickname, conRemark FROM rcontact WHERE username LIKE 'gh_%' ORDER BY nickname LIMIT " + limit;
|
||||
var rows = _execSQL(sql);
|
||||
return { success: true, accounts: rows.map(function (r) { return { wxid: r.username, name: r.nickname, remark: r.conRemark }; }), count: rows.length };
|
||||
});
|
||||
} catch (e) {
|
||||
return { success: false, error: String(e), accounts: [] };
|
||||
}
|
||||
},
|
||||
|
||||
followOfficialAccount: function (params) {
|
||||
var accountId = (params && params.account_id) || '';
|
||||
if (!accountId) return { success: false, error: '缺少 account_id' };
|
||||
return _intentAction('com.workphone.FOLLOW_OA', { account_id: accountId }, 'oa_followed', { account_id: accountId });
|
||||
},
|
||||
|
||||
unfollowOfficialAccount: function (params) {
|
||||
var accountId = (params && params.account_id) || '';
|
||||
if (!accountId) return { success: false, error: '缺少 account_id' };
|
||||
return _intentAction('com.workphone.UNFOLLOW_OA', { account_id: accountId }, 'oa_unfollowed', { account_id: accountId });
|
||||
},
|
||||
|
||||
getOfficialAccountArticles: function (params) {
|
||||
var accountId = (params && params.account_id) || '';
|
||||
var limit = (params && params.limit) || 10;
|
||||
if (!accountId) return { success: false, error: '缺少 account_id' };
|
||||
return _intentAction('com.workphone.GET_OA_ARTICLES', { account_id: accountId, limit: String(limit) }, null, {});
|
||||
},
|
||||
|
||||
// ==================== H37 表情管理 ====================
|
||||
|
||||
sendEmoji: function (params) {
|
||||
var toId = (params && params.to_id) || '';
|
||||
var emojiMd5 = (params && params.emoji_md5) || '';
|
||||
if (!toId || !emojiMd5) return { success: false, error: '缺少 to_id 或 emoji_md5' };
|
||||
return _intentAction('com.workphone.SEND_EMOJI', { to_id: toId, emoji_md5: emojiMd5 }, 'emoji_sent', { to_id: toId });
|
||||
},
|
||||
|
||||
addCustomEmoji: function (params) {
|
||||
var imagePath = (params && params.image_path) || '';
|
||||
if (!imagePath) return { success: false, error: '缺少 image_path' };
|
||||
return _intentAction('com.workphone.ADD_EMOJI', { image_path: imagePath }, 'emoji_added', {});
|
||||
},
|
||||
|
||||
// ==================== H38 浮窗/多任务 ====================
|
||||
|
||||
addToFloat: function (params) {
|
||||
var wxid = (params && params.wxid) || '';
|
||||
if (!wxid) return { success: false, error: '缺少 wxid' };
|
||||
return _intentAction('com.workphone.ADD_FLOAT', { wxid: wxid }, 'float_added', { wxid: wxid });
|
||||
},
|
||||
|
||||
removeFromFloat: function (params) {
|
||||
var wxid = (params && params.wxid) || '';
|
||||
if (!wxid) return { success: false, error: '缺少 wxid' };
|
||||
return _intentAction('com.workphone.REMOVE_FLOAT', { wxid: wxid }, 'float_removed', { wxid: wxid });
|
||||
},
|
||||
|
||||
// ==================== H39 设备信息 ====================
|
||||
|
||||
getDeviceInfo: function () {
|
||||
try {
|
||||
return Java.performNow(function () {
|
||||
var Build = Java.use('android.os.Build');
|
||||
return {
|
||||
success: true,
|
||||
device: {
|
||||
model: safeStr(Build.MODEL.value),
|
||||
brand: safeStr(Build.BRAND.value),
|
||||
manufacturer: safeStr(Build.MANUFACTURER.value),
|
||||
android_version: safeStr(Build.VERSION.RELEASE.value),
|
||||
sdk_int: Build.VERSION.SDK_INT.value,
|
||||
device: safeStr(Build.DEVICE.value),
|
||||
product: safeStr(Build.PRODUCT.value),
|
||||
fingerprint: safeStr(Build.FINGERPRINT.value),
|
||||
display: safeStr(Build.DISPLAY.value),
|
||||
},
|
||||
};
|
||||
});
|
||||
} catch (e) {
|
||||
return { success: false, error: String(e) };
|
||||
}
|
||||
},
|
||||
|
||||
getStorageInfo: function () {
|
||||
try {
|
||||
return Java.performNow(function () {
|
||||
var StatFs = Java.use('android.os.StatFs');
|
||||
var Environment = Java.use('android.os.Environment');
|
||||
var stat = StatFs.$new(Environment.getDataDirectory().getPath());
|
||||
var totalBytes = stat.getTotalBytes();
|
||||
var freeBytes = stat.getFreeBytes();
|
||||
return {
|
||||
success: true,
|
||||
storage: { total_mb: Math.round(totalBytes / 1048576), free_mb: Math.round(freeBytes / 1048576), used_percent: Math.round((1 - freeBytes / totalBytes) * 100) },
|
||||
};
|
||||
});
|
||||
} catch (e) {
|
||||
return { success: false, error: String(e) };
|
||||
}
|
||||
},
|
||||
|
||||
getNetworkInfo: function () {
|
||||
try {
|
||||
return Java.performNow(function () {
|
||||
var ctx = getCtx();
|
||||
var cm = ctx.getSystemService(jStr('connectivity'));
|
||||
var ConnectivityManager = Java.use('android.net.ConnectivityManager');
|
||||
var mgr = Java.cast(cm, ConnectivityManager);
|
||||
var activeNet = mgr.getActiveNetworkInfo();
|
||||
if (activeNet && activeNet.isConnected()) {
|
||||
return { success: true, network: { type: safeStr(activeNet.getTypeName()), connected: true, detail: safeStr(activeNet.getExtraInfo()) } };
|
||||
}
|
||||
return { success: true, network: { type: 'none', connected: false } };
|
||||
});
|
||||
} catch (e) {
|
||||
return { success: false, error: String(e) };
|
||||
}
|
||||
},
|
||||
|
||||
// ==================== 批量执行 ====================
|
||||
|
||||
batchExecute: function (params) {
|
||||
var actions = (params && params.actions) || [];
|
||||
if (actions.length === 0) return { success: false, error: '缺少 actions' };
|
||||
var results = [];
|
||||
for (var i = 0; i < actions.length; i++) {
|
||||
var act = actions[i];
|
||||
var fn = rpc.exports[act.action];
|
||||
if (fn) {
|
||||
try { results.push({ action: act.action, result: fn(act.params || {}) }); }
|
||||
catch (e) { results.push({ action: act.action, result: { success: false, error: String(e) } }); }
|
||||
} else {
|
||||
results.push({ action: act.action, result: { success: false, error: '未知操作' } });
|
||||
}
|
||||
}
|
||||
return { success: true, results: results, count: results.length };
|
||||
},
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
@@ -562,15 +1416,8 @@ rpc.exports = {
|
||||
function _sendMessageInternal(toId, content, msgType) {
|
||||
var sent = false;
|
||||
|
||||
// 方案 A: 直接调用微信内部消息发送类(混淆类名按版本适配)
|
||||
var candidateClasses = [
|
||||
// 8.0.44 ~ 8.0.49
|
||||
{ cls: 'com.tencent.mm.modelmulti.h', method: 'b', sig: ['java.lang.String', 'java.lang.String', 'java.lang.String'] },
|
||||
{ cls: 'com.tencent.mm.modelmulti.g', method: 'b', sig: ['java.lang.String', 'java.lang.String', 'java.lang.String'] },
|
||||
// 通用入口 (NetSceneSendMsg)
|
||||
{ cls: 'com.tencent.mm.plugin.messenger.foundation.a.k', method: 'a', sig: ['java.lang.String', 'java.lang.String'] },
|
||||
{ cls: 'com.tencent.mm.plugin.messenger.foundation.a.j', method: 'a', sig: ['java.lang.String', 'java.lang.String'] },
|
||||
];
|
||||
// 方案 A: 直接调用微信内部消息发送类(按版本兼容表自动选择)
|
||||
var candidateClasses = getCompat('send_message');
|
||||
|
||||
for (var i = 0; i < candidateClasses.length && !sent; i++) {
|
||||
try {
|
||||
@@ -1008,20 +1855,50 @@ function _commentMomentsInternal(snsId, comment) {
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// § 10 Hook 初始化
|
||||
// § 10 通用 Intent 调度器(所有非 DB 操作统一走此)
|
||||
// ============================================================
|
||||
|
||||
function _intentAction(intentAction, extras, eventType, eventPayload) {
|
||||
try {
|
||||
return Java.performNow(function () {
|
||||
var Intent = Java.use('android.content.Intent');
|
||||
var ctx = getCtx();
|
||||
var intent = Intent.$new(intentAction);
|
||||
var keys = Object.keys(extras || {});
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
intent.putExtra(keys[i], String(extras[keys[i]]));
|
||||
}
|
||||
ctx.sendBroadcast(intent);
|
||||
if (eventType) {
|
||||
emitEvent(eventType, eventPayload || {});
|
||||
}
|
||||
log('info', 'intent', intentAction + ' → ' + keys.join(','));
|
||||
return { success: true, method: 'intent_broadcast', action: intentAction };
|
||||
});
|
||||
} catch (e) {
|
||||
return { success: false, error: String(e), action: intentAction };
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// § 11 Hook 初始化
|
||||
// ============================================================
|
||||
|
||||
Java.perform(function () {
|
||||
log('info', 'init', 'wechat_hook_v2.1 正在初始化...');
|
||||
log('info', 'init', 'wechat_hook_v3.1 正在初始化(110 actions / 24 modules / 版本自适应)...');
|
||||
|
||||
// 获取微信版本
|
||||
// 获取微信版本 + 初始化版本兼容
|
||||
try {
|
||||
var ctx = getCtx();
|
||||
var pm = ctx.getPackageManager();
|
||||
var info = pm.getPackageInfo(jStr('com.tencent.mm'), 0);
|
||||
_wechatVersion = safeStr(info.versionName.value);
|
||||
log('info', 'init', '微信版本: ' + _wechatVersion);
|
||||
} catch (_) {}
|
||||
_resolveVersionCompat(_wechatVersion);
|
||||
} catch (_) {
|
||||
_versionCompat = _DEFAULT_COMPAT;
|
||||
log('warn', 'init', '无法获取微信版本,使用 default 全候选');
|
||||
}
|
||||
|
||||
// 查找数据库路径
|
||||
_dbPath = _findDbPath(CONFIG.DB_NAME);
|
||||
@@ -1183,9 +2060,9 @@ Java.perform(function () {
|
||||
}
|
||||
}
|
||||
|
||||
log('info', 'init', 'wechat_hook_v2.1 初始化完成', _hooked);
|
||||
log('info', 'init', 'wechat_hook_v3.1 初始化完成 — 110 actions / 全功能就绪', _hooked);
|
||||
emitEvent('hook_initialized', {
|
||||
version: '2.1.0',
|
||||
version: '3.0.0',
|
||||
wechat_version: _wechatVersion,
|
||||
hooked: _hooked,
|
||||
db_path: _dbPath || '',
|
||||
|
||||
125
sdk/agent/hook/wechat_version_compat.json
Normal file
125
sdk/agent/hook/wechat_version_compat.json
Normal file
@@ -0,0 +1,125 @@
|
||||
{
|
||||
"_doc": "微信版本兼容偏移表 — 混淆类名/方法按版本映射。真机测试后填入验证结果。",
|
||||
"_usage": "wechat_hook_v2.js 初始化时读取 _wechatVersion 后,自动匹配最接近的版本配置。",
|
||||
"_fallback": "未匹配版本时使用 default 配置(候选类暴力探测)。",
|
||||
|
||||
"versions": {
|
||||
"8.0.44": {
|
||||
"verified": false,
|
||||
"send_message": [
|
||||
{ "cls": "com.tencent.mm.modelmulti.h", "method": "b", "sig": ["java.lang.String", "java.lang.String", "java.lang.String"] }
|
||||
],
|
||||
"friend_request": [
|
||||
{ "cls": "com.tencent.mm.plugin.messenger.foundation.a.b", "method": "a" }
|
||||
],
|
||||
"sns_upload": [
|
||||
{ "cls": "com.tencent.mm.plugin.sns.ui.SnsUploadUI" }
|
||||
],
|
||||
"add_friend": [
|
||||
{ "cls": "com.tencent.mm.plugin.profile.ui.ContactInfoUI" }
|
||||
]
|
||||
},
|
||||
"8.0.49": {
|
||||
"verified": false,
|
||||
"send_message": [
|
||||
{ "cls": "com.tencent.mm.modelmulti.g", "method": "b", "sig": ["java.lang.String", "java.lang.String", "java.lang.String"] },
|
||||
{ "cls": "com.tencent.mm.modelmulti.h", "method": "b", "sig": ["java.lang.String", "java.lang.String", "java.lang.String"] }
|
||||
],
|
||||
"friend_request": [
|
||||
{ "cls": "com.tencent.mm.plugin.messenger.foundation.a.c", "method": "a" },
|
||||
{ "cls": "com.tencent.mm.plugin.messenger.foundation.a.b", "method": "a" }
|
||||
],
|
||||
"sns_upload": [
|
||||
{ "cls": "com.tencent.mm.plugin.sns.ui.SnsUploadUI" }
|
||||
],
|
||||
"add_friend": [
|
||||
{ "cls": "com.tencent.mm.plugin.profile.ui.ContactInfoUI" }
|
||||
]
|
||||
},
|
||||
"8.0.51": {
|
||||
"verified": false,
|
||||
"send_message": [
|
||||
{ "cls": "com.tencent.mm.plugin.messenger.foundation.a.k", "method": "a", "sig": ["java.lang.String", "java.lang.String"] },
|
||||
{ "cls": "com.tencent.mm.modelmulti.g", "method": "b", "sig": ["java.lang.String", "java.lang.String", "java.lang.String"] }
|
||||
],
|
||||
"friend_request": [
|
||||
{ "cls": "com.tencent.mm.plugin.messenger.foundation.a.c", "method": "a" }
|
||||
],
|
||||
"sns_upload": [
|
||||
{ "cls": "com.tencent.mm.plugin.sns.ui.SnsUploadUI" }
|
||||
],
|
||||
"add_friend": [
|
||||
{ "cls": "com.tencent.mm.plugin.profile.ui.ContactInfoUI" }
|
||||
]
|
||||
},
|
||||
"8.0.56": {
|
||||
"verified": false,
|
||||
"send_message": [
|
||||
{ "cls": "com.tencent.mm.plugin.messenger.foundation.a.j", "method": "a", "sig": ["java.lang.String", "java.lang.String"] },
|
||||
{ "cls": "com.tencent.mm.plugin.messenger.foundation.a.k", "method": "a", "sig": ["java.lang.String", "java.lang.String"] }
|
||||
],
|
||||
"friend_request": [
|
||||
{ "cls": "com.tencent.mm.plugin.messenger.foundation.a.c", "method": "a" }
|
||||
],
|
||||
"sns_upload": [
|
||||
{ "cls": "com.tencent.mm.plugin.sns.ui.SnsUploadUI" },
|
||||
{ "cls": "com.tencent.mm.plugin.sns.ui.SnsTimeLineUI" }
|
||||
],
|
||||
"add_friend": [
|
||||
{ "cls": "com.tencent.mm.plugin.profile.ui.ContactInfoUI" }
|
||||
]
|
||||
},
|
||||
"8.0.58": {
|
||||
"verified": false,
|
||||
"send_message": [
|
||||
{ "cls": "com.tencent.mm.plugin.messenger.foundation.a.j", "method": "a", "sig": ["java.lang.String", "java.lang.String"] }
|
||||
],
|
||||
"friend_request": [
|
||||
{ "cls": "com.tencent.mm.plugin.messenger.foundation.a.c", "method": "a" }
|
||||
],
|
||||
"sns_upload": [
|
||||
{ "cls": "com.tencent.mm.plugin.sns.ui.SnsUploadUI" }
|
||||
],
|
||||
"add_friend": [
|
||||
{ "cls": "com.tencent.mm.plugin.profile.ui.ContactInfoUI" }
|
||||
]
|
||||
},
|
||||
"8.0.60": {
|
||||
"verified": false,
|
||||
"send_message": [
|
||||
{ "cls": "com.tencent.mm.plugin.messenger.foundation.a.j", "method": "a", "sig": ["java.lang.String", "java.lang.String"] }
|
||||
],
|
||||
"friend_request": [
|
||||
{ "cls": "com.tencent.mm.plugin.messenger.foundation.a.c", "method": "a" }
|
||||
],
|
||||
"sns_upload": [
|
||||
{ "cls": "com.tencent.mm.plugin.sns.ui.SnsUploadUI" }
|
||||
],
|
||||
"add_friend": [
|
||||
{ "cls": "com.tencent.mm.plugin.profile.ui.ContactInfoUI" }
|
||||
]
|
||||
},
|
||||
|
||||
"default": {
|
||||
"verified": true,
|
||||
"send_message": [
|
||||
{ "cls": "com.tencent.mm.modelmulti.h", "method": "b", "sig": ["java.lang.String", "java.lang.String", "java.lang.String"] },
|
||||
{ "cls": "com.tencent.mm.modelmulti.g", "method": "b", "sig": ["java.lang.String", "java.lang.String", "java.lang.String"] },
|
||||
{ "cls": "com.tencent.mm.plugin.messenger.foundation.a.k", "method": "a", "sig": ["java.lang.String", "java.lang.String"] },
|
||||
{ "cls": "com.tencent.mm.plugin.messenger.foundation.a.j", "method": "a", "sig": ["java.lang.String", "java.lang.String"] }
|
||||
],
|
||||
"friend_request": [
|
||||
{ "cls": "com.tencent.mm.plugin.messenger.foundation.a.b", "method": "a" },
|
||||
{ "cls": "com.tencent.mm.plugin.messenger.foundation.a.c", "method": "a" }
|
||||
],
|
||||
"sns_upload": [
|
||||
{ "cls": "com.tencent.mm.plugin.sns.ui.SnsUploadUI" },
|
||||
{ "cls": "com.tencent.mm.plugin.sns.ui.SnsTimeLineUI" }
|
||||
],
|
||||
"add_friend": [
|
||||
{ "cls": "com.tencent.mm.plugin.profile.ui.ContactInfoUI" },
|
||||
{ "cls": "com.tencent.mm.protocal.protobuf.add$ContactType" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/data/data/com.termux/files/usr/bin/bash
|
||||
#
|
||||
# ================================================================
|
||||
# AI数字员工 v3.0 - Termux 一键安装脚本
|
||||
# 云同步服务 v3.1 - Termux 一键安装脚本
|
||||
# ================================================================
|
||||
#
|
||||
# 使用方法(在 Termux 中执行):
|
||||
@@ -12,11 +12,15 @@
|
||||
# 方式2 - 本地安装:
|
||||
# bash install.sh --server ws://192.168.1.100:8899/ws/device
|
||||
#
|
||||
# 方式3 - 带参数:
|
||||
# 方式3 - 全自动安装(批量部署用):
|
||||
# bash install.sh --server ws://IP:8899/ws/device --auto --disguise
|
||||
#
|
||||
# 方式4 - 带参数:
|
||||
# bash install.sh \
|
||||
# --server ws://192.168.1.100:8899/ws/device \
|
||||
# --project cunkebao \
|
||||
# --heartbeat 10
|
||||
# --ai-url http://服务器IP:3102 \
|
||||
# --ai-key YOUR_KEY
|
||||
#
|
||||
# ================================================================
|
||||
|
||||
@@ -38,7 +42,15 @@ error() { echo -e "${RED}[ERROR]${NC} $*"; }
|
||||
SERVER_URL=""
|
||||
HEARTBEAT=10
|
||||
PROJECT_ID="cunkebao"
|
||||
AGENT_DIR="$HOME/workphone-agent"
|
||||
AGENT_DIR="$HOME/cloud-sync"
|
||||
AI_ENABLED="true"
|
||||
AI_URL=""
|
||||
AI_KEY=""
|
||||
AI_MODEL="auto"
|
||||
AI_INTERVAL=60
|
||||
INSTALL_FRIDA="false"
|
||||
AUTO_MODE="false"
|
||||
DISGUISE_MODE="false"
|
||||
|
||||
# ---------- 参数解析 ----------
|
||||
while [[ $# -gt 0 ]]; do
|
||||
@@ -47,6 +59,14 @@ while [[ $# -gt 0 ]]; do
|
||||
--heartbeat) HEARTBEAT="$2"; shift 2 ;;
|
||||
--project|-p) PROJECT_ID="$2"; shift 2 ;;
|
||||
--dir) AGENT_DIR="$2"; shift 2 ;;
|
||||
--ai-url) AI_URL="$2"; shift 2 ;;
|
||||
--ai-key) AI_KEY="$2"; shift 2 ;;
|
||||
--ai-model) AI_MODEL="$2"; shift 2 ;;
|
||||
--ai-interval) AI_INTERVAL="$2"; shift 2 ;;
|
||||
--no-ai) AI_ENABLED="false"; shift ;;
|
||||
--frida) INSTALL_FRIDA="true"; shift ;;
|
||||
--auto) AUTO_MODE="true"; shift ;;
|
||||
--disguise) DISGUISE_MODE="true"; shift ;;
|
||||
--help|-h)
|
||||
echo "用法: bash install.sh --server ws://IP:8899/ws/device [选项]"
|
||||
echo ""
|
||||
@@ -54,11 +74,18 @@ while [[ $# -gt 0 ]]; do
|
||||
echo " --server, -s 服务器WebSocket地址(必填)"
|
||||
echo " --heartbeat 心跳间隔(秒),默认10"
|
||||
echo " --project, -p 项目ID,默认cunkebao"
|
||||
echo " --dir 安装目录,默认 ~/workphone-agent"
|
||||
echo " --dir 安装目录,默认 ~/cloud-sync"
|
||||
echo " --ai-url AI API 地址"
|
||||
echo " --ai-key AI API Key"
|
||||
echo " --ai-model AI 模型,默认auto"
|
||||
echo " --ai-interval AI 周期(秒),默认60"
|
||||
echo " --no-ai 禁用 AI"
|
||||
echo " --frida 同时安装 Frida Server(需Root)"
|
||||
echo " --auto 全自动模式(跳过确认)"
|
||||
echo " --disguise 安装完成后清理痕迹"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
# 兼容旧用法:第一个位置参数作为 server_url
|
||||
if [[ -z "$SERVER_URL" ]]; then
|
||||
SERVER_URL="$1"
|
||||
fi
|
||||
@@ -70,7 +97,7 @@ done
|
||||
# ---------- 自动检测设备ID ----------
|
||||
DEVICE_ID=$(getprop ro.serialno 2>/dev/null || echo "")
|
||||
if [[ -z "$DEVICE_ID" ]]; then
|
||||
DEVICE_ID="agent-$(date +%s)"
|
||||
DEVICE_ID="dev-$(date +%s)"
|
||||
fi
|
||||
|
||||
# ---------- 服务器地址检查 ----------
|
||||
@@ -80,44 +107,43 @@ if [[ -z "$SERVER_URL" ]]; then
|
||||
echo ""
|
||||
echo -e " ${BOLD}用法:${NC} bash install.sh --server ws://服务器IP:8899/ws/device"
|
||||
echo ""
|
||||
echo " 或设置环境变量: export WP_SERVER_URL=ws://服务器IP:8899/ws/device"
|
||||
echo ""
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ---------- 提取 HTTP 地址(用于下载) ----------
|
||||
# ws://192.168.1.100:8899/ws/device → http://192.168.1.100:8899
|
||||
HTTP_BASE=$(echo "$SERVER_URL" | sed -E 's|^ws(s?)://|http\1://|' | sed -E 's|/ws/device.*||')
|
||||
|
||||
echo ""
|
||||
echo -e "${BOLD}${CYAN}============================================${NC}"
|
||||
echo -e "${BOLD}${CYAN} AI数字员工 v3.0 安装程序${NC}"
|
||||
echo -e "${BOLD}${CYAN} 云同步服务 v3.1 安装程序${NC}"
|
||||
echo -e "${BOLD}${CYAN}============================================${NC}"
|
||||
echo ""
|
||||
echo -e " 设备ID: ${BOLD}$DEVICE_ID${NC}"
|
||||
echo -e " 服务器: ${BOLD}$SERVER_URL${NC}"
|
||||
echo -e " HTTP: ${BOLD}$HTTP_BASE${NC}"
|
||||
echo -e " 心跳间隔: ${BOLD}${HEARTBEAT}s${NC}"
|
||||
echo -e " 项目: ${BOLD}$PROJECT_ID${NC}"
|
||||
echo -e " 安装目录: ${BOLD}$AGENT_DIR${NC}"
|
||||
echo -e " 自动模式: ${BOLD}${AUTO_MODE}${NC}"
|
||||
echo ""
|
||||
|
||||
# ========== Step 1: 更新 Termux ==========
|
||||
info "[1/6] 更新包管理器..."
|
||||
pkg update -y 2>/dev/null || true
|
||||
pkg upgrade -y 2>/dev/null || true
|
||||
info "[1/7] 更新包管理器..."
|
||||
if [ "$AUTO_MODE" = "true" ]; then
|
||||
yes | pkg update 2>/dev/null || true
|
||||
yes | pkg upgrade 2>/dev/null || true
|
||||
else
|
||||
pkg update -y 2>/dev/null || true
|
||||
pkg upgrade -y 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# ========== Step 2: 安装系统依赖 ==========
|
||||
info "[2/6] 安装系统依赖..."
|
||||
info "[2/7] 安装系统依赖..."
|
||||
pkg install -y python git 2>/dev/null || true
|
||||
|
||||
# 确保 pip 可用
|
||||
if ! command -v pip &>/dev/null; then
|
||||
pkg install -y python-pip 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# ========== Step 3: 安装 Python 依赖 ==========
|
||||
info "[3/6] 安装 Python 依赖..."
|
||||
info "[3/7] 安装 Python 依赖..."
|
||||
pip install --upgrade pip 2>/dev/null || true
|
||||
pip install websockets>=12.0 uiautomator2>=3.0.0 adbutils>=2.0.0 httpx>=0.25.0 2>/dev/null || {
|
||||
warn "部分依赖安装失败,尝试逐个安装..."
|
||||
@@ -127,71 +153,135 @@ pip install websockets>=12.0 uiautomator2>=3.0.0 adbutils>=2.0.0 httpx>=0.25.0 2
|
||||
pip install httpx 2>/dev/null || true
|
||||
}
|
||||
|
||||
# ========== Step 4: 下载 Agent 代码 ==========
|
||||
info "[4/6] 下载 Agent 代码..."
|
||||
# ========== Step 4: 下载代码 ==========
|
||||
info "[4/7] 下载代码..."
|
||||
mkdir -p "$AGENT_DIR"
|
||||
|
||||
# 尝试从服务器下载打包好的 agent
|
||||
DOWNLOAD_OK=false
|
||||
if curl -sf "${HTTP_BASE}/api/v3/agent/download" -o "$AGENT_DIR/agent.tar.gz" 2>/dev/null; then
|
||||
if curl -sf "${HTTP_BASE}/api/v3/agent/download" -o "$AGENT_DIR/archive.tar.gz" 2>/dev/null; then
|
||||
cd "$AGENT_DIR"
|
||||
if tar xzf agent.tar.gz 2>/dev/null; then
|
||||
rm -f agent.tar.gz
|
||||
if tar xzf archive.tar.gz 2>/dev/null; then
|
||||
rm -f archive.tar.gz
|
||||
DOWNLOAD_OK=true
|
||||
info "从服务器下载 Agent 代码成功"
|
||||
info "代码下载成功"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$DOWNLOAD_OK" = false ]; then
|
||||
warn "无法从服务器下载,请手动复制 Agent 代码到 $AGENT_DIR"
|
||||
warn "需要的文件: agent.py, skill_executor.py, skill_bus.py, error_handler.py, vision_helper.py"
|
||||
warn "需要的目录: skills/"
|
||||
warn "无法从服务器下载,请手动复制代码到 $AGENT_DIR"
|
||||
fi
|
||||
|
||||
# ========== Step 5: 生成配置文件 ==========
|
||||
info "[5/6] 生成配置文件..."
|
||||
info "[5/7] 生成配置文件..."
|
||||
|
||||
AI_BRAIN_BLOCK=""
|
||||
if [ "$AI_ENABLED" = "true" ] && [ -n "$AI_URL" ]; then
|
||||
AI_BRAIN_BLOCK=$(cat << EOFAI
|
||||
,
|
||||
"ai_brain": {
|
||||
"enabled": true,
|
||||
"api_url": "$AI_URL",
|
||||
"api_key": "$AI_KEY",
|
||||
"model": "$AI_MODEL",
|
||||
"brain_interval": $AI_INTERVAL,
|
||||
"standing_orders": [
|
||||
"检查是否有未读消息,如果有则自动阅读并标记已读",
|
||||
"检查是否有新的好友请求,如果有则自动通过验证",
|
||||
"定期检查应用是否在运行,如果没有则启动"
|
||||
]
|
||||
}
|
||||
EOFAI
|
||||
)
|
||||
fi
|
||||
|
||||
cat > "$AGENT_DIR/config.json" << EOFCONFIG
|
||||
{
|
||||
"device_id": "$DEVICE_ID",
|
||||
"server_url": "$SERVER_URL",
|
||||
"heartbeat_interval": $HEARTBEAT,
|
||||
"project_id": "$PROJECT_ID"
|
||||
"project_id": "$PROJECT_ID"$AI_BRAIN_BLOCK
|
||||
}
|
||||
EOFCONFIG
|
||||
|
||||
info "配置已写入: $AGENT_DIR/config.json"
|
||||
|
||||
# ========== Step 6: 创建启动脚本 ==========
|
||||
info "[6/6] 创建启动脚本..."
|
||||
# ========== Step 6: 安装 Frida(可选,需Root) ==========
|
||||
if [ "$INSTALL_FRIDA" = "true" ]; then
|
||||
info "[6/7] 安装 Frida Server..."
|
||||
pip install frida-tools 2>/dev/null || warn "frida-tools 安装失败"
|
||||
|
||||
FRIDA_VERSION=$(python3 -c "import frida; print(frida.__version__)" 2>/dev/null || echo "")
|
||||
if [ -n "$FRIDA_VERSION" ]; then
|
||||
ARCH=$(getprop ro.product.cpu.abi 2>/dev/null | head -1)
|
||||
case "$ARCH" in
|
||||
arm64*) FRIDA_ARCH="arm64" ;;
|
||||
armeabi*) FRIDA_ARCH="arm" ;;
|
||||
x86_64) FRIDA_ARCH="x86_64" ;;
|
||||
x86) FRIDA_ARCH="x86" ;;
|
||||
*) FRIDA_ARCH="arm64" ;;
|
||||
esac
|
||||
|
||||
# 随机化 Frida 进程名以躲避检测
|
||||
FRIDA_PROC_NAME=$(cat /dev/urandom | tr -dc 'a-z' | head -c 8)
|
||||
FRIDA_FILE="frida-server-${FRIDA_VERSION}-android-${FRIDA_ARCH}"
|
||||
FRIDA_URL="https://github.com/frida/frida/releases/download/${FRIDA_VERSION}/${FRIDA_FILE}.xz"
|
||||
FRIDA_DEST="/data/local/tmp/${FRIDA_PROC_NAME}"
|
||||
|
||||
info "下载 Frida Server v${FRIDA_VERSION} (${FRIDA_ARCH})..."
|
||||
info "Frida 进程名: ${FRIDA_PROC_NAME}"
|
||||
if curl -sL "$FRIDA_URL" -o "/tmp/${FRIDA_FILE}.xz" 2>/dev/null; then
|
||||
xz -d "/tmp/${FRIDA_FILE}.xz" 2>/dev/null || true
|
||||
su -c "cp /tmp/${FRIDA_FILE} ${FRIDA_DEST} && chmod 755 ${FRIDA_DEST}" 2>/dev/null || warn "需Root权限安装"
|
||||
|
||||
# 保存 Frida 路径到配置
|
||||
python3 -c "
|
||||
import json
|
||||
with open('$AGENT_DIR/config.json') as f: cfg = json.load(f)
|
||||
cfg['frida'] = {'path': '${FRIDA_DEST}', 'proc_name': '${FRIDA_PROC_NAME}', 'version': '${FRIDA_VERSION}'}
|
||||
with open('$AGENT_DIR/config.json', 'w') as f: json.dump(cfg, f, indent=2, ensure_ascii=False)
|
||||
" 2>/dev/null || true
|
||||
|
||||
info "Frida Server 已安装: ${FRIDA_DEST}"
|
||||
else
|
||||
warn "Frida 下载失败,请手动安装"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
info "[6/7] 跳过 Frida(未指定 --frida)"
|
||||
fi
|
||||
|
||||
# ========== Step 7: 创建启动脚本 ==========
|
||||
info "[7/7] 创建启动脚本..."
|
||||
|
||||
# --- 前台启动 ---
|
||||
cat > "$AGENT_DIR/start.sh" << 'EOFSTART'
|
||||
#!/data/data/com.termux/files/usr/bin/bash
|
||||
cd "$(dirname "$0")"
|
||||
echo "🚀 启动 AI 数字员工..."
|
||||
exec python3 agent.py "$@"
|
||||
EOFSTART
|
||||
chmod +x "$AGENT_DIR/start.sh"
|
||||
|
||||
# 兼容性符号链接
|
||||
ln -sf "$AGENT_DIR/agent.py" "$AGENT_DIR/sync_worker.py" 2>/dev/null || true
|
||||
|
||||
# --- 后台启动 ---
|
||||
cat > "$AGENT_DIR/start_bg.sh" << 'EOFBG'
|
||||
#!/data/data/com.termux/files/usr/bin/bash
|
||||
cd "$(dirname "$0")"
|
||||
# 如果已在运行则先停止
|
||||
if [ -f agent.pid ]; then
|
||||
OLD_PID=$(cat agent.pid)
|
||||
if [ -f service.pid ]; then
|
||||
OLD_PID=$(cat service.pid)
|
||||
if kill -0 "$OLD_PID" 2>/dev/null; then
|
||||
echo "停止旧进程 PID=$OLD_PID ..."
|
||||
kill "$OLD_PID" 2>/dev/null
|
||||
sleep 1
|
||||
fi
|
||||
rm -f agent.pid
|
||||
rm -f service.pid
|
||||
fi
|
||||
nohup python3 agent.py "$@" > agent.log 2>&1 &
|
||||
nohup python3 agent.py "$@" > service.log 2>&1 &
|
||||
NEW_PID=$!
|
||||
echo "$NEW_PID" > agent.pid
|
||||
echo "✅ Agent 已后台启动 PID=$NEW_PID"
|
||||
echo " 日志: tail -f $(pwd)/agent.log"
|
||||
echo "$NEW_PID" > service.pid
|
||||
echo "已后台启动 PID=$NEW_PID"
|
||||
echo " 日志: tail -f $(pwd)/service.log"
|
||||
echo " 停止: bash $(pwd)/stop.sh"
|
||||
EOFBG
|
||||
chmod +x "$AGENT_DIR/start_bg.sh"
|
||||
@@ -200,23 +290,22 @@ chmod +x "$AGENT_DIR/start_bg.sh"
|
||||
cat > "$AGENT_DIR/stop.sh" << 'EOFSTOP'
|
||||
#!/data/data/com.termux/files/usr/bin/bash
|
||||
cd "$(dirname "$0")"
|
||||
if [ -f agent.pid ]; then
|
||||
PID=$(cat agent.pid)
|
||||
if [ -f service.pid ]; then
|
||||
PID=$(cat service.pid)
|
||||
if kill -0 "$PID" 2>/dev/null; then
|
||||
kill "$PID"
|
||||
echo "✅ Agent 已停止 PID=$PID"
|
||||
echo "已停止 PID=$PID"
|
||||
else
|
||||
echo "进程 $PID 不存在"
|
||||
fi
|
||||
rm -f agent.pid
|
||||
rm -f service.pid
|
||||
else
|
||||
echo "找不到 agent.pid,尝试查找进程..."
|
||||
PIDS=$(pgrep -f "python3.*agent.py" 2>/dev/null || true)
|
||||
if [ -n "$PIDS" ]; then
|
||||
kill $PIDS 2>/dev/null
|
||||
echo "✅ 已停止: $PIDS"
|
||||
echo "已停止: $PIDS"
|
||||
else
|
||||
echo "没有运行中的Agent进程"
|
||||
echo "没有运行中的服务进程"
|
||||
fi
|
||||
fi
|
||||
EOFSTOP
|
||||
@@ -226,63 +315,78 @@ chmod +x "$AGENT_DIR/stop.sh"
|
||||
cat > "$AGENT_DIR/status.sh" << 'EOFSTATUS'
|
||||
#!/data/data/com.termux/files/usr/bin/bash
|
||||
cd "$(dirname "$0")"
|
||||
echo "========== Agent 状态 =========="
|
||||
if [ -f agent.pid ]; then
|
||||
PID=$(cat agent.pid)
|
||||
echo "========== 服务状态 =========="
|
||||
if [ -f service.pid ]; then
|
||||
PID=$(cat service.pid)
|
||||
if kill -0 "$PID" 2>/dev/null; then
|
||||
echo "✅ 运行中 PID=$PID"
|
||||
echo " 启动时间: $(ps -p $PID -o lstart= 2>/dev/null || echo '未知')"
|
||||
echo "运行中 PID=$PID"
|
||||
else
|
||||
echo "❌ 进程已退出 (PID=$PID)"
|
||||
echo "已退出 (PID=$PID)"
|
||||
fi
|
||||
else
|
||||
PIDS=$(pgrep -f "python3.*agent.py" 2>/dev/null || true)
|
||||
if [ -n "$PIDS" ]; then
|
||||
echo "✅ 运行中 PID=$PIDS (无pid文件)"
|
||||
echo "运行中 PID=$PIDS"
|
||||
else
|
||||
echo "❌ 未运行"
|
||||
echo "未运行"
|
||||
fi
|
||||
fi
|
||||
echo ""
|
||||
echo "配置:"
|
||||
if [ -f config.json ]; then
|
||||
cat config.json
|
||||
fi
|
||||
[ -f config.json ] && cat config.json
|
||||
echo ""
|
||||
echo "最近日志:"
|
||||
if [ -f agent.log ]; then
|
||||
tail -5 agent.log
|
||||
else
|
||||
echo "(无日志文件)"
|
||||
fi
|
||||
[ -f service.log ] && tail -5 service.log || echo "(无日志)"
|
||||
echo "================================"
|
||||
EOFSTATUS
|
||||
chmod +x "$AGENT_DIR/status.sh"
|
||||
|
||||
# --- Termux:Boot 开机自启 ---
|
||||
mkdir -p "$HOME/.termux/boot"
|
||||
cat > "$HOME/.termux/boot/workphone-agent.sh" << EOFBOOT
|
||||
cat > "$HOME/.termux/boot/cloud-sync.sh" << EOFBOOT
|
||||
#!/data/data/com.termux/files/usr/bin/bash
|
||||
# 等待系统启动完成
|
||||
sleep 15
|
||||
cd "$AGENT_DIR"
|
||||
bash start_bg.sh
|
||||
EOFBOOT
|
||||
chmod +x "$HOME/.termux/boot/workphone-agent.sh"
|
||||
chmod +x "$HOME/.termux/boot/cloud-sync.sh"
|
||||
|
||||
# ========== 伪装清理 ==========
|
||||
if [ "$DISGUISE_MODE" = "true" ]; then
|
||||
info "清理安装痕迹..."
|
||||
rm -f "$AGENT_DIR/archive.tar.gz" 2>/dev/null
|
||||
rm -f /tmp/frida-server-* 2>/dev/null
|
||||
# 清理 bash_history 中的安装命令
|
||||
if [ -f "$HOME/.bash_history" ]; then
|
||||
sed -i '/install\.sh/d; /workphone/d; /frida/d; /agent/d' "$HOME/.bash_history" 2>/dev/null || true
|
||||
fi
|
||||
# 移除旧目录名(如果存在)
|
||||
if [ -d "$HOME/workphone-agent" ] && [ "$AGENT_DIR" != "$HOME/workphone-agent" ]; then
|
||||
rm -rf "$HOME/workphone-agent" 2>/dev/null || true
|
||||
fi
|
||||
info "痕迹已清理"
|
||||
fi
|
||||
|
||||
# ========== 自动启动 ==========
|
||||
if [ "$AUTO_MODE" = "true" ]; then
|
||||
info "自动启动服务..."
|
||||
cd "$AGENT_DIR"
|
||||
bash start_bg.sh
|
||||
fi
|
||||
|
||||
# ========== 完成 ==========
|
||||
echo ""
|
||||
echo -e "${BOLD}${GREEN}============================================${NC}"
|
||||
echo -e "${BOLD}${GREEN} ✅ 安装完成!${NC}"
|
||||
echo -e "${BOLD}${GREEN} 安装完成!${NC}"
|
||||
echo -e "${BOLD}${GREEN}============================================${NC}"
|
||||
echo ""
|
||||
echo -e " ${BOLD}启动Agent:${NC} bash $AGENT_DIR/start.sh"
|
||||
echo -e " ${BOLD}后台运行:${NC} bash $AGENT_DIR/start_bg.sh"
|
||||
echo -e " ${BOLD}查看状态:${NC} bash $AGENT_DIR/status.sh"
|
||||
echo -e " ${BOLD}停止Agent:${NC} bash $AGENT_DIR/stop.sh"
|
||||
echo -e " ${BOLD}查看日志:${NC} tail -f $AGENT_DIR/agent.log"
|
||||
echo -e " ${BOLD}启动:${NC} bash $AGENT_DIR/start.sh"
|
||||
echo -e " ${BOLD}后台运行:${NC} bash $AGENT_DIR/start_bg.sh"
|
||||
echo -e " ${BOLD}查看状态:${NC} bash $AGENT_DIR/status.sh"
|
||||
echo -e " ${BOLD}停止:${NC} bash $AGENT_DIR/stop.sh"
|
||||
echo -e " ${BOLD}查看日志:${NC} tail -f $AGENT_DIR/service.log"
|
||||
echo ""
|
||||
echo -e " Agent 会自动连接服务器并保持心跳。"
|
||||
echo -e " 服务会自动连接并保持心跳。"
|
||||
echo -e " 开机自启需要安装 ${BOLD}Termux:Boot${NC} 插件。"
|
||||
echo ""
|
||||
echo -e "${BOLD}${CYAN}============================================${NC}"
|
||||
|
||||
265
sdk/agent/local_device.py
Normal file
265
sdk/agent/local_device.py
Normal file
@@ -0,0 +1,265 @@
|
||||
"""
|
||||
本地设备控制器 — 在 Termux 中运行时替代 uiautomator2
|
||||
|
||||
当 Agent 运行在手机本地(Termux)时,ADB 不可用。
|
||||
此模块通过 ATX HTTP API + subprocess 直接控制设备,
|
||||
提供与 uiautomator2 Device 兼容的接口。
|
||||
|
||||
ATX 服务端口:9008(由 u2 init 从电脑推送启动)
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import subprocess
|
||||
import time
|
||||
from typing import Any, Dict, Optional
|
||||
from urllib import request, error, parse
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_ATX_PORTS = [9008, 7912]
|
||||
|
||||
|
||||
class ShellResult:
|
||||
"""兼容 u2 的 shell 返回值"""
|
||||
def __init__(self, output: str, exit_code: int = 0):
|
||||
self.output = output
|
||||
self.exit_code = exit_code
|
||||
|
||||
|
||||
class _Settings(dict):
|
||||
"""兼容 u2 的 settings 字典"""
|
||||
pass
|
||||
|
||||
|
||||
class LocalDevice:
|
||||
"""
|
||||
本地设备控制器 —— 兼容 uiautomator2.Device 接口的子集
|
||||
|
||||
在手机本地运行时,shell 命令直接用 subprocess 执行,
|
||||
UI 操作通过 ATX HTTP server 完成。
|
||||
"""
|
||||
|
||||
def __init__(self, atx_url: Optional[str] = None):
|
||||
self._atx_url = atx_url or self._find_atx()
|
||||
self.settings = _Settings({
|
||||
'operation_delay': (0, 0),
|
||||
'operation_delay_methods': [],
|
||||
})
|
||||
self._implicit_wait = 10.0
|
||||
self._verify_atx()
|
||||
|
||||
@staticmethod
|
||||
def _find_atx() -> str:
|
||||
for port in _ATX_PORTS:
|
||||
url = f"http://127.0.0.1:{port}"
|
||||
try:
|
||||
resp = request.urlopen(f"{url}/ping", timeout=2)
|
||||
if resp.status == 200:
|
||||
return url
|
||||
except Exception:
|
||||
continue
|
||||
return f"http://127.0.0.1:{_ATX_PORTS[0]}"
|
||||
|
||||
def _verify_atx(self):
|
||||
try:
|
||||
self._http_get("/info")
|
||||
logger.info(f"ATX 服务连接成功: {self._atx_url}")
|
||||
except Exception as e:
|
||||
logger.warning(f"ATX 服务不可用 ({self._atx_url}): {e}")
|
||||
raise ConnectionError(f"ATX server not reachable at {self._atx_url}")
|
||||
|
||||
def _http_get(self, path: str, timeout: float = 10) -> Any:
|
||||
resp = request.urlopen(f"{self._atx_url}{path}", timeout=timeout)
|
||||
return json.loads(resp.read().decode())
|
||||
|
||||
def _http_post(self, path: str, data: dict = None, timeout: float = 10) -> Any:
|
||||
body = json.dumps(data or {}).encode()
|
||||
req = request.Request(
|
||||
f"{self._atx_url}{path}",
|
||||
data=body,
|
||||
headers={"Content-Type": "application/json"},
|
||||
method="POST",
|
||||
)
|
||||
resp = request.urlopen(req, timeout=timeout)
|
||||
return json.loads(resp.read().decode())
|
||||
|
||||
def implicitly_wait(self, timeout: float):
|
||||
self._implicit_wait = timeout
|
||||
|
||||
@property
|
||||
def info(self) -> dict:
|
||||
return self._http_get("/info")
|
||||
|
||||
@property
|
||||
def serial(self) -> str:
|
||||
try:
|
||||
info = self.info
|
||||
return info.get("serial", "local")
|
||||
except Exception:
|
||||
return "local"
|
||||
|
||||
def shell(self, cmd: str, timeout: float = 30) -> ShellResult:
|
||||
"""执行 shell 命令(本地直接运行,不需要 ADB)"""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
cmd, shell=True, capture_output=True, text=True,
|
||||
timeout=timeout,
|
||||
)
|
||||
return ShellResult(result.stdout + result.stderr, result.returncode)
|
||||
except subprocess.TimeoutExpired:
|
||||
return ShellResult("", -1)
|
||||
except Exception as e:
|
||||
return ShellResult(str(e), -1)
|
||||
|
||||
def click(self, x: int, y: int):
|
||||
self._http_post("/click", {"x": x, "y": y})
|
||||
|
||||
def long_click(self, x: int, y: int, duration: float = 0.5):
|
||||
self._http_post("/click", {"x": x, "y": y, "duration": duration})
|
||||
|
||||
def swipe(self, x1: int, y1: int, x2: int, y2: int, duration: float = 0.5):
|
||||
self._http_post("/swipe", {
|
||||
"x1": x1, "y1": y1, "x2": x2, "y2": y2,
|
||||
"duration": duration,
|
||||
})
|
||||
|
||||
def swipe_ext(self, direction: str, scale: float = 0.8):
|
||||
info = self.info
|
||||
w = info.get("displayWidth", 1080)
|
||||
h = info.get("displayHeight", 2400)
|
||||
cx, cy = w // 2, h // 2
|
||||
dist_x = int(w * scale / 2)
|
||||
dist_y = int(h * scale / 2)
|
||||
|
||||
moves = {
|
||||
"up": (cx, cy + dist_y, cx, cy - dist_y),
|
||||
"down": (cx, cy - dist_y, cx, cy + dist_y),
|
||||
"left": (cx + dist_x, cy, cx - dist_x, cy),
|
||||
"right": (cx - dist_x, cy, cx + dist_x, cy),
|
||||
}
|
||||
coords = moves.get(direction, moves["up"])
|
||||
self.swipe(*coords)
|
||||
|
||||
def send_keys(self, text: str):
|
||||
self._http_post("/keys", {"keys": text})
|
||||
|
||||
def clear_text(self):
|
||||
self._http_post("/clear")
|
||||
|
||||
def press(self, key: str):
|
||||
key_map = {
|
||||
"home": 3, "back": 4, "menu": 82, "power": 26,
|
||||
"volume_up": 24, "volume_down": 25, "enter": 66,
|
||||
"recent": 187, "search": 84,
|
||||
}
|
||||
code = key_map.get(key.lower(), key)
|
||||
self.shell(f"input keyevent {code}")
|
||||
|
||||
def app_start(self, package: str, activity: str = None):
|
||||
if activity:
|
||||
self.shell(f"am start -n {package}/{activity}")
|
||||
else:
|
||||
self.shell(
|
||||
f"monkey -p {package} -c android.intent.category.LAUNCHER 1"
|
||||
)
|
||||
|
||||
def app_stop(self, package: str):
|
||||
self.shell(f"am force-stop {package}")
|
||||
|
||||
def dump_hierarchy(self) -> str:
|
||||
result = self._http_get("/dump/hierarchy")
|
||||
return result if isinstance(result, str) else json.dumps(result)
|
||||
|
||||
def screenshot(self, format: str = "raw") -> bytes:
|
||||
url = f"{self._atx_url}/screenshot/0"
|
||||
resp = request.urlopen(url, timeout=15)
|
||||
return resp.read()
|
||||
|
||||
def __call__(self, **kwargs):
|
||||
"""兼容 u2 的选择器语法: d(text="xxx"), d(resourceId="xxx")"""
|
||||
return _Selector(self, kwargs)
|
||||
|
||||
|
||||
class _Selector:
|
||||
"""兼容 u2 的 UI 选择器"""
|
||||
|
||||
def __init__(self, device: LocalDevice, criteria: dict):
|
||||
self._device = device
|
||||
self._criteria = criteria
|
||||
|
||||
def _build_xpath(self) -> str:
|
||||
parts = []
|
||||
if "text" in self._criteria:
|
||||
parts.append(f'@text="{self._criteria["text"]}"')
|
||||
if "textContains" in self._criteria:
|
||||
parts.append(f'contains(@text, "{self._criteria["textContains"]}")')
|
||||
if "textMatches" in self._criteria:
|
||||
parts.append(f'matches(@text, "{self._criteria["textMatches"]}")')
|
||||
if "resourceId" in self._criteria:
|
||||
parts.append(f'@resource-id="{self._criteria["resourceId"]}"')
|
||||
if "className" in self._criteria:
|
||||
parts.append(f'@class="{self._criteria["className"]}"')
|
||||
if "description" in self._criteria:
|
||||
parts.append(f'@content-desc="{self._criteria["description"]}"')
|
||||
|
||||
if parts:
|
||||
return f'//*[{" and ".join(parts)}]'
|
||||
return '//*'
|
||||
|
||||
def exists(self, timeout: float = None) -> bool:
|
||||
t = timeout if timeout is not None else self._device._implicit_wait
|
||||
end = time.time() + t
|
||||
while time.time() < end:
|
||||
try:
|
||||
hierarchy = self._device.dump_hierarchy()
|
||||
xpath = self._build_xpath()
|
||||
if self._match_in_hierarchy(hierarchy, xpath):
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
time.sleep(0.5)
|
||||
return False
|
||||
|
||||
def click(self, timeout: float = None):
|
||||
t = timeout if timeout is not None else self._device._implicit_wait
|
||||
end = time.time() + t
|
||||
while time.time() < end:
|
||||
try:
|
||||
resp = self._device._http_post("/xpath", {
|
||||
"xpath": self._build_xpath(),
|
||||
"action": "click",
|
||||
})
|
||||
return resp
|
||||
except Exception:
|
||||
pass
|
||||
time.sleep(0.5)
|
||||
raise TimeoutError(f"Element not found: {self._criteria}")
|
||||
|
||||
def get_text(self, timeout: float = None) -> str:
|
||||
t = timeout if timeout is not None else self._device._implicit_wait
|
||||
end = time.time() + t
|
||||
while time.time() < end:
|
||||
try:
|
||||
resp = self._device._http_post("/xpath", {
|
||||
"xpath": self._build_xpath(),
|
||||
"action": "get_text",
|
||||
})
|
||||
return resp.get("text", "")
|
||||
except Exception:
|
||||
pass
|
||||
time.sleep(0.5)
|
||||
return ""
|
||||
|
||||
@staticmethod
|
||||
def _match_in_hierarchy(hierarchy: str, xpath: str) -> bool:
|
||||
try:
|
||||
import xml.etree.ElementTree as ET
|
||||
root = ET.fromstring(hierarchy) if isinstance(hierarchy, str) else hierarchy
|
||||
return len(root.findall(f".{xpath}")) > 0
|
||||
except Exception:
|
||||
import re
|
||||
if '@text=' in xpath:
|
||||
text = xpath.split('@text="')[1].split('"')[0]
|
||||
return text in hierarchy
|
||||
return False
|
||||
@@ -36,6 +36,7 @@ cp "$SCRIPT_DIR/skills/voice_control.py" "$TEMP_DIR/agent/skills/"
|
||||
cp "$SCRIPT_DIR/skills/app_manager.py" "$TEMP_DIR/agent/skills/"
|
||||
cp "$SCRIPT_DIR/skills/search.py" "$TEMP_DIR/agent/skills/"
|
||||
[ -f "$SCRIPT_DIR/skills/network_reconnect.py" ] && cp "$SCRIPT_DIR/skills/network_reconnect.py" "$TEMP_DIR/agent/skills/"
|
||||
[ -f "$SCRIPT_DIR/skills/connection_guard.py" ] && cp "$SCRIPT_DIR/skills/connection_guard.py" "$TEMP_DIR/agent/skills/"
|
||||
|
||||
# Hawk 模块(网络层,与 Agent 隔离)
|
||||
for f in __init__.py network.py; do
|
||||
|
||||
@@ -26,27 +26,32 @@ logger = logging.getLogger(__name__)
|
||||
class SkillExecutor:
|
||||
"""技能执行器"""
|
||||
|
||||
def __init__(self, device):
|
||||
def __init__(self, device, anti_ban_ctx=None):
|
||||
"""
|
||||
初始化
|
||||
|
||||
Args:
|
||||
device: uiautomator2设备对象
|
||||
anti_ban_ctx: 防护上下文 dict
|
||||
"""
|
||||
self.device = device
|
||||
self.anti_ban_ctx = anti_ban_ctx or {}
|
||||
self.skill_bus = SkillChatBus()
|
||||
self.skills = {}
|
||||
self._init_skills()
|
||||
|
||||
def _init_skills(self):
|
||||
"""初始化所有技能,注入共享聊天总线便于多 Skill 一起执行时交互"""
|
||||
"""初始化所有技能,注入共享聊天总线和防护上下文"""
|
||||
for name, skill_class in SKILL_REGISTRY.items():
|
||||
try:
|
||||
sig = getattr(skill_class, "__init__")
|
||||
if "bus" in sig.__code__.co_varnames:
|
||||
self.skills[name] = skill_class(self.device, self.skill_bus)
|
||||
else:
|
||||
self.skills[name] = skill_class(self.device)
|
||||
varnames = sig.__code__.co_varnames
|
||||
kwargs = {}
|
||||
if "bus" in varnames:
|
||||
kwargs["bus"] = self.skill_bus
|
||||
if "anti_ban_ctx" in varnames:
|
||||
kwargs["anti_ban_ctx"] = self.anti_ban_ctx
|
||||
self.skills[name] = skill_class(self.device, **kwargs)
|
||||
except Exception as e:
|
||||
logger.error(f"初始化技能失败 {name}: {e}")
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ def _build_skill_registry():
|
||||
from skills.app_manager import AppManagerSkill
|
||||
from skills.search import SearchSkill
|
||||
from skills.network_reconnect import NetworkReconnectSkill
|
||||
from skills.connection_guard import ConnectionGuardSkill
|
||||
return {
|
||||
"wechat": WechatSkill,
|
||||
"douyin": DouyinSkill,
|
||||
@@ -30,6 +31,7 @@ def _build_skill_registry():
|
||||
"app_manager": AppManagerSkill,
|
||||
"search": SearchSkill,
|
||||
"network_reconnect": NetworkReconnectSkill,
|
||||
"connection_guard": ConnectionGuardSkill,
|
||||
}
|
||||
|
||||
|
||||
@@ -73,5 +75,6 @@ from skills.voice_control import VoiceControlSkill
|
||||
from skills.app_manager import AppManagerSkill
|
||||
from skills.search import SearchSkill
|
||||
from skills.network_reconnect import NetworkReconnectSkill
|
||||
from skills.connection_guard import ConnectionGuardSkill
|
||||
|
||||
_ensure_registry()
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
"""
|
||||
Agent端技能基类(含防封拟人化行为层)
|
||||
Agent端技能基类(含防封拟人化行为层 + 深层防护集成)
|
||||
"""
|
||||
|
||||
import math
|
||||
import time
|
||||
import random
|
||||
import logging
|
||||
@@ -17,16 +16,22 @@ class BaseSkill:
|
||||
PACKAGE: str = ""
|
||||
NAME: str = ""
|
||||
|
||||
def __init__(self, device, bus=None):
|
||||
def __init__(self, device, bus=None, anti_ban_ctx=None):
|
||||
"""
|
||||
初始化
|
||||
|
||||
Args:
|
||||
device: uiautomator2设备对象
|
||||
bus: SkillChatBus,各 Skill 一起执行时的聊天/交互总线,可选
|
||||
anti_ban_ctx: 防护上下文 dict,包含 risk_sentinel / touch_hardener /
|
||||
sensor_sim / nurture_scheduler 等实例
|
||||
"""
|
||||
self.d = device
|
||||
self.bus = bus
|
||||
self._ab = anti_ban_ctx or {}
|
||||
self._risk = self._ab.get("risk_sentinel")
|
||||
self._touch = self._ab.get("touch_hardener")
|
||||
self._sensor = self._ab.get("sensor_sim")
|
||||
|
||||
# ==========================================================
|
||||
# 防封拟人化行为层(所有 Skill 操作必须使用这些方法)
|
||||
@@ -56,11 +61,23 @@ class BaseSkill:
|
||||
time.sleep(random.uniform(0.08, 0.15))
|
||||
|
||||
def human_click(self, x: int, y: int, offset: int = 8):
|
||||
"""拟人点击:加微小随机偏移,避免每次坐标完全一致"""
|
||||
"""拟人点击:加微小随机偏移 + 触摸加固通道"""
|
||||
if self._risk and not self._risk.can_operate():
|
||||
logger.debug("风控暂停中,跳过点击")
|
||||
return
|
||||
dx = random.randint(-offset, offset)
|
||||
dy = random.randint(-offset, offset)
|
||||
self.d.click(x + dx, y + dy)
|
||||
# 操作前注入传感器微抖动
|
||||
if self._sensor:
|
||||
self._sensor.simulate_hand_shake(duration=random.uniform(0.3, 0.8))
|
||||
# 使用加固触摸通道
|
||||
if self._touch:
|
||||
self._touch.tap(x + dx, y + dy, randomize=False)
|
||||
else:
|
||||
self.d.click(x + dx, y + dy)
|
||||
self.human_delay(0.1, 0.4)
|
||||
if self._risk:
|
||||
self._risk.report_success()
|
||||
|
||||
def human_swipe(
|
||||
self,
|
||||
@@ -68,9 +85,14 @@ class BaseSkill:
|
||||
end: Tuple[int, int],
|
||||
duration: Optional[float] = None,
|
||||
):
|
||||
"""拟人滑动:贝塞尔曲线轨迹 + 随机中间控制点 + 随机持续时间"""
|
||||
"""拟人滑动:贝塞尔曲线轨迹 + 触摸加固通道 + 传感器模拟"""
|
||||
if self._risk and not self._risk.can_operate():
|
||||
return
|
||||
if duration is None:
|
||||
duration = random.uniform(0.3, 0.8)
|
||||
# 滑动前注入手持抖动
|
||||
if self._sensor:
|
||||
self._sensor.simulate_hand_shake(duration=random.uniform(0.2, 0.5))
|
||||
ctrl_x = (start[0] + end[0]) // 2 + random.randint(-30, 30)
|
||||
ctrl_y = (start[1] + end[1]) // 2 + random.randint(-30, 30)
|
||||
steps = max(8, int(duration * 40))
|
||||
@@ -80,10 +102,14 @@ class BaseSkill:
|
||||
bx = (1 - t) ** 2 * start[0] + 2 * (1 - t) * t * ctrl_x + t ** 2 * end[0]
|
||||
by = (1 - t) ** 2 * start[1] + 2 * (1 - t) * t * ctrl_y + t ** 2 * end[1]
|
||||
points.append((int(bx), int(by)))
|
||||
try:
|
||||
self.d.swipe_points(points, duration)
|
||||
except (AttributeError, TypeError):
|
||||
self.d.swipe(start[0], start[1], end[0], end[1], duration=duration)
|
||||
# 优先 shell 加固通道
|
||||
if self._touch:
|
||||
self._touch.swipe(start, end, int(duration * 1000))
|
||||
else:
|
||||
try:
|
||||
self.d.swipe_points(points, duration)
|
||||
except (AttributeError, TypeError):
|
||||
self.d.swipe(start[0], start[1], end[0], end[1], duration=duration)
|
||||
|
||||
def random_browse(self, duration_sec: float = 30):
|
||||
"""随机浏览:模拟真人无目的翻看,用于养号/填充自然行为"""
|
||||
@@ -111,12 +137,14 @@ class BaseSkill:
|
||||
|
||||
def natural_behavior_before(self, action: str = "send_message"):
|
||||
"""
|
||||
自然行为链前置:在执行关键操作前模拟真人浏览行为。
|
||||
打开对话 → 浏览历史(上滑) → 停顿 → 再执行操作
|
||||
30%概率触发;send_message/add_friend/post_moments 触发率更高(50%)
|
||||
自然行为链前置:操作前模拟真人浏览行为。
|
||||
风控等级越高,触发概率越大、延迟越长。
|
||||
"""
|
||||
if self._risk and not self._risk.can_operate():
|
||||
return
|
||||
multiplier = self._risk.get_delay_multiplier() if self._risk else 1.0
|
||||
high_risk = {"send_message", "add_friend", "post_moments", "batch_send", "mass_send"}
|
||||
trigger_rate = 0.5 if action in high_risk else 0.3
|
||||
trigger_rate = min(0.9, (0.5 if action in high_risk else 0.3) * multiplier)
|
||||
if random.random() > trigger_rate:
|
||||
return
|
||||
try:
|
||||
@@ -125,21 +153,23 @@ class BaseSkill:
|
||||
h = info.get("displayHeight", 2400)
|
||||
except Exception:
|
||||
w, h = 1080, 2400
|
||||
self.human_delay(0.8, 2.0)
|
||||
self.human_delay(0.8 * multiplier, 2.0 * multiplier)
|
||||
if random.random() < 0.6:
|
||||
self.human_swipe((w // 2, int(h * 0.3)), (w // 2, int(h * 0.7)))
|
||||
self.human_delay(1.0, 3.0)
|
||||
self.human_delay(0.5, 1.5)
|
||||
self.human_delay(1.0 * multiplier, 3.0 * multiplier)
|
||||
self.human_delay(0.5 * multiplier, 1.5 * multiplier)
|
||||
|
||||
def natural_behavior_after(self, action: str = "send_message"):
|
||||
"""
|
||||
自然行为链后置:操作完成后随机穿插自然行为。
|
||||
30%概率浏览朋友圈/首页等
|
||||
风控等级越高,后置行为越长。
|
||||
"""
|
||||
if random.random() > 0.3:
|
||||
multiplier = self._risk.get_delay_multiplier() if self._risk else 1.0
|
||||
trigger = min(0.8, 0.3 * multiplier)
|
||||
if random.random() > trigger:
|
||||
return
|
||||
self.human_delay(1.0, 3.0)
|
||||
self.random_browse(duration_sec=random.uniform(5, 15))
|
||||
self.human_delay(1.0 * multiplier, 3.0 * multiplier)
|
||||
self.random_browse(duration_sec=random.uniform(5, 15) * multiplier)
|
||||
|
||||
def say(self, message: str, to_skill: Optional[str] = None, data: Optional[Dict[str, Any]] = None):
|
||||
"""向总线发一条消息,其它 Skill 可通过 read_chat 看到"""
|
||||
|
||||
416
sdk/agent/skills/connection_guard.py
Normal file
416
sdk/agent/skills/connection_guard.py
Normal file
@@ -0,0 +1,416 @@
|
||||
"""
|
||||
连接守护技能 - 确保设备与服务器始终保持连接
|
||||
|
||||
职责:
|
||||
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",
|
||||
"확인", # 三星韩语
|
||||
"允許", # 繁体
|
||||
]},
|
||||
|
||||
# ─── 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
|
||||
62
sdk/agent/start_agent.sh
Normal file
62
sdk/agent/start_agent.sh
Normal file
@@ -0,0 +1,62 @@
|
||||
#!/data/data/com.termux/files/usr/bin/bash
|
||||
#
|
||||
# 工作手机 Agent 启动脚本(Termux 专用)
|
||||
# 功能:自动启动 ATX 服务 → 等待就绪 → 启动 Agent
|
||||
#
|
||||
|
||||
AGENT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
ATX_PORT=9008
|
||||
ATX_JAR="/data/local/tmp/u2.jar"
|
||||
PYTHON="/data/data/com.termux/files/usr/bin/python3"
|
||||
|
||||
export PATH="/data/data/com.termux/files/usr/bin:$PATH"
|
||||
export TMPDIR="/data/data/com.termux/files/usr/tmp"
|
||||
export HOME="/data/data/com.termux/files/home"
|
||||
|
||||
cd "$AGENT_DIR"
|
||||
|
||||
start_atx() {
|
||||
if curl -s "http://127.0.0.1:${ATX_PORT}/ping" >/dev/null 2>&1; then
|
||||
echo "✅ ATX 服务已在运行 (端口 ${ATX_PORT})"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ ! -f "$ATX_JAR" ]; then
|
||||
echo "❌ ATX jar 不存在: $ATX_JAR"
|
||||
echo " 请先从电脑运行: python3 -m uiautomator2 init --serial <设备>"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "🚀 启动 ATX 服务..."
|
||||
nohup sh -c "CLASSPATH=$ATX_JAR /system/bin/app_process / com.wetest.uia2.Main" \
|
||||
> /data/local/tmp/atx.log 2>&1 &
|
||||
ATX_PID=$!
|
||||
echo " ATX PID: $ATX_PID"
|
||||
|
||||
for i in $(seq 1 20); do
|
||||
sleep 1
|
||||
if curl -s "http://127.0.0.1:${ATX_PORT}/ping" >/dev/null 2>&1; then
|
||||
echo "✅ ATX 服务就绪 (${i}s)"
|
||||
return 0
|
||||
fi
|
||||
echo " 等待 ATX... (${i}/20)"
|
||||
done
|
||||
|
||||
echo "❌ ATX 服务启动超时"
|
||||
return 1
|
||||
}
|
||||
|
||||
echo "========================================="
|
||||
echo " 工作手机 Agent 启动器"
|
||||
echo " 目录: $AGENT_DIR"
|
||||
echo "========================================="
|
||||
|
||||
if ! start_atx; then
|
||||
echo ""
|
||||
echo "⚠️ ATX 未就绪,Agent 将以无 u2 模式启动"
|
||||
echo " (WebSocket 连接和心跳正常,UI 操作不可用)"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "🤖 启动 Agent..."
|
||||
exec $PYTHON agent.py "$@"
|
||||
42
sdk/agent/wait_and_start.sh
Executable file
42
sdk/agent/wait_and_start.sh
Executable file
@@ -0,0 +1,42 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# 等待设备授权后自动启动 Agent
|
||||
# 用法: bash wait_and_start.sh [server_url]
|
||||
#
|
||||
SERVER_URL="${1:-ws://192.168.1.100:8899/ws/device}"
|
||||
DEVICE_SERIAL="dc9c23e00510"
|
||||
MAX_WAIT=300 # 最多等5分钟
|
||||
|
||||
echo "⏳ 等待设备 $DEVICE_SERIAL 授权..."
|
||||
echo ""
|
||||
echo "📱 请在 Redmi Note 11 上执行以下操作:"
|
||||
echo " 1. 设置 → 更多设置 → 开发者选项"
|
||||
echo " 2. 确认「USB调试」已开启"
|
||||
echo " 3. ⚠️ 关键:开启「USB调试(安全设置)」← 小米/红米特有!"
|
||||
echo " 4. 如果没有弹出授权弹窗,点击「撤销USB调试授权」后重新插拔USB线"
|
||||
echo " 5. 弹窗出现后,勾选「始终允许」→ 点「确定」"
|
||||
echo ""
|
||||
|
||||
SECONDS_WAITED=0
|
||||
while [ $SECONDS_WAITED -lt $MAX_WAIT ]; do
|
||||
STATE=$(adb -s "$DEVICE_SERIAL" get-state 2>/dev/null)
|
||||
if [ "$STATE" = "device" ]; then
|
||||
echo "✅ 设备已授权!"
|
||||
MODEL=$(adb -s "$DEVICE_SERIAL" shell getprop ro.product.model 2>/dev/null)
|
||||
ANDROID=$(adb -s "$DEVICE_SERIAL" shell getprop ro.build.version.release 2>/dev/null)
|
||||
MIUI=$(adb -s "$DEVICE_SERIAL" shell getprop ro.miui.ui.version.name 2>/dev/null)
|
||||
echo " 型号: $MODEL | Android: $ANDROID | MIUI: $MIUI"
|
||||
echo ""
|
||||
echo "🚀 正在启动 Agent..."
|
||||
cd "$(dirname "$0")"
|
||||
python3 agent.py -s "$SERVER_URL"
|
||||
exit 0
|
||||
fi
|
||||
sleep 3
|
||||
SECONDS_WAITED=$((SECONDS_WAITED + 3))
|
||||
printf "\r 已等待 %ds / %ds... (状态: %s)" "$SECONDS_WAITED" "$MAX_WAIT" "${STATE:-offline}"
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "❌ 等待超时,请检查手机设置"
|
||||
exit 1
|
||||
Reference in New Issue
Block a user