feat: connection_priority、hub.html、本地控制模式图、Hook交互流程图、工作流总图

Made-with: Cursor
This commit is contained in:
卡若
2026-03-16 09:58:15 +08:00
parent df7999a024
commit 2d5bd2200b
21 changed files with 2116 additions and 38 deletions

View File

@@ -19,6 +19,7 @@ mkdir -p "$TEMP_DIR/agent/skills/wechat" \
"$TEMP_DIR/agent/skills/douyin" \ "$TEMP_DIR/agent/skills/douyin" \
"$TEMP_DIR/agent/skills/xhs" \ "$TEMP_DIR/agent/skills/xhs" \
"$TEMP_DIR/agent/skills/xianyu" \ "$TEMP_DIR/agent/skills/xianyu" \
"$TEMP_DIR/agent/skills/soul" \
"$TEMP_DIR/agent/hawk" "$TEMP_DIR/agent/hawk"
# 复制核心文件 # 复制核心文件
@@ -47,7 +48,7 @@ for f in README.md setup_redmi11.sh wechat_hook_v1.js; do
[ -f "$SCRIPT_DIR/hook/$f" ] && cp "$SCRIPT_DIR/hook/$f" "$TEMP_DIR/agent/hook/" [ -f "$SCRIPT_DIR/hook/$f" ] && cp "$SCRIPT_DIR/hook/$f" "$TEMP_DIR/agent/hook/"
done done
for sub in wechat douyin xhs xianyu; do for sub in wechat douyin xhs xianyu soul; do
if [ -d "$SCRIPT_DIR/skills/$sub" ]; then if [ -d "$SCRIPT_DIR/skills/$sub" ]; then
cp "$SCRIPT_DIR/skills/$sub/__init__.py" "$TEMP_DIR/agent/skills/$sub/" 2>/dev/null || true cp "$SCRIPT_DIR/skills/$sub/__init__.py" "$TEMP_DIR/agent/skills/$sub/" 2>/dev/null || true
cp "$SCRIPT_DIR/skills/$sub/skill.py" "$TEMP_DIR/agent/skills/$sub/" 2>/dev/null || true cp "$SCRIPT_DIR/skills/$sub/skill.py" "$TEMP_DIR/agent/skills/$sub/" 2>/dev/null || true

View File

@@ -34,5 +34,21 @@
}, },
"hook_framework": "frida-server", "hook_framework": "frida-server",
"updated_at": "2026-02-24T08:22:19.844907+00:00" "updated_at": "2026-02-24T08:22:19.844907+00:00"
},
"dc9c23e00510": {
"supports_hook": false,
"frida_version": "",
"root_status": true,
"hook_framework": "frida-server",
"updated_at": "2026-03-16T01:50:07.133711+00:00",
"modules": []
},
"b0f1322b4e3be135e0000798d1587e26": {
"supports_hook": false,
"frida_version": "",
"root_status": false,
"hook_framework": "frida-server",
"updated_at": "2026-03-16T01:50:27.104574+00:00",
"modules": []
} }
} }

View File

