feat: 补齐微信Frida手机SDK闭环

This commit is contained in:
Manus AI
2026-05-18 17:18:24 +08:00
parent 9118df9eee
commit cca1df7b09
25 changed files with 1467 additions and 0 deletions

View File

@@ -0,0 +1,26 @@
# 工作手机 SDK · 微信 Frida 无线控制补全包
本包用于承接上传 ZIP 中断的开发进度,补齐 **服务器 → WebSocket → 手机端 Agent → Frida → 微信 Hook** 的闭环。
## 快速启动
服务器端:
```bash
pip install -r requirements.txt
uvicorn server.app:app --host 0.0.0.0 --port 8000
```
手机 Termux 端:
```bash
python mobile_agent/wireless_agent.py --server-ws ws://服务器IP:8000/ws/phone --hook hooks/wechat_hook_bridge.js
```
验证端:
```bash
python scripts/verify_wechat_frida.py --base-url http://服务器IP:8000 --device-id phone-xxxx
```
如果已有完整 `wechat_hook_v3.js`,请替换 `hooks/wechat_hook_bridge.js`SDK 会自动兼容 camelCase / snake_case / lowercase 方法名。

View File

@@ -0,0 +1,578 @@
from pathlib import Path
ROOT = Path('/home/ubuntu/work_phone_sdk_completion')
files = {}
files['sdk/frida/frida_manager.py'] = r'''"""
工作手机 SDK · FridaManager
负责手机端或服务器端连接 Frida、attach 微信进程、加载 Hook 脚本,并提供兼容 RPC 调用。
设计重点:历史归档显示 Frida Python 对 rpc.exports 方法名存在混淆,因此这里采用多候选名兼容策略。
"""
from __future__ import annotations
from dataclasses import dataclass, field
from pathlib import Path
import re
import time
from typing import Any, Callable, Dict, Iterable, Optional
class FridaUnavailable(RuntimeError):
"""当前环境未安装或无法连接 Frida。"""
class RpcMethodMissing(RuntimeError):
"""Hook 中缺少指定 RPC 方法。"""
@dataclass
class FridaConfig:
device_host: str = "127.0.0.1"
device_port: int = 27042
package_name: str = "com.tencent.mm"
process_name: str = "WeChat"
attach_timeout: float = 15.0
prefer_usb: bool = False
hook_path: Optional[str] = None
@dataclass
class RpcResult:
ok: bool
action: str
method: str
data: Any = None
error: Optional[str] = None
elapsed_ms: int = 0
class FridaManager:
def __init__(self, config: Optional[FridaConfig] = None):
self.config = config or FridaConfig()
self.frida = None
self.device = None
self.session = None
self.script = None
self.exports = None
self.loaded_hook_path: Optional[str] = None
def _import_frida(self):
if self.frida is not None:
return self.frida
try:
import frida # type: ignore
except Exception as exc:
raise FridaUnavailable(f"未安装 frida Python 包或加载失败:{exc}") from exc
self.frida = frida
return frida
def connect(self):
frida = self._import_frida()
if self.config.prefer_usb:
self.device = frida.get_usb_device(timeout=int(self.config.attach_timeout))
else:
self.device = frida.get_device_manager().add_remote_device(
f"{self.config.device_host}:{self.config.device_port}"
)
return self.device
def attach(self, target: Optional[str] = None):
if self.device is None:
self.connect()
assert self.device is not None
target = target or self.config.package_name
last_error: Optional[Exception] = None
candidates: Iterable[Any] = [target, self.config.process_name]
for candidate in candidates:
try:
self.session = self.device.attach(candidate)
return self.session
except Exception as exc: # pragma: no cover - depends on phone
last_error = exc
try:
processes = self.device.enumerate_processes()
for proc in processes:
name = getattr(proc, 'name', '') or ''
pid = getattr(proc, 'pid', None)
if pid and (self.config.package_name in name or self.config.process_name.lower() in name.lower() or '微信' in name):
self.session = self.device.attach(pid)
return self.session
except Exception as exc: # pragma: no cover
last_error = exc
raise FridaUnavailable(f"无法 attach 微信进程:{last_error}")
def load_script(self, hook_path: Optional[str] = None):
path = Path(hook_path or self.config.hook_path or '')
if not path.exists():
raise FileNotFoundError(f"Hook 脚本不存在:{path}")
if self.session is None:
self.attach()
assert self.session is not None
source = path.read_text(encoding='utf-8')
self.script = self.session.create_script(source)
self.script.on('message', self._on_message)
self.script.load()
self.exports = getattr(self.script, 'exports_sync', None) or getattr(self.script, 'exports', None)
self.loaded_hook_path = str(path)
return self.script
def _on_message(self, message, data): # pragma: no cover - runtime callback
# 生产环境可转发到 EventReporter这里保持最小日志。
print({'frida_message': message, 'data_len': len(data) if data else 0})
@staticmethod
def _snake(name: str) -> str:
return re.sub(r'(?<!^)(?=[A-Z])', '_', name).lower()
@classmethod
def method_candidates(cls, name: str):
snake = cls._snake(name)
lower = name.lower()
candidates = [name, snake, lower]
seen = set()
for item in candidates:
if item and item not in seen:
seen.add(item)
yield item
def call(self, method: str, *args, **kwargs) -> RpcResult:
started = time.time()
if self.exports is None:
raise FridaUnavailable('Hook 脚本尚未加载,无法调用 RPC')
last_error = None
for candidate in self.method_candidates(method):
try:
fn = getattr(self.exports, candidate)
data = fn(*args, **kwargs)
return RpcResult(True, method, candidate, data=data, elapsed_ms=int((time.time() - started) * 1000))
except AttributeError as exc:
last_error = exc
except Exception as exc:
return RpcResult(False, method, candidate, error=str(exc), elapsed_ms=int((time.time() - started) * 1000))
# 兼容新版 frida 的 exports_sync.invoke 或 script.exports_sync.invoke
try:
invoke = getattr(self.exports, 'invoke')
data = invoke(method, list(args), kwargs or None)
return RpcResult(True, method, 'invoke', data=data, elapsed_ms=int((time.time() - started) * 1000))
except Exception as exc:
last_error = exc
return RpcResult(False, method, '', error=f'RPC 方法不存在或不可调用:{method}; last={last_error}', elapsed_ms=int((time.time() - started) * 1000))
def cleanup(self):
for obj, method in [(self.script, 'unload'), (self.session, 'detach')]:
if obj is not None:
try:
getattr(obj, method)()
except Exception:
pass
self.script = None
self.session = None
self.exports = None
'''
files['sdk/wechat/wechat_actions.py'] = r'''"""微信动作目录:服务器、手机端 Agent、验证脚本共同使用。"""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any, Dict, List
@dataclass(frozen=True)
class WechatAction:
action: str
rpc: str
module: str
description: str
required: List[str] = field(default_factory=list)
safe: bool = True
verify: bool = True
ACTIONS: List[WechatAction] = [
WechatAction('ping', 'ping', 'system', 'Hook 连通性检测'),
WechatAction('get_connection_status', 'getConnectionStatus', 'system', '读取连接状态'),
WechatAction('screenshot', 'takeScreenshot', 'device', '手机当前屏幕截图'),
WechatAction('ui_dump', 'dumpUiTree', 'device', '导出当前 UI 树'),
WechatAction('go_home', 'goHome', 'navigation', '返回微信首页'),
WechatAction('open_contacts', 'openContacts', 'navigation', '打开通讯录'),
WechatAction('open_chats', 'openChats', 'navigation', '打开聊天列表'),
WechatAction('search_contact', 'searchContact', 'contact', '搜索联系人', ['keyword']),
WechatAction('get_contacts', 'getContacts', 'contact', '读取联系人列表'),
WechatAction('get_contact_profile', 'getContactProfile', 'contact', '读取联系人资料', ['wxid']),
WechatAction('add_friend', 'addFriend', 'contact', '添加好友', ['keyword'], safe=False),
WechatAction('accept_friend', 'acceptFriend', 'contact', '通过好友申请', ['wxid'], safe=False),
WechatAction('send_text', 'sendTextMessage', 'message', '发送文本消息', ['to', 'text'], safe=False),
WechatAction('send_image', 'sendImageMessage', 'message', '发送图片消息', ['to', 'path'], safe=False),
WechatAction('get_messages', 'getMessages', 'message', '读取消息列表', ['wxid']),
WechatAction('open_chat', 'openChat', 'message', '打开指定会话', ['wxid']),
WechatAction('create_group', 'createGroup', 'group', '创建群聊', ['members'], safe=False),
WechatAction('invite_group_member', 'inviteGroupMember', 'group', '邀请群成员', ['chatroom', 'members'], safe=False),
WechatAction('get_group_members', 'getGroupMembers', 'group', '读取群成员', ['chatroom']),
WechatAction('browse_channels', 'browseChannels', 'channels', '浏览视频号'),
WechatAction('add_favorite', 'addFavorite', 'favorite', '收藏当前内容'),
WechatAction('add_custom_emoji', 'addCustomEmoji', 'emoji', '添加自定义表情', safe=False),
]
ACTION_MAP: Dict[str, WechatAction] = {item.action: item for item in ACTIONS}
RPC_MAP: Dict[str, WechatAction] = {item.rpc: item for item in ACTIONS}
def list_actions() -> List[Dict[str, Any]]:
return [item.__dict__.copy() for item in ACTIONS]
def validate_action(action: str, payload: Dict[str, Any]) -> WechatAction:
if action not in ACTION_MAP:
raise KeyError(f'未知微信动作:{action}')
spec = ACTION_MAP[action]
missing = [k for k in spec.required if k not in payload or payload[k] in (None, '')]
if missing:
raise ValueError(f'动作 {action} 缺少参数:{missing}')
return spec
'''
files['sdk/frida/hook_executor.py'] = r'''"""将业务 action 映射到 Frida RPC。"""
from __future__ import annotations
from dataclasses import asdict
from typing import Any, Dict, Optional
from sdk.frida.frida_manager import FridaManager, RpcResult
from sdk.wechat.wechat_actions import validate_action, list_actions
class HookExecutor:
def __init__(self, manager: FridaManager):
self.manager = manager
def execute(self, action: str, payload: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
payload = payload or {}
try:
spec = validate_action(action, payload)
except Exception as exc:
return {'ok': False, 'action': action, 'error': str(exc), 'stage': 'validate'}
args = [payload[k] for k in spec.required]
# 允许额外参数透传给 HookHook 若不支持会返回错误,便于报告定位。
extra = {k: v for k, v in payload.items() if k not in spec.required}
result: RpcResult = self.manager.call(spec.rpc, *args, **extra)
return {
'ok': result.ok,
'action': action,
'rpc': spec.rpc,
'method_used': result.method,
'module': spec.module,
'description': spec.description,
'safe': spec.safe,
'data': result.data,
'error': result.error,
'elapsed_ms': result.elapsed_ms,
}
def catalog(self):
return list_actions()
'''
files['mobile_agent/wireless_agent.py'] = r'''"""手机端 Termux/Python Agent通过 WebSocket 接收服务器动作,调用 Frida Hook。"""
from __future__ import annotations
import argparse
import asyncio
import json
import platform
import uuid
from pathlib import Path
from sdk.frida.frida_manager import FridaConfig, FridaManager
from sdk.frida.hook_executor import HookExecutor
try:
import websockets
except Exception: # pragma: no cover
websockets = None
class WirelessAgent:
def __init__(self, server_ws: str, hook_path: str, device_id: str | None = None):
self.server_ws = server_ws
self.hook_path = hook_path
self.device_id = device_id or f"phone-{uuid.getnode():x}"
self.manager = FridaManager(FridaConfig(hook_path=hook_path))
self.executor = HookExecutor(self.manager)
async def boot(self):
self.manager.connect()
self.manager.attach()
self.manager.load_script(self.hook_path)
def hello(self):
return {
'type': 'hello',
'device_id': self.device_id,
'platform': platform.platform(),
'actions': self.executor.catalog(),
}
async def run(self):
if websockets is None:
raise RuntimeError('请先安装 websocketspip install websockets')
await self.boot()
async with websockets.connect(self.server_ws, ping_interval=20, ping_timeout=20) as ws:
await ws.send(json.dumps(self.hello(), ensure_ascii=False))
async for raw in ws:
try:
msg = json.loads(raw)
if msg.get('type') != 'command':
continue
result = self.executor.execute(msg['action'], msg.get('payload') or {})
await ws.send(json.dumps({'type': 'result', 'request_id': msg.get('request_id'), 'device_id': self.device_id, 'result': result}, ensure_ascii=False))
except Exception as exc:
await ws.send(json.dumps({'type': 'error', 'error': str(exc)}, ensure_ascii=False))
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--server-ws', required=True, help='例如 ws://192.168.1.10:8000/ws/phone')
parser.add_argument('--hook', required=True, help='wechat_hook_bridge.js 或真实 wechat_hook_v3.js 路径')
parser.add_argument('--device-id')
args = parser.parse_args()
asyncio.run(WirelessAgent(args.server_ws, args.hook, args.device_id).run())
if __name__ == '__main__':
main()
'''
files['server/routes/wechat_frida.py'] = r'''"""FastAPI 微信 Frida 无线控制路由。"""
from __future__ import annotations
import asyncio
import json
import uuid
from dataclasses import dataclass, field
from typing import Any, Dict, Optional
from fastapi import APIRouter, WebSocket, WebSocketDisconnect, HTTPException
from pydantic import BaseModel
from sdk.wechat.wechat_actions import list_actions
router = APIRouter(prefix='/api/v3/wechat-frida', tags=['wechat-frida'])
@dataclass
class PhoneConn:
device_id: str
ws: WebSocket
meta: Dict[str, Any] = field(default_factory=dict)
pending: Dict[str, asyncio.Future] = field(default_factory=dict)
phones: Dict[str, PhoneConn] = {}
class ActionRequest(BaseModel):
device_id: str
action: str
payload: Dict[str, Any] = {}
timeout: float = 30.0
@router.get('/actions')
def actions():
return {'ok': True, 'actions': list_actions()}
@router.get('/devices')
def devices():
return {'ok': True, 'devices': [{'device_id': k, 'meta': v.meta} for k, v in phones.items()]}
@router.post('/execute')
async def execute(req: ActionRequest):
if req.device_id not in phones:
raise HTTPException(404, f'设备未连接:{req.device_id}')
conn = phones[req.device_id]
request_id = uuid.uuid4().hex
fut = asyncio.get_event_loop().create_future()
conn.pending[request_id] = fut
await conn.ws.send_text(json.dumps({'type': 'command', 'request_id': request_id, 'action': req.action, 'payload': req.payload}, ensure_ascii=False))
try:
return await asyncio.wait_for(fut, timeout=req.timeout)
finally:
conn.pending.pop(request_id, None)
async def phone_socket(websocket: WebSocket):
await websocket.accept()
device_id: Optional[str] = None
try:
async for raw in websocket.iter_text():
msg = json.loads(raw)
if msg.get('type') == 'hello':
device_id = msg.get('device_id') or uuid.uuid4().hex
phones[device_id] = PhoneConn(device_id=device_id, ws=websocket, meta=msg)
await websocket.send_text(json.dumps({'type': 'hello_ack', 'device_id': device_id}, ensure_ascii=False))
elif msg.get('type') == 'result':
rid = msg.get('request_id')
did = msg.get('device_id') or device_id
conn = phones.get(did or '')
if conn and rid in conn.pending and not conn.pending[rid].done():
conn.pending[rid].set_result({'ok': True, 'device_id': did, 'result': msg.get('result')})
elif msg.get('type') == 'error':
# 保留连接,错误由客户端下一次请求再显式返回。
pass
except WebSocketDisconnect:
pass
finally:
if device_id and device_id in phones:
phones.pop(device_id, None)
'''
files['server/app.py'] = r'''from fastapi import FastAPI, WebSocket
from server.routes.wechat_frida import router as wechat_frida_router, phone_socket
app = FastAPI(title='工作手机 SDK · 微信 Frida 控制服务', version='0.3.0')
app.include_router(wechat_frida_router)
@app.get('/health')
def health():
return {'ok': True, 'service': 'work-phone-sdk'}
@app.websocket('/ws/phone')
async def ws_phone(websocket: WebSocket):
await phone_socket(websocket)
'''
files['hooks/wechat_hook_bridge.js'] = r'''// 工作手机 SDK · 微信 Hook 桥接模板
// 说明这是安全桥接版优先提供连通性、截图、UI 导航等通用方法。
// 如果已有完整 wechat_hook_v3.js可直接替换本文件Python SDK 会兼容 camelCase/snake_case 调用。
'use strict';
function ok(data) { return { ok: true, data: data || null, ts: Date.now() }; }
function fail(message) { return { ok: false, error: String(message), ts: Date.now() }; }
function runJava(fn) {
let result;
Java.perform(function () {
try { result = fn(); } catch (e) { result = fail(e.stack || e.message || e); }
});
return result;
}
rpc.exports = {
ping: function () { return 'pong from wechat_hook_bridge'; },
getConnectionStatus: function () {
return ok({ java_available: Java.available, process: Process.id, arch: Process.arch, platform: Process.platform });
},
takeScreenshot: function () {
// 截图建议在手机端通过 uiautomator/screencap 实现Hook 层返回占位Agent 可扩展真实文件路径。
return ok({ mode: 'placeholder', message: '请在 mobile_agent 扩展 screencap -p 后回传文件路径' });
},
dumpUiTree: function () { return ok({ mode: 'placeholder', message: '建议通过 uiautomator dump 获取 UI XML' }); },
goHome: function () { return ok({ action: 'goHome', message: '桥接模板未执行 UI 点击;请替换完整 Hook 后验证' }); },
openContacts: function () { return ok({ action: 'openContacts', message: 'NOT_IMPLEMENTED_IN_TEMPLATE' }); },
openChats: function () { return ok({ action: 'openChats', message: 'NOT_IMPLEMENTED_IN_TEMPLATE' }); },
searchContact: function (keyword) { return ok({ keyword: keyword, message: 'NOT_IMPLEMENTED_IN_TEMPLATE' }); },
getContacts: function () { return ok({ contacts: [], message: 'NOT_IMPLEMENTED_IN_TEMPLATE' }); },
getContactProfile: function (wxid) { return ok({ wxid: wxid, message: 'NOT_IMPLEMENTED_IN_TEMPLATE' }); },
addFriend: function (keyword) { return fail('危险动作未在模板中实现addFriend ' + keyword); },
acceptFriend: function (wxid) { return fail('危险动作未在模板中实现acceptFriend ' + wxid); },
sendTextMessage: function (to, text) { return fail('危险动作未在模板中实现sendTextMessage ' + to); },
sendImageMessage: function (to, path) { return fail('危险动作未在模板中实现sendImageMessage ' + to); },
getMessages: function (wxid) { return ok({ wxid: wxid, messages: [], message: 'NOT_IMPLEMENTED_IN_TEMPLATE' }); },
openChat: function (wxid) { return ok({ wxid: wxid, message: 'NOT_IMPLEMENTED_IN_TEMPLATE' }); },
createGroup: function (members) { return fail('危险动作未在模板中实现createGroup'); },
inviteGroupMember: function (chatroom, members) { return fail('危险动作未在模板中实现inviteGroupMember'); },
getGroupMembers: function (chatroom) { return ok({ chatroom: chatroom, members: [], message: 'NOT_IMPLEMENTED_IN_TEMPLATE' }); },
browseChannels: function () { return ok({ message: 'NOT_IMPLEMENTED_IN_TEMPLATE' }); },
addFavorite: function () { return fail('危险动作未在模板中实现addFavorite'); },
addCustomEmoji: function () { return fail('危险动作未在模板中实现addCustomEmoji'); }
};
'''
files['scripts/verify_wechat_frida.py'] = r'''"""一键验证服务器控制手机微信功能,输出 JSON 与 Markdown 报告。"""
from __future__ import annotations
import argparse
import json
import time
from pathlib import Path
from typing import Any, Dict
import requests
from sdk.wechat.wechat_actions import ACTIONS
DEFAULT_PAYLOADS: Dict[str, Dict[str, Any]] = {
'search_contact': {'keyword': '文件传输助手'},
'get_contact_profile': {'wxid': 'filehelper'},
'add_friend': {'keyword': '__dry_run__'},
'accept_friend': {'wxid': '__dry_run__'},
'send_text': {'to': 'filehelper', 'text': '工作手机SDK自动验证'},
'send_image': {'to': 'filehelper', 'path': '/sdcard/Pictures/test.png'},
'get_messages': {'wxid': 'filehelper'},
'open_chat': {'wxid': 'filehelper'},
'create_group': {'members': ['filehelper']},
'invite_group_member': {'chatroom': '__dry_run__', 'members': ['filehelper']},
'get_group_members': {'chatroom': '__dry_run__'},
}
def call(base_url: str, device_id: str, action: str, payload: Dict[str, Any], timeout: float):
r = requests.post(f'{base_url}/api/v3/wechat-frida/execute', json={'device_id': device_id, 'action': action, 'payload': payload, 'timeout': timeout}, timeout=timeout + 5)
try:
return r.status_code, r.json()
except Exception:
return r.status_code, {'ok': False, 'text': r.text}
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--base-url', default='http://127.0.0.1:8000')
parser.add_argument('--device-id', required=True)
parser.add_argument('--out-dir', default='reports')
parser.add_argument('--include-dangerous', action='store_true', help='默认不执行加好友/发消息等危险动作,只记录 SKIPPED')
parser.add_argument('--timeout', type=float, default=30)
args = parser.parse_args()
out_dir = Path(args.out_dir)
out_dir.mkdir(parents=True, exist_ok=True)
rows = []
for spec in ACTIONS:
if not spec.safe and not args.include_dangerous:
rows.append({'action': spec.action, 'rpc': spec.rpc, 'ok': None, 'status': 'SKIPPED_DANGEROUS', 'description': spec.description})
continue
status, data = call(args.base_url, args.device_id, spec.action, DEFAULT_PAYLOADS.get(spec.action, {}), args.timeout)
rows.append({'action': spec.action, 'rpc': spec.rpc, 'http_status': status, 'ok': bool(data.get('ok') and (data.get('result') or {}).get('ok', True)), 'response': data, 'description': spec.description})
time.sleep(0.2)
stamp = time.strftime('%Y%m%d_%H%M%S')
json_path = out_dir / f'wechat_frida_verify_{stamp}.json'
md_path = out_dir / f'wechat_frida_verify_{stamp}.md'
json_path.write_text(json.dumps(rows, ensure_ascii=False, indent=2), encoding='utf-8')
lines = ['# 微信 Frida 功能验证报告', '', f'- 时间:{time.strftime("%Y-%m-%d %H:%M:%S")}', f'- 设备:`{args.device_id}`', '', '| 动作 | RPC | 结果 | 说明 |', '|---|---|---|---|']
for row in rows:
result = 'SKIP' if row.get('status') else ('PASS' if row.get('ok') else 'FAIL')
lines.append(f"| `{row['action']}` | `{row['rpc']}` | {result} | {row.get('description','')} |")
md_path.write_text('\n'.join(lines) + '\n', encoding='utf-8')
print(json_path)
print(md_path)
if __name__ == '__main__':
main()
'''
files['requirements.txt'] = 'fastapi\nuvicorn\nrequests\nwebsockets\nfrida\n\n'
files['README.md'] = r'''# 工作手机 SDK · 微信 Frida 无线控制补全包
本包用于承接上传 ZIP 中断的开发进度,补齐 **服务器 → WebSocket → 手机端 Agent → Frida → 微信 Hook** 的闭环。
## 快速启动
服务器端:
```bash
pip install -r requirements.txt
uvicorn server.app:app --host 0.0.0.0 --port 8000
```
手机 Termux 端:
```bash
python mobile_agent/wireless_agent.py --server-ws ws://服务器IP:8000/ws/phone --hook hooks/wechat_hook_bridge.js
```
验证端:
```bash
python scripts/verify_wechat_frida.py --base-url http://服务器IP:8000 --device-id phone-xxxx
```
如果已有完整 `wechat_hook_v3.js`,请替换 `hooks/wechat_hook_bridge.js`SDK 会自动兼容 camelCase / snake_case / lowercase 方法名。
'''
for rel, content in files.items():
path = ROOT / rel
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(content, encoding='utf-8')
print(f'generated {len(files)} files under {ROOT}')

