Files
workphone-sdk/sdk/workphone_server_v7.py

401 lines
12 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
"""
工作手机 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/<wxid>', 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/<group_id>', methods=['GET'])
@require_api_key
def get_group(group_id):
return api_response(call_rpc('getGroupDetail', group_id))
@app.route('/api/v1/groups/<group_id>/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/<group_id>/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/<group_id>/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/<label_id>/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)