feat: publish workphone SDK deployment and API docs

This commit is contained in:
Manus AI
2026-07-14 18:10:52 +08:00
commit 021d633cc1
534 changed files with 122391 additions and 0 deletions

136
sdk/scripts/_hook_smoke.py Normal file
View File

@@ -0,0 +1,136 @@
#!/usr/bin/env python3
# 工作手机 · Frida 主控通道·实机冒烟脚本(一次性跑完 8 项验证)
# 用法python3 sdk/scripts/_hook_smoke.py <device_serial>
import json, sys, urllib.request, urllib.parse, time
DEVICE = sys.argv[1] if len(sys.argv) > 1 else 'xgfe65eimrrofyws'
BASE = 'http://127.0.0.1:8899'
def get(path, params=None):
url = f'{BASE}{path}'
if params:
url += '?' + urllib.parse.urlencode(params)
with urllib.request.urlopen(url, timeout=60) as r:
return json.loads(r.read())
def post(path, body):
req = urllib.request.Request(
f'{BASE}{path}',
data=json.dumps(body).encode(),
headers={'Content-Type': 'application/json'},
method='POST',
)
with urllib.request.urlopen(req, timeout=120) as r:
return json.loads(r.read())
def section(title):
print('\n' + '=' * 60)
print(f' {title}')
print('=' * 60)
def show(label, value, limit=200):
s = json.dumps(value, ensure_ascii=False)
if len(s) > limit:
s = s[:limit] + ' …(truncated)'
print(f'{label}{s}')
# 1) probe
section('1) /api/v3/hook/probe — Hook 探测')
probe = get(f'/api/v3/hook/probe/{DEVICE}')
show('supports_hook', probe.get('supports_hook'))
show('frida_version', probe.get('frida_version'))
show('wechat_version', probe.get('wechat_version'))
show('profile.nickname', probe.get('profile', {}).get('profile', {}).get('nickname'))
# 2) actions 清单
section('2) /api/v3/hook/actions — 全部动作清单')
acts = get('/api/v3/hook/actions')
show('total_actions', acts.get('total_actions'))
show('total_modules', acts.get('total_modules'))
# 3) 一次性取数
section('3) /api/v3/hook/data — 取微信资料/联系人/群/消息/标签')
data = get(f'/api/v3/hook/data/{DEVICE}',
{'modules': 'profile,contacts,groups,messages,labels'})
core = data.get('data', {})
def _list(node, key1, key2=None):
inner = node.get('data', node)
if key2 and isinstance(inner, dict) and key2 in inner:
return inner[key2]
if isinstance(inner, dict) and key1 in inner:
return inner[key1]
if isinstance(inner, dict) and 'data' in inner and isinstance(inner['data'], list):
return inner['data']
return inner if isinstance(inner, list) else []
contacts = _list(core['contacts'], 'contacts')
groups = _list(core['groups'], 'groups')
messages = _list(core['messages'], 'messages')
labels = _list(core['labels'], 'labels')
print(f'contacts : {len(contacts)}')
print(f'groups : {len(groups)}')
print(f'messages : {len(messages)}')
print(f'labels : {len(labels)}')
remark = [c for c in contacts if c.get('remark')]
namedgrp = [g for g in groups if g.get('name')]
print(f' - 有备注联系人: {len(remark)} 例子:',
', '.join((c.get('remark') or '?')[:24] for c in remark[:5]))
print(f' - 已命名群 : {len(namedgrp)} 例子:',
', '.join((g.get('name') or '?')[:24] for g in namedgrp[:5]))
print(f' - 标签 : {len(labels)} 例子:',
', '.join((l.get('name') or '?')[:18] for l in labels[:8]))
# 4) hookStatus / getMoments / getWechatVersion 直调
section('4) /api/v3/hook/execute — hookStatus + getWechatVersion')
hs = post('/api/v3/hook/execute', {
'device_id': DEVICE, 'platform': 'wechat',
'action': 'get_hook_status', 'params': {}, 'hook_only': True
})
show('hookStatus', hs)
gv = post('/api/v3/hook/execute', {
'device_id': DEVICE, 'platform': 'wechat',
'action': 'get_wechat_version', 'params': {}, 'hook_only': True
})
show('getWechatVersion', gv)
# 5) 朋友圈
section('5) /api/v3/hook/execute — getMoments(limit=3)')
mm = post('/api/v3/hook/execute', {
'device_id': DEVICE, 'platform': 'wechat',
'action': 'get_moments', 'params': {'limit': 3}, 'hook_only': True
})
show('getMoments', mm, 400)
# 6) 真发一条消息到文件传输助手
section('6) /api/v3/hook/execute — send_message → 文件传输助手')
ts = time.strftime('%H:%M:%S')
send = post('/api/v3/hook/execute', {
'device_id': DEVICE, 'platform': 'wechat',
'action': 'send_message',
'params': {
'to_id': 'filehelper',
'content': f'[E2E] Frida 主控通道 实机冒烟 {ts}',
'msg_type': 'text',
},
'hook_only': True
})
show('send_message', send)
# 7) 拉一条最近消息证明 read-back
section('7) /api/v3/hook/execute — get_recent_messages(limit=1)')
rm = post('/api/v3/hook/execute', {
'device_id': DEVICE, 'platform': 'wechat',
'action': 'get_recent_messages', 'params': {'limit': 1}, 'hook_only': True
})
show('get_recent_messages', rm, 400)
# 8) 走标准 unified channel=hook 的 sendMessage验证存客宝侧路径
section('8) /api/v3/message/send channel=hook (存客宝标准入口)')
um = post('/api/v3/message/send', {
'device_id': DEVICE, 'platform': 'wechat', 'to_id': 'filehelper',
'content': f'[E2E] unified channel=hook {ts}',
'msg_type': 'text', 'channel': 'hook'
})
show('unified.sendMessage', um, 400)
print('\n ✅ Hook 主控冒烟测试完成')

View File

@@ -0,0 +1,60 @@
#!/usr/bin/env bash
# 运维兜底ADB 一旦连上红米,自动在 Termux 启动 WS Agent一次性 bootstrap非日常主通道
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
LAN_ENV="${SCRIPT_DIR}/../config/lan.env"
if [[ -f "$LAN_ENV" ]]; then
# shellcheck source=/dev/null
source "$LAN_ENV"
fi
export ANDROID_ADB_SERVER_PORT="${ANDROID_ADB_SERVER_PORT:-5038}"
ADB=(adb -P "${ANDROID_ADB_SERVER_PORT}")
DEVICE_IP="${DEVICE_IP:-192.168.110.80:5555}"
DEVICE_ID="${DEVICE_ID:-xgfe65eimrrofyws}"
SDK_BASE="${WORKPHONE_API_URL:-http://127.0.0.1:8899}"
FRIDA_PORT="$(python3 -c "import json; print(json.load(open('${SCRIPT_DIR}/anti_detect/phantom_frida_config.json'))['listen_port'])" 2>/dev/null || echo 17263)"
WS_URL="${WORKPHONE_WS_URL:-ws://127.0.0.1:8899/ws/device/${DEVICE_ID}}"
LOG="/tmp/wp_adb_bootstrap_agent.log"
MAX_WAIT="${ADB_BOOTSTRAP_WAIT:-7200}"
exec >>"$LOG" 2>&1
echo "=== $(date) adb bootstrap agent wait ${DEVICE_IP} max=${MAX_WAIT}s ==="
deadline=$((SECONDS + MAX_WAIT))
while (( SECONDS < deadline )); do
# 优先 USB 序列号,其次任意非 TV 的无线 device
serial=""
if [[ -n "${DEVICE_SERIAL:-}" ]] && "${ADB[@]}" -s "$DEVICE_SERIAL" get-state 2>/dev/null | grep -qx device; then
serial="$DEVICE_SERIAL"
else
while IFS= read -r line; do
s="${line%%$'\t'*}"
[[ -z "$s" ]] && continue
brand=$("${ADB[@]}" -s "$s" shell getprop ro.product.brand 2>/dev/null | tr -d '\r')
[[ "$brand" == "haier" ]] && continue
serial="$s"
break
done < <("${ADB[@]}" devices 2>/dev/null | awk '/\tdevice$/{print $1}')
fi
if [[ -n "$serial" ]]; then
DEVICE_IP="$serial"
echo "=== $(date) ADB ready serial=${serial}, bootstrap Termux Agent ==="
"${ADB[@]}" -s "$serial" shell "am start -n com.termux/.HomeActivity" >/dev/null 2>&1 || true
sleep 2
CMD="export WP_AGENT_ON_DEVICE=1 WP_FRIDA_MODE=remote WP_FRIDA_PORT=${FRIDA_PORT}; cd ~/workphone/agent && python agent.py -d ${DEVICE_ID} -s ${WS_URL} --heartbeat 10"
"${ADB[@]}" -s "$serial" shell am startservice -n com.termux/com.termux.app.RunCommandService \
-a com.termux.RUN_COMMAND \
--es com.termux.RUN_COMMAND_PATH /data/data/com.termux/files/usr/bin/bash \
--esa com.termux.RUN_COMMAND_ARGUMENTS "-lc,${CMD}" 2>/dev/null || {
echo "RunCommand 失败,请手动在 Termux 粘贴 termux_agent_bootstrap.sh 输出"
bash "${SCRIPT_DIR}/termux_agent_bootstrap.sh" "$DEVICE_ID"
}
echo "=== bootstrap 已触发,等待 WS 上线 ==="
bash "${SCRIPT_DIR}/wait_ws_agent.sh" "$DEVICE_ID" "$SDK_BASE" 120 5 && exit 0
fi
"${ADB[@]}" connect "${DEVICE_IP}" >/dev/null 2>&1 || true
echo "$(date '+%H:%M:%S') waiting phone (skip haier TV)"
sleep 10
done
echo "=== timeout ==="
exit 1

View File

@@ -0,0 +1,82 @@
#!/usr/bin/env bash
# ADB 连接稳定化:宿主机 adb server + USB/无线扫描 + u2/ATX + Agent/WS 自检
set -euo pipefail
SDK_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
SDK_PORT="${SDK_PORT:-8899}"
LAN_ENV="${SDK_ROOT}/config/lan.env"
if [[ -f "${LAN_ENV}" ]]; then
# shellcheck source=/dev/null
source "${LAN_ENV}" || true
fi
WIRELESS_IPS="${WIRELESS_IPS:-192.168.110.80 192.168.1.12 192.168.0.12}"
export ANDROID_ADB_SERVER_PORT="${ANDROID_ADB_SERVER_PORT:-5038}"
LOG="${TMPDIR:-/tmp}/adb-stabilize-$(date +%Y%m%d_%H%M%S).log"
adb() {
command adb -P "${ANDROID_ADB_SERVER_PORT}" "$@"
}
exec > >(tee -a "$LOG") 2>&1
echo "=== ADB 连接稳定化 $(date) ==="
echo "日志: $LOG"
echo "ADB port: ${ANDROID_ADB_SERVER_PORT}"
bash "${SDK_ROOT}/scripts/adb_host_server_listen_all.sh" --background || true
sleep 1
echo "--- 清理离线无线连接 ---"
adb disconnect >/dev/null 2>&1 || true
adb kill-server >/dev/null 2>&1 || true
adb start-server >/dev/null 2>&1 || true
sleep 2
echo "--- USB / 已配对设备 ---"
adb devices -l || true
# 尝试无线(短超时,避免 hang
for ip in $WIRELESS_IPS; do
[[ "${ip}" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]] || continue
( adb connect "${ip}:5555" & pid=$!; sleep 3; kill $pid 2>/dev/null || true ) || true
done
adb devices -l || true
SERIAL=""
while read -r s st _rest; do
[[ -z "${s}" || -z "${st}" ]] && continue
if [[ "$st" == "device" ]]; then
SERIAL="$s"
break
fi
if [[ "$st" == "unauthorized" ]]; then
echo "⚠️ 设备 ${s} 未授权:请在手机上点「允许 USB 调试」并选「文件传输/MTP」"
fi
done < <(adb devices | awk 'NR>1 && NF>=2 {print $1, $2}')
if [[ -z "$SERIAL" ]]; then
echo "❌ 当前无已授权 ADB 设备"
echo " 1) 开发者选项 → USB 调试 开启"
echo " 2) USB 模式 → 文件传输"
echo " 3) 重新插拔数据线,看手机弹窗点「允许」"
echo " 4) 再运行: bash sdk/scripts/adb_connection_stabilize.sh"
exit 1
fi
echo "✅ 已授权设备: $SERIAL"
if command -v python3 >/dev/null 2>&1; then
echo "--- uiautomator2 init (ATX 9008) ---"
python3 -m uiautomator2 init --serial "$SERIAL" 2>/dev/null || \
python3 -m uiautomator2 init -s "$SERIAL" 2>/dev/null || \
echo "⚠️ u2 init 跳过(可手动: python3 -m uiautomator2 init --serial $SERIAL"
fi
echo "--- 启动 Frida 主控 + Agent ---"
bash "${SDK_ROOT}/scripts/frida_workphone_oneclick.sh" -d "$SERIAL" || true
echo "--- 连接状态 ---"
curl -s "http://127.0.0.1:${SDK_PORT}/api/v3/connection/status" | python3 -m json.tool 2>/dev/null || true
curl -s "http://127.0.0.1:${SDK_PORT}/api/v3/hook/probe/${SERIAL}" | python3 -m json.tool 2>/dev/null | head -20 || true
echo ""
echo "管理端: http://127.0.0.1:${SDK_PORT}/hub"
echo "设备控制台: http://127.0.0.1:${SDK_PORT}/static/index.html"

View File

@@ -0,0 +1,23 @@
#!/usr/bin/env bash
# 宿主机 ADB server 监听所有网卡,供 Docker 内通过 ADB_SERVER_SOCKET=tcp:host.docker.internal:5038 连接。
# 见:机擎/references/ADB_Docker_USB排查.md
set -euo pipefail
ADB_PORT="${ANDROID_ADB_SERVER_PORT:-5038}"
export ANDROID_ADB_SERVER_PORT="${ADB_PORT}"
ADB=(adb -P "${ADB_PORT}")
LOG="${ADB_HOST_SERVER_LOG:-${TMPDIR:-/tmp}/adb-host-server-${ADB_PORT}.log}"
case "${1:-}" in
--background)
"${ADB[@]}" kill-server 2>/dev/null || true
# -a允许非本机连接Docker 网桥访问宿主机 ADB 端口)
nohup "${ADB[@]}" -a nodaemon server >>"$LOG" 2>&1 &
echo "adb host server PID $! port=${ADB_PORT} (log: $LOG)"
sleep 1
"${ADB[@]}" devices -l || true
;;
*)
"${ADB[@]}" kill-server 2>/dev/null || true
exec "${ADB[@]}" -a nodaemon server
;;
esac

View File

@@ -0,0 +1,53 @@
#!/usr/bin/env bash
# 通过 ADB 安全释放手机存储:系统缓存裁剪 + 常见临时/缩略图(不卸载应用、不清登录态)
# 用法:手机 USB 调试已连接后执行
# bash sdk/scripts/android_free_space.sh
# 多设备ANDROID_SERIAL=序列号 bash sdk/scripts/android_free_space.sh
# 更激进(仍不 pm clear 应用):加参数 --more
set -euo pipefail
ADB=(adb)
if [[ -n "${ANDROID_SERIAL:-}" ]]; then
ADB=(adb -s "$ANDROID_SERIAL")
fi
if ! "${ADB[@]}" get-state 2>/dev/null | grep -q device; then
echo "❌ 未检测到已授权的 ADB 设备。请USB 连接 → 开启 USB 调试 → 本机执行 adb devices 出现 device。"
exit 1
fi
echo "=== 设备 ==="
"${ADB[@]}" shell getprop ro.product.model 2>/dev/null | tr -d '\r'
"${ADB[@]}" devices -l | grep -v "^List"
echo ""
echo "=== 清理前 /data或整机存储==="
"${ADB[@]}" shell df -h /data 2>/dev/null | tr -d '\r' || "${ADB[@]}" shell df -h | head -20 | tr -d '\r'
# 系统级裁剪各应用 cacheAndroid 7+,无需 root
echo ""
echo "=== pm trim-caches按目标剩余空间触发缓存回收==="
# 部分机型参数为「希望至少腾出/保持」的数值,单位因版本而异;依次尝试常见写法
"${ADB[@]}" shell pm trim-caches 999999M 2>/dev/null | tr -d '\r' || true
"${ADB[@]}" shell "pm trim-caches 8589934592" 2>/dev/null | tr -d '\r' || true
echo ""
echo "=== 临时目录 ==="
"${ADB[@]}" shell "rm -rf /data/local/tmp/*" 2>/dev/null || true
echo "=== DCIM 缩略图(可再生成)==="
"${ADB[@]}" shell "rm -rf /sdcard/DCIM/.thumbnails/*" 2>/dev/null || true
"${ADB[@]}" shell "rm -rf /storage/emulated/0/DCIM/.thumbnails/*" 2>/dev/null || true
if [[ "${1:-}" == "--more" ]]; then
echo "=== --more常见缓存目录不含微信/QQ 聊天记录)==="
"${ADB[@]}" shell "rm -rf /sdcard/Android/data/*/cache/*" 2>/dev/null || true
"${ADB[@]}" shell "rm -rf /storage/emulated/0/Android/data/*/cache/*" 2>/dev/null || true
"${ADB[@]}" shell "rm -rf /sdcard/.Trash/*" 2>/dev/null || true
"${ADB[@]}" shell "rm -rf /sdcard/Download/.tmp/*" 2>/dev/null || true
fi
echo ""
echo "=== 清理后 /data ==="
"${ADB[@]}" shell df -h /data 2>/dev/null | tr -d '\r' || true
echo "✅ 完成。若空间仍不足,可在手机「设置 → 存储」里清理各 App或卸载不常用应用。"

View File

@@ -0,0 +1,6 @@
{
"device_serial": "xgfe65eimrrofyws",
"listen_port": 14537,
"binary_name": "fs_3f823a",
"binary_path": "/data/local/tmp/fs_3f823a"
}

View File

@@ -0,0 +1,116 @@
#!/usr/bin/env bash
# ==================================================================
# AF9 — Phantom-Frida / StrongR-Frida 自动部署脚本
#
# 功能:
# 1. 检测设备 CPU 架构
# 2. 下载对应的反检测 Frida Server 二进制
# 3. 推送到设备 /data/local/tmp/,随机命名(反扫描)
# 4. 启动时使用随机端口(避免 27042 默认端口检测)
#
# 用法:
# bash setup_phantom_frida.sh [device_serial]
#
# 依赖adb, curl/wget, unxz
# ==================================================================
set -euo pipefail
DEVICE_SERIAL="${1:-}"
ADB_CMD="adb"
[ -n "$DEVICE_SERIAL" ] && ADB_CMD="adb -s $DEVICE_SERIAL"
FRIDA_VERSION="${FRIDA_VERSION:-16.6.6}"
PHANTOM_REPO="https://github.com/aspect-dev/phantom-frida/releases/download"
STRONGR_REPO="https://github.com/aspect-dev/strongr-frida/releases/download"
if command -v shuf >/dev/null 2>&1; then
RAND_PORT=$(shuf -i 10000-60000 -n 1)
else
RAND_PORT=$((10000 + RANDOM % 50001))
fi
RAND_NAME="fs_$(head -c 6 /dev/urandom | xxd -p)"
echo "=== Phantom-Frida 部署脚本 ==="
echo "Frida版本: $FRIDA_VERSION"
echo "随机端口: $RAND_PORT"
echo "服务名称: $RAND_NAME"
# Step 1: 检测 CPU 架构
ARCH=$($ADB_CMD shell getprop ro.product.cpu.abi | tr -d '\r')
echo "设备架构: $ARCH"
case "$ARCH" in
arm64-v8a) FRIDA_ARCH="arm64" ;;
armeabi-v7a) FRIDA_ARCH="arm" ;;
x86_64) FRIDA_ARCH="x86_64" ;;
x86) FRIDA_ARCH="x86" ;;
*)
echo "不支持的架构: $ARCH"
exit 1
;;
esac
BINARY_NAME="frida-server-${FRIDA_VERSION}-android-${FRIDA_ARCH}"
DOWNLOAD_URL="${PHANTOM_REPO}/${FRIDA_VERSION}/${BINARY_NAME}.xz"
FALLBACK_URL="${STRONGR_REPO}/${FRIDA_VERSION}/${BINARY_NAME}.xz"
TMPDIR=$(mktemp -d)
trap "rm -rf $TMPDIR" EXIT
echo "下载 Phantom-Frida..."
if ! curl -L -f -o "$TMPDIR/${BINARY_NAME}.xz" "$DOWNLOAD_URL" 2>/dev/null; then
echo "Phantom-Frida 下载失败,尝试 StrongR-Frida..."
if ! curl -L -f -o "$TMPDIR/${BINARY_NAME}.xz" "$FALLBACK_URL" 2>/dev/null; then
echo "=== 自动下载失败 ==="
echo "请手动下载以下任一版本:"
echo " Phantom-Frida: https://github.com/aspect-dev/phantom-frida/releases"
echo " StrongR-Frida: https://github.com/aspect-dev/strongr-frida/releases"
echo "下载后放到 $TMPDIR/${BINARY_NAME}.xz 再重新执行"
exit 1
fi
fi
echo "解压..."
unxz "$TMPDIR/${BINARY_NAME}.xz"
echo "推送到设备..."
$ADB_CMD push "$TMPDIR/${BINARY_NAME}" "/data/local/tmp/${RAND_NAME}"
$ADB_CMD shell "chmod 755 /data/local/tmp/${RAND_NAME}"
echo "停止旧 Frida 进程..."
$ADB_CMD shell "su -c 'pkill -f frida-server'" 2>/dev/null || true
$ADB_CMD shell "su -c 'pkill -f hluda-server'" 2>/dev/null || true
sleep 1
echo "启动 Phantom-Frida端口=$RAND_PORT..."
$ADB_CMD shell "su -c 'nohup /data/local/tmp/${RAND_NAME} -l 0.0.0.0:${RAND_PORT} &'" &
sleep 2
RUNNING=$($ADB_CMD shell "ps | grep ${RAND_NAME}" | wc -l || echo "0")
if [ "$RUNNING" -gt 0 ]; then
echo "=== 部署成功 ==="
echo "名称: $RAND_NAME"
echo "端口: $RAND_PORT"
echo "路径: /data/local/tmp/${RAND_NAME}"
echo ""
echo "连接方式: frida -H 127.0.0.1:${RAND_PORT} -n com.tencent.mm"
echo "ADB转发: adb forward tcp:${RAND_PORT} tcp:${RAND_PORT}"
CONFIG_FILE="$(dirname "$0")/phantom_frida_config.json"
cat > "$CONFIG_FILE" <<EOJSON
{
"device_serial": "${DEVICE_SERIAL:-auto}",
"frida_version": "${FRIDA_VERSION}",
"binary_name": "${RAND_NAME}",
"binary_path": "/data/local/tmp/${RAND_NAME}",
"listen_port": ${RAND_PORT},
"arch": "${FRIDA_ARCH}",
"deployed_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
}
EOJSON
echo "配置已保存: $CONFIG_FILE"
else
echo "=== 启动失败,请检查 Root 权限 ==="
exit 1
fi

View File

@@ -0,0 +1,161 @@
#!/usr/bin/env bash
# ==================================================================
# AF10 — Root 隐藏模块自动安装脚本
#
# 安装顺序(针对 Magisk/KernelSU 设备):
# 1. Shamiko (Magisk Zygisk) 或 KernelSU 自带隐藏
# 2. HMA-OSS (Hide My Applist) — 隐藏应用列表
# 3. Play Integrity Fork — 通过 Play Integrity 检测
# 4. TrickStore — 签名欺骗
#
# 本脚本提供模板和检查流程,实际需要在已 Root 的真机上执行。
#
# 用法:
# bash setup_root_hide.sh [device_serial]
# ==================================================================
set -euo pipefail
DEVICE_SERIAL="${1:-}"
ADB_CMD="adb"
[ -n "$DEVICE_SERIAL" ] && ADB_CMD="adb -s $DEVICE_SERIAL"
echo "=== Root 隐藏模块部署脚本 ==="
check_root_method() {
if $ADB_CMD shell "su -c 'magisk -v'" 2>/dev/null | grep -q "magisk"; then
echo "magisk"
elif $ADB_CMD shell "su -c 'ksud --version'" 2>/dev/null | grep -q "kernelsu\|ksu"; then
echo "kernelsu"
elif $ADB_CMD shell "su -c 'apd --version'" 2>/dev/null | grep -q "apatch"; then
echo "apatch"
else
echo "unknown"
fi
}
ROOT_METHOD=$(check_root_method)
echo "Root方案: $ROOT_METHOD"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
MODULES_DIR="$SCRIPT_DIR/root_modules"
mkdir -p "$MODULES_DIR"
# Shamiko
SHAMIKO_URL="https://github.com/aspect-dev/Shamiko/releases/latest/download/Shamiko.zip"
SHAMIKO_ZIP="$MODULES_DIR/Shamiko.zip"
# HMA-OSS (Hide My Applist)
HMA_URL="https://github.com/aspect-dev/HMA-OSS/releases/latest/download/HMA-OSS.zip"
HMA_ZIP="$MODULES_DIR/HMA-OSS.zip"
# Play Integrity Fork
PIF_URL="https://github.com/aspect-dev/PlayIntegrityFork/releases/latest/download/PlayIntegrityFork.zip"
PIF_ZIP="$MODULES_DIR/PlayIntegrityFork.zip"
download_if_missing() {
local url="$1" dest="$2" name="$3"
if [ -f "$dest" ]; then
echo "[$name] 已缓存: $dest"
else
echo "[$name] 下载中..."
if curl -L -f -o "$dest" "$url" 2>/dev/null; then
echo "[$name] 下载完成"
else
echo "[$name] 下载失败,请手动下载: $url"
echo " 放到: $dest"
fi
fi
}
install_magisk_module() {
local zip_path="$1" name="$2"
if [ ! -f "$zip_path" ]; then
echo "[$name] 模块文件不存在,跳过"
return
fi
echo "[$name] 推送到设备..."
$ADB_CMD push "$zip_path" "/data/local/tmp/$(basename "$zip_path")"
echo "[$name] 安装..."
$ADB_CMD shell "su -c 'magisk --install-module /data/local/tmp/$(basename "$zip_path")'" 2>/dev/null || \
echo "[$name] 自动安装失败。请手动在 Magisk App 中安装: /data/local/tmp/$(basename "$zip_path")"
}
echo ""
echo "=== Step 1: 下载模块 ==="
download_if_missing "$SHAMIKO_URL" "$SHAMIKO_ZIP" "Shamiko"
download_if_missing "$HMA_URL" "$HMA_ZIP" "HMA-OSS"
download_if_missing "$PIF_URL" "$PIF_ZIP" "PlayIntegrityFork"
echo ""
echo "=== Step 2: 安装模块 ==="
if [ "$ROOT_METHOD" = "magisk" ]; then
echo "检测到 Magisk确保 Zygisk 已开启..."
ZYGISK=$($ADB_CMD shell "su -c 'magisk --sqlite \"SELECT value FROM settings WHERE key=\\\"zygisk\\\"\"'" 2>/dev/null || echo "")
if echo "$ZYGISK" | grep -q "1"; then
echo "Zygisk 已启用"
else
echo "⚠ Zygisk 未启用。请在 Magisk App 设置中开启 Zygisk 后重启设备"
fi
install_magisk_module "$SHAMIKO_ZIP" "Shamiko"
install_magisk_module "$HMA_ZIP" "HMA-OSS"
install_magisk_module "$PIF_ZIP" "PlayIntegrityFork"
elif [ "$ROOT_METHOD" = "kernelsu" ]; then
echo "KernelSU 模式,推送模块到 /data/local/tmp/..."
[ -f "$HMA_ZIP" ] && $ADB_CMD push "$HMA_ZIP" "/data/local/tmp/HMA-OSS.zip"
[ -f "$PIF_ZIP" ] && $ADB_CMD push "$PIF_ZIP" "/data/local/tmp/PlayIntegrityFork.zip"
echo "请在 KernelSU Manager 中手动安装以上模块"
else
echo "未检测到已知 Root 方案,请确保设备已 Root"
echo "模块已下载到: $MODULES_DIR/"
echo "请手动安装"
fi
echo ""
echo "=== Step 3: 配置排除列表 ==="
HIDE_APPS=(
"com.tencent.mm"
"com.tencent.xin"
"com.ss.android.ugc.aweme"
"com.xingin.xhs"
"com.taobao.idlefish"
)
echo "以下应用应加入排除/隐藏列表:"
for app in "${HIDE_APPS[@]}"; do
echo " - $app"
done
echo ""
echo "Shamiko: 在 Magisk 排除列表(DenyList)中勾选以上应用"
echo "HMA-OSS: 在 HMA 配置中启用对以上应用的应用列表隐藏"
echo ""
echo "=== Step 4: 验证 ==="
echo "验证命令(在设备上执行):"
echo " su -c 'ls /system/xbin/su' # 应隐藏"
echo " su -c 'which magisk' # 应隐藏"
echo " pm list packages | grep magisk # 应无结果"
echo " pm list packages | grep supersu # 应无结果"
echo ""
REPORT_FILE="$SCRIPT_DIR/root_hide_report.json"
cat > "$REPORT_FILE" <<EOJSON
{
"device_serial": "${DEVICE_SERIAL:-auto}",
"root_method": "${ROOT_METHOD}",
"modules_downloaded": {
"shamiko": $([ -f "$SHAMIKO_ZIP" ] && echo "true" || echo "false"),
"hma_oss": $([ -f "$HMA_ZIP" ] && echo "true" || echo "false"),
"play_integrity_fork": $([ -f "$PIF_ZIP" ] && echo "true" || echo "false")
},
"hide_apps": $(printf '%s\n' "${HIDE_APPS[@]}" | jq -R . | jq -s .),
"deployed_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
}
EOJSON
echo "报告已保存: $REPORT_FILE"
echo ""
echo "=== 完成(重启设备后生效) ==="
echo "执行: $ADB_CMD reboot"

View File

@@ -0,0 +1,151 @@
#!/usr/bin/env python3
"""
奥创 SDK 通信协议(客服通信接口总目录 · EnumMsgType 指令类 Task↔ 工作手机 SDK action 覆盖矩阵。
目的:对照奥创 proto 的服务端→设备「指令类任务」(1070-1270),核对本 SDK 是否已具备
可下发的等价 actionACTION_TO_RPC + ACTION_ALIASES catalog。锁定真实缺口禁止漏项。
用法: python3 sdk/scripts/aochuang_protocol_coverage.py [--md 输出.md]
真机铁律:本脚本仅做「能力覆盖」静态核对,不代表真机已验收。
"""
from __future__ import annotations
import argparse
import re
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
HE = ROOT / "sdk/agent/hook/hook_executor.py"
# 奥创协议「服务端/PC → 设备」指令类任务枚举 → 本 SDK 期望 actionNone = 缺口)
# 依据 资料/工作手机客服通信接口总目录(带通信协议)SDK.doc EnumMsgType
PROTO_TASKS: dict[str, tuple[str, str | None]] = {
"TalkToFriendTask(1070)": ("给好友发消息", "send_message"),
"PostSNSNewsTask(1071)": ("发朋友圈", "post_moments"),
"AddFriendsTask(1072)": ("主动添加好友", "add_friend"),
"DeleteSNSNewsTask(1074)": ("删除朋友圈", "delete_moments"),
"AcceptFriendAddRequestTask(1075)": ("接受好友请求", "accept_friend"),
"WeChatGroupSendTask(1076)": ("群发消息", "mass_send"),
"WeChatMaintenanceTask(1077)": ("养号任务", "start_nurture"),
"RequestTalkDetailTask(1078)": ("请求图片/视频详情", "get_message_raw_xml"),
"PullWeChatQrCodeTask(1079)": ("上传当前微信二维码", "generate_my_qr_code"),
"TriggerFriendPushTask(1080)": ("推送好友列表", "get_contacts"),
"TriggerCirclePushTask(1081)": ("推送朋友圈列表", "get_moments"),
"CircleCommentDeleteTask(1082)": ("朋友圈评论删除", "delete_moment_comment"),
"CircleCommentReplyTask(1084)": ("朋友圈评论回复", "reply_moment_comment"),
"TriggerMessageReadTask(1086)": ("聊天窗口置已读", "mark_conversation_read"),
"RevokeMessageTask(1087)": ("消息撤回", "revoke_message"),
"ForwardMessageTask(1088)": ("转发消息", "forward_message"),
"TriggerHistoryMsgPushTask(1089)": ("推送历史聊天记录", "get_recent_messages"),
"PullChatRoomQrCodeTask(1090)": ("获取群二维码", "generate_group_qr_code"),
"SendMultiPictureTask(1091)": ("发送多张图片", "send_multi_image"),
"ForwardMultiMessageTask(1092)": ("转发多条消息", "forward_multiple"),
"PostFriendDetectTask(1095)": ("清粉任务", "detect_zombie_fans"),
"PostStopFriendDetectTask(1096)": ("终止清粉", "stop_detect_zombie_fans"),
"PostMomentsPraiseTask(1098)": ("朋友圈点赞", "like_moments"),
"PostStopMomentsPraiseTask(1099)": ("停止朋友圈点赞", "stop_moments_praise"),
"PostStopWeChatMaintenanceTask(1100)": ("养号停止", "stop_nurture"),
"ModifyFriendMemoTask(1101)": ("修改备注", "set_friend_remark"),
"AddFriendWithSceneTask(1102)": ("场景加好友", "add_friend_with_scene"),
"TakeLuckyMoneyTask(1200)": ("领取红包/转账", "receive_red_packet"),
"PullFriendCircleTask(1201)": ("获取指定好友朋友圈", "get_friend_moments"),
"PullCircleDetailTask(1202)": ("获取朋友圈图片", "get_moment_detail"),
"CircleLikeTask(1203)": ("单条朋友圈点赞", "like_moments"),
"TriggerChatroomPushTask(1210)": ("推送群聊列表", "get_groups"),
"RequestChatRoomInfoTask(1211)": ("群聊详情", "get_group_info"),
"RequestContactsInfoTask(1212)": ("联系人详情", "get_contact_info"),
"ChatRoomActionTask(1213)": ("群聊管理", "set_group_name"),
"AddFriendInChatRoomTask(1214)": ("群内加好友", "add_friend_in_room"),
"AddFriendFromPhonebookTask(1215)": ("通讯录加好友", "add_friend_from_phonebook"),
"DeleteFriendTask(1216)": ("删除好友", "delete_friend"),
"SendLuckyMoneyTask(1217)": ("发红包", "send_red_packet"),
"RequestTalkContentTask(1218)": ("获取消息原始xml", "get_message_raw_xml"),
"ForwardMessageByContentTask(1220)": ("转发消息内容", "forward_message"),
"ChatRoomInviteApproveTask(1221)": ("群主确认入群申请", "approve_group_invite"),
"WechatLogoutTask(1222)": ("微信登出", "logout"),
"PhoneActionTask(1223)": ("手机操作(重启等)", "phone_action"),
"ContactLabelTask(1224)": ("设置联系人标签", "set_contact_label"),
"ContactLabelDeleteTask(1225)": ("删除联系人标签", "delete_label"),
"VoiceTransTextTask(1226)": ("语音转文字", "voice_to_text"),
"FindContactTask(1227)": ("查找微信联系人", "search_contacts"),
"AgreeJoinChatRoomTask(1229)": ("同意加入群聊", "agree_join_group"),
"ClearAllChatMsgTask(1230)": ("清空聊天记录", "clear_chat_history"),
"SendFriendVerifyTask(1231)": ("聊天界面发送朋友验证", "send_friend_verify"),
"TriggerConversationPushTask(1232)": ("会话列表推送", "get_conversations"),
"WechatSettingTask(1233)": ("微信设置(昵称/头像)", "set_nickname"),
"PullFriendAddReqListTask(1234)": ("加好友请求列表", "get_friend_requests"),
"TriggerBizContactPushTask(1235)": ("公众号列表", "get_official_accounts"),
"AddFriendNameCardTask(1236)": ("名片加好友", "add_friend_by_card"),
"TriggerChatMsgIdsPushTask(1251)": ("时间段内 msgSvrId", "get_msg_ids_by_time"),
"RequestTalkMsgTask(1252)": ("按 msgSvrId 取消息", "get_message_by_id"),
"SearchBizContactTask(1254)": ("搜索公众号/小程序", "search_official_account"),
"PhoneStateTask(1256)": ("手机状态(电量/存储)", "get_device_info"),
"WeChatLocationTask(1258)": ("微信查手机位置", "get_wechat_location"),
"RemittanceTask(1260)": ("转账", "send_transfer"),
"WalletBalanceTask(1262)": ("钱包余额", "get_wallet_balance"),
"QueryHbDetailTask(1265)": ("查询红包详情", "query_red_packet"),
"JoinGroupByQrTask(1267)": ("扫二维码进群", "join_group_by_qr"),
"SendJielongTask(1268)": ("发接龙消息", "send_jielong"),
"ContactSetLabelTask(1270)": ("设置用户标签", "set_contact_label"),
"WechatLogoutTask(cmd)": ("账号登出命令", "logout"),
"PhoneActionTask(cmd-reboot)": ("重启手机等命令", "phone_action"),
"UpgradeDeviceAppNotice(cmd)": ("软件升级通知", "check_for_update"),
}
def load_sdk_catalog() -> set[str]:
text = HE.read_text(encoding="utf-8", errors="ignore")
a = text.split("ACTION_TO_RPC:", 1)[1].split("ACTION_ALIASES:", 1)[0]
b = text.split("ACTION_ALIASES:", 1)[1].split("\n\n", 1)[0]
rpc = set(re.findall(r'"([a-z_]+)"\s*:', a))
alias = set(re.findall(r'"([a-z_]+)"\s*:', b))
return rpc | alias
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--md", default="")
args = ap.parse_args()
catalog = load_sdk_catalog()
covered, gaps = [], []
for task, (cn, action) in PROTO_TASKS.items():
if action and action in catalog:
covered.append((task, cn, action))
elif action and action not in catalog:
gaps.append((task, cn, f"{action}(映射缺失)"))
else:
gaps.append((task, cn, "无对应 action"))
total = len(PROTO_TASKS)
lines = [
"# 奥创协议 ↔ 工作手机 SDK action 覆盖矩阵",
"",
f"> 真源:`资料/工作手机客服通信接口总目录(带通信协议)SDK.doc` · 自动生成 `sdk/scripts/aochuang_protocol_coverage.py`",
f"> 指令类任务 **{total}** 项 · 已覆盖 **{len(covered)}** · 缺口 **{len(gaps)}** · 覆盖率 **{len(covered)*100//total}%**",
"> 真机铁律:覆盖=能力映射就绪;真机 E2E 另见验收清单,禁止无证据标功能 ✅",
"",
"## 一、已覆盖",
"",
"| 协议任务 | 中文 | SDK action |",
"|:---|:---|:---|",
]
for t, cn, a in covered:
lines.append(f"| {t} | {cn} | `{a}` |")
lines += ["", "## 二、缺口(待补 action", "", "| 协议任务 | 中文 | 现状 |", "|:---|:---|:---|"]
for t, cn, a in gaps:
lines.append(f"| {t} | {cn} | {a} |")
md = "\n".join(lines) + "\n"
if args.md:
Path(args.md).write_text(md, encoding="utf-8")
print(f"已写入 {args.md}")
print(f"覆盖 {len(covered)}/{total} · 缺口 {len(gaps)}")
for t, cn, _ in gaps:
print(f" GAP {t} {cn}")
return 0
if __name__ == "__main__":
sys.exit(main())

View File

@@ -0,0 +1,43 @@
#!/usr/bin/env bash
# auto_accept_when_ready.sh - guard: when phone frida attaches WeChat, auto run 3-item acceptance
# Polls hook/probe; on supports_hook=true runs run_three_acceptance.sh and archives evidence.
# Usage: bash sdk/scripts/auto_accept_when_ready.sh [device_id] [max_wait_sec]
set -o pipefail
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
DEVICE="${1:-xgfe65eimrrofyws}"
MAX_WAIT="${2:-7200}"
BASE="${SDK_BASE_URL:-http://127.0.0.1:8899}"
INTERVAL=10
LOG="$ROOT/sdk/logs/auto_accept_${DEVICE}.log"
mkdir -p "$(dirname "$LOG")"
echo "[auto-accept] start $(date) device=$DEVICE max=${MAX_WAIT}s" | tee -a "$LOG"
deadline=$(( $(date +%s) + MAX_WAIT ))
while [ "$(date +%s)" -lt "$deadline" ]; do
# 就绪信号diag_wcdb RPC 可用(= 新 wechat_hook_v2.js 已重载,含 WCDB 2.x 适配)
DIAG="$(curl -s --max-time 30 -X POST "$BASE/api/v3/hook/execute" -H 'Content-Type: application/json' \
-d "{\"device_id\":\"$DEVICE\",\"platform\":\"wechat\",\"action\":\"diag_wcdb\",\"params\":{},\"hook_only\":true}" 2>/dev/null)"
SUP="$(printf '%s' "$DIAG" | python3 -c 'import sys,json
try:
d=json.load(sys.stdin); inner=d.get("data",d)
# 最新 JS 标志diag_wcdb 含 main_query 实读测试字段
print("True" if (inner.get("success") and "main_query" in inner) else "ERR")
except Exception:
print("ERR")' 2>/dev/null)"
SUP="${SUP:-ERR}"
if [ "$SUP" = "True" ]; then
echo "[auto-accept] $(date) diag_wcdb ok (new JS loaded) -> run 3-item acceptance" | tee -a "$LOG"
echo "[auto-accept] WCDB diag: $DIAG" >> "$LOG"
bash "$ROOT/sdk/scripts/run_three_acceptance.sh" "$DEVICE" 2>&1 | tee -a "$LOG"
RC="${PIPESTATUS[0]:-0}"
echo "[auto-accept] acceptance exit=$RC $(date)" | tee -a "$LOG"
exit "$RC"
fi
echo "[auto-accept] $(date '+%H:%M:%S') 新JS未就绪(diag_wcdb=$SUP)等手机重启Agentretry ${INTERVAL}s" >> "$LOG"
sleep "$INTERVAL"
done
echo "[auto-accept] timeout, frida not ready $(date)" | tee -a "$LOG"
exit 1

View File

@@ -0,0 +1,31 @@
#!/usr/bin/env bash
# 守护:轮询微信 Frida 附着状态,一旦 frida_connected=true 立即跑真机验收(截图+读/发/转发+revoke并退出。
# 真机铁律:只在 Frida 真附着时才跑 E2E绝不伪造。
set -uo pipefail
SDK="${WORKPHONE_SDK_URL:-http://127.0.0.1:8899}"
DEVICE_ID="${DEVICE_ID:-xgfe65eimrrofyws}"
MAX_MIN="${MAX_MIN:-120}" # 最长守护分钟
INTERVAL="${INTERVAL:-30}"
REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
LOG="$REPO_ROOT/sdk/tmp/auto_capture_$(date +%Y%m%d_%H%M%S).log"
mkdir -p "$(dirname "$LOG")"
deadline=$(( $(date +%s) + MAX_MIN*60 ))
echo "[watch] start sdk=$SDK device=$DEVICE_ID interval=${INTERVAL}s max=${MAX_MIN}min" | tee -a "$LOG"
while [ "$(date +%s)" -lt "$deadline" ]; do
R="$(curl -sS -m 15 -X POST "$SDK/api/v3/wechat/execute" -H 'Content-Type: application/json' \
-d "{\"device_id\":\"$DEVICE_ID\",\"action\":\"get_profile\",\"params\":{}}" 2>/dev/null)"
FR="$(printf '%s' "$R" | python3 -c "import sys,json
try:
d=json.load(sys.stdin);print((d.get('data') or {}).get('frida_connected'))
except: print('err')" 2>/dev/null)"
echo "[watch] $(date +%H:%M:%S) frida_connected=$FR" | tee -a "$LOG"
if [ "$FR" = "True" ]; then
echo "[watch] Frida 已附着 → 跑真机验收" | tee -a "$LOG"
WORKPHONE_SDK_URL="$SDK" DEVICE_ID="$DEVICE_ID" bash "$REPO_ROOT/sdk/scripts/run_wechat_realdevice_acceptance.sh" 2>&1 | tee -a "$LOG"
echo "[watch] done exit=${PIPESTATUS[0]}" | tee -a "$LOG"
exit 0
fi
sleep "$INTERVAL"
done
echo "[watch] 超时未等到 Frida 附着(${MAX_MIN}min退出。设备侧需微信前台+frida-server+Agent。" | tee -a "$LOG"
exit 2

View File

@@ -0,0 +1,98 @@
#!/usr/bin/env bash
# 重启/断线后自动等待 ADB → oneclick → 验收 WS 在线
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SDK_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
DEVICE_SERIAL="${DEVICE_SERIAL:-192.168.110.80:5555}"
SDK_PORT="${WORKPHONE_SDK_PORT:-8899}"
INTERVAL="${INTERVAL:-10}"
LOG="${SDK_ROOT}/logs/auto_recover.log"
mkdir -p "${SDK_ROOT}/logs"
log() { echo "[$(date '+%F %T')] $*" | tee -a "$LOG"; }
try_wireless_debug_ports() {
local host="${DEVICE_SERIAL%%:*}"
(
local p open_port=""
for p in $(seq 37000 37200); do
if nc -z -G 1 "$host" "$p" 2>/dev/null; then
open_port="$p"
break
fi
done
if [[ -n "$open_port" ]]; then
log "wireless_debug_port=${host}:${open_port} → adb connect"
try_adb_connect "${host}:${open_port}"
fi
) &
}
try_usb_device() {
local any
any="$(adb devices | awk '/\tdevice$/{print $1; exit}')"
if [[ -n "$any" && "$any" != "$DEVICE_SERIAL" ]]; then
log "usb_device_found=${any}"
DEVICE_SERIAL="$any"
fi
}
try_adb_connect() {
local target="$1"
[[ "$target" != *:* ]] && return 0
adb connect "$target" >/dev/null 2>&1 &
local pid=$!
local waited=0
while kill -0 "$pid" 2>/dev/null && (( waited < 3 )); do
sleep 1
waited=$((waited + 1))
done
if kill -0 "$pid" 2>/dev/null; then
kill "$pid" 2>/dev/null || true
wait "$pid" 2>/dev/null || true
else
wait "$pid" 2>/dev/null || true
fi
}
log "auto_recover start device=${DEVICE_SERIAL} interval=${INTERVAL}s"
cycle=0
while true; do
cycle=$((cycle + 1))
try_usb_device
if [[ "$DEVICE_SERIAL" == *:* ]]; then
try_adb_connect "$DEVICE_SERIAL"
fi
if (( cycle % 6 == 0 )); then
try_wireless_debug_ports
fi
state="$(adb -s "$DEVICE_SERIAL" get-state 2>/dev/null || true)"
if [[ "$state" != "device" ]]; then
any="$(adb devices | awk '/\tdevice$/{print $1; exit}')"
if [[ -n "$any" ]]; then
DEVICE_SERIAL="$any"
state="$(adb -s "$DEVICE_SERIAL" get-state 2>/dev/null || true)"
fi
fi
if [[ "$state" == "device" ]]; then
log "adb online serial=${DEVICE_SERIAL} → running oneclick"
if DEVICE_SERIAL="$DEVICE_SERIAL" bash "${SCRIPT_DIR}/frida_workphone_oneclick.sh" >>"$LOG" 2>&1; then
ws="$(curl -s "http://127.0.0.1:${SDK_PORT}/api/v3/connection/status" | python3 -c "import sys,json;d=json.load(sys.stdin).get('data',{});print(d.get('online_ws_count',0))" 2>/dev/null || echo 0)"
log "oneclick done ws_online=${ws}"
if [[ "$ws" -ge 1 ]]; then
log "auto_recover success"
exit 0
fi
fi
log "oneclick incomplete, retry in ${INTERVAL}s"
else
host="${DEVICE_SERIAL%%:*}"
if ping -c 1 -W 2 "$host" >/dev/null 2>&1; then
log "waiting adb (${state:-offline}) phone_ping=ok → 开「无线调试」或 USB 连一次 adb tcpip 5555"
else
log "waiting adb (${state:-offline}) phone_ping=fail"
fi
fi
sleep "$INTERVAL"
done

151
sdk/scripts/batch_deploy.sh Executable file
View File

@@ -0,0 +1,151 @@
#!/bin/bash
# batch_deploy.sh — 批量部署 Agent v3.1 到多台 Android 设备
#
# 用法:
# ./scripts/batch_deploy.sh --server ws://IP:8899/ws/device
# ./scripts/batch_deploy.sh --server ws://IP:8899/ws/device --ai-url http://IP:3102 --ai-key KEY
# ./scripts/batch_deploy.sh --devices "serial1,serial2,serial3" --server ws://...
#
# 前置:所有目标设备已通过 ADB 连接
set -eo pipefail
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[0;33m'; CYAN='\033[0;36m'; BOLD='\033[1m'; NC='\033[0m'
info() { echo -e "${GREEN}[INFO]${NC} $*"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
error() { echo -e "${RED}[ERROR]${NC} $*"; }
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
SDK_ROOT="$(dirname "$SCRIPT_DIR")"
AGENT_DIR="$SDK_ROOT/agent"
SERVER_URL=""
DEVICE_LIST=""
AI_URL=""
AI_KEY=""
INSTALL_FRIDA="false"
RESULTS=()
while [[ $# -gt 0 ]]; do
case "$1" in
--server|-s) SERVER_URL="$2"; shift 2 ;;
--devices|-d) DEVICE_LIST="$2"; shift 2 ;;
--ai-url) AI_URL="$2"; shift 2 ;;
--ai-key) AI_KEY="$2"; shift 2 ;;
--frida) INSTALL_FRIDA="true"; shift ;;
--help|-h)
echo "用法: bash batch_deploy.sh --server ws://IP:8899/ws/device [选项]"
echo ""
echo " --server, -s SDK 服务器 WebSocket 地址(必填)"
echo " --devices, -d 设备序列号(逗号分隔),不填则自动检测全部"
echo " --ai-url AI Brain API 地址"
echo " --ai-key AI Brain API Key"
echo " --frida 同时部署 Frida Server"
exit 0 ;;
*) shift ;;
esac
done
if [[ -z "$SERVER_URL" ]]; then
error "缺少 --server 参数"; exit 1
fi
if ! command -v adb &>/dev/null; then
error "adb 未安装"; exit 1
fi
if [[ -n "$DEVICE_LIST" ]]; then
IFS=',' read -ra DEVICES <<< "$DEVICE_LIST"
else
mapfile -t DEVICES < <(adb devices | grep -E 'device$' | awk '{print $1}')
fi
if [[ ${#DEVICES[@]} -eq 0 ]]; then
error "未检测到 ADB 设备"; exit 1
fi
echo ""
echo -e "${BOLD}${CYAN}============================================${NC}"
echo -e "${BOLD}${CYAN} 批量部署 Agent v3.1 (${#DEVICES[@]} 台设备)${NC}"
echo -e "${BOLD}${CYAN}============================================${NC}"
echo ""
deploy_one() {
local serial="$1"
local idx="$2"
local total="$3"
echo ""
info "[$idx/$total] 部署到 $serial ..."
if ! adb -s "$serial" shell echo ok &>/dev/null; then
warn "[$serial] 设备不可达,跳过"
RESULTS+=("$serial FAIL(不可达)")
return
fi
local brand; brand=$(adb -s "$serial" shell getprop ro.product.brand 2>/dev/null || echo "unknown")
local model; model=$(adb -s "$serial" shell getprop ro.product.model 2>/dev/null || echo "unknown")
info "[$serial] $brand $model"
local has_termux; has_termux=$(adb -s "$serial" shell pm list packages com.termux 2>/dev/null || echo "")
if [[ -z "$has_termux" || "$has_termux" != *"com.termux"* ]]; then
warn "[$serial] Termux 未安装,跳过(请先安装 Termux"
RESULTS+=("$serial FAIL(无Termux)")
return
fi
info "[$serial] 打包 Agent 代码..."
local tmp_tar="/tmp/agent_deploy_${serial}.tar.gz"
tar czf "$tmp_tar" -C "$AGENT_DIR" \
agent.py ai_brain.py skill_executor.py skill_bus.py error_handler.py \
vision_helper.py voice_agent.py config.json.example \
skills/ hook/ hawk/ 2>/dev/null || {
tar czf "$tmp_tar" -C "$AGENT_DIR" agent.py skill_executor.py skills/ 2>/dev/null
}
info "[$serial] 推送文件..."
adb -s "$serial" push "$tmp_tar" /sdcard/agent_deploy.tar.gz 2>/dev/null
info "[$serial] 解压并安装..."
adb -s "$serial" shell "run-as com.termux mkdir -p /data/data/com.termux/files/home/workphone-agent 2>/dev/null || true"
adb -s "$serial" shell "
cd /sdcard && \
cp agent_deploy.tar.gz /data/local/tmp/ 2>/dev/null; \
su -c 'cd /data/local/tmp && tar xzf agent_deploy.tar.gz -C /data/data/com.termux/files/home/workphone-agent/' 2>/dev/null || \
echo 'fallback' \
" 2>/dev/null || true
info "[$serial] 生成配置..."
local ai_block=""
if [[ -n "$AI_URL" ]]; then
ai_block=",\"ai_brain\":{\"enabled\":true,\"api_url\":\"$AI_URL\",\"api_key\":\"$AI_KEY\",\"model\":\"auto\",\"brain_interval\":60,\"standing_orders\":[\"检查微信未读消息\",\"自动通过好友请求\",\"保活微信\"]}"
fi
local config="{\"device_id\":\"$serial\",\"server_url\":\"$SERVER_URL\",\"heartbeat_interval\":10,\"project_id\":\"cunkebao\"$ai_block}"
adb -s "$serial" shell "echo '$config' > /data/local/tmp/wp_config.json" 2>/dev/null
if [[ "$INSTALL_FRIDA" == "true" ]]; then
info "[$serial] 安装 Frida Server..."
bash "$SCRIPT_DIR/setup_frida.sh" "$serial" 2>/dev/null || warn "[$serial] Frida 安装失败"
fi
rm -f "$tmp_tar"
info "[$serial] ✅ 部署完成"
RESULTS+=("$serial OK ($brand $model)")
}
idx=0
for serial in "${DEVICES[@]}"; do
idx=$((idx + 1))
deploy_one "$serial" "$idx" "${#DEVICES[@]}"
done
echo ""
echo -e "${BOLD}${CYAN}============================================${NC}"
echo -e "${BOLD}${GREEN} 批量部署结果${NC}"
echo -e "${BOLD}${CYAN}============================================${NC}"
for r in "${RESULTS[@]}"; do
echo " $r"
done
ok_count=$(printf '%s\n' "${RESULTS[@]}" | grep -c "OK" || true)
echo ""
echo -e " 成功: ${GREEN}${ok_count}${NC} / ${#DEVICES[@]}"
echo -e "${BOLD}${CYAN}============================================${NC}"

150
sdk/scripts/build_agent_config.py Executable file
View File

@@ -0,0 +1,150 @@
#!/usr/bin/env python3
"""生成 Termux Agent config.json — 连接顺序 ① NAS 局域网 ② 本机 Docker ③ NAS 外网 frp。
与 agent.py BIND-03 / workphone.mdc §零.十 一致。
用法:
python3 build_agent_config.py --device-id xgfe65eimrrofyws --out /tmp/config.json
python3 build_agent_config.py --print-primary # 只输出当前可达主服 ws 基址
"""
from __future__ import annotations
import argparse
import json
import socket
import sys
import urllib.request
from typing import List, Optional, Tuple
SDK_PORT = 8899
NAS_LAN_HOSTS = ("192.168.110.101", "192.168.1.201")
NAS_FRP_HOST = "open.quwanzhi.com"
HEARTBEAT = 10
PROJECT_ID = "cunkebao"
def _tcp_ok(host: str, port: int, timeout: float = 2.5) -> bool:
try:
with socket.create_connection((host, port), timeout=timeout):
return True
except OSError:
return False
def _health_ok(host: str, port: int = SDK_PORT, timeout: float = 3.0) -> bool:
try:
url = f"http://{host}:{port}/health"
with urllib.request.urlopen(url, timeout=timeout) as resp:
return resp.status == 200
except Exception:
return False
def pick_mac_lan_ip() -> str:
import subprocess
try:
out = subprocess.check_output(["ifconfig"], text=True, stderr=subprocess.DEVNULL)
except Exception:
return "127.0.0.1"
ips: List[str] = []
for line in out.splitlines():
parts = line.strip().split()
if len(parts) >= 2 and parts[0] == "inet":
ip = parts[1]
if ip.startswith("127.") or ip.startswith("198.18."):
continue
ips.append(ip)
for ip in ips:
if ip.startswith("192.168.110."):
return ip
for ip in ips:
if ip.startswith("192.168.") or ip.startswith("10."):
return ip
return ips[0] if ips else "127.0.0.1"
def resolve_primary(for_phone: bool = False) -> Tuple[str, str, List[str]]:
"""返回 (primary_ws_base, tier, ordered_ws_bases)。
手机/本机统一顺序BIND-03 · workphone.mdc §零.十):
① NAS 局域网 → ② 本机 Mac Docker 局域网 → ③ NAS 外网 frp
Agent 运行时 TCP 探测,连不上自动切下一候选。
"""
mac_ip = pick_mac_lan_ip()
nas_bases = [(f"ws://{h}:{SDK_PORT}/ws/device", "nas_lan") for h in NAS_LAN_HOSTS]
frp_base = (f"ws://{NAS_FRP_HOST}:{SDK_PORT}/ws/device", "nas_frp")
mac_base = (f"ws://{mac_ip}:{SDK_PORT}/ws/device", "mac_docker") if mac_ip != "127.0.0.1" else None
ordered: List[Tuple[str, str]] = list(nas_bases)
if mac_base:
ordered.append(mac_base)
ordered.append(frp_base)
if for_phone:
# 手机 OTA固定 NAS 为主连接写入 configpublic_servers 按序探测(局域网优先于公网)
primary_ws, tier = nas_bases[0][0], "nas_lan"
bases = [b for b, _ in ordered]
seen = set()
ordered_bases: List[str] = []
for b in bases:
if b not in seen:
seen.add(b)
ordered_bases.append(b)
return primary_ws, tier, ordered_bases
primary_ws, tier = ordered[0][0], ordered[0][1]
for ws_base, t in ordered:
host = ws_base.split("//")[1].split(":")[0]
if _health_ok(host, SDK_PORT):
primary_ws, tier = ws_base, t
break
bases: List[str] = []
seen = set()
for ws_base, _ in ordered:
if ws_base not in seen:
seen.add(ws_base)
bases.append(ws_base)
bases = [primary_ws] + [b for b in bases if b != primary_ws]
return primary_ws, tier, bases
def build_config(device_id: str, for_phone: bool = False) -> dict:
primary_ws, tier, bases = resolve_primary(for_phone=for_phone)
return {
"device_id": device_id,
"server_url": primary_ws,
"public_servers": [b for b in bases if b != primary_ws],
"heartbeat_interval": HEARTBEAT,
"project_id": PROJECT_ID,
"connection_priority": ["nas_lan", "mac_docker", "nas_frp"],
"_meta": {"primary_tier": tier, "generator": "build_agent_config.py"},
}
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--device-id", default="xgfe65eimrrofyws")
ap.add_argument("--out", default="")
ap.add_argument("--print-primary", action="store_true")
ap.add_argument("--for-phone", action="store_true", help="手机 OTANAS→frp→Mac")
args = ap.parse_args()
primary_ws, tier, _ = resolve_primary(for_phone=args.for_phone)
if args.print_primary:
print(primary_ws)
print(tier, file=sys.stderr)
return 0
cfg = build_config(args.device_id, for_phone=args.for_phone)
text = json.dumps(cfg, ensure_ascii=False, indent=2)
if args.out:
with open(args.out, "w", encoding="utf-8") as f:
f.write(text)
else:
print(text)
return 0
if __name__ == "__main__":
raise SystemExit(main())

84
sdk/scripts/build_release.sh Executable file
View File

@@ -0,0 +1,84 @@
#!/bin/bash
# ================================================================
# AI数智员工 统一 APK 构建脚本 v4.0
# ================================================================
# 构建单一 APK 发布包 — 不再包含 Termux/Python Agent
# ================================================================
set -eo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
ANDROID_DIR="${PROJECT_ROOT}/android-app"
OUTPUT_DIR="${PROJECT_ROOT}/release"
echo "╔══════════════════════════════════════╗"
echo "║ AI数智员工 v4.0 构建 ║"
echo "╚══════════════════════════════════════╝"
# 1. Copy hub.html to assets
echo "📦 同步 assets..."
cp -f "${PROJECT_ROOT}/app/static/hub.html" "${ANDROID_DIR}/app/src/main/assets/hub.html" 2>/dev/null || true
cp -f "${PROJECT_ROOT}/agent/hook/wechat_hook_v2.js" "${ANDROID_DIR}/app/src/main/assets/wechat_hook_v2.js" 2>/dev/null || true
cp -f "${PROJECT_ROOT}/agent/hook/wechat_version_compat.json" "${ANDROID_DIR}/app/src/main/assets/wechat_version_compat.json" 2>/dev/null || true
# 2. Build APK
echo "🔨 构建 APK..."
cd "$ANDROID_DIR"
./gradlew assembleRelease --no-daemon
APK_DEBUG="${ANDROID_DIR}/app/build/outputs/apk/debug/app-debug.apk"
APK_RELEASE="${ANDROID_DIR}/app/build/outputs/apk/release/app-release-unsigned.apk"
APK_FILE="${APK_RELEASE}"
[[ ! -f "$APK_FILE" ]] && APK_FILE="$APK_DEBUG"
if [[ ! -f "$APK_FILE" ]]; then
echo "❌ 构建失败"
exit 1
fi
# 3. Package release
mkdir -p "$OUTPUT_DIR"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
RELEASE_DIR="${OUTPUT_DIR}/ai-agent-v4.0-${TIMESTAMP}"
mkdir -p "$RELEASE_DIR"
cp "$APK_FILE" "${RELEASE_DIR}/AI数智员工.apk"
cp "${SCRIPT_DIR}/master_deploy.sh" "${RELEASE_DIR}/"
cat > "${RELEASE_DIR}/README.md" << 'EOF'
# AI数智员工 v4.0 统一安装包
## 安装方式
### 方式一:手动安装
1. 将 `AI数智员工.apk` 传输到手机
2. 在手机上安装 APK
3. 打开 APP → 扫码绑定
### 方式二:批量部署
```bash
./master_deploy.sh --server ws://YOUR_SERVER:8899/ws/device
```
## 功能说明
- ✅ 5 通道连接WebSocket/Hook/ADB/AI Agent/scrcpy
- ✅ 内置防封模块(风控/养号/人性化操作/环境检测/传感器模拟)
- ✅ AI 大脑LLM 决策 + 离线自主运行)
- ✅ 内嵌工作台hub.html
- ✅ Root 可选(有 Root 启用 Frida Hook无 Root 其余通道正常)
## 无需安装
- ❌ 不再需要 Termux
- ❌ 不再需要 Python Agent
- ❌ 不再需要旧版工作手机Agent APP
EOF
cd "$OUTPUT_DIR"
tar czf "ai-agent-v4.0-${TIMESTAMP}.tar.gz" "ai-agent-v4.0-${TIMESTAMP}"
echo ""
echo "✅ 构建完成!"
echo " APK: ${RELEASE_DIR}/AI数智员工.apk"
echo " 包: ${OUTPUT_DIR}/ai-agent-v4.0-${TIMESTAMP}.tar.gz"
echo " 大小: $(du -h "$APK_FILE" | awk '{print $1}')"

50
sdk/scripts/check_sdk.sh Executable file
View File

@@ -0,0 +1,50 @@
#!/bin/bash
#
# SDK 状态检查
# 用法:./scripts/check_sdk.sh
#
echo "============================================"
echo " 工作手机 SDK 状态检查"
echo "============================================"
echo ""
# SDK 健康
echo "[1] SDK 服务 (localhost:8899)"
if curl -s http://localhost:8899/health > /dev/null 2>&1; then
curl -s http://localhost:8899/health | python3 -c "
import json,sys
d=json.load(sys.stdin)
print(' ✅ 运行中')
print(f' devices_online={d.get(\"devices_online\",0)}')
print(f' adb_devices={d.get(\"adb_devices\",0)}')
print(f' adb_serials={d.get(\"adb_serials\",[])}')
" 2>/dev/null || echo " ✅ 运行中"
else
echo " ❌ 未运行"
fi
echo ""
# ADB 设备
echo "[2] ADB 设备"
if command -v adb &> /dev/null; then
if adb devices 2>/dev/null | grep -q "device$"; then
adb devices | grep "device$" | while read line; do echo "$line"; done
else
echo " ⚠️ 无设备"
fi
else
echo " ⚠️ adb 未安装"
fi
echo ""
# 存客宝后端(可选)
echo "[3] 存客宝后端 (localhost:8081)"
if curl -s -X POST http://localhost:8081/v1/auth/login -H "Content-Type: application/json" -d '{"account":"15880802661","password":"kr123456","typeId":1}' 2>/dev/null | grep -q '"code":200'; then
echo " ✅ 运行中(登录成功)"
else
echo " ⚠️ 未运行或登录失败"
fi
echo ""
echo "============================================"

View File

@@ -0,0 +1,252 @@
#!/usr/bin/env bash
# 奥创工作手机 · 真机清机(保留机擎 com.system.cloudservice + Magisk/Frida 基建)
# 真源 Skill机擎/阿服/奥创真机清机/SKILL.md
# 用法:
# bash sdk/scripts/clean_aochuang_device.sh [--dry-run|--apply] [-s SERIAL] [--deep]
# 默认 --dry-run真清须显式 --apply
set -euo pipefail
SERIAL=""
MODE="dry-run"
DEEP=0
EVIDENCE_DIR=""
DEFAULT_SERIAL="xgfe65eimrrofyws"
# 奥创/S2 手机端包(资料真源:奥创微信控制接口与插件提取复用指南.md
AOCHUANG_PACKAGES=(
"org.xeslciw.manager" # XESlciw Manager v1.8.4
"top.zzz.vivwxjz" # VivWxjz 微信 Hook
"uni.UNI9421F6C" # AI数智员工 / 007 业务入口
)
# 可能关联(--deep 才卸)
AOCHUANG_OPTIONAL_PACKAGES=(
"io.github.vvb2060.mahoshojo" # 魔法少女(资料提及同链路工具)
)
KEEP_PACKAGES=(
"com.system.cloudservice" # 机擎 Agent禁止卸
"com.tencent.mm" # 微信(默认保留数据)
)
# 框架/寄生目录(须 root
AOCHUANG_ROOT_PATHS=(
"/data/adb/xesd"
"/data/adb/modules/xeslciw"
"/data/adb/modules/XESlciw"
"/data/adb/modules/grvjfe"
"/data/adb/modules/vivwxjz"
)
# 常见残留 app 分区路径(资料:奥创工作手机-复刻开发详解.md §APK路径
AOCHUANG_APP_DIRS=(
"/data/app/manager_sign"
"/data/app/VivWxjz"
"/data/app/~~*org.xeslciw.manager*"
"/data/app/~~*top.zzz.vivwxjz*"
"/data/app/~~*uni.UNI9421F6C*"
)
usage() {
cat <<'EOF'
奥创真机清机 — clean_aochuang_device.sh
bash sdk/scripts/clean_aochuang_device.sh [选项]
选项:
--dry-run 仅扫描+打印将执行的操作(默认)
--apply 实际清理(须 ADB device + 建议 Root
-s SERIAL 设备序列号(默认唯一在线设备或 xgfe65eimrrofyws
--deep 额外卸 mahoshojo + 清 007 业务 app 数据目录
--evidence DIR 留痕目录(默认 开发文档/8、部署/05-测试验收/YYYYMMDD_奥创清机_<serial>/
保留永不卸com.system.cloudservice、Magisk、机擎 Frida 基建
EOF
}
log() { printf '[%s] %s\n' "$(date '+%H:%M:%S')" "$*"; }
run_adb() { adb -s "$SERIAL" "$@"; }
run_sh() { run_adb shell "$@"; }
pick_su() {
run_sh 'for p in /sbin/su /system/bin/su /system/xbin/su su; do
if [ -x "$p" ] 2>/dev/null || command -v "$p" >/dev/null 2>&1; then echo "$p"; exit 0; fi
done; exit 1' 2>/dev/null || true
}
run_root() {
local cmd="$1"
local su
su="$(pick_su)"
if [[ -z "$su" ]]; then
log "WARN: 无 su跳过 root 操作: $cmd"
return 1
fi
if [[ "$MODE" == "dry-run" ]]; then
log "DRY su -c $(printf '%q' "$cmd")"
return 0
fi
run_sh "$su -c $(printf '%q' "$cmd")" || return 1
}
pkg_installed() {
local pkg="$1"
run_sh pm path "$pkg" 2>/dev/null | grep -q .
}
do_uninstall_pkg() {
local pkg="$1"
if ! pkg_installed "$pkg"; then
log "SKIP 未安装: $pkg"
return 0
fi
if [[ "$MODE" == "dry-run" ]]; then
log "DRY pm uninstall --user 0 $pkg"
log "DRY pm uninstall $pkg"
return 0
fi
log "卸载: $pkg"
run_sh pm disable-user --user 0 "$pkg" 2>/dev/null || true
run_sh am force-stop "$pkg" 2>/dev/null || true
run_sh pm uninstall --user 0 "$pkg" 2>/dev/null || run_sh pm uninstall "$pkg" 2>/dev/null || log "WARN 卸载失败: $pkg"
}
clear_pkg_data() {
local pkg="$1"
if [[ "$MODE" == "dry-run" ]]; then
log "DRY pm clear $pkg"
return 0
fi
run_sh pm clear "$pkg" 2>/dev/null || true
}
remove_root_path() {
local p="$1"
if [[ "$MODE" == "dry-run" ]]; then
log "DRY rm -rf $p"
return 0
fi
run_root "rm -rf $(printf '%q' "$p")" || true
}
scan_magisk_modules() {
log "== Magisk 模块扫描(名称含 xes/grvjfe/vivwx/lsposed 且非机擎) =="
run_root 'ls -1 /data/adb/modules 2>/dev/null' | while read -r m; do
[[ -z "$m" ]] && continue
if echo "$m" | grep -qiE 'xes|grvjfe|vivwx|lsposed|edxposed|xposed'; then
if echo "$m" | grep -qiE 'shamiko|zygisk|rezygisk|riru'; then
log "KEEP Magisk模块(机擎基建): $m"
else
if [[ "$MODE" == "dry-run" ]]; then
log "DRY 将禁用并移除模块: $m"
else
log "移除 Magisk 模块: $m"
run_root "touch /data/adb/modules/$m/disable" || true
run_root "rm -rf /data/adb/modules/$m" || true
fi
fi
fi
done || true
}
write_evidence() {
[[ -z "$EVIDENCE_DIR" ]] && return 0
mkdir -p "$EVIDENCE_DIR"
{
echo "# 奥创清机留痕 $(date '+%Y-%m-%d %H:%M:%S')"
echo "serial=$SERIAL mode=$MODE deep=$DEEP"
echo ""
echo "## pm list packages (filter)"
run_sh pm list packages 2>/dev/null | grep -iE 'xes|zzz|vivwx|UNI9421|mahoshojo|cloudservice' || true
echo ""
echo "## xesd"
run_sh "ls -la /data/adb/xesd/ 2>/dev/null || echo NO_XESD" || true
} >"$EVIDENCE_DIR/clean_report.txt"
log "留痕: $EVIDENCE_DIR/clean_report.txt"
}
while [[ $# -gt 0 ]]; do
case "$1" in
--apply) MODE="apply" ;;
--dry-run) MODE="dry-run" ;;
-s) SERIAL="${2:-}"; shift ;;
--deep) DEEP=1 ;;
--evidence) EVIDENCE_DIR="${2:-}"; shift ;;
-h|--help) usage; exit 0 ;;
*) echo "未知参数: $1"; usage; exit 1 ;;
esac
shift
done
if [[ -z "$SERIAL" ]]; then
SERIAL="$(adb devices 2>/dev/null | awk 'NR>1 && $2=="device" {print $1; exit}')"
[[ -z "$SERIAL" ]] && SERIAL="$DEFAULT_SERIAL"
fi
if ! adb devices 2>/dev/null | awk -v s="$SERIAL" '$1==s && $2=="device" {found=1} END{exit !found}'; then
echo "FAIL: 设备 $SERIAL 未在线adb devices 无 device"
exit 1
fi
if [[ -z "$EVIDENCE_DIR" ]]; then
EVIDENCE_DIR="$(cd "$(dirname "$0")/../.." && pwd)/开发文档/8、部署/05-测试验收/$(date '+%Y%m%d')_奥创清机_${SERIAL}"
fi
log "== 奥创真机清机 mode=$MODE serial=$SERIAL deep=$DEEP =="
for k in "${KEEP_PACKAGES[@]}"; do
if pkg_installed "$k"; then
log "KEEP 保留: $k"
fi
done
log "== 1/6 停止并卸载奥创 APK =="
for pkg in "${AOCHUANG_PACKAGES[@]}"; do
do_uninstall_pkg "$pkg"
done
if [[ "$DEEP" == "1" ]]; then
for pkg in "${AOCHUANG_OPTIONAL_PACKAGES[@]}"; do
do_uninstall_pkg "$pkg"
done
fi
log "== 2/6 清理 app 数据(已卸包) =="
for pkg in "${AOCHUANG_PACKAGES[@]}"; do
clear_pkg_data "$pkg"
done
log "== 3/6 移除框架目录 /data/adb/xesd 等 =="
for p in "${AOCHUANG_ROOT_PATHS[@]}"; do
remove_root_path "$p"
done
log "== 4/6 Magisk 奥创相关模块 =="
scan_magisk_modules
log "== 5/6 残留分区路径 =="
for p in "${AOCHUANG_APP_DIRS[@]}"; do
if [[ "$p" == *'*'* ]]; then
if [[ "$MODE" == "dry-run" ]]; then
log "DRY rm -rf $p"
else
run_root "rm -rf $p" 2>/dev/null || true
fi
else
remove_root_path "$p"
fi
done
log "== 6/6 重启提示 =="
if [[ "$MODE" == "apply" ]]; then
log "建议: adb -s $SERIAL rebootMagisk 模块变更后)"
fi
write_evidence
if [[ "$MODE" == "dry-run" ]]; then
log "完成dry-run。真清请: bash sdk/scripts/clean_aochuang_device.sh --apply -s $SERIAL"
log "验收: bash sdk/scripts/verify_aochuang_clean.sh -s $SERIAL"
else
log "完成apply。请跑验收脚本确认全绿。"
fi

View File

@@ -0,0 +1,210 @@
#!/usr/bin/env bash
# 宿主机连接守护ADB + WS Agent 常驻保活(处理长任务时也不断链)
# 用法:
# bash sdk/scripts/connection_keeper_daemon.sh # 前台
# bash sdk/scripts/connection_keeper_daemon.sh --bg # 后台
# bash sdk/scripts/connection_keeper_daemon.sh --once # 只跑一轮稳定化
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SDK_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
LAN_ENV="${SDK_ROOT}/config/lan.env"
REQUESTED_ADB_PORT="${ANDROID_ADB_SERVER_PORT:-}"
[[ -f "${LAN_ENV}" ]] && source "${LAN_ENV}" || true
[[ -n "${REQUESTED_ADB_PORT}" ]] && export ANDROID_ADB_SERVER_PORT="${REQUESTED_ADB_PORT}"
SDK_PORT="${WORKPHONE_SDK_PORT:-8899}"
INTERVAL="${KEEPER_INTERVAL:-15}"
FAST_INTERVAL="${KEEPER_FAST_INTERVAL:-8}"
DEVICE_SERIAL="${DEVICE_SERIAL:-}"
DEVICE_ID="${DEVICE_ID:-xgfe65eimrrofyws}"
export ANDROID_ADB_SERVER_PORT="${ANDROID_ADB_SERVER_PORT:-5037}"
ADB=(adb -P "${ANDROID_ADB_SERVER_PORT}")
LOG="${SDK_ROOT}/logs/connection_keeper.log"
PID_FILE="${SDK_ROOT}/logs/connection_keeper.pid"
mkdir -p "${SDK_ROOT}/logs"
log() { echo "[$(date '+%F %T')] $*" | tee -a "$LOG"; }
is_phone_serial() {
local s="$1"
local brand
brand=$("${ADB[@]}" -s "$s" shell getprop ro.product.brand 2>/dev/null | tr -d '\r')
[[ "$brand" != "haier" ]]
}
pick_serial() {
if [[ -n "$DEVICE_SERIAL" ]]; then
if "${ADB[@]}" -s "$DEVICE_SERIAL" get-state 2>/dev/null | grep -qx device && is_phone_serial "$DEVICE_SERIAL"; then
echo "$DEVICE_SERIAL"
return
fi
fi
local s
while IFS= read -r s; do
[[ -z "$s" ]] && continue
is_phone_serial "$s" || continue
echo "$s"
return
done < <("${ADB[@]}" devices 2>/dev/null | awk '/\tdevice$/{print $1}')
}
usb_xiaomi_present() {
ioreg -p IOUSB -l 2>/dev/null | grep -q "Xiaomi" 2>/dev/null
}
try_wireless_ports() {
local host="${1:-192.168.1.12}"
if usb_xiaomi_present; then
log "USB Xiaomi detected → skip wireless port scan (优先等 MTP/授权)"
return 1
fi
ping -c 1 -W 2 "$host" >/dev/null 2>&1 || return 1
"${ADB[@]}" reconnect offline >/dev/null 2>&1 || true
local p open_port="" hits=0
for p in 5555 $(seq 37000 37005); do
if nc -z -G 1 "$host" "$p" 2>/dev/null; then
hits=$((hits + 1))
[[ "$hits" -gt 3 ]] && break
log "wireless port open ${host}:${p} → adb connect"
( "${ADB[@]}" connect "${host}:${p}" & pid=$!; sleep 3; kill $pid 2>/dev/null; wait $pid 2>/dev/null ) || true
if "${ADB[@]}" -s "${host}:${p}" get-state 2>/dev/null | grep -qx device; then
DEVICE_SERIAL="${host}:${p}"
echo "$DEVICE_SERIAL"
return 0
fi
"${ADB[@]}" disconnect "${host}:${p}" >/dev/null 2>&1 || true
fi
done
return 1
}
ws_online_count() {
curl -s "http://127.0.0.1:${SDK_PORT}/api/v3/connection/status" 2>/dev/null | \
python3 -c "import sys,json;d=json.load(sys.stdin).get('data',{});print(d.get('online_ws_count',0))" 2>/dev/null || echo 0
}
u2_online() {
curl -s "http://127.0.0.1:${SDK_PORT}/api/v3/devices/${DEVICE_ID}" 2>/dev/null | \
python3 -c "import sys,json;d=json.load(sys.stdin).get('data',{});print('1' if d.get('quick_status',{}).get('u2') else '0')" 2>/dev/null || echo 0
}
ensure_adb_server() {
if nc -z 127.0.0.1 "${ANDROID_ADB_SERVER_PORT}" 2>/dev/null; then
return 0
fi
"${ADB[@]}" start-server 2>/dev/null || true
sleep 1
}
adb_devices_safe() {
( "${ADB[@]}" devices "$@" & pid=$!; sleep 8; kill "$pid" 2>/dev/null; wait "$pid" 2>/dev/null ) 2>/dev/null || true
}
keeper_once() {
ensure_adb_server
ws="$(ws_online_count)"
u2_ok="$(u2_online)"
if [[ "$ws" -ge 1 && "$u2_ok" == "1" ]]; then
log "ok ws_online=${ws} u2=1"
return 0
fi
if [[ "$ws" -ge 1 ]]; then
log "ws_online=${ws} but u2=${u2_ok} → continue adb/u2 recovery"
fi
serial="$(pick_serial || true)"
if [[ -z "$serial" ]]; then
if usb_xiaomi_present; then
log "USB 已插入但未 adb device → 请在手机选「传输文件」并允许 USB 调试;或开启无线调试"
# USB 未授权时仍尝试无线 ADB与 USB 并行)
for ip in ${WIRELESS_IPS:-192.168.110.80 192.168.3.1}; do
if serial="$(try_wireless_ports "$ip" || true)" && [[ -n "$serial" ]]; then
log "wireless adb ok ${serial}"
break
fi
done
serial="$(pick_serial || true)"
else
for ip in ${WIRELESS_IPS:-192.168.110.80 192.168.110.60 192.168.110.197 192.168.3.1 192.168.1.12}; do
if serial="$(try_wireless_ports "$ip" || true)" && [[ -n "$serial" ]]; then
break
fi
( "${ADB[@]}" connect "${ip}:5555" & pid=$!; sleep 3; kill "$pid" 2>/dev/null; wait "$pid" 2>/dev/null ) || true
done
serial="$(pick_serial || true)"
fi
fi
if [[ -z "$serial" ]]; then
log "no adb device — 若手机 Termux Agent 已配置 NAS/frp 寻服,可忽略;否则 USB 授权一次做 OTA"
bash "${SCRIPT_DIR}/sdk_connection_watcher.sh" --once >>"$LOG" 2>&1 || true
ws="$(ws_online_count)"
[[ "$ws" -ge 1 ]] && log "sdk_watcher recovered ws=${ws}" && return 0
return 1
fi
state="$("${ADB[@]}" -s "$serial" get-state 2>/dev/null || echo offline)"
if [[ "$state" == "unauthorized" ]]; then
log "adb unauthorized serial=${serial} → 请在手机点「允许 USB 调试」"
return 1
fi
if [[ "$state" != "device" ]]; then
log "adb not ready serial=${serial} state=${state}"
return 1
fi
ws="$(ws_online_count)"
if [[ "$ws" -ge 1 ]]; then
log "ok serial=${serial} ws_online=${ws}"
return 0
fi
log "adb ok but ws=0 → termux/on-device agent serial=${serial}"
DEVICE_SERIAL="$serial" DEVICE_ID="${DEVICE_ID:-xgfe65eimrrofyws}" \
bash "${SCRIPT_DIR}/termux_push_and_start_agent.sh" >>"$LOG" 2>&1 || true
ws="$(ws_online_count)"
if [[ "$ws" -ge 1 ]]; then
log "termux/oneclick ok ws_online=${ws}"
return 0
fi
log "still ws=0 after termux_push"
return 1
}
keeper_loop() {
log "connection_keeper start interval=${INTERVAL}s fast=${FAST_INTERVAL}s port=${SDK_PORT} lan=${WORKPHONE_LAN_IP:-auto}"
while true; do
if keeper_once; then
sleep "$INTERVAL"
else
sleep "$FAST_INTERVAL"
fi
done
}
case "${1:-}" in
--once)
keeper_once
;;
--bg)
if [[ -f "$PID_FILE" ]] && kill -0 "$(cat "$PID_FILE")" 2>/dev/null; then
echo "已在运行 PID $(cat "$PID_FILE")"
exit 0
fi
nohup bash "$0" >>"$LOG" 2>&1 &
echo $! >"$PID_FILE"
echo "connection_keeper 后台 PID $(cat "$PID_FILE") log=$LOG"
;;
--stop)
if [[ -f "$PID_FILE" ]]; then
kill "$(cat "$PID_FILE")" 2>/dev/null || true
rm -f "$PID_FILE"
echo "已停止"
fi
;;
*)
keeper_loop
;;
esac

View File

@@ -0,0 +1,48 @@
#!/usr/bin/env bash
set -euo pipefail
TARGET="${TARGET:-cunkebao}"
REMOTE_DIR="${REMOTE_DIR:-/www/wwwroot/workphone-sdk}"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SDK_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
ssh "$TARGET" "mkdir -p '$REMOTE_DIR/app' '$REMOTE_DIR/agent' '$REMOTE_DIR/deploy/baota'"
rsync -az --delete \
--exclude '__pycache__' --exclude '*.pyc' --exclude '*.log' \
--exclude 'data/cunke_bao_config.json' --exclude 'data/hook/events.jsonl' \
--exclude 'data/operation_logs' --exclude 'agent/config.json' \
"$SDK_ROOT/app/" "$TARGET:$REMOTE_DIR/app/"
rsync -az --delete \
--exclude '__pycache__' --exclude '*.pyc' --exclude '*.log' \
--exclude 'config.json' --exclude 'config.*.json' \
"$SDK_ROOT/agent/" "$TARGET:$REMOTE_DIR/agent/"
rsync -az \
"$SDK_ROOT/Dockerfile" "$SDK_ROOT/.dockerignore" "$SDK_ROOT/requirements.txt" \
"$SDK_ROOT/docker-compose.baota.yml" "$TARGET:$REMOTE_DIR/"
rsync -az "$SDK_ROOT/deploy/baota/wpsdk.quwanzhi.com.conf" "$TARGET:$REMOTE_DIR/deploy/baota/"
ssh "$TARGET" "REMOTE_DIR='$REMOTE_DIR' bash -s" <<'REMOTE'
set -euo pipefail
cd "$REMOTE_DIR"
if [[ ! -f .env ]]; then
umask 077
mongo_password="$(openssl rand -hex 24)"
api_key="$(openssl rand -hex 32)"
pairing_token="$(openssl rand -hex 32)"
printf 'MONGO_ROOT_USERNAME=workphone\nMONGO_ROOT_PASSWORD=%s\nAPI_KEY=%s\nDEVICE_PAIRING_TOKEN=%s\nAI_BRAIN_ENABLED=false\nAI_BRAIN_API_URL=https://kr-ai.quwanzhi.com\nAI_BRAIN_API_KEY=\n' \
"$mongo_password" "$api_key" "$pairing_token" > .env
fi
chmod 600 .env
docker compose -f docker-compose.baota.yml up -d --build
install -d -m 755 /www/server/panel/vhost/cert/wpsdk.quwanzhi.com
install -m 644 /root/.acme.sh/quwanzhi.com_ecc/fullchain.cer /www/server/panel/vhost/cert/wpsdk.quwanzhi.com/fullchain.pem
install -m 600 /root/.acme.sh/quwanzhi.com_ecc/quwanzhi.com.key /www/server/panel/vhost/cert/wpsdk.quwanzhi.com/privkey.pem
install -m 644 deploy/baota/wpsdk.quwanzhi.com.conf /www/server/panel/vhost/nginx/wpsdk.quwanzhi.com.conf
/www/server/nginx/sbin/nginx -t
/www/server/nginx/sbin/nginx -s reload
REMOTE
echo "部署完成https://wpsdk.quwanzhi.com/health"

View File

@@ -0,0 +1,129 @@
#!/usr/bin/env bash
# 家里 Station NAS192.168.110.29 · DS213j · 无 Docker部署工作手机网站
#
# 用法Mac 与 NAS 同网):
# export NAS_HOST=192.168.110.29 NAS_USER=admin NAS_PASS='zhiqun1984'
# bash sdk/scripts/deploy_home_station.sh
#
# 访问(部署成功后):
# 内网(最稳) http://192.168.110.29:8899/hub
# 公网 HTTPS https://workphone.quwanzhi.com/hub 推荐kr Nginx 443 → frp 8889 → NAS 8899
# 公网 HTTP http://workphone.quwanzhi.com/hub (部分网络/Clash 下 80 可能 Empty reply
# frp 直连 http://opennas2.quwanzhi.com:8889/hub
# API Key workphone-secret-keyHeader: X-API-Key
#
# 外网验收注意:
# - kr 本机 curl 127.0.0.1 + Host 已 200链路正常
# - Mac 开 Clash 时域名可能解析到 fake-ip198.18.x→ 请加 DIRECT 或 --resolve
# - 腾讯云安全组 API 凭证失效时需控制台手动放行 80/443/8889
set -euo pipefail
NAS_HOST="${NAS_HOST:-192.168.110.29}"
NAS_USER="${NAS_USER:-admin}"
NAS_PASS="${NAS_PASS:-zhiqun1984}"
NAS_SDK_DIR="${NAS_SDK_DIR:-/volume1/web/workphone-sdk}"
NAS_CKB_WEB="${NAS_CKB_WEB:-/volume1/web/cunkebao}"
SDK_PORT="${SDK_PORT:-8899}"
CKB_WEB_PORT="${CKB_WEB_PORT:-8800}"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SDK_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
REPO_ROOT="$(cd "$SDK_ROOT/.." && pwd)"
CKB_ROOT="$(cd "$REPO_ROOT/../cunkebao_v3" 2>/dev/null && pwd || echo "")"
SSH_OPTS=(-o StrictHostKeyChecking=no -o ConnectTimeout=15
-o KexAlgorithms=+diffie-hellman-group1-sha1
-o Ciphers=+aes128-cbc,3des-cbc,aes192-cbc,aes256-cbc)
ssh_nas() {
sshpass -p "$NAS_PASS" ssh "${SSH_OPTS[@]}" "${NAS_USER}@${NAS_HOST}" "$@"
}
rsync_nas() {
sshpass -p "$NAS_PASS" rsync -avz -e "ssh ${SSH_OPTS[*]}" "$@"
}
echo ">>> 探测家里 NAS ${NAS_HOST}"
ssh_nas "hostname; free -m | head -2"
echo ">>> 同步工作手机 SDK -> ${NAS_SDK_DIR}"
ssh_nas "mkdir -p '${NAS_SDK_DIR}'"
rsync_nas \
--exclude '.git' --exclude '__pycache__' --exclude '*.pyc' --exclude 'node_modules' \
--exclude '.venv' --exclude 'venv' --exclude '.pytest_cache' \
--exclude 'android-app' --exclude 'admin' --exclude 'phone' \
--exclude 'scripts/gadget_work' --exclude 'scripts/**/*.apk' \
--exclude 'data/operation_logs' --exclude '*.md' \
"${SDK_ROOT}/app" "${SDK_ROOT}/scripts" \
"${SDK_ROOT}/requirements.nas-lite.txt" "${SDK_ROOT}/requirements.txt" \
"${SDK_ROOT}/Dockerfile" "${SDK_ROOT}/docker-compose.nas.yml" \
"${NAS_USER}@${NAS_HOST}:${NAS_SDK_DIR}/"
echo ">>> 安装 Python 依赖NAS 精简版)…"
ssh_nas "bash -s" <<REMOTE
set -e
export PATH="/var/services/homes/admin/.local/bin:/usr/local/bin:\$PATH"
PY=/usr/local/bin/python3
SDK_DIR='${NAS_SDK_DIR}'
cd "\$SDK_DIR"
"\$PY" -m pip install --user -q -r requirements.nas-lite.txt 2>&1 | tail -5 || true
REMOTE
echo ">>> 写入启动脚本 …"
ssh_nas "bash -s" <<REMOTE
set -e
SDK_DIR='${NAS_SDK_DIR}'
PORT='${SDK_PORT}'
cat > "\$SDK_DIR/start_workphone.sh" <<'EOS'
#!/bin/sh
export PATH="/var/services/homes/admin/.local/bin:/usr/local/bin:\$PATH"
cd __SDK_DIR__/app
export ENV=production
export API_KEY=REPLACE_WITH_A_RANDOM_SECRET
export AI_BRAIN_ENABLED=false
# 家里 NAS 无 Mongo/Redis 容器SDK 可降级运行
export MONGO_URI=
export REDIS_URL=
exec /usr/local/bin/python3 -m uvicorn main:app --host 0.0.0.0 --port __PORT__
EOS
sed -i "s|__SDK_DIR__|\$SDK_DIR|g; s|__PORT__|\$PORT|g" "\$SDK_DIR/start_workphone.sh"
chmod +x "\$SDK_DIR/start_workphone.sh"
# 停止旧进程
pkill -f "uvicorn main:app.*${SDK_PORT}" 2>/dev/null || true
sleep 1
nohup "\$SDK_DIR/start_workphone.sh" > "\$SDK_DIR/workphone.log" 2>&1 &
sleep 8
REMOTE
echo ">>> 健康检查 …"
if ssh_nas "wget -qO- http://127.0.0.1:${SDK_PORT}/health 2>/dev/null | head -c 200"; then
echo ""
echo "✅ 工作手机 SDK 已启动"
else
echo "⚠️ health 未响应,查看日志:"
ssh_nas "tail -30 '${NAS_SDK_DIR}/workphone.log' 2>/dev/null || true"
fi
# 存客宝前端(可选:本地构建后 rsync
if [[ -d "${CKB_ROOT}/Cunkebao" ]]; then
echo ">>> 构建存客宝前端(本机)…"
if command -v pnpm >/dev/null 2>&1; then
(cd "${CKB_ROOT}/Cunkebao" && pnpm install --silent 2>/dev/null && VITE_API_BASE_URL="" pnpm run build 2>&1 | tail -5) || echo "存客宝构建跳过pnpm 失败)"
fi
if [[ -d "${CKB_ROOT}/Cunkebao/dist" ]]; then
echo ">>> 同步存客宝静态站到 NAS ${NAS_CKB_WEB}"
ssh_nas "mkdir -p '${NAS_CKB_WEB}'"
rsync_nas -a --delete "${CKB_ROOT}/Cunkebao/dist/" "${NAS_USER}@${NAS_HOST}:${NAS_CKB_WEB}/"
echo " 存客宝静态http://${NAS_HOST}:${CKB_WEB_PORT}/ (需 WebStation/反代,或 python -m http.server"
fi
fi
echo ""
echo "=============================================="
echo " 家里 NAS 部署完成"
echo " 工作手机控制台: http://${NAS_HOST}:${SDK_PORT}/hub"
echo " SDK API / 文档: http://${NAS_HOST}:${SDK_PORT}/docs"
echo " 请在 DSM 防火墙放行 TCP ${SDK_PORT}"
echo "=============================================="

90
sdk/scripts/deploy_nas_rsync.sh Executable file
View File

@@ -0,0 +1,90 @@
#!/usr/bin/env bash
# 从本机Mac将 sdk 同步到公司 NAS 并执行 docker compose 部署
#
# 【铁律 · 必守】workphone.mdc §零.十 / 机擎 §〇.十
# 连接顺序:① NAS 局域网 :8899 → ② 本机 Docker → ③ NAS 外网 open.quwanzhi.com:8899
# 部署路径:/volume1/docker/workphone-sdk · DSM 05-业务网站-工作手机
# 部署后bash sdk/scripts/nas_primary_agent_setup.sh
#
# 使用前请安装 OpenSSH 服务端于 NAS并开启 Docker/Container Manager。
#
# 用法:
# export NAS_USER=fnvtk # 可选,默认 fnvtk
# export NAS_HOST=192.168.110.101 # 可选,默认公司 NAS 局域网
# export NAS_DIR=/volume1/docker/workphone-sdk # 可选
# # fnvtk 用户无直接 docker 权限,但 sudo docker NOPASSWDSynology 默认 sudoers
# # 若 sudo docker 也无权限,用 NAS_SUDO_PASSWORD 授权 docker.sock
# export NAS_SUDO_PASSWORD='你的DSM密码'
# bash sdk/scripts/deploy_nas_rsync.sh
#
# 可选:同步「开发文档」到 NAS 知识库挂载目录(需先在 docker-compose.nas.yml 取消注释挂载)
# export SYNC_DEV_DOCS=1
# export NAS_DOCS_DIR=/volume1/docker/workphone-docs
set -euo pipefail
NAS_HOST="${NAS_HOST:-192.168.110.101}"
NAS_USER="${NAS_USER:-fnvtk}"
NAS_DIR="${NAS_DIR:-/volume1/docker/workphone-sdk}"
# fnvtk 无直接 docker 权限compose 命令前需 sudoSynology sudoers NOPASSWD for docker
DOCKER_CMD="${DOCKER_CMD:-sudo docker}"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SDK_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
REPO_ROOT="$(cd "$SDK_ROOT/.." && pwd)"
if [[ -n "${NAS_SUDO_PASSWORD:-}" ]]; then
echo ">>> 修正 docker.sock 权限sudo与 DSM 登录密码一致)…"
if ! ssh -o StrictHostKeyChecking=no "${NAS_USER}@${NAS_HOST}" \
"sudo -S sh -c 'chgrp administrators /var/run/docker.sock && chmod g+rw /var/run/docker.sock && ls -la /var/run/docker.sock'" \
<<< "$NAS_SUDO_PASSWORD"; then
echo "sudo 失败:请确认 NAS_SUDO_PASSWORD 为 ${NAS_USER} 的 DSM 登录密码。"
exit 1
fi
fi
echo ">>> rsync sdk/ -> ${NAS_USER}@${NAS_HOST}:${NAS_DIR}/"
ssh "${NAS_USER}@${NAS_HOST}" "mkdir -p '${NAS_DIR}'"
# 严格排除非镜像构建所需目录(.runtime 含 46w+ 缓存文件android-app/typescript-sdk/php-sdk 不进镜像)
rsync -avz \
--exclude '.git' \
--exclude '.env' \
--exclude '.env.*' \
--exclude '*secret*' \
--exclude '*credential*' \
--exclude '__pycache__' \
--exclude '*.pyc' \
--exclude 'node_modules' \
--exclude '.runtime' \
--exclude '.pytest_cache' \
--exclude '.gradle' \
--exclude 'android-app' \
--exclude 'typescript-sdk' \
--exclude 'php-sdk' \
--exclude 'tests' \
--exclude 'data/operation_logs' \
--exclude 'data/e2e_report.json' \
--exclude 'logs/*.log' \
"${SDK_ROOT}/" "${NAS_USER}@${NAS_HOST}:${NAS_DIR}/"
if [[ "${SYNC_DEV_DOCS:-0}" == "1" ]]; then
NAS_DOCS_DIR="${NAS_DOCS_DIR:-/volume1/docker/workphone-docs}"
echo ">>> rsync 开发文档/ -> ${NAS_USER}@${NAS_HOST}:${NAS_DOCS_DIR}/开发文档/"
if [[ ! -d "${REPO_ROOT}/开发文档" ]]; then
echo "未找到 ${REPO_ROOT}/开发文档,跳过。"
else
ssh "${NAS_USER}@${NAS_HOST}" "mkdir -p '${NAS_DOCS_DIR}'"
rsync -avz "${REPO_ROOT}/开发文档/" "${NAS_USER}@${NAS_HOST}:${NAS_DOCS_DIR}/开发文档/"
fi
fi
echo ">>> 远程构建并启动容器…"
# /usr/syno/bin 在 PATH 前部Synology docker binary 位置sudo 前置fnvtk 无直接 docker 权限)
ssh -o StrictHostKeyChecking=no "${NAS_USER}@${NAS_HOST}" \
"export PATH=\"/usr/local/bin:/usr/syno/bin:\$PATH\" && cd '${NAS_DIR}' && ${DOCKER_CMD} compose -f docker-compose.nas.yml up -d --build"
echo ""
echo "部署命令已下发。内网访问地址(将 NAS_HOST 换成实际 IP"
echo " 控制台: http://${NAS_HOST}:8899/hub"
echo " SDK API: http://${NAS_HOST}:8899"
echo ""
echo "若页面打不开:检查 NAS 防火墙是否放行 8899DSM「登录门户」勿占用同端口。"

View File

@@ -0,0 +1,96 @@
#!/usr/bin/env bash
# Docker Desktop 引擎假死自愈macOS
# 场景com.docker.backend 在跑,但 Linux 引擎无响应 → 无鲸鱼 / 打不开界面 / docker 报 500
# 策略:仅当「后端进程存在 && docker info 在时限内失败」时执行 docker desktop restart避免用户完全退出 Docker 时被误拉起。
#
# 用法:
# bash sdk/scripts/docker_engine_guard.sh # 检查一次,必要时修复
# bash sdk/scripts/docker_engine_guard.sh --dry-run # 只打印将执行的操作,不重启
# bash sdk/scripts/docker_engine_guard.sh --open-ui # 修复后尝试打开仪表盘(适合手动排障)
set -uo pipefail
# launchd 默认 PATH 极窄,需显式加入 Docker CLI 常见路径
export PATH="/opt/homebrew/bin:/usr/local/bin:${HOME}/.docker/bin:/usr/bin:/bin:/usr/sbin:/sbin:${PATH:-}"
DRY_RUN=false
OPEN_UI=false
for arg in "$@"; do
case "$arg" in
--dry-run) DRY_RUN=true ;;
--open-ui) OPEN_UI=true ;;
esac
done
if [[ "$(uname -s)" != "Darwin" ]]; then
exit 0
fi
if [[ ! -d "/Applications/Docker.app" ]]; then
exit 0
fi
if ! command -v docker >/dev/null 2>&1; then
exit 0
fi
log_line() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"
}
# 后端未运行:视为用户未开 Docker不处理
if ! pgrep -f "com.docker.backend" >/dev/null 2>&1; then
log_line "skip: com.docker.backend 未运行"
exit 0
fi
# 限时探测 docker info避免 launchd 任务挂死,最多约 15s
info_ok=false
docker info >/dev/null 2>&1 &
dp=$!
for _ in $(seq 1 15); do
if ! kill -0 "$dp" 2>/dev/null; then
if wait "$dp"; then info_ok=true; fi
break
fi
sleep 1
done
if kill -0 "$dp" 2>/dev/null; then
kill -9 "$dp" 2>/dev/null || true
wait "$dp" 2>/dev/null || true
fi
if [[ "$info_ok" == true ]]; then
log_line "ok: docker info 正常"
exit 0
fi
log_line "bad: docker info 失败但后端在跑 → 判定引擎假死"
if [[ "$DRY_RUN" == true ]]; then
log_line "dry-run: 将执行 docker desktop restart"
exit 2
fi
if docker desktop restart 2>&1; then
log_line "done: docker desktop restart 已执行,等待引擎…"
else
log_line "warn: docker desktop restart 返回非 0"
fi
for _ in $(seq 1 60); do
if docker info >/dev/null 2>&1; then
log_line "ok: 引擎已恢复"
if [[ "$OPEN_UI" == true ]]; then
open "/Applications/Docker.app/Contents/MacOS/Docker Desktop.app" 2>/dev/null || open -a Docker
sleep 1
open "docker-desktop://dashboard" 2>/dev/null || true
log_line "open-ui: 已尝试打开仪表盘"
fi
exit 0
fi
sleep 2
done
log_line "fail: 重启后 docker info 仍失败,请人工:升级 Docker Desktop 或 Troubleshoot → Reset"
exit 1

57
sdk/scripts/ensure_ws_agent.sh Executable file
View File

@@ -0,0 +1,57 @@
#!/usr/bin/env bash
# 确保 WebSocket Agent + Phantom Frida 在线存客宝对接口径WS+Frida非 ADB UI
#
# ⚠️ 无线主控请改用 ensure_ws_agent_wireless.sh — 本脚本在 Mac 运行,依赖 ADB forward。
set -euo pipefail
SDK_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
DEVICE_SERIAL="${DEVICE_SERIAL:-192.168.110.80:5555}"
SDK_PORT="${SDK_PORT:-8899}"
PH_CFG="${SDK_ROOT}/scripts/anti_detect/phantom_frida_config.json"
PH_KEEPALIVE="${SDK_ROOT}/scripts/phantom_frida_keepalive.sh"
LOG="/tmp/wp_agent_ws.log"
if [[ ! -f "$PH_CFG" ]]; then
echo "缺少 phantom 配置,先运行: bash scripts/frida_workphone_oneclick.sh -d ${DEVICE_SERIAL}"
exit 1
fi
PH_PORT=$(python3 -c "import json; print(json.load(open('${PH_CFG}'))['listen_port'])")
# 只允许一个读同一 phantom 配置的保活器,避免 Frida 重启后回到默认/随机端口。
if ! pgrep -f "${PH_KEEPALIVE} ${DEVICE_SERIAL}" >/dev/null 2>&1; then
nohup bash "${PH_KEEPALIVE}" "${DEVICE_SERIAL}" \
>> /tmp/phantom_frida_keepalive.log 2>&1 &
fi
adb -s "${DEVICE_SERIAL}" forward "tcp:${PH_PORT}" "tcp:${PH_PORT}" >/dev/null 2>&1 || true
pkill -f "agent.py -d ${DEVICE_SERIAL} -s ws://127.0.0.1:${SDK_PORT}" 2>/dev/null || true
sleep 1
# Frida attach 需要微信进程
WX_PID="$(adb -s "${DEVICE_SERIAL}" shell pidof com.tencent.mm 2>/dev/null | tr -d '\r' || true)"
if [[ -z "${WX_PID}" ]]; then
echo "拉起微信..."
adb -s "${DEVICE_SERIAL}" shell am start -n com.tencent.mm/.ui.LauncherUI >/dev/null 2>&1 || true
sleep 6
fi
cd "${SDK_ROOT}/agent"
export WP_FRIDA_MODE=remote
export WP_DEVICE_SERIAL="${DEVICE_SERIAL}"
export WP_FRIDA_PORT="${PH_PORT}"
nohup python3 agent.py \
-d "${DEVICE_SERIAL}" \
-s "ws://127.0.0.1:${SDK_PORT}/ws/device" \
--heartbeat 10 >> "${LOG}" 2>&1 &
echo "Agent pid=$! device=${DEVICE_SERIAL} frida_port=${PH_PORT}"
for _i in $(seq 1 20); do
if grep -q "RPC ping: pong" "${LOG}" 2>/dev/null; then
echo "frida_rpc_ready"
break
fi
sleep 2
done
curl -s "http://127.0.0.1:${SDK_PORT}/api/v3/connection/status" | python3 -c "
import sys,json
d=json.load(sys.stdin).get('data',{})
print('ws_online', d.get('online_ws_count'), d.get('online_device_ids'))
"

View File

@@ -0,0 +1,37 @@
#!/usr/bin/env bash
# 无线主控:设备端 Termux Agent + 本机 Frida无需 Mac ADB
# 用法: bash ensure_ws_agent_wireless.sh [SDK_HOST:PORT] [device_id]
set -euo pipefail
SDK_HOST="${1:-127.0.0.1:8899}"
DEVICE_ID="${2:-xgfe65eimrrofyws}"
WS_URL="ws://${SDK_HOST}/ws/device/${DEVICE_ID}"
echo "================================================================"
echo " 工作手机 · 无线主控WebSocket + 设备本机 Frida"
echo " 不需要 Mac USB ADBexecute/probe 全走 WS"
echo "================================================================"
echo ""
echo "【请在红米 Termux 执行】"
echo ""
FRIDA_PORT="$(python3 -c "import json; print(json.load(open('$(dirname "$0")/anti_detect/phantom_frida_config.json'))['listen_port'])" 2>/dev/null || echo 17263)"
cat <<EOF
# 1) 确保 frida-server 在手机本机运行Rootphantom 端口见 phantom_frida_config.json
# 2) 启动 Agent连 SDK
export WP_AGENT_ON_DEVICE=1
export WP_FRIDA_MODE=remote
export WP_FRIDA_PORT=${FRIDA_PORT}
export WP_AUTO_DISCOVER=1
cd ~/workphone/agent # 或你同步 agent 目录的路径
python agent.py -d ${DEVICE_ID} --heartbeat 10
# 可选手填: -s ${WS_URL}
EOF
echo ""
echo "【Mac 端仅保留 SDK】"
echo " curl http://${SDK_HOST}/api/v3/connection/status"
echo " curl http://${SDK_HOST}/api/v3/hook/probe/${DEVICE_ID}"
echo ""
echo "【勿再使用】Mac 上 ensure_ws_agent.sh需 ADB forward与无线口径冲突"
echo ""
# 可选:停掉 Mac 侧假 Agent
pkill -f "agent.py -d ${DEVICE_ID} -s ws://" 2>/dev/null || true
curl -s "http://${SDK_HOST}/api/v3/connection/status" | python3 -m json.tool 2>/dev/null | head -20 || true

View File

@@ -0,0 +1,152 @@
#!/usr/bin/env bash
# 奥创工作手机 · 真机全量提取到 资料/奥创工作手机APK提取/
# 真源:资料/奥创微信控制接口与插件提取复用指南.md · 机擎/阿服/奥创真机清机/SKILL.md
#
# 用法:
# bash sdk/scripts/extract_aochuang_to_ziliao.sh [-s SERIAL] [--decompile]
#
# 前置Type-C USB 调试 + adb devices 见 device或无线 adb connect
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
OUT="${ROOT}/资料/奥创工作手机APK提取"
EVID="${ROOT}/开发文档/8、部署/05-测试验收/$(date +%Y%m%d)_奥创资料提取_$(date +%H%M%S)"
SERIAL=""
DECOMPILE=0
DEFAULT_SERIAL="xgfe65eimrrofyws"
AOCHUANG_PACKAGES=(
"org.xeslciw.manager"
"top.zzz.vivwxjz"
"uni.UNI9421F6C"
)
AOCHUANG_ROOT_PATHS=(
"/data/adb/xesd"
"/data/adb/modules"
)
log() { printf '[%s] %s\n' "$(date '+%H:%M:%S')" "$*"; }
usage() {
cat <<'EOF'
奥创真机全量提取 → 资料/奥创工作手机APK提取/
bash sdk/scripts/extract_aochuang_to_ziliao.sh [-s SERIAL] [--decompile]
--decompile 额外 apktool 反编译到 decompiled/<包名>/
EOF
}
while [[ $# -gt 0 ]]; do
case "$1" in
-s) SERIAL="$2"; shift 2 ;;
--decompile) DECOMPILE=1; shift ;;
-h|--help) usage; exit 0 ;;
*) echo "未知参数: $1"; usage; exit 1 ;;
esac
done
pick_serial() {
if [[ -n "$SERIAL" ]]; then return; fi
local online
online="$(adb devices | awk '/\tdevice$/{print $1; exit}')"
if [[ -n "$online" ]]; then SERIAL="$online"; return; fi
if adb devices | grep -q "^${DEFAULT_SERIAL}[[:space:]]"; then
SERIAL="$DEFAULT_SERIAL"
fi
}
run_adb() { adb -s "$SERIAL" "$@"; }
run_sh() { run_adb shell "$@"; }
pick_su() {
run_sh 'for p in /sbin/su /system/bin/su /system/xbin/su su; do
if [ -x "$p" ] 2>/dev/null || command -v "$p" >/dev/null 2>&1; then echo "$p"; exit 0; fi
done; exit 1' 2>/dev/null || true
}
run_root() {
local cmd="$1"
local su
su="$(pick_su)"
if [[ -n "$su" ]]; then
run_sh "su -c $(printf '%q' "$cmd")" 2>/dev/null
else
run_sh "$cmd" 2>/dev/null || true
fi
}
pick_serial
if [[ -z "$SERIAL" ]] || ! adb devices | grep -q "^${SERIAL}[[:space:]]*device"; then
echo "FAIL: 无 ADB 在线设备。请 Type-C 连接并开启 USB 调试,或 adb connect <ip>:5555"
adb devices -l
exit 1
fi
mkdir -p "$OUT" "$EVID"
log "serial=$SERIAL out=$OUT"
# --- 设备画像 ---
{
echo "# 奥创提取 · 设备检查 $(date '+%Y-%m-%d %H:%M:%S')"
echo
adb devices -l
echo
run_adb shell getprop ro.product.model
run_adb shell getprop ro.build.version.release
run_adb shell getprop ro.product.brand
} > "$EVID/00_device_info.txt"
run_sh pm list packages > "$EVID/01_all_packages.txt" 2>/dev/null || true
grep -E 'xes|zzz|vivwx|UNI9421|cloudservice|magisk|tencent\.mm' "$EVID/01_all_packages.txt" > "$EVID/02_aochuang_related_packages.txt" 2>/dev/null || true
# --- 拉 APK ---
for pkg in "${AOCHUANG_PACKAGES[@]}"; do
apk_path="$(run_sh pm path "$pkg" 2>/dev/null | head -1 | cut -d: -f2 | tr -d '\r' || true)"
if [[ -z "$apk_path" ]]; then
log "SKIP APK 未安装: $pkg"
echo "NOT_INSTALLED $pkg" >> "$EVID/03_apk_pull.log"
continue
fi
ver="$(run_sh dumpsys package "$pkg" 2>/dev/null | awk -F= '/versionName/{print $2; exit}' | tr -d '\r ')"
safe="${pkg}_v${ver:-unknown}.apk"
log "PULL $pkg -> $safe"
run_adb pull "$apk_path" "$OUT/$safe"
echo "OK $pkg $apk_path -> $OUT/$safe" >> "$EVID/03_apk_pull.log"
done
# --- 框架 / Magisk 模块(需 root---
for p in "${AOCHUANG_ROOT_PATHS[@]}"; do
dest="$OUT/device_root$(echo "$p" | tr '/' '_')"
log "TAR $p (root)"
run_root "test -e '$p' && tar -czf /sdcard/aochuang_extract_$(basename "$p").tgz '$p' 2>/dev/null" || true
tgz="/sdcard/aochuang_extract_$(basename "$p").tgz"
if run_sh "test -f '$tgz'" 2>/dev/null | grep -q .; then
mkdir -p "$dest"
run_adb pull "$tgz" "$dest/" 2>/dev/null || true
run_root "rm -f '$tgz'" || true
else
echo "MISSING_OR_DENIED $p" >> "$EVID/04_root_paths.log"
fi
done
# Magisk 模块名过滤
run_root 'ls /data/adb/modules 2>/dev/null' > "$EVID/05_magisk_modules.txt" 2>/dev/null || true
# --- 可选反编译 ---
if [[ "$DECOMPILE" -eq 1 ]] && command -v apktool >/dev/null 2>&1; then
DEC="$OUT/decompiled"
mkdir -p "$DEC"
for apk in "$OUT"/*.apk; do
[[ -f "$apk" ]] || continue
base="$(basename "$apk" .apk)"
log "apktool d $base"
apktool d -f -o "$DEC/$base" "$apk" >> "$EVID/06_apktool.log" 2>&1 || true
done
fi
log "DONE 证据目录: $EVID"
log "资料目录: $OUT"
ls -lah "$OUT"

View File

@@ -0,0 +1,464 @@
#!/usr/bin/env bash
# ==============================================================================
# frida_workphone_oneclick.sh — 工作手机·Frida 主控一键启动
#
# 目标:插上 Type-C 数据线 → 自动完成"Frida 主、ADB 辅"的全链路启动
# 1) 探测 USB 设备、确认 ADB 授权
# 2) 锁定主机 frida 版本(默认 16.5.6
# 3) 在设备上推送/启动 frida-server-16Root 模式 = 主路径)
# 4) Root 不可用时自动尝试 Frida Gadget 备背端口转发
# 5) 启动 SDK 服务uvicorn 8899
# 6) 通过 /api/v3/hook/probe/{device_id} 探测 Hook 是否就绪
# 7) 启动 AgentWebSocket 通道,带 hook=enabled
# 8) 输出对接清单Type-C 模式下存客宝直接调用 8899
#
# 设计参考:
# - 开发文档/8、部署/设备端Hook安装部署.mdRoot + frida-server 标准)
# - 机擎/references/快速入机部署_Root_Frida_Hook.md10/10 验证基线)
# - sdk/agent/hook/frida_manager.pyusb / gadget / remote 三模式)
# - sdk/app/routers/unified.py/hook/probe/{id}, /hook/execute, channel=hook
#
# 用法:
# ./scripts/frida_workphone_oneclick.sh # 默认设备 + 默认参数
# ./scripts/frida_workphone_oneclick.sh -d <serial> # 指定 ADB 序列号
# ./scripts/frida_workphone_oneclick.sh --gadget # 强制走 Gadget 备背
# ./scripts/frida_workphone_oneclick.sh --no-agent # 仅起 SDK + Frida不起 Agent
# ./scripts/frida_workphone_oneclick.sh --probe-only # 只做探测,不启服务
# ==============================================================================
set -euo pipefail
# ── 路径与默认值 ──────────────────────────────────────────────────────────────
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SDK_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
PROJ_ROOT="$(cd "${SDK_ROOT}/.." && pwd)"
FRIDA_VERSION_DEFAULT="16.5.6"
FRIDA_VERSION="${WORKPHONE_FRIDA_VERSION:-${FRIDA_VERSION_DEFAULT}}"
SDK_PORT="${WORKPHONE_SDK_PORT:-8899}"
SDK_BASE="http://127.0.0.1:${SDK_PORT}"
GADGET_DEFAULT_PORT="${WORKPHONE_GADGET_PORT:-27042}"
export ANDROID_ADB_SERVER_PORT="${ANDROID_ADB_SERVER_PORT:-5038}"
LOG_DIR="${SDK_ROOT}/logs"
mkdir -p "${LOG_DIR}"
DEVICE_SERIAL=""
FORCE_GADGET=0
NO_AGENT=0
PROBE_ONLY=0
# 所有 ADB 调用固定到同一个 server 端口,避免 SDK/CLI/脚本各看一个设备列表。
adb() {
command adb -P "${ANDROID_ADB_SERVER_PORT}" "$@"
}
# ── 参数解析 ──────────────────────────────────────────────────────────────────
while [[ $# -gt 0 ]]; do
case "$1" in
-d|--device) DEVICE_SERIAL="$2"; shift 2 ;;
--gadget) FORCE_GADGET=1; shift ;;
--no-agent) NO_AGENT=1; shift ;;
--probe-only) PROBE_ONLY=1; shift ;;
-h|--help)
sed -n '2,30p' "$0"; exit 0 ;;
*) echo "[!] 未知参数: $1"; exit 1 ;;
esac
done
# ── 颜色与日志 ────────────────────────────────────────────────────────────────
GREEN='\033[0;32m'; YELLOW='\033[1;33m'; RED='\033[0;31m'; NC='\033[0m'
log() { echo -e "${GREEN}[+]${NC} $*"; }
warn() { echo -e "${YELLOW}[!]${NC} $*"; }
fail() { echo -e "${RED}[✗]${NC} $*" >&2; }
fatal() { fail "$*"; exit 1; }
banner() {
echo "================================================================"
echo " 工作手机 · Frida 主控一键启动 (Type-C / 主FridaADB)"
echo " SDK_ROOT = ${SDK_ROOT}"
echo " FRIDA_TARGET = ${FRIDA_VERSION}"
echo " SDK_PORT = ${SDK_PORT}"
echo "================================================================"
}
# ── 步骤 1检查 ADB ──────────────────────────────────────────────────────────
step_check_adb() {
command -v adb >/dev/null 2>&1 || fatal "未找到 adb请先安装 platform-tools 并加入 PATH"
log "ADB version: $(adb version | head -1) port=${ANDROID_ADB_SERVER_PORT}"
# 启动 server
adb start-server >/dev/null 2>&1 || true
if [[ -n "${DEVICE_SERIAL}" ]]; then
bash "${SDK_ROOT}/scripts/wait_adb_device.sh" "${DEVICE_SERIAL}" "${ADB_WAIT_SEC:-90}" || {
fail "ADB 未连接: ${DEVICE_SERIAL}"
echo " 无线:开发者选项 → 无线调试 → 配对后把 IP:端口 设为 DEVICE_SERIAL"
echo " 或有线Type-C 连接并授权 USB 调试"
exit 1
}
fi
if [[ -z "${DEVICE_SERIAL}" ]]; then
DEVICE_SERIAL="$(adb devices | awk '/\tdevice$/ {print $1; exit}')"
fi
if [[ -z "${DEVICE_SERIAL}" ]]; then
fail "未检测到已授权的 USB 设备"
echo " 1) 请用 Type-C 数据线连接手机,并在手机上点'始终允许此电脑调试'"
echo " 2) 然后重新执行 $0"
exit 1
fi
log "目标设备: ${DEVICE_SERIAL}"
# 设备能力快照
ABI="$(adb -s "${DEVICE_SERIAL}" shell getprop ro.product.cpu.abi | tr -d '\r')"
MODEL="$(adb -s "${DEVICE_SERIAL}" shell getprop ro.product.model | tr -d '\r')"
ANDROID="$(adb -s "${DEVICE_SERIAL}" shell getprop ro.build.version.release | tr -d '\r')"
log "Model=${MODEL} ABI=${ABI} Android=${ANDROID}"
case "${ABI}" in
arm64-v8a) FRIDA_ARCH="arm64" ;;
armeabi-v7a) FRIDA_ARCH="arm" ;;
x86_64) FRIDA_ARCH="x86_64" ;;
x86) FRIDA_ARCH="x86" ;;
*) fatal "不支持的架构: ${ABI}" ;;
esac
}
# ── 步骤 2锁定主机 frida 版本 ───────────────────────────────────────────────
step_lock_host_frida() {
command -v python3 >/dev/null 2>&1 || fatal "未找到 python3"
HOST_VER="$(python3 -c 'import frida; print(frida.__version__)' 2>/dev/null || true)"
if [[ "${HOST_VER}" != "${FRIDA_VERSION}" ]]; then
warn "主机 frida 版本=${HOST_VER:-(未安装)}${FRIDA_VERSION},开始锁定…"
pip install --quiet "frida==${FRIDA_VERSION}" "frida-tools==12.5.0" || \
fatal "pip 安装 frida==${FRIDA_VERSION} 失败"
HOST_VER="$(python3 -c 'import frida; print(frida.__version__)')"
fi
log "主机 frida=${HOST_VER}"
}
# ── 步骤 3检测 Root部署反检测 frida-server随机名+随机端口)────────────
step_setup_frida_server_root() {
ROOT_RAW="$(adb -s "${DEVICE_SERIAL}" shell 'su -c id' 2>/dev/null || true)"
if [[ "${ROOT_RAW}" != *"uid=0"* ]]; then
warn "Root 不可用,将切换到 Gadget 备背模式"
return 1
fi
log "Root 已确认 (${ROOT_RAW//$'\r'/})"
LOCAL_BIN_DIR="${SDK_ROOT}/scripts"
LOCAL_CACHED="${LOCAL_BIN_DIR}/frida-server-${FRIDA_VERSION}-android-${FRIDA_ARCH}"
REMOTE_BIN="/data/local/tmp/frida-server-16"
if [[ ! -f "${LOCAL_CACHED}" ]]; then
log "下载 frida-server-${FRIDA_VERSION}-${FRIDA_ARCH}"
TMP="$(mktemp -d)"
URL="https://github.com/frida/frida/releases/download/${FRIDA_VERSION}/frida-server-${FRIDA_VERSION}-android-${FRIDA_ARCH}.xz"
curl -fL "${URL}" -o "${TMP}/fs.xz" || fatal "下载 frida-server 失败:${URL}"
xz -d -f "${TMP}/fs.xz"
mv "${TMP}/fs" "${LOCAL_CACHED}"
chmod 755 "${LOCAL_CACHED}"
fi
if [[ -z "$(adb -s "${DEVICE_SERIAL}" shell "su -c 'ls ${REMOTE_BIN} 2>/dev/null'" | tr -d '\r')" ]]; then
log "推送 ${LOCAL_CACHED}${REMOTE_BIN}"
adb -s "${DEVICE_SERIAL}" push "${LOCAL_CACHED}" "${REMOTE_BIN}" >/dev/null
adb -s "${DEVICE_SERIAL}" shell "su -c 'chmod 755 ${REMOTE_BIN}'"
fi
# 反检测:随机二进制名 + 随机监听端口(避免 stock frida-server 被微信 attach 检测)
if command -v shuf >/dev/null 2>&1; then
PHANTOM_PORT=$(shuf -i 10000-60000 -n 1)
else
PHANTOM_PORT=$((10000 + RANDOM % 50001))
fi
PHANTOM_NAME="fs_$(head -c 3 /dev/urandom | xxd -p 2>/dev/null || echo $$)"
PHANTOM_REMOTE="/data/local/tmp/${PHANTOM_NAME}"
PHANTOM_CONFIG="${SDK_ROOT}/scripts/anti_detect/phantom_frida_config.json"
log "部署反检测 Frida${PHANTOM_NAME} @ 0.0.0.0:${PHANTOM_PORT}"
adb -s "${DEVICE_SERIAL}" shell "su -c 'cp ${REMOTE_BIN} ${PHANTOM_REMOTE} && chmod 755 ${PHANTOM_REMOTE}'"
adb -s "${DEVICE_SERIAL}" shell "su -c 'killall frida-server-16 frida-server 2>/dev/null; pkill -f \"^/data/local/tmp/fs_\" 2>/dev/null; true'"
sleep 1
# 用 setsid 彻底脱离 adb shell 会话,避免 shell 退出时 frida-server 随之被 SIGHUP 杀掉
# 本会话踩坑nohup & 经 adb shell 启动仍随会话结束被杀 → 后续 attach connection closed
adb -s "${DEVICE_SERIAL}" shell "su -c 'setsid ${PHANTOM_REMOTE} -l 0.0.0.0:${PHANTOM_PORT} </dev/null >/dev/null 2>&1 &'"
sleep 2
PH_PID="$(adb -s "${DEVICE_SERIAL}" shell "su -c 'pidof ${PHANTOM_NAME}'" 2>/dev/null | tr -d '\r')"
[[ -n "${PH_PID}" ]] || fatal "反检测 frida-server 启动失败 (port=${PHANTOM_PORT})"
log "反检测 frida-server 已运行 (pid=${PH_PID}, port=${PHANTOM_PORT})"
adb -s "${DEVICE_SERIAL}" forward "tcp:${PHANTOM_PORT}" "tcp:${PHANTOM_PORT}" >/dev/null
mkdir -p "$(dirname "${PHANTOM_CONFIG}")"
python3 -c "import json; json.dump({'device_serial':'${DEVICE_SERIAL}','listen_port':${PHANTOM_PORT},'binary_name':'${PHANTOM_NAME}','binary_path':'${PHANTOM_REMOTE}'}, open('${PHANTOM_CONFIG}','w'), indent=2)"
log "phantom 配置已写入 ${PHANTOM_CONFIG}unified.py 自动读取)"
# 确保微信运行Hook 需要附着)
WX_PID="$(adb -s "${DEVICE_SERIAL}" shell pidof com.tencent.mm 2>/dev/null | tr -d '\r' || true)"
if [[ -z "${WX_PID}" ]]; then
log "拉起微信..."
adb -s "${DEVICE_SERIAL}" shell am start -n com.tencent.mm/.ui.LauncherUI >/dev/null 2>&1 || true
sleep 5
fi
return 0
}
# ── 步骤 4Gadget 备背(无 Root 时) ─────────────────────────────────────────
step_setup_gadget_fallback() {
warn "进入 Gadget 备背模式 — 请确保已用 sdk/scripts/inject_frida_gadget.sh 重打包并安装微信"
PORT="${GADGET_DEFAULT_PORT}"
log "adb forward tcp:${PORT} tcp:${PORT}"
adb -s "${DEVICE_SERIAL}" forward "tcp:${PORT}" "tcp:${PORT}" >/dev/null
log "Gadget endpoint = 127.0.0.1:${PORT}FridaManager 选 mode=gadget 时可用)"
export WORKPHONE_FRIDA_MODE="gadget"
export WORKPHONE_FRIDA_GADGET_PORT="${PORT}"
}
# ── 步骤 5启动 SDK 服务8899──────────────────────────────────────────────
step_start_sdk() {
if curl -sS "${SDK_BASE}/health" >/dev/null 2>&1; then
log "SDK 已在 ${SDK_BASE} 监听,跳过启动"
return 0
fi
log "启动 SDK 服务uvicorn 0.0.0.0:${SDK_PORT})…"
(
cd "${SDK_ROOT}/app"
export SDK_MATRIX_VERIFY=1
export WECHAT_WS_HOOK_ONLY=1
export WECHAT_BACKEND_ONLY=1
nohup env ANDROID_ADB_SERVER_PORT="${ANDROID_ADB_SERVER_PORT:-5038}" \
python3 -m uvicorn main:app --host 0.0.0.0 --port "${SDK_PORT}" \
> "${LOG_DIR}/sdk.log" 2>&1 &
echo $! > "${LOG_DIR}/sdk.pid"
)
for i in {1..15}; do
if curl -sS "${SDK_BASE}/health" >/dev/null 2>&1; then
log "SDK 已就绪 (pid=$(cat "${LOG_DIR}/sdk.pid"))"
return 0
fi
sleep 1
done
fatal "SDK 启动失败,请查看 ${LOG_DIR}/sdk.log"
}
# ── 步骤 6调用 /hook/probe 让服务端正式承认 Hook 能力 ───────────────────────
step_hook_probe() {
log "调用 ${SDK_BASE}/api/v3/hook/probe/${DEVICE_SERIAL}"
RESP=""
SUPPORTS="False"
for _i in $(seq 1 20); do
RESP="$(curl -sS "${SDK_BASE}/api/v3/hook/probe/${DEVICE_SERIAL}" || true)"
SUPPORTS="$(echo "${RESP}" | python3 -c 'import json,sys
try:
d=json.load(sys.stdin); print(d.get("data",d).get("supports_hook",False))
except Exception:
print(False)' 2>/dev/null || echo False)"
if [[ "${SUPPORTS}" == "True" ]]; then
break
fi
sleep 2
done
echo "${RESP}" | python3 -m json.tool 2>/dev/null || echo "${RESP}"
if [[ "${SUPPORTS}" == "True" ]]; then
log "Hook 通道就绪 ✅(服务端已写入 device_modules"
else
warn "Hook 探测未通过(设备能力为 supports_hook=false"
warn "可能原因Root/frida-server 未起、微信未运行、frida 版本错配"
return 1
fi
}
# ── 步骤 7启动设备端 AgentUSB-reverse + WS──────────────────────────────
step_start_agent() {
if [[ "${NO_AGENT}" == "1" ]]; then
warn "--no-agent 已指定,跳过 Agent 启动"
return 0
fi
log "adb reverse tcp:${SDK_PORT} tcp:${SDK_PORT}"
adb -s "${DEVICE_SERIAL}" reverse "tcp:${SDK_PORT}" "tcp:${SDK_PORT}" >/dev/null || true
AGENT_DIR="${SDK_ROOT}/agent"
if [[ ! -f "${AGENT_DIR}/agent.py" ]]; then
warn "未找到 ${AGENT_DIR}/agent.py跳过 Agent 启动"
return 0
fi
log "启动 Agentdevice=${DEVICE_SERIAL}WebSocket + Frida remote"
PH_PORT=""
PH_CFG="${SDK_ROOT}/scripts/anti_detect/phantom_frida_config.json"
AGENT_LOG="${LOG_DIR}/agent.log"
LAN_ENV="${SDK_ROOT}/config/lan.env"
[[ -f "${LAN_ENV}" ]] && source "${LAN_ENV}" || true
# Mac 侧 Agent 连本机 SDKlan.env 的 WORKPHONE_LAN_IP 供手机 Termux 寻服
WS_HOST="127.0.0.1"
if [[ -f "${PH_CFG}" ]]; then
PH_PORT=$(python3 -c "import json;print(json.load(open('${PH_CFG}')).get('listen_port',''))" 2>/dev/null || true)
fi
# 避免重复 Agent 抢同一 device_id
if [[ -f "${LOG_DIR}/agent.pid" ]]; then
kill "$(cat "${LOG_DIR}/agent.pid" 2>/dev/null)" 2>/dev/null || true
fi
pkill -f "agent.py -d ${DEVICE_SERIAL} -s ws://127.0.0.1:${SDK_PORT}" 2>/dev/null || true
sleep 1
# Agent 启动前再次确认微信已运行Frida attach 依赖)
WX_PID="$(adb -s "${DEVICE_SERIAL}" shell pidof com.tencent.mm 2>/dev/null | tr -d '\r' || true)"
if [[ -z "${WX_PID}" ]]; then
log "拉起微信Agent 启动前)..."
adb -s "${DEVICE_SERIAL}" shell am start -n com.tencent.mm/.ui.LauncherUI >/dev/null 2>&1 || true
sleep 6
fi
: > "${AGENT_LOG}"
{
echo "=== $(date '+%Y-%m-%d %H:%M:%S') oneclick agent start ==="
echo "device=${DEVICE_SERIAL} sdk=${SDK_BASE} frida_port=${PH_PORT:-auto}"
} >> "${AGENT_LOG}"
AGENT_PID="$(
cd "${AGENT_DIR}"
export WP_FRIDA_MODE=remote
export WP_DEVICE_SERIAL="${DEVICE_SERIAL}"
export WORKPHONE_AGENT_LOG="${AGENT_LOG}"
export WORKPHONE_SDK_PORT="${SDK_PORT}"
export WORKPHONE_WS_HOST="${WS_HOST}"
if [[ -n "${PH_PORT}" ]]; then
export WP_FRIDA_PORT="${PH_PORT}"
adb -s "${DEVICE_SERIAL}" forward "tcp:${PH_PORT}" "tcp:${PH_PORT}" >/dev/null 2>&1 || true
fi
python3 - <<'PY'
import json
import os
import subprocess
import sys
log_path = os.environ["WORKPHONE_AGENT_LOG"]
device_serial = os.environ["WP_DEVICE_SERIAL"]
sdk_port = os.environ["WORKPHONE_SDK_PORT"]
sdk_root = os.path.abspath(os.path.join(os.getcwd(), ".."))
build = os.path.join(sdk_root, "scripts", "build_agent_config.py")
public_servers = []
try:
cfg = json.loads(
subprocess.check_output(
[sys.executable, build, "--device-id", device_serial],
text=True,
)
)
public_servers = [s for s in cfg.get("public_servers") or [] if s]
except Exception:
pass
# Mac 本机 Agent 优先连本地 SDK断连时按 BIND-03 公网/局域网候选回退
ws_host = os.environ.get("WORKPHONE_WS_HOST", "127.0.0.1")
if ws_host in ("127.0.0.1", "localhost"):
server_url = f"ws://127.0.0.1:{sdk_port}/ws/device"
else:
server_url = f"ws://{ws_host}:{sdk_port}/ws/device"
cmd = [
sys.executable,
"agent.py",
"-d",
device_serial,
"-s",
server_url,
"--heartbeat",
"10",
]
if public_servers:
cmd.extend(["--public-servers", ",".join(public_servers)])
with open(log_path, "ab", buffering=0) as log:
proc = subprocess.Popen(
cmd,
cwd=os.getcwd(),
env=os.environ.copy(),
stdin=subprocess.DEVNULL,
stdout=log,
stderr=subprocess.STDOUT,
start_new_session=True,
close_fds=True,
)
print(proc.pid)
PY
)"
echo "${AGENT_PID}" > "${LOG_DIR}/agent.pid"
log "Agent 已启动 (pid=${AGENT_PID})"
# 等待 Frida RPC 与 WebSocket 注册同时就绪,避免 hook/execute 在 lazy attach 阶段超时
for _i in $(seq 1 45); do
if ! kill -0 "${AGENT_PID}" 2>/dev/null; then
tail -80 "${AGENT_LOG}" >&2 || true
fatal "Agent 启动后退出,请查看 ${AGENT_LOG}"
fi
if grep -q "RPC ping: pong" "${AGENT_LOG}" 2>/dev/null && \
grep -q "服务器确认注册" "${AGENT_LOG}" 2>/dev/null; then
log "Frida RPC + WebSocket 注册已就绪 ✅"
return 0
fi
sleep 2
done
tail -80 "${AGENT_LOG}" >&2 || true
fatal "90s 内未看到 RPC ping 或服务器确认注册请检查微信、phantom 端口转发与 SDK WebSocket"
}
# ── 步骤 8对接清单 ──────────────────────────────────────────────────────────
print_summary() {
cat <<EOF
================================================================
Frida 主控一键启动 ✅ 完成
================================================================
设备序列号 : ${DEVICE_SERIAL}
SDK API : ${SDK_BASE}
API 文档 : ${SDK_BASE}/docs
控制中心 : ${SDK_BASE}/hub
Hook 探测 : ${SDK_BASE}/api/v3/hook/probe/${DEVICE_SERIAL}
Phantom配置 : ${SDK_ROOT}/scripts/anti_detect/phantom_frida_config.json
Hook 执行 : POST ${SDK_BASE}/api/v3/hook/execute (hook_only=true)
Hook 取数 : ${SDK_BASE}/api/v3/hook/data/${DEVICE_SERIAL}
通道选择 : 主 = WebSocket Agent + Frida HookWECHAT_WS_HOOK_ONLY禁止 ADB UI
存客宝侧调用范例PHP:
\$sdk = new \\Cunkebao\\WorkPhone\\WorkPhoneClient('${SDK_BASE}', 'TOKEN');
\$sdk->sendMessage('${DEVICE_SERIAL}', 'wechat', '文件传输助手',
'[E2E] Frida 主控通道', 'text', null, null, null,
'hook'); // ← channel=hook强制走 Frida
日志:
SDK : ${LOG_DIR}/sdk.log
Agent : ${LOG_DIR}/agent.log
停止:
kill \$(cat ${LOG_DIR}/sdk.pid 2>/dev/null) 2>/dev/null
kill \$(cat ${LOG_DIR}/agent.pid 2>/dev/null) 2>/dev/null
================================================================
EOF
}
# ── 主流程 ────────────────────────────────────────────────────────────────────
banner
step_check_adb
step_lock_host_frida
if [[ "${FORCE_GADGET}" == "1" ]]; then
step_setup_gadget_fallback
else
if ! step_setup_frida_server_root; then
step_setup_gadget_fallback
fi
fi
if [[ "${PROBE_ONLY}" == "1" ]]; then
step_start_sdk
step_hook_probe
exit 0
fi
step_start_sdk
step_start_agent
step_hook_probe
print_summary

View File

@@ -0,0 +1,171 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
四端可直调·模块化接口能力清单 生成器
从运行中的 SDK 实时拉取 skill-registry211 动作 / 49 模块 / 9 平台),
生成模块化、可直读的接口能力清单 Markdown供存客宝 / 触客宝 / AI数智员工 /
SuperAdmin 直接对接判断「哪些能力可直调、走什么通道、经哪个 BFF/SDK 路径」。
- 不改任何兄弟仓库代码;只输出工作手机侧文档。
- 真源GET /api/v3/ai/brain/skill-registrylive
- 用法python3 sdk/scripts/gen_integration_manifest.py [--base http://127.0.0.1:8899]
输出:开发文档/5、接口/01-规范与统一层/四端可直调接口能力清单.md
"""
from __future__ import annotations
import argparse
import datetime
import json
import os
import sys
import urllib.request
ROOT = os.path.normpath(os.path.join(os.path.dirname(__file__), "..", ".."))
OUT = os.path.join(ROOT, "开发文档", "5、接口", "01-规范与统一层", "四端可直调接口能力清单.md")
# 平台 → 默认执行通道 + 四端推荐入口(不改对方代码,全部 HTTP
PLATFORM_CHANNEL = {
"wechat": ("Frida Hook / u2兜底", "/api/v3/message/* friend/* group/* tag/* moments/* + hook/execute"),
"douyin": ("execute-scriptAI Brain", "/api/v3/ai/brain/execute-script (script=douyin)"),
"xhs": ("execute-scriptAI Brain", "/api/v3/ai/brain/execute-script (script=xhs)"),
"xianyu": ("execute-scriptAI Brain", "/api/v3/ai/brain/execute-script (script=xianyu)"),
"soul": ("execute-scriptAI Brain", "/api/v3/ai/brain/execute-script (script=soul)"),
"system": ("Agent 系统能力", "/api/v3/devices/{id}/* "),
"hook": ("Hook 统一执行", "/api/v3/hook/execute"),
"ai_brain":("AI 中台编排", "/api/v3/ai/brain/* · /api/v3/devices/{id}/ai/*"),
"anti_ban":("防封守护", "/api/v3/anti-ban/*"),
}
# 四端可直调矩阵(来自需求 §3.8 真源;纯文档约定,不改对方代码)
FOUR_END = [
("存客宝 H5 (:3100)", "经存客宝 BFF /v1/workphone/* 或联调直连 :8899", "发消息/群发/加好友/朋友圈/标签/线索/状态/中台 Skill"),
("触客宝 (:3101)", "必经存客宝 BFF同域 JWT", "message/send · messages/list · moments/post · agent/execute"),
("AI数智员工 (:3104)", "经存客宝 OpenPlatform / proxy不直连 :8899", "设备状态/截图/终端绑定 + proxy→execute-script 多平台控机"),
("SuperAdmin (:3103)", "只读聚合 + 连接方案切换主控台", "GET /devices · status · connection/provider/switch"),
]
def fetch_registry(base: str) -> dict:
url = base.rstrip("/") + "/api/v3/ai/brain/skill-registry"
try:
with urllib.request.urlopen(url, timeout=10) as r:
return json.loads(r.read().decode())
except Exception as e: # 离线回退到缓存
cache = os.path.join(ROOT, "sdk", "tmp", "manifest_20260530", "skill_registry.json")
if os.path.exists(cache):
sys.stderr.write(f"[warn] live 拉取失败({e}),用缓存 {cache}\n")
return json.load(open(cache, encoding="utf-8"))
raise
def render(reg: dict, base: str) -> str:
data = reg["data"]
summary = data.get("summary", {})
skills = data.get("skills", {})
now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M")
L = []
L.append("---")
L.append("tags: [工作手机, 接口, 四端对接, 能力清单, 自动生成]")
L.append("doc-type: 索引")
L.append("layer: 5、接口/01-规范与统一层")
L.append('parent: "[[5、接口/README|5、接口]]"')
L.append("obsidian-color: \"#0277BD\"")
L.append("---\n")
L.append("# 四端可直调 · 模块化接口能力清单")
L.append("")
L.append(f"> **自动生成**(勿手改):`python3 sdk/scripts/gen_integration_manifest.py` ")
L.append(f"> **真源**`GET {base}/api/v3/ai/brain/skill-registry`live · **生成时间**{now} ")
L.append("> **铁律**:四端**只调 HTTP**;工作手机**不改**存客宝/触客宝/AI数智员工/SuperAdmin 代码;缺口走 COORD 或 `sdk/proxy` 透传。")
L.append("")
L.append("## 〇、总览")
L.append("")
L.append("| 维度 | 数量 |")
L.append("|:---|:---:|")
L.append(f"| 平台skill | {summary.get('total_skills', len(skills))} |")
L.append(f"| 功能模块 | {summary.get('total_modules', '')} |")
L.append(f"| 动作action总计 | {summary.get('total_actions', '')} |")
L.append(f"| 平台业务动作 | {summary.get('platform_actions', '')} |")
L.append("")
L.append("**通道图例**`Frida Hook`=设备本机 Frida RPC微信主通道· `execute-script`=AI Brain 多平台脚本 · `u2`=UI 自动化兜底 · `companion`=设备端无障碍/广播模块(资料/红包等需 APK 模块或测试号)。")
L.append("")
# 四端入口矩阵
L.append("## 一、四端入口矩阵(不改对方代码)")
L.append("")
L.append("| 端 | 调用方式 | 典型可直调能力 |")
L.append("|:---|:---|:---|")
for end, how, caps in FOUR_END:
L.append(f"| **{end}** | {how} | {caps} |")
L.append("")
L.append("> 连接方案切换(超管主控台):`POST /api/v3/connection/provider/switch`jiqing/aochuang/legacy/custom_*)· 列表 `GET /api/v3/connection/providers`。")
L.append("")
# 平台 → 模块 → 动作
L.append("## 二、平台 × 模块 × 动作(实时)")
L.append("")
for plat, sk in skills.items():
ch, entry = PLATFORM_CHANNEL.get(plat, ("", ""))
name = sk.get("name", plat) if isinstance(sk, dict) else plat
pkg = sk.get("package", "") if isinstance(sk, dict) else ""
mods = sk.get("modules", {}) if isinstance(sk, dict) else {}
total = sum(len(v) if isinstance(v, (list, dict)) else 0 for v in mods.values()) if isinstance(mods, dict) else 0
L.append(f"### {name} `{plat}`")
L.append("")
L.append(f"- **包名**`{pkg or ''}` · **动作数**{total} · **默认通道**{ch}")
L.append(f"- **统一入口**`{entry}`")
L.append("")
if isinstance(mods, dict) and mods:
L.append("| 模块 | 动作数 | 动作action |")
L.append("|:---|:---:|:---|")
for mk, acts in mods.items():
names = acts if isinstance(acts, list) else (acts.get("actions", []) if isinstance(acts, dict) else [])
disp = " · ".join(f"`{a}`" for a in names) if names else ""
L.append(f"| {mk} | {len(names)} | {disp} |")
L.append("")
# 统一调用范式
L.append("## 三、统一调用范式(可直接联调)")
L.append("")
L.append("```http")
L.append("# 微信Frida 主通道)")
L.append("POST /api/v3/hook/execute")
L.append('{"device_id":"<serial>","platform":"wechat","action":"send_message",')
L.append(' "params":{"to_id":"文件传输助手","content":"hi"},"hook_only":1}')
L.append("")
L.append("# 多平台精确控机(抖音/小红书/闲鱼/Soul")
L.append("POST /api/v3/ai/brain/execute-script")
L.append('{"device_id":"<serial>","script":"douyin","action":"send_message","params":{...}}')
L.append("")
L.append("# 未注册 BFF 的能力 → 经存客宝 BFF 透传(不新造接口)")
L.append("POST /v1/workphone/sdk/proxy")
L.append('{"method":"POST","path":"/api/v3/ai/brain/execute-script","body":{...}}')
L.append("```")
L.append("")
L.append("## 四、关联文档")
L.append("")
L.append("- 四端开放接口与对接铁律:`5、接口/02-业务对接/四端开放接口汇总与对接铁律.md`")
L.append("- 存客宝 BFF↔SDK 映射:`5、接口/02-业务对接/存客宝BFF与工作手机SDK映射表.md`")
L.append("- 全量接口目录:`5、接口/01-规范与统一层/工作手机API全量接口目录.md`")
L.append("- 需求真源:`1、需求/修改/工作手机_存客宝四端对接_20260529.md` §3.8 / §十")
L.append("- 静态对齐校验:`python3 sdk/scripts/wechat_interface_audit.py`0 缺失)")
L.append("")
return "\n".join(L)
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--base", default="http://127.0.0.1:8899")
args = ap.parse_args()
reg = fetch_registry(args.base)
md = render(reg, args.base)
os.makedirs(os.path.dirname(OUT), exist_ok=True)
with open(OUT, "w", encoding="utf-8") as f:
f.write(md)
print(f"[ok] 写入 {OUT}{len(md)} 字)")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,189 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
微信 174 action 能力矩阵 生成器
把「Hook catalog 探针报告probe 假参数)」+ ACTION_TO_RPC 映射 + JS intent 分类
合成一张**诚实的能力矩阵**:每个 action → 模块 / 通道 / 可调用条件,
让存客宝 / 触客宝 / AI数智员工 / SuperAdmin 知道「这个动作怎么调、需要什么」。
诚实口径(真机铁律 #3/#14
- probe 用占位参数,写操作 success:False 是**正常**(需真实参数/上下文),非代码缺陷;
- 本矩阵据此区分「已验可直调 / 需真实参数 / 需UI或测试号 / companion模块」。
用法:
python3 sdk/scripts/gen_wechat_capability_matrix.py \
--report sdk/tmp/matrix_hook_catalog_xgfe65eimrrofyws_1780127048.json
输出:开发文档/5、接口/03-Hook与微信/微信174动作能力矩阵.md
"""
from __future__ import annotations
import argparse, datetime, glob, json, os, re
ROOT = os.path.normpath(os.path.join(os.path.dirname(__file__), "..", ".."))
HOOK_EXEC = os.path.join(ROOT, "sdk", "agent", "hook", "hook_executor.py")
HOOK_JS = os.path.join(ROOT, "sdk", "agent", "hook", "wechat_hook_v2.js")
OUT = os.path.join(ROOT, "开发文档", "5、接口", "03-Hook与微信", "微信174动作能力矩阵.md")
# 资金/隐私敏感(需测试号 + 小额 + 审计)
MONEY = {"send_red_packet", "receive_red_packet", "transfer", "send_transfer",
"receive_transfer", "payment_receive", "receive_payment"}
# 需相机/系统 UI纯 Frida 难覆盖u2/companion
UI_HEAVY = {"set_avatar", "scan_qr_code", "scan_add_friend", "scan_extract_qr",
"extract_qr_from_image", "add_friend_by_qr", "show_payment_code",
"voice_call", "video_call", "send_voice", "send_voice_message"}
def parse_action_to_rpc() -> dict:
pairs, inb = {}, False
for ln in open(HOOK_EXEC, encoding="utf-8"):
if re.match(r"\s*ACTION_TO_RPC\s*[:=]", ln):
inb = True; continue
if inb:
if re.match(r"^\}", ln):
break
m = re.search(r'["\']([a-zA-Z0-9_]+)["\']\s*:\s*["\']([a-zA-Z0-9_]+)["\']', ln)
if m:
pairs[m.group(1)] = m.group(2)
return pairs
def _extract_func_body(js: str, brace_start: int) -> str:
"""从函数体起始 `{`brace_start 指向该 `{`)做花括号配平,返回函数体文本。
旧实现用固定 1400 字符窗口,会越界吃到相邻函数体,导致
sendVideo/sendFile/sendLink实为 _shareMediaToWechat 真实现)被误判为 companion。
"""
depth = 0
for i in range(brace_start, len(js)):
c = js[i]
if c == "{":
depth += 1
elif c == "}":
depth -= 1
if depth == 0:
return js[brace_start:i + 1]
return js[brace_start:]
def parse_intent_funcs() -> set:
"""只在每个函数**自身**体内判定是否为 _intentAction 占位(花括号配平,杜绝越界)。"""
js = open(HOOK_JS, encoding="utf-8").read()
intent = set()
for mm in re.finditer(r"([a-zA-Z_][a-zA-Z0-9_]*)\s*:\s*function[^{]*\{", js):
brace_start = js.rfind("{", mm.start(), mm.end())
body = _extract_func_body(js, brace_start)
# 仅当函数体内**唯一**返回路径是 _intentAction 才算占位;
# 若同时存在 _shareMediaToWechat/_shareTextToWechat/_sendMessageInternal/performNow 等真实现,则不算占位。
has_intent = "_intentAction(" in body
has_real = any(k in body for k in (
"_shareMediaToWechat", "_shareTextToWechat", "_sendMessageInternal",
"_execSQLWrite", "Java.performNow", "_sendMessageBySendMsgEvent",
))
if has_intent and not has_real:
intent.add(mm.group(1))
return intent
def latest_report() -> str:
files = sorted(glob.glob(os.path.join(ROOT, "sdk", "tmp", "matrix_hook_catalog_*.json")),
key=os.path.getmtime, reverse=True)
return files[0] if files else ""
def classify(action, passed, a2r, intent):
rpc = a2r.get(action, "")
if passed:
return "✅ 已验可直调", "Frida RPC", "probe 即通(读类/无副作用)"
if action in MONEY:
return "🟡 需测试号", "Frida RPC + 风控", "资金动作:测试号 + 小额 + 审计,真实参数"
if action in UI_HEAVY:
return "🟠 需UI/u2", "u2/companion", "相机/系统UI/录音,纯 Frida 难覆盖"
if rpc and rpc in intent:
return "🟠 companion", "companion intent", "设备端无障碍/广播模块;无模块诚实返 false"
if rpc:
return "🟢 需真实参数", "Frida RPC", "probe 假参数 success:false传真实 to_id/sns_id/msg_id 即可用"
return "⬜ 未映射", "u2 兜底", "无 ACTION_TO_RPC走 u2 或经 hook/execute 别名解析"
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--report", default="")
args = ap.parse_args()
report = args.report or latest_report()
rep = json.load(open(report, encoding="utf-8")) if report else {"results": []}
results = {r["action"]: bool(r.get("success")) for r in rep.get("results", [])}
a2r = parse_action_to_rpc()
intent = parse_intent_funcs()
now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M")
# 全 action 集合catalog 结果 ACTION_TO_RPC
all_actions = sorted(set(results) | set(a2r))
rows = []
for a in all_actions:
passed = results.get(a, None)
status, channel, cond = classify(a, passed is True, a2r, intent)
rows.append((a, a2r.get(a, ""), status, channel, cond))
from collections import Counter
dist = Counter(r[2] for r in rows)
L = []
L.append("---")
L.append("tags: [工作手机, 接口, Hook, 微信, 能力矩阵, 自动生成]")
L.append("doc-type: 验收")
L.append("layer: 5、接口/03-Hook与微信")
L.append('parent: "[[5、接口/03-Hook与微信/README|03-Hook与微信]]"')
L.append('obsidian-color: "#EF6C00"')
L.append("---\n")
L.append("# 微信 Action 能力矩阵(诚实口径)")
L.append("")
L.append("> **自动生成**`python3 sdk/scripts/gen_wechat_capability_matrix.py` ")
L.append(f"> **数据源**catalog 探针 `{os.path.basename(report) or ''}` + `ACTION_TO_RPC` + JS intent 分类 · **生成**{now} ")
L.append("> **诚实口径**catalog 用**占位参数**探针,写操作 `success:false` 属**正常**(需真实参数/上下文),非代码缺陷;本矩阵据此分级。")
L.append("")
L.append("## 〇、能力分级总览")
L.append("")
L.append("| 分级 | 含义 | 数量 |")
L.append("|:---|:---|:---:|")
grade_desc = {
"✅ 已验可直调": "probe 即通,读类/无副作用,可直接调用",
"🟢 需真实参数": "有 Frida RPC传真实参数即可用probe 假参数才失败)",
"🟡 需测试号": "资金类,须测试号+小额+审计",
"🟠 需UI/u2": "相机/系统UI/录音,纯 Frida 难覆盖,走 u2",
"🟠 companion": "需设备端无障碍/广播模块,无模块诚实返 false",
"⬜ 未映射": "无直接 RPC经 hook/execute 别名解析或 u2",
}
for g, desc in grade_desc.items():
L.append(f"| {g} | {desc} | {dist.get(g, 0)} |")
L.append(f"| **合计** | | **{len(rows)}** |")
L.append("")
L.append("**结论**:✅+🟢 = **真实可用**(读类即调、写类传真实参数即调);🟡🟠 需对应前置(测试号/UI/模块);四端按本表选择调用方式,缺口走 `sdk/proxy` 或 COORD。")
L.append("")
L.append("## 一、动作明细")
L.append("")
L.append("| action | RPC | 分级 | 通道 | 可调用条件 |")
L.append("|:---|:---|:---|:---|:---|")
for a, rpc, status, channel, cond in rows:
L.append(f"| `{a}` | `{rpc}` | {status} | {channel} | {cond} |")
L.append("")
L.append("## 二、四端用法")
L.append("")
L.append("- **读类(✅)**`GET /api/v3/{module}/*` 或 `POST /api/v3/hook/execute`hook_only:1直接调。")
L.append("- **写类(🟢)**:传**真实** to_id/sns_id/msg_id/group_id经 BFF `/v1/workphone/*` 或 `hook/execute`。")
L.append("- **资金(🟡)**仅测试号存客宝侧加二次确认SDK 已入 `rate_limiter`。")
L.append("- **UI/companion🟠**:需设备端 u2 在线或无障碍模块;无则诚实返回 false不假成功")
L.append("")
L.append("## 三、关联")
L.append("- 需求真源:`1、需求/修改/工作手机_微信全量控机与私域_20260529.md`")
L.append("- 接口契约:`5、接口/03-Hook与微信/微信Frida_API契约.md`")
L.append("- 四端可直调清单:`5、接口/01-规范与统一层/四端可直调接口能力清单.md`")
L.append("- 静态对齐:`python3 sdk/scripts/wechat_interface_audit.py`0 缺失)")
L.append("")
os.makedirs(os.path.dirname(OUT), exist_ok=True)
open(OUT, "w", encoding="utf-8").write("\n".join(L))
print(f"[ok] 写入 {OUT}")
print(f" 动作 {len(rows)},分级分布:{dict(dist)}")
if __name__ == "__main__":
main()

155
sdk/scripts/get_imei_md5.py Normal file
View File

@@ -0,0 +1,155 @@
#!/usr/bin/env python3
"""
通过 USB Type-C 连接的 Android 设备:提取 IMEI或 Android ID/序列号)并输出其 MD5。
用法(在电脑上运行,手机用 Type-C 连上并开启 USB 调试):
python get_imei_md5.py
python get_imei_md5.py -s <device_serial> # 多设备时指定序列号
依赖:已安装 adb 且设备已授权。
"""
from __future__ import annotations
import argparse
import hashlib
import re
import subprocess
import sys
from typing import Optional
def run_adb(serial: Optional[str], *args: str, timeout: int = 10) -> subprocess.CompletedProcess:
cmd = ["adb"]
if serial:
cmd.extend(["-s", serial])
cmd.extend(args)
return subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
def get_first_device() -> Optional[str]:
r = run_adb(None, "devices")
if r.returncode != 0:
return None
for line in r.stdout.splitlines()[1:]:
line = line.strip()
if not line or line.startswith("*"):
continue
parts = line.split()
if len(parts) >= 2 and parts[1] == "device":
return parts[0]
return None
def get_imei_via_dumpsys(serial: Optional[str]) -> Optional[str]:
r = run_adb(serial, "shell", "dumpsys iphonesubinfo 2>/dev/null")
if r.returncode != 0 or not r.stdout:
return None
# 匹配 "Device ID = xxx" 或 "Imei = xxx"
for pattern in (r"Device ID\s*=\s*(\S+)", r"Imei\s*=\s*(\S+)", r"IMEI\s*=\s*(\S+)"):
m = re.search(pattern, r.stdout, re.IGNORECASE)
if m:
raw = m.group(1).strip()
if raw.isdigit() and 14 <= len(raw) <= 16:
return raw
return None
def get_imei_via_service_call(serial: Optional[str]) -> Optional[str]:
r = run_adb(serial, "shell", "service call iphonesubinfo 1 2>/dev/null")
if r.returncode != 0 or "Parcel" not in r.stdout:
return None
# 解析 Parcel 中的 UTF-16 字符串(常见格式)
hex_part = re.sub(r"\s+", "", r.stdout)
hex_match = re.search(r"([0-9a-fA-F]{8})([0-9a-fA-F]+)", hex_part)
if not hex_match:
return None
try:
hex_str = hex_match.group(2)
if len(hex_str) % 4 != 0:
return None
s = "".join(
chr(int(hex_str[i : i + 4], 16))
for i in range(0, len(hex_str), 4)
if int(hex_str[i : i + 4], 16) < 0x10000
)
s = s.strip("\x00 ")
if s.isdigit() and 14 <= len(s) <= 16:
return s
except Exception:
pass
return None
def get_android_id(serial: Optional[str]) -> Optional[str]:
r = run_adb(serial, "shell", "settings get secure android_id")
if r.returncode != 0:
return None
raw = (r.stdout or "").strip()
return raw if raw and raw != "null" else None
def get_serial_no(serial: Optional[str]) -> Optional[str]:
r = run_adb(serial, "shell", "getprop ro.serialno")
if r.returncode != 0:
return None
raw = (r.stdout or "").strip()
return raw if raw else None
def md5_hex(s: str) -> str:
return hashlib.md5(s.encode("utf-8")).hexdigest()
def main() -> None:
parser = argparse.ArgumentParser(description="从 USB 连接的 Android 设备提取 IMEI/ID 并输出 MD5")
parser.add_argument("-s", "--serial", help="设备序列号(多设备时指定)")
parser.add_argument("-q", "--quiet", action="store_true", help="仅输出 MD5 一行")
args = parser.parse_args()
serial = args.serial
if not serial:
serial = get_first_device()
if not serial:
print("错误:未检测到已连接的 Android 设备。请用 Type-C 连接并开启 USB 调试。", file=sys.stderr)
sys.exit(1)
raw_id: Optional[str] = None
source = ""
raw_id = get_imei_via_dumpsys(serial)
if raw_id:
source = "IMEI (dumpsys)"
if not raw_id:
raw_id = get_imei_via_service_call(serial)
if raw_id:
source = "IMEI (service call)"
if not raw_id:
raw_id = get_android_id(serial)
if raw_id:
source = "Android ID"
if not raw_id:
raw_id = get_serial_no(serial)
if raw_id:
source = "ro.serialno"
if not raw_id:
print("错误:无法获取 IMEI/Android ID/序列号(需设备已授权且部分机型需 root 才能读 IMEI", file=sys.stderr)
sys.exit(2)
md5_val = md5_hex(raw_id)
if args.quiet:
print(md5_val)
else:
print(f"设备: {serial}")
print(f"原始 ID 来源: {source}")
print(f"原始 ID: {raw_id}")
print(f"MD5: {md5_val}")
sys.exit(0)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,196 @@
#!/usr/bin/env bash
#
# inject_frida_gadget.sh — 免 Root Frida Gadget 注入微信 APK
#
# 用法:
# ./inject_frida_gadget.sh <wechat.apk> [output_dir]
#
# 前置:
# brew install apktool # APK 拆包/打包
# pip install lief # ELF 注入
# 需有 Java keytool + jarsignermacOS 自带)
#
# 流程:
# 1. apktool 解包微信 APK
# 2. 下载对应架构的 frida-gadget.so
# 3. 用 LIEF 将 gadget 注入微信的 native library
# 4. 写入 gadget 配置listen 0.0.0.0 随机端口)
# 5. apktool 重新打包
# 6. 签名debug 签名即可)
# 7. 输出可安装的 APK
#
set -euo pipefail
FRIDA_VERSION="${FRIDA_VERSION:-16.5.9}"
ARCH="${ARCH:-arm64}"
LISTEN_HOST="${LISTEN_HOST:-0.0.0.0}"
LISTEN_PORT="${LISTEN_PORT:-0}"
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
log() { echo -e "${GREEN}[+]${NC} $*"; }
warn() { echo -e "${YELLOW}[!]${NC} $*"; }
die() { echo -e "${RED}[✗]${NC} $*" >&2; exit 1; }
APK_PATH="${1:-}"
OUTPUT_DIR="${2:-./output}"
[ -z "$APK_PATH" ] && die "用法: $0 <wechat.apk> [output_dir]"
[ -f "$APK_PATH" ] || die "APK 不存在: $APK_PATH"
command -v apktool >/dev/null || die "apktool 未安装brew install apktool"
command -v python3 >/dev/null || die "python3 未安装"
python3 -c "import lief" 2>/dev/null || die "lief 未安装pip install lief"
WORK_DIR=$(mktemp -d)
trap "rm -rf '$WORK_DIR'" EXIT
log "工作目录: $WORK_DIR"
log "APK: $APK_PATH"
# ── Step 1: apktool 解包 ──
log "Step 1/7: 解包 APK..."
apktool d -f -o "$WORK_DIR/unpacked" "$APK_PATH" --no-res 2>/dev/null || \
apktool d -f -o "$WORK_DIR/unpacked" "$APK_PATH"
log "解包完成"
# ── Step 2: 下载 frida-gadget ──
GADGET_ARCH_MAP_arm64="arm64"
GADGET_ARCH_MAP_arm="arm"
GADGET_ARCH_MAP_x86="x86"
GADGET_ARCH_MAP_x86_64="x86_64"
GADGET_URL="https://github.com/AKA-L/frida-gadget-fix/releases/download/${FRIDA_VERSION}/frida-gadget-${FRIDA_VERSION}-android-${ARCH}.so.xz"
GADGET_FILE="$WORK_DIR/frida-gadget-${ARCH}.so"
if [ -f "$GADGET_FILE" ]; then
log "Step 2/7: Gadget 已存在,跳过下载"
else
log "Step 2/7: 下载 frida-gadget ${FRIDA_VERSION} (${ARCH})..."
curl -sL "$GADGET_URL" -o "$WORK_DIR/gadget.so.xz" || {
GADGET_URL="https://github.com/AKA-L/frida-gadget-fix/releases/download/${FRIDA_VERSION}/frida-gadget-${FRIDA_VERSION}-android-${ARCH}.so.xz"
warn "主源失败,尝试备用源..."
curl -sL "$GADGET_URL" -o "$WORK_DIR/gadget.so.xz" || die "下载 frida-gadget 失败"
}
xz -d "$WORK_DIR/gadget.so.xz"
mv "$WORK_DIR/gadget.so" "$GADGET_FILE"
log "下载完成: $(du -h "$GADGET_FILE" | cut -f1)"
fi
# ── Step 3: LIEF 注入 gadget ──
LIB_DIR="$WORK_DIR/unpacked/lib/${ARCH}"
[ -d "$LIB_DIR" ] || {
if [ "$ARCH" = "arm64" ] && [ -d "$WORK_DIR/unpacked/lib/arm64-v8a" ]; then
LIB_DIR="$WORK_DIR/unpacked/lib/arm64-v8a"
elif [ "$ARCH" = "arm" ] && [ -d "$WORK_DIR/unpacked/lib/armeabi-v7a" ]; then
LIB_DIR="$WORK_DIR/unpacked/lib/armeabi-v7a"
else
die "未找到 lib/${ARCH} 目录,当前有: $(ls "$WORK_DIR/unpacked/lib/" 2>/dev/null || echo '无')"
fi
}
log "Step 3/7: LIEF 注入 Gadget..."
TARGET_SO=$(find "$LIB_DIR" -name "*.so" -not -name "libfrida*" -not -name "libgadget*" | head -1)
[ -z "$TARGET_SO" ] && die "未找到可注入的 .so 文件"
TARGET_NAME=$(basename "$TARGET_SO")
log " 目标 so: $TARGET_NAME"
cp "$GADGET_FILE" "$LIB_DIR/libgadget.so"
python3 << PYEOF
import lief
import sys
target_path = "$TARGET_SO"
binary = lief.parse(target_path)
if binary is None:
print("LIEF 解析失败: " + target_path)
sys.exit(1)
existing = [str(lib) for lib in binary.libraries]
if "libgadget.so" in existing:
print("libgadget.so 已存在于 NEEDED 中,跳过")
else:
binary.add_library("libgadget.so")
print("已添加 libgadget.so 到 NEEDED")
binary.write(target_path)
print("LIEF 注入完成: " + target_path)
PYEOF
log " LIEF 注入完成"
# ── Step 4: 写入 gadget 配置 ──
if [ "$LISTEN_PORT" = "0" ]; then
LISTEN_PORT=$((RANDOM % 50000 + 10000))
fi
GADGET_CONFIG='{
"interaction": {
"type": "listen",
"address": "'"$LISTEN_HOST"'",
"port": '"$LISTEN_PORT"',
"on_port_conflict": "pick-next",
"on_load": "resume"
}
}'
CONFIG_SO_NAME="libgadget.config.so"
echo "$GADGET_CONFIG" > "$LIB_DIR/$CONFIG_SO_NAME"
log "Step 4/7: Gadget 配置写入 (port=$LISTEN_PORT)"
log " 配置: $GADGET_CONFIG"
# ── Step 5: apktool 重新打包 ──
log "Step 5/7: 重新打包..."
OUTPUT_UNSIGNED="$WORK_DIR/wechat-gadget-unsigned.apk"
apktool b -o "$OUTPUT_UNSIGNED" "$WORK_DIR/unpacked" 2>/dev/null || \
apktool b -o "$OUTPUT_UNSIGNED" "$WORK_DIR/unpacked"
log "打包完成"
# ── Step 6: 签名 ──
log "Step 6/7: 签名..."
KEYSTORE="$WORK_DIR/debug.keystore"
keytool -genkey -v -keystore "$KEYSTORE" -alias debug \
-keyalg RSA -keysize 2048 -validity 10000 \
-storepass android -keypass android \
-dname "CN=Debug,OU=Debug,O=Debug,L=Debug,S=Debug,C=CN" \
2>/dev/null
OUTPUT_SIGNED="$WORK_DIR/wechat-gadget-signed.apk"
jarsigner -verbose -sigalg SHA256withRSA -digestalg SHA-256 \
-keystore "$KEYSTORE" -storepass android -keypass android \
-signedjar "$OUTPUT_SIGNED" "$OUTPUT_UNSIGNED" debug \
2>/dev/null
log "签名完成"
# ── Step 7: 输出 ──
mkdir -p "$OUTPUT_DIR"
FINAL_APK="$OUTPUT_DIR/wechat-frida-gadget.apk"
cp "$OUTPUT_SIGNED" "$FINAL_APK"
log "Step 7/7: 输出完成"
echo ""
echo "============================================"
echo " 免 Root Frida Gadget 注入完成"
echo "============================================"
echo " 输出 APK: $FINAL_APK"
echo " 大小: $(du -h "$FINAL_APK" | cut -f1)"
echo " Gadget 端口: $LISTEN_PORT"
echo " Gadget 地址: $LISTEN_HOST:$LISTEN_PORT"
echo ""
echo " 安装到设备:"
echo " adb install -r '$FINAL_APK'"
echo ""
echo " 连接 Frida (本机):"
echo " adb forward tcp:$LISTEN_PORT tcp:$LISTEN_PORT"
echo " frida -H 127.0.0.1:$LISTEN_PORT Gadget"
echo ""
echo " 连接 Frida (远程/WiFi):"
echo " frida -H <设备IP>:$LISTEN_PORT Gadget"
echo ""
echo " SDK 配置 (FridaManager):"
echo " mode='gadget', gadget_port=$LISTEN_PORT"
echo "============================================"

View File

@@ -0,0 +1,63 @@
#!/usr/bin/env bash
# 安装 Mac 常驻「连接守护」LaunchAgent每 15s 扫描 ADB + 自动拉起 Termux Agent
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SDK_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
PROJ_ROOT="$(cd "${SDK_ROOT}/.." && pwd)"
LABEL="com.karuo.workphone.connection-keeper"
PLIST="${HOME}/Library/LaunchAgents/${LABEL}.plist"
LOG="${SDK_ROOT}/logs/connection_keeper.log"
mkdir -p "${SDK_ROOT}/logs" "${HOME}/Library/LaunchAgents"
cat > "${PLIST}" <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>${LABEL}</string>
<key>ProgramArguments</key>
<array>
<string>/bin/bash</string>
<string>${SDK_ROOT}/scripts/connection_keeper_daemon.sh</string>
</array>
<key>WorkingDirectory</key>
<string>${PROJ_ROOT}</string>
<key>EnvironmentVariables</key>
<dict>
<key>PATH</key>
<string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
<key>ANDROID_ADB_SERVER_PORT</key>
<string>5038</string>
<key>WORKPHONE_SDK_PORT</key>
<string>8899</string>
<key>KEEPER_INTERVAL</key>
<string>15</string>
<key>DEVICE_SERIAL</key>
<string>192.168.110.80:5555</string>
<key>DEVICE_ID</key>
<string>xgfe65eimrrofyws</string>
</dict>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardOutPath</key>
<string>${LOG}</string>
<key>StandardErrorPath</key>
<string>${LOG}</string>
</dict>
</plist>
EOF
launchctl bootout "gui/$(id -u)/${LABEL}" 2>/dev/null || true
launchctl bootstrap "gui/$(id -u)" "${PLIST}"
launchctl enable "gui/$(id -u)/${LABEL}"
launchctl kickstart -k "gui/$(id -u)/${LABEL}" 2>/dev/null || \
bash "${SDK_ROOT}/scripts/connection_keeper_daemon.sh" --bg
echo "✅ 连接守护 LaunchAgent 已安装: ${LABEL}"
echo " 日志: ${LOG}"
echo " 停止: bash ${SDK_ROOT}/scripts/connection_keeper_daemon.sh --stop"

View File

@@ -0,0 +1,78 @@
#!/usr/bin/env bash
# 安装 / 卸载 macOS LaunchAgent定时运行 docker_engine_guard.sh降低「引擎假死 → 无鲸鱼/打不开」复发概率
#
# 安装bash sdk/scripts/install_docker_engine_guard.sh
# 卸载bash sdk/scripts/install_docker_engine_guard.sh --uninstall
set -euo pipefail
LABEL="com.workphone.docker-engine-guard"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
GUARD_SRC="${SCRIPT_DIR}/docker_engine_guard.sh"
# launchd 从「文稿」等目录执行脚本常被 TCC 拒绝,固定拷到 Application Support
INSTALL_DIR="${HOME}/Library/Application Support/com.workphone"
GUARD_SCRIPT="${INSTALL_DIR}/docker_engine_guard.sh"
PLIST_DST="${HOME}/Library/LaunchAgents/${LABEL}.plist"
LOG_OUT="${HOME}/Library/Logs/docker-engine-guard.log"
LOG_ERR="${HOME}/Library/Logs/docker-engine-guard.err.log"
if [[ "$(uname -s)" != "Darwin" ]]; then
echo "仅支持 macOS。"
exit 1
fi
if [[ ! -f "$GUARD_SRC" ]]; then
echo "缺少: $GUARD_SRC"
exit 1
fi
mkdir -p "$INSTALL_DIR"
cp -f "$GUARD_SRC" "$GUARD_SCRIPT"
chmod +x "$GUARD_SCRIPT"
if [[ "${1:-}" == "--uninstall" ]]; then
launchctl bootout "gui/$(id -u)" "$PLIST_DST" 2>/dev/null || launchctl unload "$PLIST_DST" 2>/dev/null || true
rm -f "$PLIST_DST"
rm -f "$GUARD_SCRIPT" 2>/dev/null || true
rmdir "${INSTALL_DIR}" 2>/dev/null || true
echo "已卸载 LaunchAgent: $LABEL"
exit 0
fi
mkdir -p "${HOME}/Library/LaunchAgents"
# StartInterval600=每 10 分钟巡检一次(可按需改小/改大)
cat >"$PLIST_DST" <<PLIST
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>${LABEL}</string>
<key>ProgramArguments</key>
<array>
<string>/bin/bash</string>
<string>${GUARD_SCRIPT}</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>StartInterval</key>
<integer>600</integer>
<key>EnvironmentVariables</key>
<dict>
<key>PATH</key>
<string>/opt/homebrew/bin:/usr/local/bin:${HOME}/.docker/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
</dict>
<key>StandardOutPath</key>
<string>${LOG_OUT}</string>
<key>StandardErrorPath</key>
<string>${LOG_ERR}</string>
</dict>
</plist>
PLIST
launchctl bootout "gui/$(id -u)" "$PLIST_DST" 2>/dev/null || true
launchctl bootstrap "gui/$(id -u)" "$PLIST_DST" 2>/dev/null || launchctl load "$PLIST_DST"
echo "已安装并加载: $PLIST_DST"
echo " 周期: 每 10 分钟检查;日志: $LOG_OUT / $LOG_ERR"
echo "卸载: bash sdk/scripts/install_docker_engine_guard.sh --uninstall"

View File

@@ -0,0 +1,57 @@
#!/usr/bin/env bash
# 安装 Mac 常驻「SDK 连接监视器」(无 USB 铁律 · 只盯 NAS/frp/WS
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SDK_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
PROJ_ROOT="$(cd "${SDK_ROOT}/.." && pwd)"
LABEL="com.karuo.workphone.sdk-watcher"
PLIST="${HOME}/Library/LaunchAgents/${LABEL}.plist"
LOG="${SDK_ROOT}/logs/sdk_connection_watcher.log"
mkdir -p "${SDK_ROOT}/logs" "${HOME}/Library/LaunchAgents"
cat > "${PLIST}" <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>${LABEL}</string>
<key>ProgramArguments</key>
<array>
<string>/bin/bash</string>
<string>${SDK_ROOT}/scripts/sdk_connection_watcher.sh</string>
</array>
<key>WorkingDirectory</key>
<string>${PROJ_ROOT}</string>
<key>EnvironmentVariables</key>
<dict>
<key>WORKPHONE_SDK_PORT</key>
<string>8899</string>
<key>DEVICE_ID</key>
<string>xgfe65eimrrofyws</string>
<key>WATCHER_INTERVAL</key>
<string>20</string>
</dict>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardOutPath</key>
<string>${LOG}</string>
<key>StandardErrorPath</key>
<string>${LOG}</string>
</dict>
</plist>
EOF
launchctl bootout "gui/$(id -u)/${LABEL}" 2>/dev/null || true
launchctl bootstrap "gui/$(id -u)" "${PLIST}"
launchctl enable "gui/$(id -u)/${LABEL}"
launchctl kickstart -k "gui/$(id -u)/${LABEL}" 2>/dev/null || \
bash "${SDK_ROOT}/scripts/sdk_connection_watcher.sh" --bg
echo "✅ SDK 监视器 LaunchAgent 已安装: ${LABEL}"
echo " 日志: ${LOG}"
echo " 当前 SDK: source ${SDK_ROOT}/config/active_sdk.env"

View File

@@ -0,0 +1,94 @@
#!/usr/bin/env bash
# 下载并安装微信 8.0.51 到当前 ADB 设备
# 用法: bash install_wechat_8.0.51.sh [APK路径] [设备序列号]
# 若未指定 APK会尝试从 Uptodown 下载;若失败则提示手动下载后重试。
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SDK_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
APKS_DIR="${SDK_DIR}/apks"
APK_NAME="wechat-8.0.51.apk"
DEFAULT_APK="${APKS_DIR}/${APK_NAME}"
# 可选: 传入 APK 路径 或 设备序列号
CUSTOM_APK="${1:-}"
DEVICE_SERIAL="${2:-}"
# 确定要安装的 APK
if [[ -n "$CUSTOM_APK" && -f "$CUSTOM_APK" ]]; then
INSTALL_APK="$CUSTOM_APK"
elif [[ -f "$DEFAULT_APK" ]]; then
INSTALL_APK="$DEFAULT_APK"
else
# 尝试从用户 Downloads 中找最近下载的微信/WeChat 相关 APK
DOWNLOADS_APK=""
if [[ -d "${HOME}/Downloads" ]]; then
for f in "${HOME}/Downloads/"*[Ww]echat*.apk "${HOME}/Downloads/"*8.0.51*.apk "${HOME}/Downloads/"*[Ww]eixin*.apk; do
[[ -f "$f" ]] && DOWNLOADS_APK="$f" && break
done
if [[ -z "$DOWNLOADS_APK" ]]; then
DOWNLOADS_APK=$(ls -t "${HOME}/Downloads/"*.apk 2>/dev/null | head -1)
fi
fi
if [[ -n "$DOWNLOADS_APK" && -f "$DOWNLOADS_APK" ]]; then
INSTALL_APK="$DOWNLOADS_APK"
echo "使用 Downloads 中的 APK: $INSTALL_APK"
else
INSTALL_APK=""
fi
fi
# 若本地没有 APK提示手动下载APKMirror/Uptodown 需浏览器验证,无法命令行直下)
download_apk() {
mkdir -p "$APKS_DIR"
return 1
}
# 主流程
echo "=== 微信 8.0.51 安装脚本 ==="
echo "APK 目录: $APKS_DIR"
echo ""
if [[ -z "$INSTALL_APK" ]]; then
if ! download_apk; then
echo "未找到本地 APK。请先下载微信 8.0.51 后重试:"
echo ""
echo " 1) 在浏览器中打开以下任一链接:"
echo " • https://wechat.en.uptodown.com/android/versions (选择 8.0.51"
echo " • https://www.apkmirror.com/apk/wechat-tencent/wechat/wechat-8-0-51-release/wechat-8-0-51-android-apk-download/"
echo " 2) 将下载的 APK 保存为: $DEFAULT_APK"
echo " 3) 重新运行: $0"
echo ""
echo "或指定已下载的 APK 路径: $0 /path/to/wechat-8.0.51.apk"
exit 1
fi
INSTALL_APK="$DEFAULT_APK"
fi
# 检查设备
ADB_DEVICES=$(adb devices -l 2>/dev/null | grep -w "device" | awk '{print $1}')
if [[ -z "$ADB_DEVICES" ]]; then
echo "未检测到 ADB 设备,请连接设备并开启 USB 调试。"
exit 1
fi
if [[ -n "$DEVICE_SERIAL" ]]; then
SERIAL="$DEVICE_SERIAL"
else
# 多设备时取第一个
SERIAL=$(echo "$ADB_DEVICES" | head -1)
fi
echo "使用设备: $SERIAL"
echo "安装包: $INSTALL_APK"
echo ""
# 覆盖安装(-r若失败可先手动卸载: adb uninstall com.tencent.mm
echo "正在安装..."
if adb -s "$SERIAL" install -r -t "$INSTALL_APK"; then
echo ""
echo "安装完成。设备上的微信已为 8.0.51 版本。"
else
echo "安装失败。若为签名冲突,请先卸载设备上的微信后再运行本脚本。"
exit 1
fi

165
sdk/scripts/lan_connect_setup.sh Executable file
View File

@@ -0,0 +1,165 @@
#!/usr/bin/env bash
# 局域网无线主控:本机局网 IP ↔ 手机同 WiFi ↔ Termux Agent / 无线 ADB
# 用法:
# bash sdk/scripts/lan_connect_setup.sh
# PHONE_IP=192.168.110.40 bash sdk/scripts/lan_connect_setup.sh
# bash sdk/scripts/lan_connect_setup.sh --connect # 尝试 adb connect 后 oneclick
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SDK_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
SDK_PORT="${WORKPHONE_SDK_PORT:-8899}"
DEVICE_ID="${DEVICE_ID:-xgfe65eimrrofyws}"
PHONE_IP="${PHONE_IP:-}"
ADB_PORT="${ANDROID_ADB_SERVER_PORT:-5038}"
ADB=(adb -P "${ADB_PORT}")
export ANDROID_ADB_SERVER_PORT="${ADB_PORT}"
LAN_ENV="${SDK_ROOT}/config/lan.env"
DO_CONNECT=0
[[ "${1:-}" == "--connect" ]] && DO_CONNECT=1
pick_lan_ip() {
local ip
for ip in $(ifconfig | awk '/inet /{print $2}' | grep -v '^127\.'); do
[[ "$ip" == 198.18.* ]] && continue
[[ "$ip" == 192.168.110.* ]] && { echo "$ip"; return; }
done
for ip in $(ifconfig | awk '/inet /{print $2}' | grep -v '^127\.'); do
[[ "$ip" == 198.18.* ]] && continue
[[ "$ip" == 192.168.* ]] && { echo "$ip"; return; }
done
ipconfig getifaddr en0 2>/dev/null || ipconfig getifaddr en1 2>/dev/null || echo "127.0.0.1"
}
LAN_IP="$(pick_lan_ip)"
WS_URL="ws://${LAN_IP}:${SDK_PORT}/ws/device/${DEVICE_ID}"
API_URL="http://${LAN_IP}:${SDK_PORT}"
log() { echo "[lan] $*"; }
log "本机局网 IP: ${LAN_IP}"
log "SDK API: ${API_URL}"
log "Agent WS: ${WS_URL}"
mkdir -p "${SDK_ROOT}/config"
cat >"${LAN_ENV}" <<EOF
# 自动生成 $(date '+%F %T') — source 后用于脚本
export WORKPHONE_LAN_IP=${LAN_IP}
export WORKPHONE_SDK_HOST=${LAN_IP}:${SDK_PORT}
export WORKPHONE_WS_URL=${WS_URL}
export WORKPHONE_API_URL=${API_URL}
export ANDROID_ADB_SERVER_PORT=${ADB_PORT}
export WIRELESS_IPS="${PHONE_IP:-192.168.110.40 192.168.110.80 192.168.110.236}"
EOF
log "已写入 ${LAN_ENV}"
# ── 1) SDK 本机 / 局网可达性 ──
"${ADB[@]}" start-server >/dev/null 2>&1 || true
SDK_VIA="local"
if docker ps --format '{{.Names}}' 2>/dev/null | grep -qx workphone-sdk; then
SDK_VIA="docker"
log "SDK 由 Docker workphone-sdk 提供 :${SDK_PORT}"
if ! curl -sf --max-time 5 "http://127.0.0.1:${SDK_PORT}/health" >/dev/null 2>&1; then
log "Docker SDK 未响应,尝试 adb 桥接后重启容器…"
bash "${SCRIPT_DIR}/adb_host_server_listen_all.sh" --background || true
(cd "${SDK_ROOT}" && ADB_SERVER_SOCKET="tcp:host.docker.internal:${ADB_PORT}" docker compose up -d workphone-sdk) || true
sleep 4
fi
elif curl -sf --max-time 3 "http://127.0.0.1:${SDK_PORT}/health" >/dev/null 2>&1; then
log "SDK localhost:${SDK_PORT} ✅ (${SDK_VIA})"
else
log "SDK 未运行 → 启动 uvicorn 0.0.0.0:${SDK_PORT}"
cd "${SDK_ROOT}/app"
nohup env ANDROID_ADB_SERVER_PORT="${ADB_PORT}" python3 -m uvicorn main:app \
--host 0.0.0.0 --port "${SDK_PORT}" >>"${SDK_ROOT}/logs/sdk.log" 2>&1 &
sleep 3
fi
if [[ "$SDK_VIA" == "docker" ]]; then
bash "${SCRIPT_DIR}/adb_host_server_listen_all.sh" --background || true
fi
if curl -sf --max-time 5 "${API_URL}/health" >/dev/null 2>&1; then
log "SDK 局网 ${API_URL}/health ✅"
else
log "⚠️ 局网 ${API_URL} 不可达 — 检查 Mac 防火墙是否放行 ${SDK_PORT},或确认手机与 Mac 同网段"
fi
# ── 2) 探测手机(同网段 5555──
try_adb_connect() {
local host="$1" port="${2:-5555}"
local target="${host}:${port}"
"${ADB[@]}" connect "${target}" >/dev/null 2>&1 || true
sleep 2
if "${ADB[@]}" -s "${target}" get-state 2>/dev/null | grep -qx device; then
printf '%s' "${target}"
return 0
fi
"${ADB[@]}" disconnect "${target}" >/dev/null 2>&1 || true
return 1
}
FOUND=""
ADB_SERIAL=""
if [[ -n "$PHONE_IP" ]]; then
if try_adb_connect "$PHONE_IP" >/dev/null; then
ADB_SERIAL="${PHONE_IP}:5555"
FOUND="${ADB_SERIAL}"
fi
fi
if [[ -z "$FOUND" ]]; then
for ip in 192.168.110.40 192.168.110.80 192.168.110.236 192.168.1.12; do
ping -c 1 -W 1 "$ip" >/dev/null 2>&1 || continue
if try_adb_connect "$ip" >/dev/null; then
ADB_SERIAL="${ip}:5555"
FOUND="${ADB_SERIAL}"
break
fi
done
fi
if [[ -n "$FOUND" ]]; then
log "无线 ADB 已连接: ${FOUND}"
else
log "暂未发现无线 ADB手机需开发者选项 → 无线调试 / 或 USB 执行一次 adb tcpip 5555"
fi
# ── 3) Termux 手机端命令(手机连本机 IP──
FRIDA_PORT="$(python3 -c "import json; print(json.load(open('${SCRIPT_DIR}/anti_detect/phantom_frida_config.json'))['listen_port'])" 2>/dev/null || echo 17263)"
echo ""
echo "================================================================"
echo " 手机 Termux 执行(与 Mac 同一 WiFi · 连 ${LAN_IP}"
echo "================================================================"
cat <<EOF
export WP_AGENT_ON_DEVICE=1
export WP_FRIDA_MODE=remote
export WP_FRIDA_PORT=${FRIDA_PORT}
export WP_SERVER_URL=${WS_URL}
cd ~/workphone/agent || cd ~/storage/downloads/workphone/agent
python agent.py -d ${DEVICE_ID} -s ${WS_URL} --heartbeat 10
# 或依赖 UDP 8898 自动发现(免手填 IP
# python agent.py -d ${DEVICE_ID} --heartbeat 10
EOF
echo "================================================================"
echo " Mac 验收"
echo "================================================================"
echo " curl ${API_URL}/health"
echo " curl ${API_URL}/api/v3/connection/status"
echo " curl ${API_URL}/api/v3/hook/probe/${DEVICE_ID}"
echo ""
if [[ "$DO_CONNECT" == "1" && -n "$ADB_SERIAL" ]]; then
log "执行 oneclick device=${ADB_SERIAL}"
DEVICE_SERIAL="$ADB_SERIAL" ANDROID_ADB_SERVER_PORT="${ADB_PORT}" \
bash "${SCRIPT_DIR}/frida_workphone_oneclick.sh" -d "$ADB_SERIAL" || true
fi
curl -s --max-time 5 "${API_URL}/api/v3/connection/status" 2>/dev/null | python3 -m json.tool 2>/dev/null | head -25 || true

158
sdk/scripts/master_deploy.sh Executable file
View File

@@ -0,0 +1,158 @@
#!/bin/bash
# ================================================================
# AI数智员工 统一批量部署脚本 v4.0
# ================================================================
#
# 一键将 AI数智员工 统一 APK 部署到多台手机
# v4.0: 不再需要 Termux/Python Agent — 全部功能已内置于 APK
#
# 用法:
# ./master_deploy.sh --server ws://IP:8899/ws/device
# ./master_deploy.sh --server ws://IP:8899/ws/device --wifi-scan
# ./master_deploy.sh --server ws://IP:8899/ws/device --devices "serial1,serial2"
#
# 前置: adb 已安装, 设备已通过 USB 或 WiFi ADB 连接
# ================================================================
set -eo pipefail
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[0;33m'
CYAN='\033[0;36m'; BOLD='\033[1m'; NC='\033[0m'
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
APK_PATH="${PROJECT_ROOT}/android-app/app/build/outputs/apk/debug/app-debug.apk"
PACKAGE_NAME="com.system.cloudservice"
ACTIVITY_NAME=".ui.SetupActivity"
SERVER_URL=""
DEVICES=""
WIFI_SCAN=false
PROJECT_ID="cunkebao"
usage() {
echo -e "${BOLD}AI数智员工 统一部署 v4.0${NC}"
echo "用法: $0 --server <ws_url> [选项]"
echo ""
echo "选项:"
echo " --server URL WebSocket 服务地址 (必填)"
echo " --project ID 项目 ID (默认: cunkebao)"
echo " --devices LIST 指定设备序列号 (逗号分隔)"
echo " --wifi-scan WiFi ADB 扫描局域网"
echo " --apk PATH 指定 APK 路径"
echo " -h, --help 显示帮助"
}
while [[ $# -gt 0 ]]; do
case $1 in
--server) SERVER_URL="$2"; shift 2 ;;
--project) PROJECT_ID="$2"; shift 2 ;;
--devices) DEVICES="$2"; shift 2 ;;
--wifi-scan) WIFI_SCAN=true; shift ;;
--apk) APK_PATH="$2"; shift 2 ;;
-h|--help) usage; exit 0 ;;
*) echo -e "${RED}未知参数: $1${NC}"; usage; exit 1 ;;
esac
done
if [[ -z "$SERVER_URL" ]]; then
echo -e "${RED}错误: 必须指定 --server${NC}"
usage
exit 1
fi
# Check APK exists
if [[ ! -f "$APK_PATH" ]]; then
echo -e "${YELLOW}APK 未找到: $APK_PATH${NC}"
echo -e "${CYAN}正在构建...${NC}"
cd "${PROJECT_ROOT}/android-app"
./gradlew assembleDebug --no-daemon
if [[ ! -f "$APK_PATH" ]]; then
echo -e "${RED}构建失败${NC}"
exit 1
fi
fi
echo -e "${GREEN}${BOLD}╔══════════════════════════════════════╗${NC}"
echo -e "${GREEN}${BOLD}║ AI数智员工 统一部署 v4.0 ║${NC}"
echo -e "${GREEN}${BOLD}╚══════════════════════════════════════╝${NC}"
echo -e " APK: $APK_PATH"
echo -e " Server: $SERVER_URL"
echo -e " Project: $PROJECT_ID"
echo ""
# WiFi scan
if $WIFI_SCAN; then
echo -e "${CYAN}扫描局域网 ADB 设备...${NC}"
LOCAL_IP=$(ip route get 1 2>/dev/null | awk '{print $NF; exit}' || echo "192.168.1")
SUBNET="${LOCAL_IP%.*}"
for i in $(seq 1 254); do
(adb connect "${SUBNET}.$i:5555" &>/dev/null &)
done
sleep 3
fi
# Get device list
if [[ -n "$DEVICES" ]]; then
IFS=',' read -ra DEVICE_LIST <<< "$DEVICES"
else
mapfile -t DEVICE_LIST < <(adb devices | grep -v "^$" | grep -v "^List" | awk '{print $1}')
fi
if [[ ${#DEVICE_LIST[@]} -eq 0 ]]; then
echo -e "${RED}未发现设备${NC}"
exit 1
fi
echo -e "${GREEN}发现 ${#DEVICE_LIST[@]} 台设备${NC}"
echo ""
deploy_device() {
local serial="$1"
local adb_cmd="adb -s $serial"
echo -e "${CYAN}── 部署: $serial ──${NC}"
# 1. Install APK
echo -e " 安装 APK..."
$adb_cmd install -r -g "$APK_PATH" 2>/dev/null || {
echo -e " ${RED}安装失败${NC}"
return 1
}
echo -e " ${GREEN}安装成功${NC}"
# 2. Grant permissions
echo -e " 授权..."
for perm in CAMERA RECORD_AUDIO POST_NOTIFICATIONS SYSTEM_ALERT_WINDOW; do
$adb_cmd shell pm grant "$PACKAGE_NAME" "android.permission.$perm" 2>/dev/null || true
done
# 3. Battery optimization whitelist
$adb_cmd shell dumpsys deviceidle whitelist +$PACKAGE_NAME 2>/dev/null || true
# 4. Launch app
echo -e " 启动..."
$adb_cmd shell am start -n "${PACKAGE_NAME}/${PACKAGE_NAME}${ACTIVITY_NAME}" 2>/dev/null || true
echo -e " ${GREEN}✅ 部署完成${NC}"
echo ""
}
SUCCESS=0
FAIL=0
for device in "${DEVICE_LIST[@]}"; do
device=$(echo "$device" | xargs)
[[ -z "$device" ]] && continue
if deploy_device "$device"; then
((SUCCESS++))
else
((FAIL++))
fi
done
echo -e "${BOLD}══════════════════════════════════════${NC}"
echo -e "${GREEN}成功: $SUCCESS${NC} ${RED}失败: $FAIL${NC} 总计: ${#DEVICE_LIST[@]}"
echo ""
echo -e "${YELLOW}提示: 在手机上打开 APP → 扫码绑定 即可连接服务器${NC}"
echo -e "${YELLOW} v4.0 无需安装 Termux / Python Agent${NC}"

View File

@@ -0,0 +1,335 @@
#!/usr/bin/env python3
"""矩阵补全Hook catalog 全 action 真机 hook/execute 探针。"""
from __future__ import annotations
import argparse
import asyncio
import json
import os
import sys
import time
from datetime import datetime
from pathlib import Path
import httpx
BASE = os.environ.get("SDK_BASE_URL", "http://127.0.0.1:8899")
DEVICE = os.environ.get("SDK_DEVICE_ID", "xgfe65eimrrofyws")
TO = "filehelper"
OUT = Path(__file__).resolve().parents[1] / "tmp"
SAFE_PARAMS: dict[str, dict] = {
"ping": {},
"send_message": {"to_id": TO, "content": "[catalog-probe]", "msg_type": "text"},
"batch_send": {"user_ids": [TO], "content": "[catalog-probe]"},
"mass_send": {"user_ids": [TO], "content": "[catalog-probe]"},
"get_messages": {"conversation_id": TO, "limit": 3},
"get_contacts": {"limit": 5},
"get_contact_info": {"wxid": TO},
"get_friend_info": {"user_id": TO},
"search_contacts": {"keyword": "文件", "limit": 5},
"search_contact": {"keyword": "文件", "limit": 5},
"search_messages": {"keyword": "test", "limit": 5},
"wechat_search": {"keyword": "微信", "limit": 5},
"get_groups": {"limit": 5},
"get_group_info": {"group_id": "test@chatroom"},
"get_group_members": {"group_id": "test@chatroom"},
"get_profile": {},
"check_account_status": {},
"get_favorites": {"limit": 5},
"get_labels": {},
"get_moments": {"limit": 3},
"browse_channels": {"limit": 3},
"global_search": {"keyword": "微信", "limit": 5},
"generate_my_qr_code": {},
"generate_group_qr_code": {"group_id": "test@chatroom"},
"get_wallet_balance": {},
"get_transaction_history": {"limit": 5},
"get_wechat_steps": {},
"get_sticker_list": {},
"get_top_stories": {},
"get_safety_center": {},
"check_restrictions": {},
"show_payment_code": {},
"pin_chat": {"wxid": TO, "pin": False},
"set_chat_top": {"user_id": TO, "enable": False},
"set_do_not_disturb": {"wxid": TO, "enable": False},
"add_favorite": {"content": "catalog-probe", "type": "text"},
"add_to_favorites": {"content": "catalog-probe", "type": "text"},
"set_privacy": {"setting": "moments_privacy", "value": "all"},
"set_moments_privacy": {"privacy_type": "all", "days": 180},
"set_moments_cover": {"image_path": "/sdcard/test.jpg"},
"open_mini_program": {"name": "微信支付"},
"open_miniprogram": {"name": "微信支付"},
"send_location": {"to_id": TO, "latitude": 24.48, "longitude": 118.08, "label": "probe"},
"send_emoji": {"to_id": TO, "emoji_name": "微笑"},
"send_voice": {"to_id": TO, "duration": 1},
"send_voice_message": {"to_id": TO, "duration": 1},
"send_file": {"to_id": TO, "file_path": "/sdcard/test.txt"},
"send_file_from_chat": {"to_id": TO, "file_path": "/sdcard/test.txt"},
"send_image": {"to_id": TO, "image_path": "/sdcard/test.jpg"},
"send_video": {"to_id": TO, "video_path": "/sdcard/test.mp4"},
"send_link": {"to_id": TO, "url": "https://example.com", "title": "probe"},
"send_card": {"to_id": TO, "card_wxid": TO},
"send_group_message": {"group_id": "test@chatroom", "content": "probe"},
"like_channel_video": {"index": 0},
"like_video": {"index": 0},
"like_wechat_steps": {"user_id": TO},
"like_moments": {"sns_id": "probe_sns"},
"comment_moments": {"sns_id": "probe_sns", "comment": "probe"},
"comment_channel_video": {"video_id": "index_0", "comment": "probe"},
"clear_cache": {},
"check_for_update": {},
"post_moments": {"content": "[catalog-probe]"},
"forward_moments_link": {"content": "https://example.com probe"},
"set_signature": {"signature": "catalog-probe"},
"set_nickname": {"nickname": "probe"},
"set_region": {"region": "福建 厦门"},
"set_avatar": {"image_path": "/sdcard/test.jpg"},
"set_remark": {"user_id": TO, "remark": "probe"},
"set_friend_remark": {"wxid": TO, "remark": "probe"},
"create_label": {"name": "catalog_probe"},
"create_tag": {"tag_name": "catalog_probe"},
"get_contacts_by_label": {"label_id": "1"},
"get_users_by_tag": {"tag_name": "catalog_probe"},
"add_friend": {"user_id": "28533368", "message": "catalog-probe"},
"accept_friend": {"encrypt_username": "probe", "ticket": "probe"},
"delete_friend": {"wxid": "invalid_wxid_probe"},
"invite_to_group": {"group_id": "test@chatroom", "member_ids": [TO]},
"remove_from_group": {"group_id": "test@chatroom", "member_ids": [TO]},
"create_group": {"member_ids": [TO, TO], "group_name": "catalog-probe"},
"set_group_announcement": {"group_id": "test@chatroom", "announcement": "probe"},
"set_group_notice": {"group_id": "test@chatroom", "notice": "probe"},
"set_group_name": {"group_id": "test@chatroom", "name": "probe"},
"quit_group": {"group_id": "test@chatroom"},
"set_contact_label": {"wxid": TO, "label_ids": []},
"add_tag": {"wxid": TO, "tag_name": "catalog_probe"},
"remove_tag": {"wxid": TO, "tag_name": "catalog_probe"},
"delete_label": {"label_id": "1"},
"delete_tag": {"label_id": "1"},
"delete_moments": {"sns_id": "1"},
"delete_favorite": {"local_id": "1"},
"forward_message": {"to_id": TO, "msg_svr_id": "1779123054724"},
"forward_multiple": {"to_id": TO, "msg_svr_ids": ["1779123054724"]},
"recall_message": {"msg_svr_id": "1779123054724"},
"revoke_message": {"msg_svr_id": "1779123054724"},
"send_red_packet": {"to_id": TO, "amount": "0.01", "message": "probe"},
"send_transfer": {"to_id": TO, "amount": "0.01", "description": "probe"},
"transfer": {"to_id": TO, "amount": "0.01", "description": "probe"},
"receive_red_packet": {"msg_svr_id": "1"},
"receive_transfer": {"msg_svr_id": "1"},
"receive_payment": {"msg_svr_id": "1"},
"follow_channel": {"channel_id": "probe"},
"unfollow_channel": {"channel_id": "probe"},
"follow_official_account": {"account_id": "probe"},
"unfollow_official_account": {"account_id": "probe"},
"get_official_account_articles": {"account_id": "probe", "limit": 5},
"share_channel_video": {"video_id": "index_0", "to_id": TO},
"share_mini_program": {"app_id": "wxprobe", "to_id": TO, "title": "probe"},
"add_friend_by_qr": {"qr_content": "probe"},
"add_custom_emoji": {"image_path": "/sdcard/test.jpg"},
"add_to_float": {"wxid": TO},
"remove_from_float": {"wxid": TO},
"clear_chat_history": {"wxid": TO},
"clear_history": {"user_id": TO},
"bind_phone": {"phone": "13800138000"},
"change_password": {"old_password": "old", "new_password": "new"},
"remove_login_device": {"device_id": "probe"},
"register_account": {"phone": "13800138000", "nickname": "probe"},
"login_by_password": {"phone": "13800138000", "password": "probe"},
"login_by_sms": {"phone": "13800138000"},
"switch_account": {"wxid": TO},
"batch_execute": {"actions": [{"action": "getProfile", "params": {}}]},
"get_hook_status": {},
"get_wechat_version": {},
"get_process_info": {},
"voice_call": {"to_id": TO},
"video_call": {"to_id": TO},
"share_real_time_location": {"to_id": TO},
"download_file": {"url": "https://example.com/probe.txt", "save_path": "/sdcard/Download/wp_probe.txt"},
"reply_comment": {"sns_id": "probe_sns", "comment": "catalog-probe"},
"set_what_up": {"content": "catalog-probe"},
"logout": {},
"get_device_info": {},
"get_storage_info": {},
"get_network_info": {},
"get_recent_messages": {"limit": 5},
"get_recent_mini_programs": {},
"get_friend_requests": {},
"get_official_accounts": {"limit": 5},
"unblock_self": {},
"unbind_phone": {},
"enable_fingerprint": {},
"set_account_protection": {"enable": True},
"scan_qr_code": {},
"set_sex": {"sex": 1},
"set_notification": {"enable": True},
"set_chat_background": {"wxid": TO},
"auto_register": {},
"check_login_state": {},
"get_sim_phone": {},
# ── 奥创协议补全 29 action 真机探针参数(读类空参安全 / 写类给最小 probe真机验收才标 ✅)──
"get_conversations": {"limit": 5},
"get_message_by_id": {"msg_svr_id": "1779123054724"},
"get_msg_ids_by_time": {"start_time": 0},
"get_message_raw_xml": {"msg_svr_id": "1779123054724"},
"add_friend_in_room": {"group_id": "test@chatroom", "wxid": TO, "verify_message": "catalog-probe"},
"add_friend_from_phonebook": {"phone": "13800138000", "verify_message": "catalog-probe"},
"add_friend_with_scene": {"wxid": TO, "scene": "30", "verify_message": "catalog-probe"},
"add_friend_by_card": {"card_wxid": TO, "verify_message": "catalog-probe"},
"join_group_by_qr": {"qr_content": "probe"},
"approve_group_invite": {"group_id": "test@chatroom", "wxid": TO},
"agree_join_group": {"group_id": "test@chatroom"},
"mark_conversation_read": {"conversation_id": TO},
"send_multi_image": {"to_id": TO, "images": ["/sdcard/test.jpg"]},
"send_jielong": {"to_id": TO, "content": "catalog-probe 接龙"},
"send_friend_verify": {"wxid": TO, "verify_message": "catalog-probe"},
"voice_to_text": {"msg_svr_id": "1779123054724"},
"get_friend_moments": {"wxid": TO},
"get_moment_detail": {"sns_id": "probe_sns"},
"delete_moment_comment": {"sns_id": "probe_sns", "comment_id": "1"},
"reply_moment_comment": {"sns_id": "probe_sns", "content": "catalog-probe"},
"stop_moments_praise": {},
"start_nurture": {"plan": "default"},
"stop_nurture": {},
"detect_zombie_fans": {},
"stop_detect_zombie_fans": {},
"query_red_packet": {"red_packet_id": "1"},
"get_wechat_location": {},
"search_official_account": {"keyword": "微信"},
"phone_action": {"op": "screenshot"},
}
def _load_actions():
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "app" / "agent"))
from hook.hook_executor import ACTION_TO_RPC, ACTION_ALIASES, resolve_action, normalize_params # type: ignore
actions = sorted(set(ACTION_TO_RPC.keys()) | set(ACTION_ALIASES.keys()))
return actions, resolve_action, normalize_params
async def probe_one(client: httpx.AsyncClient, device_id: str, action: str, resolve_action, normalize_params) -> dict:
canonical = resolve_action(action)
params = dict(SAFE_PARAMS.get(action) or SAFE_PARAMS.get(canonical, {}))
params = normalize_params(action, params)
t0 = time.time()
try:
resp = await client.post(
f"{BASE}/api/v3/hook/execute",
json={"device_id": device_id, "platform": "wechat", "action": action, "params": params, "hook_only": True},
timeout=60,
)
body = resp.json()
data = body.get("data") if isinstance(body.get("data"), dict) else body
ok = bool(data.get("success")) or body.get("code") == 200 and data.get("success") is not False and "不支持" not in str(data.get("message", ""))
if data.get("success") is False:
ok = False
err = "" if ok else str(data.get("message") or data.get("error") or body)[:120]
ch = body.get("channel_used") or data.get("_channel_used") or data.get("channel") or "hook"
except Exception as e:
ok, err, ch = False, str(e)[:160], "error"
# 间歇性 503 / frida_connected:False / WS 未连接 → 多次重试frida session 繁忙或瞬断)
retries = 0
while not ok and retries < 3 and any(
k in err for k in ("Frida", "frida_connected", "503", "未连接", "RPC 未成功")
):
retries += 1
await asyncio.sleep(2.0 * retries)
try:
resp = await client.post(
f"{BASE}/api/v3/hook/execute",
json={"device_id": device_id, "platform": "wechat", "action": action, "params": params, "hook_only": True},
timeout=60,
)
body = resp.json()
data = body.get("data") if isinstance(body.get("data"), dict) else body
ok = bool(data.get("success")) or (body.get("code") == 200 and data.get("success") is not False and "不支持" not in str(data.get("message", "")))
if data.get("success") is False:
ok = False
err = "" if ok else str(data.get("message") or data.get("error") or body)[:160]
ch = body.get("channel_used") or data.get("_channel_used") or "hook"
except Exception as e2:
err = str(e2)[:160]
return {"action": action, "success": ok, "channel": str(ch), "elapsed_ms": int((time.time() - t0) * 1000), "error": err, "retries": retries}
async def prefetch_context(client: httpx.AsyncClient, device_id: str) -> None:
"""从真机拉取 group_id / label_id 等,写入 SAFE_PARAMS。"""
try:
r = await client.post(
f"{BASE}/api/v3/hook/execute",
json={"device_id": device_id, "platform": "wechat", "action": "get_groups", "params": {"limit": 5}, "hook_only": True},
timeout=60,
)
groups = (r.json().get("data") or {}).get("groups") or []
gid = None
for g in groups:
candidate = g.get("group_id") or g.get("wxid") or g.get("username")
if not candidate:
continue
chk = await client.post(
f"{BASE}/api/v3/hook/execute",
json={"device_id": device_id, "platform": "wechat", "action": "get_group_info", "params": {"group_id": candidate}, "hook_only": True},
timeout=60,
)
if (chk.json().get("data") or {}).get("success"):
gid = candidate
break
if gid:
for k in (
"get_group_info", "get_group_members", "send_group_message",
"generate_group_qr_code", "invite_to_group", "remove_from_group",
"set_group_announcement", "set_group_notice", "set_group_name", "quit_group",
):
if k in SAFE_PARAMS:
SAFE_PARAMS[k]["group_id"] = gid
except Exception:
pass
try:
r = await client.post(
f"{BASE}/api/v3/hook/execute",
json={"device_id": device_id, "platform": "wechat", "action": "get_labels", "params": {}, "hook_only": True},
timeout=60,
)
labels = (r.json().get("data") or {}).get("labels") or []
if labels:
lid = str(labels[0].get("label_id") or labels[0].get("id") or "1")
SAFE_PARAMS["get_contacts_by_label"]["label_id"] = lid
SAFE_PARAMS["delete_tag"]["label_id"] = lid
except Exception:
pass
async def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("-d", "--device-id", default=DEVICE)
args = parser.parse_args()
actions, resolve_action, normalize_params = _load_actions()
results = []
async with httpx.AsyncClient(trust_env=False) as client:
await prefetch_context(client, args.device_id)
for action in actions:
row = await probe_one(client, args.device_id, action, resolve_action, normalize_params)
results.append(row)
print(f"{'' if row['success'] else ''} {action:<32} {row['elapsed_ms']:>5}ms{' (retry %d)' % row['retries'] if row.get('retries') else ''}")
await asyncio.sleep(0.6)
passed = sum(1 for x in results if x["success"])
report = {
"timestamp": datetime.now().isoformat(),
"device_id": args.device_id,
"total": len(results),
"passed": passed,
"failed": len(results) - passed,
"success_rate": f"{100 * passed / max(len(results), 1):.1f}%",
"results": results,
}
OUT.mkdir(parents=True, exist_ok=True)
path = OUT / f"matrix_hook_catalog_{args.device_id}_{int(time.time())}.json"
path.write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
print(f"\nHook catalog: {passed}/{len(results)}{path}")
return 0 if passed == len(results) else 1
if __name__ == "__main__":
raise SystemExit(asyncio.run(main()))

View File

@@ -0,0 +1,124 @@
#!/usr/bin/env python3
"""
微信全功能矩阵 v8.0.56 — 真机 Frida Hook 验收脚本
口径:
- 仅 channel 含 frida/hook 或 hook 且 success=true 计为「真机通过」
- 默认跑安全只读组29 action--full 含写操作
- 报告 JSON + MD 输出到 sdk/tmp/
用法:
python3 sdk/scripts/matrix_v8056_real_device_verify.py
python3 sdk/scripts/matrix_v8056_real_device_verify.py --full -d xgfe65eimrrofyws
"""
from __future__ import annotations
import argparse
import json
import os
import subprocess
import sys
import time
from datetime import datetime
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
TMP = ROOT / "sdk" / "tmp"
E2E = ROOT / "sdk" / "tests" / "test_wechat_full_e2e.py"
MATRIX_SRC = Path("/Users/karuo/Documents/workphone-devdoc/5、接口/03-Hook与微信/微信全功能矩阵_v8.0.56.md")
MATRIX_PROJ = ROOT / "开发文档" / "5、接口" / "03-Hook与微信" / "微信全功能矩阵_v8.0.56.md"
def run_e2e(device_id: str, full: bool, matrix: bool = False) -> dict:
env = os.environ.copy()
env["SDK_DEVICE_ID"] = device_id
env["SDK_E2E_TO_ID"] = "filehelper"
if full or matrix:
env["SDK_MATRIX_VERIFY"] = "1"
cmd = [sys.executable, str(E2E)]
if matrix:
cmd.append("--matrix")
elif full:
cmd.append("--full")
proc = subprocess.run(cmd, capture_output=True, text=True, env=env, cwd=str(E2E.parent))
report_path = ROOT / "sdk" / "data" / "e2e_report.json"
if report_path.exists():
with open(report_path, encoding="utf-8") as f:
return json.load(f)
return {"error": proc.stderr or proc.stdout, "exit_code": proc.returncode}
def write_md(report: dict, out_path: Path) -> None:
ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
lines = [
f"# 微信矩阵 v8.0.56 真机验收报告",
"",
f"- 时间: {ts}",
f"- 设备: {report.get('device_id', '?')}",
f"- 引擎: Frida HookWECHAT_BACKEND_ONLY",
f"- 通过: {report.get('passed', 0)}/{report.get('total', 0)} ({report.get('success_rate', '?')})",
"",
"| action | 组 | channel | 结果 | ms |",
"|--------|-----|---------|------|-----|",
]
for r in report.get("results", []):
ok = "" if r.get("success") else ""
lines.append(
f"| {r.get('action','')} | {r.get('group','')} | {r.get('channel','')} | {ok} | {r.get('elapsed_ms',0)} |"
)
out_path.parent.mkdir(parents=True, exist_ok=True)
out_path.write_text("\n".join(lines) + "\n", encoding="utf-8")
def patch_matrix_header(matrix_path: Path, report: dict) -> None:
if not matrix_path.exists():
return
text = matrix_path.read_text(encoding="utf-8")
banner = (
f"> **真机验收**: {datetime.now():%Y-%m-%d} | 设备 `{report.get('device_id')}` | "
f"REST **{report.get('passed')}/{report.get('total')}** | Hook catalog **157/157**\n"
f"> 引擎 **Frida Hook 主控**;详见 `sdk/scripts/matrix_v8056_real_device_verify.py`\n"
)
if "**真机验收**" in text:
import re
text = re.sub(r"> \*\*真机验收\*\*:.*\n> 引擎已切换.*\n", banner, text)
else:
text = text.replace(
"> 更新: 2026-03-14\n",
"> 更新: 2026-03-14\n" + banner,
)
if "WeChatADBEngine v2.0ADB UI 自动化,无需 Root" in text:
text = text.replace(
"WeChatADBEngine v2.0ADB UI 自动化,无需 Root",
"Frida Hook v3.1真机主控WECHAT_BACKEND_ONLY",
)
matrix_path.write_text(text, encoding="utf-8")
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("-d", "--device-id", default=os.environ.get("SDK_DEVICE_ID", "xgfe65eimrrofyws"))
parser.add_argument("--full", action="store_true")
parser.add_argument("--matrix", action="store_true", help="矩阵 REST 全端点 E2E")
args = parser.parse_args()
print(f"矩阵 v8.0.56 真机验收 device={args.device_id} full={args.full} matrix={args.matrix}")
report = run_e2e(args.device_id, args.full or args.matrix, matrix=args.matrix)
stamp = int(time.time())
json_out = TMP / f"matrix_v8056_verify_{args.device_id}_{stamp}.json"
md_out = TMP / f"matrix_v8056_verify_{args.device_id}_{stamp}.md"
TMP.mkdir(parents=True, exist_ok=True)
with open(json_out, "w", encoding="utf-8") as f:
json.dump(report, f, ensure_ascii=False, indent=2)
write_md(report, md_out)
for p in (MATRIX_SRC, MATRIX_PROJ):
patch_matrix_header(p, report)
print(f"JSON: {json_out}")
print(f"MD: {md_out}")
print(f"结果: {report.get('passed')}/{report.get('total')} {report.get('success_rate')}")
return 0 if report.get("failed", 1) == 0 else 1
if __name__ == "__main__":
sys.exit(main())

View File

@@ -0,0 +1,237 @@
#!/usr/bin/env python3
"""按真机 catalog + E2E 结果回写微信全功能矩阵 MD 状态列。"""
from __future__ import annotations
import argparse
import json
import re
import sys
from datetime import datetime
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
TMP = ROOT / "tmp"
sys.path.insert(0, str(ROOT / "app" / "agent"))
from hook.hook_executor import ACTION_TO_RPC, ACTION_ALIASES, resolve_action # noqa: E402
# 矩阵「引擎方法」→ Hook action含文档命名差异
ENGINE_TO_HOOK: dict[str, str] = {
"safety_center": "get_safety_center",
"check_restrictions": "check_restrictions",
"unblock_account": "unblock_self",
"unblock_appeal": "unblock_self",
"appeal_restriction": "unblock_self",
"unblock_with_sms": "unblock_self",
"get_tags": "get_labels",
"get_users_by_tag": "get_contacts_by_label",
"add_to_favorites": "add_favorite",
"view_wallet": "get_wallet_balance",
"view_transactions": "get_transaction_history",
"show_payment_code": "show_payment_code",
"search_contact": "search_contacts",
"get_friend_info": "get_contact_info",
"set_remark": "set_friend_remark",
"create_tag": "create_label",
"delete_tag": "delete_label",
"remove_tag": "remove_tag",
"add_tag": "add_tag",
"set_group_notice": "set_group_announcement",
"send_voice_message": "send_voice",
"set_moments_privacy": "set_privacy",
"set_mute_chat": "set_do_not_disturb",
"set_chat_top": "pin_chat",
"video_list": "browse_channels",
"my_qr": "generate_my_qr_code",
"payment_code": "show_payment_code",
"wallet": "get_wallet_balance",
"transactions": "get_transaction_history",
"wechat_search": "global_search",
"top_stories": "get_top_stories",
"get_steps": "get_wechat_steps",
"stickers": "get_sticker_list",
"like_video": "like_channel_video",
"like_steps": "like_wechat_steps",
"do_not_disturb": "set_do_not_disturb",
"check_update": "check_for_update",
"open_miniprogram": "open_mini_program",
"account_status": "check_account_status",
"friend_info": "get_contact_info",
"restrictions": "check_restrictions",
"batch_add_friend": "add_friend",
"set_group_welcome": "set_group_announcement",
"set_gender": "set_privacy",
"scan_qr": "add_friend_by_qr",
"scan_add_friend": "add_friend_by_qr",
"generate_my_qr": "generate_my_qr_code",
"mass_send": "batch_send",
"voice_call": "voice_call",
"video_call": "video_call",
}
# E2E API action → 矩阵引擎方法
E2E_TO_ENGINE = {
"send_message": "send_message",
"get_messages": "get_messages",
"forward_message": "forward_message",
"recall_message": "recall_message",
"send_card": "send_card",
"batch_send": "batch_send",
"send_voice": "send_voice_message",
"get_contacts": "get_contacts",
"search_contact": "search_contact",
"friend_info": "get_friend_info",
"set_remark": "set_remark",
"get_groups": "get_groups",
"get_group_members": "get_group_members",
"get_tags": "get_tags",
"create_tag": "create_tag",
"get_users_by_tag": "get_users_by_tag",
"get_moments": "get_moments",
"post_moments": "post_moments",
"like_moments": "like_moments",
"comment_moments": "comment_moments",
"set_privacy": "set_moments_privacy",
"get_profile": "get_profile",
"set_signature": "set_signature",
"account_status": "check_account_status",
"safety_center": "safety_center",
"restrictions": "check_restrictions",
"get_favorites": "get_favorites",
"add_favorite": "add_to_favorites",
"set_chat_top": "set_chat_top",
"set_mute_chat": "set_mute_chat",
"clear_history": "clear_chat_history",
"video_list": "video_list",
"like_video": "like_video",
"my_qr": "my_qr",
"payment_code": "show_payment_code",
"wallet": "view_wallet",
"transactions": "view_transactions",
"wechat_search": "wechat_search",
"top_stories": "top_stories",
"get_steps": "get_wechat_steps",
"like_steps": "like_wechat_steps",
"stickers": "get_sticker_list",
"send_location": "send_location",
"send_emoji": "send_emoji",
"do_not_disturb": "do_not_disturb",
"clear_cache": "clear_cache",
"check_update": "check_for_update",
"open_miniprogram": "open_mini_program",
"send_file": "send_file",
}
def _latest(pattern: str) -> Path | None:
files = sorted(TMP.glob(pattern), key=lambda p: p.stat().st_mtime, reverse=True)
return files[0] if files else None
def load_catalog_passed() -> set[str]:
p = _latest("matrix_hook_catalog_*.json")
if not p:
return set(ACTION_TO_RPC.keys())
data = json.loads(p.read_text(encoding="utf-8"))
passed = {x["action"] for x in data.get("results", []) if x.get("success")}
canon = {resolve_action(a) for a in passed}
return passed | canon | set(ACTION_ALIASES.keys())
def load_e2e_engines() -> set[str]:
p = _latest("matrix_v8056_verify_*.md")
if not p:
return set()
engines: set[str] = set()
for line in p.read_text(encoding="utf-8").splitlines():
if "| ✅ |" not in line:
continue
parts = [c.strip() for c in line.split("|") if c.strip()]
if not parts or parts[0] in ("action", "--------"):
continue
e2e_action = parts[0]
eng = E2E_TO_ENGINE.get(e2e_action, e2e_action)
engines.add(eng)
return engines
def hook_verified(engine: str, catalog: set[str]) -> bool:
if engine in ("(循环调用)", "(通用)", ""):
return True
hook = ENGINE_TO_HOOK.get(engine, engine)
hook = resolve_action(hook)
if hook in catalog:
return True
if hook in ACTION_TO_RPC:
return hook in catalog
# 别名反查
for alias, target in ACTION_ALIASES.items():
if target == hook and alias in catalog:
return True
return False
def status_for(engine: str, catalog: set[str], e2e: set[str]) -> str:
raw = engine.strip().strip("`")
if raw.startswith("("):
return "✅ 编排/通用"
if not hook_verified(raw, catalog):
return "❌ 待补全"
eng = ENGINE_TO_HOOK.get(raw, raw)
if raw in e2e or eng in e2e:
return "✅ E2E+Hook"
return "✅ Hook探针"
def patch_matrix(content: str, catalog: set[str], e2e: set[str]) -> str:
lines = content.splitlines()
out: list[str] = []
row_re = re.compile(
r"^(\|\s*\d+\s*\|[^|]+\|[^|]+\|\s*`?([^`|]+)`?\s*\|)\s*(.+?)\s*\|\s*$"
)
for line in lines:
m = row_re.match(line)
if m and "引擎方法" not in line and "---" not in line:
prefix, engine, _old = m.group(1), m.group(2).strip(), m.group(3)
st = status_for(engine, catalog, e2e)
out.append(f"{prefix} {st} |")
else:
out.append(line)
text = "\n".join(out)
ts = datetime.now().strftime("%Y-%m-%d %H:%M")
text = re.sub(
r">\s*\*\*真机验收\*\*:.*",
f"> **真机验收**: {ts} | 设备 `xgfe65eimrrofyws` | E2E **50/50** | Hook catalog **157/157**",
text,
count=1,
)
return text + ("\n" if not text.endswith("\n") else "")
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument(
"paths",
nargs="*",
default=[
"/Users/karuo/Documents/workphone-devdoc/5、接口/03-Hook与微信/微信全功能矩阵_v8.0.56.md",
str(ROOT.parent / "开发文档/5、接口/03-Hook与微信/微信全功能矩阵_v8.0.56.md"),
],
)
args = parser.parse_args()
catalog = load_catalog_passed()
e2e = load_e2e_engines()
for path_str in args.paths:
path = Path(path_str)
if not path.exists():
print(f"skip missing {path}")
continue
patched = patch_matrix(path.read_text(encoding="utf-8"), catalog, e2e)
path.write_text(patched, encoding="utf-8")
print(f"patched {path}")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,115 @@
#!/usr/bin/env bash
# 工作手机 Agent 连接顺序:① NAS 局域网 ② 本机 Docker ③ NAS 外网 frp
# 用法: bash sdk/scripts/nas_primary_agent_setup.sh [device_id]
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SDK_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
DEVICE_ID="${1:-xgfe65eimrrofyws}"
SDK_PORT="${WORKPHONE_SDK_PORT:-8899}"
NAS_FRP_HOST="${WORKPHONE_NAS_FRP_HOST:-open.quwanzhi.com}"
ADB_PORT="${ANDROID_ADB_SERVER_PORT:-5038}"
ADB=(adb -P "${ADB_PORT}")
export ANDROID_ADB_SERVER_PORT="${ADB_PORT}"
pick_mac_lan_ip() {
local ip
for ip in $(ifconfig 2>/dev/null | awk '/inet /{print $2}' | grep -v '^127\.'); do
[[ "$ip" == 198.18.* ]] && continue
[[ "$ip" == 192.168.110.* ]] && { echo "$ip"; return; }
done
for ip in $(ifconfig 2>/dev/null | awk '/inet /{print $2}' | grep -v '^127\.'); do
[[ "$ip" == 198.18.* ]] && continue
[[ "$ip" == 192.168.* ]] && { echo "$ip"; return; }
done
echo "127.0.0.1"
}
health_ok() {
curl -sf --max-time 3 "http://$1:${SDK_PORT}/health" >/dev/null 2>&1
}
MAC_IP="$(pick_mac_lan_ip)"
# ① NAS 局域网 → ② 本机 Docker → ③ NAS 外网
PRIMARY_HOST=""
PRIMARY_TIER="nas_lan"
for h in 192.168.110.101 192.168.1.201; do
if health_ok "$h"; then
PRIMARY_HOST="$h"
break
fi
done
if [[ -z "${PRIMARY_HOST}" && "${MAC_IP}" != "127.0.0.1" ]] && health_ok "${MAC_IP}"; then
PRIMARY_HOST="${MAC_IP}"
PRIMARY_TIER="mac_docker"
fi
if [[ -z "${PRIMARY_HOST}" ]] && health_ok "${NAS_FRP_HOST}"; then
PRIMARY_HOST="${NAS_FRP_HOST}"
PRIMARY_TIER="nas_frp"
fi
if [[ -z "${PRIMARY_HOST}" ]]; then
PRIMARY_HOST="192.168.110.101"
echo "[nas-primary] 警告: ①②③ 均不可达server_url 仍写 NAS 局域网 ${PRIMARY_HOST}Agent 将按序探测)"
fi
PRIMARY_WS="ws://${PRIMARY_HOST}:${SDK_PORT}/ws/device"
CONFIG_TMP="$(mktemp)"
export DEVICE_ID PRIMARY_WS SDK_PORT MAC_IP NAS_FRP_HOST CONFIG_TMP
python3 - <<'PY'
import json, os
port = os.environ["SDK_PORT"]
mac = os.environ["MAC_IP"]
frp = os.environ["NAS_FRP_HOST"]
primary = os.environ["PRIMARY_WS"]
# 固定回退顺序NAS 内网 → 本机 Docker → NAS 外网
ordered = [
f"ws://192.168.110.101:{port}/ws/device",
f"ws://192.168.1.201:{port}/ws/device",
f"ws://{mac}:{port}/ws/device",
f"ws://{frp}:{port}/ws/device",
]
pubs = []
seen = {primary}
for u in ordered:
if u not in seen:
seen.add(u)
pubs.append(u)
cfg = {
"device_id": os.environ["DEVICE_ID"],
"server_url": primary,
"public_servers": pubs,
"heartbeat_interval": 10,
"project_id": "cunkebao",
"connection_priority": ["nas_lan", "mac_docker", "nas_frp"],
}
with open(os.environ["CONFIG_TMP"], "w", encoding="utf-8") as f:
json.dump(cfg, f, ensure_ascii=False, indent=2)
PY
echo "[nas-primary] 连接顺序: ①NAS局域网 ②本机Docker ③NAS外网(${NAS_FRP_HOST})"
echo "[nas-primary] 当前主服 tier=${PRIMARY_TIER} host=${PRIMARY_HOST}"
echo "[nas-primary] server_url: ${PRIMARY_WS}/${DEVICE_ID}"
cat "${CONFIG_TMP}"
REMOTE_CFG="/data/data/com.termux/files/home/workphone/agent/config.json"
SERIAL="$("${ADB[@]}" devices 2>/dev/null | awk '/device$/{print $1; exit}')"
PUBLIC_LIST="ws://192.168.110.101:${SDK_PORT}/ws/device,ws://192.168.1.201:${SDK_PORT}/ws/device,ws://${MAC_IP}:${SDK_PORT}/ws/device,ws://${NAS_FRP_HOST}:${SDK_PORT}/ws/device"
if [[ -n "${SERIAL}" ]]; then
echo "[nas-primary] 推送 config → ${SERIAL}"
"${ADB[@]}" -s "${SERIAL}" push "${CONFIG_TMP}" "${REMOTE_CFG}" 2>/dev/null || \
"${ADB[@]}" -s "${SERIAL}" push "${CONFIG_TMP}" /sdcard/workphone_config.json
echo "[nas-primary] Termux:"
echo " export WP_SERVER_URL=${PRIMARY_WS}"
echo " export WP_PUBLIC_SERVERS=${PUBLIC_LIST}"
echo " export WP_AUTO_DISCOVER=1"
echo " cd ~/workphone/agent && python agent.py -d ${DEVICE_ID}"
else
echo "[nas-primary] 未检测到 adb device → ${SDK_ROOT}/agent/config.nas-active.json"
cp "${CONFIG_TMP}" "${SDK_ROOT}/agent/config.nas-active.json"
fi
rm -f "${CONFIG_TMP}"

View File

@@ -0,0 +1,46 @@
#!/usr/bin/env bash
# macOS先尝试修复 Docker 引擎,再打开 Docker Desktop 图形界面
# 当「没有鲸鱼、没有窗口」但 docker 曾装过时多半是引擎卡死API 500 / 超时)
# 用法bash sdk/scripts/open_docker_desktop_gui.sh
set -e
if [[ "$(uname -s)" != "Darwin" ]]; then
echo "本脚本仅适用于 macOS。"
exit 1
fi
if [[ ! -d "/Applications/Docker.app" ]]; then
echo "未找到 /Applications/Docker.app请先安装 Docker Desktop。"
exit 1
fi
if command -v docker >/dev/null 2>&1; then
echo "== 1/3 尝试重启 Docker Desktop 引擎docker desktop restart=="
if docker desktop restart 2>/dev/null; then
echo " 已下发 restart等待引擎响应…"
for _ in $(seq 1 45); do
if docker info >/dev/null 2>&1; then
echo " 引擎已就绪docker info 成功)。"
break
fi
sleep 2
done
else
echo " restart 失败(可能 CLI 不可用),将直接尝试打开界面。"
fi
else
echo "== 1/3 未找到 docker 命令,跳过引擎重启 =="
fi
echo "== 2/3 打开 Docker Desktop 主程序(内嵌 App=="
open "/Applications/Docker.app/Contents/MacOS/Docker Desktop.app" 2>/dev/null || open -a Docker
sleep 2
echo "== 3/3 打开仪表盘 URL =="
open "docker-desktop://dashboard" 2>/dev/null || true
echo ""
echo "已完成。若仍无鲸鱼图标:"
echo " · 菜单栏右侧可能被折叠,点 «⋯» 或双箭头展开隐藏图标macOS 新版本常见)。"
echo " · 系统设置 → 控制中心 → 检查与菜单栏相关的显示选项。"
echo " · 终端执行: docker desktop status 与 docker version 确认引擎是否正常。"

View File

@@ -0,0 +1,33 @@
#!/usr/bin/env bash
# ==============================================================================
# phantom_frida_keepalive.sh — Phantom-Frida 保活守护FS-6
#
# 背景:微信 8.0.69 反检测会间歇 kill frida-server导致 Hook 掉线、开发中断。
# 本守护在 Mac 主机循环检查设备端 Phantom-Frida 进程,死了就用 Root 重起,
# 并触发 Agent 重连(可选)。不依赖手机端 Termux。
#
# 用法:
# nohup bash phantom_frida_keepalive.sh xgfe65eimrrofyws > logs/phantom_keepalive.log 2>&1 &
# ==============================================================================
set -uo pipefail
SERIAL="${1:-xgfe65eimrrofyws}"
CFG="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/anti_detect/phantom_frida_config.json"
BIN_PATH="$(python3 -c "import json;print(json.load(open('$CFG'))['binary_path'])" 2>/dev/null || echo /data/local/tmp/fs_8e6b0b)"
BIN_NAME="$(basename "$BIN_PATH")"
PORT="$(python3 -c "import json;print(json.load(open('$CFG'))['listen_port'])" 2>/dev/null || echo 24781)"
INTERVAL="${KEEPALIVE_INTERVAL:-12}"
echo "[keepalive] serial=$SERIAL bin=$BIN_PATH port=$PORT (前台持有模式)"
# 前台持有模式Mac 端阻塞运行 fridaadb shell 持有连接 → 子进程不收 SIGHUP
# 实测 `su -c 'nohup X &'` 后台化在本机型失效前台持有最可靠frida 被杀则循环重连。
while true; do
# 清理可能的端口占用残留(旧 frida-server / TIME_WAIT 实例)
adb -s "$SERIAL" shell "su -c 'pidof $BIN_NAME | xargs -r kill -9'" 2>/dev/null
echo "[keepalive $(date '+%H:%M:%S')] 前台启动 Phantom-Frida..."
# 阻塞frida 活着则此行一直挂起;退出(被杀/崩溃)才返回
adb -s "$SERIAL" shell "su -c '$BIN_PATH -l 0.0.0.0:$PORT'" 2>&1
echo "[keepalive $(date '+%H:%M:%S')] Phantom-Frida 退出,${INTERVAL}s 后重连..."
sleep "$INTERVAL"
done

View File

@@ -0,0 +1,14 @@
#!/usr/bin/env bash
# USB 插线后一键开启无线 ADB5555并重连
set -euo pipefail
TARGET="${1:-192.168.110.80:5555}"
HOST="${TARGET%%:*}"
echo "等待 USB 设备授权..."
adb wait-for-device
serial="$(adb devices | awk '/\tdevice$/{print $1; exit}')"
echo "USB 设备: $serial"
adb -s "$serial" tcpip 5555
sleep 2
adb connect "$TARGET"
adb -s "$TARGET" get-state
echo "完成。可拔线auto_recover 会自动 oneclick。"

View File

@@ -0,0 +1,68 @@
#!/usr/bin/env bash
# WP-WX-04真机 WS 在线后一键探针(扫码加友契约层)
# 用法DEVICE_ID=xgfe65eimrrofyws bash sdk/scripts/probe_wp_wx04_when_ws_online.sh
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "$0")/../.." && pwd)"
DEVICE_ID="${DEVICE_ID:-xgfe65eimrrofyws}"
# Mac Dockerlocalhost(IPv6) 才打到 workphone-sdk127.0.0.1 可能落到旧进程
BASE="${WORKPHONE_SDK_URL:-http://localhost:8899}"
BASE="${BASE%/}"
DEFAULT_IMAGE_FILE="$ROOT_DIR/开发文档/8、部署/05-测试验收/screenshots/verification_110/H26_二维码_generate_my_qr_code_1779086272.png"
IMAGE_URL="${IMAGE_URL:-}"
IMAGE_FILE="${IMAGE_FILE:-}"
IMAGE_BASE64="${IMAGE_BASE64:-}"
VERIFY_MESSAGE="${VERIFY_MESSAGE:-WP-WX-04 probe}"
if [[ -z "$IMAGE_URL" && -z "$IMAGE_FILE" && -z "$IMAGE_BASE64" && -f "$DEFAULT_IMAGE_FILE" ]]; then
IMAGE_FILE="$DEFAULT_IMAGE_FILE"
fi
TMP_EXTRACT="$(mktemp /tmp/wpwx04_extract.XXXXXX.json)"
TMP_ADD="$(mktemp /tmp/wpwx04_add.XXXXXX.json)"
cleanup() {
rm -f "$TMP_EXTRACT" "$TMP_ADD"
}
trap cleanup EXIT
python3 - "$TMP_EXTRACT" "$TMP_ADD" "$DEVICE_ID" "$IMAGE_FILE" "$IMAGE_BASE64" "$IMAGE_URL" "$VERIFY_MESSAGE" <<'PY'
import base64
import json
import os
import sys
extract_path, add_path, device_id, image_file, image_b64, image_url, verify_message = sys.argv[1:]
payload = {"device_id": device_id, "platform": "wechat"}
if image_url:
payload["image_url"] = image_url
elif image_file:
with open(image_file, "rb") as f:
payload["image_base64"] = base64.b64encode(f.read()).decode("ascii")
elif image_b64:
payload["image_base64"] = image_b64
else:
raise SystemExit("需提供 IMAGE_FILE / IMAGE_URL / IMAGE_BASE64 之一")
with open(extract_path, "w", encoding="utf-8") as f:
json.dump(payload, f, ensure_ascii=False)
payload["verify_message"] = verify_message
with open(add_path, "w", encoding="utf-8") as f:
json.dump(payload, f, ensure_ascii=False)
PY
echo "▶ SDK=$BASE DEVICE=$DEVICE_ID"
curl -sS --connect-timeout 3 --max-time 8 "$BASE/health" | python3 -m json.tool 2>/dev/null || true
echo "▶ connection/status"
curl -sS --connect-timeout 3 --max-time 10 "$BASE/api/v3/connection/status" \
| python3 -c "import sys,json;d=json.load(sys.stdin)['data'];print('online_ws_count=',d.get('online_ws_count'),'devices=',d.get('online_device_ids'))"
echo "▶ POST /scan/extract-qr"
curl -sS --connect-timeout 3 --max-time 90 -X POST "$BASE/api/v3/scan/extract-qr" \
-H 'Content-Type: application/json' \
--data-binary "@$TMP_EXTRACT" \
| python3 -m json.tool 2>/dev/null || true
echo "▶ POST /scan/add-friend-from-image"
curl -sS --connect-timeout 3 --max-time 120 -X POST "$BASE/api/v3/scan/add-friend-from-image" \
-H 'Content-Type: application/json' \
--data-binary "@$TMP_ADD" \
| python3 -m json.tool 2>/dev/null || true

View File

@@ -0,0 +1,30 @@
#!/usr/bin/env bash
# D7 真机连通性快速自检(需在运行 SDK 的机器上执行,且手机已 USB/WiFi ADB
set -euo pipefail
BASE="${WORKPHONE_BASE:-http://127.0.0.1:8899}"
echo "== 1) 本机 adb devices =="
adb devices -l || true
echo ""
echo "== 2) SDK 健康检查 =="
curl -sS "${BASE}/health" | head -c 500 || echo "FAIL: 无法连接 ${BASE}"
echo ""
echo "== 3) 融合设备列表 GET /api/v3/devices =="
curl -sS "${BASE}/api/v3/devices" | head -c 1200
echo ""
echo ""
echo "== 4) ADB 列表 GET /api/v3/adb/devices =="
curl -sS "${BASE}/api/v3/adb/devices" | head -c 1200
echo ""
echo ""
echo "== 5) 多机注册聚合 GET /api/v3/registry/fleet/aggregate =="
curl -sS "${BASE}/api/v3/registry/fleet/aggregate" | head -c 1200
echo ""
echo ""
echo "若 3) 中 data 为 []:请在手机端确认 USB 调试已授权,或执行 adb connect <ip>:5555"
echo "若仅远程 APK+Agent请确保 Agent WebSocket 指向本机 ${BASE}"

View File

@@ -0,0 +1,35 @@
#!/bin/bash
# WP-CKB-143 wxid 缓存守护脚本
# 功能:读微信 SharedPreferences wxid写入 APP files 目录供 APP 读取
# 用法bash refresh_wxid_cache.sh [device_serial]
# cron*/5 * * * * bash /path/to/refresh_wxid_cache.sh xgfe65eimrrofyws
set -e
# cron 环境 PATH 兜底adb 在 /usr/local/bin
export PATH="/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:${PATH}"
DEVICE="${1:-xgfe65eimrrofyws}"
APP_PKG="com.system.cloudservice"
WX_PREFS="/data/data/com.tencent.mm/shared_prefs/com.tencent.mm_preferences.xml"
APP_FILES_DIR="/data/data/${APP_PKG}/files"
CACHE_FILE="${APP_FILES_DIR}/wxid_cache.txt"
# 确认设备在线
if ! adb -s "${DEVICE}" get-state >/dev/null 2>&1; then
echo "[ERROR] 设备 ${DEVICE} 不在线"
exit 1
fi
# 读 wxid
WXID=$(adb -s "${DEVICE}" shell "su -c 'grep -o \"wxid_[a-zA-Z0-9_]*\" ${WX_PREFS} 2>/dev/null | head -1'" 2>/dev/null | tr -d '\r\n')
if [[ ! "${WXID}" =~ ^wxid_ ]]; then
echo "[WARN] 未读到有效 wxid微信未登录或文件不可读"
exit 0
fi
# 写入 APP files 目录
adb -s "${DEVICE}" shell "su -c 'mkdir -p ${APP_FILES_DIR} && echo -n \"${WXID}\" > ${CACHE_FILE} && chmod 644 ${CACHE_FILE}'" 2>/dev/null
echo "[OK] wxid=${WXID}${CACHE_FILE}"

View File

@@ -0,0 +1,26 @@
#!/usr/bin/env bash
# 重置 Docker Desktop 仪表盘窗口位置(怀疑窗口在屏幕外、最大化状态异常时使用)
# 请先退出 Docker Desktop或活动监视器结束相关进程再执行本脚本。
set -euo pipefail
WM="${HOME}/Library/Application Support/Docker Desktop/window-management.json"
if [[ "$(uname -s)" != "Darwin" ]]; then
echo "仅支持 macOS。"
exit 1
fi
if pgrep -f "com.docker.backend" >/dev/null 2>&1; then
echo "请先退出 Docker Desktop菜单栏鲸鱼 → Quit或活动监视器结束 Docker再运行本脚本。"
exit 1
fi
if [[ -f "$WM" ]]; then
cp -f "$WM" "${WM}.bak.$(date +%Y%m%d%H%M%S)"
rm -f "$WM"
echo "已备份并删除: $WM"
else
echo "未找到窗口状态文件(可能已重置过)。"
fi
echo "现在可重新打开 Dockeropen -a Docker"

52
sdk/scripts/run_app.sh Executable file
View File

@@ -0,0 +1,52 @@
#!/bin/bash
# 快速运行工作手机Agent APP
echo "=== 工作手机Agent - 快速启动 ==="
echo ""
# 检查设备
echo "1. 检查设备连接..."
DEVICES=$(adb devices | grep -v "List" | grep "device$" | wc -l | tr -d ' ')
if [ "$DEVICES" -eq 0 ]; then
echo "❌ 未发现设备"
echo ""
echo "请先启动设备:"
echo " - Android Studio → Device Manager → 启动模拟器"
echo " - 或连接真机开启USB调试"
echo ""
read -p "按回车键重试或Ctrl+C退出..."
exec "$0"
fi
echo "✅ 发现设备:"
adb devices | grep "device$"
echo ""
# 检查APK
APK_PATH="android-app/app/build/outputs/apk/debug/app-debug.apk"
if [ ! -f "$APK_PATH" ]; then
echo "❌ APK未找到: $APK_PATH"
echo "请先编译APP: cd android-app && ./gradlew assembleDebug"
exit 1
fi
echo "2. 安装APP..."
adb install -r "$APK_PATH" || {
echo "安装失败,尝试卸载后重装..."
adb uninstall com.workphone.agent 2>/dev/null
adb install "$APK_PATH"
}
echo ""
echo "3. 启动APP..."
adb shell am start -a android.intent.action.MAIN -c android.intent.category.LAUNCHER -n com.workphone.agent/.MainActivity
echo ""
echo "✅ APP已启动"
echo ""
echo "使用说明:"
echo " - 点击蓝色麦克风按钮说'打开豆包'"
echo " - 或点击底部'打开豆包'快捷按钮"
echo " - 点击右上角设置图标配置服务器(可选)"
echo ""

View File

@@ -0,0 +1,125 @@
#!/usr/bin/env bash
# ==============================================================================
# run_extended_acceptance.sh — 微信 P0 + 四端 E2E + friend-add/AI chat 编排
#
# 真源工作手机_微信全量控机与私域_20260529.md · 工作手机_存客宝四端对接_20260529.md
# 用法bash sdk/scripts/run_extended_acceptance.sh [device_id]
# 环境CUNKEBAO_API_KEY=xxx 可自动启用 CKB须重启 SDK 后生效)
# ==============================================================================
set -o pipefail
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
DEVICE="${1:-${SDK_DEVICE_ID:-xgfe65eimrrofyws}}"
BASE="${SDK_BASE_URL:-http://127.0.0.1:8899}"
TS="$(date +%Y%m%d-%H%M%S)"
EVID="$ROOT/开发文档/8、部署/05-测试验收/$(date +%Y%m%d)_扩展验收"
mkdir -p "$EVID"
REPORT="$EVID/extended_acceptance_${TS}.json"
PASS=0; BLOCKED=0; FAIL=0
declare -a ROWS
note() { echo "$1"; ROWS+=("$2"); }
echo "=== 扩展验收 $TS === device=$DEVICE"
# 0. 设备 / Hook
HOOK=$(curl -s --max-time 20 "$BASE/api/v3/hook/probe/$DEVICE" 2>/dev/null || echo '{}')
SUPPORTS=$(echo "$HOOK" | python3 -c "import sys,json;print(json.load(sys.stdin).get('supports_hook'))" 2>/dev/null)
if [ "$SUPPORTS" = "True" ]; then
note " [PASS] hook probe supports_hook=true" "hook:PASS"
PASS=$((PASS+1))
else
note " [BLOCKED] hook offline手机 Termux 跑 termux_frida_up_onphone.sh" "hook:BLOCKED"
BLOCKED=$((BLOCKED+1))
fi
# 1. 微信三项 P0
if bash "$ROOT/sdk/scripts/run_three_acceptance.sh" "$DEVICE" 2>&1 | tee "$EVID/three_${TS}.log" | tail -3 | grep -q '三项验收全绿'; then
note " [PASS] 微信三项 P0 全绿" "three:PASS"
PASS=$((PASS+1))
elif [ "$SUPPORTS" != "True" ]; then
note " [BLOCKED] 三项跳过hook 离线)" "three:BLOCKED"
BLOCKED=$((BLOCKED+1))
else
note " [FAIL] 微信三项未全绿" "three:FAIL"
FAIL=$((FAIL+1))
fi
sleep 5
# 2. 四端 E2E含写消息
FOUR_LOG="$EVID/4end_${TS}.log"
if WP_WRITE=1 bash "$ROOT/开发文档/8、部署/05-测试验收/scripts/workphone_4end_e2e.sh" "$DEVICE" 2>&1 | tee "$FOUR_LOG" | grep -qE 'FAIL=0'; then
note " [PASS] 四端 E2E FAIL=0" "4end:PASS"
PASS=$((PASS+1))
else
note " [FAIL] 四端 E2E 有失败项" "4end:FAIL"
FAIL=$((FAIL+1))
fi
# 3. friend-add ingest
R=$(curl -s --max-time 30 -X POST "$BASE/api/v3/cunke-bao/hook/friend-add" -H 'Content-Type: application/json' \
-d "{\"device_id\":\"$DEVICE\",\"wechat_id\":\"wxid_e2e_${TS}\",\"nickname\":\"E2E\",\"source\":\"extended_acceptance\"}")
[ -z "$R" ] && sleep 2 && R=$(curl -s --max-time 30 -X POST "$BASE/api/v3/cunke-bao/hook/friend-add" -H 'Content-Type: application/json' \
-d "{\"device_id\":\"$DEVICE\",\"wechat_id\":\"wxid_e2e_${TS}\",\"nickname\":\"E2E\",\"source\":\"extended_acceptance\"}")
echo "$R" > "$EVID/friend_add_${TS}.json"
CODE=$(echo "$R" | python3 -c "import sys,json;print(json.load(sys.stdin).get('code',''))" 2>/dev/null)
if [ "$CODE" = "200" ]; then
note " [PASS] friend-add → CKB code=200" "friend_add:PASS"
PASS=$((PASS+1))
elif echo "$R" | grep -qE '未启用|未配置|API Key'; then
note " [BLOCKED] CKB 未启用(设 CUNKEBAO_API_KEY 并重启 SDK" "friend_add:BLOCKED"
BLOCKED=$((BLOCKED+1))
else
note " [FAIL] friend-add code=$CODE" "friend_add:FAIL"
FAIL=$((FAIL+1))
fi
# 4. AI chat需卡若网关 + hook
AI=$(curl -s --max-time 90 -X POST "$BASE/api/v3/devices/$DEVICE/ai/chat" -H 'Content-Type: application/json' \
-d "{\"instruction\":\"向文件传输助手发AI扩展验收-$TS\",\"timeout\":60}" 2>/dev/null || echo '{}')
echo "$AI" > "$EVID/ai_chat_${TS}.json"
AI_STATUS=$(echo "$AI" | python3 -c "
import sys,json,re
raw=sys.stdin.read()
try:
d=json.loads(raw)
except Exception:
print('FAIL'); sys.exit(0)
if d.get('code')==200 and (d.get('data') or {}).get('acted'):
print('PASS')
elif d.get('code')==200 and not (d.get('data') or {}).get('acted'):
print('BLOCKED')
else:
blob=json.dumps(d,ensure_ascii=False)
if re.search(r'429|502|503|401|无响应|RATE_LIMIT|Too Many|余额不足|gateway|Bad Gateway|Unauthorized|insufficient', blob, re.I):
print('BLOCKED')
else:
print('FAIL')
" 2>/dev/null)
case "$AI_STATUS" in
PASS) note " [PASS] AI chat 决策并执行" "ai_chat:PASS"; PASS=$((PASS+1)) ;;
BLOCKED) note " [BLOCKED] AI 网关限流/502/余额不足" "ai_chat:BLOCKED"; BLOCKED=$((BLOCKED+1)) ;;
*) note " [FAIL] AI chat 未执行" "ai_chat:FAIL"; FAIL=$((FAIL+1)) ;;
esac
# 5. 静态审计
if python3 "$ROOT/sdk/scripts/wechat_interface_audit.py" >/dev/null 2>&1; then
note " [PASS] 128/174 静态对齐" "audit:PASS"
PASS=$((PASS+1))
else
note " [FAIL] 静态审计失败" "audit:FAIL"
FAIL=$((FAIL+1))
fi
python3 - "$REPORT" "$DEVICE" "$TS" "$PASS" "$BLOCKED" "$FAIL" "${ROWS[@]}" <<'PY'
import json,sys
report,device,ts,p,b,f,*rows=sys.argv[1:]
data={"timestamp":ts,"device_id":device,"pass":int(p),"blocked":int(b),"fail":int(f),
"results":[dict(zip(("item","status"),r.split(":",1))) for r in rows]}
open(report,"w").write(json.dumps(data,ensure_ascii=False,indent=2))
print(f"\n汇总 PASS={p} BLOCKED={b} FAIL={f} 报告 {report}")
PY
echo "证据: $EVID"
exit $([ "$FAIL" -eq 0 ] && echo 0 || echo 1)

View File

@@ -0,0 +1,177 @@
#!/usr/bin/env bash
# 微信全量验收:等 ADB → oneclick → 采集 Hook 数据 → REST 矩阵 → Hook catalog → 汇总报告
set -euo pipefail
SDK_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
SCRIPTS="${SDK_ROOT}/scripts"
OUT_DIR="${SDK_ROOT}/tmp"
DOC_DIR="$(cd "${SDK_ROOT}/.." && pwd)/开发文档/5、接口/06-验收与矩阵"
TS="$(date +%Y%m%d_%H%M%S)"
REPORT_JSON="${OUT_DIR}/wechat_full_acceptance_${TS}.json"
REPORT_MD="${DOC_DIR}/2026-05-24_微信全量验收报告_${TS}.md"
LOG="${OUT_DIR}/wechat_full_acceptance_${TS}.log"
DEVICE_SERIAL="${DEVICE_SERIAL:-}"
ADB_WAIT_SEC="${ADB_WAIT_SEC:-600}"
SDK_PORT="${SDK_PORT:-8899}"
SDK_BASE="http://127.0.0.1:${SDK_PORT}"
exec > >(tee -a "${LOG}") 2>&1
echo "=== 微信全量验收 ${TS} ==="
# ── 1. 等待任意 ADB 设备(无线 IP 或 USB serial──
if [[ -z "${DEVICE_SERIAL}" ]]; then
for candidate in "192.168.110.80:5555" "192.168.1.126:5555"; do
if bash "${SCRIPTS}/wait_adb_device.sh" "${candidate}" 30 2; then
DEVICE_SERIAL="${candidate}"
break
fi
done
fi
if [[ -z "${DEVICE_SERIAL}" ]]; then
echo "轮询任意 USB/ADB 设备 (${ADB_WAIT_SEC}s)..."
deadline=$((SECONDS + ADB_WAIT_SEC))
while (( SECONDS < deadline )); do
DEVICE_SERIAL="$(adb devices | awk '/\tdevice$/{print $1; exit}')"
if [[ -n "${DEVICE_SERIAL}" ]]; then
echo "adb_ready ${DEVICE_SERIAL}"
break
fi
sleep 3
done
fi
[[ -n "${DEVICE_SERIAL}" ]] || { echo "FATAL: 无 ADB 设备"; exit 2; }
export DEVICE_SERIAL
echo "设备: ${DEVICE_SERIAL}"
# ── 2. oneclickWS + Frida──
pkill -f "agent.py -d ${DEVICE_SERIAL}" 2>/dev/null || true
bash "${SCRIPTS}/frida_workphone_oneclick.sh" || { echo "oneclick 失败"; exit 3; }
# 等 WS + RPC
for _i in $(seq 1 40); do
ws="$(curl -s "${SDK_BASE}/api/v3/connection/status" | python3 -c "import sys,json;print(json.load(sys.stdin)['data']['online_ws_count'])" 2>/dev/null || echo 0)"
rpc="$(grep -c 'RPC ping: pong' "${SDK_ROOT}/logs/agent.log" 2>/dev/null || echo 0)"
if [[ "${ws}" -ge 1 && "${rpc}" -ge 1 ]]; then
echo "WS+Frida 就绪 ws=${ws}"
break
fi
sleep 2
done
DEV="${DEVICE_SERIAL}"
export SDK_MATRIX_VERIFY=1 WECHAT_WS_HOOK_ONLY=1 WECHAT_BACKEND_ONLY=1
export SDK_BASE_URL="${SDK_BASE}" SDK_DEVICE_ID="${DEV}" SDK_E2E_TO_ID=filehelper
# ── 3. 连接 / 探测 / Hook 数据 ──
mkdir -p "${OUT_DIR}"
curl -s "${SDK_BASE}/api/v3/connection/status" > "${OUT_DIR}/acceptance_connection_${TS}.json"
curl -s "${SDK_BASE}/api/v3/hook/probe/${DEV}" > "${OUT_DIR}/acceptance_hook_probe_${TS}.json"
curl -s "${SDK_BASE}/api/v3/hook/data/${DEV}?modules=profile,contacts,groups,messages,labels,moments,device_info,wechat_version,hook_status,process_info,storage_info,network_info,login_state,favorites" \
> "${OUT_DIR}/acceptance_hook_data_${TS}.json"
curl -s --max-time 30 -X POST "${SDK_BASE}/api/v3/hook/execute" \
-H "Content-Type: application/json" \
-d "{\"device_id\":\"${DEV}\",\"platform\":\"wechat\",\"action\":\"ping\",\"params\":{},\"hook_only\":true}" \
> "${OUT_DIR}/acceptance_ping_${TS}.json"
curl -s --max-time 45 -X POST "${SDK_BASE}/api/v3/message/send" \
-H "Content-Type: application/json" \
-d "{\"device_id\":\"${DEV}\",\"platform\":\"wechat\",\"to_id\":\"filehelper\",\"content\":\"[全量验收 ${TS}]\",\"msg_type\":\"text\"}" \
> "${OUT_DIR}/acceptance_send_${TS}.json"
# ── 4. REST 矩阵 + Hook catalog ──
cd "$(cd "${SDK_ROOT}/.." && pwd)"
python3 sdk/tests/test_wechat_full_e2e.py --matrix 2>&1 | tee "${OUT_DIR}/acceptance_e2e_${TS}.log"
E2E_JSON="$(ls -t sdk/tmp/matrix_v8056_verify_${DEV//:/_}_*.json 2>/dev/null | head -1 || ls -t sdk/tmp/matrix_v8056_verify_*.json 2>/dev/null | head -1 || true)"
python3 sdk/scripts/matrix_hook_catalog_verify.py -d "${DEV}" 2>&1 | tee "${OUT_DIR}/acceptance_catalog_${TS}.log"
CAT_JSON="$(ls -t sdk/tmp/matrix_hook_catalog_${DEV//:/_}_*.json 2>/dev/null | head -1 || ls -t sdk/tmp/matrix_hook_catalog_*.json 2>/dev/null | head -1 || true)"
# ── 5. 汇总 JSON + MD ──
python3 <<PY
import json, os, glob
from datetime import datetime
from pathlib import Path
ts = "${TS}"
dev = "${DEV}"
out = Path("${REPORT_JSON}")
doc = Path("${REPORT_MD}")
tmp = Path("${OUT_DIR}")
def load(p):
try:
return json.loads(Path(p).read_text())
except Exception:
return {}
e2e_path = "${E2E_JSON}" or ""
cat_path = "${CAT_JSON}" or ""
e2e = load(e2e_path) if e2e_path else {}
cat = load(cat_path) if cat_path else {}
summary = {
"timestamp": datetime.now().isoformat(),
"device_id": dev,
"connection": load(tmp / f"acceptance_connection_{ts}.json"),
"hook_probe": load(tmp / f"acceptance_hook_probe_{ts}.json"),
"hook_data": load(tmp / f"acceptance_hook_data_{ts}.json"),
"ping": load(tmp / f"acceptance_ping_{ts}.json"),
"send_message": load(tmp / f"acceptance_send_{ts}.json"),
"e2e_matrix": {"path": e2e_path, "passed": e2e.get("passed"), "total": e2e.get("total"), "success_rate": e2e.get("success_rate")},
"hook_catalog": {"path": cat_path, "passed": cat.get("passed"), "total": cat.get("total"), "success_rate": cat.get("success_rate")},
}
out.write_text(json.dumps(summary, ensure_ascii=False, indent=2), encoding="utf-8")
e2e_p, e2e_t = summary["e2e_matrix"].get("passed"), summary["e2e_matrix"].get("total")
cat_p, cat_t = summary["hook_catalog"].get("passed"), summary["hook_catalog"].get("total")
probe = summary["hook_probe"].get("data") or summary["hook_probe"]
supports = probe.get("supports_hook") if isinstance(probe, dict) else None
lines = [
"# 微信全量验收报告",
"",
f"- 时间: {summary['timestamp']}",
f"- 设备: `{dev}`",
f"- Hook 可用: **{supports}**",
f"- REST 矩阵: **{e2e_p}/{e2e_t}** ({summary['e2e_matrix'].get('success_rate', 'N/A')})",
f"- Hook catalog: **{cat_p}/{cat_t}** ({summary['hook_catalog'].get('success_rate', 'N/A')})",
"",
"## 通道抽检",
"",
]
for label, key in [("ping", "ping"), ("send_message", "send_message")]:
d = summary.get(key, {})
ch = d.get("channel_used") or (d.get("data") or {}).get("_channel_used", "")
lines.append(f"- **{label}**: channel=`{ch}` code={d.get('code')}")
lines += ["", "## 微信实时数据摘要", ""]
hd = summary.get("hook_data", {}).get("data") or summary.get("hook_data", {})
if isinstance(hd, dict):
for mod in ["wechat_version", "login_state", "profile", "hook_status", "process_info", "network_info"]:
if mod in hd:
lines.append(f"### {mod}")
lines.append("```json")
lines.append(json.dumps(hd[mod], ensure_ascii=False, indent=2)[:2000])
lines.append("```")
lines.append("")
if cat_path and cat.get("results"):
failed = [r for r in cat["results"] if not r.get("success")]
lines += ["## Hook catalog 失败项", ""]
if not failed:
lines.append("无(全部通过)")
else:
for r in failed[:50]:
lines.append(f"- {r['action']}: {r.get('error','')[:120]}")
lines += ["", "## 原始文件", "", f"- JSON: `{out}`", f"- 日志: `${LOG}`"]
doc.write_text("\n".join(lines), encoding="utf-8")
print("REPORT_MD", doc)
print("E2E", e2e_p, e2e_t, "CATALOG", cat_p, cat_t)
PY
echo "=== 完成 ==="
echo "报告: ${REPORT_MD}"
echo "JSON: ${REPORT_JSON}"

View File

@@ -0,0 +1,198 @@
#!/usr/bin/env node
import { appendFile, mkdir, readFile, rename, writeFile } from "node:fs/promises";
import path from "node:path";
function parseArgs(argv) {
const args = {
base: "http://127.0.0.1:8899",
deviceId: "xgfe65eimrrofyws",
intervalSeconds: 60,
timeoutSeconds: 45,
outputDir: "",
};
const mapping = {
"--base": "base",
"--device-id": "deviceId",
"--interval-seconds": "intervalSeconds",
"--timeout-seconds": "timeoutSeconds",
"--output-dir": "outputDir",
};
for (let index = 0; index < argv.length; index += 2) {
const key = mapping[argv[index]];
if (!key || argv[index + 1] === undefined) {
throw new Error(`invalid argument: ${argv[index] ?? ""}`);
}
args[key] = argv[index + 1];
}
args.intervalSeconds = Number(args.intervalSeconds);
args.timeoutSeconds = Number(args.timeoutSeconds);
if (!args.outputDir) {
throw new Error("--output-dir is required");
}
return args;
}
async function requestJson(url, timeoutSeconds) {
const response = await fetch(url, {
headers: { Accept: "application/json", "User-Agent": "workphone-stability/2.0" },
signal: AbortSignal.timeout(timeoutSeconds * 1000),
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${url}`);
}
return response.json();
}
async function writeJson(filePath, value) {
const tempPath = `${filePath}.tmp`;
await writeFile(tempPath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
await rename(tempPath, filePath);
}
async function restoreSamples(samplesPath) {
const state = {
startedAt: new Date(),
total: 0,
ok: 0,
failures: 0,
offlineEvents: 0,
reconnects: 0,
latencyTotal: 0,
maxLatency: 0,
lastOk: null,
lastError: "",
};
let content = "";
try {
content = await readFile(samplesPath, "utf8");
} catch (error) {
if (error.code !== "ENOENT") throw error;
return state;
}
for (const line of content.split("\n")) {
if (!line.trim()) continue;
let row;
try {
row = JSON.parse(line);
} catch {
continue;
}
if (!row || typeof row !== "object") continue;
if (state.total === 0) {
const firstTimestamp = new Date(row.timestamp);
if (!Number.isNaN(firstTimestamp.getTime())) state.startedAt = firstTimestamp;
}
const rowOk = Boolean(row.ok);
const elapsedMs = Number(row.request_elapsed_ms) || 0;
state.total += 1;
state.ok += Number(rowOk);
state.failures += Number(!rowOk);
state.latencyTotal += elapsedMs;
state.maxLatency = Math.max(state.maxLatency, elapsedMs);
if (rowOk) {
if (state.lastOk === false) state.reconnects += 1;
state.lastError = "";
} else {
if (state.lastOk !== false) state.offlineEvents += 1;
state.lastError = String(row.error || "sdk/ws/hook not ready");
}
state.lastOk = rowOk;
}
return state;
}
const args = parseArgs(process.argv.slice(2));
const outputDir = path.resolve(args.outputDir);
const samplesPath = path.join(outputDir, "stability_samples.jsonl");
const summaryPath = path.join(outputDir, "stability_latest_summary.json");
await mkdir(outputDir, { recursive: true });
const state = await restoreSamples(samplesPath);
let stopping = false;
process.on("SIGTERM", () => {
stopping = true;
});
process.on("SIGINT", () => {
stopping = true;
});
while (!stopping) {
const sampleStarted = Date.now();
const now = new Date();
const row = {
timestamp: now.toISOString(),
device_id: args.deviceId,
ok: false,
};
try {
const health = await requestJson(`${args.base}/health`, args.timeoutSeconds);
const query = new URLSearchParams({
device_id: args.deviceId,
samples: "1",
interval_seconds: "0",
});
const watch = await requestJson(
`${args.base}/api/v3/stability/watch?${query.toString()}`,
args.timeoutSeconds,
);
const probe = Array.isArray(watch?.data?.samples) ? watch.data.samples[0] ?? {} : {};
Object.assign(row, {
sdk_healthy: health.status === "healthy",
devices_online: health.devices_online,
ws_online: Boolean(probe.ws_online),
adb_online: Boolean(probe.adb_online),
hook_ok: Boolean(probe.hook_ok),
wechat_version: probe.wechat_version || "",
probe_latency_ms: probe.latency_ms || 0,
error: probe.error || "",
});
row.ok = Boolean(row.sdk_healthy && row.ws_online && row.hook_ok);
} catch (error) {
row.error = String(error?.message || error).slice(0, 500);
}
row.request_elapsed_ms = Date.now() - sampleStarted;
state.total += 1;
state.latencyTotal += row.request_elapsed_ms;
state.maxLatency = Math.max(state.maxLatency, row.request_elapsed_ms);
if (row.ok) {
state.ok += 1;
if (state.lastOk === false) state.reconnects += 1;
state.lastError = "";
} else {
state.failures += 1;
state.lastError = String(row.error || "sdk/ws/hook not ready");
if (state.lastOk !== false) state.offlineEvents += 1;
}
state.lastOk = Boolean(row.ok);
await appendFile(samplesPath, `${JSON.stringify(row)}\n`, "utf8");
const elapsedSeconds = Math.max((now.getTime() - state.startedAt.getTime()) / 1000, 0);
await writeJson(summaryPath, {
started_at: state.startedAt.toISOString(),
updated_at: now.toISOString(),
elapsed_seconds: Math.trunc(elapsedSeconds),
elapsed_hours: Number((elapsedSeconds / 3600).toFixed(3)),
device_id: args.deviceId,
base: args.base,
interval_seconds: args.intervalSeconds,
total_samples: state.total,
ok_samples: state.ok,
failed_samples: state.failures,
success_rate: state.total ? Number((state.ok / state.total).toFixed(6)) : 0,
offline_events: state.offlineEvents,
reconnects: state.reconnects,
average_request_elapsed_ms: state.total
? Number((state.latencyTotal / state.total).toFixed(2))
: 0,
max_request_elapsed_ms: state.maxLatency,
last_ok: Boolean(row.ok),
last_error: state.lastError,
acceptance_24h_complete: elapsedSeconds >= 86400 && state.failures === 0,
samples_file: samplesPath,
runner: "node",
});
if (!stopping) {
await new Promise((resolve) => setTimeout(resolve, Math.max(args.intervalSeconds, 5) * 1000));
}
}

View File

@@ -0,0 +1,208 @@
#!/usr/bin/env python3
"""持续记录工作手机 SDK、WS 与 Hook 稳定性,供 24h/长期验收使用。"""
from __future__ import annotations
import argparse
import json
import signal
import time
from datetime import datetime
from pathlib import Path
from typing import Any
from urllib.parse import urlencode
from urllib.request import Request, urlopen
def request_json(url: str, timeout: float) -> dict[str, Any]:
request = Request(url, headers={"Accept": "application/json", "User-Agent": "workphone-stability/1.0"})
with urlopen(request, timeout=timeout) as response:
return json.loads(response.read().decode("utf-8"))
def write_json(path: Path, data: dict[str, Any]) -> None:
temp = path.with_suffix(path.suffix + ".tmp")
temp.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
temp.replace(path)
def restore_samples(samples_path: Path) -> tuple[datetime, int, int, int, int, int, int, int, bool | None, str]:
started_at = datetime.now().astimezone()
total = ok = failures = offline_events = reconnects = 0
latency_total = max_latency = 0
last_ok: bool | None = None
last_error = ""
if not samples_path.exists():
return (
started_at,
total,
ok,
failures,
offline_events,
reconnects,
latency_total,
max_latency,
last_ok,
last_error,
)
for line in samples_path.read_text(encoding="utf-8").splitlines():
try:
row = json.loads(line)
except json.JSONDecodeError:
continue
if not isinstance(row, dict):
continue
try:
timestamp = datetime.fromisoformat(str(row.get("timestamp", "")))
if total == 0:
started_at = timestamp
except ValueError:
pass
row_ok = bool(row.get("ok"))
total += 1
ok += int(row_ok)
failures += int(not row_ok)
elapsed_ms = int(row.get("request_elapsed_ms") or 0)
latency_total += elapsed_ms
max_latency = max(max_latency, elapsed_ms)
if row_ok:
if last_ok is False:
reconnects += 1
last_error = ""
else:
if last_ok is not False:
offline_events += 1
last_error = str(row.get("error") or "sdk/ws/hook not ready")
last_ok = row_ok
return (
started_at,
total,
ok,
failures,
offline_events,
reconnects,
latency_total,
max_latency,
last_ok,
last_error,
)
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--base", default="http://127.0.0.1:8899")
parser.add_argument("--device-id", default="xgfe65eimrrofyws")
parser.add_argument("--interval-seconds", type=float, default=60)
parser.add_argument("--timeout-seconds", type=float, default=45)
parser.add_argument("--output-dir", required=True)
args = parser.parse_args()
output_dir = Path(args.output_dir).expanduser().resolve()
output_dir.mkdir(parents=True, exist_ok=True)
samples_path = output_dir / "stability_samples.jsonl"
summary_path = output_dir / "stability_latest_summary.json"
stop = False
def handle_stop(_signum: int, _frame: Any) -> None:
nonlocal stop
stop = True
signal.signal(signal.SIGTERM, handle_stop)
signal.signal(signal.SIGINT, handle_stop)
(
started_at,
total,
ok,
failures,
offline_events,
reconnects,
latency_total,
max_latency,
last_ok,
last_error,
) = restore_samples(samples_path)
while not stop:
sample_started = time.time()
now = datetime.now().astimezone()
row: dict[str, Any] = {
"timestamp": now.isoformat(),
"device_id": args.device_id,
"ok": False,
}
try:
health = request_json(f"{args.base}/health", args.timeout_seconds)
query = urlencode({"device_id": args.device_id, "samples": 1, "interval_seconds": 0})
watch = request_json(f"{args.base}/api/v3/stability/watch?{query}", args.timeout_seconds)
watch_data = watch.get("data") if isinstance(watch.get("data"), dict) else {}
samples = watch_data.get("samples") if isinstance(watch_data.get("samples"), list) else []
probe = samples[0] if samples and isinstance(samples[0], dict) else {}
row.update(
{
"sdk_healthy": health.get("status") == "healthy",
"devices_online": health.get("devices_online"),
"ws_online": bool(probe.get("ws_online")),
"adb_online": bool(probe.get("adb_online")),
"hook_ok": bool(probe.get("hook_ok")),
"wechat_version": probe.get("wechat_version", ""),
"probe_latency_ms": probe.get("latency_ms", 0),
"error": probe.get("error", ""),
}
)
row["ok"] = bool(row["sdk_healthy"] and row["ws_online"] and row["hook_ok"])
except Exception as exc:
row["error"] = str(exc)[:500]
row["request_elapsed_ms"] = int((time.time() - sample_started) * 1000)
total += 1
latency_total += row["request_elapsed_ms"]
max_latency = max(max_latency, row["request_elapsed_ms"])
if row["ok"]:
ok += 1
if last_ok is False:
reconnects += 1
last_error = ""
else:
failures += 1
last_error = str(row.get("error") or "sdk/ws/hook not ready")
if last_ok is not False:
offline_events += 1
last_ok = bool(row["ok"])
with samples_path.open("a", encoding="utf-8") as stream:
stream.write(json.dumps(row, ensure_ascii=False) + "\n")
elapsed_seconds = max((now - started_at).total_seconds(), 0)
summary = {
"started_at": started_at.isoformat(),
"updated_at": now.isoformat(),
"elapsed_seconds": int(elapsed_seconds),
"elapsed_hours": round(elapsed_seconds / 3600, 3),
"device_id": args.device_id,
"base": args.base,
"interval_seconds": args.interval_seconds,
"total_samples": total,
"ok_samples": ok,
"failed_samples": failures,
"success_rate": round(ok / total, 6) if total else 0,
"offline_events": offline_events,
"reconnects": reconnects,
"average_request_elapsed_ms": round(latency_total / total, 2) if total else 0,
"max_request_elapsed_ms": max_latency,
"last_ok": bool(row["ok"]),
"last_error": last_error,
"acceptance_24h_complete": elapsed_seconds >= 86400 and failures == 0,
"samples_file": str(samples_path),
}
write_json(summary_path, summary)
if not stop:
time.sleep(max(args.interval_seconds, 5))
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,126 @@
#!/usr/bin/env bash
# ==============================================================================
# run_three_acceptance.sh — 微信三项 P0 真机验收(发消息/收消息/发朋友圈)
#
# 真源:开发文档/1、需求/修改/工作手机_微信全量控机与私域_20260529.md §十三
# 前提:手机 frida-server 已起 + 微信前台bash termux_frida_up_onphone.sh
# supports_hook=true 后执行本脚本。
#
# 用法:
# bash sdk/scripts/run_three_acceptance.sh [device_id]
# ==============================================================================
set -o pipefail
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
DEVICE="${1:-${SDK_DEVICE_ID:-xgfe65eimrrofyws}}"
BASE="${SDK_BASE_URL:-http://127.0.0.1:8899}"
TO="${SDK_E2E_TO_ID:-filehelper}"
TS="$(date +%Y%m%d-%H%M%S)"
EVID_DIR="$ROOT/开发文档/8、部署/05-测试验收/$(date +%Y%m%d)_微信三项验收"
mkdir -p "$EVID_DIR"
REPORT="$EVID_DIR/three_acceptance_${TS}.json"
PASS=0; FAIL=0
declare -a RESULTS
_jq() { python3 -c "import sys,json;d=json.load(sys.stdin);print($1)" 2>/dev/null; }
echo "=== 微信三项 P0 真机验收 $TS ==="
echo "设备 $DEVICE · 目标 $TO"
# ── 0. Hook 就绪检查 ──────────────────────────────────────────────
echo "[0] hook probe..."
PROBE=$(curl -s --max-time 40 "$BASE/api/v3/hook/probe/$DEVICE")
SUPPORTS=$(echo "$PROBE" | _jq "d.get('supports_hook')")
echo " supports_hook=$SUPPORTS"
if [ "$SUPPORTS" != "True" ]; then
echo "[✗] Frida 未附着微信。请先在手机 Termux 执行:"
echo " bash ~/workphone/sdk/scripts/termux_frida_up_onphone.sh -d $DEVICE"
exit 1
fi
# ── 1. 发消息 §13.1 ───────────────────────────────────────────────
echo "[1] 发消息..."
MSG="验收发消息-$TS"
R1=$(curl -s --max-time 90 -X POST "$BASE/api/v3/message/send" -H 'Content-Type: application/json' \
-d "{\"device_id\":\"$DEVICE\",\"platform\":\"wechat\",\"to_id\":\"$TO\",\"content\":\"$MSG\",\"msg_type\":\"text\"}")
echo "$R1" > "$EVID_DIR/1_send_${TS}.json"
OK1=$(echo "$R1" | _jq "d.get('data',{}).get('success') or d.get('success')")
CH1=$(echo "$R1" | _jq "d.get('channel_used','')")
if [ "$OK1" = "True" ] && echo "$CH1" | grep -qi "frida\|hook"; then
echo " ✅ 发消息成功 channel=$CH1"; PASS=$((PASS+1)); RESULTS+=("send:PASS:$CH1")
else
echo " ❌ 发消息失败 channel=$CH1"; FAIL=$((FAIL+1)); RESULTS+=("send:FAIL:$CH1")
fi
# ── 2. 收消息 §13.2filehelper 自发自收 + 列表断言)─────────────
echo "[2] 收消息list 含刚发文本)..."
sleep 2
R2=$(curl -s --max-time 90 -X POST "$BASE/api/v3/message/list" -H 'Content-Type: application/json' \
-d "{\"device_id\":\"$DEVICE\",\"platform\":\"wechat\",\"conversation_id\":\"$TO\",\"limit\":10}")
echo "$R2" > "$EVID_DIR/2_list_${TS}.json"
HIT2=$(echo "$R2" | python3 -c "
import sys,json,re
msg='$MSG'
d=json.load(sys.stdin)
msgs=(d.get('data') or {}).get('messages') or []
norm=lambda s: re.sub(r'[\u200b-\u200d\ufeff]','',str(s))
msg_n=norm(msg)
ok=any(msg_n in norm(m.get('content','')) or msg in str(m.get('content','')) for m in msgs)
print('yes' if ok else 'no')
print(len(msgs))
" 2>/dev/null)
RECV_OK=$(echo "$HIT2" | head -1)
CNT=$(echo "$HIT2" | tail -1)
if [ "$RECV_OK" = "yes" ]; then
echo " ✅ 收消息list 命中刚发文本 (messages=$CNT)"; PASS=$((PASS+1)); RESULTS+=("recv:PASS:hit")
else
echo " ⚠️ 收消息未命中刚发文本messages=$CNT"; FAIL=$((FAIL+1)); RESULTS+=("recv:FAIL:cnt=$CNT")
fi
# ── 3. 发朋友圈 §13.3hook 直连,绕过 post_moments 7200s 防封等待)──
echo "[3] 发朋友圈..."
MTAG="[验收朋友圈] $TS"
R3=$(curl -s --max-time 90 -X POST "$BASE/api/v3/hook/execute" -H 'Content-Type: application/json' \
-d "{\"device_id\":\"$DEVICE\",\"platform\":\"wechat\",\"action\":\"post_moments\",\"params\":{\"content\":\"$MTAG\"},\"hook_only\":true}")
echo "$R3" > "$EVID_DIR/3_moments_post_${TS}.json"
OK3=$(echo "$R3" | _jq "d.get('data',{}).get('success')")
MTAG_SHORT=$(echo "$MTAG" | python3 -c "import sys,re;print(re.sub(r'[\u200b-\u200d\ufeff]','',sys.stdin.read().strip()))" 2>/dev/null || echo "$MTAG")
sleep 5
R3L=$(curl -s --max-time 90 -X POST "$BASE/api/v3/hook/execute" -H 'Content-Type: application/json' \
-d "{\"device_id\":\"$DEVICE\",\"platform\":\"wechat\",\"action\":\"get_moments\",\"params\":{\"limit\":5},\"hook_only\":true}")
echo "$R3L" > "$EVID_DIR/3_moments_list_${TS}.json"
LIST_HIT=$(echo "$R3L" | python3 -c "
import sys,json,re
tag='$MTAG_SHORT'
d=json.load(sys.stdin)
data=d.get('data') or {}
moments=data.get('moments') or []
norm=lambda s: re.sub(r'[\u200b-\u200d\ufeff]','',str(s))
ok=any(tag in norm(str(m.get('content',''))) for m in moments)
print('yes' if ok else 'no')
" 2>/dev/null)
if [ "$OK3" = "True" ]; then
if [ "$LIST_HIT" = "yes" ]; then
echo " ✅ 朋友圈:发布成功 + list 命中"; PASS=$((PASS+1)); RESULTS+=("moments:PASS:hit")
else
echo " ✅ 朋友圈发布成功list 延迟/DB 未同步post 真机已开 SnsUploadUI"; PASS=$((PASS+1)); RESULTS+=("moments:PASS:post_ok")
fi
else
ERR3=$(echo "$R3" | _jq "d.get('error') or d.get('data',{}).get('error') or 'empty'")
echo " ❌ 朋友圈:发布失败 ($ERR3)"; FAIL=$((FAIL+1)); RESULTS+=("moments:FAIL:$ERR3")
fi
# ── 汇总 + 留痕 ───────────────────────────────────────────────────
python3 - "$REPORT" "$DEVICE" "$TS" "$PASS" "$FAIL" "${RESULTS[@]}" <<'PYEOF'
import json,sys
report,device,ts,p,f,*rows=sys.argv[1:]
data={"timestamp":ts,"device_id":device,"pass":int(p),"fail":int(f),
"results":[dict(zip(("item","status","detail"),r.split(":",2))) for r in rows]}
open(report,"w").write(json.dumps(data,ensure_ascii=False,indent=2))
print(f"\n汇总PASS={p} FAIL={f} 报告 {report}")
PYEOF
echo ""
echo "证据目录: $EVID_DIR"
[ "$FAIL" -eq 0 ] && echo "✅ 三项验收全绿" || echo "⚠️ 有失败项,见上"
exit $([ "$FAIL" -eq 0 ] && echo 0 || echo 1)

View File

@@ -0,0 +1,26 @@
#!/usr/bin/env bash
# 微信 128 action 全量验收:等 WS Agent → 静态审计 → Hook catalog → 业务冒烟
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
DEVICE="${SDK_DEVICE_ID:-xgfe65eimrrofyws}"
BASE="${SDK_BASE_URL:-http://127.0.0.1:8899}"
WAIT="${WAIT_SEC:-3600}"
echo "== 1/4 等待 WS Agent (${DEVICE}) 最长 ${WAIT}s =="
bash "$ROOT/sdk/scripts/wait_ws_agent.sh" "$DEVICE" "$BASE" "$WAIT" || {
echo "❌ Agent 未上线。手机 Termux 执行:"
echo "export WP_AGENT_ON_DEVICE=1 WP_FRIDA_MODE=remote WP_FRIDA_PORT=17263 WP_AUTO_DISCOVER=1"
echo "cd ~/workphone/agent && python agent.py -d $DEVICE --heartbeat 10"
exit 1
}
echo "== 2/4 静态三方对齐 =="
python3 "$ROOT/sdk/scripts/wechat_interface_audit.py" --probe -d "$DEVICE" --base "$BASE"
echo "== 3/4 Hook catalog 全 action 探针 =="
python3 "$ROOT/sdk/scripts/matrix_hook_catalog_verify.py" -d "$DEVICE"
echo "== 4/4 业务冒烟:发消息 + 朋友圈 =="
bash "$ROOT/sdk/scripts/run_kaluo_msg_moments.sh"
echo "✅ 全量验收流水线完成"

View File

@@ -0,0 +1,194 @@
#!/usr/bin/env node
import { mkdir, writeFile } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
const deviceId = process.env.WORKPHONE_DEVICE_ID || "xgfe65eimrrofyws";
const base = (process.env.WORKPHONE_BASE || "http://127.0.0.1:8899").replace(/\/$/, "");
const writeTo = process.env.WORKPHONE_TEST_TO_ID || "";
const includeRealWrite = process.env.WORKPHONE_INCLUDE_REAL_WRITE === "1";
const stamp = new Date().toISOString().replace(/\D/g, "").slice(0, 14);
const outputDir = path.join(
root,
"开发文档/8、部署/05-测试验收",
`${stamp.slice(0, 8)}_微信私域能力验收`,
);
const rows = [];
async function request(method, route, body) {
const response = await fetch(`${base}${route}`, {
method,
headers: body ? { "Content-Type": "application/json" } : undefined,
body: body ? JSON.stringify(body) : undefined,
signal: AbortSignal.timeout(90_000),
});
const text = await response.text();
let data;
try {
data = JSON.parse(text);
} catch {
data = { raw: text };
}
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${JSON.stringify(data).slice(0, 300)}`);
}
return data;
}
function query(route, params) {
const search = new URLSearchParams(params);
return `${route}?${search.toString()}`;
}
function dataOf(body) {
return body?.data && typeof body.data === "object" ? body.data : body;
}
function summarize(body) {
const data = dataOf(body) || {};
const summary = {
code: body?.code,
channel_used: body?.channel_used || data.channel,
success: data.success ?? body?.code === 200,
};
for (const key of ["contacts", "groups", "labels", "tags", "messages", "moments", "videos", "favorites", "accounts"]) {
const value = data[key];
if (Array.isArray(value)) summary[`${key}_count`] = value.length;
}
for (const key of [
"count",
"contact_count",
"group_count",
"tag_count",
"message_count",
"returned_count",
"requested_limit",
"offset",
"total_count",
"raw_total_count",
"has_more",
"dry_run",
"confirm_required",
"error_code",
"error",
"note",
]) {
if (key in data) summary[key] = data[key];
}
return summary;
}
function passes(body) {
const data = dataOf(body) || {};
return (
body?.code === undefined ||
body.code === 200 ||
data.success === true ||
data.dry_run === true ||
data.confirm_required === true ||
["anti_ban_blocked", "validation_required", "missing_whitelist"].includes(data.error_code)
);
}
async function run(name, category, task) {
const started = Date.now();
process.stdout.write(`${category.padEnd(10)} ${name.padEnd(28)} `);
try {
const body = await task();
const status = passes(body) ? "passed" : "failed";
rows.push({ name, category, status, elapsed_ms: Date.now() - started, summary: summarize(body) });
console.log(`${status} ${Date.now() - started}ms`);
} catch (error) {
rows.push({ name, category, status: "failed", elapsed_ms: Date.now() - started, error: String(error.message || error) });
console.log(`failed ${Date.now() - started}ms`);
}
}
const common = { device_id: deviceId, platform: "wechat" };
const hook = (action, params = {}) =>
request("POST", "/api/v3/hook/execute", {
...common,
action,
params,
hook_only: true,
});
await run("health", "base", () => request("GET", "/health"));
await run("hook_probe", "base", () => request("GET", `/api/v3/hook/probe/${deviceId}`));
await run("devices", "base", () => request("GET", "/api/v3/devices"));
await run("profile", "read", () => request("GET", query("/api/v3/profile/get", common)));
await run("contacts", "read", () => request("GET", query("/api/v3/contacts", { ...common, limit: 500 })));
await run("contacts_page_2", "read", () => request("GET", query("/api/v3/contacts", { ...common, limit: 200, offset: 200 })));
await run("groups", "read", () => request("GET", query("/api/v3/group/list", { ...common, limit: 500 })));
await run("group_members", "read", () => request("GET", query("/api/v3/group/members", { ...common, group_id: "23070008577@chatroom" })));
await run("tags", "read", () => request("GET", query("/api/v3/tag/list", common)));
await run("messages", "read", () => request("POST", "/api/v3/message/list", { ...common, limit: 500 }));
await run("messages_page_2", "read", () => request("POST", "/api/v3/message/list", { ...common, limit: 100, offset: 100 }));
await run("search", "read", () => request("GET", query("/api/v3/search/wechat", { ...common, keyword: "客户" })));
await run("hook_data_preview", "read", () =>
request(
"GET",
query(`/api/v3/hook/data/${deviceId}`, {
modules: "profile,contacts,groups,labels,messages,device_info",
contact_limit: 200,
message_limit: 50,
}),
),
);
await run("customer_profile_bundle", "read", () =>
request("GET", query("/api/v3/customer/profile-bundle", { ...common, limit: 10, contact_limit: 200, message_limit: 50 })),
);
await run("message_sync_since", "read", () =>
request("POST", "/api/v3/message/sync-since", { ...common, since_time: 0, limit: 500 }),
);
await run("favorites", "read", () => request("GET", query("/api/v3/favorites/list", { ...common, limit: 5 })));
await run("official_accounts", "read", () => hook("get_official_accounts", { limit: 5 }));
await run("moments", "read", () => hook("get_moments", { limit: 5 }));
await run("video_channel", "read", () => request("GET", query("/api/v3/video-channel/list", { ...common, limit: 5 })));
await run("wallet", "read", () => request("GET", query("/api/v3/payment/wallet", common)));
await run("transactions", "read", () => request("GET", query("/api/v3/payment/transactions", { ...common, limit: 5 })));
await run("stability_watch", "stability", () =>
request("GET", query("/api/v3/stability/watch", { device_id: deviceId, samples: 2, interval_seconds: 1 })),
);
await run("send_red_packet_dry_run", "gate", () =>
request("POST", "/api/v3/payment/red-packet", { ...common, to_id: writeTo || "filehelper", amount: "0.01", message: "dry-run" }),
);
await run("transfer_dry_run", "gate", () =>
request("POST", "/api/v3/payment/transfer", { ...common, to_id: writeTo || "filehelper", amount: "0.01", message: "dry-run" }),
);
if (includeRealWrite && writeTo) {
await run("send_message_whitelist", "write", () =>
request("POST", "/api/v3/message/send", {
...common,
to_id: writeTo,
content: `[私域验收] ${stamp}`,
msg_type: "text",
channel: "hook",
}),
);
} else {
rows.push({ name: "send_message_whitelist", category: "write", status: "gated", reason: "需 WORKPHONE_INCLUDE_REAL_WRITE=1 + WORKPHONE_TEST_TO_ID" });
}
for (const [name, reason] of [
["mass_send_whitelist", "需测试联系人白名单"],
["group_message_whitelist", "需测试群 group_id"],
["add_friend_whitelist", "需测试 wxid"],
["moments_like_whitelist", "需测试 sns_id"],
]) {
rows.push({ name, category: "write", status: "gated", reason });
}
const summary = {
passed: rows.filter((row) => row.status === "passed").length,
failed: rows.filter((row) => row.status === "failed").length,
gated: rows.filter((row) => row.status === "gated").length,
total: rows.length,
};
await mkdir(outputDir, { recursive: true });
const reportPath = path.join(outputDir, `private_domain_acceptance_node_${stamp}.json`);
await writeFile(reportPath, `${JSON.stringify({ timestamp: new Date().toISOString(), base, device_id: deviceId, summary, results: rows }, null, 2)}\n`);
console.log(`\n私域能力验收(Node): passed=${summary.passed} failed=${summary.failed} gated=${summary.gated} -> ${reportPath}`);
process.exitCode = summary.failed === 0 ? 0 : 1;

View File

@@ -0,0 +1,263 @@
#!/usr/bin/env python3
"""私域微信能力验收脚本。
默认策略:
- 读类接口直接跑,要求 code=200 且返回结构可计数。
- 高风险写类、资金类默认只验证 dry-run/confirm_required 或参数门禁。
- 真实写入动作只在显式传入白名单参数时执行,避免误触真实客户。
"""
from __future__ import annotations
import argparse
import json
import os
import time
from datetime import datetime
from pathlib import Path
from typing import Any, Callable
import requests
ROOT = Path(__file__).resolve().parents[2]
EVID_ROOT = ROOT / "开发文档" / "8、部署" / "05-测试验收"
REQUEST_TIMEOUT = 45
def post(base: str, path: str, payload: dict[str, Any], timeout: int | None = None) -> dict[str, Any]:
return requests.post(f"{base}{path}", json=payload, timeout=timeout or REQUEST_TIMEOUT).json()
def get(base: str, path: str, params: dict[str, Any], timeout: int | None = None) -> dict[str, Any]:
return requests.get(f"{base}{path}", params=params, timeout=timeout or REQUEST_TIMEOUT).json()
def data_of(body: dict[str, Any]) -> dict[str, Any]:
data = body.get("data")
return data if isinstance(data, dict) else body
def count_of(data: dict[str, Any], *keys: str) -> int:
for key in keys:
val = data.get(key)
if isinstance(val, list):
return len(val)
if isinstance(val, int):
return val
return 0
def summarize(body: dict[str, Any]) -> dict[str, Any]:
data = data_of(body)
out: dict[str, Any] = {
"code": body.get("code"),
"channel_used": body.get("channel_used") or data.get("channel"),
"success": data.get("success", body.get("code") == 200),
}
for key in ("contacts", "groups", "labels", "tags", "messages", "moments", "videos", "favorites", "accounts"):
val = data.get(key)
if isinstance(val, list):
out[f"{key}_count"] = len(val)
elif isinstance(val, dict):
nested = val.get(key) or val.get("items") or val.get("list") or val.get("data")
if isinstance(nested, list):
out[f"{key}_count"] = len(nested)
for meta_key in ("count", "returned_count", "requested_limit", "offset", "total_count", "raw_total_count", "has_more"):
if meta_key in val:
out[f"{key}_{meta_key}"] = val[meta_key]
for key in (
"count", "contact_count", "group_count", "tag_count", "message_count",
"requested_limit", "offset", "total_count", "raw_total_count",
"returned_count", "next_since_time", "has_more",
"dry_run", "confirm_required",
"error_code", "error", "note", "action_resolved"
):
if key in data:
out[key] = data[key]
for key in (
"returned_contact_count", "matched_contact_count", "contact_requested_limit",
"contact_offset", "contact_total_count", "contact_raw_total_count", "contacts_has_more",
"returned_message_count", "message_requested_limit", "message_offset",
"message_total_count", "messages_has_more",
):
if key in data:
out[key] = data[key]
if isinstance(data.get("summary"), dict):
out["watch_summary"] = data["summary"]
if isinstance(data.get("profile"), dict):
out["profile_present"] = True
return out
def ok_code(body: dict[str, Any]) -> bool:
return body.get("code") in (None, 200)
def ok_success_or_gate(body: dict[str, Any]) -> bool:
data = data_of(body)
if data.get("success") is True:
return True
if data.get("dry_run") is True or data.get("confirm_required") is True:
return True
if data.get("error_code") in {"anti_ban_blocked", "validation_required", "missing_whitelist"}:
return True
return ok_code(body)
def hook(base: str, device: str, action: str, params: dict[str, Any], hook_only: bool = False) -> dict[str, Any]:
return post(base, "/api/v3/hook/execute", {
"device_id": device,
"platform": "wechat",
"action": action,
"params": params,
"hook_only": hook_only,
})
def main() -> int:
global REQUEST_TIMEOUT
parser = argparse.ArgumentParser()
parser.add_argument("-d", "--device-id", default=os.getenv("WORKPHONE_DEVICE_ID", "xgfe65eimrrofyws"))
parser.add_argument("--base", default=os.getenv("WORKPHONE_BASE", "http://127.0.0.1:8899"))
parser.add_argument("--write-to", default=os.getenv("WORKPHONE_TEST_TO_ID", ""))
parser.add_argument("--test-user-ids", default=os.getenv("WORKPHONE_TEST_USER_IDS", ""))
parser.add_argument("--test-group-id", default=os.getenv("WORKPHONE_TEST_GROUP_ID", ""))
parser.add_argument("--test-sns-id", default=os.getenv("WORKPHONE_TEST_SNS_ID", ""))
parser.add_argument("--include-real-write", action="store_true")
parser.add_argument("--full-read", action="store_true", help="拉取大批量联系人/消息;默认使用预览包并读取 total_count")
parser.add_argument("--request-timeout", type=int, default=int(os.getenv("WORKPHONE_ACCEPTANCE_TIMEOUT", "45")))
args = parser.parse_args()
REQUEST_TIMEOUT = max(5, args.request_timeout)
stamp = datetime.now().strftime("%Y%m%d-%H%M%S")
out_dir = EVID_ROOT / f"{datetime.now().strftime('%Y%m%d')}_微信私域能力验收"
out_dir.mkdir(parents=True, exist_ok=True)
device = args.device_id
base = args.base.rstrip("/")
rows: list[dict[str, Any]] = []
def run(name: str, category: str, fn: Callable[[], dict[str, Any]], expect: Callable[[dict[str, Any]], bool] = ok_code):
started = time.time()
print(f"{category:<10} {name:<28}", flush=True)
try:
body = fn()
passed = expect(body)
row = {
"name": name,
"category": category,
"status": "passed" if passed else "failed",
"elapsed_ms": int((time.time() - started) * 1000),
"summary": summarize(body),
}
if not passed:
row["raw_error"] = str(body)[:500]
except Exception as exc:
row = {
"name": name,
"category": category,
"status": "failed",
"elapsed_ms": int((time.time() - started) * 1000),
"error": str(exc),
}
rows.append(row)
print(f"{'' if row['status']=='passed' else ''} {category:<10} {name:<28} {row['status']} {row['elapsed_ms']}ms", flush=True)
time.sleep(0.1)
common = {"device_id": device, "platform": "wechat"}
contact_limit = 10000 if args.full_read else 500
message_limit = 3000 if args.full_read else 500
run("health", "base", lambda: requests.get(f"{base}/health", timeout=30).json())
run("hook_probe", "base", lambda: requests.get(f"{base}/api/v3/hook/probe/{device}", timeout=60).json())
run("devices", "base", lambda: get(base, "/api/v3/devices", {}))
run("profile", "read", lambda: get(base, "/api/v3/profile/get", common))
run("contacts", "read", lambda: get(base, "/api/v3/contacts", {**common, "limit": contact_limit}))
run("contacts_page_2", "read", lambda: get(base, "/api/v3/contacts", {**common, "limit": 200, "offset": 200}))
run("groups", "read", lambda: get(base, "/api/v3/group/list", {**common, "limit": 500}))
run("tags", "read", lambda: get(base, "/api/v3/tag/list", common))
run("messages", "read", lambda: post(base, "/api/v3/message/list", {**common, "limit": message_limit}))
run("messages_page_2", "read", lambda: post(base, "/api/v3/message/list", {**common, "limit": 100, "offset": 100}))
run("search", "read", lambda: get(base, "/api/v3/search/wechat", {**common, "keyword": "客户"}))
run("hook_data_preview", "read", lambda: get(base, f"/api/v3/hook/data/{device}", {"modules": "profile,contacts,groups,labels,messages,device_info", "contact_limit": 200, "message_limit": 50, "contact_offset": 0, "message_offset": 0}))
run("customer_profile_bundle", "read", lambda: get(base, "/api/v3/customer/profile-bundle", {**common, "limit": 10, "contact_limit": 200, "message_limit": 50}))
run("message_sync_since", "read", lambda: post(base, "/api/v3/message/sync-since", {**common, "since_time": 0, "limit": message_limit}))
run("favorites", "read", lambda: get(base, "/api/v3/favorites/list", {**common, "limit": 5}), ok_success_or_gate)
run("official_accounts", "read", lambda: hook(base, device, "get_official_accounts", {"limit": 5}, hook_only=True), ok_success_or_gate)
run("wallet", "read", lambda: get(base, "/api/v3/payment/wallet", common), ok_success_or_gate)
run("transactions", "read", lambda: get(base, "/api/v3/payment/transactions", {**common, "limit": 5}), ok_success_or_gate)
run("stability_watch", "stability", lambda: get(base, "/api/v3/stability/watch", {"device_id": device, "samples": 2, "interval_seconds": 1}))
run("send_red_packet_dry_run", "gate", lambda: post(base, "/api/v3/payment/red-packet", {
**common,
"to_id": args.write_to or "filehelper",
"amount": "0.01",
"message": "dry-run",
}), ok_success_or_gate)
run("transfer_dry_run", "gate", lambda: post(base, "/api/v3/payment/transfer", {
**common,
"to_id": args.write_to or "filehelper",
"amount": "0.01",
"message": "dry-run",
}), ok_success_or_gate)
run("receive_red_packet_gate", "gate", lambda: hook(base, device, "receive_red_packet", {}, hook_only=False), ok_success_or_gate)
test_users = [x.strip() for x in args.test_user_ids.split(",") if x.strip()]
if args.include_real_write and args.write_to:
run("send_message_whitelist", "write", lambda: post(base, "/api/v3/message/send", {
**common,
"to_id": args.write_to,
"content": f"[私域验收] {stamp}",
"msg_type": "text",
"channel": "hook",
}), ok_success_or_gate)
else:
rows.append({"name": "send_message_whitelist", "category": "write", "status": "gated", "reason": "需 --include-real-write + --write-to"})
if args.include_real_write and test_users:
run("mass_send_whitelist", "write", lambda: post(base, "/api/v3/mass-send", {
**common,
"user_ids": test_users,
"content": f"[私域群发验收] {stamp}",
}), ok_success_or_gate)
else:
rows.append({"name": "mass_send_whitelist", "category": "write", "status": "gated", "reason": "需 --include-real-write + --test-user-ids"})
if args.include_real_write and args.test_group_id:
run("group_message_whitelist", "write", lambda: post(base, "/api/v3/group/send-message", {
**common,
"group_id": args.test_group_id,
"content": f"[私域群消息验收] {stamp}",
}), ok_success_or_gate)
else:
rows.append({"name": "group_message_whitelist", "category": "write", "status": "gated", "reason": "需 --include-real-write + --test-group-id"})
if args.include_real_write and args.test_sns_id:
run("moments_like_whitelist", "write", lambda: hook(base, device, "like_moments", {
"sns_id": args.test_sns_id,
}, hook_only=False), ok_success_or_gate)
else:
rows.append({"name": "moments_like_whitelist", "category": "write", "status": "gated", "reason": "需 --include-real-write + --test-sns-id"})
passed = sum(1 for r in rows if r["status"] == "passed")
failed = sum(1 for r in rows if r["status"] == "failed")
gated = sum(1 for r in rows if r["status"] == "gated")
report = {
"timestamp": datetime.now().isoformat(),
"base": base,
"device_id": device,
"summary": {"passed": passed, "failed": failed, "gated": gated, "total": len(rows)},
"results": rows,
}
path = out_dir / f"private_domain_acceptance_{stamp}.json"
path.write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
print(f"\n私域能力验收: passed={passed} failed={failed} gated={gated} -> {path}")
return 0 if failed == 0 else 1
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,98 @@
#!/usr/bin/env bash
# 真机微信全功能 + 截图 一键验收
# 目的:设备回线后,一条命令完成「读/发/转发 + 真机截图」并落盘证据(含 PNG 图片)。
# 真机铁律:只认 SDK :8899 上 WS Agent 在线的真机;离线直接 exit 2不伪造。
#
# 用法:
# WORKPHONE_SDK_URL=http://127.0.0.1:8899 DEVICE_ID=xgfe65eimrrofyws \
# bash sdk/scripts/run_wechat_realdevice_acceptance.sh
#
# 可选TO_ID=filehelper发/转发目标) INCLUDE_FORWARD=1 EVIDENCE_DIR=...
set -uo pipefail
SDK="${WORKPHONE_SDK_URL:-http://127.0.0.1:8899}"
DEVICE_ID="${DEVICE_ID:-xgfe65eimrrofyws}"
TO_ID="${TO_ID:-filehelper}"
INCLUDE_FORWARD="${INCLUDE_FORWARD:-1}"
TS="$(date +%Y%m%d_%H%M%S)"
REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
EVIDENCE_DIR="${EVIDENCE_DIR:-$REPO_ROOT/开发文档/8、部署/05-测试验收/${TS%_*}_工作手机微信全功能}"
mkdir -p "$EVIDENCE_DIR"
PASS=0; FAIL=0
ok(){ echo "PASS: $1"; PASS=$((PASS+1)); }
bad(){ echo "FAIL: $1"; FAIL=$((FAIL+1)); }
jq_get(){ python3 -c "import sys,json;d=json.load(sys.stdin);print(eval('d'+sys.argv[1]) if d else '')" "$1" 2>/dev/null; }
echo "==> 真机门禁:$SDK device=$DEVICE_ID"
HEALTH="$(curl -sS -m 10 "$SDK/health" 2>/dev/null)"
ONLINE="$(printf '%s' "$HEALTH" | python3 -c "import sys,json;d=json.load(sys.stdin);print(d.get('devices_online',0))" 2>/dev/null || echo 0)"
if [[ "${ONLINE:-0}" -lt 1 ]]; then
echo "ABORT: SDK devices_online=$ONLINE,设备未在线(请解锁屏幕/确保 Termux Agent 连 ws://<sdk>/ws/device/$DEVICE_ID)。真机铁律:不伪造验收。"
echo "$HEALTH" > "$EVIDENCE_DIR/health_offline_$TS.json"
exit 2
fi
ok "SDK devices_online=$ONLINE"
exec_wx(){ # action params_json -> stdout json
curl -sS -m 30 -X POST "$SDK/api/v3/wechat/execute" -H "Content-Type: application/json" \
-d "{\"device_id\":\"$DEVICE_ID\",\"action\":\"$1\",\"params\":$2}" 2>/dev/null
}
# 1) 截图WS 优先adb 兜底),解码 PNG —— 图片验收证据
echo "==> 截图WS Agent"
SHOT_JSON="$EVIDENCE_DIR/screenshot_${TS}.json"
curl -sS -m 25 -X POST "$SDK/api/v3/devices/$DEVICE_ID/screenshot" -H "Content-Type: application/json" -d '{}' -o "$SHOT_JSON" 2>/dev/null
PNG="$EVIDENCE_DIR/wechat_screen_${TS}.png"
if python3 - "$SHOT_JSON" "$PNG" <<'PY'
import sys,json,base64,os
src,out=sys.argv[1],sys.argv[2]
try: d=json.load(open(src))
except Exception: sys.exit(1)
b=((d.get('data') or {}).get('base64') or '')
if not b: sys.exit(1)
open(out,'wb').write(base64.b64decode(b.split(',')[-1]))
print(os.path.getsize(out))
PY
then ok "截图 PNG -> $PNG"; else
# adb 兜底
if command -v adb >/dev/null && adb -s "$DEVICE_ID" get-state >/dev/null 2>&1; then
adb -s "$DEVICE_ID" exec-out screencap -p > "$PNG" 2>/dev/null && [[ -s "$PNG" ]] && ok "截图 PNG(adb) -> $PNG" || bad "截图失败(adb)"
else
bad "截图失败WS 超时 + 无 adb 连接)"
fi
fi
# 2) 读类
echo "==> 读类"
exec_wx get_profile '{}' > "$EVIDENCE_DIR/profile_${TS}.json"
[[ "$(jq_get "['data']['success']" < "$EVIDENCE_DIR/profile_${TS}.json")" == "True" ]] && ok "get_profile" || bad "get_profile"
exec_wx get_contacts '{"limit":5}' > "$EVIDENCE_DIR/contacts_${TS}.json"
[[ "$(jq_get "['data']['success']" < "$EVIDENCE_DIR/contacts_${TS}.json")" == "True" ]] && ok "get_contacts" || bad "get_contacts"
exec_wx get_messages "{\"limit\":3,\"talker\":\"$TO_ID\"}" > "$EVIDENCE_DIR/messages_${TS}.json"
[[ "$(jq_get "['data']['success']" < "$EVIDENCE_DIR/messages_${TS}.json")" == "True" ]] && ok "get_messages" || bad "get_messages"
# 3) 发送(真机 Frida send_msg_eventverified 才算通过)
echo "==> 发送 send_message -> $TO_ID"
SEND="$(exec_wx send_message "{\"to_id\":\"$TO_ID\",\"content\":\"[真机验收] $TS\"}")"
echo "$SEND" > "$EVIDENCE_DIR/send_message_${TS}.json"
MID="$(printf '%s' "$SEND" | python3 -c "import sys,json;d=json.load(sys.stdin);print((d.get('data') or {}).get('message_id',''))" 2>/dev/null)"
VER="$(printf '%s' "$SEND" | python3 -c "import sys,json;d=json.load(sys.stdin);print((d.get('data') or {}).get('verified',False))" 2>/dev/null)"
[[ -n "$MID" && "$VER" == "True" ]] && ok "send_message verified msg_id=$MID" || bad "send_message verified=$VER mid=$MID"
# 4) 转发(可选)
if [[ "$INCLUDE_FORWARD" == "1" && -n "$MID" ]]; then
echo "==> 转发 forward_message msg_svr_id=$MID -> $TO_ID"
FWD="$(exec_wx forward_message "{\"msg_svr_id\":\"$MID\",\"to_id\":\"$TO_ID\"}")"
echo "$FWD" > "$EVIDENCE_DIR/forward_message_${TS}.json"
FVER="$(printf '%s' "$FWD" | python3 -c "import sys,json;d=json.load(sys.stdin);print((d.get('data') or {}).get('verified',False))" 2>/dev/null)"
[[ "$FVER" == "True" ]] && ok "forward_message verified" || bad "forward_message verified=$FVER"
fi
echo
echo "==> 证据目录:$EVIDENCE_DIR"
echo "==> 结果PASS=$PASS FAIL=$FAIL"
[[ "$FAIL" -gt 0 ]] && exit 1
echo "==> 真机微信全功能验收通过"
exit 0

View File

@@ -0,0 +1,65 @@
#!/usr/bin/env bash
# 微信安全批量验收:仅对白名单测试联系人执行,默认 dry-run。
set -euo pipefail
DEVICE="${SDK_DEVICE_ID:-xgfe65eimrrofyws}"
BASE="${SDK_BASE_URL:-http://127.0.0.1:8899}"
TO_IDS_RAW="${WORKPHONE_TEST_TO_IDS:-}"
CONTENT="${WORKPHONE_TEST_CONTENT:-[工作手机白名单批量验收] $(date +%Y%m%d-%H%M%S)}"
INTERVAL="${WORKPHONE_BATCH_INTERVAL:-65}"
DRY_RUN="${WORKPHONE_DRY_RUN:-1}"
MAX_COUNT="${WORKPHONE_MAX_COUNT:-20}"
if [[ -z "${TO_IDS_RAW}" ]]; then
echo "缺少 WORKPHONE_TEST_TO_IDS。示例"
echo " WORKPHONE_TEST_TO_IDS='filehelper,wxid_test_1' WORKPHONE_DRY_RUN=1 bash sdk/scripts/run_wechat_safe_batch_acceptance.sh"
exit 2
fi
IFS=',' read -r -a TO_IDS <<< "${TO_IDS_RAW}"
if (( ${#TO_IDS[@]} > MAX_COUNT )); then
echo "白名单数量 ${#TO_IDS[@]} 超过上限 ${MAX_COUNT},拒绝执行。"
exit 3
fi
TS="$(date +%Y%m%d-%H%M%S)"
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
EVID_DIR="${ROOT}/开发文档/8、部署/05-测试验收/$(date +%Y%m%d)_微信安全批量验收"
mkdir -p "${EVID_DIR}"
REPORT="${EVID_DIR}/safe_batch_${TS}.jsonl"
probe="$(curl -sS --max-time 40 "${BASE}/api/v3/hook/probe/${DEVICE}")"
supports="$(echo "${probe}" | jq -r '.supports_hook // false' 2>/dev/null || echo false)"
if [[ "${supports}" != "true" ]]; then
echo "Hook 未就绪,拒绝批量验收。"
echo "${probe}" > "${EVID_DIR}/probe_${TS}.json"
exit 4
fi
echo "设备=${DEVICE} count=${#TO_IDS[@]} interval=${INTERVAL}s dry_run=${DRY_RUN}"
for to_id in "${TO_IDS[@]}"; do
to_id="$(echo "${to_id}" | xargs)"
[[ -z "${to_id}" ]] && continue
if [[ "${DRY_RUN}" == "1" ]]; then
row="$(jq -cn \
--arg to_id "$to_id" \
--arg content "$CONTENT" \
--argjson timestamp "$(date +%s)" \
'{dry_run:true,to_id:$to_id,content:$content,timestamp:$timestamp}')"
echo "$row" | tee -a "$REPORT"
continue
fi
resp="$(curl -sS --max-time 120 -X POST "${BASE}/api/v3/message/send" \
-H 'Content-Type: application/json' \
-d "{\"device_id\":\"${DEVICE}\",\"platform\":\"wechat\",\"to_id\":\"${to_id}\",\"content\":\"${CONTENT}\",\"msg_type\":\"text\",\"channel\":\"hook\"}")"
row="$(jq -cn \
--arg to_id "$to_id" \
--arg raw "$resp" \
--argjson timestamp "$(date +%s)" \
'{dry_run:false,to_id:$to_id,response:(try ($raw|fromjson) catch {raw:$raw}),timestamp:$timestamp}')"
echo "$row" | tee -a "$REPORT"
sleep "${INTERVAL}"
done
echo "报告: ${REPORT}"

View File

@@ -0,0 +1,189 @@
#!/usr/bin/env python3
"""无线微信安全能力矩阵验收。
默认只执行低风险读类与 filehelper 白名单消息;高风险、资金、账号破坏类动作只登记 gated。
"""
from __future__ import annotations
import argparse
import json
import time
from datetime import datetime
from pathlib import Path
from typing import Any
import requests
ROOT = Path(__file__).resolve().parents[2]
EVID_ROOT = ROOT / "开发文档" / "8、部署" / "05-测试验收"
READ_ACTIONS: dict[str, dict[str, Any]] = {
"ping": {},
"get_hook_status": {},
"get_wechat_version": {},
"get_profile": {},
"get_device_info": {},
"get_storage_info": {},
"get_network_info": {},
"get_process_info": {},
"check_login_state": {},
"get_contacts": {"limit": 10},
"search_contacts": {"keyword": "文件", "limit": 5},
"get_messages": {"conversation_id": "filehelper", "limit": 5},
"get_recent_messages": {"limit": 5},
"search_messages": {"keyword": "验收", "limit": 5},
"get_groups": {"limit": 5},
"get_labels": {},
"get_moments": {"limit": 5},
"get_friend_requests": {},
"get_favorites": {"limit": 5},
"get_official_accounts": {"limit": 5},
}
SAFE_WRITE_ACTIONS: dict[str, dict[str, Any]] = {
"send_message": {
"to_id": "filehelper",
"content": f"[工作手机安全矩阵] {datetime.now().strftime('%Y%m%d-%H%M%S')}",
"msg_type": "text",
},
}
GATED_ACTIONS: dict[str, str] = {
"batch_send": "批量触达:需要 WORKPHONE_TEST_TO_IDS 白名单,默认 dry-run真实发送按 65s+。",
"mass_send": "群发:需要白名单测试联系人、内容去重、限流与停止条件。",
"add_friend": "主动加好友:需要测试 wxid/手机号白名单,默认 180s+,新号限制更严。",
"batch_add_friend": "批量加好友:需要白名单与用户确认,不自动对真实客户执行。",
"accept_friend": "通过好友:需要真实 pending 请求样本。",
"create_group": "建群:需要白名单成员,避免真实客户被拉群。",
"invite_to_group": "邀请入群:需要测试群与测试成员。",
"send_group_message": "群消息:需要测试群 group_id。",
"post_moments": "发朋友圈:受 2h+ 限流,本轮已有 P0 验收,不重复发布。",
"like_moments": "朋友圈点赞:需要真实 sns_id按 15-45s 节律。",
"comment_moments": "朋友圈评论:需要真实 sns_id按 60-180s 节律。",
"send_red_packet": "资金类:测试号、小额、二次确认、审计;默认禁自动跑。",
"send_transfer": "资金类:测试号、小额、二次确认、审计;默认禁自动跑。",
"receive_red_packet": "资金类:需要真实红包消息样本与人工确认。",
"receive_transfer": "资金类:需要真实转账消息样本与人工确认。",
"receive_payment": "资金类:需要测试场景与人工确认。",
"show_payment_code": "付款码:敏感 UI默认不自动展示。",
"logout": "破坏性账号动作:默认不执行。",
"switch_account": "账号切换:默认不执行。",
"login_by_password": "登录动作:需要测试账号凭证与人工确认。",
}
def hook_execute(base: str, device: str, action: str, params: dict[str, Any]) -> dict[str, Any]:
started = time.time()
try:
resp = requests.post(
f"{base}/api/v3/hook/execute",
json={
"device_id": device,
"platform": "wechat",
"action": action,
"params": params,
"hook_only": True,
},
timeout=90,
)
body = resp.json()
except Exception as exc:
return {
"action": action,
"status": "failed",
"elapsed_ms": int((time.time() - started) * 1000),
"error": str(exc),
}
data = body.get("data") if isinstance(body.get("data"), dict) else body
success = bool(data.get("success")) if isinstance(data, dict) and "success" in data else body.get("code") == 200
return {
"action": action,
"status": "passed" if success else "failed",
"elapsed_ms": int((time.time() - started) * 1000),
"channel": body.get("channel_used") or data.get("channel") if isinstance(data, dict) else body.get("channel_used"),
"code": body.get("code"),
"error": "" if success else str(data.get("error") or data.get("message") or body)[:300],
"summary": summarize_data(data),
}
def summarize_data(data: Any) -> dict[str, Any]:
if not isinstance(data, dict):
return {"type": type(data).__name__}
summary: dict[str, Any] = {}
for key in ("success", "wechat_version", "version", "channel", "action_resolved"):
if key in data:
summary[key] = data[key]
for key in ("contacts", "messages", "groups", "labels", "moments", "requests", "favorites", "accounts"):
val = data.get(key)
if isinstance(val, list):
summary[f"{key}_count"] = len(val)
if isinstance(data.get("profile"), dict):
summary["profile_present"] = True
if data.get("message_id"):
summary["message_id_present"] = True
return summary
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("-d", "--device-id", default="xgfe65eimrrofyws")
parser.add_argument("--base", default="http://127.0.0.1:8899")
parser.add_argument("--include-safe-write", action="store_true")
args = parser.parse_args()
stamp = datetime.now().strftime("%Y%m%d-%H%M%S")
out_dir = EVID_ROOT / f"{datetime.now().strftime('%Y%m%d')}_微信安全矩阵验收"
out_dir.mkdir(parents=True, exist_ok=True)
probe = requests.get(f"{args.base}/api/v3/hook/probe/{args.device_id}", timeout=60).json()
rows: list[dict[str, Any]] = []
if not probe.get("supports_hook"):
report = {"timestamp": stamp, "device_id": args.device_id, "probe": probe, "results": []}
path = out_dir / f"safe_matrix_{stamp}.json"
path.write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
print(f"Hook 未就绪,报告 {path}")
return 2
for action, params in READ_ACTIONS.items():
row = hook_execute(args.base, args.device_id, action, params)
rows.append(row)
print(f"{'' if row['status'] == 'passed' else ''} {action:<24} {row['status']} {row['elapsed_ms']}ms")
time.sleep(0.8)
if args.include_safe_write:
for action, params in SAFE_WRITE_ACTIONS.items():
row = hook_execute(args.base, args.device_id, action, params)
rows.append(row)
print(f"{'' if row['status'] == 'passed' else ''} {action:<24} {row['status']} {row['elapsed_ms']}ms")
time.sleep(1.5)
for action, reason in GATED_ACTIONS.items():
rows.append({"action": action, "status": "gated", "reason": reason})
passed = sum(1 for row in rows if row["status"] == "passed")
failed = sum(1 for row in rows if row["status"] == "failed")
gated = sum(1 for row in rows if row["status"] == "gated")
report = {
"timestamp": datetime.now().isoformat(),
"device_id": args.device_id,
"base": args.base,
"probe": {
"supports_hook": probe.get("supports_hook"),
"wechat_version": probe.get("wechat_version"),
"transport": probe.get("transport"),
},
"summary": {"passed": passed, "failed": failed, "gated": gated, "total": len(rows)},
"results": rows,
}
path = out_dir / f"safe_matrix_{stamp}.json"
path.write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
print(f"\n安全矩阵: passed={passed} failed={failed} gated={gated}{path}")
return 0 if failed == 0 else 1
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,90 @@
#!/usr/bin/env bash
# 无线无 USB 微信全功能验收(纯 WS · adb_count 须为 0
# 用法:
# bash sdk/scripts/run_wireless_wechat_full_suite.sh
# WORKPHONE_SDK_URL=http://192.168.3.1:8899 DEVICE_ID=xgfe65eimrrofyws bash ...
set -uo pipefail
REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
SDK="${WORKPHONE_SDK_URL:-http://127.0.0.1:8899}"
if [[ -f "$REPO_ROOT/sdk/config/active_sdk.env" ]]; then
# shellcheck disable=SC1091
source "$REPO_ROOT/sdk/config/active_sdk.env"
SDK="${WORKPHONE_SDK_URL:-$SDK}"
fi
DEVICE_ID="${DEVICE_ID:-xgfe65eimrrofyws}"
TS="$(date +%Y%m%d_%H%M%S)"
EVID="${EVIDENCE_DIR:-$REPO_ROOT/开发文档/8、部署/05-测试验收/${TS%_*}_无线无USB全功能}"
mkdir -p "$EVID"
echo "==> 无线全量验收 SDK=$SDK device=$DEVICE_ID"
echo "==> 证据: $EVID"
bash "$REPO_ROOT/sdk/scripts/wait_ws_agent.sh" -d "$DEVICE_ID" -b "$SDK" -t "${WAIT_WS_SEC:-300}" -i 5
ADB_COUNT="$(curl -s -m 8 "$SDK/api/v3/connection/status" | python3 -c "import sys,json; print(json.load(sys.stdin)['data'].get('adb_count',-1))" 2>/dev/null || echo -1)"
WS_COUNT="$(curl -s -m 8 "$SDK/api/v3/connection/status" | python3 -c "import sys,json; print(json.load(sys.stdin)['data'].get('online_ws_count',0))" 2>/dev/null || echo 0)"
echo "transport: ws=$WS_COUNT adb=$ADB_COUNT" | tee "$EVID/00_transport.txt"
if [[ "${WS_COUNT:-0}" -lt 1 ]]; then
echo "ABORT: WS 未在线" >&2; exit 2
fi
echo "==> Frida 无线 reload"
curl -s -m 35 -X POST "$SDK/api/v3/devices/$DEVICE_ID/modules/reload" \
-H 'Content-Type: application/json' \
-d '{"module_ids":["wechat"],"force":true}' | tee "$EVID/frida_reload.json" >/dev/null
sleep 8
EXIT=0
WORKPHONE_SDK_URL="$SDK" DEVICE_ID="$DEVICE_ID" INCLUDE_FORWARD=1 EVIDENCE_DIR="$EVID/realdevice" \
bash "$REPO_ROOT/sdk/scripts/run_wechat_realdevice_acceptance.sh" 2>&1 | tee "$EVID/01_realdevice.log" || EXIT=1
cd "$REPO_ROOT/../cunkebao_v3/Server" 2>/dev/null || cd "$REPO_ROOT/../../cunkebao_v3/Server"
CKB_PASSWORD="${CKB_PASSWORD:-zhiqun1984}" DEVICE_ID="$DEVICE_ID" TIMEOUT_SEC="${TIMEOUT_SEC:-90}" \
bash scripts/workphone_bff_acceptance.sh 2>&1 | tee "$EVID/02_bff.log" || EXIT=1
SDK_BASE_URL="$SDK" SDK_DEVICE_ID="$DEVICE_ID" \
python3 "$REPO_ROOT/sdk/scripts/matrix_hook_catalog_verify.py" -d "$DEVICE_ID" 2>&1 | tee "$EVID/03_hook_catalog.log" || true
export WORKPHONE_SDK_URL="$SDK"
python3 <<'PY' | tee "$EVID/04_revoke_e2e.json"
import json, urllib.request, time, os
SDK=os.environ["WORKPHONE_SDK_URL"]
BFF="http://127.0.0.1:8082"
DEV=os.environ.get("DEVICE_ID","xgfe65eimrrofyws")
out={"wireless_only": True, "results": []}
def req(url, data=None, token=None, timeout=120):
h={"Content-Type":"application/json"}
if token: h["Authorization"]="Bearer "+token
r=urllib.request.Request(url, (json.dumps(data).encode() if data is not None else None), headers=h, method=("POST" if data is not None else "GET"))
with urllib.request.urlopen(r, timeout=timeout) as resp:
return json.loads(resp.read())
token=req(BFF+"/v1/auth/login", {"account":"karuo","password":"zhiqun1984","typeId":1})["data"]["token"]
send=req(BFF+"/v1/workphone/message/send", {"device_id":DEV,"platform":"wechat","to_id":"filehelper","content":f"[无线撤回] {time.strftime('%H:%M:%S')}","msg_type":"text"}, token)
sd=(send.get("data") or {}).get("data", send.get("data"))
mid=str(sd.get("message_id")) if isinstance(sd,dict) and sd.get("message_id") else None
out["results"].append({"test":"send","ok":send.get("code")==200 and sd.get("verified"),"message_id":mid})
if mid:
time.sleep(2)
body={"device_id":DEV,"action":"revoke_message","params":{"msg_svr_id":mid,"talker":"filehelper"}}
r=urllib.request.Request(SDK+"/api/v3/wechat/execute", json.dumps(body).encode(), headers={"Content-Type":"application/json"}, method="POST")
rv=json.loads(urllib.request.urlopen(r, timeout=120).read())
d=rv.get("data") or {}
out["results"].append({"test":"revoke_message","ok":bool(d.get("verified") or d.get("success") or d.get("revoked")),"data":d})
time.sleep(3)
msgs=req(BFF+f"/v1/workphone/messages?device_id={DEV}&platform=wechat&limit=8&talker=filehelper", token=token)
items=(msgs.get("data") or {}).get("items") or (msgs.get("data") or {}).get("messages") or []
out["results"].append({"test":"revoke_in_history","ok":any("撤回" in str(m.get("content","")) or str(m.get("type"))=="10000" for m in items if isinstance(m,dict))})
out["pass"]=sum(1 for x in out["results"] if x.get("ok"))
out["total"]=len(out["results"])
print(json.dumps(out, ensure_ascii=False, indent=2))
PY
if command -v docker >/dev/null && docker ps --format '{{.Names}}' | grep -q cunkebao-server; then
docker exec cunkebao-server php tests/four_end_device_wechat_chain_test.php 2>&1 | tee "$EVID/05_four_end_chain.log" || EXIT=1
fi
echo "==> 汇总见 $EVID"
exit "$EXIT"

View File

@@ -0,0 +1,155 @@
#!/usr/bin/env bash
# SDK 连接监视器(无 USB 依赖)— 探测 NAS/Docker/frp 可达性,写 active_sdk.env监控 WS 在线
# 用法:
# bash sdk/scripts/sdk_connection_watcher.sh --once
# bash sdk/scripts/sdk_connection_watcher.sh --bg
# bash sdk/scripts/install_sdk_watcher_launchagent.sh
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SDK_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
DEVICE_ID="${DEVICE_ID:-xgfe65eimrrofyws}"
SDK_PORT="${WORKPHONE_SDK_PORT:-8899}"
INTERVAL="${WATCHER_INTERVAL:-20}"
FAST_INTERVAL="${WATCHER_FAST_INTERVAL:-10}"
ACTIVE_ENV="${SDK_ROOT}/config/active_sdk.env"
LOG="${SDK_ROOT}/logs/sdk_connection_watcher.log"
PID_FILE="${SDK_ROOT}/logs/sdk_connection_watcher.pid"
BUILD_CFG="${SCRIPT_DIR}/build_agent_config.py"
mkdir -p "${SDK_ROOT}/logs" "${SDK_ROOT}/config"
log() { echo "[$(date '+%F %T')] $*" | tee -a "$LOG"; }
health_ok() {
curl -sf --max-time 3 "http://$1:${SDK_PORT}/health" >/dev/null 2>&1
}
pick_sdk_url() {
local h url=""
for h in 192.168.110.101 192.168.1.201; do
if health_ok "$h"; then
url="http://${h}:${SDK_PORT}"
echo "$url"
return 0
fi
done
local mac_ip
mac_ip=$(python3 -c "
import subprocess
o=subprocess.check_output(['ifconfig'], text=True)
for line in o.splitlines():
p=line.strip().split()
if len(p)>=2 and p[0]=='inet' and not p[1].startswith('127.'):
if p[1].startswith('192.168.') or p[1].startswith('10.'):
print(p[1]); break
" 2>/dev/null || echo "")
if [[ -n "$mac_ip" ]] && health_ok "$mac_ip"; then
echo "http://${mac_ip}:${SDK_PORT}"
return 0
fi
if health_ok "open.quwanzhi.com"; then
echo "http://open.quwanzhi.com:${SDK_PORT}"
return 0
fi
echo "http://192.168.110.101:${SDK_PORT}"
}
ws_count() {
local base="$1"
curl -s --max-time 5 "${base}/api/v3/connection/status" 2>/dev/null | \
python3 -c "import sys,json;d=json.load(sys.stdin).get('data',{});print(d.get('online_ws_count',0))" 2>/dev/null || echo 0
}
device_online() {
local base="$1"
curl -s --max-time 5 "${base}/api/v3/connection/status" 2>/dev/null | \
python3 -c "
import sys,json
d=json.load(sys.stdin).get('data',{})
ids=d.get('online_device_ids') or []
print(1 if '${DEVICE_ID}' in ids else 0)
" 2>/dev/null || echo 0
}
write_active_env() {
local url="$1"
local ws="$2"
local tier="unknown"
case "$url" in
*192.168.110.101*|*192.168.1.201*) tier="nas_lan" ;;
*open.quwanzhi.com*) tier="nas_frp" ;;
*) tier="mac_docker" ;;
esac
cat >"$ACTIVE_ENV" <<EOF
# auto $(date '+%F %T') sdk_connection_watcher
export WORKPHONE_SDK_URL=${url}
export WORKPHONE_SDK_TIER=${tier}
export WORKPHONE_WS_ONLINE=${ws}
export WORKPHONE_DEVICE_ID=${DEVICE_ID}
EOF
}
watcher_once() {
local url ws dev
url="$(pick_sdk_url)"
ws="$(ws_count "$url")"
dev="$(device_online "$url")"
write_active_env "$url" "$ws"
export WORKPHONE_SDK_URL="$url"
if [[ "$dev" == "1" ]]; then
log "OK tier=$(grep TIER "$ACTIVE_ENV" | cut -d= -f2) sdk=${url} ws=${ws} device=${DEVICE_ID} online"
return 0
fi
if [[ "$ws" -ge 1 ]]; then
log "WARN sdk=${url} ws=${ws} but ${DEVICE_ID} not in list"
return 0
fi
log "OFFLINE sdk=${url} ws=0 — 手机 Termux Agent 应自主寻服重连(无需 Mac USB"
# 若有 ADB 则尝试 OTA 推配置+拉起(可选,不阻塞)
if command -v adb >/dev/null 2>&1; then
local serial
serial=$(adb devices 2>/dev/null | awk '/\tdevice$/{print $1; exit}')
if [[ -n "$serial" ]]; then
log "ADB 可用 ${serial} → OTA termux_push"
DEVICE_SERIAL="$serial" DEVICE_ID="$DEVICE_ID" \
bash "${SCRIPT_DIR}/termux_push_and_start_agent.sh" >>"$LOG" 2>&1 || true
ws="$(ws_count "$url")"
write_active_env "$url" "$ws"
[[ "$ws" -ge 1 ]] && log "OTA ok ws=${ws}" && return 0
fi
fi
return 1
}
watcher_loop() {
log "sdk_connection_watcher start interval=${INTERVAL}s device=${DEVICE_ID}"
while true; do
if watcher_once; then
sleep "$INTERVAL"
else
sleep "$FAST_INTERVAL"
fi
done
}
case "${1:-}" in
--once) watcher_once ;;
--bg)
if [[ -f "$PID_FILE" ]] && kill -0 "$(cat "$PID_FILE")" 2>/dev/null; then
echo "已在运行 PID $(cat "$PID_FILE")"
exit 0
fi
nohup bash "$0" >>"$LOG" 2>&1 &
echo $! >"$PID_FILE"
echo "sdk_connection_watcher 后台 PID $(cat "$PID_FILE")"
;;
--stop)
[[ -f "$PID_FILE" ]] && kill "$(cat "$PID_FILE")" 2>/dev/null || true
rm -f "$PID_FILE"
echo "已停止"
;;
*) watcher_loop ;;
esac

152
sdk/scripts/setup_emulator.sh Executable file
View File

@@ -0,0 +1,152 @@
#!/bin/bash
#
# 一键设置模拟器创建红米13 AVD + 中文系统 + 搜狗五笔 + Agent
# 前置:已安装 Android SDKsdkmanager/avdmanager/emulator/adb
#
set -e
ANDROID_HOME="${ANDROID_HOME:-$HOME/Library/Android/sdk}"
export PATH="$ANDROID_HOME/emulator:$ANDROID_HOME/platform-tools:$ANDROID_HOME/cmdline-tools/latest/bin:$PATH"
AVD_NAME="Redmi13"
IMAGE="system-images;android-34;google_apis;arm64-v8a"
SDK_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
echo "============================================"
echo " 红米13模拟器 一键设置"
echo "============================================"
# 1. 检查镜像
if [ ! -d "$ANDROID_HOME/system-images/android-34/google_apis/arm64-v8a" ]; then
echo "[1/6] 下载 ARM64 系统镜像..."
yes | sdkmanager "$IMAGE"
else
echo "[1/6] 系统镜像已存在 ✓"
fi
# 检查 emulator
if [ ! -f "$ANDROID_HOME/emulator/emulator" ]; then
echo "[1b] 下载 emulator..."
yes | sdkmanager "emulator"
fi
# 2. 创建 AVD
if $ANDROID_HOME/emulator/emulator -list-avds 2>/dev/null | grep -q "$AVD_NAME"; then
echo "[2/6] AVD $AVD_NAME 已存在 ✓"
else
echo "[2/6] 创建 AVD: $AVD_NAME红米13: 1080x2400, 8GB RAM"
echo "no" | avdmanager create avd -n "$AVD_NAME" -k "$IMAGE" -d "pixel_6" --force
# 配置8GB RAM、128GB 存储、1080x2400
AVD_INI="$HOME/.android/avd/${AVD_NAME}.avd/config.ini"
if [ -f "$AVD_INI" ]; then
cat >> "$AVD_INI" <<'CONF'
hw.ramSize=8192
disk.dataPartition.size=128G
hw.lcd.width=1080
hw.lcd.height=2400
hw.lcd.density=420
hw.keyboard=yes
hw.gpu.enabled=yes
hw.gpu.mode=auto
skin.dynamic=yes
CONF
echo " 配置已写入: 8GB RAM, 1080x2400, 128GB"
fi
fi
# 3. 启动模拟器
echo "[3/6] 启动模拟器..."
$ANDROID_HOME/emulator/emulator -avd "$AVD_NAME" -no-snapshot-save -gpu auto &
EMU_PID=$!
echo " 模拟器 PID: $EMU_PID"
# 等设备上线
echo " 等待设备就绪(最多 120s..."
for i in $(seq 1 60); do
if adb shell getprop sys.boot_completed 2>/dev/null | grep -q "1"; then
echo " 设备已就绪 ✓ (${i}x2s)"
break
fi
sleep 2
done
SERIAL=$(adb devices | grep "emulator" | head -1 | awk '{print $1}')
if [ -z "$SERIAL" ]; then
echo "❌ 模拟器未上线,请检查"
exit 1
fi
echo " 设备序列号: $SERIAL"
# 4. 设置中文
echo "[4/6] 设置中文系统..."
adb -s "$SERIAL" shell "settings put system system_locales zh-CN"
adb -s "$SERIAL" shell "setprop persist.sys.language zh"
adb -s "$SERIAL" shell "setprop persist.sys.country CN"
adb -s "$SERIAL" shell "setprop persist.sys.localevar"
adb -s "$SERIAL" shell "settings put global device_name '红米13'"
echo " 中文系统设置完成 ✓"
# 5. 安装搜狗五笔输入法
echo "[5/6] 安装搜狗五笔输入法..."
SOGOU_APK="$SDK_ROOT/apks/sogou_wubi.apk"
if [ -f "$SOGOU_APK" ]; then
adb -s "$SERIAL" install -r "$SOGOU_APK"
echo " 搜狗五笔已安装 ✓"
# 设为默认输入法
adb -s "$SERIAL" shell "ime enable com.sohu.inputmethod.sogou.wubi/.SogouIME" 2>/dev/null || true
adb -s "$SERIAL" shell "ime set com.sohu.inputmethod.sogou.wubi/.SogouIME" 2>/dev/null || true
echo " 搜狗五笔已设为默认 ✓"
else
echo " ⚠️ 未找到 $SOGOU_APK"
echo " 请下载搜狗五笔 APK 到 $SDK_ROOT/apks/sogou_wubi.apk 后重新执行本脚本"
echo " 或手动安装adb install sogou_wubi.apk"
fi
# 6. 安装并启动 Agent
echo "[6/6] 安装 Agent 到模拟器..."
# 先安装 Python如果是 Termux 环境;模拟器走 ADB push
AGENT_DIR="$SDK_ROOT/agent"
REMOTE_DIR="/data/local/tmp/workphone-agent"
adb -s "$SERIAL" shell "mkdir -p $REMOTE_DIR"
adb -s "$SERIAL" push "$AGENT_DIR/agent.py" "$REMOTE_DIR/"
adb -s "$SERIAL" push "$AGENT_DIR/skill_executor.py" "$REMOTE_DIR/"
adb -s "$SERIAL" push "$AGENT_DIR/error_handler.py" "$REMOTE_DIR/"
adb -s "$SERIAL" push "$AGENT_DIR/vision_helper.py" "$REMOTE_DIR/"
adb -s "$SERIAL" push "$AGENT_DIR/voice_agent.py" "$REMOTE_DIR/"
adb -s "$SERIAL" push "$AGENT_DIR/requirements.txt" "$REMOTE_DIR/"
adb -s "$SERIAL" shell "mkdir -p $REMOTE_DIR/skills"
for sk in wechat douyin xhs xianyu; do
adb -s "$SERIAL" shell "mkdir -p $REMOTE_DIR/skills/$sk"
adb -s "$SERIAL" push "$AGENT_DIR/skills/$sk/" "$REMOTE_DIR/skills/$sk/" 2>/dev/null || true
done
adb -s "$SERIAL" push "$AGENT_DIR/skills/__init__.py" "$REMOTE_DIR/skills/"
adb -s "$SERIAL" push "$AGENT_DIR/skills/base.py" "$REMOTE_DIR/skills/"
adb -s "$SERIAL" push "$AGENT_DIR/skills/app_manager.py" "$REMOTE_DIR/skills/"
adb -s "$SERIAL" push "$AGENT_DIR/skills/search.py" "$REMOTE_DIR/skills/"
adb -s "$SERIAL" push "$AGENT_DIR/skills/voice_control.py" "$REMOTE_DIR/skills/"
# 写 config.json
adb -s "$SERIAL" shell "cat > $REMOTE_DIR/config.json" <<AGENTCONF
{
"device_id": "$SERIAL",
"server_url": "ws://10.0.2.2:8899/ws/device",
"heartbeat_interval": 10,
"project_id": "cunkebao"
}
AGENTCONF
echo " Agent 文件已推送 ✓"
echo " 注意10.0.2.2 是模拟器访问宿主机的地址"
echo ""
echo "============================================"
echo " 设置完成!"
echo "============================================"
echo "模拟器: $AVD_NAME ($SERIAL)"
echo "控制台: http://localhost:8899/static/index.html"
echo "API: http://localhost:8899/docs"
echo ""
echo "下一步:"
echo " 1. 先启动 SDKcd sdk && ./scripts/start_sdk.sh"
echo " 2. 手动下载搜狗五笔 APK 到 sdk/apks/ 后执行本脚本"
echo " 3. 模拟器内启动 Agent需 Termux 或 ADB shell"
echo "============================================"

85
sdk/scripts/setup_frida.sh Executable file
View File

@@ -0,0 +1,85 @@
#!/bin/bash
# setup_frida.sh — 一键安装 Frida Server 到 Root Android 设备
#
# 用法:
# ./scripts/setup_frida.sh [device_serial]
#
# 前置条件:
# - 设备已 RootMagisk
# - ADB 已连接且授权
# - Python 已安装 frida-toolspip install frida-tools
set -e
DEVICE_ARG=""
if [ -n "$1" ]; then
DEVICE_ARG="-s $1"
fi
echo "=== 机擎 SDK — Frida Server 安装脚本 ==="
# 1. 获取 Frida 版本(与 Python frida 包一致)
FRIDA_VERSION=$(python3 -c "import frida; print(frida.__version__)" 2>/dev/null)
if [ -z "$FRIDA_VERSION" ]; then
echo "❌ Python frida 未安装,先执行: pip install frida-tools"
exit 1
fi
echo "✅ 本机 Frida 版本: $FRIDA_VERSION"
# 2. 获取设备架构
ABI=$(adb $DEVICE_ARG shell getprop ro.product.cpu.abi)
echo "✅ 设备架构: $ABI"
case "$ABI" in
arm64-v8a) ARCH="arm64" ;;
armeabi-v7a) ARCH="arm" ;;
x86_64) ARCH="x86_64" ;;
x86) ARCH="x86" ;;
*) echo "❌ 不支持的架构: $ABI"; exit 1 ;;
esac
# 3. 下载 frida-server
FILENAME="frida-server-${FRIDA_VERSION}-android-${ARCH}"
URL="https://github.com/frida/frida/releases/download/${FRIDA_VERSION}/${FILENAME}.xz"
TMPDIR="/tmp/frida_setup"
mkdir -p "$TMPDIR"
if [ ! -f "$TMPDIR/$FILENAME" ]; then
echo "📥 下载 frida-server..."
curl -L -o "$TMPDIR/${FILENAME}.xz" "$URL"
xz -d "$TMPDIR/${FILENAME}.xz"
fi
echo "✅ frida-server 已就绪: $TMPDIR/$FILENAME"
# 4. 推送到设备
echo "📤 推送到设备 /data/local/tmp/frida-server ..."
adb $DEVICE_ARG push "$TMPDIR/$FILENAME" /data/local/tmp/frida-server
adb $DEVICE_ARG shell "su -c 'chmod 755 /data/local/tmp/frida-server'"
echo "✅ 已推送并赋权"
# 5. 启动 frida-server
echo "🚀 启动 frida-server ..."
adb $DEVICE_ARG shell "su -c 'pkill -f frida-server || true'" 2>/dev/null || true
adb $DEVICE_ARG shell "su -c 'nohup /data/local/tmp/frida-server >/dev/null 2>&1 &'"
sleep 2
# 6. 验证
echo "🔍 验证连接..."
PONG=$(python3 -c "
import frida
try:
d = frida.get_usb_device(timeout=5)
print(f'✅ Frida 已连接设备: {d.name}')
procs = len(d.enumerate_processes())
print(f'✅ 检测到 {procs} 个进程')
except Exception as e:
print(f'❌ 连接失败: {e}')
" 2>&1)
echo "$PONG"
echo ""
echo "=== 安装完成 ==="
echo "后续使用:"
echo " 1. Agent 启动时会自动通过 FridaManager 连接"
echo " 2. 或手动测试: frida -U -n com.tencent.mm"
echo " 3. SDK 调用: POST /api/v3/message/send {\"channel\": \"hook\", ...}"

319
sdk/scripts/setup_noroot.sh Executable file
View File

@@ -0,0 +1,319 @@
#!/bin/bash
# ============================================================
# 无 Root 全自动部署脚本
# 一键完成:检测设备 → 注入 Gadget → 安装 → 连通 → 控制微信
# ============================================================
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
SDK_DIR="$(dirname "$SCRIPT_DIR")"
GADGET_SO="$SCRIPT_DIR/frida-gadget-17.8.1-android-arm64.so"
KEYSTORE="$SCRIPT_DIR/debug.keystore"
WECHAT_PKG="com.tencent.mm"
GADGET_PORT=27042
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
CYAN='\033[0;36m'
NC='\033[0m'
log() { echo -e "${GREEN}[✓]${NC} $1"; }
warn() { echo -e "${YELLOW}[!]${NC} $1"; }
err() { echo -e "${RED}[✗]${NC} $1"; exit 1; }
info() { echo -e "${CYAN}[→]${NC} $1"; }
# ─────────────────────────────────────
# Phase 1: 等待设备
# ─────────────────────────────────────
info "Phase 1: 等待 ADB 设备连接..."
SERIAL=""
for i in $(seq 1 60); do
DEV=$(adb devices 2>/dev/null | grep -w "device" | head -1 | awk '{print $1}')
if [ -n "$DEV" ]; then
SERIAL="$DEV"
break
fi
printf "."
sleep 2
done
echo ""
[ -z "$SERIAL" ] && err "等待 120 秒仍未检测到设备,请检查 USB 连接和 USB 调试"
log "设备已连接: $SERIAL"
ADB="adb -s $SERIAL"
# 设备基本信息
MODEL=$($ADB shell getprop ro.product.model 2>/dev/null | tr -d '\r')
ANDROID=$($ADB shell getprop ro.build.version.release 2>/dev/null | tr -d '\r')
log "型号: $MODEL | Android: $ANDROID"
# ─────────────────────────────────────
# Phase 2: 检查微信
# ─────────────────────────────────────
info "Phase 2: 检查微信状态..."
WX_VER=$($ADB shell dumpsys package $WECHAT_PKG 2>/dev/null | grep versionName | head -1 | awk -F= '{print $2}' | tr -d '\r ')
[ -z "$WX_VER" ] && err "微信未安装"
log "微信版本: $WX_VER"
# 检查是否已经是注入版
ALREADY_INJECTED=$($ADB shell "run-as $WECHAT_PKG ls lib/arm64/libfrida-gadget.so 2>/dev/null" 2>/dev/null | grep -c "gadget" || echo "0")
if [ "$ALREADY_INJECTED" != "0" ]; then
log "微信已包含 Frida Gadget跳过注入步骤"
SKIP_INJECT=1
else
SKIP_INJECT=0
fi
# ─────────────────────────────────────
# Phase 3: 注入 Frida Gadget如需
# ─────────────────────────────────────
if [ "$SKIP_INJECT" = "0" ]; then
info "Phase 3: 提取并注入微信 APK..."
WORK_DIR="$SCRIPT_DIR/gadget_work"
rm -rf "$WORK_DIR"
mkdir -p "$WORK_DIR"
# 提取 APK
APK_PATH=$($ADB shell pm path $WECHAT_PKG 2>/dev/null | head -1 | sed 's/package://' | tr -d '\r\n')
log "微信 APK: $APK_PATH"
info "拉取 APK可能需要几分钟..."
$ADB pull "$APK_PATH" "$WORK_DIR/wechat_original.apk" 2>&1 | tail -1
APK_SIZE=$(ls -lh "$WORK_DIR/wechat_original.apk" | awk '{print $5}')
log "APK 大小: $APK_SIZE"
# 反编译(只反编译资源,不反编译代码以加快速度)
info "反编译 APK..."
apktool d -f -s -o "$WORK_DIR/wechat_dec" "$WORK_DIR/wechat_original.apk" 2>&1 | tail -2
# 注入 gadget
LIB_DIR="$WORK_DIR/wechat_dec/lib/arm64-v8a"
mkdir -p "$LIB_DIR"
cp "$GADGET_SO" "$LIB_DIR/libfrida-gadget.so"
log "frida-gadget.so 已注入到 lib/arm64-v8a/"
# Gadget 配置listen 模式,自动 resume
cat > "$LIB_DIR/libfrida-gadget.config.so" << 'EOF'
{
"interaction": {
"type": "listen",
"address": "0.0.0.0",
"port": 27042,
"on_port_conflict": "pick-next",
"on_load": "resume"
}
}
EOF
log "Gadget 配置: listen 模式, 端口 $GADGET_PORT"
# 修改 AndroidManifest
MANIFEST="$WORK_DIR/wechat_dec/AndroidManifest.xml"
sed -i.bak 's/android:extractNativeLibs="false"/android:extractNativeLibs="true"/g' "$MANIFEST" 2>/dev/null || true
log "AndroidManifest: extractNativeLibs=true"
# 在 smali 中注入 loadLibrary
info "注入 System.loadLibrary(\"frida-gadget\")..."
# 查找 Application 类
APP_CLASS=$(grep -oP 'android:name="\K[^"]+(?=.*application)' "$MANIFEST" 2>/dev/null | head -1 || true)
[ -z "$APP_CLASS" ] && APP_CLASS="com.tencent.mm.app.MMApplication"
SMALI_REL=$(echo "$APP_CLASS" | sed 's/\./\//g')
SMALI_FILE=""
for d in "$WORK_DIR/wechat_dec"/smali*; do
[ -f "$d/${SMALI_REL}.smali" ] && SMALI_FILE="$d/${SMALI_REL}.smali" && break
done
if [ -n "$SMALI_FILE" ]; then
python3 << PYEOF
import re, sys
path = "$SMALI_FILE"
with open(path, 'r') as f:
code = f.read()
load = ' const-string v0, "frida-gadget"\n invoke-static {v0}, Ljava/lang/System;->loadLibrary(Ljava/lang/String;)V\n'
# 注入到 static initializer
clinit = re.search(r'(\.method\s+.*?static\s+.*?<clinit>.*?\n.*?\.locals\s+\d+\n)', code, re.DOTALL)
if clinit:
pos = clinit.end()
code = code[:pos] + load + code[pos:]
with open(path, 'w') as f:
f.write(code)
print("[✓] 注入到 <clinit>")
sys.exit(0)
# 注入到 attachBaseContext 或 onCreate
for method_name in ['attachBaseContext', 'onCreate']:
pat = rf'(\.method\s+.*?{method_name}\(.*?\n(?:.*?\n)*? \.locals\s+\d+\n)'
m = re.search(pat, code)
if m:
pos = m.end()
code = code[:pos] + load + code[pos:]
with open(path, 'w') as f:
f.write(code)
print(f"[✓] 注入到 {method_name}")
sys.exit(0)
print("[!] 未找到合适注入点gadget 将通过 native lib 自动加载")
PYEOF
else
warn "未找到 Application smali: $APP_CLASS"
fi
# 重新打包
info "重新打包 APK..."
apktool b -o "$WORK_DIR/wechat_unsigned.apk" "$WORK_DIR/wechat_dec" 2>&1 | tail -2
# 签名
info "签名 APK..."
jarsigner -sigalg SHA256withRSA -digestalg SHA-256 \
-keystore "$KEYSTORE" -storepass android -keypass android \
"$WORK_DIR/wechat_unsigned.apk" sdkkey 2>&1 | tail -2
cp "$WORK_DIR/wechat_unsigned.apk" "$WORK_DIR/wechat_patched.apk"
PATCHED_SIZE=$(ls -lh "$WORK_DIR/wechat_patched.apk" | awk '{print $5}')
log "签名完成: $PATCHED_SIZE"
# 备份微信数据(重要!)
info "备份微信数据..."
$ADB shell "pm clear $WECHAT_PKG 2>/dev/null" || true
warn "注意:重新安装会清除微信登录状态,需要重新登录"
# 卸载 + 安装
info "卸载原版微信..."
$ADB shell pm uninstall $WECHAT_PKG 2>&1 || true
info "安装注入版微信(可能需要几分钟)..."
$ADB install "$WORK_DIR/wechat_patched.apk" 2>&1 | tail -1
log "微信安装完成"
fi
# ─────────────────────────────────────
# Phase 4: 启动微信 + 端口转发
# ─────────────────────────────────────
info "Phase 4: 启动微信..."
$ADB shell am force-stop $WECHAT_PKG 2>/dev/null || true
sleep 1
$ADB shell am start -n $WECHAT_PKG/.ui.LauncherUI 2>&1 | tail -1
log "微信已启动,等待 Gadget 初始化..."
sleep 8
info "设置端口转发..."
$ADB forward tcp:$GADGET_PORT tcp:$GADGET_PORT 2>&1
log "端口转发: localhost:$GADGET_PORT → device:$GADGET_PORT"
# ─────────────────────────────────────
# Phase 5: 验证 Frida 连接
# ─────────────────────────────────────
info "Phase 5: 验证 Frida Gadget 连接..."
sleep 3
python3 << 'PYEOF'
import frida
import time
import sys
host = "127.0.0.1:27042"
max_retries = 5
for attempt in range(max_retries):
try:
mgr = frida.get_device_manager()
device = mgr.add_remote_device(host)
session = device.attach("Gadget")
print(f"[✓] Frida Gadget 连接成功(尝试 {attempt+1}")
# 加载 Hook 脚本测试
script = session.create_script('rpc.exports = { ping: function() { return "pong_from_gadget"; } };')
script.load()
result = script.exports_sync.ping()
print(f"[✓] RPC 测试: ping → {result}")
script.unload()
session.detach()
print("[✓] Frida Gadget 工作正常!")
sys.exit(0)
except Exception as e:
print(f"[!] 尝试 {attempt+1}/{max_retries}: {e}")
time.sleep(3)
print("[✗] Frida Gadget 连接失败")
sys.exit(1)
PYEOF
FRIDA_OK=$?
# ─────────────────────────────────────
# Phase 6: 加载微信 Hook 脚本
# ─────────────────────────────────────
if [ "$FRIDA_OK" = "0" ]; then
info "Phase 6: 加载微信 Hook 脚本..."
python3 << PYEOF
import frida, json, sys, os
host = "127.0.0.1:$GADGET_PORT"
script_path = "$SDK_DIR/agent/hook/wechat_hook_v2.js"
if not os.path.exists(script_path):
print(f"[!] Hook 脚本不存在: {script_path}")
sys.exit(1)
with open(script_path, 'r') as f:
source = f.read()
mgr = frida.get_device_manager()
device = mgr.add_remote_device(host)
session = device.attach("Gadget")
script = session.create_script(source)
def on_message(message, data):
if message['type'] == 'send':
payload = message.get('payload', {})
if payload.get('type') == 'hook_event':
print(f" 事件: {payload.get('event_type', '?')} | {json.dumps(payload.get('data', {}), ensure_ascii=False)[:100]}")
elif payload.get('type') == 'log':
print(f" 日志: [{payload.get('tag','')}] {payload.get('message','')}")
elif message['type'] == 'error':
print(f" 错误: {message.get('description','')}")
script.on('message', on_message)
script.load()
rpc = script.exports_sync
try:
pong = rpc.ping()
print(f"[✓] Hook 脚本已加载ping={pong}")
except Exception as e:
print(f"[!] ping 失败: {e}")
try:
info = rpc.get_process_info({})
print(f"[✓] 微信进程: {json.dumps(info, ensure_ascii=False)}")
except Exception as e:
print(f"[!] get_process_info: {e}")
try:
status = rpc.get_hook_status({})
print(f"[✓] Hook 状态: {json.dumps(status, ensure_ascii=False)}")
except Exception as e:
print(f"[!] get_hook_status: {e}")
print("")
print("=" * 50)
print("[✓] 全部完成!微信 Hook 通道已连通")
print("=" * 50)
print(f" Frida Gadget: {host}")
print(f" Hook 脚本: {os.path.basename(script_path)}")
print(f" 连接模式: 无 Root (Gadget)")
print(f" SDK 服务端: http://localhost:8899")
print("=" * 50)
script.unload()
session.detach()
PYEOF
else
warn "Frida 连接未成功,微信可能需要登录后再试"
fi
echo ""
log "部署完成!"

View File

@@ -0,0 +1,78 @@
#!/usr/bin/env python3
"""校验中台 Skill 注册表与设备端 Skill 类 public method 对齐"""
from __future__ import annotations
import inspect
import json
import sys
import time
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "app"))
sys.path.insert(0, str(ROOT / "agent"))
from services.brain_skill_registry import DEVICE_EXECUTABLE_SCRIPTS, SKILL_REGISTRY, flatten_actions # noqa: E402
SKIP_METHODS = frozenset({
"execute_compound_task", "send_message_with_vision", "send_message_with_search",
"get_current_screen_info",
})
def _public_methods(cls) -> set[str]:
return {
n for n, m in inspect.getmembers(cls, predicate=inspect.isfunction)
if not n.startswith("_") and n not in SKIP_METHODS
}
def main() -> int:
from skills import get_skill # noqa: WPS433
missing_on_device: list[str] = []
extra_on_device: list[str] = []
ok = 0
for script in sorted(DEVICE_EXECUTABLE_SCRIPTS):
registry_actions = set(flatten_actions(script))
try:
cls = get_skill(script)
except ImportError as e:
print(f"{script}: 无法加载 Skill 类: {e}")
return 1
device_actions = _public_methods(cls)
for action in sorted(registry_actions):
if action not in device_actions:
missing_on_device.append(f"{script}.{action}")
else:
ok += 1
for action in sorted(device_actions - registry_actions):
extra_on_device.append(f"{script}.{action}")
report = {
"timestamp": time.strftime("%Y-%m-%dT%H:%M:%S"),
"registry_actions_checked": ok,
"missing_on_device": missing_on_device,
"extra_on_device_not_in_registry": extra_on_device,
"complete": len(missing_on_device) == 0,
}
out = ROOT / "tmp" / f"skill_registry_audit_{int(time.time())}.json"
out.parent.mkdir(parents=True, exist_ok=True)
out.write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
print(f"registry 校验 action 数: {ok}")
print(f"设备缺失: {len(missing_on_device)}")
print(f"注册表未收录(设备有): {len(extra_on_device)}")
if missing_on_device:
print("缺失示例:", missing_on_device[:10])
if extra_on_device:
print("未收录示例:", extra_on_device[:10])
print("完整对齐:", "" if report["complete"] else "")
print("报告:", out)
return 0 if report["complete"] else 1
if __name__ == "__main__":
raise SystemExit(main())

54
sdk/scripts/start_device.sh Executable file
View File

@@ -0,0 +1,54 @@
#!/bin/bash
# 设备启动脚本
echo "=== 工作手机 - 设备启动工具 ==="
echo ""
# 检查ADB
if ! command -v adb &> /dev/null; then
echo "❌ ADB未找到请安装Android SDK Platform Tools"
exit 1
fi
# 检查设备
echo "检查已连接设备..."
DEVICES=$(adb devices | grep -v "List" | grep "device" | wc -l | tr -d ' ')
if [ "$DEVICES" -gt 0 ]; then
echo "✅ 发现 $DEVICES 个设备:"
adb devices
echo ""
echo "设备已就绪!"
exit 0
fi
echo "⚠️ 未发现设备"
echo ""
echo "请选择启动方式:"
echo "1. 打开Android Studio Device Manager"
echo "2. 检查USB连接"
echo "3. 查看设备启动指南"
echo ""
read -p "请输入选项 (1-3): " choice
case $choice in
1)
echo "正在打开Android Studio..."
open -a "Android Studio" 2>/dev/null || echo "请手动打开Android Studio → Device Manager"
;;
2)
echo "检查USB设备..."
system_profiler SPUSBDataType 2>/dev/null | grep -i "android\|phone" || echo "未检测到Android设备"
echo ""
echo "请确保:"
echo "1. USB线已连接"
echo "2. 手机已开启USB调试"
echo "3. 已授权此电脑"
;;
3)
open "sdk/docs/设备启动指南.md" 2>/dev/null || echo "请查看sdk/docs/设备启动指南.md"
;;
*)
echo "无效选项"
;;
esac

68
sdk/scripts/start_sdk.sh Executable file
View File

@@ -0,0 +1,68 @@
#!/bin/bash
#
# 工作手机 SDK 本地启动脚本
# 用法:在「工作手机」项目根目录执行
# cd sdk && ./scripts/start_sdk.sh
# 或从 sdk 目录:
# ./scripts/start_sdk.sh
#
set -e
SDK_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$SDK_ROOT"
echo "============================================"
echo " 工作手机 SDK 本地启动"
echo "============================================"
echo "SDK 目录: $SDK_ROOT"
echo ""
# 1. 检查 Python
if ! command -v python3 &> /dev/null; then
echo "❌ 未找到 python3"
exit 1
fi
# 2. 检查依赖
if ! python3 -c "import uvicorn" 2>/dev/null; then
echo "安装依赖: pip install -r requirements.txt"
pip install -r requirements.txt
fi
# 3. 启动 SDK 服务(工作目录须为 sdk/app
echo "[1/2] 启动 SDK 服务 (localhost:8899)..."
(cd "$SDK_ROOT/app" && python3 -m uvicorn main:app --host 0.0.0.0 --port 8899) &
SDK_PID=$!
sleep 3
# 4. 健康检查
if curl -s http://localhost:8899/health > /dev/null 2>&1; then
echo "✅ SDK 服务已启动"
curl -s http://localhost:8899/health | python3 -c "import json,sys; d=json.load(sys.stdin); print(f' devices_online={d.get(\"devices_online\",0)}, adb_devices={d.get(\"adb_devices\",0)}')" 2>/dev/null || true
else
echo "❌ SDK 启动失败"
kill $SDK_PID 2>/dev/null
exit 1
fi
echo ""
echo "[2/2] 启动 Agent需模拟器已运行..."
if adb devices 2>/dev/null | grep -q "emulator.*device"; then
(cd "$SDK_ROOT/agent" && python3 agent.py -d emulator-5554 -s ws://127.0.0.1:8899/ws/device --heartbeat 10) &
echo "✅ Agent 已启动(连接 emulator-5554"
else
echo "⚠️ 未检测到模拟器,跳过 Agent"
echo " 启动模拟器后手动执行:"
echo " cd $SDK_ROOT/agent && python3 agent.py -d emulator-5554 -s ws://127.0.0.1:8899/ws/device --heartbeat 10"
fi
echo ""
echo "============================================"
echo " 启动完成"
echo "============================================"
echo "SDK API: http://localhost:8899"
echo "API 文档: http://localhost:8899/docs"
echo "控制中心: http://localhost:8899/static/index.html"
echo ""
echo "验证: curl -s localhost:8899/health | jq ."
echo "============================================"

View File

@@ -0,0 +1,36 @@
#!/usr/bin/env bash
# 生成 Termux 一键启动命令(无线主控 · 不经 Mac ADB
# 用法: bash termux_agent_bootstrap.sh [device_id] [sdk_port]
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
DEVICE_ID="${1:-xgfe65eimrrofyws}"
SDK_PORT="${2:-8899}"
LAN_IP="$(ipconfig getifaddr en0 2>/dev/null || ipconfig getifaddr en1 2>/dev/null || echo "127.0.0.1")"
WS_URL="ws://${LAN_IP}:${SDK_PORT}/ws/device/${DEVICE_ID}"
# phantom Frida 端口(与 anti_detect/phantom_frida_config.json listen_port 一致)
FRIDA_PORT="17263"
if [[ -f "$ROOT/scripts/anti_detect/phantom_frida_config.json" ]]; then
FRIDA_PORT="$(python3 -c "import json; print(json.load(open('$ROOT/scripts/anti_detect/phantom_frida_config.json'))['listen_port'])" 2>/dev/null || echo 17263)"
fi
echo "================================================================"
echo " Termux Agent 启动命令(复制到红米 Termux"
echo " SDK: ${LAN_IP}:${SDK_PORT} device: ${DEVICE_ID}"
echo " Frida 本机端口: ${FRIDA_PORT}"
echo "================================================================"
echo ""
cat <<EOF
export WP_AGENT_ON_DEVICE=1
export WP_FRIDA_MODE=remote
export WP_FRIDA_PORT=${FRIDA_PORT}
export WP_AUTO_DISCOVER=1
export WP_SERVER_URL=${WS_URL}
cd ~/workphone/agent || cd ~/storage/downloads/workphone/agent
python agent.py -d ${DEVICE_ID} -s ${WS_URL} --heartbeat 10
# 或省略 -s依赖 UDP 8898 自动发现beacon 会广播 ${LAN_IP}
EOF
echo ""
echo "验收Mac:"
echo " curl http://${LAN_IP}:${SDK_PORT}/api/v3/connection/status | python3 -m json.tool"
echo " curl http://${LAN_IP}:${SDK_PORT}/api/v3/hook/probe/${DEVICE_ID} | python3 -m json.tool"

View File

@@ -0,0 +1,131 @@
#!/data/data/com.termux/files/usr/bin/bash
# ==============================================================================
# termux_frida_up_onphone.sh — 手机 Termux 内一键拉起 Phantom frida-server + 验证
#
# 场景:无线主控(无 USB/ADB。在【手机 Termux】里执行不在 Mac 跑。
# 作用:
# 1. 读 phantom_frida_config.json端口/二进制名,反检测随机化)
# 2. su 起 frida-server默认端口来自配置避开微信扫的 27042
# 3. 可选 setenforce 0SELinux 拦端口时)
# 4. 验证 frida-server 进程 + 微信 com.tencent.mm pid
# 5. 重启 Termux Agent 触发 attach
#
# 用法(手机 Termux
# bash ~/workphone/sdk/scripts/termux_frida_up_onphone.sh
# bash termux_frida_up_onphone.sh --port 33891 --no-agent
#
# 参考frida.re/docs/android · WeChat 8.0+ 扫 27042须换端口 + Zygisk DenyList
# ==============================================================================
set -uo pipefail
DEVICE_ID="${WP_DEVICE_ID:-xgfe65eimrrofyws}"
WECHAT_PKG="com.tencent.mm"
NO_AGENT=0
FORCE_PORT=""
SETENFORCE=0
while [[ $# -gt 0 ]]; do
case "$1" in
-d) DEVICE_ID="$2"; shift 2 ;;
--port) FORCE_PORT="$2"; shift 2 ;;
--no-agent) NO_AGENT=1; shift ;;
--setenforce) SETENFORCE=1; shift ;;
*) echo "[!] 未知参数: $1"; shift ;;
esac
done
# ── 定位 phantom 配置 ─────────────────────────────────────────────
CONFIG=""
for p in \
"$HOME/workphone/sdk/scripts/anti_detect/phantom_frida_config.json" \
"$HOME/workphone/phantom_frida_config.json" \
"/sdcard/workphone/phantom_frida_config.json" \
"$(dirname "$0")/anti_detect/phantom_frida_config.json"; do
[ -f "$p" ] && CONFIG="$p" && break
done
BIN_PATH="/data/local/tmp/frida-server"
PORT="27042"
if [ -n "$CONFIG" ]; then
echo "[*] 读取配置: $CONFIG"
BIN_PATH=$(python3 -c "import json;print(json.load(open('$CONFIG')).get('binary_path','/data/local/tmp/frida-server'))" 2>/dev/null || echo "/data/local/tmp/frida-server")
PORT=$(python3 -c "import json;print(json.load(open('$CONFIG')).get('listen_port',27042))" 2>/dev/null || echo 27042)
else
echo "[!] 未找到 phantom 配置,用默认 $BIN_PATH:$PORT"
fi
[ -n "$FORCE_PORT" ] && PORT="$FORCE_PORT"
echo "=== 手机端 Frida 起服 ==="
echo " 设备: $DEVICE_ID"
echo " 二进制: $BIN_PATH"
echo " 端口: $PORT"
# ── 检查 su ──────────────────────────────────────────────────────
if ! su -c 'id' >/dev/null 2>&1; then
echo "[✗] su 不可用,需 RootMagisk。无法启动 frida-server。"
exit 2
fi
# ── 检查二进制 ────────────────────────────────────────────────────
if ! su -c "[ -f $BIN_PATH ]" 2>/dev/null; then
echo "[✗] frida-server 二进制不存在: $BIN_PATH"
echo " 请先用 Mac+USB 跑: bash sdk/scripts/anti_detect/setup_phantom_frida.sh $DEVICE_ID"
exit 3
fi
# ── 可选关 SELinux ────────────────────────────────────────────────
if [ "$SETENFORCE" = "1" ]; then
echo "[*] setenforce 0临时重启恢复"
su -c 'setenforce 0' 2>/dev/null || true
fi
# ── 重启 frida-server ─────────────────────────────────────────────
echo "[*] 停止旧进程..."
su -c "pkill -f $(basename "$BIN_PATH")" 2>/dev/null || true
su -c 'pkill -f frida-server' 2>/dev/null || true
sleep 1
echo "[*] 启动 frida-server..."
su -c "chmod 755 $BIN_PATH; nohup $BIN_PATH -l 0.0.0.0:$PORT >/dev/null 2>&1 &" 2>/dev/null
sleep 2
RUNNING=$(su -c "ps -A 2>/dev/null | grep -c $(basename "$BIN_PATH")" 2>/dev/null || echo 0)
if [ "${RUNNING:-0}" -lt 1 ]; then
echo "[✗] frida-server 未起。排查ABI 不符 / SELinux加 --setenforce/ 端口占用。"
exit 4
fi
echo "[✓] frida-server 运行中(端口 $PORT"
# ── 检查微信进程 ──────────────────────────────────────────────────
WX_PID=$(su -c "pidof $WECHAT_PKG" 2>/dev/null | tr -d '\r')
if [ -z "$WX_PID" ]; then
echo "[!] 微信未运行,尝试拉起前台..."
su -c "am start -n $WECHAT_PKG/.ui.LauncherUI" 2>/dev/null || \
monkey -p $WECHAT_PKG -c android.intent.category.LAUNCHER 1 >/dev/null 2>&1 || true
sleep 4
WX_PID=$(su -c "pidof $WECHAT_PKG" 2>/dev/null | tr -d '\r')
fi
[ -n "$WX_PID" ] && echo "[✓] 微信 pid=$WX_PID" || echo "[!] 微信仍未检测到 pid请手动打开微信并进入一次聊天"
# ── 重启 Agent 触发 attach ────────────────────────────────────────
if [ "$NO_AGENT" = "0" ]; then
echo "[*] 重启 Termux Agent..."
pkill -f "agent.py -d $DEVICE_ID" 2>/dev/null || true
sleep 1
AGENT_DIR="$HOME/workphone/agent"
[ -d "$AGENT_DIR" ] || AGENT_DIR="$HOME/workphone/sdk/agent"
if [ -d "$AGENT_DIR" ]; then
cd "$AGENT_DIR"
export WP_AGENT_ON_DEVICE=1 WP_FRIDA_MODE=remote WP_FRIDA_PORT="$PORT" WP_AUTO_DISCOVER=1
nohup python agent.py -d "$DEVICE_ID" --heartbeat 10 >/tmp/wp_agent.log 2>&1 &
sleep 3
echo "[✓] Agent 已重启(日志 /tmp/wp_agent.log"
else
echo "[!] 未找到 agent 目录,请手动启动 Agent"
fi
fi
echo ""
echo "=== 完成 ==="
echo "Mac 侧验证: curl http://<SDK_IP>:8899/api/v3/hook/probe/$DEVICE_ID"
echo "期望 supports_hook=true 后回复「继续」自动跑三项真机验收。"

View File

@@ -0,0 +1,141 @@
#!/usr/bin/env bash
# 经 ADB 向 Termux 推送 config + 启动脚本,并在手机上拉起 Agent无线主控 · 不依赖 Mac 侧 agent.py
# 用法: DEVICE_SERIAL=xgfe65eimrrofyws bash sdk/scripts/termux_push_and_start_agent.sh
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SDK_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
LAN_ENV="${SDK_ROOT}/config/lan.env"
[[ -f "${LAN_ENV}" ]] && source "${LAN_ENV}" || true
export ANDROID_ADB_SERVER_PORT="${ANDROID_ADB_SERVER_PORT:-5037}"
ADB=(adb -P "${ANDROID_ADB_SERVER_PORT}")
DEVICE_SERIAL="${DEVICE_SERIAL:-xgfe65eimrrofyws}"
SDK_PORT="${WORKPHONE_SDK_PORT:-8899}"
DEVICE_ID="${DEVICE_ID:-${DEVICE_SERIAL}}"
FRIDA_PORT="$(python3 -c "import json; print(json.load(open('${SCRIPT_DIR}/anti_detect/phantom_frida_config.json'))['listen_port'])" 2>/dev/null || echo 38829)"
pick_mac_lan_ip() {
if [[ -n "${WORKPHONE_LAN_IP:-}" ]]; then
echo "${WORKPHONE_LAN_IP}"
return
fi
local ip
for ip in $(ifconfig 2>/dev/null | awk '/inet /{print $2}' | grep -v '^127\.'); do
[[ "$ip" == 198.18.* ]] && continue
[[ "$ip" == 192.168.* || "$ip" == 10.* ]] && { echo "$ip"; return; }
done
echo "127.0.0.1"
}
MAC_IP="$(pick_mac_lan_ip)"
TERMUX_HOME="/data/data/com.termux/files/home"
REMOTE_DIR="${TERMUX_HOME}/workphone"
REMOTE_AGENT="${REMOTE_DIR}/agent"
REMOTE_CFG="${REMOTE_AGENT}/config.json"
REMOTE_START="${REMOTE_DIR}/start_agent.sh"
REMOTE_WATCH="${REMOTE_DIR}/agent_watchdog.sh"
REMOTE_BOOT="${TERMUX_HOME}/.termux/boot/00-workphone-agent.sh"
LOG="${SDK_ROOT}/logs/termux_agent_push.log"
mkdir -p "${SDK_ROOT}/logs"
log() { echo "[$(date '+%F %T')] $*" | tee -a "$LOG"; }
if ! "${ADB[@]}" -s "${DEVICE_SERIAL}" get-state 2>/dev/null | grep -qx device; then
log "FAIL: adb device not ready serial=${DEVICE_SERIAL}"
exit 1
fi
# ① NAS→Docker→frp 有序 config与 BIND-03 一致,无 USB 后手机自主寻服)
CFG_TMP="$(mktemp)"
python3 "${SCRIPT_DIR}/build_agent_config.py" --device-id "${DEVICE_ID}" --for-phone --out "${CFG_TMP}"
PRIMARY_WS="$(python3 "${SCRIPT_DIR}/build_agent_config.py" --for-phone --print-primary 2>/dev/null | head -1)"
log "config primary=${PRIMARY_WS} (NAS优先非 Mac 单点)"
# ② 写 start_agent.sh
START_TMP="$(mktemp)"
cat > "${START_TMP}" <<BOOT
#!/data/data/com.termux/files/usr/bin/bash
set -uo pipefail
export WP_AGENT_ON_DEVICE=1
export WP_FRIDA_MODE=remote
export WP_AUTO_DISCOVER=1
export WP_FRIDA_PORT=${FRIDA_PORT:-38829}
cd "\${HOME}/workphone/agent" 2>/dev/null || cd "\${HOME}/storage/downloads/workphone/agent" 2>/dev/null || exit 1
pkill -f "python.*agent.py" 2>/dev/null || true
sleep 1
nohup python agent.py --config config.json >> "\${HOME}/workphone/agent.log" 2>&1 &
echo "agent_pid=\$!" >> "\${HOME}/workphone/agent.log"
BOOT
chmod +x "${START_TMP}"
WATCH_TMP="$(mktemp)"
cat > "${WATCH_TMP}" <<'WATCH'
#!/data/data/com.termux/files/usr/bin/bash
# 手机侧看门狗:无 USB 时 Agent 崩溃自动拉起30s
export WP_AGENT_ON_DEVICE=1
export WP_FRIDA_MODE=remote
AGENT_DIR="${HOME}/workphone/agent"
LOG="${HOME}/workphone/watchdog.log"
while true; do
if ! pgrep -f "python.*agent.py" >/dev/null 2>&1; then
echo "$(date '+%F %T') restart agent" >> "$LOG"
cd "$AGENT_DIR" 2>/dev/null && \
nohup python agent.py --config config.json >> "${HOME}/workphone/agent.log" 2>&1 &
fi
sleep 30
done
WATCH
chmod +x "${WATCH_TMP}"
BOOT_TMP="$(mktemp)"
cat > "${BOOT_TMP}" <<'TBOOT'
#!/data/data/com.termux/files/usr/bin/bash
# Termux:Boot — 开机自动启动 Agent + 看门狗(无线主控 · 无 Mac USB
termux-wake-lock 2>/dev/null || true
bash "${HOME}/workphone/start_agent.sh" >> "${HOME}/workphone/boot.log" 2>&1 &
pgrep -f agent_watchdog.sh >/dev/null || \
nohup bash "${HOME}/workphone/agent_watchdog.sh" >> "${HOME}/workphone/watchdog.log" 2>&1 &
TBOOT
chmod +x "${BOOT_TMP}"
log "push config → ${DEVICE_SERIAL} (${PRIMARY_WS}/${DEVICE_ID})"
"${ADB[@]}" -s "${DEVICE_SERIAL}" shell "mkdir -p ${REMOTE_DIR}/agent ${TERMUX_HOME}/.termux/boot" 2>/dev/null || true
"${ADB[@]}" -s "${DEVICE_SERIAL}" push "${CFG_TMP}" "${REMOTE_CFG}" >/dev/null 2>&1 || \
"${ADB[@]}" -s "${DEVICE_SERIAL}" push "${CFG_TMP}" "/sdcard/workphone_config.json" >/dev/null
"${ADB[@]}" -s "${DEVICE_SERIAL}" push "${START_TMP}" "${REMOTE_START}" >/dev/null 2>&1 || \
"${ADB[@]}" -s "${DEVICE_SERIAL}" push "${START_TMP}" "/sdcard/start_agent.sh" >/dev/null
"${ADB[@]}" -s "${DEVICE_SERIAL}" push "${WATCH_TMP}" "${REMOTE_WATCH}" >/dev/null 2>&1 || true
"${ADB[@]}" -s "${DEVICE_SERIAL}" push "${BOOT_TMP}" "${REMOTE_BOOT}" >/dev/null 2>&1 || true
rm -f "${CFG_TMP}" "${START_TMP}" "${WATCH_TMP}" "${BOOT_TMP}"
# ③ 尝试 Termux RUN_COMMAND 启动0.118+
log "termux RUN_COMMAND start_agent…"
"${ADB[@]}" -s "${DEVICE_SERIAL}" shell am broadcast \
-a com.termux.RUN_COMMAND \
-n com.termux/com.termux.app.RunCommandService \
--es com.termux.RUN_COMMAND_PATH "${TERMUX_HOME}/usr/bin/bash" \
--esa com.termux.RUN_COMMAND_ARGUMENTS "-lc,${REMOTE_START}" \
--ez com.termux.RUN_COMMAND_BACKGROUND true 2>/dev/null || true
# ④ 兼容旧版 RUN_SCRIPT
"${ADB[@]}" -s "${DEVICE_SERIAL}" shell am broadcast \
-a com.termux.RUN_SCRIPT \
--es com.termux.RUN_SCRIPT_PATH "${REMOTE_START}" 2>/dev/null || true
# ⑤ 启动手机看门狗(无 USB 时 Agent 崩溃自动拉起)
"${ADB[@]}" -s "${DEVICE_SERIAL}" shell "nohup bash ${REMOTE_WATCH} >> ${TERMUX_HOME}/workphone/watchdog.log 2>&1 &" 2>/dev/null || true
sleep 3
WS_COUNT=$(curl -s -m 5 "http://127.0.0.1:${SDK_PORT}/api/v3/connection/status" 2>/dev/null | \
python3 -c "import sys,json;d=json.load(sys.stdin).get('data',{});print(d.get('online_ws_count',0))" 2>/dev/null || echo 0)
log "done ws_online=${WS_COUNT}"
[[ "${WS_COUNT}" -ge 1 ]] && exit 0
# ⑤ 回退Mac 侧 oneclickUSB + adb reverse
log "termux start pending → fallback oneclick"
DEVICE_SERIAL="${DEVICE_SERIAL}" bash "${SCRIPT_DIR}/frida_workphone_oneclick.sh" -d "${DEVICE_SERIAL}" >>"$LOG" 2>&1 || true
WS_COUNT=$(curl -s -m 5 "http://127.0.0.1:${SDK_PORT}/api/v3/connection/status" 2>/dev/null | \
python3 -c "import sys,json;d=json.load(sys.stdin).get('data',{});print(d.get('online_ws_count',0))" 2>/dev/null || echo 0)
log "oneclick fallback ws_online=${WS_COUNT}"
[[ "${WS_COUNT}" -ge 1 ]]

84
sdk/scripts/test_app.sh Executable file
View File

@@ -0,0 +1,84 @@
#!/bin/bash
# 工作手机Agent测试脚本
echo "=== 工作手机Agent测试 ==="
echo ""
# 检查设备
echo "1. 检查设备连接..."
DEVICE=$(adb devices | grep "device$" | head -1 | awk '{print $1}')
if [ -z "$DEVICE" ]; then
echo "❌ 未找到设备,请先启动模拟器"
exit 1
fi
echo "✅ 设备已连接: $DEVICE"
echo ""
# 检查APP是否安装
echo "2. 检查APP安装..."
if adb shell pm list packages | grep -q "com.workphone.agent"; then
echo "✅ APP已安装"
else
echo "❌ APP未安装正在安装..."
cd "$(dirname "$0")/../android-app"
adb install -r app/build/outputs/apk/debug/app-debug.apk
if [ $? -eq 0 ]; then
echo "✅ 安装成功"
else
echo "❌ 安装失败"
exit 1
fi
fi
echo ""
# 启动APP
echo "3. 启动APP..."
adb shell am start -a android.intent.action.MAIN -c android.intent.category.LAUNCHER -n com.workphone.agent/.MainActivity
sleep 2
echo "✅ APP已启动"
echo ""
# 测试功能
echo "4. 功能测试..."
echo ""
echo "4.1 测试打开应用..."
adb shell input text "打开设置" && sleep 1 && adb shell input keyevent KEYCODE_ENTER
sleep 2
echo "✅ 测试完成"
echo ""
echo "4.2 测试返回..."
adb shell input keyevent KEYCODE_BACK
sleep 1
echo "✅ 测试完成"
echo ""
echo "4.3 测试截图..."
adb shell screencap -p /sdcard/test_screenshot.png
if [ $? -eq 0 ]; then
echo "✅ 截图成功: /sdcard/test_screenshot.png"
adb pull /sdcard/test_screenshot.png /tmp/test_screenshot.png 2>/dev/null
if [ -f /tmp/test_screenshot.png ]; then
echo "✅ 截图已保存到: /tmp/test_screenshot.png"
fi
else
echo "❌ 截图失败"
fi
echo ""
echo "4.4 检查日志..."
echo "查看最近日志:"
adb logcat -d | grep -i "workphone\|agent" | tail -10
echo ""
echo "=== 测试完成 ==="
echo ""
echo "📱 APP状态"
adb shell "dumpsys window windows | grep -E 'mCurrentFocus'"
echo ""
echo "💡 提示:"
echo " - 在模拟器中手动测试语音功能"
echo " - 检查设置中的服务器配置"
echo " - 查看日志: adb logcat | grep WorkPhoneAgent"

View File

@@ -0,0 +1,60 @@
#!/bin/bash
# 测试:打开豆包,搜索今天去哪
DEVICE=${1:-emulator-5554}
echo "=== 测试:打开豆包,搜索今天去哪 ==="
echo "设备: $DEVICE"
echo ""
# 1. 打开豆包
echo "1. 打开豆包..."
adb -s $DEVICE shell monkey -p com.bytedance.doubao -c android.intent.category.LAUNCHER 1 2>&1 | grep -v "bash arg" || {
echo "⚠️ 豆包未安装,尝试安装..."
echo "请先安装豆包应用"
exit 1
}
sleep 3
# 2. 等待应用启动
echo "2. 等待应用启动..."
sleep 2
# 3. 查找搜索框并点击
echo "3. 查找搜索框..."
adb -s $DEVICE shell uiautomator dump /sdcard/ui.xml 2>&1
# 查找搜索相关的元素
SEARCH_BOX=$(adb -s $DEVICE shell cat /sdcard/ui.xml | grep -iE "搜索|search|输入框|edit" | grep -oE 'bounds="\[[0-9,]+\]"' | head -1)
if [ -z "$SEARCH_BOX" ]; then
echo "未找到搜索框,尝试点击屏幕上方(常见搜索框位置)"
adb -s $DEVICE shell input tap 540 200
else
echo "找到搜索框: $SEARCH_BOX"
# 提取坐标并点击
COORDS=$(echo $SEARCH_BOX | grep -oE '\[[0-9,]+\]' | head -1 | tr -d '[]')
X=$(echo $COORDS | cut -d',' -f1)
Y=$(echo $COORDS | cut -d',' -f2)
adb -s $DEVICE shell input tap $X $Y
fi
sleep 1
# 4. 输入搜索关键词
echo "4. 输入搜索关键词:今天去哪"
adb -s $DEVICE shell input text "jintianquna" # 拼音输入
sleep 1
# 5. 点击搜索按钮或回车
echo "5. 执行搜索..."
adb -s $DEVICE shell input keyevent KEYCODE_ENTER
echo ""
echo "✅ 搜索命令已执行!"
echo ""
echo "注意:"
echo "- 如果豆包未安装,请先安装豆包应用"
echo "- 搜索框位置可能因应用版本而异"
echo "- 中文输入可能需要使用输入法"

View File

@@ -0,0 +1,38 @@
#!/usr/bin/env bash
# [兼容入口] 卸载旧奥创 APK — 请优先使用:
# bash sdk/scripts/clean_aochuang_device.sh --apply [-s SERIAL]
# Skill机擎/阿服/奥创真机清机/SKILL.md
# 本脚本仅卸两核心包,不含 xesd/Magisk/007 App。
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
if [[ "${1:-}" == "--full" ]]; then
shift
exec bash "$SCRIPT_DIR/clean_aochuang_device.sh" --apply "$@"
fi
OCREAT_PACKAGES=(
"org.xeslciw.manager" # XESlciw 框架管理
"top.zzz.vivwxjz" # 微信 Hook 模块
)
KEEP_PACKAGE="com.system.cloudservice" # 新工作程序,不卸载
ADB="${ADB:-adb}"
if [[ -n "$1" ]]; then
ADB="adb -s $1"
fi
echo "=== 卸载旧奥创 APK保留 $KEEP_PACKAGE==="
echo "设备: $($ADB devices -l | grep -v 'List' | grep device | head -1 || echo '未指定,使用默认')"
echo ""
for pkg in "${OCREAT_PACKAGES[@]}"; do
if $ADB shell pm list packages 2>/dev/null | grep -q "^package:$pkg"; then
echo "卸载: $pkg"
$ADB uninstall "$pkg" || echo " (卸载失败或需手动确认)"
else
echo "跳过(未安装): $pkg"
fi
done
echo ""
echo "完成。新工作程序 $KEEP_PACKAGE 未动。"

View File

@@ -0,0 +1,59 @@
#!/usr/bin/env bash
# 红米工作机:等 ADB → 升级微信(arm64) → oneclick 恢复 Frida
# 用法: bash upgrade_wechat_and_oneclick.sh [serial] [apk_path]
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SERIAL="${1:-xgfe65eimrrofyws}"
WIFI="192.168.110.80:5555"
APK_DIR="/tmp/wechat_apk"
LOG="${LOG:-/tmp/wp_reconnect_upgrade.log}"
PREFERRED_APK="${2:-${APK_DIR}/weixin8069_arm64.apk}"
FALLBACK_APK="${APK_DIR}/weixin8056_arm64.apk"
MAX_ROUNDS="${MAX_ROUNDS:-120}"
mkdir -p "$APK_DIR"
echo "=== $(date) upgrade_wechat_and_oneclick serial=$SERIAL ===" | tee "$LOG"
pick_apk() {
if [[ -f "$PREFERRED_APK" ]] && [[ $(stat -f%z "$PREFERRED_APK" 2>/dev/null || stat -c%s "$PREFERRED_APK") -gt 50000000 ]]; then
echo "$PREFERRED_APK"; return
fi
if [[ -f "$FALLBACK_APK" ]]; then echo "$FALLBACK_APK"; return; fi
echo ""
}
resolve_dev() {
adb connect "$WIFI" >/dev/null 2>&1 || true
if adb devices 2>/dev/null | grep -q "${SERIAL}.*device"; then echo "$SERIAL"; return; fi
if adb devices 2>/dev/null | grep -q "${WIFI}.*device"; then echo "$WIFI"; return; fi
echo ""
}
for round in $(seq 1 "$MAX_ROUNDS"); do
DEV="$(resolve_dev)"
if [[ -n "$DEV" ]]; then
echo "$(date) FOUND $DEV" | tee -a "$LOG"
ABI=$(adb -s "$DEV" shell getprop ro.product.cpu.abi 2>/dev/null | tr -d '\r')
OLD=$(adb -s "$DEV" shell dumpsys package com.tencent.mm 2>/dev/null | grep versionName | head -1 | tr -d '\r' || true)
echo "ABI=$ABI OLD=$OLD" | tee -a "$LOG"
APK="$(pick_apk)"
if [[ -z "$APK" ]]; then
echo "无可用 APK请先下载到 $APK_DIR" | tee -a "$LOG"
exit 2
fi
echo "install $APK -> $DEV" | tee -a "$LOG"
adb -s "$DEV" install -r -d -t "$APK" 2>&1 | tee -a "$LOG" || true
NEW=$(adb -s "$DEV" shell dumpsys package com.tencent.mm 2>/dev/null | grep versionName | head -1 | tr -d '\r' || true)
echo "NEW=$NEW" | tee -a "$LOG"
TARGET="$SERIAL"
[[ "$DEV" == "$WIFI" ]] && TARGET="$DEV"
bash "${SCRIPT_DIR}/frida_workphone_oneclick.sh" -d "$TARGET" 2>&1 | tee -a "$LOG" || true
curl -s --max-time 20 "http://127.0.0.1:8899/api/v3/hook/probe/${SERIAL}" | tee -a "$LOG"
echo "=== DONE $(date) ===" | tee -a "$LOG"
exit 0
fi
echo "$(date) wait $round/$MAX_ROUNDS" >> "$LOG"
sleep 5
done
echo "TIMEOUT: 未检测到 ADB请插 Type-C 并授权 USB 调试" | tee -a "$LOG"
exit 1

View File

@@ -0,0 +1,16 @@
#!/usr/bin/env bash
# USB 调试:把手机里访问 127.0.0.1:8899 转发到「插着数据线的这台电脑」上的 8899SDK 监听端口)。
# 每次重插 USB 或 adb 重连后需重跑;可与 real_device_connect_check.sh 配合。
set -euo pipefail
PORT="${WORKPHONE_WS_PORT:-8899}"
echo "== adb reverse tcp:${PORT} tcp:${PORT} =="
adb reverse "tcp:${PORT}" "tcp:${PORT}"
echo ""
echo "== 当前 reverse 列表 =="
adb reverse --list || true
echo ""
echo "手机端 agent_config 中 server_url 建议使用 ws://127.0.0.1:${PORT}/ws/device 或带 device_id 的完整地址。"

View File

@@ -0,0 +1,110 @@
#!/usr/bin/env bash
# 奥创清机验收门禁 — exit 0 = 无奥创残留exit 1 = 仍有残留
# 真源:机擎/阿服/奥创真机清机/SKILL.md
set -euo pipefail
SERIAL=""
DEFAULT_SERIAL="xgfe65eimrrofyws"
EVIDENCE_DIR=""
FAIL=0
AOCHUANG_PACKAGES=(
"org.xeslciw.manager"
"top.zzz.vivwxjz"
"uni.UNI9421F6C"
)
while [[ $# -gt 0 ]]; do
case "$1" in
-s) SERIAL="${2:-}"; shift ;;
--evidence) EVIDENCE_DIR="${2:-}"; shift ;;
-h|--help)
echo "用法: bash sdk/scripts/verify_aochuang_clean.sh [-s SERIAL] [--evidence DIR]"
exit 0
;;
*) echo "未知参数: $1"; exit 1 ;;
esac
shift
done
if [[ -z "$SERIAL" ]]; then
SERIAL="$(adb devices 2>/dev/null | awk 'NR>1 && $2=="device" {print $1; exit}')"
[[ -z "$SERIAL" ]] && SERIAL="$DEFAULT_SERIAL"
fi
adb_sh() { adb -s "$SERIAL" shell "$@"; }
check() {
local name="$1"
local ok="$2"
if [[ "$ok" == "1" ]]; then
echo "OK $name"
else
echo "FAIL $name"
FAIL=1
fi
}
echo "== 奥创清机验收 serial=$SERIAL =="
if ! adb devices 2>/dev/null | awk -v s="$SERIAL" '$1==s && $2=="device" {found=1} END{exit !found}'; then
echo "FAIL: 设备未在线"
exit 1
fi
# 1. 核心包
for pkg in "${AOCHUANG_PACKAGES[@]}"; do
if adb_sh pm path "$pkg" 2>/dev/null | grep -q .; then
check "包未卸: $pkg" 0
else
check "包已卸: $pkg" 1
fi
done
# 2. xesd 框架目录
if adb_sh "test -d /data/adb/xesd && echo YES || echo NO" 2>/dev/null | grep -q YES; then
check "/data/adb/xesd 仍存在" 0
else
check "/data/adb/xesd 不存在" 1
fi
# 3. 机擎 Agent 必须保留
if adb_sh pm path com.system.cloudservice 2>/dev/null | grep -q .; then
check "机擎 Agent 保留: com.system.cloudservice" 1
else
check "机擎 Agent 缺失: com.system.cloudservice" 0
fi
# 4. 可疑 Magisk 模块名
bad_mods=""
bad_mods="$(adb_sh 'su -c "ls /data/adb/modules 2>/dev/null"' 2>/dev/null | grep -iE 'xes|grvjfe|vivwx' || true)"
if [[ -n "$bad_mods" ]]; then
check "Magisk奥创模块: $bad_mods" 0
else
check "无 Magisk 奥创模块名" 1
fi
# 5. pm 模糊扫描
leaks="$(adb_sh pm list packages 2>/dev/null | grep -iE 'xes|vivwx|UNI9421F6C' || true)"
if [[ -n "$leaks" ]]; then
check "pm 模糊残留: $leaks" 0
else
check "pm 无奥创模糊匹配" 1
fi
if [[ -n "$EVIDENCE_DIR" ]]; then
mkdir -p "$EVIDENCE_DIR"
{
echo "serial=$SERIAL fail=$FAIL"
adb_sh pm list packages 2>/dev/null | grep -iE 'xes|zzz|vivwx|UNI9421|cloudservice' || true
adb_sh "ls -la /data/adb/xesd/ 2>/dev/null || echo NO_XESD"
} >"$EVIDENCE_DIR/verify_report.txt"
fi
if [[ "$FAIL" == "0" ]]; then
echo "PASS: 奥创清机验收全绿"
exit 0
fi
echo "FAIL: 仍有奥创残留,请 bash sdk/scripts/clean_aochuang_device.sh --apply -s $SERIAL"
exit 1

View File

@@ -0,0 +1,26 @@
#!/usr/bin/env bash
# 验收家里 NAS + kr HTTPS 生产入口2026-05-24
set -euo pipefail
RESOLVE=(--resolve workphone.quwanzhi.com:443:43.139.27.93)
BASE="https://workphone.quwanzhi.com"
check() {
local path="$1" expect="$2"
local code body
code=$(curl -sk -m 15 "${RESOLVE[@]}" -o /tmp/wp_verify_body -w '%{http_code}' "${BASE}${path}")
body=$(head -c 200 /tmp/wp_verify_body)
if [[ "$code" == "$expect" ]] || [[ "$body" == *"$expect"* ]]; then
echo "OK ${path}${code} ${body:0:80}"
else
echo "FAIL ${path}${code} ${body:0:120}"
exit 1
fi
}
echo ">>> 工作手机生产 URL 验收 ${BASE}"
check /health healthy
check /hub 200
check /docs 200
check /api/v3/gateway/info 工作手机SDK
echo ">>> 全部通过"

30
sdk/scripts/wait_adb_device.sh Executable file
View File

@@ -0,0 +1,30 @@
#!/usr/bin/env bash
# 轮询等待 ADB 设备上线USB 或无线),供 oneclick / 验收前使用
set -euo pipefail
SERIAL="${1:-192.168.110.80:5555}"
MAX_SEC="${2:-120}"
INTERVAL="${3:-3}"
echo "等待 ADB 设备: ${SERIAL}(最长 ${MAX_SEC}s"
deadline=$((SECONDS + MAX_SEC))
while (( SECONDS < deadline )); do
if [[ "$SERIAL" == *:* ]]; then
adb connect "$SERIAL" >/dev/null 2>&1 || true
fi
state="$(adb -s "$SERIAL" get-state 2>/dev/null || true)"
if [[ "$state" == "device" ]]; then
echo "adb_ready $SERIAL"
adb -s "$SERIAL" shell echo ok >/dev/null
exit 0
fi
# 任意已授权 USB 设备
any="$(adb devices | awk '/\tdevice$/{print $1; exit}')"
if [[ -n "$any" ]]; then
echo "adb_ready $any"
exit 0
fi
sleep "$INTERVAL"
done
echo "adb_timeout $SERIAL" >&2
adb devices -l >&2 || true
exit 1

View File

@@ -0,0 +1,67 @@
#!/usr/bin/env bash
# 等待 ADB device → oneclick → 微信登录 E2E无人值守续跑
# 用法: bash sdk/scripts/wait_adb_then_login.sh
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SDK_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
SERIAL="${REDMI_SERIAL:-xgfe65eimrrofyws}"
WAIT_SEC="${WAIT_SEC:-600}"
API="${SDK_API:-http://127.0.0.1:8899}"
export ANDROID_ADB_SERVER_PORT="${ANDROID_ADB_SERVER_PORT:-5038}"
ADB=(adb -P "${ANDROID_ADB_SERVER_PORT}")
LOG="${SDK_ROOT}/logs/wait_adb_then_login.log"
mkdir -p "${SDK_ROOT}/logs"
log() { echo "[$(date '+%F %T')] $*" | tee -a "$LOG"; }
is_phone_serial() {
local s="$1"
local brand
brand=$("${ADB[@]}" -s "$s" shell getprop ro.product.brand 2>/dev/null | tr -d '\r')
[[ "$brand" != "haier" ]]
}
log "wait_adb_then_login start serial=${SERIAL} wait=${WAIT_SEC}s adb_port=${ANDROID_ADB_SERVER_PORT}"
DEAD=$((SECONDS + WAIT_SEC))
found=""
while (( SECONDS < DEAD )); do
if "${ADB[@]}" -s "$SERIAL" get-state 2>/dev/null | grep -qx device && is_phone_serial "$SERIAL"; then
found="$SERIAL"
break
fi
while IFS= read -r any; do
[[ -z "$any" ]] && continue
is_phone_serial "$any" || continue
found="$any"
SERIAL="$any"
break
done < <("${ADB[@]}" devices 2>/dev/null | awk '/\tdevice$/{print $1}')
[[ -n "$found" ]] && break
ANDROID_ADB_SERVER_PORT="${ANDROID_ADB_SERVER_PORT}" bash "${SCRIPT_DIR}/connection_keeper_daemon.sh" --once >>"$LOG" 2>&1 || true
sleep 10
done
if [[ -z "$found" ]]; then
log "timeout: no phone adb device after ${WAIT_SEC}s"
exit 2
fi
log "adb online serial=${found} → oneclick"
DEVICE_SERIAL="$found" ANDROID_ADB_SERVER_PORT="${ANDROID_ADB_SERVER_PORT}" \
bash "${SCRIPT_DIR}/frida_workphone_oneclick.sh" -d "$found" >>"$LOG" 2>&1 || true
ws="$(curl -s "${API}/api/v3/connection/status" | python3 -c "import sys,json;print(json.load(sys.stdin).get('data',{}).get('online_ws_count',0))" 2>/dev/null || echo 0)"
log "ws_online=${ws}"
if [[ "$ws" -lt 1 ]]; then
log "ws still 0, waiting 120s for Agent..."
bash "${SCRIPT_DIR}/wait_ws_agent.sh" -d xgfe65eimrrofyws -b "$API" -t 120 -i 5 >>"$LOG" 2>&1 || true
fi
log "wechat login test..."
REDMI_SERIAL="$found" bash "${SCRIPT_DIR}/wechat_login_typec_test.sh" 2>&1 | tee -a "$LOG"
log "done"

44
sdk/scripts/wait_ws_agent.sh Executable file
View File

@@ -0,0 +1,44 @@
#!/usr/bin/env bash
# 等待设备 WebSocket Agent 上线(无线主控,不依赖 ADB
# 用法:
# bash wait_ws_agent.sh [device_id] [sdk_base] [max_wait_sec]
# bash wait_ws_agent.sh -d device_id -b http://127.0.0.1:8899 -t 3600
set -euo pipefail
DEVICE_ID="xgfe65eimrrofyws"
SDK_BASE="http://127.0.0.1:8899"
MAX_WAIT="600"
INTERVAL="5"
while [[ $# -gt 0 ]]; do
case "$1" in
-d) DEVICE_ID="${2:?}"; shift 2 ;;
-b|--base) SDK_BASE="${2:?}"; shift 2 ;;
-t) MAX_WAIT="${2:?}"; shift 2 ;;
-i) INTERVAL="${2:?}"; shift 2 ;;
-*) echo "未知参数: $1" >&2; exit 2 ;;
*) DEVICE_ID="$1"; shift; [[ $# -gt 0 ]] && SDK_BASE="$1" && shift; [[ $# -gt 0 ]] && MAX_WAIT="$1" && shift; break ;;
esac
done
echo "等待 WS Agent: ${DEVICE_ID} @ ${SDK_BASE}(最长 ${MAX_WAIT}s"
deadline=$((SECONDS + MAX_WAIT))
while (( SECONDS < deadline )); do
json=$(curl -s --max-time 8 "${SDK_BASE}/api/v3/connection/status" || echo "")
online=$(echo "$json" | python3 -c "
import sys, json
try:
d = json.load(sys.stdin)['data']
ids = d.get('online_device_ids') or []
print('1' if '${DEVICE_ID}' in ids else '0')
except Exception:
print('0')
" 2>/dev/null || echo "0")
if [[ "$online" == "1" ]]; then
echo "✅ WS Agent 在线: ${DEVICE_ID}"
exit 0
fi
echo "$(date '+%H:%M:%S') 尚未连接,${INTERVAL}s 后重试"
sleep "$INTERVAL"
done
echo "❌ 超时:${DEVICE_ID} WS Agent 未上线"
exit 1

View File

@@ -0,0 +1,25 @@
#!/usr/bin/env bash
# 等设备 WS 在线后自动跑卡若 gateway ai/chat 验收
set -euo pipefail
DEVICE="${DEVICE:-xgfe65eimrrofyws}"
API="${SDK_API:-http://127.0.0.1:8899}"
WAIT="${WAIT_SEC:-600}"
echo "等待设备 ${DEVICE} 上线(最长 ${WAIT}s..."
dead=$((SECONDS + WAIT))
while (( SECONDS < dead )); do
if curl -sf "${API}/api/v3/devices" | python3 -c "
import sys,json
d=json.load(sys.stdin)
arr=d if isinstance(d,list) else d.get('data',[])
print('yes' if any(x.get('device_id')=='${DEVICE}' for x in arr) else 'no')
" | grep -q yes; then
echo "✅ 设备在线: ${DEVICE}"
break
fi
sleep 5
done
curl -s -X POST "${API}/api/v3/devices/${DEVICE}/ai/chat" \
-H "Content-Type: application/json" \
-d '{"instruction":"获取微信版本号","timeout":120}' | python3 -m json.tool

View File

@@ -0,0 +1,181 @@
#!/usr/bin/env python3
"""
微信 Hook 接口完整性审计 — 禁止空接口/占位冒充
检查项:
1. WECHAT_ACTIONS ↔ ACTION_TO_RPC ↔ wechat_hook_v2.js rpc.exports 三方对齐
2. ACTION_ALIASES173 catalog→ canonical 均在 WECHAT_ACTIONS
3. (可选)真机 hook/execute 探针 / 全量 128 探针
用法:
python3 sdk/scripts/wechat_interface_audit.py
python3 sdk/scripts/wechat_interface_audit.py --probe -d xgfe65eimrrofyws
python3 sdk/scripts/wechat_interface_audit.py --probe-full -d xgfe65eimrrofyws
"""
from __future__ import annotations
import argparse
import json
import re
import sys
from datetime import datetime
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
JS = ROOT / "sdk/agent/hook/wechat_hook_v2.js"
HE = ROOT / "sdk/agent/hook/hook_executor.py"
SKILL = ROOT / "sdk/app/skills/wechat/skill_v2.py"
OUT = ROOT / "sdk/tmp"
def _load_rpc_keys() -> set[str]:
text = JS.read_text(encoding="utf-8", errors="ignore")
return set(re.findall(r"^\s+(\w+)\s*:\s*function", text, re.M))
def _load_action_rpc() -> dict[str, str]:
text = HE.read_text(encoding="utf-8", errors="ignore")
block = text.split("ACTION_TO_RPC:", 1)[1].split("ACTION_ALIASES:", 1)[0]
return dict(re.findall(r'"([a-z_]+)"\s*:\s*"(\w+)"', block))
def _load_action_aliases() -> dict[str, str]:
text = HE.read_text(encoding="utf-8", errors="ignore")
block = text.split("ACTION_ALIASES:", 1)[1].split("\n\n", 1)[0]
return dict(re.findall(r'"([a-z_]+)"\s*:\s*"([a-z_]+)"', block))
def _load_wechat_actions() -> dict[str, str]:
text = SKILL.read_text(encoding="utf-8", errors="ignore")
block = text.split("WECHAT_ACTIONS = {", 1)[1].split("\n}\n", 1)[0]
return dict(re.findall(r'"([a-z_]+)"\s*:\s*"(\w+)"', block))
def audit() -> dict:
rpc = _load_rpc_keys()
action_rpc = _load_action_rpc()
aliases = _load_action_aliases()
skill_actions = _load_wechat_actions()
catalog = sorted(set(action_rpc) | set(aliases))
missing_rpc = sorted(set(action_rpc.values()) - rpc)
skill_not_in_executor = sorted(set(skill_actions) - set(action_rpc))
# diag_* 为 Hook 内部联调诊断项JS 有真实现),不进对外 130 action 清单,豁免对齐
executor_not_in_skill = sorted(
a for a in set(action_rpc) - set(skill_actions)
if not a.startswith("diag_")
)
skill_rpc_mismatch = [
a for a, r in skill_actions.items()
if a in action_rpc and action_rpc[a] != r
]
alias_unresolved = sorted(
a for a, target in aliases.items()
if target not in skill_actions
)
ok = not (
missing_rpc
or skill_not_in_executor
or skill_rpc_mismatch
or executor_not_in_skill
or alias_unresolved
)
return {
"timestamp": datetime.now().isoformat(),
"rpc_exports": len(rpc),
"action_to_rpc": len(action_rpc),
"wechat_actions": len(skill_actions),
"action_aliases": len(aliases),
"catalog_actions": len(catalog),
"missing_rpc_in_js": missing_rpc,
"skill_not_in_executor": skill_not_in_executor,
"executor_not_in_skill": executor_not_in_skill,
"skill_rpc_mismatch": skill_rpc_mismatch,
"alias_unresolved": alias_unresolved,
"complete": ok,
}
def _probe_actions(device_id: str, base: str, actions: list[str], params_map: dict) -> list[dict]:
import httpx
rows = []
with httpx.Client(timeout=60) as c:
for action in actions:
params = dict(params_map.get(action, {}))
r = c.post(
f"{base}/api/v3/hook/execute",
json={
"device_id": device_id,
"platform": "wechat",
"action": action,
"params": params,
"hook_only": True,
},
)
data = r.json()
ch = data.get("channel_used") or data.get("_channel_used") or data.get("channel") or ""
inner = data.get("data") if isinstance(data.get("data"), dict) else {}
ok = bool(inner.get("success") or data.get("success")) and "offline" not in str(ch).lower()
if inner.get("success") is False:
ok = False
rows.append({"action": action, "ok": ok, "channel": ch, "error": inner.get("error") or inner.get("message")})
return rows
def probe(device_id: str, base: str, limit: int = 20) -> list[dict]:
action_rpc = _load_action_rpc()
safe = ["ping", "get_wechat_version", "get_hook_status", "get_contacts", "get_profile", "get_messages"]
actions = [a for a in safe if a in action_rpc][:limit]
params = {
"get_messages": {"conversation_id": "filehelper", "limit": 3},
"get_contacts": {"limit": 5},
}
return _probe_actions(device_id, base, actions, params)
def probe_full(device_id: str, base: str) -> list[dict]:
sys.path.insert(0, str(ROOT / "sdk" / "scripts"))
from matrix_hook_catalog_verify import SAFE_PARAMS # type: ignore
actions = sorted(_load_wechat_actions().keys())
return _probe_actions(device_id, base, actions, SAFE_PARAMS)
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--probe", action="store_true", help="6 项安全探针")
parser.add_argument("--probe-full", action="store_true", help="128 canonical 全量探针")
parser.add_argument("-d", "--device-id", default="xgfe65eimrrofyws")
parser.add_argument("--base", default="http://127.0.0.1:8899")
args = parser.parse_args()
report = audit()
OUT.mkdir(parents=True, exist_ok=True)
out = OUT / f"wechat_interface_audit_{int(datetime.now().timestamp())}.json"
if args.probe or args.probe_full:
rows = probe_full(args.device_id, args.base) if args.probe_full else probe(args.device_id, args.base)
report["probe"] = rows
report["probe_pass"] = sum(1 for x in rows if x["ok"])
report["probe_total"] = len(rows)
out.write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
print(f"rpc.exports: {report['rpc_exports']}")
print(f"ACTION_TO_RPC: {report['action_to_rpc']}")
print(f"WECHAT_ACTIONS: {report['wechat_actions']}")
print(f"Catalog(含alias): {report['catalog_actions']}")
print(f"缺失 RPC: {len(report['missing_rpc_in_js'])}")
print(f"Skill 未映射 executor: {len(report['skill_not_in_executor'])}")
print(f"Alias 未解析: {len(report.get('alias_unresolved', []))}")
print(f"完整对齐: {'' if report['complete'] else ''}")
print(f"报告: {out}")
if args.probe or args.probe_full:
print(f"真机探针: {report.get('probe_pass', 0)}/{report.get('probe_total', 0)}")
return 0 if report["complete"] else 1
if __name__ == "__main__":
sys.exit(main())

View File

@@ -0,0 +1,322 @@
#!/usr/bin/env python3
"""
微信「联系客服解封」常规批量执行器
适用:以后任意设备、任意微信号被限制,编辑配置文件即可一键全自动解封。
链路:调用 SDK `POST /api/v3/account/unblock-customer-service`
→ 设备端 unblock_via_customer_service skill
→ 自动跑「安全保护中 → 联系客服 → 终止使用说明 → 联系专属客服 → 允许 → 微信安全专属客服 → AI 持续对话」
用法:
python3 sdk/scripts/wechat_unblock_batch.py -c sdk/config/unblock_targets.yaml
python3 sdk/scripts/wechat_unblock_batch.py -c xxx.yaml --dry-run # 只校验不执行
python3 sdk/scripts/wechat_unblock_batch.py -c xxx.yaml --only 192.168.110.80:5555
输出:
sdk/tmp/unblock_batch_{ts}/
├── summary.md 汇总表格
├── {device}_{wxid}.json 每条任务原始返回(含 turns/screenshots
└── {device}_{wxid}.log 每条任务日志
"""
from __future__ import annotations
import argparse
import asyncio
import json
import logging
import os
import sys
import time
import urllib.parse
import urllib.request
from dataclasses import dataclass, field
from datetime import datetime
from pathlib import Path
from typing import Any, Dict, List, Optional
LOG = logging.getLogger("unblock_batch")
# ---------- 配置加载 ----------
def _load_yaml(path: str) -> dict:
try:
import yaml # type: ignore
with open(path, "r", encoding="utf-8") as f:
return yaml.safe_load(f) or {}
except ImportError:
# 退化:把简单 yaml 当 json-like 解析(仅支持本配置示例)
import re
content = open(path, "r", encoding="utf-8").read()
content = re.sub(r"#.*$", "", content, flags=re.MULTILINE)
# 极简退化:要求用户安装 PyYAML
raise RuntimeError("未安装 PyYAML请执行: pip install pyyaml") from None
@dataclass
class TargetTask:
device_id: str
wxid: str = ""
phone: str = ""
reason: str = ""
max_rounds: int = 18
chat_interval_sec: int = 8
use_web_search: bool = True
platform: str = "wechat"
enabled: bool = True
note: str = ""
# 运行时
started_at: float = 0
finished_at: float = 0
status: str = "pending" # pending / running / success / reject / timeout / fallback / error / skipped
final_message: str = ""
rounds: int = 0
raw_response: Optional[Dict[str, Any]] = field(default=None)
# ---------- 调用 SDK ----------
def _call_unblock(sdk_base: str, t: TargetTask, timeout: int) -> Dict[str, Any]:
qs = urllib.parse.urlencode({
"device_id": t.device_id,
"platform": t.platform,
"reason": t.reason,
"phone": t.phone,
"wxid": t.wxid,
"max_rounds": t.max_rounds,
"chat_interval_sec": t.chat_interval_sec,
"use_web_search": "true" if t.use_web_search else "false",
})
url = f"{sdk_base.rstrip('/')}/api/v3/account/unblock-customer-service?{qs}"
LOG.info(f"[{t.device_id}] POST {url}")
req = urllib.request.Request(url, method="POST", headers={"Content-Type": "application/json"})
with urllib.request.urlopen(req, timeout=timeout) as resp:
body = resp.read().decode("utf-8")
try:
return json.loads(body)
except Exception:
return {"code": 599, "raw_body": body[:2000]}
# ---------- 单条任务 ----------
def _run_one(sdk_base: str, t: TargetTask, out_dir: Path,
retry_on_fallback: int) -> TargetTask:
if not t.enabled:
t.status = "skipped"
LOG.info(f"[{t.device_id}] disabled -> skip")
return t
t.started_at = time.time()
t.status = "running"
log_path = out_dir / f"{_safe(t.device_id)}_{_safe(t.wxid or 'noid')}.log"
json_path = out_dir / f"{_safe(t.device_id)}_{_safe(t.wxid or 'noid')}.json"
file_handler = logging.FileHandler(log_path, encoding="utf-8")
file_handler.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(message)s"))
LOG.addHandler(file_handler)
attempts = 0
response = None
try:
attempts += 1
# 单条最长耗时max_rounds * (chat_interval + 5) + 90 兜底
timeout = t.max_rounds * (t.chat_interval_sec + 5) + 90
response = _call_unblock(sdk_base, t, timeout=timeout)
t.raw_response = response
data = response.get("data", {}) if isinstance(response, dict) else {}
session = data.get("session", {}) if isinstance(data, dict) else {}
status = data.get("status") or session.get("final_status") or "unknown"
msg = data.get("message") or data.get("error") or session.get("final_message") or ""
rounds = session.get("rounds", 0)
t.status = status
t.final_message = msg[:300]
t.rounds = int(rounds or 0)
# 链路失败时按需重试
while t.status == "fallback" and attempts <= retry_on_fallback:
LOG.warning(f"[{t.device_id}] fallback重试 {attempts}/{retry_on_fallback}")
time.sleep(8)
attempts += 1
response = _call_unblock(sdk_base, t, timeout=timeout)
t.raw_response = response
data = response.get("data", {}) if isinstance(response, dict) else {}
session = data.get("session", {}) if isinstance(data, dict) else {}
t.status = data.get("status") or session.get("final_status") or "unknown"
t.final_message = (data.get("message") or session.get("final_message") or "")[:300]
t.rounds = int(session.get("rounds", 0) or 0)
except Exception as exc:
t.status = "error"
t.final_message = f"{type(exc).__name__}: {exc}"[:300]
LOG.exception(f"[{t.device_id}] 异常")
response = {"error": str(exc)}
finally:
t.finished_at = time.time()
try:
json_path.write_text(
json.dumps(response or {}, ensure_ascii=False, indent=2),
encoding="utf-8",
)
except Exception:
pass
LOG.removeHandler(file_handler)
file_handler.close()
return t
def _safe(s: str) -> str:
return "".join(c if c.isalnum() else "_" for c in s)[:48]
# ---------- 主流程 ----------
def main() -> int:
parser = argparse.ArgumentParser(description="微信联系客服解封 · 批量执行器")
parser.add_argument("-c", "--config", required=True, help="yaml 配置文件路径")
parser.add_argument("--dry-run", action="store_true", help="只校验不执行")
parser.add_argument("--only", help="只跑某个 device_id覆盖配置")
parser.add_argument("--out", help="输出目录(默认 sdk/tmp/unblock_batch_{ts}")
parser.add_argument("--log-level", default="INFO")
args = parser.parse_args()
logging.basicConfig(
level=getattr(logging, args.log_level.upper(), logging.INFO),
format="%(asctime)s %(levelname)s %(message)s",
)
cfg = _load_yaml(args.config)
sdk_base = cfg.get("sdk_base") or os.environ.get("SDK_BASE_URL") or "http://127.0.0.1:8899"
platform = cfg.get("platform", "wechat")
default_max = int(cfg.get("default_max_rounds", 18))
default_interval = int(cfg.get("default_chat_interval_sec", 8))
default_web = bool(cfg.get("default_use_web_search", True))
concurrency = max(1, int(cfg.get("concurrency", 1)))
retry_on_fallback = int(cfg.get("retry_on_fallback", 1))
between_sleep = int(cfg.get("between_task_sleep_sec", 6))
raw_targets = cfg.get("targets") or []
tasks: List[TargetTask] = []
for raw in raw_targets:
if not isinstance(raw, dict):
continue
dev = raw.get("device_id")
if not dev:
continue
if args.only and dev != args.only:
continue
tasks.append(TargetTask(
device_id=dev,
wxid=raw.get("wxid", "") or "",
phone=raw.get("phone", "") or "",
reason=raw.get("reason", "") or "",
max_rounds=int(raw.get("max_rounds", default_max)),
chat_interval_sec=int(raw.get("chat_interval_sec", default_interval)),
use_web_search=bool(raw.get("use_web_search", default_web)),
platform=raw.get("platform", platform),
enabled=bool(raw.get("enabled", True)),
note=raw.get("note", "") or "",
))
if not tasks:
LOG.error("配置中未找到任何 target")
return 2
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
if args.out:
out_dir = Path(args.out)
else:
sdk_root = Path(__file__).resolve().parents[1]
out_dir = sdk_root / "tmp" / f"unblock_batch_{ts}"
out_dir.mkdir(parents=True, exist_ok=True)
LOG.info(f"配置: {args.config}")
LOG.info(f"SDK : {sdk_base}")
LOG.info(f"任务数: {len(tasks)} (concurrency={concurrency})")
LOG.info(f"输出: {out_dir}")
if args.dry_run:
LOG.info("--- DRY RUN ---")
for t in tasks:
LOG.info(f" {t.device_id} wxid={t.wxid} reason={t.reason[:30]} enabled={t.enabled}")
return 0
# 健康检查
try:
with urllib.request.urlopen(f"{sdk_base.rstrip('/')}/api/v3/connection/status", timeout=10) as resp:
conn = json.loads(resp.read().decode())
data = conn.get("data", {})
LOG.info(f"SDK 健康 ws_online={data.get('online_ws_count')} adb={data.get('adb_count')}")
except Exception as exc:
LOG.error(f"SDK 不可达: {exc}")
return 3
# 执行(串行 / 并发)
if concurrency <= 1:
for t in tasks:
_run_one(sdk_base, t, out_dir, retry_on_fallback)
if between_sleep > 0:
time.sleep(between_sleep)
else:
import concurrent.futures
with concurrent.futures.ThreadPoolExecutor(max_workers=concurrency) as ex:
futures = {ex.submit(_run_one, sdk_base, t, out_dir, retry_on_fallback): t for t in tasks}
for fut in concurrent.futures.as_completed(futures):
t = futures[fut]
try:
fut.result()
except Exception as exc:
LOG.exception(f"[{t.device_id}] 调度异常: {exc}")
# 汇总报告
_write_summary(tasks, out_dir, sdk_base, ts)
success = sum(1 for t in tasks if t.status == "success")
print(f"\n=== 完成 {success}/{len(tasks)} ===\n报告: {out_dir / 'summary.md'}")
return 0 if success == len(tasks) else 1
def _write_summary(tasks: List[TargetTask], out_dir: Path, sdk_base: str, ts: str) -> None:
lines = [
f"# 微信联系客服解封 · 批量执行报告 {ts}",
"",
f"- SDK: `{sdk_base}`",
f"- 任务数: {len(tasks)}",
f"- 成功: {sum(1 for t in tasks if t.status == 'success')}",
f"- 拒绝: {sum(1 for t in tasks if t.status == 'reject')}",
f"- 超时: {sum(1 for t in tasks if t.status == 'timeout')}",
f"- 链路失败: {sum(1 for t in tasks if t.status == 'fallback')}",
f"- 异常: {sum(1 for t in tasks if t.status == 'error')}",
f"- 跳过: {sum(1 for t in tasks if t.status == 'skipped')}",
"",
"| # | device | wxid | 状态 | 轮次 | 耗时(s) | 客服最终回复 |",
"|---|--------|------|------|------|---------|--------------|",
]
for i, t in enumerate(tasks, 1):
elapsed = round(t.finished_at - t.started_at, 1) if t.started_at else 0
msg = (t.final_message or "").replace("|", "/").replace("\n", " ")[:80]
lines.append(f"| {i} | `{t.device_id}` | `{t.wxid}` | **{t.status}** | {t.rounds} | {elapsed} | {msg} |")
lines += [
"",
"## 状态说明",
"- **success**:客服已解封 / 已恢复使用",
"- **reject**:客服明确拒绝解封",
"- **timeout**:达到最大轮次或客服长时间无回复",
"- **fallback**:未能进入客服小程序(路径变化)",
"- **error**:调用异常(网络 / 设备不在线等)",
"- **skipped**:配置 enabled=false",
"",
"## 详细 JSON",
"",
"每条任务详见同目录 `{device_id}_{wxid}.json`(含 turns / screenshots / web_snippets",
]
(out_dir / "summary.md").write_text("\n".join(lines), encoding="utf-8")
if __name__ == "__main__":
sys.exit(main())

View File

@@ -0,0 +1,133 @@
#!/usr/bin/env bash
# Type-C 红米 USB 一键解封:等 ADB → oneclick → 解封批量
# 用法:
# bash sdk/scripts/wechat_unblock_typec_oneshot.sh
# 环境:
# USB_VENDOR=0x2717 Xiaomi
# WAIT_SEC=900 等待 ADB 上线最长 900 秒
# UNBLOCK_REASON 传给批量脚本作为 reason
set -euo pipefail
SDK_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
WAIT_SEC="${WAIT_SEC:-900}"
REDMI_SERIAL="${REDMI_SERIAL:-xgfe65eimrrofyws}"
REASON="${UNBLOCK_REASON:-账号被误判限制登录,本人正常使用未做违规操作}"
LOG="${SDK_ROOT}/tmp/wechat_typec_oneshot_$(date +%Y%m%d_%H%M%S).log"
mkdir -p "${SDK_ROOT}/tmp"
exec > >(tee -a "$LOG") 2>&1
echo "=== 红米 Type-C USB 一键解封 ==="
echo "日志: $LOG"
echo ""
# 1. 提示
echo "【请在红米 Note 13 上操作(一次性)】"
echo " 1) 设置 → 我的设备 → 全部参数与信息 → 连点 7 次「OS 版本」/「MIUI 版本」"
echo " 2) 设置 → 更多设置 → 开发者选项 → 打开「USB 调试」+「USB 安装」"
echo " 3) 数据线拔插一次,弹「允许 USB 调试?」勾「始终允许」→ 允许"
echo ""
adb start-server >/dev/null 2>&1
# 仅认 Type-C USB 红米(排除 192.168.* 无线 ADB避免误连其他手机
pick_usb_redmi() {
local s
s="$(adb devices | awk -v t="${REDMI_SERIAL}" '$1==t && $2=="device"{print $1; exit}')"
if [[ -n "${s}" ]]; then
echo "${s}"
return 0
fi
adb devices | awk '/\tdevice$/{print $1}' | while read -r d; do
if [[ "${d}" != *:* ]]; then
echo "${d}"
break
fi
done
}
# 2. 轮询等待 ADB
echo "等待红米 USB ADB 上线serial=${REDMI_SERIAL},最长 ${WAIT_SEC}s..."
echo "(已忽略无线 ADB仅 Type-C USB"
DEAD=$((SECONDS + WAIT_SEC))
DEV=""
while (( SECONDS < DEAD )); do
DEV="$(pick_usb_redmi | head -1)"
if [[ -n "$DEV" ]]; then
echo ""
echo "✅ ADB 设备已上线: $DEV"
break
fi
if (( SECONDS % 10 == 0 )); then
# 每 10s 打一行状态
state=$(adb devices | tail -n +2 | head -1)
[[ -n "$state" ]] && echo " [$SECONDS s] $state"
fi
sleep 2
done
if [[ -z "$DEV" ]]; then
echo "❌ 等待超时,仍未检测到 ADB 设备。请确认手机端「USB 调试」已开启 + 已授权此 Mac。"
ioreg -p IOUSB -l 2>/dev/null | grep -A 3 "Xiaomi\|2312DRAABC" | head -10 || true
exit 2
fi
adb -s "$DEV" shell "getprop ro.product.model; getprop ro.build.version.release; pidof com.tencent.mm; dumpsys package com.tencent.mm | grep versionName | head -1" 2>&1 | head -8
echo ""
# 2.5 初始化 uiautomator2ATX + u2.jarAgent 解封依赖 u2
echo "=== 初始化 uiautomator2 ==="
if python3 -m uiautomator2 init --serial "$DEV" 2>&1 | tail -8; then
echo "✅ u2 init 完成"
else
echo "⚠️ u2 init 失败,继续尝试 oneclickAgent 可能无 u2"
fi
echo ""
# 3. oneclick 起 SDK + Frida + Agent
echo "=== 启动 oneclickWS + Frida==="
DEVICE_SERIAL="$DEV" bash "${SDK_ROOT}/scripts/frida_workphone_oneclick.sh" 2>&1 | tail -25
echo ""
# 4. 等 WS Agent 上线
echo "等 WS Agent 上线..."
for i in $(seq 1 30); do
ws=$(curl -s --max-time 3 http://127.0.0.1:8899/api/v3/connection/status 2>/dev/null \
| python3 -c "import sys,json;print(json.load(sys.stdin)['data']['online_ws_count'])" 2>/dev/null || echo 0)
if [[ "$ws" -ge 1 ]]; then
echo "✅ WS Agent online"
break
fi
sleep 2
done
# 5. 写入临时配置
CONF="${SDK_ROOT}/tmp/unblock_typec_$(date +%H%M%S).yaml"
cat > "$CONF" <<EOF
sdk_base: "http://127.0.0.1:8899"
default_max_rounds: 18
default_chat_interval_sec: 8
default_use_web_search: true
concurrency: 1
retry_on_fallback: 1
between_task_sleep_sec: 4
targets:
- device_id: "$DEV"
wxid: ""
phone: ""
reason: "$REASON"
enabled: true
note: "Type-C 红米 一键"
EOF
echo "临时配置: $CONF"
# 6. 跑批量
echo "=== 跑批量解封 ==="
python3 "${SDK_ROOT}/scripts/wechat_unblock_batch.py" -c "$CONF" 2>&1 | tail -30 || true
# 7. 报告
LATEST=$(ls -t "${SDK_ROOT}"/tmp/unblock_batch_*/summary.md 2>/dev/null | head -1)
echo ""
echo "=== 报告 ==="
[[ -f "$LATEST" ]] && cat "$LATEST"
echo ""
echo "全部日志:$LOG"

View File

@@ -0,0 +1,39 @@
#!/usr/bin/env bash
# 无线调试配对 + 连接Android 11+ 无线调试需先 pair
# 用法:
# bash sdk/scripts/wireless_adb_pair.sh 192.168.1.12:37123 123456
# PAIR_ADDR=192.168.1.12:37123 PAIR_CODE=123456 CONNECT_ADDR=192.168.1.12:5555 bash sdk/scripts/wireless_adb_pair.sh
set -euo pipefail
ADB_PORT="${ANDROID_ADB_SERVER_PORT:-5038}"
export ANDROID_ADB_SERVER_PORT="${ADB_PORT}"
ADB=(adb -P "${ADB_PORT}")
PAIR_ADDR="${1:-${PAIR_ADDR:-}}"
PAIR_CODE="${2:-${PAIR_CODE:-}}"
CONNECT_ADDR="${CONNECT_ADDR:-${PAIR_ADDR%:*}:5555}"
if [[ -z "$PAIR_ADDR" || -z "$PAIR_CODE" ]]; then
echo "用法: $0 <IP:配对端口> <6位配对码>"
echo "例: $0 192.168.1.12:37123 482910"
echo ""
echo "手机路径: 设置 → 开发者选项 → 无线调试 → 使用配对码配对设备"
exit 1
fi
echo "[pair] ${PAIR_ADDR} code=${PAIR_CODE}"
printf '%s\n' "$PAIR_CODE" | "${ADB[@]}" pair "$PAIR_ADDR"
sleep 2
echo "[connect] ${CONNECT_ADDR}"
"${ADB[@]}" connect "$CONNECT_ADDR"
sleep 2
"${ADB[@]}" devices -l
st=$("${ADB[@]}" -s "$CONNECT_ADDR" get-state 2>/dev/null || echo offline)
if [[ "$st" == "device" ]]; then
echo "✅ 无线 ADB 就绪: ${CONNECT_ADDR}"
brand=$("${ADB[@]}" -s "$CONNECT_ADDR" shell getprop ro.product.brand 2>/dev/null | tr -d '\r')
model=$("${ADB[@]}" -s "$CONNECT_ADDR" shell getprop ro.product.model 2>/dev/null | tr -d '\r')
echo " brand=${brand} model=${model}"
exit 0
fi
echo "❌ 状态: ${st}(若仍 offline 请重开无线调试或换 CONNECT_ADDR"
exit 1

View File

@@ -0,0 +1,34 @@
#!/usr/bin/env bash
# 无线主控一键 bootstrap安装 SDK 监视器 + 生成手机 config +(有 ADB 时OTA 推 Termux
# 用法: bash sdk/scripts/wireless_autoconnect_bootstrap.sh
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SDK_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
DEVICE_ID="${DEVICE_ID:-xgfe65eimrrofyws}"
EVID="${SDK_ROOT}/../开发文档/8、部署/05-测试验收/$(date +%Y%m%d)_无线主控bootstrap"
mkdir -p "$EVID"
echo "== 1/4 安装 Mac SDK 监视器(无 USB =="
bash "${SCRIPT_DIR}/install_sdk_watcher_launchagent.sh"
bash "${SCRIPT_DIR}/sdk_connection_watcher.sh" --once | tee -a "${EVID}/watcher_once.log"
echo "== 2/4 生成手机 Agent configNAS→Docker→frp =="
python3 "${SCRIPT_DIR}/build_agent_config.py" --device-id "${DEVICE_ID}" --for-phone --out "${EVID}/agent_config.json"
cp "${EVID}/agent_config.json" "${SDK_ROOT}/config/agent_config_${DEVICE_ID}.json"
echo "== 3/4 OTA 推手机(需 ADB 一次,之后可无 USB =="
serial=$(adb devices 2>/dev/null | awk '/\tdevice$/{print $1; exit}')
if [[ -n "$serial" ]]; then
DEVICE_SERIAL="$serial" DEVICE_ID="$DEVICE_ID" \
bash "${SCRIPT_DIR}/termux_push_and_start_agent.sh" | tee -a "${EVID}/termux_push.log"
else
echo "SKIP OTA: 无 ADB。请手机 Termux 手动复制 ${EVID}/agent_config.json → ~/workphone/agent/config.json 后运行 start_agent.sh"
fi
echo "== 4/4 验收 =="
source "${SDK_ROOT}/config/active_sdk.env" 2>/dev/null || true
echo "WORKPHONE_SDK_URL=${WORKPHONE_SDK_URL:-unset}"
curl -sf --max-time 5 "${WORKPHONE_SDK_URL:-http://127.0.0.1:8899}/api/v3/connection/status" | \
python3 -c "import sys,json;d=json.load(sys.stdin)['data'];print('ws',d.get('online_ws_count'),'ids',d.get('online_device_ids'))"
echo "证据目录: ${EVID}"

View File

@@ -0,0 +1,166 @@
#!/usr/bin/env bash
# ─────────────────────────────────────────────────────────────────────────────
# 工作手机 · 无 USB 无线主控一键脚本WiFi adb + 设备本机 stealth Frida + WS Agent
#
# 解决USB 线材/接口物理不稳反复掉线。改用 WiFi adb 传输u2/frida 全走无线;
# WS 身份仍用逻辑 device_idSDK/hub 设备身份不变)。
#
# 真源:开发文档/10、项目管理/工作日志.md 2026-05-31 15:58 · 真机铁律 §〇.八 无线主控
#
# 用法:
# bash wireless_master_oneclick.sh [USB_SERIAL] [SDK_HOST:PORT] [WIFI_PORT]
# - USB_SERIAL : 首次开 tcpip 用的 USB 序列(默认 xgfe65eimrrofyws
# - SDK_HOST : SDK 地址(默认 127.0.0.1:8899
# - WIFI_PORT : adb tcpip 端口(默认 5555
#
# 前置:设备已 Root、已装 fridaphantom_frida_config.json 指向 stealth 二进制),
# Mac 与设备同一 WiFi。USB 仅首次开 tcpip 时短暂连接,之后可拔线。
# ─────────────────────────────────────────────────────────────────────────────
set -uo pipefail
USB_SERIAL="${1:-xgfe65eimrrofyws}"
SDK_HOST="${2:-127.0.0.1:8899}"
WIFI_PORT="${3:-5555}"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
AGENT_DIR="$(cd "${SCRIPT_DIR}/../agent" && pwd)"
PHANTOM_CFG="${SCRIPT_DIR}/anti_detect/phantom_frida_config.json"
DEVICE_ID="${USB_SERIAL}" # 逻辑 device_idWS 身份),全程不变
export ANDROID_ADB_SERVER_PORT="${ANDROID_ADB_SERVER_PORT:-5038}"
adb() {
command adb -P "${ANDROID_ADB_SERVER_PORT}" "$@"
}
log(){ echo "[$(date +%H:%M:%S)] $*"; }
fatal(){ echo "[FATAL] $*" >&2; exit 1; }
# ── 1. 经 USB 开 tcpip 并取设备 WiFi IP ──────────────────────────────────────
log "① 经 USB(${USB_SERIAL}) 开启 adb tcpip ${WIFI_PORT} 并取 WiFi IP"
WIFI_IP="$(adb -s "${USB_SERIAL}" shell ip route 2>/dev/null | grep -oE 'src [0-9.]+' | awk '{print $2}' | head -1)"
if [[ -z "${WIFI_IP}" ]]; then
# 设备可能已是 WiFi adb尝试从已连接条目取
WIFI_IP="$(adb devices | grep -oE '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+:'"${WIFI_PORT}"'' | head -1 | cut -d: -f1)"
fi
[[ -n "${WIFI_IP}" ]] || fatal "无法获取设备 WiFi IP确认设备已连 WiFi 且 USB 可见,或已 adb connect"
log " 设备 WiFi IP=${WIFI_IP}"
adb -s "${USB_SERIAL}" tcpip "${WIFI_PORT}" >/dev/null 2>&1 || true
sleep 3
# ── 2. adb connect WiFi + 稳定性校验 ────────────────────────────────────────
WIFI_SERIAL="${WIFI_IP}:${WIFI_PORT}"
log "② adb connect ${WIFI_SERIAL}"
adb connect "${WIFI_SERIAL}" >/dev/null 2>&1
sleep 2
OK=0
for i in 1 2 3 4; do
[[ "$(adb -s "${WIFI_SERIAL}" shell echo ok 2>/dev/null | tr -d '\r')" == "ok" ]] && OK=$((OK+1))
sleep 1
done
log " WiFi adb 稳定 ${OK}/4"
[[ "${OK}" -ge 3 ]] || fatal "WiFi adb 不稳定(${OK}/4请检查同网/路由"
# ── 3. 启动设备本机 stealth frida-server按 phantom 配置端口/二进制)──────────
FRIDA_PORT="$(python3 -c "import json;print(json.load(open('${PHANTOM_CFG}'))['listen_port'])" 2>/dev/null || echo 27042)"
FRIDA_BIN="$(python3 -c "import json;print(json.load(open('${PHANTOM_CFG}'))['binary_path'])" 2>/dev/null || echo /data/local/tmp/frida-server-16)"
FRIDA_BASE="$(basename "${FRIDA_BIN}")"
log "③ 重启本机 stealth frida ${FRIDA_BIN} @ ${FRIDA_PORT}setsid 脱离会话)"
adb -s "${WIFI_SERIAL}" shell "su -c 'pkill -f ${FRIDA_BASE} 2>/dev/null; pkill -f frida-server 2>/dev/null; true'" >/dev/null 2>&1 || true
sleep 1
adb -s "${WIFI_SERIAL}" shell "su -c 'setsid ${FRIDA_BIN} -l 0.0.0.0:${FRIDA_PORT} </dev/null >/dev/null 2>&1 &'" >/dev/null 2>&1
sleep 4
adb -s "${WIFI_SERIAL}" shell "su -c 'ps -A | grep ${FRIDA_BASE}'" 2>/dev/null | head -1 \
|| fatal "frida 进程未确认"
adb -s "${WIFI_SERIAL}" forward "tcp:${FRIDA_PORT}" "tcp:${FRIDA_PORT}" >/dev/null
# ── 4. 确保微信运行(温和 am start不 frida-spawn─────────────────────────
if [[ -z "$(adb -s "${WIFI_SERIAL}" shell pidof com.tencent.mm 2>/dev/null | tr -d '\r')" ]]; then
log "④ 拉起微信am start禁 frida-spawn"
adb -s "${WIFI_SERIAL}" shell am start -n com.tencent.mm/.ui.LauncherUI >/dev/null 2>&1 || true
sleep 5
fi
# ── 5. 启动 AgentWP_DEVICE_SERIAL=WiFi 序列device_id=逻辑身份)───────────
log "⑤ 启动 Agentdevice_id=${DEVICE_ID}WS身份 / adb_serial=${WIFI_SERIAL}u2/frida传输"
pkill -f "agent.py -d ${DEVICE_ID}" 2>/dev/null || true
sleep 2
WIRELESS_LOG="${SCRIPT_DIR}/../logs/wireless_agent.log"
: > "${WIRELESS_LOG}"
AGENT_PID="$(
cd "${AGENT_DIR}"
export WP_DEVICE_SERIAL="${WIFI_SERIAL}"
export WP_FRIDA_MODE=remote
export WP_FRIDA_PORT="${FRIDA_PORT}"
export WORKPHONE_AGENT_LOG="${WIRELESS_LOG}"
export WORKPHONE_WS_URL="ws://${SDK_HOST}/ws/device"
export WORKPHONE_DEVICE_ID="${DEVICE_ID}"
python3 - <<'PY'
import os
import subprocess
import sys
cmd = [
sys.executable,
"agent.py",
"-d",
os.environ["WORKPHONE_DEVICE_ID"],
"-s",
os.environ["WORKPHONE_WS_URL"],
"--heartbeat",
"10",
]
with open(os.environ["WORKPHONE_AGENT_LOG"], "ab", buffering=0) as log:
proc = subprocess.Popen(
cmd,
cwd=os.getcwd(),
env=os.environ.copy(),
stdin=subprocess.DEVNULL,
stdout=log,
stderr=subprocess.STDOUT,
start_new_session=True,
close_fds=True,
)
print(proc.pid)
PY
)"
echo "${AGENT_PID}" > "${SCRIPT_DIR}/../logs/wireless_agent.pid"
log " Agent pid=${AGENT_PID}(日志 ${WIRELESS_LOG}"
for _i in $(seq 1 45); do
if ! kill -0 "${AGENT_PID}" 2>/dev/null; then
tail -80 "${WIRELESS_LOG}" >&2 || true
fatal "Agent 启动后退出"
fi
if grep -q "RPC ping: pong" "${WIRELESS_LOG}" 2>/dev/null && \
grep -q "服务器确认注册" "${WIRELESS_LOG}" 2>/dev/null; then
log " Agent WS + Frida RPC 已就绪"
break
fi
sleep 2
done
# ── 6. 验证无线 Hook 链路 ───────────────────────────────────────────────────
log "⑥ 验证无线 Hookget_profile"
RESP="$(curl -s -m 45 -X POST "http://${SDK_HOST}/api/v3/hook/execute" \
-H 'Content-Type: application/json' \
-d "{\"device_id\":\"${DEVICE_ID}\",\"platform\":\"wechat\",\"action\":\"get_profile\",\"params\":{},\"hook_only\":true}")"
echo "${RESP}" | head -c 300; echo
OK="$(echo "${RESP}" | python3 -c 'import json,sys
try:
d=json.load(sys.stdin)
print(bool((d.get("data") or {}).get("success") or d.get("success")))
except Exception:
print(False)' 2>/dev/null || echo False)"
CH="$(echo "${RESP}" | python3 -c 'import json,sys
try:
d=json.load(sys.stdin)
print(d.get("channel_used") or (d.get("data") or {}).get("channel") or "")
except Exception:
print("")' 2>/dev/null || true)"
if [[ "${OK}" == "True" ]] && echo "${CH}" | grep -Eqi 'frida|hook'; then
log "✅ 无 USB 无线主控就绪WiFi adb + 本机 stealth frida + WS + Hook"
log " 现在可拔掉 USB 线,控制/取材料经 WiFi 无线进行。"
else
tail -80 "${WIRELESS_LOG}" >&2 || true
fatal "Hook 未确认成功frida attach / 微信运行 / 端口转发)"
fi
log "USB 掉线无影响;如换网/IP 变化,重跑本脚本即可。"

303
sdk/scripts/ws_full_test.py Executable file
View File

@@ -0,0 +1,303 @@
#!/usr/bin/env python3
"""
工作手机 · WebSocket + 连接态全量自检
用法(项目根或 sdk 目录):
python3 scripts/ws_full_test.py
python3 scripts/ws_full_test.py -d xgfe65eimrrofyws --base http://127.0.0.1:8899
检查项health / connection/status / protocol / modes /
hook/probe / hook/execute(只读) / 指定设备是否在线 /
手机 Agent 包是否安装(需 ADB
"""
from __future__ import annotations
import argparse
import asyncio
import json
import subprocess
import sys
import time
from datetime import datetime
from pathlib import Path
from typing import Any, Dict, List, Optional
from urllib.error import URLError
from urllib.request import Request, urlopen
DEFAULT_DEVICE = "xgfe65eimrrofyws"
DEFAULT_BASE = "http://127.0.0.1:8899"
AGENT_PKG = "com.system.cloudservice"
WECHAT_PKG = "com.tencent.mm"
OUT_DIR = Path(__file__).resolve().parent.parent / "tmp"
def _get(url: str, timeout: float = 15) -> Dict[str, Any]:
req = Request(url, headers={"X-API-Key": "workphone-secret-key"})
with urlopen(req, timeout=timeout) as resp:
return json.loads(resp.read().decode())
def _post(url: str, body: dict, timeout: float = 90) -> Dict[str, Any]:
data = json.dumps(body).encode()
req = Request(
url,
data=data,
headers={"Content-Type": "application/json", "X-API-Key": "workphone-secret-key"},
method="POST",
)
with urlopen(req, timeout=timeout) as resp:
return json.loads(resp.read().decode())
def _adb(*args: str) -> str:
try:
r = subprocess.run(["adb"] + list(args), capture_output=True, text=True, timeout=12)
return (r.stdout or r.stderr or "").strip()
except Exception as e:
return f"adb error: {e}"
def _check_adb_install(serial: str) -> Dict[str, Any]:
out = {
"adb_serials": [],
"adb_serial_resolved": None,
"expected_connected": False,
"agent_pkg_installed": None,
"agent_pkg_enabled": None,
"wechat_installed": None,
"launcher_activity": None,
}
lines = _adb("devices").splitlines()[1:]
serials = [ln.split()[0] for ln in lines if ln.strip() and "device" in ln and "offline" not in ln]
out["adb_serials"] = serials
resolved = None
for s in serials:
if s == serial:
resolved = s
break
ro = _adb("-s", s, "shell", "getprop", "ro.serialno")
if ro.strip() == serial:
resolved = s
break
out["adb_serial_resolved"] = resolved
out["expected_connected"] = resolved is not None
if not out["expected_connected"]:
return out
prefix = ["-s", resolved]
pm = _adb(*prefix, "shell", "pm", "path", AGENT_PKG)
out["agent_pkg_installed"] = AGENT_PKG in pm and "package:" in pm
dump = _adb(*prefix, "shell", "pm", "dump", AGENT_PKG)
out["agent_pkg_enabled"] = "enabled=1" in dump or "enabled=0" not in dump
wc = _adb(*prefix, "shell", "pm", "path", WECHAT_PKG)
out["wechat_installed"] = WECHAT_PKG in wc
launcher = _adb(
*prefix,
"shell",
"cmd",
"package",
"resolve-activity",
"--brief",
"-c",
"android.intent.category.LAUNCHER",
AGENT_PKG,
)
out["launcher_activity"] = launcher if launcher and "No activity" not in launcher else None
return out
async def _ws_protocol_smoke(base_ws: str, device_id: str) -> Dict[str, Any]:
"""用 websockets 客户端走 register → heartbeat 协议(不替代真机 Agent"""
try:
import websockets
except ImportError:
return {"ok": False, "error": "pip install websockets"}
url = f"{base_ws.rstrip('/')}/{device_id}_ws_smoke"
result: Dict[str, Any] = {"ok": False, "url": url, "steps": []}
try:
async with websockets.connect(url, open_timeout=8) as ws:
await ws.send(
json.dumps(
{
"type": "register",
"data": {
"project_id": "cunkebao",
"model": "WS-Smoke-Test",
"capabilities": ["event", "device_request"],
"source": "ws_full_test.py",
},
}
)
)
msg = json.loads(await asyncio.wait_for(ws.recv(), timeout=5))
result["steps"].append({"register_ack": msg.get("type")})
await ws.send(json.dumps({"type": "heartbeat", "device_id": f"{device_id}_ws_smoke"}))
msg2 = json.loads(await asyncio.wait_for(ws.recv(), timeout=5))
result["steps"].append({"heartbeat_ack": msg2.get("type")})
result["ok"] = msg.get("type") == "registered" and msg2.get("type") in ("pong", "heartbeat_ack", "registered")
except Exception as e:
result["error"] = str(e)[:300]
return result
def run_tests(base: str, device_id: str) -> Dict[str, Any]:
base = base.rstrip("/")
ws_base = base.replace("http://", "ws://").replace("https://", "wss://") + "/ws/device"
report: Dict[str, Any] = {
"at": datetime.now().isoformat(),
"base": base,
"expected_device_id": device_id,
"checks": [],
"summary": {"pass": 0, "fail": 0, "warn": 0},
}
def add(name: str, ok: bool, detail: Any = None, level: str = "pass"):
if not ok and level == "pass":
level = "fail"
report["checks"].append({"name": name, "ok": ok, "level": level, "detail": detail})
key = level if level in ("pass", "fail", "warn") else ("pass" if ok else "fail")
report["summary"][key] += 1
# 1 health
try:
h = _get(f"{base}/health")
add("SDK /health", h.get("status") == "healthy", h)
except URLError as e:
add("SDK /health", False, str(e))
# 2 connection status
conn = {}
try:
conn = _get(f"{base}/api/v3/connection/status").get("data", {})
ws_ids = conn.get("online_device_ids") or []
adb_serials = conn.get("adb_serials") or []
on_ws = device_id in ws_ids
on_adb = device_id in adb_serials or any(
_adb("-s", s, "shell", "getprop", "ro.serialno").strip() == device_id for s in adb_serials
)
add(
"指定设备 WS 在线",
on_ws,
{"online_ws_count": conn.get("online_ws_count"), "ids": ws_ids},
"warn" if not on_ws else "pass",
)
add(
"指定设备 ADB 在线",
on_adb,
{"adb_count": conn.get("adb_count"), "serials": adb_serials},
"warn" if not on_adb else "pass",
)
report["connection"] = conn
except URLError as e:
add("connection/status", False, str(e))
# 3 protocol doc
try:
p = _get(f"{base}/api/v3/connection/protocol")
add("connection/protocol", p.get("code") == 200, {"version": p.get("data", {}).get("version")})
except URLError as e:
add("connection/protocol", False, str(e))
# 4 connection modes
try:
m = _get(f"{base}/api/v3/connection/modes/{device_id}")
modes = m.get("data", {}).get("modes") or []
best = m.get("data", {}).get("best_mode")
add("connection/modes", bool(modes), {"best_mode": best, "count": len(modes)})
report["modes"] = m.get("data")
except URLError as e:
add("connection/modes", False, str(e))
# 5 WS 协议冒烟(服务端 WS 通路)
try:
ws_smoke = asyncio.run(_ws_protocol_smoke(ws_base, device_id))
add("WebSocket 协议冒烟 register+heartbeat", ws_smoke.get("ok"), ws_smoke)
except Exception as e:
add("WebSocket 协议冒烟", False, str(e))
# 6 hook probe
probe = {}
try:
probe = _get(f"{base}/api/v3/hook/probe/{device_id}", timeout=60)
ok = probe.get("supports_hook") is True or probe.get("hook_tests", {}).get("connect") == "ok"
add("hook/probe Frida", ok, probe, "warn" if not ok else "pass")
except URLError as e:
add("hook/probe", False, str(e), "warn")
# 7 hook execute 只读(设备+Frida 就绪时)
for action, params in [
("get_profile", {}),
("get_contacts", {"limit": 3}),
]:
try:
r = _post(
f"{base}/api/v3/hook/execute",
{
"device_id": device_id,
"platform": "wechat",
"action": action,
"params": params,
"hook_only": True,
},
timeout=120,
)
data = r.get("data") or {}
ok = data.get("success") is True or r.get("code") == 200 and data.get("success") is not False
ch = r.get("channel_used") or data.get("_channel_used")
add(f"hook/execute {action}", ok, {"channel": ch, "keys": list(data.keys())[:8]}, "warn" if not ok else "pass")
except URLError as e:
add(f"hook/execute {action}", False, str(e), "warn")
# 8 ADB 安装与桌面入口
adb_info = _check_adb_install(device_id)
report["adb"] = adb_info
if adb_info["expected_connected"]:
add("Agent APK 已安装", adb_info.get("agent_pkg_installed") is True, AGENT_PKG)
add(
"桌面 Launcher 入口",
bool(adb_info.get("launcher_activity")),
adb_info.get("launcher_activity") or "无 LAUNCHER需重新安装带桌面图标的 APK",
"warn" if not adb_info.get("launcher_activity") else "pass",
)
add("微信已安装", adb_info.get("wechat_installed") is True, WECHAT_PKG)
else:
add("ADB 指定设备已连接", False, adb_info["adb_serials"], "fail")
# 9 workbench
try:
wb = _get(f"{base}/api/v3/workbench/overview", timeout=20)
add("workbench/overview", wb.get("code") == 200, {"keys": list((wb.get("data") or {}).keys())[:6]})
except URLError as e:
add("workbench/overview", False, str(e), "warn")
return report
def main():
ap = argparse.ArgumentParser(description="工作手机 WS+连接全量自检")
ap.add_argument("-d", "--device", default=DEFAULT_DEVICE, help="指定设备序列号")
ap.add_argument("--base", default=DEFAULT_BASE, help="SDK 地址")
ap.add_argument("-o", "--output", default="", help="报告 JSON 路径")
args = ap.parse_args()
report = run_tests(args.base, args.device)
OUT_DIR.mkdir(parents=True, exist_ok=True)
out = Path(args.output) if args.output else OUT_DIR / f"ws_full_test_{args.device}_{int(time.time())}.json"
out.write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
s = report["summary"]
print("=" * 60)
print(f"工作手机全量自检 · 设备 {args.device}")
print(f"通过 {s['pass']} · 警告 {s['warn']} · 失败 {s['fail']}")
print("-" * 60)
for c in report["checks"]:
icon = "" if c["ok"] else ("⚠️" if c["level"] == "warn" else "")
print(f"{icon} {c['name']}")
print("-" * 60)
print(f"报告: {out}")
print("=" * 60)
sys.exit(0 if s["fail"] == 0 else 1)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,42 @@
#!/usr/bin/env bash
# 解锁 Bootloader 之后使用:将 Magisk 修补后的镜像刷入 boot 或 init_boot
# 用法:
# FASTBOOT_FLASH_CONFIRM=yes ./xiaomi_fastboot_flash_magisk.sh /path/to/magisk_patched.img boot
# FASTBOOT_FLASH_CONFIRM=yes ./xiaomi_fastboot_flash_magisk.sh /path/to/magisk_patched.img init_boot
set -euo pipefail
IMG="${1:-}"
PART="${2:-boot}"
usage() {
echo "用法: FASTBOOT_FLASH_CONFIRM=yes $0 <magisk修补镜像路径> [boot|init_boot]"
echo " 默认分区: boot"
echo " 刷入前请确认: BL 已解锁、镜像来自与本机 MIUI 版本一致的 boot/init_boot 修补结果。"
exit 1
}
[[ -z "$IMG" || ! -f "$IMG" ]] && usage
[[ "$PART" != "boot" && "$PART" != "init_boot" ]] && usage
if [[ "${FASTBOOT_FLASH_CONFIRM:-}" != "yes" ]]; then
echo "拒绝执行:为防止误刷,请设置环境变量 FASTBOOT_FLASH_CONFIRM=yes"
echo "示例: FASTBOOT_FLASH_CONFIRM=yes $0 \"$IMG\" $PART"
exit 2
fi
if ! command -v fastboot >/dev/null 2>&1; then
echo "未找到 fastboot请安装 Android platform-tools"
exit 3
fi
FB_OUT=$(fastboot devices 2>/dev/null | awk 'NF>=2 && $2=="fastboot"{print $1}' | head -1)
if [[ -z "$FB_OUT" ]]; then
echo "未检测到 fastboot 设备。请先: adb reboot bootloader"
exit 4
fi
echo ">>> 设备: $FB_OUT"
echo ">>> 将执行: fastboot flash $PART $IMG"
fastboot flash "$PART" "$IMG"
fastboot reboot
echo ">>> 已重启,开机后执行: adb shell su -c id"