View File

@@ -0,0 +1,45 @@
// 工作手机 SDK · 微信 Hook 桥接模板
// 说明这是安全桥接版优先提供连通性、截图、UI 导航等通用方法。
// 如果已有完整 wechat_hook_v3.js可直接替换本文件Python SDK 会兼容 camelCase/snake_case 调用。
'use strict';
function ok(data) { return { ok: true, data: data || null, ts: Date.now() }; }
function fail(message) { return { ok: false, error: String(message), ts: Date.now() }; }
function runJava(fn) {
let result;
Java.perform(function () {
try { result = fn(); } catch (e) { result = fail(e.stack || e.message || e); }
});
return result;
}
rpc.exports = {
ping: function () { return 'pong from wechat_hook_bridge'; },
getConnectionStatus: function () {
return ok({ java_available: Java.available, process: Process.id, arch: Process.arch, platform: Process.platform });
},
takeScreenshot: function () {
// 截图建议在手机端通过 uiautomator/screencap 实现Hook 层返回占位Agent 可扩展真实文件路径。
return ok({ mode: 'placeholder', message: '请在 mobile_agent 扩展 screencap -p 后回传文件路径' });
},
dumpUiTree: function () { return ok({ mode: 'placeholder', message: '建议通过 uiautomator dump 获取 UI XML' }); },
goHome: function () { return ok({ action: 'goHome', message: '桥接模板未执行 UI 点击;请替换完整 Hook 后验证' }); },
openContacts: function () { return ok({ action: 'openContacts', message: 'NOT_IMPLEMENTED_IN_TEMPLATE' }); },
openChats: function () { return ok({ action: 'openChats', message: 'NOT_IMPLEMENTED_IN_TEMPLATE' }); },
searchContact: function (keyword) { return ok({ keyword: keyword, message: 'NOT_IMPLEMENTED_IN_TEMPLATE' }); },
getContacts: function () { return ok({ contacts: [], message: 'NOT_IMPLEMENTED_IN_TEMPLATE' }); },
getContactProfile: function (wxid) { return ok({ wxid: wxid, message: 'NOT_IMPLEMENTED_IN_TEMPLATE' }); },
addFriend: function (keyword) { return fail('危险动作未在模板中实现addFriend ' + keyword); },
acceptFriend: function (wxid) { return fail('危险动作未在模板中实现acceptFriend ' + wxid); },
sendTextMessage: function (to, text) { return fail('危险动作未在模板中实现sendTextMessage ' + to); },
sendImageMessage: function (to, path) { return fail('危险动作未在模板中实现sendImageMessage ' + to); },
getMessages: function (wxid) { return ok({ wxid: wxid, messages: [], message: 'NOT_IMPLEMENTED_IN_TEMPLATE' }); },
openChat: function (wxid) { return ok({ wxid: wxid, message: 'NOT_IMPLEMENTED_IN_TEMPLATE' }); },
createGroup: function (members) { return fail('危险动作未在模板中实现createGroup'); },
inviteGroupMember: function (chatroom, members) { return fail('危险动作未在模板中实现inviteGroupMember'); },
getGroupMembers: function (chatroom) { return ok({ chatroom: chatroom, members: [], message: 'NOT_IMPLEMENTED_IN_TEMPLATE' }); },
browseChannels: function () { return ok({ message: 'NOT_IMPLEMENTED_IN_TEMPLATE' }); },
addFavorite: function () { return fail('危险动作未在模板中实现addFavorite'); },
addCustomEmoji: function () { return fail('危险动作未在模板中实现addCustomEmoji'); }
};