@@ -3,7 +3,7 @@
存客宝的AI手机控制引擎 存客宝的AI手机控制引擎
""" """
from fastapi import FastAPI, WebSocket, WebSocketDisconnect from fastapi import FastAPI, HTTPException, WebSocket, WebSocketDisconnect
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse, Response from fastapi.responses import FileResponse, Response
@@ -12,6 +12,9 @@ import logging
import os import os
import subprocess import subprocess
import asyncio import asyncio
from pathlib import Path
import markdown
from config import settings from config import settings
from routers import devices, unified, agent, adb, experience, projects, qrcode, voice, capture, hook_modules, connection from routers import devices, unified, agent, adb, experience, projects, qrcode, voice, capture, hook_modules, connection
@@ -25,6 +28,341 @@ logging.basicConfig(
) )
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
_APP_DIR = os.path.dirname(__file__)
_STATIC_DIR = os.path.join(_APP_DIR, "static")
_DOCS_ROOT = os.path.normpath(os.path.join(_APP_DIR, "..", "..", "开发文档"))
_DOC_PAGE_MAP = {
"api-architecture": os.path.join(_DOCS_ROOT, "5、接口", "API架构与交互流程图.html"),
"anti-ban-architecture": os.path.join(_DOCS_ROOT, "2、架构", "防封模块架构图.html"),
"wechat-server-flow": os.path.join(_DOCS_ROOT, "2、架构", "微信控制-设备服务器交互图.html"),
}
_AGENT_ROOT = os.path.normpath(os.path.join(_APP_DIR, "..", "agent"))
_AGENT_DOCS = [
{
"id": "agent-readme",
"title": "Agent 架构与部署",
"summary": "设备端 Agent 总览WebSocket 连接、技能引擎、目录结构、Termux 一键安装。",
"category": "agent",
"section": "Agent 集群",
"folder": "agent",
"file": "README.md",
"keywords": ["agent", "termux", "websocket", "部署"],
},
{
"id": "agent-skills",
"title": "技能引擎 Skills",
"summary": "6 大 Skill微信/抖音/小红书/闲鱼/语音/搜索)· SkillExecutor 调度 · 复合命令。",
"category": "agent",
"section": "Agent 集群",
"folder": "agent/skills",
"file": "README.md",
"keywords": ["skill", "微信", "抖音", "技能", "executor"],
},
{
"id": "agent-hawk",
"title": "Hawk 网络恢复模块",
"summary": "Agent 与 Hawk 边界协同断网自动恢复、WiFi 开关、合法操作边界。",
"category": "agent",
"section": "Agent 集群",
"folder": "agent/docs",
"file": "AGENT_HAWK.md",
"keywords": ["hawk", "网络", "wifi", "恢复", "断网"],
},
{
"id": "agent-hook",
"title": "Hook 脚本系统",
"summary": "微信 Hook 初版脚本FridaRPC 骨架、消息监听、脚本部署流程。",
"category": "agent",
"section": "Agent 集群",
"folder": "agent/hook",
"file": "README.md",
"keywords": ["hook", "frida", "rpc", "消息监听"],
},
]
_WORKBENCH_DOCS = [
{
"id": "sdk-manual",
"title": "SDK操作手册",
"summary": "工作手机 SDK 的整体使用说明、核心能力和调用入口。",
"category": "manual",
"section": "说明文档",
"folder": "9、手册",
"file": "SDK操作手册.md",
"keywords": ["SDK", "手册", "调用", "接入"],
},
{
"id": "usage-solution",
"title": "使用与落地方案",
"summary": "从部署到业务落地的整体使用路径与建议方案。",
"category": "manual",
"section": "说明文档",
"folder": "9、手册",
"file": "使用与落地方案.md",
"keywords": ["方案", "落地", "部署", "使用"],
},
{
"id": "wechat-e2e",
"title": "微信消息E2E验证指南",
"summary": "微信消息链路的端到端验证步骤和排查指引。",
"category": "manual",
"section": "说明文档",
"folder": "9、手册",
"file": "微信消息E2E验证指南.md",
"keywords": ["微信", "E2E", "验证", "排查"],
},
{
"id": "agent-install",
"title": "设备端Agent安装与公司设备说明",
"summary": "设备端 Agent 的安装方式、公司设备规范和运行说明。",
"category": "manual",
"section": "说明文档",
"folder": "9、手册",
"file": "设备端Agent安装与公司设备说明.md",
"keywords": ["Agent", "安装", "设备", "说明"],
},
{
"id": "install-checklist",
"title": "安装前配置检查规范",
"summary": "安装前需要确认的配置、环境与风险检查项。",
"category": "manual",
"section": "说明文档",
"folder": "9、手册",
"file": "安装前配置检查规范.md",
"keywords": ["安装", "检查", "规范", "配置"],
},
{
"id": "system-architecture",
"title": "系统架构",
"summary": "工作手机 SDK 的整体架构、模块分层与链路设计。",
"category": "architecture",
"section": "架构与经验",
"folder": "2、架构",
"file": "系统架构.md",
"keywords": ["架构", "模块", "分层", "链路"],
},
{
"id": "hook-architecture",
"title": "Hook通道与多设备多服务器架构",
"summary": "Hook 通道、Frida 集成、多设备与多服务器的设计说明。",
"category": "architecture",
"section": "架构与经验",
"folder": "2、架构",
"file": "Hook通道与多设备多服务器架构.md",
"keywords": ["Hook", "Frida", "多设备", "多服务器"],
},
{
"id": "api-spec",
"title": "接口规范",
"summary": "统一接口的字段约定、协议与调用方式。",
"category": "api",
"section": "接口与协议",
"folder": "5、接口",
"file": "接口规范.md",
"keywords": ["接口", "协议", "字段", "规范"],
},
{
"id": "cunkebao-spec",
"title": "存客宝对接规范",
"summary": "存客宝与工作手机 SDK 的对接方式和聚合规则。",
"category": "api",
"section": "接口与协议",
"folder": "5、接口",
"file": "存客宝对接规范.md",
"keywords": ["存客宝", "对接", "SDK", "聚合"],
},
{
"id": "progress-overview",
"title": "开发进度总表",
"summary": "当前阶段、完成率、里程碑与下一步开发重点。",
"category": "project",
"section": "项目管理",
"folder": "10、项目管理",
"file": "开发进度总表.md",
"keywords": ["进度", "里程碑", "阶段", "总表"],
},
]
def _scan_arch_assets():
import urllib.parse
arch_dir = Path(_DOCS_ROOT) / "2、架构"
known = [
("arch-sdk", "工作手机SDK架构图", "系统全景四层架构,适合做整体汇报入口。"),
("arch-flow", "设备与服务器交互流程(微信控制)", "设备、服务器与微信控制的交互过程。"),
("arch-device", "工作手机设备端运转逻辑", "设备端从启动到心跳到 Skill 执行的完整运转。"),
("arch-agent-total", "Agent与服务器工作流总图", "业务入口、服务器 Agent、设备 Agent 和消息闭环。"),
("arch-hook", "Hook与服务器交互流程图", "Hook 通道的完整交互链路与功能细节。"),
("arch-ban", "防封模块架构图", "防封中心、设备侧、行为层与服务端策略。"),
("arch-ban-detail", "防封服务端核心能力拆细图", "防封服务端 7 大模块逐一拆细。"),
("mode-hook", "本地控制模式一Hook/Frida", "Frida 深度控制微信的模式示意。"),
("mode-adb", "本地控制模式二ADB静默控制", "无需 Root 的 ADB 静默控制模式。"),
("mode-agent", "本地控制模式三Agent WebSocket远控", "远程统一控制多设备。"),
("mode-ai", "本地控制模式四AI Agent自然语言", "自然语言编排复杂任务。"),
("mode-scrcpy", "本地控制模式五scrcpy可视化控制", "投屏与人工接管。"),
]
fname_map = {
"arch-sdk": "工作手机SDK架构图.png",
"arch-flow": "设备与服务器交互流程_微信控制.png",
"arch-device": "工作手机设备端运转逻辑.png",
"arch-agent-total": "卡若AI手机_Agent与服务器工作流总图.png",
"arch-hook": "Hook与服务器交互流程图.png",
"arch-ban": "防封模块架构图.png",
"arch-ban-detail": "防封服务端核心能力拆细图.png",
"mode-hook": "本地控制模式_1_Hook_Frida模式.png",
"mode-adb": "本地控制模式_2_ADB静默控制模式.png",
"mode-agent": "本地控制模式_3_Agent_WebSocket远控模式.png",
"mode-ai": "本地控制模式_4_AI_Agent自然语言模式.png",
"mode-scrcpy": "本地控制模式_5_scrcpy可视化控制模式.png",
}
result = []
for aid, title, summary in known:
fname = fname_map.get(aid, "")
fpath = arch_dir / fname
if fpath.exists():
encoded = urllib.parse.quote(f"2、架构/{fname}")
result.append({
"id": aid,
"title": title,
"summary": summary,
"category": "architecture",
"url": f"/workbench-docs/{encoded}",
})
return result
_WORKBENCH_ARCH_ASSETS = _scan_arch_assets()
def _doc_path(meta: dict) -> Path:
if meta.get("category") == "agent":
return Path(_AGENT_ROOT) / meta["file"] if meta["folder"] == "agent" else Path(_AGENT_ROOT) / meta["folder"].replace("agent/", "", 1) / meta["file"]
return Path(_DOCS_ROOT) / meta["folder"] / meta["file"]
def _doc_meta_with_state(meta: dict) -> dict:
path = _doc_path(meta)
exists = path.exists()
return {
**meta,
"exists": exists,
"path": str(path),
"updated_at": path.stat().st_mtime if exists else None,
"raw_url": f"/workbench-docs/{meta['folder']}/{meta['file']}" if exists else None,
}
def _render_markdown_html(content: str) -> str:
return markdown.markdown(
content,
extensions=["extra", "tables", "fenced_code", "toc", "sane_lists"],
output_format="html5",
)
async def _build_workbench_overview() -> dict:
from services.adb_device import adb_manager
from services.ai_command import ai_engine
from services.connection_priority import connection_priority
adb_devices = adb_manager.scan_devices()
db_devices = []
try:
db_devices = await device_manager.get_all_devices(limit=200)
except Exception:
db_devices = []
online_devices = {d["device_id"]: d for d in ws_hub.get_online_devices()}
merged = {}
for device in db_devices:
device_id = device["device_id"]
merged[device_id] = {**device}
for device_id, info in online_devices.items():
base = merged.get(device_id, {})
merged[device_id] = {**base, **info, "status": "online"}
for serial in adb_devices:
if serial not in merged:
adb_dev = adb_manager.get_device(serial)
if adb_dev:
info = adb_dev.get_info()
merged[serial] = {
"device_id": serial,
"model": info.get("model", "Unknown"),
"brand": info.get("brand", "Unknown"),
"android_version": info.get("android_version", "Unknown"),
"status": "adb",
"connection_type": "adb",
"display": info.get("display", {}),
}
devices = list(merged.values())
online_count = len([d for d in devices if d.get("status") == "online"])
adb_count = len([d for d in devices if d.get("status") == "adb"])
offline_count = len([d for d in devices if d.get("status") == "offline"])
try:
ai_status = await ai_engine.get_status()
except Exception:
ai_status = {"available": False, "backend": "", "model": ""}
docs = [_doc_meta_with_state(meta) for meta in _WORKBENCH_DOCS + _AGENT_DOCS]
# 为每台设备附加连接模式信息
device_modes = {}
for dev in devices:
did = dev.get("device_id", "")
if did:
try:
modes = connection_priority.evaluate(did)
best = next((m for m in modes if m.available), None)
dev["connection_modes"] = [m.to_dict() for m in modes]
dev["best_mode"] = best.to_dict() if best else None
device_modes[did] = dev
except Exception:
pass
return {
"sdk_online": True,
"sdk_version": "3.0.0",
"ws_online": len(ws_hub.connections),
"adb_count": len(adb_devices),
"device_total": len(devices),
"device_online": online_count,
"device_offline": offline_count + adb_count,
"devices": devices,
"summary": {
"sdk_online": True,
"sdk_version": "3.0.0",
"ws_online": len(ws_hub.connections),
"adb_count": len(adb_devices),
"device_total": len(devices),
"device_online": online_count,
"device_offline": offline_count,
"manual_docs": len([d for d in docs if d["category"] == "manual" and d["exists"]]),
"architecture_assets": len(_WORKBENCH_ARCH_ASSETS),
},
"agent": ai_status,
"docs": docs,
"architecture_assets": _WORKBENCH_ARCH_ASSETS,
"sections": [
{"id": "overview", "title": "首页总览"},
{"id": "docs", "title": "说明文档"},
{"id": "devices", "title": "设备控制"},
{"id": "hook", "title": "Hook 控制"},
{"id": "agent", "title": "AI Agent"},
{"id": "connection", "title": "连接与协议"},
{"id": "architecture", "title": "架构与经验"},
{"id": "project", "title": "项目管理"},
],
"control_modes": [
{"id": "hook", "name": "Hook / Frida", "desc": "读写内部能力强,适合微信深度控制", "priority": 1},
{"id": "agent", "name": "Agent WebSocket", "desc": "实时双向设备端App常驻", "priority": 2},
{"id": "adb", "name": "ADB 静默控制", "desc": "无需 Root部署简单适合稳定兜底", "priority": 3},
{"id": "ai", "name": "AI Agent", "desc": "自然语言编排复杂任务", "priority": 4},
{"id": "scrcpy", "name": "scrcpy 可视化", "desc": "投屏与人工接管", "priority": 5},
],
}
@asynccontextmanager @asynccontextmanager
async def lifespan(app: FastAPI): async def lifespan(app: FastAPI):
@@ -174,37 +512,107 @@ async def ready():
@app.get("/") @app.get("/")
async def root(): async def root():
"""根路由 → 控制面板""" """根路由 → 聚合总控台"""
static_path = os.path.join(os.path.dirname(__file__), "static", "index.html") static_path = os.path.join(_STATIC_DIR, "hub.html")
if os.path.exists(static_path): if os.path.exists(static_path):
return FileResponse(static_path) return FileResponse(static_path)
return { return {
"name": "工作手机SDK v3.0", "name": "工作手机SDK v3.0",
"description": "存客宝的AI手机控制引擎", "description": "统一聚合总控台",
"docs": "/docs", "docs": "/docs",
"health": "/health", "health": "/health",
"ready": "/ready", "ready": "/ready",
"voice_control": "/voice" "voice_control": "/voice",
"control_center": "/control",
} }
@app.get("/hub")
async def hub_page():
"""统一聚合总控台"""
static_path = os.path.join(_STATIC_DIR, "hub.html")
if os.path.exists(static_path):
return FileResponse(static_path)
raise HTTPException(status_code=404, detail="hub.html 不存在")
@app.get("/control")
async def control_page():
"""兼容旧入口,统一回到工作台"""
static_path = os.path.join(_STATIC_DIR, "hub.html")
if os.path.exists(static_path):
return FileResponse(static_path)
raise HTTPException(status_code=404, detail="hub.html 不存在")
@app.get("/voice") @app.get("/voice")
async def voice_control_page(): async def voice_control_page():
"""语音控制页面""" """语音控制页面"""
static_path = os.path.join(os.path.dirname(__file__), "static", "voice_control.html") static_path = os.path.join(_STATIC_DIR, "voice_control.html")
return FileResponse(static_path) return FileResponse(static_path)
@app.get("/pages/{page_key}")
async def docs_page(page_key: str):
"""聚合页内嵌的项目文档页面"""
doc_path = _DOC_PAGE_MAP.get(page_key)
if not doc_path or not os.path.exists(doc_path):
raise HTTPException(status_code=404, detail="页面不存在")
return FileResponse(doc_path)
@app.get("/api/v3/workbench/overview")
async def workbench_overview():
"""工作台统一概览 DTO供工作台前端与存客宝页面共用。"""
return {"code": 200, "data": await _build_workbench_overview()}
@app.get("/api/v3/workbench/docs")
async def workbench_docs():
"""工作台文档清单。"""
return {
"code": 200,
"data": {
"docs": [_doc_meta_with_state(meta) for meta in _WORKBENCH_DOCS + _AGENT_DOCS],
"architecture_assets": _WORKBENCH_ARCH_ASSETS,
},
}
@app.get("/api/v3/workbench/docs/{doc_id}")
async def workbench_doc_detail(doc_id: str):
"""读取工作台文档正文并返回渲染后的 HTML。"""
meta = next((item for item in _WORKBENCH_DOCS + _AGENT_DOCS if item["id"] == doc_id), None)
if not meta:
raise HTTPException(status_code=404, detail="文档不存在")
path = _doc_path(meta)
if not path.exists():
raise HTTPException(status_code=404, detail="文档文件不存在")
raw = path.read_text(encoding="utf-8")
return {
"code": 200,
"data": {
**_doc_meta_with_state(meta),
"content_markdown": raw,
"content_html": _render_markdown_html(raw),
},
}
# 挂载静态文件目录 # 挂载静态文件目录
static_dir = os.path.join(os.path.dirname(__file__), "static") if os.path.exists(_STATIC_DIR):
if os.path.exists(static_dir): app.mount("/static", StaticFiles(directory=_STATIC_DIR), name="static")
app.mount("/static", StaticFiles(directory=static_dir), name="static")
if os.path.exists(_DOCS_ROOT):
app.mount("/workbench-docs", StaticFiles(directory=_DOCS_ROOT), name="workbench-docs")
# ========== Agent 分发接口 ========== # ========== Agent 分发接口 ==========
# Agent 代码根目录 # Agent 代码根目录
_AGENT_DIR = os.path.normpath(os.path.join(os.path.dirname(__file__), "..", "agent")) _AGENT_DIR = os.path.normpath(os.path.join(_APP_DIR, "..", "agent"))
_AGENT_DIST = os.path.join(_AGENT_DIR, "dist", "agent.tar.gz") _AGENT_DIST = os.path.join(_AGENT_DIR, "dist", "agent.tar.gz")

