#!/usr/bin/env python3 """ 工作手机 SDK v7 - HTTP REST API 服务 基于 Frida + 微信8.0.56 开发 提供50+个HTTP接口,供存客宝等系统调用 启动方式: PHONE_IP=192.168.110.80 python3 workphone_server_v7.py 接口地址:http://localhost:8899/api/v1/... 认证方式:Header: X-API-Key: workphone-sdk-2026 """ import frida import json import time import threading import os import subprocess from flask import Flask, request, jsonify from functools import wraps app = Flask(__name__) # ==================== 配置 ==================== PHONE_IP = os.environ.get('PHONE_IP', '192.168.110.80') PHONE_PORT = int(os.environ.get('PHONE_PORT', '5555')) WECHAT_PKG = 'com.tencent.mm' API_KEY = os.environ.get('API_KEY', 'workphone-sdk-2026') SERVER_PORT = int(os.environ.get('SERVER_PORT', '8899')) JS_PATH = os.path.join(os.path.dirname(__file__), 'sdk/agent/hook/wechat_api_v7.js') # ==================== Frida管理 ==================== _session = None _script = None _lock = threading.Lock() def get_js_code(): """读取JS代码""" if os.path.exists(JS_PATH): with open(JS_PATH, 'r') as f: return f.read() # 内嵌备用代码 return open(os.path.join(os.path.dirname(__file__), 'wechat_api_v7.js')).read() def connect_frida(): """连接Frida""" global _session, _script with _lock: try: if _session and _script: # 测试连接是否有效 try: _script.exports.ping() return True except: pass # 重新连接 device = frida.get_device_manager().add_remote_device(f'{PHONE_IP}:{PHONE_PORT}') _session = device.attach(WECHAT_PKG) js_code = get_js_code() _script = _session.create_script(js_code) _script.load() return True except Exception as e: print(f'[Frida] Connect failed: {e}') _session = None _script = None return False def call_rpc(method, *args): """调用Frida RPC""" if not connect_frida(): return {'error': 'Frida connection failed'} try: fn = getattr(_script.exports, method) result = fn(*args) return result except Exception as e: return {'error': str(e)} # ==================== 认证中间件 ==================== 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': 'Unauthorized', 'code': 401}), 401 return f(*args, **kwargs) return decorated def api_response(data, status=200): return jsonify({'code': 0, 'data': data, 'timestamp': int(time.time())}), status def api_error(msg, status=400): return jsonify({'code': -1, 'error': msg, 'timestamp': int(time.time())}), status # ==================== API路由 ==================== # --- 系统 --- @app.route('/api/v1/ping', methods=['GET']) def ping(): return api_response({'status': 'ok', 'version': 'v7', 'phone': PHONE_IP}) @app.route('/api/v1/system/info', methods=['GET']) @require_api_key def system_info(): return api_response(call_rpc('getSystemInfo')) @app.route('/api/v1/system/db', methods=['GET']) @require_api_key def db_info(): return api_response(call_rpc('getDbInfo')) @app.route('/api/v1/system/screenshot', methods=['POST']) @require_api_key def screenshot(): data = request.json or {} result = call_rpc('takeScreenshot', data.get('path', '/sdcard/wpsdk_screenshot.png')) if result.get('success'): # 通过ADB拉取截图 path = result['path'] local_path = f'/tmp/screenshot_{int(time.time())}.png' subprocess.run(['adb', '-s', f'{PHONE_IP}:{PHONE_PORT}', 'pull', path, local_path]) result['local_path'] = local_path return api_response(result) # --- 账号 --- @app.route('/api/v1/account', methods=['GET']) @require_api_key def get_account(): return api_response(call_rpc('getAccountInfo')) # --- 联系人 --- @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)) return api_response(call_rpc('getContacts', limit, offset)) @app.route('/api/v1/contacts/count', methods=['GET']) @require_api_key def get_contact_count(): return api_response(call_rpc('getContactCount')) @app.route('/api/v1/contacts/search', methods=['GET']) @require_api_key def search_contacts(): keyword = request.args.get('q', '') if not keyword: return api_error('Missing query parameter: q') return api_response(call_rpc('searchContact', keyword)) @app.route('/api/v1/contacts/', methods=['GET']) @require_api_key def get_contact(wxid): return api_response(call_rpc('getContactDetail', wxid)) @app.route('/api/v1/contacts/stats', methods=['GET']) @require_api_key def contact_stats(): return api_response(call_rpc('getContactStats')) # --- 消息 --- @app.route('/api/v1/messages', methods=['GET']) @require_api_key def get_messages(): talker = request.args.get('talker', 'filehelper') limit = int(request.args.get('limit', 50)) return api_response(call_rpc('getMessages', talker, limit)) @app.route('/api/v1/messages/recent', methods=['GET']) @require_api_key def get_recent_messages(): hours = int(request.args.get('hours', 24)) return api_response(call_rpc('getRecentMessages', hours)) @app.route('/api/v1/messages/send', methods=['POST']) @require_api_key def send_message(): data = request.json or {} to_user = data.get('to_user') content = data.get('content') if not to_user or not content: return api_error('Missing required fields: to_user, content') return api_response(call_rpc('sendMessage', to_user, content)) @app.route('/api/v1/messages/mass', methods=['POST']) @require_api_key def mass_message(): data = request.json or {} targets = data.get('targets', []) content = data.get('content') if not targets or not content: return api_error('Missing required fields: targets, content') return api_response(call_rpc('massMessage', targets, content)) # --- 会话 --- @app.route('/api/v1/conversations', methods=['GET']) @require_api_key def get_conversations(): limit = int(request.args.get('limit', 30)) return api_response(call_rpc('getConversations', limit)) # --- 群组 --- @app.route('/api/v1/groups', methods=['GET']) @require_api_key def get_groups(): limit = int(request.args.get('limit', 50)) return api_response(call_rpc('getGroups', limit)) @app.route('/api/v1/groups/count', methods=['GET']) @require_api_key def get_group_count(): return api_response(call_rpc('getGroupCount')) @app.route('/api/v1/groups/', methods=['GET']) @require_api_key def get_group(group_id): return api_response(call_rpc('getGroupDetail', group_id)) @app.route('/api/v1/groups//members', methods=['GET']) @require_api_key def get_group_members(group_id): return api_response(call_rpc('getGroupMembers', group_id)) @app.route('/api/v1/groups//announcement', methods=['GET']) @require_api_key def get_group_announcement(group_id): return api_response(call_rpc('getGroupAnnouncement', group_id)) @app.route('/api/v1/groups//remark', methods=['PUT']) @require_api_key def set_group_remark(group_id): data = request.json or {} remark = data.get('remark', '') return api_response(call_rpc('setGroupRemark', group_id, remark)) # --- 标签 --- @app.route('/api/v1/labels', methods=['GET']) @require_api_key def get_labels(): return api_response(call_rpc('getLabels')) @app.route('/api/v1/labels//contacts', methods=['GET']) @require_api_key def get_contacts_by_label(label_id): return api_response(call_rpc('getContactsByLabel', label_id)) # --- 朋友圈 --- @app.route('/api/v1/moments', methods=['GET']) @require_api_key def get_moments(): limit = int(request.args.get('limit', 20)) return api_response(call_rpc('getMoments', limit)) @app.route('/api/v1/moments/count', methods=['GET']) @require_api_key def get_moment_count(): return api_response(call_rpc('getMomentCount')) @app.route('/api/v1/moments', methods=['POST']) @require_api_key def post_moment(): data = request.json or {} content = data.get('content') if not content: return api_error('Missing required field: content') return api_response(call_rpc('postMoment', content)) # --- 视频号 --- @app.route('/api/v1/finder/info', methods=['GET']) @require_api_key def get_finder_info(): return api_response(call_rpc('getFinderInfo')) @app.route('/api/v1/finder/contacts', methods=['GET']) @require_api_key def get_finder_contacts(): return api_response(call_rpc('getFinderContacts')) @app.route('/api/v1/finder/media', methods=['GET']) @require_api_key def get_finder_media(): return api_response(call_rpc('getFinderMedia')) # --- 收藏 --- @app.route('/api/v1/favorites', methods=['GET']) @require_api_key def get_favorites(): limit = int(request.args.get('limit', 20)) return api_response(call_rpc('getFavorites', limit)) # --- 钱包/红包 --- @app.route('/api/v1/wallet/info', methods=['GET']) @require_api_key def get_wallet_info(): return api_response(call_rpc('getWalletInfo')) @app.route('/api/v1/wallet/ledger', methods=['GET']) @require_api_key def get_wallet_ledger(): limit = int(request.args.get('limit', 20)) return api_response(call_rpc('getWalletLedger', limit)) @app.route('/api/v1/wallet/lucky-money', methods=['GET']) @require_api_key def get_lucky_money(): limit = int(request.args.get('limit', 20)) return api_response(call_rpc('getLuckyMoney', limit)) @app.route('/api/v1/wallet/balance', methods=['GET']) @require_api_key def get_wallet_balance(): return api_response(call_rpc('getWalletBalance')) # --- 图片/文件/视频 --- @app.route('/api/v1/media/images', methods=['GET']) @require_api_key def get_images(): limit = int(request.args.get('limit', 20)) return api_response(call_rpc('getImageList', limit)) @app.route('/api/v1/media/images/count', methods=['GET']) @require_api_key def get_image_count(): return api_response(call_rpc('getImageCount')) @app.route('/api/v1/media/videos', methods=['GET']) @require_api_key def get_videos(): limit = int(request.args.get('limit', 20)) return api_response(call_rpc('getVideoList', limit)) @app.route('/api/v1/media/videos/count', methods=['GET']) @require_api_key def get_video_count(): return api_response(call_rpc('getVideoCount')) @app.route('/api/v1/media/files', methods=['GET']) @require_api_key def get_files(): limit = int(request.args.get('limit', 20)) return api_response(call_rpc('getFileList', limit)) # --- 小程序 --- @app.route('/api/v1/appbrand', methods=['GET']) @require_api_key def get_appbrand(): return api_response(call_rpc('getAppBrandList')) # --- 搜索 --- @app.route('/api/v1/search', methods=['GET']) @require_api_key def global_search(): keyword = request.args.get('q', '') if not keyword: return api_error('Missing query parameter: q') return api_response(call_rpc('globalSearch', keyword)) # --- 调试 --- @app.route('/api/v1/debug/sql', methods=['POST']) @require_api_key def raw_sql(): data = request.json or {} db_name = data.get('db', 'main') sql = data.get('sql', '') if not sql: return api_error('Missing required field: sql') return api_response(call_rpc('rawQuery', db_name, sql)) # ==================== 健康检查 ==================== @app.route('/health', methods=['GET']) def health(): return jsonify({'status': 'ok', 'version': 'v7'}) @app.route('/api/v1/endpoints', methods=['GET']) def list_endpoints(): """列出所有API端点""" endpoints = [] for rule in app.url_map.iter_rules(): if rule.endpoint != 'static': endpoints.append({ 'path': str(rule), 'methods': list(rule.methods - {'HEAD', 'OPTIONS'}) }) return jsonify({'endpoints': sorted(endpoints, key=lambda x: x['path']), 'count': len(endpoints)}) if __name__ == '__main__': print(f'[*] WorkPhone SDK v7 starting on port {SERVER_PORT}') print(f'[*] Phone: {PHONE_IP}:{PHONE_PORT}') print(f'[*] API Key: {API_KEY}') print(f'[*] Connecting to Frida...') if connect_frida(): print('[+] Frida connected successfully!') else: print('[-] Frida connection failed, will retry on first request') app.run(host='0.0.0.0', port=SERVER_PORT, debug=False, threaded=True)