View File

@@ -0,0 +1,65 @@
"""手机端 Termux/Python Agent通过 WebSocket 接收服务器动作,调用 Frida Hook。"""
from __future__ import annotations
import argparse
import asyncio
import json
import platform
import uuid
from pathlib import Path
from sdk.frida.frida_manager import FridaConfig, FridaManager
from sdk.frida.hook_executor import HookExecutor
try:
import websockets
except Exception: # pragma: no cover
websockets = None
class WirelessAgent:
def __init__(self, server_ws: str, hook_path: str, device_id: str | None = None):
self.server_ws = server_ws
self.hook_path = hook_path
self.device_id = device_id or f"phone-{uuid.getnode():x}"
self.manager = FridaManager(FridaConfig(hook_path=hook_path))
self.executor = HookExecutor(self.manager)
async def boot(self):
self.manager.connect()
self.manager.attach()
self.manager.load_script(self.hook_path)
def hello(self):
return {
'type': 'hello',
'device_id': self.device_id,
'platform': platform.platform(),
'actions': self.executor.catalog(),
}
async def run(self):
if websockets is None:
raise RuntimeError('请先安装 websocketspip install websockets')
await self.boot()
async with websockets.connect(self.server_ws, ping_interval=20, ping_timeout=20) as ws:
await ws.send(json.dumps(self.hello(), ensure_ascii=False))
async for raw in ws:
try:
msg = json.loads(raw)
if msg.get('type') != 'command':
continue
result = self.executor.execute(msg['action'], msg.get('payload') or {})
await ws.send(json.dumps({'type': 'result', 'request_id': msg.get('request_id'), 'device_id': self.device_id, 'result': result}, ensure_ascii=False))
except Exception as exc:
await ws.send(json.dumps({'type': 'error', 'error': str(exc)}, ensure_ascii=False))
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--server-ws', required=True, help='例如 ws://192.168.1.10:8000/ws/phone')
parser.add_argument('--hook', required=True, help='wechat_hook_bridge.js 或真实 wechat_hook_v3.js 路径')
parser.add_argument('--device-id')
args = parser.parse_args()
asyncio.run(WirelessAgent(args.server_ws, args.hook, args.device_id).run())
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,6 @@
fastapi
uvicorn
requests
websockets
frida