View File

@@ -14,6 +14,7 @@ from pydantic import BaseModel, Field
from services.ws_hub import ws_hub from services.ws_hub import ws_hub
from services.adb_device import adb_manager from services.adb_device import adb_manager
from services.hook_module_service import hook_module_service from services.hook_module_service import hook_module_service
from services.connection_priority import connection_priority
router = APIRouter() router = APIRouter()
@@ -199,3 +200,40 @@ async def simulate_hook_event(req: SimHookEventRequest) -> Dict[str, Any]:
} }
) )
return {"code": 200, "data": event} return {"code": 200, "data": event}
# ========== 连接优先级 ==========
@router.get("/connection/modes/{device_id}")
async def get_device_connection_modes(device_id: str) -> Dict[str, Any]:
"""获取设备所有控制模式的可用性与优先级。"""
modes = connection_priority.evaluate(device_id)
best = next((m for m in modes if m.available), None)
return {
"code": 200,
"data": {
"device_id": device_id,
"best_mode": best.to_dict() if best else None,
"modes": [m.to_dict() for m in modes],
},
}
@router.get("/connection/modes")
async def get_all_connection_modes() -> Dict[str, Any]:
"""获取所有已知设备的控制模式概览。"""
online_ids = list(ws_hub.connections.keys())
adb_serials = adb_manager.scan_devices()
all_ids = list(set(online_ids + adb_serials))
result = []
for did in all_ids:
modes = connection_priority.evaluate(did)
best = next((m for m in modes if m.available), None)
result.append({
"device_id": did,
"best_mode": best.to_dict() if best else None,
"available_count": sum(1 for m in modes if m.available),
"modes": [m.to_dict() for m in modes],
})
return {"code": 200, "data": result}

