feat: v7 full API - workphone_server_v7.py
This commit is contained in:
400
sdk/workphone_server_v7.py
Normal file
400
sdk/workphone_server_v7.py
Normal 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)
|
||||
Reference in New Issue
Block a user