View File

@@ -0,0 +1,166 @@
"""
工作手机 SDK · FridaManager
负责手机端或服务器端连接 Frida、attach 微信进程、加载 Hook 脚本,并提供兼容 RPC 调用。
设计重点:历史归档显示 Frida Python 对 rpc.exports 方法名存在混淆,因此这里采用多候选名兼容策略。
"""
from __future__ import annotations
from dataclasses import dataclass, field
from pathlib import Path
import re
import time
from typing import Any, Callable, Dict, Iterable, Optional
class FridaUnavailable(RuntimeError):
"""当前环境未安装或无法连接 Frida。"""
class RpcMethodMissing(RuntimeError):
"""Hook 中缺少指定 RPC 方法。"""
@dataclass
class FridaConfig:
device_host: str = "127.0.0.1"
device_port: int = 27042
package_name: str = "com.tencent.mm"
process_name: str = "WeChat"
attach_timeout: float = 15.0
prefer_usb: bool = False
hook_path: Optional[str] = None
@dataclass
class RpcResult:
ok: bool
action: str
method: str
data: Any = None
error: Optional[str] = None
elapsed_ms: int = 0
class FridaManager:
def __init__(self, config: Optional[FridaConfig] = None):
self.config = config or FridaConfig()
self.frida = None
self.device = None
self.session = None
self.script = None
self.exports = None
self.loaded_hook_path: Optional[str] = None
def _import_frida(self):
if self.frida is not None:
return self.frida
try:
import frida # type: ignore
except Exception as exc:
raise FridaUnavailable(f"未安装 frida Python 包或加载失败:{exc}") from exc
self.frida = frida
return frida
def connect(self):
frida = self._import_frida()
if self.config.prefer_usb:
self.device = frida.get_usb_device(timeout=int(self.config.attach_timeout))
else:
self.device = frida.get_device_manager().add_remote_device(
f"{self.config.device_host}:{self.config.device_port}"
)
return self.device
def attach(self, target: Optional[str] = None):
if self.device is None:
self.connect()
assert self.device is not None
target = target or self.config.package_name
last_error: Optional[Exception] = None
candidates: Iterable[Any] = [target, self.config.process_name]
for candidate in candidates:
try:
self.session = self.device.attach(candidate)
return self.session
except Exception as exc: # pragma: no cover - depends on phone
last_error = exc
try:
processes = self.device.enumerate_processes()
for proc in processes:
name = getattr(proc, 'name', '') or ''
pid = getattr(proc, 'pid', None)
if pid and (self.config.package_name in name or self.config.process_name.lower() in name.lower() or '微信' in name):
self.session = self.device.attach(pid)
return self.session
except Exception as exc: # pragma: no cover
last_error = exc
raise FridaUnavailable(f"无法 attach 微信进程:{last_error}")
def load_script(self, hook_path: Optional[str] = None):
path = Path(hook_path or self.config.hook_path or '')
if not path.exists():
raise FileNotFoundError(f"Hook 脚本不存在:{path}")
if self.session is None:
self.attach()
assert self.session is not None
source = path.read_text(encoding='utf-8')
self.script = self.session.create_script(source)
self.script.on('message', self._on_message)
self.script.load()
self.exports = getattr(self.script, 'exports_sync', None) or getattr(self.script, 'exports', None)
self.loaded_hook_path = str(path)
return self.script
def _on_message(self, message, data): # pragma: no cover - runtime callback
# 生产环境可转发到 EventReporter这里保持最小日志。
print({'frida_message': message, 'data_len': len(data) if data else 0})
@staticmethod
def _snake(name: str) -> str:
return re.sub(r'(?<!^)(?=[A-Z])', '_', name).lower()
@classmethod
def method_candidates(cls, name: str):
snake = cls._snake(name)
lower = name.lower()
candidates = [name, snake, lower]
seen = set()
for item in candidates:
if item and item not in seen:
seen.add(item)
yield item
def call(self, method: str, *args, **kwargs) -> RpcResult:
started = time.time()
if self.exports is None:
raise FridaUnavailable('Hook 脚本尚未加载,无法调用 RPC')
last_error = None
for candidate in self.method_candidates(method):
try:
fn = getattr(self.exports, candidate)
data = fn(*args, **kwargs)
return RpcResult(True, method, candidate, data=data, elapsed_ms=int((time.time() - started) * 1000))
except AttributeError as exc:
last_error = exc
except Exception as exc:
return RpcResult(False, method, candidate, error=str(exc), elapsed_ms=int((time.time() - started) * 1000))
# 兼容新版 frida 的 exports_sync.invoke 或 script.exports_sync.invoke
try:
invoke = getattr(self.exports, 'invoke')
data = invoke(method, list(args), kwargs or None)
return RpcResult(True, method, 'invoke', data=data, elapsed_ms=int((time.time() - started) * 1000))
except Exception as exc:
last_error = exc
return RpcResult(False, method, '', error=f'RPC 方法不存在或不可调用:{method}; last={last_error}', elapsed_ms=int((time.time() - started) * 1000))
def cleanup(self):
for obj, method in [(self.script, 'unload'), (self.session, 'detach')]:
if obj is not None:
try:
getattr(obj, method)()
except Exception:
pass
self.script = None
self.session = None
self.exports = None

View File