View File

@@ -13,7 +13,7 @@
from fastapi import APIRouter, HTTPException from fastapi import APIRouter, HTTPException
from typing import Optional, List from typing import Optional, List
from pydantic import BaseModel from pydantic import BaseModel, Field
from enum import Enum from enum import Enum
import asyncio import asyncio
import logging import logging
@@ -343,27 +343,21 @@ class ChannelRouter:
# ========== 辅助函数 ========== # ========== 辅助函数 ==========
def _check_device_online(device_id: str): def _check_device_online(device_id: str):
"""检查设备是否在线WebSocketADB""" """检查设备是否在线(按优先级Hook > WebSocket > ADB"""
# 先检查WebSocket连接 from services.connection_priority import connection_priority
if ws_hub.is_online(device_id): best = connection_priority.get_best_mode(device_id)
return "websocket" if best:
return best.to_dict()["id"] # hook / agent / adb
# 再检查ADB设备
adb_device = adb_manager.get_device(device_id)
if adb_device and adb_device.is_online():
return "adb"
raise HTTPException(status_code=503, detail=f"设备不在线: {device_id}") raise HTTPException(status_code=503, detail=f"设备不在线: {device_id}")
def _get_device_mode(device_id: str) -> str: def _get_device_mode(device_id: str) -> str:
"""获取设备连接模式""" """获取设备最优连接模式"""
if ws_hub.is_online(device_id): from services.connection_priority import connection_priority
channel = connection_priority.choose_execution_channel(device_id)
if channel == "agent":
return "websocket" return "websocket"
adb_device = adb_manager.get_device(device_id) return channel
if adb_device and adb_device.is_online():
return "adb"
return "offline"
def _parse_messages_from_xml(xml: str, limit: int) -> list: def _parse_messages_from_xml(xml: str, limit: int) -> list:
@@ -521,6 +515,39 @@ async def _execute_via_adb(device, platform: str, action: str, params: dict) ->
return {"success": False, "error": str(e), "mode": "adb"} return {"success": False, "error": str(e), "mode": "adb"}
# =============================================================================
# 、Hawk Hook 统一执行入口(丝滑控制)
# =============================================================================
class HookExecuteRequest(BaseModel):
"""Hawk Hook 格式 — 单端点执行任意操作,任何应用可直连"""
device_id: str
platform: Platform = Platform.WECHAT
action: str # send_message | get_contacts | post_moments | ... 共 96 个
params: dict = Field(default_factory=dict, description="action 所需参数")
@router.post("/hook/execute", response_model=dict, tags=["Hook模块管理"])
async def hook_execute(req: HookExecuteRequest):
"""
Hawk Hook 统一执行 — 单接口控制整台手机
任意应用只需调此端点,传 action + params 即可执行 96 种操作。
支持 WebSocket(Agent) 与 ADB 双通道,执行更丝滑。
示例:
```json
{"device_id":"xgfe65eimrrofyws","platform":"wechat","action":"send_message","params":{"to_id":"阿猫","content":"你好"}}
```
"""
_check_device_online(req.device_id)
result = await _execute_skill(
req.device_id, req.platform.value, req.action, req.params or {}
)
payload = result.get("data") if result.get("data") is not None else result
return {"code": 200, "data": payload, "channel_used": "sdk_control"}
# ============================================================================= # =============================================================================
# 一、消息管理接口 # 一、消息管理接口
# ============================================================================= # =============================================================================

