Files
workphone-sdk/sdk/workphone_api_server.py

505 lines
18 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
工作手机SDK - HTTP REST API服务
版本: v5.0
功能: 将所有Frida微信控制接口封装为HTTP API供存客宝等系统调用
启动: python3 workphone_api_server.py
默认端口: 8899
API文档: http://localhost:8899/docs
"""
import frida, time, json, subprocess, os, threading, logging
from flask import Flask, request, jsonify
from datetime import datetime
from functools import wraps
# ============================================================
# 配置
# ============================================================
PHONE_IP = os.environ.get("PHONE_IP", "192.168.110.80")
FRIDA_PORT = int(os.environ.get("FRIDA_PORT", "27042"))
ADB_SERIAL = os.environ.get("ADB_SERIAL", "192.168.110.80:5555")
API_PORT = int(os.environ.get("API_PORT", "8899"))
API_KEY = os.environ.get("API_KEY", "workphone-sdk-2026")
BASE = os.path.dirname(os.path.abspath(__file__))
HOOK_JS = os.path.join(BASE, "sdk/agent/hook/wechat_full_control_v5.js")
logging.basicConfig(level=logging.INFO, format='%(asctime)s [%(levelname)s] %(message)s')
logger = logging.getLogger("WorkphoneAPI")
app = Flask(__name__)
# ============================================================
# Frida连接管理
# ============================================================
class FridaManager:
def __init__(self):
self.device = None
self.session = None
self.script = None
self.rpc = None
self.connected = False
self.wechat_pid = None
self.lock = threading.Lock()
self.events = []
def get_wechat_pid(self):
try:
result = subprocess.run(
["adb", "-s", ADB_SERIAL, "shell", "ps -A | grep 'com.tencent.mm$'"],
capture_output=True, text=True, timeout=5
)
line = result.stdout.strip()
if line:
return int(line.split()[1])
except Exception as e:
logger.error(f"获取PID失败: {e}")
return None
def connect(self):
with self.lock:
try:
self.wechat_pid = self.get_wechat_pid()
if not self.wechat_pid:
return False, "无法获取微信PID"
dm = frida.get_device_manager()
self.device = dm.add_remote_device(f"{PHONE_IP}:{FRIDA_PORT}")
self.session = self.device.attach(self.wechat_pid)
def on_msg(m, d):
if m.get("type") == "send":
p = m["payload"]
self.events.append({**p, "time": datetime.now().isoformat()})
if len(self.events) > 1000:
self.events = self.events[-500:]
logger.info(f"[EVENT:{p.get('type','')}] {json.dumps(p, ensure_ascii=False)[:100]}")
with open(HOOK_JS, encoding="utf-8") as f:
src = f.read()
self.script = self.session.create_script(src)
self.script.on("message", on_msg)
self.script.load()
time.sleep(3)
self.rpc = self.script.exports_sync
self.connected = True
logger.info(f"Frida连接成功 PID={self.wechat_pid}")
return True, f"连接成功 PID={self.wechat_pid}"
except Exception as e:
self.connected = False
logger.error(f"Frida连接失败: {e}")
return False, str(e)
def reconnect(self):
try:
if self.session:
try: self.session.detach()
except: pass
except: pass
self.connected = False
return self.connect()
def call(self, method, params=None):
if not self.connected or not self.rpc:
ok, msg = self.reconnect()
if not ok:
return {"success": False, "error": f"Frida未连接: {msg}"}
try:
fn = getattr(self.rpc, method)
result = fn(params) if params is not None else fn()
return result
except frida.InvalidOperationError as e:
logger.warning(f"Frida会话失效重连中: {e}")
ok, msg = self.reconnect()
if ok:
try:
fn = getattr(self.rpc, method)
return fn(params) if params is not None else fn()
except Exception as e2:
return {"success": False, "error": str(e2)}
return {"success": False, "error": f"重连失败: {msg}"}
except Exception as e:
return {"success": False, "error": str(e)}
frida_mgr = FridaManager()
# ============================================================
# 中间件
# ============================================================
def require_api_key(f):
@wraps(f)
def decorated(*args, **kwargs):
key = request.headers.get("X-API-Key") or request.args.get("api_key")
if key != API_KEY:
return jsonify({"success": False, "error": "Invalid API Key", "code": 401}), 401
return f(*args, **kwargs)
return decorated
def api_response(data, status=200):
data["timestamp"] = datetime.now().isoformat()
return jsonify(data), status
def get_params():
if request.method == "GET":
return dict(request.args)
try:
return request.get_json(force=True) or {}
except:
return {}
# ============================================================
# 路由
# ============================================================
# --- 系统 ---
@app.route("/api/v1/system/info", methods=["GET"])
@require_api_key
def system_info():
return api_response(frida_mgr.call("getSystemInfo"))
@app.route("/api/v1/system/connect", methods=["POST"])
@require_api_key
def system_connect():
ok, msg = frida_mgr.connect() if not frida_mgr.connected else (True, "已连接")
return api_response({"success": ok, "message": msg, "pid": frida_mgr.wechat_pid})
@app.route("/api/v1/system/reconnect", methods=["POST"])
@require_api_key
def system_reconnect():
ok, msg = frida_mgr.reconnect()
return api_response({"success": ok, "message": msg, "pid": frida_mgr.wechat_pid})
@app.route("/api/v1/system/events", methods=["GET"])
@require_api_key
def system_events():
limit = int(request.args.get("limit", 50))
return api_response({"success": True, "events": frida_mgr.events[-limit:], "total": len(frida_mgr.events)})
@app.route("/api/v1/system/screenshot", methods=["POST"])
@require_api_key
def system_screenshot():
params = get_params()
path = params.get("path", "/sdcard/sdk_screenshot.png")
r = frida_mgr.call("takeScreenshot", {"path": path})
if r.get("success"):
local = f"/tmp/sdk_screenshot_{int(time.time())}.png"
subprocess.run(["adb", "-s", ADB_SERIAL, "pull", path, local], capture_output=True)
r["local_path"] = local
return api_response(r)
# --- 账号 ---
@app.route("/api/v1/account/info", methods=["GET"])
@require_api_key
def account_info():
return api_response(frida_mgr.call("getAccountInfo"))
# --- 联系人 ---
@app.route("/api/v1/contacts", methods=["GET"])
@require_api_key
def get_contacts():
params = {
"limit": int(request.args.get("limit", 200)),
"offset": int(request.args.get("offset", 0)),
"keyword": request.args.get("keyword")
}
return api_response(frida_mgr.call("getContacts", params))
@app.route("/api/v1/contacts/count", methods=["GET"])
@require_api_key
def contacts_count():
return api_response(frida_mgr.call("getContactsCount"))
@app.route("/api/v1/contacts/<wxid>", methods=["GET"])
@require_api_key
def contact_detail(wxid):
return api_response(frida_mgr.call("getContactDetail", {"wxid": wxid}))
@app.route("/api/v1/contacts/<wxid>/remark", methods=["PUT"])
@require_api_key
def set_remark(wxid):
params = get_params()
return api_response(frida_mgr.call("setRemark", {"wxid": wxid, "remark": params.get("remark")}))
@app.route("/api/v1/contacts/add", methods=["POST"])
@require_api_key
def add_friend():
params = get_params()
return api_response(frida_mgr.call("addFriend", {"wxid": params.get("wxid")}))
@app.route("/api/v1/contacts/search", methods=["GET"])
@require_api_key
def search_contacts():
return api_response(frida_mgr.call("searchContacts", {"keyword": request.args.get("keyword")}))
# --- 消息 ---
@app.route("/api/v1/messages/send", methods=["POST"])
@require_api_key
def send_message():
params = get_params()
return api_response(frida_mgr.call("sendMessage", {
"to_id": params.get("to_id"),
"content": params.get("content"),
"type": params.get("type", 1)
}))
@app.route("/api/v1/messages/mass", methods=["POST"])
@require_api_key
def mass_message():
params = get_params()
return api_response(frida_mgr.call("massMessage", {
"targets": params.get("targets", []),
"content": params.get("content")
}))
@app.route("/api/v1/messages", methods=["GET"])
@require_api_key
def get_messages():
params = {
"talker": request.args.get("talker"),
"limit": int(request.args.get("limit", 50))
}
return api_response(frida_mgr.call("getMessages", params))
@app.route("/api/v1/messages/search", methods=["GET"])
@require_api_key
def search_messages():
return api_response(frida_mgr.call("searchMessages", {"keyword": request.args.get("keyword")}))
@app.route("/api/v1/conversations", methods=["GET"])
@require_api_key
def get_conversations():
return api_response(frida_mgr.call("getConversations", {"limit": int(request.args.get("limit", 20))}))
# --- 朋友圈 ---
@app.route("/api/v1/moments", methods=["GET"])
@require_api_key
def get_moments():
params = {
"limit": int(request.args.get("limit", 20)),
"offset": int(request.args.get("offset", 0))
}
return api_response(frida_mgr.call("getMoments", params))
@app.route("/api/v1/moments/post", methods=["POST"])
@require_api_key
def post_moment():
params = get_params()
return api_response(frida_mgr.call("postMoment", {"content": params.get("content")}))
@app.route("/api/v1/moments/<sns_id>", methods=["GET"])
@require_api_key
def moment_detail(sns_id):
return api_response(frida_mgr.call("getMomentDetail", {"sns_id": sns_id}))
@app.route("/api/v1/moments/<sns_id>/comments", methods=["GET"])
@require_api_key
def moment_comments(sns_id):
return api_response(frida_mgr.call("getMomentComments", {"sns_id": sns_id}))
# --- 视频号 ---
@app.route("/api/v1/finder/contacts", methods=["GET"])
@require_api_key
def finder_contacts():
return api_response(frida_mgr.call("getFinderContacts", {"limit": int(request.args.get("limit", 20))}))
@app.route("/api/v1/finder/videos", methods=["GET"])
@require_api_key
def finder_videos():
return api_response(frida_mgr.call("getFinderVideos", {"limit": int(request.args.get("limit", 20))}))
@app.route("/api/v1/finder/open", methods=["POST"])
@require_api_key
def open_finder():
return api_response(frida_mgr.call("openFinder"))
# --- 收藏 ---
@app.route("/api/v1/favorites", methods=["GET"])
@require_api_key
def get_favorites():
return api_response(frida_mgr.call("getFavorites", {"limit": int(request.args.get("limit", 50))}))
# --- 红包/钱包 ---
@app.route("/api/v1/wallet/redpackets", methods=["GET"])
@require_api_key
def get_redpackets():
return api_response(frida_mgr.call("getRedPackets", {"limit": int(request.args.get("limit", 50))}))
@app.route("/api/v1/wallet/info", methods=["GET"])
@require_api_key
def wallet_info():
return api_response(frida_mgr.call("getWalletInfo"))
@app.route("/api/v1/wallet/ledger", methods=["GET"])
@require_api_key
def wallet_ledger():
return api_response(frida_mgr.call("getWalletLedger", {"limit": int(request.args.get("limit", 100))}))
# --- 文件/媒体 ---
@app.route("/api/v1/media/files", methods=["GET"])
@require_api_key
def get_files():
return api_response(frida_mgr.call("getFiles", {"limit": int(request.args.get("limit", 50))}))
@app.route("/api/v1/media/images", methods=["GET"])
@require_api_key
def get_images():
return api_response(frida_mgr.call("getImages", {"limit": int(request.args.get("limit", 50))}))
@app.route("/api/v1/media/videos", methods=["GET"])
@require_api_key
def get_videos():
return api_response(frida_mgr.call("getVideos", {"limit": int(request.args.get("limit", 50))}))
@app.route("/api/v1/media/voices", methods=["GET"])
@require_api_key
def get_voices():
return api_response(frida_mgr.call("getVoices", {"limit": int(request.args.get("limit", 50))}))
# --- 群组 ---
@app.route("/api/v1/groups", methods=["GET"])
@require_api_key
def get_groups():
return api_response(frida_mgr.call("getGroups", {"limit": int(request.args.get("limit", 100))}))
@app.route("/api/v1/groups/<group_id>", methods=["GET"])
@require_api_key
def group_detail(group_id):
return api_response(frida_mgr.call("getGroupDetail", {"group_id": group_id}))
@app.route("/api/v1/groups/<group_id>/members", methods=["GET"])
@require_api_key
def group_members(group_id):
return api_response(frida_mgr.call("getGroupMembers", {"group_id": group_id}))
@app.route("/api/v1/groups/<group_id>/notice", methods=["PUT"])
@require_api_key
def set_group_notice(group_id):
params = get_params()
return api_response(frida_mgr.call("setGroupNotice", {"group_id": group_id, "notice": params.get("notice")}))
@app.route("/api/v1/groups/<group_id>/name", methods=["PUT"])
@require_api_key
def set_group_name(group_id):
params = get_params()
return api_response(frida_mgr.call("setGroupName", {"group_id": group_id, "name": params.get("name")}))
@app.route("/api/v1/groups/<group_id>/nickname", methods=["PUT"])
@require_api_key
def set_group_nickname(group_id):
params = get_params()
return api_response(frida_mgr.call("setGroupNickname", {"group_id": group_id, "nickname": params.get("nickname")}))
@app.route("/api/v1/groups/<group_id>/kick", methods=["POST"])
@require_api_key
def kick_member(group_id):
params = get_params()
return api_response(frida_mgr.call("kickGroupMember", {"group_id": group_id, "member_id": params.get("member_id")}))
@app.route("/api/v1/groups/<group_id>/message", methods=["POST"])
@require_api_key
def send_group_message(group_id):
params = get_params()
return api_response(frida_mgr.call("sendMessage", {"to_id": group_id, "content": params.get("content")}))
# --- 标签 ---
@app.route("/api/v1/labels", methods=["GET"])
@require_api_key
def get_labels():
return api_response(frida_mgr.call("getLabels"))
@app.route("/api/v1/labels/<label_id>/members", methods=["GET"])
@require_api_key
def label_members(label_id):
return api_response(frida_mgr.call("getLabelMembers", {"label_id": label_id}))
# --- 小程序 ---
@app.route("/api/v1/miniapps", methods=["GET"])
@require_api_key
def get_miniapps():
return api_response(frida_mgr.call("getMiniApps", {"limit": int(request.args.get("limit", 50))}))
@app.route("/api/v1/miniapps/open", methods=["POST"])
@require_api_key
def open_miniapp():
params = get_params()
return api_response(frida_mgr.call("openMiniApp", {"app_id": params.get("app_id")}))
# --- 导航 ---
@app.route("/api/v1/navigate/chat", methods=["POST"])
@require_api_key
def navigate_chat():
params = get_params()
return api_response(frida_mgr.call("navigateToChat", {"wxid": params.get("wxid")}))
@app.route("/api/v1/navigate/moments", methods=["POST"])
@require_api_key
def navigate_moments():
return api_response(frida_mgr.call("navigateToMoments"))
@app.route("/api/v1/navigate/contacts", methods=["POST"])
@require_api_key
def navigate_contacts():
return api_response(frida_mgr.call("navigateToContacts"))
# --- 高级SQL ---
@app.route("/api/v1/db/query", methods=["POST"])
@require_api_key
def db_query():
params = get_params()
return api_response(frida_mgr.call("rawQuery", {"sql": params.get("sql"), "db": params.get("db")}))
@app.route("/api/v1/db/info", methods=["GET"])
@require_api_key
def db_info():
return api_response(frida_mgr.call("getDbInfo"))
@app.route("/api/v1/db/tables", methods=["GET"])
@require_api_key
def db_tables():
return api_response(frida_mgr.call("listAllTables"))
# --- 健康检查 ---
@app.route("/health", methods=["GET"])
def health():
return jsonify({
"status": "ok",
"connected": frida_mgr.connected,
"pid": frida_mgr.wechat_pid,
"timestamp": datetime.now().isoformat()
})
@app.route("/docs", methods=["GET"])
def docs():
routes = []
for rule in app.url_map.iter_rules():
if rule.endpoint not in ("static", "health", "docs"):
routes.append({
"path": str(rule),
"methods": list(rule.methods - {"HEAD", "OPTIONS"}),
"endpoint": rule.endpoint
})
routes.sort(key=lambda x: x["path"])
return jsonify({
"title": "工作手机SDK API文档",
"version": "v5.0",
"base_url": f"http://{{host}}:{API_PORT}",
"auth": "Header: X-API-Key: workphone-sdk-2026",
"total_endpoints": len(routes),
"endpoints": routes
})
# ============================================================
# 启动
# ============================================================
if __name__ == "__main__":
logger.info("=== 工作手机SDK API服务 v5.0 ===")
logger.info(f"手机IP: {PHONE_IP}:{FRIDA_PORT}")
logger.info("正在连接Frida...")
ok, msg = frida_mgr.connect()
logger.info(f"Frida连接: {''+msg if ok else ''+msg}")
logger.info(f"API服务启动: http://0.0.0.0:{API_PORT}")
logger.info(f"API文档: http://localhost:{API_PORT}/docs")
logger.info(f"健康检查: http://localhost:{API_PORT}/health")
app.run(host="0.0.0.0", port=API_PORT, debug=False, threaded=True)