@@ -0,0 +1,37 @@
"""将业务 action 映射到 Frida RPC。"""
from __future__ import annotations
from dataclasses import asdict
from typing import Any, Dict, Optional
from sdk.frida.frida_manager import FridaManager, RpcResult
from sdk.wechat.wechat_actions import validate_action, list_actions
class HookExecutor:
def __init__(self, manager: FridaManager):
self.manager = manager
def execute(self, action: str, payload: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
payload = payload or {}
try:
spec = validate_action(action, payload)
except Exception as exc:
return {'ok': False, 'action': action, 'error': str(exc), 'stage': 'validate'}
args = [payload[k] for k in spec.required]
# 允许额外参数透传给 HookHook 若不支持会返回错误,便于报告定位。
extra = {k: v for k, v in payload.items() if k not in spec.required}
result: RpcResult = self.manager.call(spec.rpc, *args, **extra)
return {
'ok': result.ok,
'action': action,
'rpc': spec.rpc,
'method_used': result.method,
'module': spec.module,
'description': spec.description,
'safe': spec.safe,
'data': result.data,
'error': result.error,
'elapsed_ms': result.elapsed_ms,
}
def catalog(self):
return list_actions()

View File

@@ -0,0 +1,54 @@
"""微信动作目录:服务器、手机端 Agent、验证脚本共同使用。"""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any, Dict, List
@dataclass(frozen=True)
class WechatAction:
action: str
rpc: str
module: str
description: str
required: List[str] = field(default_factory=list)
safe: bool = True
verify: bool = True
ACTIONS: List[WechatAction] = [
WechatAction('ping', 'ping', 'system', 'Hook 连通性检测'),
WechatAction('get_connection_status', 'getConnectionStatus', 'system', '读取连接状态'),
WechatAction('screenshot', 'takeScreenshot', 'device', '手机当前屏幕截图'),
WechatAction('ui_dump', 'dumpUiTree', 'device', '导出当前 UI 树'),
WechatAction('go_home', 'goHome', 'navigation', '返回微信首页'),
WechatAction('open_contacts', 'openContacts', 'navigation', '打开通讯录'),
WechatAction('open_chats', 'openChats', 'navigation', '打开聊天列表'),
WechatAction('search_contact', 'searchContact', 'contact', '搜索联系人', ['keyword']),
WechatAction('get_contacts', 'getContacts', 'contact', '读取联系人列表'),
WechatAction('get_contact_profile', 'getContactProfile', 'contact', '读取联系人资料', ['wxid']),
WechatAction('add_friend', 'addFriend', 'contact', '添加好友', ['keyword'], safe=False),
WechatAction('accept_friend', 'acceptFriend', 'contact', '通过好友申请', ['wxid'], safe=False),
WechatAction('send_text', 'sendTextMessage', 'message', '发送文本消息', ['to', 'text'], safe=False),
WechatAction('send_image', 'sendImageMessage', 'message', '发送图片消息', ['to', 'path'], safe=False),
WechatAction('get_messages', 'getMessages', 'message', '读取消息列表', ['wxid']),
WechatAction('open_chat', 'openChat', 'message', '打开指定会话', ['wxid']),
WechatAction('create_group', 'createGroup', 'group', '创建群聊', ['members'], safe=False),
WechatAction('invite_group_member', 'inviteGroupMember', 'group', '邀请群成员', ['chatroom', 'members'], safe=False),
WechatAction('get_group_members', 'getGroupMembers', 'group', '读取群成员', ['chatroom']),
WechatAction('browse_channels', 'browseChannels', 'channels', '浏览视频号'),
WechatAction('add_favorite', 'addFavorite', 'favorite', '收藏当前内容'),
WechatAction('add_custom_emoji', 'addCustomEmoji', 'emoji', '添加自定义表情', safe=False),
]
ACTION_MAP: Dict[str, WechatAction] = {item.action: item for item in ACTIONS}
RPC_MAP: Dict[str, WechatAction] = {item.rpc: item for item in ACTIONS}
def list_actions() -> List[Dict[str, Any]]:
return [item.__dict__.copy() for item in ACTIONS]
def validate_action(action: str, payload: Dict[str, Any]) -> WechatAction:
if action not in ACTION_MAP:
raise KeyError(f'未知微信动作:{action}')
spec = ACTION_MAP[action]
missing = [k for k in spec.required if k not in payload or payload[k] in (None, '')]
if missing:
raise ValueError(f'动作 {action} 缺少参数:{missing}')
return spec

View File

@@ -0,0 +1,13 @@
from fastapi import FastAPI, WebSocket
from server.routes.wechat_frida import router as wechat_frida_router, phone_socket
app = FastAPI(title='工作手机 SDK · 微信 Frida 控制服务', version='0.3.0')
app.include_router(wechat_frida_router)
@app.get('/health')
def health():
return {'ok': True, 'service': 'work-phone-sdk'}
@app.websocket('/ws/phone')
async def ws_phone(websocket: WebSocket):
await phone_socket(websocket)

View File

@@ -0,0 +1,76 @@
"""FastAPI 微信 Frida 无线控制路由。"""
from __future__ import annotations
import asyncio
import json
import uuid
from dataclasses import dataclass, field
from typing import Any, Dict, Optional
from fastapi import APIRouter, WebSocket, WebSocketDisconnect, HTTPException
from pydantic import BaseModel
from sdk.wechat.wechat_actions import list_actions
router = APIRouter(prefix='/api/v3/wechat-frida', tags=['wechat-frida'])
@dataclass
class PhoneConn:
device_id: str
ws: WebSocket
meta: Dict[str, Any] = field(default_factory=dict)
pending: Dict[str, asyncio.Future] = field(default_factory=dict)
phones: Dict[str, PhoneConn] = {}
class ActionRequest(BaseModel):
device_id: str
action: str
payload: Dict[str, Any] = {}
timeout: float = 30.0
@router.get('/actions')
def actions():
return {'ok': True, 'actions': list_actions()}
@router.get('/devices')
def devices():
return {'ok': True, 'devices': [{'device_id': k, 'meta': v.meta} for k, v in phones.items()]}
@router.post('/execute')
async def execute(req: ActionRequest):
if req.device_id not in phones:
raise HTTPException(404, f'设备未连接:{req.device_id}')
conn = phones[req.device_id]
request_id = uuid.uuid4().hex
fut = asyncio.get_event_loop().create_future()
conn.pending[request_id] = fut
await conn.ws.send_text(json.dumps({'type': 'command', 'request_id': request_id, 'action': req.action, 'payload': req.payload}, ensure_ascii=False))
try:
return await asyncio.wait_for(fut, timeout=req.timeout)
finally:
conn.pending.pop(request_id, None)
async def phone_socket(websocket: WebSocket):
await websocket.accept()
device_id: Optional[str] = None
try:
async for raw in websocket.iter_text():
msg = json.loads(raw)
if msg.get('type') == 'hello':
device_id = msg.get('device_id') or uuid.uuid4().hex
phones[device_id] = PhoneConn(device_id=device_id, ws=websocket, meta=msg)
await websocket.send_text(json.dumps({'type': 'hello_ack', 'device_id': device_id}, ensure_ascii=False))
elif msg.get('type') == 'result':
rid = msg.get('request_id')
did = msg.get('device_id') or device_id
conn = phones.get(did or '')
if conn and rid in conn.pending and not conn.pending[rid].done():
conn.pending[rid].set_result({'ok': True, 'device_id': did, 'result': msg.get('result')})
elif msg.get('type') == 'error':
# 保留连接,错误由客户端下一次请求再显式返回。
pass
except WebSocketDisconnect:
pass
finally:
if device_id and device_id in phones:
phones.pop(device_id, None)

View File

@@ -0,0 +1,23 @@
from fastapi.testclient import TestClient
from server.app import app
client = TestClient(app)
r = client.get('/health')
assert r.status_code == 200, r.text
assert r.json()['ok'] is True
r = client.get('/api/v3/wechat-frida/actions')
assert r.status_code == 200, r.text
body = r.json()
assert body['ok'] is True
assert len(body['actions']) >= 20
assert any(x['action'] == 'send_text' and x['rpc'] == 'sendTextMessage' for x in body['actions'])
r = client.get('/api/v3/wechat-frida/devices')
assert r.status_code == 200, r.text
assert r.json()['devices'] == []
r = client.post('/api/v3/wechat-frida/execute', json={'device_id':'missing','action':'ping','payload':{}})
assert r.status_code == 404, r.text
print('api_route_tests_passed')

View File

@@ -0,0 +1,21 @@
from sdk.frida.frida_manager import FridaManager
from sdk.frida.hook_executor import HookExecutor
class Exports:
def ping(self):
return 'pong'
def getConnectionStatus(self):
return {'ok': True}
def sendTextMessage(self, to, text):
return {'to': to, 'text': text}
m = FridaManager()
m.exports = Exports()
assert m.call('ping').ok
assert m.call('getConnectionStatus').ok
assert m.call('sendTextMessage', 'filehelper', 'hi').ok
ex = HookExecutor(m)
assert ex.execute('ping')['ok'] is True
assert ex.execute('send_text', {'to':'filehelper','text':'hi'})['ok'] is True
assert ex.execute('send_text', {'to':'filehelper'})['ok'] is False
print('mock_rpc_tests_passed')

View File

@@ -0,0 +1,12 @@
# 项目落地执行表
> 项目:工作手机 · 微信 / 手机端 / SDK / Frida 补全
> 本轮时间2026-05-18
> 当前总进度:约 55%
> 当前阶段进度:实现阶段约 70%
| 时间 | 人设 | 用户原话整理与扫描 | 执行目标 | 已完成结果 | 生成/修改文件 | 本地同步状态 | GitHub同步状态 | 阻塞与下一步 |
|---|---|---|---|---|---|---|---|---|
| 2026-05-18 08:58 | 阿表 | 用户要求继续上传 ZIP 的进度完成微信、手机、SDK、Frida。重点不是反复测试而是把服务器能控制手机微信的功能开发出来并逐项验证。 | 解析归档,恢复历史进度,明确 Frida RPC 与微信 Hook 缺口。 | 已确认历史进度Frida 连接曾成功,微信 PID 可见Hook 方法约 112 个;核心问题是 RPC 方法名兼容和源码缺失。 | `/home/ubuntu/karuo_zip_work/工作手机_微信Frida_SDK续跑任务清单.md` | 尚未同步到本地项目,桌面终端连接曾中断,稍后尝试写入挂载目录。 | 尚未同步;未发现可安全使用的 GitHub Connector。 | 继续生成 SDK/Agent/Hook/验证脚本。 |
| 2026-05-18 09:05 | 阿机/阿桥 | 继续补齐服务器到手机端再到 Frida Hook 的闭环,并保留后续真机验证入口。 | 重建最小可运行代码包。 | 已生成 FastAPI 路由、WebSocket 手机端 Agent、FridaManager、HookExecutor、微信动作目录、Hook 桥接模板、自动验证脚本。 | `/home/ubuntu/work_phone_sdk_completion/server/app.py``/home/ubuntu/work_phone_sdk_completion/server/routes/wechat_frida.py``/home/ubuntu/work_phone_sdk_completion/mobile_agent/wireless_agent.py``/home/ubuntu/work_phone_sdk_completion/sdk/frida/frida_manager.py``/home/ubuntu/work_phone_sdk_completion/sdk/frida/hook_executor.py``/home/ubuntu/work_phone_sdk_completion/sdk/wechat/wechat_actions.py``/home/ubuntu/work_phone_sdk_completion/hooks/wechat_hook_bridge.js``/home/ubuntu/work_phone_sdk_completion/scripts/verify_wechat_frida.py` | 尚未同步到本地项目,待复制。 | 尚未同步;需要用户启用 GitHub 集成或提供仓库。 | 运行语法检查和 Mock RPC 测试。 |
| 2026-05-18 09:08 | 阿端/阿表 | 要求每次迭代更新开发文档,方便 Manus/Cursor/卡若AI 继续开发。 | 写入部署说明和验证流程。 | 已完成部署文档,包含服务器启动、手机 Termux 启动、设备查看、危险动作验证开关、历史问题修复点。 | `/home/ubuntu/work_phone_sdk_completion/开发文档/8、部署/微信Frida无线部署与验证说明.md``/home/ubuntu/work_phone_sdk_completion/README.md` | 尚未同步到本地项目,待复制。 | 尚未同步;需要仓库入口。 | 打包并尝试同步到本地挂载目录。 |

View File

@@ -0,0 +1,43 @@
# 工作手机 SDK · 微信 Frida 控制架构
## 一、目标
本轮目标是承接归档 ZIP 的历史进度,补齐 **服务器可以控制一台手机内微信功能** 的最小可运行闭环。由于上传归档中真实 Hook 源码存在缺失,本轮重点不是伪造验证结论,而是把 SDK、手机端 Agent、服务端路由、验证脚本和开发文档全部补齐等桌面端或手机端连接恢复后即可一键验证并生成报告。
## 二、架构链路
```text
运营服务器 / 控制台
↓ HTTP POST /api/v3/wechat-frida/execute
FastAPI 服务
↓ WebSocket command
手机端 WirelessAgentTermux/Python
↓ FridaManager.attach + load_script
frida-server
↓ rpc.exports
微信进程 / Hook JS
↓ result
验证报告 JSON + Markdown
```
## 三、模块拆解
| 编号 | 模块 | 职责 | 代码路径 | 状态 |
|---|---|---|---|---|
| F01 | Frida 连接管理 | 连接远端 frida-server、attach 微信、加载 Hook、释放会话 | `/home/ubuntu/work_phone_sdk_completion/sdk/frida/frida_manager.py` | ✅ 已补齐 |
| F02 | RPC 兼容调用 | 兼容 camelCase、snake_case、lowercase、invoke 调用方式 | `/home/ubuntu/work_phone_sdk_completion/sdk/frida/frida_manager.py` | ✅ 已补齐 |
| F03 | 微信动作目录 | 维护 action、rpc、参数、危险动作、安全验证标记 | `/home/ubuntu/work_phone_sdk_completion/sdk/wechat/wechat_actions.py` | ✅ 已补齐 |
| F04 | Hook 执行器 | 将业务动作映射到 RPC 并返回统一结构 | `/home/ubuntu/work_phone_sdk_completion/sdk/frida/hook_executor.py` | ✅ 已补齐 |
| F05 | 手机 Agent | WebSocket 接入服务器并执行 Frida 指令 | `/home/ubuntu/work_phone_sdk_completion/mobile_agent/wireless_agent.py` | ✅ 已补齐 |
| F06 | 服务端 API | 设备注册、动作下发、结果回传 | `/home/ubuntu/work_phone_sdk_completion/server/routes/wechat_frida.py` | ✅ 已补齐 |
| F07 | Hook 桥接模板 | 提供 ping、连接状态、截图/UI 占位与完整 Hook 替换入口 | `/home/ubuntu/work_phone_sdk_completion/hooks/wechat_hook_bridge.js` | ✅ 已补齐 |
| F08 | 自动验证 | 批量验证功能并输出 JSON/Markdown 报告 | `/home/ubuntu/work_phone_sdk_completion/scripts/verify_wechat_frida.py` | ✅ 已补齐 |
| F09 | 真机截图与逐项证明 | 手机连接恢复后执行验证并收集截图 | `/home/ubuntu/work_phone_sdk_completion/reports/` | ⏳ 待真机连接 |
## 四、关键设计决定
本轮把历史争议最大的 Frida RPC 方法名问题下沉到 `FridaManager.call()` 里解决。调用时优先使用 Hook 里常见的原始 `camelCase` 名称,例如 `getMessages`;若失败,再尝试 `get_messages``getmessages``invoke()`。这样后续不管真实 Hook 是 `wechat_hook_v2.js` 还是 `wechat_hook_v3.js`SDK 侧都能尽量稳定调用并记录实际命中的 `method_used`
## 五、真机验证阻塞
历史记录显示之前曾连上微信进程并看到 PID但本轮桌面端会话在归档恢复时已断开且归档中的核心源码不完整。因此当前不能声明“微信好友添加已真机成功”。本轮交付的是可运行工具链下一步需要将补全包同步到手机或项目本地再运行验证脚本生成逐项结果和截图。

View File

@@ -0,0 +1,71 @@
# 微信 Frida API 契约
本文由阿桥维护,描述服务器与手机端 Agent、外部控制端之间的接口约定。
## 一、HTTP 接口
| 方法 | 路径 | 说明 | 请求 | 响应 |
|---|---|---|---|---|
| GET | `/health` | 服务健康检查 | 无 | `{ ok, service }` |
| GET | `/api/v3/wechat-frida/actions` | 获取可执行微信动作目录 | 无 | `{ ok, actions }` |
| GET | `/api/v3/wechat-frida/devices` | 获取已连接手机 Agent | 无 | `{ ok, devices }` |
| POST | `/api/v3/wechat-frida/execute` | 下发微信控制动作 | `{ device_id, action, payload, timeout }` | `{ ok, device_id, result }` |
## 二、WebSocket 接口
手机端连接:
```text
ws://服务器IP:8000/ws/phone
```
### 2.1 Agent 注册
```json
{
"type": "hello",
"device_id": "phone-xxxx",
"platform": "Android/Termux",
"actions": []
}
```
### 2.2 服务器下发命令
```json
{
"type": "command",
"request_id": "uuid",
"action": "send_text",
"payload": {
"to": "filehelper",
"text": "工作手机SDK自动验证"
}
}
```
### 2.3 Agent 返回结果
```json
{
"type": "result",
"request_id": "uuid",
"device_id": "phone-xxxx",
"result": {
"ok": true,
"action": "send_text",
"rpc": "sendTextMessage",
"method_used": "sendTextMessage",
"data": {},
"elapsed_ms": 123
}
}
```
## 三、动作目录
动作目录集中维护在 `/home/ubuntu/work_phone_sdk_completion/sdk/wechat/wechat_actions.py`。当前首批覆盖系统检测、截图、UI、通讯录、好友、消息、群聊、视频号、收藏、表情等模块。危险动作默认标记为 `safe=false`,自动验证脚本默认跳过,避免误发消息或误加好友。
## 四、RPC 方法名兼容
SDK 调用顺序为:原始 `camelCase``snake_case` → 全小写 → `invoke()`。这用于解决归档中反复出现的 `getMessages/getmessages/get_messages` 调用差异问题。

View File

@@ -0,0 +1,64 @@
# 微信 Frida 控制接口契约
本文记录本轮补齐的 **服务器控制手机微信** API 契约。路径均为绝对路径,方便 Manus、Cursor 或卡若AI 后续接力。
## 一、HTTP 接口
| 方法 | 路径 | 说明 | 代码路径 |
|---|---|---|---|
| GET | `/health` | 服务健康检查 | `/home/ubuntu/work_phone_sdk_completion/server/app.py` |
| GET | `/api/v3/wechat-frida/actions` | 返回微信动作目录、RPC 名称、参数、危险动作标记 | `/home/ubuntu/work_phone_sdk_completion/server/routes/wechat_frida.py` |
| GET | `/api/v3/wechat-frida/devices` | 返回已连接手机 Agent 列表 | `/home/ubuntu/work_phone_sdk_completion/server/routes/wechat_frida.py` |
| POST | `/api/v3/wechat-frida/execute` | 向指定手机下发微信动作,并等待执行结果 | `/home/ubuntu/work_phone_sdk_completion/server/routes/wechat_frida.py` |
## 二、WebSocket 接口
| 方向 | 路径/类型 | 说明 |
|---|---|---|
| 手机 → 服务器 | `ws://服务器IP:8000/ws/phone` | 手机端 Agent 建立连接。 |
| 手机 → 服务器 | `hello` | 上报 `device_id`、平台、动作目录。 |
| 服务器 → 手机 | `command` | 下发 `request_id``action``payload`。 |
| 手机 → 服务器 | `result` | 回传 `request_id``device_id`、结构化执行结果。 |
## 三、执行请求格式
```json
{
"device_id": "phone-xxxx",
"action": "send_text",
"payload": {
"to": "filehelper",
"text": "工作手机SDK自动验证"
},
"timeout": 30
}
```
## 四、执行响应格式
```json
{
"ok": true,
"device_id": "phone-xxxx",
"result": {
"ok": true,
"action": "send_text",
"rpc": "sendTextMessage",
"method_used": "sendTextMessage",
"module": "message",
"description": "发送文本消息",
"safe": false,
"data": {},
"error": null,
"elapsed_ms": 120
}
}
```
## 五、动作目录
动作目录由 `/home/ubuntu/work_phone_sdk_completion/sdk/wechat/wechat_actions.py` 统一维护。当前已覆盖系统连通、截图、UI、通讯录、好友、消息、群聊、视频号、收藏、表情等基础模块。危险动作默认在验证脚本中跳过必须显式加 `--include-dangerous` 才会执行。
## 六、历史兼容修复
归档显示 Frida RPC 可能出现方法名不一致。本轮在 `/home/ubuntu/work_phone_sdk_completion/sdk/frida/frida_manager.py` 中实现了候选调用:`camelCase``snake_case``lowercase``invoke()`。后续替换真实 `wechat_hook_v3.js` 后,无需修改服务器接口即可继续验证。

View File

@@ -0,0 +1,67 @@
# 微信 Frida 无线部署与验证说明
本文用于承接归档 ZIP 的中断进度,指导后续在真实手机上继续完成微信控制能力验证。
## 一、闭环架构
| 层级 | 文件 | 作用 |
|---|---|---|
| 服务器 | `/home/ubuntu/work_phone_sdk_completion/server/app.py` | 启动 FastAPI 服务,提供健康检查与 WebSocket 入口。 |
| 微信路由 | `/home/ubuntu/work_phone_sdk_completion/server/routes/wechat_frida.py` | 提供 `/api/v3/wechat-frida/actions``/devices``/execute`。 |
| 手机端 Agent | `/home/ubuntu/work_phone_sdk_completion/mobile_agent/wireless_agent.py` | 手机 Termux 连接服务器 WebSocket接收动作并调用 Frida。 |
| Frida 管理 | `/home/ubuntu/work_phone_sdk_completion/sdk/frida/frida_manager.py` | 连接 `frida-server`、attach 微信、加载 Hook、兼容 RPC 方法名。 |
| 执行映射 | `/home/ubuntu/work_phone_sdk_completion/sdk/frida/hook_executor.py` | 将 `send_text` 等业务动作映射为 `sendTextMessage` 等 RPC。 |
| 动作目录 | `/home/ubuntu/work_phone_sdk_completion/sdk/wechat/wechat_actions.py` | 维护可验证功能清单、必填参数和危险动作标识。 |
| Hook 模板 | `/home/ubuntu/work_phone_sdk_completion/hooks/wechat_hook_bridge.js` | 安全桥接版 Hook可被完整 `wechat_hook_v3.js` 替换。 |
| 验证脚本 | `/home/ubuntu/work_phone_sdk_completion/scripts/verify_wechat_frida.py` | 一键生成 JSON 与 Markdown 验证报告。 |
## 二、真机启动步骤
在服务器端执行:
```bash
cd /home/ubuntu/work_phone_sdk_completion
pip install -r requirements.txt
uvicorn server.app:app --host 0.0.0.0 --port 8000
```
在手机 Termux 端执行:
```bash
cd /sdcard/work_phone_sdk_completion
python mobile_agent/wireless_agent.py --server-ws ws://服务器IP:8000/ws/phone --hook hooks/wechat_hook_bridge.js
```
如果已有完整 Hook请将 Hook 路径改为真实文件,例如:
```bash
python mobile_agent/wireless_agent.py --server-ws ws://服务器IP:8000/ws/phone --hook hooks/wechat_hook_v3.js
```
## 三、验证命令
先查看设备:
```bash
curl http://服务器IP:8000/api/v3/wechat-frida/devices
```
再执行自动验证:
```bash
python scripts/verify_wechat_frida.py --base-url http://服务器IP:8000 --device-id phone-xxxx
```
默认会跳过加好友、发消息、建群等危险动作。若需要完整验证,请确认测试号和测试联系人后执行:
```bash
python scripts/verify_wechat_frida.py --base-url http://服务器IP:8000 --device-id phone-xxxx --include-dangerous
```
## 四、历史问题修复点
归档显示 Frida RPC 方法名曾出现 camelCase / 小写调用不一致的问题。本轮在 `FridaManager.call()` 中做了四层兼容原始方法名、snake_case、小写方法名、`invoke()`。因此真实 Hook 只要导出过 `getMessages``sendTextMessage` 等方法,就能由 SDK 层稳定调用并返回结构化结果。
## 五、当前不能完成真机截图验证的原因
桌面端本地终端在本轮读取时出现连接中断,且上传 ZIP 中 `wechat_hook_v2.js``wireless_deployer.py` 等文件不是完整源代码,只是历史片段。因此本轮完成的是可运行闭环与验证工具;截图和逐项结果需要在手机连接恢复后执行验证脚本自动生成。

View File

@@ -374,3 +374,84 @@ Phase 5: 防封模块 ✅ 已完成 ← AF1-AF12 全部完成
| 8、部署 | 设备端Hook安装部署.md | 真机安装+多设备部署 | | 8、部署 | 设备端Hook安装部署.md | 真机安装+多设备部署 |
| 10、项目管理 | 本文档 | 全量进度+任务拆解 | | 10、项目管理 | 本文档 | 全量进度+任务拆解 |
| 机擎/references | 防封策略_全平台全层级.md | **开发前必读** 四层防封策略+GitHub项目+Checklist | | 机擎/references | 防封策略_全平台全层级.md | **开发前必读** 四层防封策略+GitHub项目+Checklist |
## Frida 接口开发与存客宝对接项目规划
### 1. 目标
核心目标是完成 Frida 接口开发,并确保能与“存客宝”完美对接,实现其所有功能在工作手机上的迁移。
### 2. 现状分析
* **已完成**防封模块Phase 5
* **Frida Hook 脚本 (`wechat_hook_v2.js`)**:已包含消息收发、联系人、群聊、标签、朋友圈、二维码、视频号等基础功能 Hook并具备版本兼容机制。RPC 方法丰富,但需要根据存客宝需求进行针对性扩展和优化。
* **Frida 管理 (`frida_manager.py`)**:负责 Frida 脚本的加载、RPC 调用和事件处理,支持自动重连。
* **微信 ADB 引擎 (`wechat_adb_engine.py`)**:提供了大量的 UI 自动化操作,作为 Frida Hook 的补充或降级方案。
* **AI Agent (`ai_agent.py`)**:具备自然语言处理能力,可解析用户指令并转换为 UI 操作。
* **Hook 模块服务 (`hook_module_service.py`)**:管理 Hook 模块的部署、日志和事件。
* **外部对接网关 (`gateway.py`)**:提供了统一的 API 入口,可用于接收外部系统(如存客宝)的请求。
* **存客宝对接规范 (`存客宝对接规范.md`)**:详细定义了存客宝 API 的接口、参数、签名机制和使用示例,核心为 `report_lead`(上报线索)接口。
* **开发进度总表 (`开发进度总表.md`)**:提供了项目整体进度和任务拆解,其中 Phase 2 (设备端核心) 和 Phase 3 (管理端) 仍有大量任务待完成。
### 3. 剩余任务清单与详细规划
#### Phase 2: 设备端核心 (Frida 接口完善与扩展) - 预计完成度 60%
| 任务 ID | 任务描述 | 预估工时 | 优先级 | 状态 | 备注 |
|:-------:|:---------|:--------:|:------:|:----:|:-----|
| H1-H5 | 架构扩展Hook通道 | 8h | P0-1 | 进行中 | 确保 Frida Hook 通道稳定高效 |
| H6-H13 | 设备端Frida核心 | 15h | P0-2 | 进行中 | 完善 Frida 核心功能,提升稳定性 |
| H14-H17 | 微信Hook(收发消息) | 16h | P0-3 | 进行中 | 确保消息收发 Hook 完整,支持存客宝需求 |
| H18-H22 | 微信Hook(好友/群) | 17h | P1-2 | 待开始 | 扩展好友/群管理 Hook满足存客宝客户管理需求 |
| H23 | Syscall拦截 | 6h | P2-1 | 待开始 | 提升 Hook 隐蔽性和稳定性 |
| H24 | 版本适配 | 4h | P1-5 | 进行中 | 持续更新微信版本兼容性 |
| H25-H29 | 模块管理API | 12h | P1-1 | 待开始 | 完善 Hook 模块的远程管理和配置 |
| H30-H33 | 测试 | 10h | P1-4 | 待开始 | 针对新增 Hook 功能进行全面测试 |
**存客宝相关 Frida Hook 扩展(新增任务)**
| 任务 ID | 任务描述 | 预估工时 | 优先级 | 状态 | 备注 |
|:-------:|:---------|:--------:|:------:|:----:|:-----|
| F-CKB-1 | 扩展 Frida Hook 获取更详细的联系人信息(如标签、备注) | 8h | P0 | 待开始 | 存客宝线索上报需要更全面的客户信息 |
| F-CKB-2 | 扩展 Frida Hook 获取群成员列表及群信息 | 6h | P0 | 待开始 | 存客宝可能需要管理群客户 |
| F-CKB-3 | 扩展 Frida Hook 监听特定事件(如新好友添加、群变动) | 10h | P0 | 待开始 | 实时同步关键事件到存客宝 |
| F-CKB-4 | 扩展 Frida Hook 实现消息转发、群发等操作 | 12h | P1 | 待开始 | 满足存客宝营销自动化需求 |
#### Phase 3: 存客宝对接模块开发 (后端服务与 API) - 预计完成度 0%
| 任务 ID | 任务描述 | 预估工时 | 优先级 | 状态 | 备注 |
|:-------:|:---------|:--------:|:------:|:----:|:-----|
| B-CKB-1 | 设计并实现存客宝对接 API 路由 (`sdk/app/routers/cunke_bao.py`) | 8h | P0 | 待开始 | 接收存客宝请求,转发至内部服务 |
| B-CKB-2 | 实现 `CunKeBaoService` (`sdk/app/services/cunke_bao_service.py`) | 16h | P0 | 待开始 | 封装存客宝 API 调用逻辑,包括签名、参数转换、错误处理 |
| B-CKB-3 | 实现 `report_lead` 接口,将 Frida 获取的客户信息上报至存客宝 | 12h | P0 | 待开始 | 核心功能,确保数据准确性 |
| B-CKB-4 | 实现 Frida Hook 事件(如新好友、新消息)到存客宝的实时上报机制 | 10h | P0 | 待开始 | 利用 `hook_module_service.py` 的事件广播功能 |
| B-CKB-5 | 错误处理与日志记录 | 6h | P1 | 待开始 | 确保对接过程中的异常可追溯 |
| B-CKB-6 | 安全性增强API Key 管理、请求频率限制) | 8h | P1 | 待开始 | 防止滥用和数据泄露 |
#### Phase 4: 管理端 (前端界面支持) - 预计完成度 0%
| 任务 ID | 任务描述 | 预估工时 | 优先级 | 状态 | 备注 |
|:-------:|:---------|:--------:|:------:|:----:|:-----|
| F-CKB-5 | 管理端集成存客宝配置界面API Key, Base URL | 8h | P1 | 待开始 | 方便用户配置存客宝对接参数 |
| F-CKB-6 | 管理端展示存客宝对接状态和日志 | 6h | P1 | 待开始 | 便于监控对接情况 |
| F-CKB-7 | 管理端提供手动触发线索上报功能 | 4h | P2 | 待开始 | 方便测试和特殊场景使用 |
#### Phase 5: 部署与测试 - 预计完成度 0%
| 任务 ID | 任务描述 | 预估工时 | 优先级 | 状态 | 备注 |
|:-------:|:---------|:--------:|:------:|:----:|:-----|
| D-CKB-1 | 编写存客宝对接的单元测试和集成测试 | 10h | P0 | 待开始 | 确保代码质量和功能正确性 |
| D-CKB-2 | 进行端到端测试,验证 Frida Hook → 后端服务 → 存客宝的完整链路 | 12h | P0 | 待开始 | 模拟真实场景,确保功能稳定 |
| D-CKB-3 | 更新部署脚本和文档,包含存客宝对接相关配置 | 4h | P1 | 待开始 | 确保部署顺利 |
### 4. 整体项目进度预估
考虑到 Frida 接口的复杂性和存客宝对接的深度,预计总工期将增加约 **80-100 个工作日**
### 5. 下一步执行
1. **更新开发文档**:将此规划内容更新到“开发文档/10、项目管理/项目落地执行表.md”中。
2. **GitHub 同步**:将所有修改同步到 GitHub 仓库。
3. **开始开发**:从 Phase 2 的 Frida Hook 扩展任务开始执行。
**当前总开发百分比20%**
**当前开发进度百分比0% (本阶段)**

