feat: publish workphone SDK deployment and API docs
This commit is contained in:
136
sdk/scripts/_hook_smoke.py
Normal file
136
sdk/scripts/_hook_smoke.py
Normal 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 主控冒烟测试完成')
|
||||
60
sdk/scripts/adb_bootstrap_termux_agent.sh
Executable file
60
sdk/scripts/adb_bootstrap_termux_agent.sh
Executable 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
|
||||
82
sdk/scripts/adb_connection_stabilize.sh
Executable file
82
sdk/scripts/adb_connection_stabilize.sh
Executable 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"
|
||||
23
sdk/scripts/adb_host_server_listen_all.sh
Executable file
23
sdk/scripts/adb_host_server_listen_all.sh
Executable 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
|
||||
53
sdk/scripts/android_free_space.sh
Executable file
53
sdk/scripts/android_free_space.sh
Executable 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'
|
||||
|
||||
# 系统级裁剪各应用 cache(Android 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,或卸载不常用应用。"
|
||||
6
sdk/scripts/anti_detect/phantom_frida_config.json
Normal file
6
sdk/scripts/anti_detect/phantom_frida_config.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"device_serial": "xgfe65eimrrofyws",
|
||||
"listen_port": 14537,
|
||||
"binary_name": "fs_3f823a",
|
||||
"binary_path": "/data/local/tmp/fs_3f823a"
|
||||
}
|
||||
116
sdk/scripts/anti_detect/setup_phantom_frida.sh
Executable file
116
sdk/scripts/anti_detect/setup_phantom_frida.sh
Executable 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
|
||||
161
sdk/scripts/anti_detect/setup_root_hide.sh
Executable file
161
sdk/scripts/anti_detect/setup_root_hide.sh
Executable 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"
|
||||
151
sdk/scripts/aochuang_protocol_coverage.py
Normal file
151
sdk/scripts/aochuang_protocol_coverage.py
Normal file
@@ -0,0 +1,151 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
奥创 SDK 通信协议(客服通信接口总目录 · EnumMsgType 指令类 Task)↔ 工作手机 SDK action 覆盖矩阵。
|
||||
|
||||
目的:对照奥创 proto 的服务端→设备「指令类任务」(1070-1270),核对本 SDK 是否已具备
|
||||
可下发的等价 action(ACTION_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 期望 action(None = 缺口)
|
||||
# 依据 资料/工作手机客服通信接口总目录(带通信协议)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())
|
||||
43
sdk/scripts/auto_accept_when_ready.sh
Executable file
43
sdk/scripts/auto_accept_when_ready.sh
Executable 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),等手机重启Agent,retry ${INTERVAL}s" >> "$LOG"
|
||||
sleep "$INTERVAL"
|
||||
done
|
||||
|
||||
echo "[auto-accept] timeout, frida not ready $(date)" | tee -a "$LOG"
|
||||
exit 1
|
||||
31
sdk/scripts/auto_capture_when_frida_back.sh
Executable file
31
sdk/scripts/auto_capture_when_frida_back.sh
Executable 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
|
||||
98
sdk/scripts/auto_recover_adb_oneclick.sh
Executable file
98
sdk/scripts/auto_recover_adb_oneclick.sh
Executable 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
151
sdk/scripts/batch_deploy.sh
Executable 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
150
sdk/scripts/build_agent_config.py
Executable 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 为主连接写入 config,public_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="手机 OTA:NAS→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
84
sdk/scripts/build_release.sh
Executable 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
50
sdk/scripts/check_sdk.sh
Executable 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 "============================================"
|
||||
252
sdk/scripts/clean_aochuang_device.sh
Executable file
252
sdk/scripts/clean_aochuang_device.sh
Executable 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 reboot(Magisk 模块变更后)"
|
||||
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
|
||||
210
sdk/scripts/connection_keeper_daemon.sh
Executable file
210
sdk/scripts/connection_keeper_daemon.sh
Executable 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
|
||||
48
sdk/scripts/deploy_baota_wpsdk.sh
Executable file
48
sdk/scripts/deploy_baota_wpsdk.sh
Executable 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"
|
||||
129
sdk/scripts/deploy_home_station.sh
Executable file
129
sdk/scripts/deploy_home_station.sh
Executable file
@@ -0,0 +1,129 @@
|
||||
#!/usr/bin/env bash
|
||||
# 家里 Station NAS(192.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-key(Header: X-API-Key)
|
||||
#
|
||||
# 外网验收注意:
|
||||
# - kr 本机 curl 127.0.0.1 + Host 已 200;链路正常
|
||||
# - Mac 开 Clash 时域名可能解析到 fake-ip(198.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
90
sdk/scripts/deploy_nas_rsync.sh
Executable 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 NOPASSWD(Synology 默认 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 命令前需 sudo(Synology 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 防火墙是否放行 8899;DSM「登录门户」勿占用同端口。"
|
||||
96
sdk/scripts/docker_engine_guard.sh
Executable file
96
sdk/scripts/docker_engine_guard.sh
Executable 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
57
sdk/scripts/ensure_ws_agent.sh
Executable 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'))
|
||||
"
|
||||
37
sdk/scripts/ensure_ws_agent_wireless.sh
Executable file
37
sdk/scripts/ensure_ws_agent_wireless.sh
Executable 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 ADB;execute/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 在手机本机运行(Root,phantom 端口见 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
|
||||
152
sdk/scripts/extract_aochuang_to_ziliao.sh
Normal file
152
sdk/scripts/extract_aochuang_to_ziliao.sh
Normal 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"
|
||||
464
sdk/scripts/frida_workphone_oneclick.sh
Executable file
464
sdk/scripts/frida_workphone_oneclick.sh
Executable 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-16(Root 模式 = 主路径)
|
||||
# 4) Root 不可用时自动尝试 Frida Gadget 备背端口转发
|
||||
# 5) 启动 SDK 服务(uvicorn 8899)
|
||||
# 6) 通过 /api/v3/hook/probe/{device_id} 探测 Hook 是否就绪
|
||||
# 7) 启动 Agent(WebSocket 通道,带 hook=enabled)
|
||||
# 8) 输出对接清单(Type-C 模式下存客宝直接调用 8899)
|
||||
#
|
||||
# 设计参考:
|
||||
# - 开发文档/8、部署/设备端Hook安装部署.md(Root + frida-server 标准)
|
||||
# - 机擎/references/快速入机部署_Root_Frida_Hook.md(10/10 验证基线)
|
||||
# - sdk/agent/hook/frida_manager.py(usb / 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 / 主:Frida,辅:ADB)"
|
||||
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
|
||||
}
|
||||
|
||||
# ── 步骤 4:Gadget 备背(无 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:启动设备端 Agent(USB-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 "启动 Agent(device=${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 连本机 SDK;lan.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 Hook(WECHAT_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
|
||||
171
sdk/scripts/gen_integration_manifest.py
Normal file
171
sdk/scripts/gen_integration_manifest.py
Normal file
@@ -0,0 +1,171 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
四端可直调·模块化接口能力清单 生成器
|
||||
|
||||
从运行中的 SDK 实时拉取 skill-registry(211 动作 / 49 模块 / 9 平台),
|
||||
生成模块化、可直读的接口能力清单 Markdown,供存客宝 / 触客宝 / AI数智员工 /
|
||||
SuperAdmin 直接对接判断「哪些能力可直调、走什么通道、经哪个 BFF/SDK 路径」。
|
||||
|
||||
- 不改任何兄弟仓库代码;只输出工作手机侧文档。
|
||||
- 真源:GET /api/v3/ai/brain/skill-registry(live)。
|
||||
- 用法: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-script(AI Brain)", "/api/v3/ai/brain/execute-script (script=douyin)"),
|
||||
"xhs": ("execute-script(AI Brain)", "/api/v3/ai/brain/execute-script (script=xhs)"),
|
||||
"xianyu": ("execute-script(AI Brain)", "/api/v3/ai/brain/execute-script (script=xianyu)"),
|
||||
"soul": ("execute-script(AI 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()
|
||||
189
sdk/scripts/gen_wechat_capability_matrix.py
Normal file
189
sdk/scripts/gen_wechat_capability_matrix.py
Normal 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
155
sdk/scripts/get_imei_md5.py
Normal 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()
|
||||
196
sdk/scripts/inject_frida_gadget.sh
Executable file
196
sdk/scripts/inject_frida_gadget.sh
Executable 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 + jarsigner(macOS 自带)
|
||||
#
|
||||
# 流程:
|
||||
# 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 "============================================"
|
||||
63
sdk/scripts/install_connection_keeper_launchagent.sh
Executable file
63
sdk/scripts/install_connection_keeper_launchagent.sh
Executable 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"
|
||||
78
sdk/scripts/install_docker_engine_guard.sh
Executable file
78
sdk/scripts/install_docker_engine_guard.sh
Executable 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"
|
||||
|
||||
# StartInterval:秒;600=每 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"
|
||||
57
sdk/scripts/install_sdk_watcher_launchagent.sh
Executable file
57
sdk/scripts/install_sdk_watcher_launchagent.sh
Executable 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"
|
||||
94
sdk/scripts/install_wechat_8.0.51.sh
Normal file
94
sdk/scripts/install_wechat_8.0.51.sh
Normal 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
165
sdk/scripts/lan_connect_setup.sh
Executable 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
158
sdk/scripts/master_deploy.sh
Executable 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}"
|
||||
335
sdk/scripts/matrix_hook_catalog_verify.py
Normal file
335
sdk/scripts/matrix_hook_catalog_verify.py
Normal 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()))
|
||||
124
sdk/scripts/matrix_v8056_real_device_verify.py
Normal file
124
sdk/scripts/matrix_v8056_real_device_verify.py
Normal 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 Hook(WECHAT_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.0(ADB UI 自动化,无需 Root)" in text:
|
||||
text = text.replace(
|
||||
"WeChatADBEngine v2.0(ADB 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())
|
||||
237
sdk/scripts/matrix_v8056_sync_doc_status.py
Normal file
237
sdk/scripts/matrix_v8056_sync_doc_status.py
Normal 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())
|
||||
115
sdk/scripts/nas_primary_agent_setup.sh
Executable file
115
sdk/scripts/nas_primary_agent_setup.sh
Executable 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}"
|
||||
46
sdk/scripts/open_docker_desktop_gui.sh
Executable file
46
sdk/scripts/open_docker_desktop_gui.sh
Executable 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 确认引擎是否正常。"
|
||||
33
sdk/scripts/phantom_frida_keepalive.sh
Normal file
33
sdk/scripts/phantom_frida_keepalive.sh
Normal 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 端阻塞运行 frida(adb 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
|
||||
14
sdk/scripts/phone_usb_enable_adb.sh
Executable file
14
sdk/scripts/phone_usb_enable_adb.sh
Executable file
@@ -0,0 +1,14 @@
|
||||
#!/usr/bin/env bash
|
||||
# USB 插线后一键开启无线 ADB(5555)并重连
|
||||
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。"
|
||||
68
sdk/scripts/probe_wp_wx04_when_ws_online.sh
Executable file
68
sdk/scripts/probe_wp_wx04_when_ws_online.sh
Executable 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 Docker:localhost(IPv6) 才打到 workphone-sdk;127.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
|
||||
30
sdk/scripts/real_device_connect_check.sh
Executable file
30
sdk/scripts/real_device_connect_check.sh
Executable 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}"
|
||||
35
sdk/scripts/refresh_wxid_cache.sh
Executable file
35
sdk/scripts/refresh_wxid_cache.sh
Executable 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}"
|
||||
26
sdk/scripts/reset_docker_desktop_window.sh
Executable file
26
sdk/scripts/reset_docker_desktop_window.sh
Executable 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 "现在可重新打开 Docker:open -a Docker"
|
||||
52
sdk/scripts/run_app.sh
Executable file
52
sdk/scripts/run_app.sh
Executable 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 ""
|
||||
125
sdk/scripts/run_extended_acceptance.sh
Executable file
125
sdk/scripts/run_extended_acceptance.sh
Executable 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)
|
||||
177
sdk/scripts/run_full_wechat_acceptance.sh
Executable file
177
sdk/scripts/run_full_wechat_acceptance.sh
Executable 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. oneclick(WS + 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}"
|
||||
198
sdk/scripts/run_stability_longrun.mjs
Executable file
198
sdk/scripts/run_stability_longrun.mjs
Executable 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));
|
||||
}
|
||||
}
|
||||
208
sdk/scripts/run_stability_longrun.py
Executable file
208
sdk/scripts/run_stability_longrun.py
Executable 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())
|
||||
126
sdk/scripts/run_three_acceptance.sh
Executable file
126
sdk/scripts/run_three_acceptance.sh
Executable 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.2(filehelper 自发自收 + 列表断言)─────────────
|
||||
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.3(hook 直连,绕过 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)
|
||||
26
sdk/scripts/run_wechat_full_acceptance.sh
Executable file
26
sdk/scripts/run_wechat_full_acceptance.sh
Executable 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 "✅ 全量验收流水线完成"
|
||||
194
sdk/scripts/run_wechat_private_domain_acceptance.mjs
Normal file
194
sdk/scripts/run_wechat_private_domain_acceptance.mjs
Normal 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;
|
||||
263
sdk/scripts/run_wechat_private_domain_acceptance.py
Executable file
263
sdk/scripts/run_wechat_private_domain_acceptance.py
Executable 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())
|
||||
98
sdk/scripts/run_wechat_realdevice_acceptance.sh
Executable file
98
sdk/scripts/run_wechat_realdevice_acceptance.sh
Executable 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_event,verified 才算通过)
|
||||
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
|
||||
65
sdk/scripts/run_wechat_safe_batch_acceptance.sh
Executable file
65
sdk/scripts/run_wechat_safe_batch_acceptance.sh
Executable 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}"
|
||||
189
sdk/scripts/run_wechat_safe_matrix_acceptance.py
Executable file
189
sdk/scripts/run_wechat_safe_matrix_acceptance.py
Executable 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())
|
||||
90
sdk/scripts/run_wireless_wechat_full_suite.sh
Executable file
90
sdk/scripts/run_wireless_wechat_full_suite.sh
Executable 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"
|
||||
155
sdk/scripts/sdk_connection_watcher.sh
Executable file
155
sdk/scripts/sdk_connection_watcher.sh
Executable 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
152
sdk/scripts/setup_emulator.sh
Executable file
@@ -0,0 +1,152 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# 一键设置模拟器:创建红米13 AVD + 中文系统 + 搜狗五笔 + Agent
|
||||
# 前置:已安装 Android SDK(sdkmanager/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. 先启动 SDK:cd 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
85
sdk/scripts/setup_frida.sh
Executable file
@@ -0,0 +1,85 @@
|
||||
#!/bin/bash
|
||||
# setup_frida.sh — 一键安装 Frida Server 到 Root Android 设备
|
||||
#
|
||||
# 用法:
|
||||
# ./scripts/setup_frida.sh [device_serial]
|
||||
#
|
||||
# 前置条件:
|
||||
# - 设备已 Root(Magisk)
|
||||
# - ADB 已连接且授权
|
||||
# - Python 已安装 frida-tools(pip 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
319
sdk/scripts/setup_noroot.sh
Executable 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 "部署完成!"
|
||||
78
sdk/scripts/skill_registry_audit.py
Normal file
78
sdk/scripts/skill_registry_audit.py
Normal 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
54
sdk/scripts/start_device.sh
Executable 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
68
sdk/scripts/start_sdk.sh
Executable 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 "============================================"
|
||||
36
sdk/scripts/termux_agent_bootstrap.sh
Normal file
36
sdk/scripts/termux_agent_bootstrap.sh
Normal 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"
|
||||
131
sdk/scripts/termux_frida_up_onphone.sh
Executable file
131
sdk/scripts/termux_frida_up_onphone.sh
Executable 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 0(SELinux 拦端口时)
|
||||
# 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 不可用,需 Root(Magisk)。无法启动 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 后回复「继续」自动跑三项真机验收。"
|
||||
141
sdk/scripts/termux_push_and_start_agent.sh
Executable file
141
sdk/scripts/termux_push_and_start_agent.sh
Executable 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 侧 oneclick(USB + 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
84
sdk/scripts/test_app.sh
Executable 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"
|
||||
60
sdk/scripts/test_doubao_search.sh
Executable file
60
sdk/scripts/test_doubao_search.sh
Executable 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 "- 中文输入可能需要使用输入法"
|
||||
38
sdk/scripts/uninstall_old_ocreat_apks.sh
Executable file
38
sdk/scripts/uninstall_old_ocreat_apks.sh
Executable 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 未动。"
|
||||
59
sdk/scripts/upgrade_wechat_and_oneclick.sh
Executable file
59
sdk/scripts/upgrade_wechat_and_oneclick.sh
Executable 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
|
||||
16
sdk/scripts/usb_reverse_workphone_ws.sh
Executable file
16
sdk/scripts/usb_reverse_workphone_ws.sh
Executable file
@@ -0,0 +1,16 @@
|
||||
#!/usr/bin/env bash
|
||||
# USB 调试:把手机里访问 127.0.0.1:8899 转发到「插着数据线的这台电脑」上的 8899(SDK 监听端口)。
|
||||
# 每次重插 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 的完整地址。"
|
||||
110
sdk/scripts/verify_aochuang_clean.sh
Executable file
110
sdk/scripts/verify_aochuang_clean.sh
Executable 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
|
||||
26
sdk/scripts/verify_production_urls.sh
Executable file
26
sdk/scripts/verify_production_urls.sh
Executable 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
30
sdk/scripts/wait_adb_device.sh
Executable 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
|
||||
67
sdk/scripts/wait_adb_then_login.sh
Executable file
67
sdk/scripts/wait_adb_then_login.sh
Executable 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
44
sdk/scripts/wait_ws_agent.sh
Executable 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
|
||||
25
sdk/scripts/watch_device_ai_chat.sh
Executable file
25
sdk/scripts/watch_device_ai_chat.sh
Executable 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
|
||||
181
sdk/scripts/wechat_interface_audit.py
Normal file
181
sdk/scripts/wechat_interface_audit.py
Normal file
@@ -0,0 +1,181 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
微信 Hook 接口完整性审计 — 禁止空接口/占位冒充
|
||||
|
||||
检查项:
|
||||
1. WECHAT_ACTIONS ↔ ACTION_TO_RPC ↔ wechat_hook_v2.js rpc.exports 三方对齐
|
||||
2. ACTION_ALIASES(173 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())
|
||||
322
sdk/scripts/wechat_unblock_batch.py
Executable file
322
sdk/scripts/wechat_unblock_batch.py
Executable 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())
|
||||
133
sdk/scripts/wechat_unblock_typec_oneshot.sh
Executable file
133
sdk/scripts/wechat_unblock_typec_oneshot.sh
Executable 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 初始化 uiautomator2(ATX + u2.jar,Agent 解封依赖 u2)
|
||||
echo "=== 初始化 uiautomator2 ==="
|
||||
if python3 -m uiautomator2 init --serial "$DEV" 2>&1 | tail -8; then
|
||||
echo "✅ u2 init 完成"
|
||||
else
|
||||
echo "⚠️ u2 init 失败,继续尝试 oneclick(Agent 可能无 u2)"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# 3. oneclick 起 SDK + Frida + Agent
|
||||
echo "=== 启动 oneclick(WS + 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"
|
||||
39
sdk/scripts/wireless_adb_pair.sh
Executable file
39
sdk/scripts/wireless_adb_pair.sh
Executable 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
|
||||
34
sdk/scripts/wireless_autoconnect_bootstrap.sh
Executable file
34
sdk/scripts/wireless_autoconnect_bootstrap.sh
Executable 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 config(NAS→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}"
|
||||
166
sdk/scripts/wireless_master_oneclick.sh
Executable file
166
sdk/scripts/wireless_master_oneclick.sh
Executable file
@@ -0,0 +1,166 @@
|
||||
#!/usr/bin/env bash
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# 工作手机 · 无 USB 无线主控一键脚本(WiFi adb + 设备本机 stealth Frida + WS Agent)
|
||||
#
|
||||
# 解决:USB 线材/接口物理不稳反复掉线。改用 WiFi adb 传输,u2/frida 全走无线;
|
||||
# WS 身份仍用逻辑 device_id(SDK/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、已装 frida(phantom_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_id(WS 身份),全程不变
|
||||
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. 启动 Agent(WP_DEVICE_SERIAL=WiFi 序列,device_id=逻辑身份)───────────
|
||||
log "⑤ 启动 Agent:device_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 "⑥ 验证无线 Hook(get_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
303
sdk/scripts/ws_full_test.py
Executable 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()
|
||||
42
sdk/scripts/xiaomi_fastboot_flash_magisk.sh
Executable file
42
sdk/scripts/xiaomi_fastboot_flash_magisk.sh
Executable 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"
|
||||
Reference in New Issue
Block a user