feat: v7 full API - workphone_server_v7.py

This commit is contained in:
2026-05-18 21:38:06 +08:00
parent 89c96e9ad4
commit ca22160000

400
sdk/workphone_server_v7.py Normal file
View File

@@ -0,0 +1,400 @@
#!/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)