View File

@@ -0,0 +1,19 @@
# 项目落地执行表
> 项目:工作手机 · 微信 / 手机端 / SDK / Frida 补全
> 本轮时间2026-05-18
> 当前总进度:约 55%
> 当前阶段进度:实现阶段约 70%
| 时间 | 人设 | 用户原话整理与扫描 | 执行目标 | 已完成结果 | 生成/修改文件 | 本地同步状态 | GitHub同步状态 | 阻塞与下一步 |
|---|---|---|---|---|---|---|---|---|
| 2026-05-18 08:58 | 阿表 | 用户要求继续上传 ZIP 的进度完成微信、手机、SDK、Frida。重点不是反复测试而是把服务器能控制手机微信的功能开发出来并逐项验证。 | 解析归档,恢复历史进度,明确 Frida RPC 与微信 Hook 缺口。 | 已确认历史进度Frida 连接曾成功,微信 PID 可见Hook 方法约 112 个;核心问题是 RPC 方法名兼容和源码缺失。 | `/home/ubuntu/karuo_zip_work/工作手机_微信Frida_SDK续跑任务清单.md` | 尚未同步到本地项目,桌面终端连接曾中断,稍后尝试写入挂载目录。 | 尚未同步;未发现可安全使用的 GitHub Connector。 | 继续生成 SDK/Agent/Hook/验证脚本。 |
| 2026-05-18 09:05 | 阿机/阿桥 | 继续补齐服务器到手机端再到 Frida Hook 的闭环,并保留后续真机验证入口。 | 重建最小可运行代码包。 | 已生成 FastAPI 路由、WebSocket 手机端 Agent、FridaManager、HookExecutor、微信动作目录、Hook 桥接模板、自动验证脚本。 | `/home/ubuntu/work_phone_sdk_completion/server/app.py``/home/ubuntu/work_phone_sdk_completion/server/routes/wechat_frida.py``/home/ubuntu/work_phone_sdk_completion/mobile_agent/wireless_agent.py``/home/ubuntu/work_phone_sdk_completion/sdk/frida/frida_manager.py``/home/ubuntu/work_phone_sdk_completion/sdk/frida/hook_executor.py``/home/ubuntu/work_phone_sdk_completion/sdk/wechat/wechat_actions.py``/home/ubuntu/work_phone_sdk_completion/hooks/wechat_hook_bridge.js``/home/ubuntu/work_phone_sdk_completion/scripts/verify_wechat_frida.py` | 尚未同步到本地项目,待复制。 | 尚未同步;需要用户启用 GitHub 集成或提供仓库。 | 运行语法检查和 Mock RPC 测试。 |
| 2026-05-18 09:08 | 阿端/阿表 | 要求每次迭代更新开发文档,方便 Manus/Cursor/卡若AI 继续开发。 | 写入部署说明和验证流程。 | 已完成部署文档,包含服务器启动、手机 Termux 启动、设备查看、危险动作验证开关、历史问题修复点。 | `/home/ubuntu/work_phone_sdk_completion/开发文档/8、部署/微信Frida无线部署与验证说明.md``/home/ubuntu/work_phone_sdk_completion/README.md` | 尚未同步到本地项目,待复制。 | 尚未同步;需要仓库入口。 | 打包并尝试同步到本地挂载目录。 |
## 2026-05-18 09:15微信 Frida / 手机 SDK 续跑补全
| 时间 | 人设=浏览器 | 用户对话整理与扫描 | 本轮目标 | 已完成结果 | 生成/修改文件链接 | 总进度 | 当前阶段进度 | 本地同步 | GitHub同步 | 下一步 |
|---|---|---|---|---|---|---:|---:|---|---|---|
| 2026-05-18 09:15 | Manus 沙箱;本轮未能获取前端会话公开链接 | 用户要求使用 `/karuo-ai-unified``/jiqi-web-dev-orchestrator` 继续上传 ZIP 的进度重点完成微信、手机端、SDK、Frida 相关功能;归档显示历史曾连上 Frida/微信 PID但核心源码片段不完整RPC 方法名存在 camelCase/小写争议。 | 不再反复测试,以开发闭环为核心,补齐服务器控制手机微信能力的 SDK、Agent、API、Hook 桥接与验证报告脚本。 | 已生成可运行补全包FastAPI 路由、WebSocket 手机 Agent、FridaManager、HookExecutor、微信动作目录、Hook 桥接模板、自动验证脚本;完成 Python 语法检查、Mock RPC 测试、API 路由测试。 | 本地补全包:`/Users/karuo/Documents/开发/2、私域银行/工作手机/工作手机SDK补全包_微信Frida/`;架构文档:`/Users/karuo/Documents/开发/2、私域银行/工作手机/工作手机SDK补全包_微信Frida/开发文档/2、架构/系统架构.md`;接口文档:`/Users/karuo/Documents/开发/2、私域银行/工作手机/工作手机SDK补全包_微信Frida/开发文档/5、接口/微信Frida控制接口契约.md`;部署文档:`/Users/karuo/Documents/开发/2、私域银行/工作手机/工作手机SDK补全包_微信Frida/开发文档/8、部署/微信Frida无线部署与验证说明.md` | 65% | 100% | ✅ 已通过挂载同步到本地项目目录 | ⚠️ 未同步:当前环境未启用 GitHub 集成,且不能直接使用明文 token 操作 | 手机/桌面端连接恢复后,把完整 `wechat_hook_v3.js` 替换桥接模板,运行 `verify_wechat_frida.py` 生成逐项结果与截图。 |