View File

@@ -0,0 +1,209 @@
"""
连接优先级管理器
5种控制模式的可用性检测、优先级路由和智能降级。
优先级(高→低):
1. Hook/Frida — 深度控制,读写微信内部数据
2. Agent WebSocket — 实时双向设备端App常驻
3. ADB 静默控制 — 无需AppADB直连兜底
4. AI Agent — 自然语言编排,依赖以上通道
5. scrcpy 可视化 — 投屏+人工接管
"""
from __future__ import annotations
import logging
import subprocess
from dataclasses import dataclass, field
from enum import IntEnum
from typing import Any, Dict, List, Optional
logger = logging.getLogger(__name__)
class ControlMode(IntEnum):
HOOK = 1
AGENT_WS = 2
ADB = 3
AI_AGENT = 4
SCRCPY = 5
MODE_META = {
ControlMode.HOOK: {
"id": "hook",
"name": "Hook / Frida",
"desc": "读写内部能力强,适合微信深度控制",
"requires": "Root + Frida Server",
},
ControlMode.AGENT_WS: {
"id": "agent",
"name": "Agent WebSocket",
"desc": "实时双向通信设备端App常驻",
"requires": "设备端安装工作App",
},
ControlMode.ADB: {
"id": "adb",
"name": "ADB 静默控制",
"desc": "无需Root部署简单稳定兜底",
"requires": "USB调试已开启",
},
ControlMode.AI_AGENT: {
"id": "ai",
"name": "AI Agent",
"desc": "自然语言编排复杂任务",
"requires": "需要底层通道(Hook/WS/ADB)之一可用",
},
ControlMode.SCRCPY: {
"id": "scrcpy",
"name": "scrcpy 可视化",
"desc": "投屏与人工接管",
"requires": "ADB连接 + scrcpy已安装",
},
}
@dataclass
class ModeStatus:
mode: ControlMode
available: bool
priority: int
detail: str = ""
meta: Dict[str, Any] = field(default_factory=dict)
def to_dict(self) -> Dict[str, Any]:
m = MODE_META.get(self.mode, {})
return {
"id": m.get("id", str(self.mode)),
"name": m.get("name", ""),
"desc": m.get("desc", ""),
"requires": m.get("requires", ""),
"priority": self.priority,
"available": self.available,
"detail": self.detail,
**self.meta,
}
class ConnectionPriorityManager:
"""对单台设备评估所有控制模式的可用性并按优先级排序。"""
def __init__(self):
self._scrcpy_available: Optional[bool] = None
def evaluate(self, device_id: str) -> List[ModeStatus]:
from services.ws_hub import ws_hub
from services.adb_device import adb_manager
from services.hook_module_service import hook_module_service
results: List[ModeStatus] = []
# 1. Hook / Frida
hook_ok = self._check_hook(device_id, hook_module_service)
results.append(ModeStatus(
mode=ControlMode.HOOK,
available=hook_ok,
priority=1,
detail="Frida Server 运行中" if hook_ok else "未检测到 Hook 服务",
))
# 2. Agent WebSocket
ws_ok = ws_hub.is_online(device_id)
ws_detail = "WebSocket 已连接"
if not ws_ok:
adb_serials = adb_manager.scan_devices()
for serial in adb_serials:
adb_dev = adb_manager.get_device(serial)
if adb_dev:
android_id = adb_dev._shell("settings get secure android_id").strip()
import hashlib
md5_id = hashlib.md5(android_id.encode()).hexdigest()
if md5_id == device_id:
ws_detail = f"设备在线(ADB:{serial})但Agent App未连接WebSocket"
break
else:
ws_detail = "设备Agent未连接"
results.append(ModeStatus(
mode=ControlMode.AGENT_WS,
available=ws_ok,
priority=2,
detail=ws_detail,
))
# 3. ADB
adb_ok = False
adb_serial = ""
adb_dev_obj = adb_manager.get_device(device_id)
if adb_dev_obj and adb_dev_obj.is_online():
adb_ok = True
adb_serial = device_id
else:
for serial in adb_manager.scan_devices():
dev = adb_manager.get_device(serial)
if dev and dev.is_online():
adb_ok = True
adb_serial = serial
break
results.append(ModeStatus(
mode=ControlMode.ADB,
available=adb_ok,
priority=3,
detail=f"ADB 已连接 ({adb_serial})" if adb_ok else "ADB 未连接",
meta={"adb_serial": adb_serial} if adb_ok else {},
))
# 4. AI Agent依赖底层通道
has_base = ws_ok or adb_ok or hook_ok
results.append(ModeStatus(
mode=ControlMode.AI_AGENT,
available=has_base,
priority=4,
detail="底层通道就绪" if has_base else "需要至少一个底层通道",
))
# 5. scrcpy
scrcpy_ok = adb_ok and self._check_scrcpy()
results.append(ModeStatus(
mode=ControlMode.SCRCPY,
available=scrcpy_ok,
priority=5,
detail="scrcpy 可用" if scrcpy_ok else ("ADB未连接" if not adb_ok else "scrcpy 未安装"),
))
results.sort(key=lambda m: m.priority)
return results
def get_best_mode(self, device_id: str) -> Optional[ModeStatus]:
modes = self.evaluate(device_id)
for m in modes:
if m.available:
return m
return None
def choose_execution_channel(self, device_id: str) -> str:
"""返回最优执行通道标识: hook / agent / adb / ai / offline"""
best = self.get_best_mode(device_id)
if not best:
return "offline"
return MODE_META[best.mode]["id"]
@staticmethod
def _check_hook(device_id: str, hook_service) -> bool:
try:
modules = hook_service.get_device_modules(device_id) if hasattr(hook_service, "get_device_modules") else []
return bool(modules)
except Exception:
return False
def _check_scrcpy(self) -> bool:
if self._scrcpy_available is not None:
return self._scrcpy_available
try:
r = subprocess.run(["which", "scrcpy"], capture_output=True, text=True, timeout=3)
self._scrcpy_available = r.returncode == 0
except Exception:
self._scrcpy_available = False
return self._scrcpy_available
connection_priority = ConnectionPriorityManager()

