750 lines
22 KiB
Python
750 lines
22 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
工作手机 SDK - 完整HTTP REST API服务
|
||
版本: v9-complete
|
||
功能: 微信全量控制接口,基于Frida无感注入
|
||
"""
|
||
|
||
import frida
|
||
import json
|
||
import time
|
||
import threading
|
||
import subprocess
|
||
import os
|
||
import sys
|
||
from flask import Flask, request, jsonify
|
||
from functools import wraps
|
||
import logging
|
||
|
||
logging.basicConfig(level=logging.INFO, format='%(asctime)s %(levelname)s %(message)s')
|
||
logger = logging.getLogger(__name__)
|
||
|
||
app = Flask(__name__)
|
||
|
||
# 配置
|
||
API_KEY = os.environ.get('API_KEY', 'workphone-sdk-2026')
|
||
PHONE_IP = os.environ.get('PHONE_IP', '192.168.110.80')
|
||
PHONE_PORT = int(os.environ.get('PHONE_PORT', 27042))
|
||
JS_FILE = os.path.join(os.path.dirname(__file__), 'sdk/agent/hook/wechat_api_v8_fix.js')
|
||
|
||
# 全局状态
|
||
class SDKState:
|
||
device = None
|
||
session = None
|
||
script = None
|
||
connected = False
|
||
wechat_pid = None
|
||
lock = threading.Lock()
|
||
last_heartbeat = 0
|
||
|
||
state = SDKState()
|
||
|
||
def get_js_code():
|
||
"""读取JS脚本"""
|
||
js_paths = [
|
||
JS_FILE,
|
||
os.path.join(os.path.dirname(__file__), 'wechat_api_v8_fix.js'),
|
||
'/Users/karuo/Documents/开发/2、私域银行/工作手机/sdk/agent/hook/wechat_api_v8_fix.js',
|
||
]
|
||
for p in js_paths:
|
||
if os.path.exists(p):
|
||
with open(p, 'r', encoding='utf-8') as f:
|
||
return f.read()
|
||
raise FileNotFoundError(f"JS文件未找到,已尝试: {js_paths}")
|
||
|
||
def get_wechat_pid():
|
||
"""获取微信主进程PID"""
|
||
try:
|
||
result = subprocess.run(
|
||
['adb', '-s', f'{PHONE_IP}:5555', 'shell', 'ps -A | grep com.tencent.mm'],
|
||
capture_output=True, text=True, timeout=5
|
||
)
|
||
for line in result.stdout.split('\n'):
|
||
if 'com.tencent.mm' in line and ':push' not in line and ':sandbox' not in line and ':tools' not in line:
|
||
parts = line.split()
|
||
if len(parts) >= 2:
|
||
return int(parts[1])
|
||
except Exception as e:
|
||
logger.error(f"获取微信PID失败: {e}")
|
||
return None
|
||
|
||
def ensure_frida_forward():
|
||
"""确保ADB端口转发"""
|
||
try:
|
||
subprocess.run(
|
||
['adb', '-s', f'{PHONE_IP}:5555', 'forward', 'tcp:27042', 'tcp:27042'],
|
||
capture_output=True, timeout=5
|
||
)
|
||
except Exception as e:
|
||
logger.warning(f"端口转发失败: {e}")
|
||
|
||
def connect_frida():
|
||
"""连接Frida并注入脚本"""
|
||
with state.lock:
|
||
try:
|
||
ensure_frida_forward()
|
||
|
||
# 获取设备
|
||
dm = frida.get_device_manager()
|
||
state.device = dm.add_remote_device('localhost:27042')
|
||
|
||
# 获取微信PID
|
||
pid = get_wechat_pid()
|
||
if not pid:
|
||
# 尝试启动微信
|
||
subprocess.run(
|
||
['adb', '-s', f'{PHONE_IP}:5555', 'shell',
|
||
'monkey -p com.tencent.mm -c android.intent.category.LAUNCHER 1'],
|
||
capture_output=True, timeout=10
|
||
)
|
||
time.sleep(3)
|
||
pid = get_wechat_pid()
|
||
|
||
if not pid:
|
||
raise Exception("无法获取微信PID")
|
||
|
||
state.wechat_pid = pid
|
||
logger.info(f"微信PID: {pid}")
|
||
|
||
# Attach
|
||
state.session = state.device.attach(pid)
|
||
|
||
# 加载脚本
|
||
js_code = get_js_code()
|
||
state.script = state.session.create_script(js_code)
|
||
|
||
def on_message(message, data):
|
||
if message.get('type') == 'send':
|
||
payload = message.get('payload', {})
|
||
if isinstance(payload, dict):
|
||
logger.debug(f"Frida: {payload.get('type', '')} - {str(payload.get('data', ''))[:100]}")
|
||
elif message.get('type') == 'error':
|
||
logger.error(f"Frida错误: {message.get('description', '')}")
|
||
|
||
state.script.on('message', on_message)
|
||
state.script.load()
|
||
|
||
# 初始化DB
|
||
time.sleep(1)
|
||
try:
|
||
state.script.exports.initDbs()
|
||
except Exception as e:
|
||
logger.warning(f"initDbs: {e}")
|
||
|
||
state.connected = True
|
||
state.last_heartbeat = time.time()
|
||
logger.info("✅ Frida连接成功")
|
||
return True
|
||
|
||
except Exception as e:
|
||
logger.error(f"Frida连接失败: {e}")
|
||
state.connected = False
|
||
return False
|
||
|
||
def ensure_connected():
|
||
"""确保Frida连接有效"""
|
||
if not state.connected or not state.script:
|
||
return connect_frida()
|
||
|
||
# 心跳检测(每30秒)
|
||
if time.time() - state.last_heartbeat > 30:
|
||
try:
|
||
state.script.exports.ping()
|
||
state.last_heartbeat = time.time()
|
||
except Exception:
|
||
logger.warning("Frida心跳失败,重新连接...")
|
||
state.connected = False
|
||
return connect_frida()
|
||
|
||
return True
|
||
|
||
def call_rpc(method, *args):
|
||
"""调用Frida RPC方法"""
|
||
if not ensure_connected():
|
||
raise Exception("Frida未连接")
|
||
|
||
try:
|
||
func = getattr(state.script.exports, method)
|
||
result = func(*args) if args else func()
|
||
state.last_heartbeat = time.time()
|
||
return result
|
||
except Exception as e:
|
||
logger.error(f"RPC调用失败 {method}: {e}")
|
||
# 尝试重连一次
|
||
if 'session' in str(e).lower() or 'detach' in str(e).lower():
|
||
state.connected = False
|
||
if connect_frida():
|
||
func = getattr(state.script.exports, method)
|
||
return func(*args) if args else func()
|
||
raise
|
||
|
||
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({'error': '无效的API Key'}), 401
|
||
return f(*args, **kwargs)
|
||
return decorated
|
||
|
||
def success(data=None, **kwargs):
|
||
resp = {'success': True, 'timestamp': int(time.time() * 1000)}
|
||
if data is not None:
|
||
resp['data'] = data
|
||
resp.update(kwargs)
|
||
return jsonify(resp)
|
||
|
||
def error(msg, code=500):
|
||
return jsonify({'success': False, 'error': str(msg)}), code
|
||
|
||
# ==================== 系统接口 ====================
|
||
|
||
@app.route('/api/v1/ping', methods=['GET'])
|
||
def ping():
|
||
return jsonify({
|
||
'status': 'ok',
|
||
'version': 'v9-complete',
|
||
'frida_connected': state.connected,
|
||
'wechat_pid': state.wechat_pid,
|
||
'timestamp': int(time.time() * 1000)
|
||
})
|
||
|
||
@app.route('/api/v1/init', methods=['POST'])
|
||
@require_api_key
|
||
def init():
|
||
result = connect_frida()
|
||
return success({'connected': result, 'pid': state.wechat_pid})
|
||
|
||
@app.route('/api/v1/dbs', methods=['GET'])
|
||
@require_api_key
|
||
def list_dbs():
|
||
try:
|
||
result = call_rpc('getDbInfo')
|
||
return success(result)
|
||
except Exception as e:
|
||
return error(e)
|
||
|
||
# ==================== 账号接口 ====================
|
||
|
||
@app.route('/api/v1/account', methods=['GET'])
|
||
@require_api_key
|
||
def get_account():
|
||
try:
|
||
result = call_rpc('getAccountInfo')
|
||
return success(result)
|
||
except Exception as e:
|
||
return error(e)
|
||
|
||
# ==================== 联系人接口 ====================
|
||
|
||
@app.route('/api/v1/contacts', methods=['GET'])
|
||
@require_api_key
|
||
def get_contacts():
|
||
limit = int(request.args.get('limit', 100))
|
||
offset = int(request.args.get('offset', 0))
|
||
try:
|
||
result = call_rpc('getContacts', limit, offset)
|
||
return success(result)
|
||
except Exception as e:
|
||
return error(e)
|
||
|
||
@app.route('/api/v1/contacts/count', methods=['GET'])
|
||
@require_api_key
|
||
def get_contacts_count():
|
||
try:
|
||
result = call_rpc('getContactStats')
|
||
return success(result)
|
||
except Exception as e:
|
||
return error(e)
|
||
|
||
@app.route('/api/v1/contacts/search', methods=['GET'])
|
||
@require_api_key
|
||
def search_contacts():
|
||
keyword = request.args.get('keyword', '')
|
||
limit = int(request.args.get('limit', 50))
|
||
try:
|
||
result = call_rpc('searchContacts', keyword, limit)
|
||
return success(result)
|
||
except Exception as e:
|
||
return error(e)
|
||
|
||
@app.route('/api/v1/contacts/<wxid>', methods=['GET'])
|
||
@require_api_key
|
||
def get_contact(wxid):
|
||
try:
|
||
result = call_rpc('getContactDetail', wxid)
|
||
return success(result)
|
||
except Exception as e:
|
||
return error(e)
|
||
|
||
# ==================== 消息接口 ====================
|
||
|
||
@app.route('/api/v1/messages/send', methods=['POST'])
|
||
@require_api_key
|
||
def send_message():
|
||
data = request.get_json()
|
||
to_user = data.get('to_user', 'filehelper')
|
||
content = data.get('content', '')
|
||
msg_type = data.get('type', 1)
|
||
|
||
if not content:
|
||
return error('content不能为空', 400)
|
||
|
||
try:
|
||
result = call_rpc('sendMessage', to_user, content, msg_type)
|
||
return success(result)
|
||
except Exception as e:
|
||
return error(e)
|
||
|
||
@app.route('/api/v1/messages/mass', methods=['POST'])
|
||
@require_api_key
|
||
def mass_send():
|
||
data = request.get_json()
|
||
targets = data.get('targets', [])
|
||
content = data.get('content', '')
|
||
|
||
if not targets or not content:
|
||
return error('targets和content不能为空', 400)
|
||
|
||
try:
|
||
result = call_rpc('massMessage', targets, content)
|
||
return success(result)
|
||
except Exception as e:
|
||
return error(e)
|
||
|
||
@app.route('/api/v1/messages/<talker>', methods=['GET'])
|
||
@require_api_key
|
||
def get_messages(talker):
|
||
limit = int(request.args.get('limit', 50))
|
||
try:
|
||
result = call_rpc('getMessages', talker, limit)
|
||
return success(result)
|
||
except Exception as e:
|
||
return error(e)
|
||
|
||
@app.route('/api/v1/messages/recent', methods=['GET'])
|
||
@require_api_key
|
||
def get_recent_messages():
|
||
hours = int(request.args.get('hours', 24))
|
||
try:
|
||
result = call_rpc('getRecentMessages', hours)
|
||
return success(result)
|
||
except Exception as e:
|
||
return error(e)
|
||
|
||
# ==================== 会话接口 ====================
|
||
|
||
@app.route('/api/v1/conversations', methods=['GET'])
|
||
@require_api_key
|
||
def get_conversations():
|
||
limit = int(request.args.get('limit', 30))
|
||
try:
|
||
result = call_rpc('getConversations', limit)
|
||
return success(result)
|
||
except Exception as e:
|
||
return error(e)
|
||
|
||
# ==================== 群组接口 ====================
|
||
|
||
@app.route('/api/v1/groups', methods=['GET'])
|
||
@require_api_key
|
||
def get_groups():
|
||
limit = int(request.args.get('limit', 50))
|
||
try:
|
||
result = call_rpc('getGroups', limit)
|
||
return success(result)
|
||
except Exception as e:
|
||
return error(e)
|
||
|
||
@app.route('/api/v1/groups/count', methods=['GET'])
|
||
@require_api_key
|
||
def get_groups_count():
|
||
try:
|
||
result = call_rpc('getGroupStats')
|
||
return success(result)
|
||
except Exception as e:
|
||
return error(e)
|
||
|
||
@app.route('/api/v1/groups/<chatroom_id>/members', methods=['GET'])
|
||
@require_api_key
|
||
def get_group_members(chatroom_id):
|
||
try:
|
||
result = call_rpc('getGroupMembers', chatroom_id)
|
||
return success(result)
|
||
except Exception as e:
|
||
return error(e)
|
||
|
||
@app.route('/api/v1/groups/<chatroom_id>/remark', methods=['PUT'])
|
||
@require_api_key
|
||
def set_group_remark(chatroom_id):
|
||
data = request.get_json()
|
||
remark = data.get('remark', '')
|
||
try:
|
||
result = call_rpc('setGroupRemark', chatroom_id, remark)
|
||
return success(result)
|
||
except Exception as e:
|
||
return error(e)
|
||
|
||
@app.route('/api/v1/groups/<chatroom_id>/announcement', methods=['PUT'])
|
||
@require_api_key
|
||
def set_group_announcement(chatroom_id):
|
||
data = request.get_json()
|
||
announcement = data.get('announcement', '')
|
||
try:
|
||
result = call_rpc('setGroupAnnouncement', chatroom_id, announcement)
|
||
return success(result)
|
||
except Exception as e:
|
||
return error(e)
|
||
|
||
# ==================== 标签接口 ====================
|
||
|
||
@app.route('/api/v1/labels', methods=['GET'])
|
||
@require_api_key
|
||
def get_labels():
|
||
try:
|
||
result = call_rpc('getLabels')
|
||
return success(result)
|
||
except Exception as e:
|
||
return error(e)
|
||
|
||
@app.route('/api/v1/labels/<label_id>/contacts', methods=['GET'])
|
||
@require_api_key
|
||
def get_label_contacts(label_id):
|
||
limit = int(request.args.get('limit', 100))
|
||
try:
|
||
result = call_rpc('getContactsByLabel', label_id, limit)
|
||
return success(result)
|
||
except Exception as e:
|
||
return error(e)
|
||
|
||
# ==================== 朋友圈接口 ====================
|
||
|
||
@app.route('/api/v1/moments', methods=['GET'])
|
||
@require_api_key
|
||
def get_moments():
|
||
limit = int(request.args.get('limit', 20))
|
||
try:
|
||
result = call_rpc('getMoments', limit)
|
||
return success(result)
|
||
except Exception as e:
|
||
return error(e)
|
||
|
||
@app.route('/api/v1/moments', methods=['POST'])
|
||
@require_api_key
|
||
def post_moment():
|
||
data = request.get_json()
|
||
content = data.get('content', '')
|
||
if not content:
|
||
return error('content不能为空', 400)
|
||
try:
|
||
result = call_rpc('postMoment', content)
|
||
return success(result)
|
||
except Exception as e:
|
||
return error(e)
|
||
|
||
@app.route('/api/v1/moments/dbinfo', methods=['GET'])
|
||
@require_api_key
|
||
def moments_dbinfo():
|
||
try:
|
||
result = call_rpc('getMomentsDbInfo')
|
||
return success(result)
|
||
except Exception as e:
|
||
return error(e)
|
||
|
||
# ==================== 视频号接口 ====================
|
||
|
||
@app.route('/api/v1/finder/info', methods=['GET'])
|
||
@require_api_key
|
||
def finder_info():
|
||
try:
|
||
result = call_rpc('getFinderInfo')
|
||
return success(result)
|
||
except Exception as e:
|
||
return error(e)
|
||
|
||
@app.route('/api/v1/finder/contacts', methods=['GET'])
|
||
@require_api_key
|
||
def finder_contacts():
|
||
limit = int(request.args.get('limit', 20))
|
||
try:
|
||
result = call_rpc('getFinderContacts', limit)
|
||
return success(result)
|
||
except Exception as e:
|
||
return error(e)
|
||
|
||
@app.route('/api/v1/finder/accounts', methods=['GET'])
|
||
@require_api_key
|
||
def finder_accounts():
|
||
try:
|
||
result = call_rpc('getFinderAccounts')
|
||
return success(result)
|
||
except Exception as e:
|
||
return error(e)
|
||
|
||
@app.route('/api/v1/finder/media', methods=['GET'])
|
||
@require_api_key
|
||
def finder_media():
|
||
limit = int(request.args.get('limit', 20))
|
||
try:
|
||
result = call_rpc('getFinderMedia', limit)
|
||
return success(result)
|
||
except Exception as e:
|
||
return error(e)
|
||
|
||
# ==================== 收藏接口 ====================
|
||
|
||
@app.route('/api/v1/favorites', methods=['GET'])
|
||
@require_api_key
|
||
def get_favorites():
|
||
limit = int(request.args.get('limit', 20))
|
||
try:
|
||
result = call_rpc('getFavorites', limit)
|
||
return success(result)
|
||
except Exception as e:
|
||
return error(e)
|
||
|
||
# ==================== 钱包接口 ====================
|
||
|
||
@app.route('/api/v1/wallet/info', methods=['GET'])
|
||
@require_api_key
|
||
def wallet_info():
|
||
try:
|
||
result = call_rpc('getWalletInfo')
|
||
return success(result)
|
||
except Exception as e:
|
||
return error(e)
|
||
|
||
@app.route('/api/v1/wallet/ledger', methods=['GET'])
|
||
@require_api_key
|
||
def wallet_ledger():
|
||
limit = int(request.args.get('limit', 20))
|
||
try:
|
||
result = call_rpc('getWalletLedger', limit)
|
||
return success(result)
|
||
except Exception as e:
|
||
return error(e)
|
||
|
||
@app.route('/api/v1/wallet/lucky-money', methods=['GET'])
|
||
@require_api_key
|
||
def lucky_money():
|
||
limit = int(request.args.get('limit', 20))
|
||
try:
|
||
result = call_rpc('getLuckyMoney', limit)
|
||
return success(result)
|
||
except Exception as e:
|
||
return error(e)
|
||
|
||
# ==================== 媒体接口 ====================
|
||
|
||
@app.route('/api/v1/media/images', methods=['GET'])
|
||
@require_api_key
|
||
def get_images():
|
||
limit = int(request.args.get('limit', 20))
|
||
try:
|
||
result = call_rpc('getImages', limit)
|
||
return success(result)
|
||
except Exception as e:
|
||
return error(e)
|
||
|
||
@app.route('/api/v1/media/images/count', methods=['GET'])
|
||
@require_api_key
|
||
def images_count():
|
||
try:
|
||
result = call_rpc('getImageCount')
|
||
return success(result)
|
||
except Exception as e:
|
||
return error(e)
|
||
|
||
@app.route('/api/v1/media/videos', methods=['GET'])
|
||
@require_api_key
|
||
def get_videos():
|
||
limit = int(request.args.get('limit', 20))
|
||
try:
|
||
result = call_rpc('getVideos', limit)
|
||
return success(result)
|
||
except Exception as e:
|
||
return error(e)
|
||
|
||
@app.route('/api/v1/media/files', methods=['GET'])
|
||
@require_api_key
|
||
def get_files():
|
||
limit = int(request.args.get('limit', 20))
|
||
try:
|
||
result = call_rpc('getFiles', limit)
|
||
return success(result)
|
||
except Exception as e:
|
||
return error(e)
|
||
|
||
# ==================== 小程序接口 ====================
|
||
|
||
@app.route('/api/v1/appbrand', methods=['GET'])
|
||
@require_api_key
|
||
def get_appbrand():
|
||
try:
|
||
result = call_rpc('getAppBrandInfo')
|
||
return success(result)
|
||
except Exception as e:
|
||
return error(e)
|
||
|
||
# ==================== 搜索接口 ====================
|
||
|
||
@app.route('/api/v1/search', methods=['GET'])
|
||
@require_api_key
|
||
def search():
|
||
keyword = request.args.get('keyword', '')
|
||
if not keyword:
|
||
return error('keyword不能为空', 400)
|
||
try:
|
||
result = call_rpc('globalSearch', keyword)
|
||
return success(result)
|
||
except Exception as e:
|
||
return error(e)
|
||
|
||
# ==================== 原始SQL接口 ====================
|
||
|
||
@app.route('/api/v1/raw/query', methods=['POST'])
|
||
@require_api_key
|
||
def raw_query():
|
||
data = request.get_json()
|
||
db = data.get('db', 'main')
|
||
sql = data.get('sql', '')
|
||
if not sql:
|
||
return error('sql不能为空', 400)
|
||
try:
|
||
result = call_rpc('rawSql', db, sql)
|
||
return success(result)
|
||
except Exception as e:
|
||
return error(e)
|
||
|
||
@app.route('/api/v1/raw/write', methods=['POST'])
|
||
@require_api_key
|
||
def raw_write():
|
||
data = request.get_json()
|
||
db = data.get('db', 'main')
|
||
sql = data.get('sql', '')
|
||
if not sql:
|
||
return error('sql不能为空', 400)
|
||
# 安全检查
|
||
sql_upper = sql.strip().upper()
|
||
if sql_upper.startswith('DROP') or sql_upper.startswith('TRUNCATE'):
|
||
return error('不允许DROP/TRUNCATE操作', 403)
|
||
try:
|
||
result = call_rpc('rawSqlWrite', db, sql)
|
||
return success(result)
|
||
except Exception as e:
|
||
return error(e)
|
||
|
||
@app.route('/api/v1/raw/tables', methods=['GET'])
|
||
@require_api_key
|
||
def list_tables():
|
||
db = request.args.get('db', 'main')
|
||
try:
|
||
result = call_rpc('listTables', db)
|
||
return success(result)
|
||
except Exception as e:
|
||
return error(e)
|
||
|
||
@app.route('/api/v1/raw/table', methods=['GET'])
|
||
@require_api_key
|
||
def table_info():
|
||
db = request.args.get('db', 'main')
|
||
table = request.args.get('table', '')
|
||
if not table:
|
||
return error('table不能为空', 400)
|
||
try:
|
||
result = call_rpc('getTableInfo', db, table)
|
||
return success(result)
|
||
except Exception as e:
|
||
return error(e)
|
||
|
||
# ==================== 存客宝对接接口 ====================
|
||
|
||
@app.route('/api/v3/cunke-bao/contacts/sync', methods=['POST'])
|
||
@require_api_key
|
||
def ckb_sync_contacts():
|
||
"""批量同步联系人到存客宝"""
|
||
data = request.get_json()
|
||
limit = data.get('limit', 100)
|
||
offset = data.get('offset', 0)
|
||
try:
|
||
contacts = call_rpc('getContacts', limit, offset)
|
||
# 格式化为存客宝格式
|
||
ckb_contacts = []
|
||
for c in (contacts.get('contacts', []) if isinstance(contacts, dict) else []):
|
||
ckb_contacts.append({
|
||
'wxid': c.get('username', ''),
|
||
'nickname': c.get('nickname', ''),
|
||
'remark': c.get('remark', ''),
|
||
'avatar': c.get('headImgUrl', ''),
|
||
'labels': c.get('contactLabelIds', ''),
|
||
'sex': c.get('sex', 0),
|
||
'region': f"{c.get('province', '')} {c.get('city', '')}".strip(),
|
||
})
|
||
return success({'contacts': ckb_contacts, 'count': len(ckb_contacts), 'total': contacts.get('total', 0) if isinstance(contacts, dict) else 0})
|
||
except Exception as e:
|
||
return error(e)
|
||
|
||
@app.route('/api/v3/cunke-bao/message/send', methods=['POST'])
|
||
@require_api_key
|
||
def ckb_send_message():
|
||
"""存客宝发送消息"""
|
||
data = request.get_json()
|
||
to_user = data.get('wxid', data.get('to_user', ''))
|
||
content = data.get('content', data.get('message', ''))
|
||
if not to_user or not content:
|
||
return error('wxid和content不能为空', 400)
|
||
try:
|
||
result = call_rpc('sendMessage', to_user, content, 1)
|
||
return success(result)
|
||
except Exception as e:
|
||
return error(e)
|
||
|
||
@app.route('/api/v3/cunke-bao/groups', methods=['GET'])
|
||
@require_api_key
|
||
def ckb_get_groups():
|
||
"""存客宝获取群列表"""
|
||
try:
|
||
result = call_rpc('getGroups', 100)
|
||
return success(result)
|
||
except Exception as e:
|
||
return error(e)
|
||
|
||
# ==================== 健康检查和自动重连 ====================
|
||
|
||
def heartbeat_thread():
|
||
"""后台心跳线程,保持Frida连接"""
|
||
while True:
|
||
try:
|
||
time.sleep(60)
|
||
if state.connected:
|
||
try:
|
||
call_rpc('ping')
|
||
logger.debug("心跳正常")
|
||
except Exception as e:
|
||
logger.warning(f"心跳失败: {e},尝试重连...")
|
||
state.connected = False
|
||
connect_frida()
|
||
except Exception as e:
|
||
logger.error(f"心跳线程异常: {e}")
|
||
|
||
if __name__ == '__main__':
|
||
print("=" * 60)
|
||
print("工作手机 SDK API 服务 v9-complete")
|
||
print(f"手机IP: {PHONE_IP}")
|
||
print(f"API Key: {API_KEY}")
|
||
print("=" * 60)
|
||
|
||
# 初始连接
|
||
logger.info("正在连接Frida...")
|
||
if connect_frida():
|
||
logger.info("✅ Frida连接成功")
|
||
else:
|
||
logger.warning("⚠️ Frida初始连接失败,将在请求时重试")
|
||
|
||
# 启动心跳线程
|
||
t = threading.Thread(target=heartbeat_thread, daemon=True)
|
||
t.start()
|
||
|
||
# 启动API服务
|
||
app.run(host='0.0.0.0', port=8899, debug=False, threaded=True)
|