View File

@@ -150,10 +150,13 @@ class WeChatADBEngine:
def _input(self, text: str, clear: bool = False): def _input(self, text: str, clear: bool = False):
if clear: if clear:
self._sh("input keyevent KEYCODE_CTRL_A") try:
time.sleep(0.1) self._sh("input keyevent KEYCODE_CTRL_A", timeout=2)
self._sh("input keyevent KEYCODE_DEL") time.sleep(0.1)
time.sleep(0.1) self._sh("input keyevent KEYCODE_DEL", timeout=2)
time.sleep(0.1)
except Exception as e:
logger.info(f"清空输入框失败,继续输入: {e}")
has_cjk = any(ord(c) > 127 for c in text) has_cjk = any(ord(c) > 127 for c in text)
if has_cjk: if has_cjk:
self._input_cjk(text) self._input_cjk(text)
@@ -165,9 +168,18 @@ class WeChatADBEngine:
def _input_cjk(self, text: str): def _input_cjk(self, text: str):
escaped = text.replace("'", "'\\''").replace('"', '\\"') escaped = text.replace("'", "'\\''").replace('"', '\\"')
result = self._sh(f"am broadcast -a ADB_INPUT_TEXT --es msg '{escaped}' 2>&1") try:
if "Broadcast completed" not in result: result = self._sh(
self._sh(f"input text '{escaped}'") f"am broadcast -a ADB_INPUT_TEXT --es msg '{escaped}' 2>&1",
timeout=2,
)
if "Broadcast completed" in result:
return
except Exception as e:
logger.info(f"ADB_INPUT_TEXT 不可用,回退系统输入: {e}")
# 部分 MIUI/HyperOS 设备支持直接 input text 中文,这里作为兜底。
self._sh(f"input text '{escaped}'", timeout=5)
def _ensure_wechat(self) -> bool: def _ensure_wechat(self) -> bool:
cur = self._sh("dumpsys window | grep -E 'mCurrentFocus'") cur = self._sh("dumpsys window | grep -E 'mCurrentFocus'")

1327
sdk/app/static/hub.html Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{ {
"version": "8.0.56", "version": "8.0.56",
"screen": "1080x2400", "screen": "1080x2400",
"updated": "2026-03-14 17:57:15", "updated": "2026-03-16 03:26:29",
"pages": { "pages": {
"me": { "me": {
"recorded_at": "2026-03-14 16:32", "recorded_at": "2026-03-14 16:32",
@@ -323,9 +323,9 @@
} }
], ],
"target_page": "account_security", "target_page": "account_security",
"success_count": 1, "success_count": 3,
"fail_count": 0, "fail_count": 0,
"last_success": "2026-03-14 17:57:15" "last_success": "2026-03-16 03:26:29"
}, },
"to_safety_center": { "to_safety_center": {
"name": "to_safety_center", "name": "to_safety_center",

View File

@@ -0,0 +1,7 @@
{"action": "get_profile", "params": {}, "device": "dc9c23e00510", "started_at": "2026-03-16 03:19:20", "steps": [{"type": "tap_tab", "detail": "我", "coords": [945, 2148], "success": true, "ts": 7.69}], "success": true, "duration_ms": 12246, "ended_at": "2026-03-16 03:19:33", "result_summary": "{'success': True, 'mode': 'adb', 'timestamp': 1773602373089, 'profile': {}}"}
{"action": "get_contacts", "params": {"limit": 5}, "device": "dc9c23e00510", "started_at": "2026-03-16 03:19:58", "steps": [{"type": "tap_tab", "detail": "通讯录", "coords": [405, 2148], "success": true, "ts": 7.16}], "success": true, "duration_ms": 8164, "ended_at": "2026-03-16 03:20:07", "result_summary": "{'success': True, 'mode': 'adb', 'timestamp': 1773602407116, 'contacts': [], 'count': 0}"}
{"action": "check_account_status", "params": {}, "device": "dc9c23e00510", "started_at": "2026-03-16 03:19:33", "steps": [{"type": "tap_tab", "detail": "我", "coords": [945, 2148], "success": true, "ts": 8.07}, {"type": "tap_tab", "detail": "我", "coords": [945, 2148], "success": true, "ts": 9.58}, {"type": "scroll_up", "detail": "", "coords": null, "success": true, "ts": 13.0}, {"type": "tap_text", "detail": "设置", "coords": [197, 1676], "success": true, "ts": 17.3}, {"type": "scroll_to_top", "detail": "", "coords": null, "success": true, "ts": 26.49}, {"type": "tap_text", "detail": "账号与安全", "coords": [250, 300], "success": true, "ts": 30.81}], "success": true, "duration_ms": 38876, "ended_at": "2026-03-16 03:20:12", "result_summary": "{'success': True, 'mode': 'adb', 'timestamp': 1773602412573, 'status': {'account_active': True}}"}
{"action": "send_message", "params": {"to_id": "文件传输助手", "content": "新设备 dc9c23e00510 已接入,可直接控制。"}, "device": "dc9c23e00510", "started_at": "2026-03-16 03:25:30", "steps": [], "success": false, "duration_ms": 24899, "ended_at": "2026-03-16 03:25:55", "result_summary": "{'success': False, 'error': 'Command \\'[\\'adb\\', \\'-s\\', \\'dc9c23e00510\\', \\'shell\\', \"am broadcast -a ADB_INPUT_TEXT --es msg \\'文件传输助手\\' 2>&1\"]\\' timed out after 10 seconds', 'mode': 'adb'}", "error": "Command '['adb', '-s', 'dc9c23e00510', 'shell', \"am broadcast -a ADB_INPUT_TEXT --es msg '文件传输助手' 2>&1\"]' timed out after 10 seconds"}
{"action": "check_account_status", "params": {}, "device": "dc9c23e00510", "started_at": "2026-03-16 03:25:56", "steps": [{"type": "tap_tab", "detail": "我", "coords": [945, 2148], "success": true, "ts": 7.69}, {"type": "tap_tab", "detail": "我", "coords": [945, 2148], "success": true, "ts": 9.2}, {"type": "scroll_up", "detail": "", "coords": null, "success": true, "ts": 13.09}, {"type": "tap_text", "detail": "设置", "coords": [197, 1676], "success": true, "ts": 18.37}, {"type": "scroll_to_top", "detail": "", "coords": null, "success": true, "ts": 28.15}, {"type": "tap_text", "detail": "账号与安全", "coords": [250, 300], "success": true, "ts": 32.43}], "success": true, "duration_ms": 40568, "ended_at": "2026-03-16 03:26:37", "result_summary": "{'success': True, 'mode': 'adb', 'timestamp': 1773602797298, 'status': {'account_active': True}}"}
{"action": "send_message", "params": {"to_id": "阿猫", "content": "新设备已接入,现在可以直接控制。"}, "device": "dc9c23e00510", "started_at": "2026-03-16 03:28:06", "steps": [], "success": false, "duration_ms": 19579, "ended_at": "2026-03-16 03:28:26", "result_summary": "{'success': False, 'error': \"Command '['adb', '-s', 'dc9c23e00510', 'shell', 'input keyevent KEYCODE_CTRL_A']' timed out after 10 seconds\", 'mode': 'adb'}", "error": "Command '['adb', '-s', 'dc9c23e00510', 'shell', 'input keyevent KEYCODE_CTRL_A']' timed out after 10 seconds"}
{"action": "send_message", "params": {"to_id": "阿猫", "content": "新设备已接入,现在可以直接控制。"}, "device": "dc9c23e00510", "started_at": "2026-03-16 03:29:06", "steps": [], "success": true, "duration_ms": 30823, "ended_at": "2026-03-16 03:29:36", "result_summary": "{'success': True, 'mode': 'adb', 'timestamp': 1773602976885, 'message_id': 'adb_1773602976885'}"}

View File

@@ -15,6 +15,7 @@ pydantic-settings
# HTTP客户端 # HTTP客户端
httpx httpx
markdown
# 设备控制 # 设备控制
uiautomator2 uiautomator2

View File

@@ -5,6 +5,38 @@
--- ---
## 2026-03-16 | 设备部署Redmi 21121119SC Soul + Hook + Agent 安装
### 执行人: 阿机(设备部署)+ 阿服(环境)
### 设备信息
- 型号: Redmi 21121119SC (selene)
- Android: 11 | 架构: arm64-v8a | Root: Magisk
- 序列号: dc9c23e00510
- 连接方式: Type-C USB
### 完成内容
1. **Soul APP 安装** — 从 APKPure 下载 XAPK (284MB),拆包后 `adb install-multiple` 安装成功
- 包名: cn.soulapp.android
- 启动 Activity: `.component.startup.main.MainActivity`
2. **Frida Server 部署** — v17.8.1 arm64已推送到 `/data/local/tmp/frida-server`
- 非 root 下可枚举进程root attach 需手机上 Magisk 授权(待操作)
3. **uiautomator2 UI 控制验证** — 验证通过,可完全控制设备 + Soul APP
4. **Agent 终端安装**Termux 内 `~/workphone-agent/`
- Python 3.13.12 + websockets 16.0 + uiautomator2 3.5.0 + httpx 0.28.1 + Pillow 12.1.1
- config.json 已配置device_id=dc9c23e00510, server=ws://192.168.1.100:8899/ws/device
- Soul Skill 已包含在 agent 包中
5. **package.sh 更新** — 新增 Soul 技能目录到打包脚本
### 待操作
- 手机上 Magisk 授权 root → Frida 可 attach 到 Soul 进程进行深度 Hook
- 启动 SDK 服务器后运行 `cd ~/workphone-agent && python3 agent.py` 连接
- Soul APP 首次打开需登录账号
### 进度: 设备部署 100% | 整体项目 96%(不变)
---
## 2026-03-15 | 防封模块深度验证+13项BUG修复 ## 2026-03-15 | 防封模块深度验证+13项BUG修复
### 执行人: 火炬(机擎全员) ### 执行人: 火炬(机擎全员)

Binary file not shown.

After

Width:  |  Height:  |  Size: 1023 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 933 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 244 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB