diff --git a/phoneuse/android_mcp.py b/phoneuse/android_mcp.py index 1dd8fc4..4e91dae 100644 --- a/phoneuse/android_mcp.py +++ b/phoneuse/android_mcp.py @@ -889,17 +889,22 @@ def _build_server(): @mcp.tool() def send_wechat_voice(to: str, silk_path: str, duration_ms: int = 3000) -> str: """ - Send a voice message to a WeChat contact via the internal hook API (no UI simulation). + Send a FULLY-SYNTHETIC voice message to a WeChat contact (no recording, no hot-swap). - Uses WeChat's w11.r0 factory (MsgType 34). The hook injects the correct - voice file ID into the DB insert so the message is stored with the right - SILK file reference and is immediately playable. + Fires SEND_VOICE_CGI → the hook chunks the SILK, builds WeChat's own + /cgi-bin/micromsg-bin/uploadvoice request (audio inline), dispatches it via the + NetSceneQueue, and inserts a local sent bubble. Result: the recipient gets a + playable voice AND the sender sees a sent bubble. Verified end-to-end on WeChat + 8.0.74 (recipient + Mac both received). Delivery is async; confirm via UI if needed. + + The SILK is auto-staged into WeChat's data dir (its uid + SELinux context) so the + WeChat process can read it — a device /sdcard path alone would EACCES. Args: - to: Contact wxid (e.g. 'filehelper'). IMPORTANT: test only with 'filehelper'. - silk_path: Absolute device path to a SILK V3 file (Tencent variant, - starts with \\x02#!SILK_V3). Generate with silkv3-encoder - or convert from PCM via silk-v3-encoder. + to: Contact wxid as in rcontact.username (e.g. 'filehelper', 'wxid_xxx', + 'yibooo'). Test with 'filehelper' first to avoid account risk. + silk_path: Absolute device path to a SILK V3 file (Tencent variant, starts + with \\x02#!SILK_V3), e.g. edge-tts → ffmpeg 16k mono → silk-wasm. duration_ms: Voice duration in milliseconds shown in WeChat UI. """ import sys @@ -907,8 +912,8 @@ def _build_server(): if _base not in sys.path: sys.path.insert(0, _base) try: - from wechat_hook_send import send_voice_via_hook - result = send_voice_via_hook(wxid=to, silk_path=silk_path, duration_ms=duration_ms) + from wechat_hook_send import send_voice_cgi_via_hook + result = send_voice_cgi_via_hook(wxid=to, silk_path=silk_path, duration_ms=duration_ms) return json.dumps(result, ensure_ascii=False) except Exception as e: return json.dumps({"status": "error", "error": str(e)}) diff --git a/phoneuse/wechat_hook_send.py b/phoneuse/wechat_hook_send.py index 6e03a15..21e2f0a 100644 --- a/phoneuse/wechat_hook_send.py +++ b/phoneuse/wechat_hook_send.py @@ -24,6 +24,7 @@ from ui_control_tools import _run, _adb_prefix, _ON_DEVICE SEND_ACTION = "com.hermes.wechat.SEND_MSG_INTERNAL" SEND_IMAGE_ACTION = "com.hermes.wechat.SEND_IMAGE_INTERNAL" +SEND_VOICE_CGI_ACTION = "com.hermes.wechat.SEND_VOICE_CGI" _WECHAT_PKG = "com.tencent.mm" _WECHAT_MAIN = "com.tencent.mm/.ui.LauncherUI" @@ -249,6 +250,69 @@ def send_image_via_hook(wxid: str, image_path: str) -> dict[str, Any]: } +def send_voice_cgi_via_hook(wxid: str, silk_path: str, duration_ms: int = 3000) -> dict[str, Any]: + """Send a FULLY-SYNTHETIC voice to *wxid* via the internal uploadvoice cgi. + + No recording, no hot-swap. The hook chunks the SILK, builds WeChat's own + /cgi-bin/micromsg-bin/uploadvoice request (audio inline), dispatches it via the + NetSceneQueue (modelbase.i.l() + subscribe), and inserts a local f9 bubble marked + sent. Result: the recipient gets a playable voice AND the sender sees a sent bubble. + Verified end-to-end on WeChat 8.0.74 (recipient + Mac both received; both sides visible). + + The hook reads *silk_path* AS the WeChat process, so the SILK is first staged into + WeChat's own data dir (its uid + app_data_file SELinux context) — /sdcard gives + EACCES and a plain root cp into the sandbox needs chown + restorecon. + + Args: + wxid: talker id as in rcontact.username (e.g. 'filehelper', 'wxid_xxx', 'yibooo'). + Test with 'filehelper' first to avoid account risk. + silk_path: device path to a SILK V3 file (Tencent variant, \\x02#!SILK_V3), + e.g. from edge-tts → ffmpeg 16k mono → silk-wasm. + duration_ms: voice duration in milliseconds shown in WeChat UI. + """ + if not wxid or not silk_path: + return {"status": "error", "error": "wxid and silk_path required"} + if not _wechat_running(): + if not _start_wechat(): + return {"status": "error", "error": "WeChat not running and failed to start"} + time.sleep(3) + + # Resolve WeChat's uid (changes across reinstalls) and stage the SILK into its data + # dir with the matching owner + SELinux context so the WeChat process can read it. + uid_res = _root_sh("stat -c %U /data/user/0/com.tencent.mm") + wx_uid = (uid_res.stdout or "").strip() + if not wx_uid: + return {"status": "error", "error": f"could not resolve WeChat uid: {uid_res.stderr or '(no output)'}"} + stage_path = "/data/data/com.tencent.mm/files/hook_voice_cgi.silk" + stage = _root_sh( + f'cp "{silk_path}" "{stage_path}" && ' + f'chown {wx_uid}:{wx_uid} "{stage_path}" && ' + f'chmod 600 "{stage_path}"; restorecon "{stage_path}" 2>/dev/null; true' + ) + if stage.returncode != 0: + return {"status": "error", "error": f"failed to stage SILK: {stage.stderr or stage.stdout}"} + + cmd = ( + f"am broadcast -a {SEND_VOICE_CGI_ACTION} " + f"--es to {wxid} --es src_path {stage_path} --ei duration {duration_ms}" + ) + try: + r = _root_sh(cmd) + except subprocess.TimeoutExpired: + return {"status": "error", "error": "voice cgi broadcast timed out"} + stdout = (r.stdout or "").strip() + stderr = (r.stderr or "").strip() + if r.returncode == 0 and ("result=" in stdout or "Broadcast completed" in stdout): + return { + "status": "sent", + "to": wxid, + "method": "uploadvoice_cgi", + "verified": False, # cgi is async; confirm via UI / recipient + "detail": stdout[:200], + } + return {"status": "error", "error": f"broadcast rc={r.returncode}: {stderr or stdout or '(no output)'}"} + + def send_voice_via_hook(wxid: str, silk_path: str, duration_ms: int = 3000) -> dict[str, Any]: """Send a voice message to *wxid* via the wechathook internal API. diff --git a/xhs_lsposed_a16_fix/backups/wechat_send_20260707_voice-cgi.sh b/xhs_lsposed_a16_fix/backups/wechat_send_20260707_voice-cgi.sh new file mode 100755 index 0000000..01eac79 --- /dev/null +++ b/xhs_lsposed_a16_fix/backups/wechat_send_20260707_voice-cgi.sh @@ -0,0 +1,103 @@ +#!/bin/bash +# wechat_send.sh — unified WeChat send CLI via the internal hook +# +# Usage: +# ./wechat_send.sh text +# ./wechat_send.sh image +# ./wechat_send.sh voice [duration_ms] +# +# All send operations go through the wechathook LSPosed module's internal API, +# which calls WeChat's own w11.r0/w11.s1 factory — no UI simulation. +# : talker id as stored in WeChat DB (e.g. 'filehelper', 'wxid_xxx'). +# IMPORTANT: test-only with 'filehelper' to avoid account risk. +# +# Voice notes (SEND_VOICE_CGI — fully synthetic, no recording/hot-swap): +# — local SILK V3 file (Tencent prefix \x02#!SILK_V3), e.g. from +# edge-tts → ffmpeg 16k mono → silk-wasm. +# [duration_ms] — playback duration in ms (default: 3000). +# Stages the SILK into WeChat's own data dir (uid + app_data_file ctx) then broadcasts +# the internal uploadvoice cgi: uploads audio inline + delivers to the recipient AND +# inserts a sent bubble on the sender side. Verified end-to-end + both-sides visible. +# +# Examples: +# ./wechat_send.sh text filehelper "hello world" +# ./wechat_send.sh image filehelper /tmp/screenshot.png +# ./wechat_send.sh voice filehelper /tmp/tts_out.silk 4617 + +set -e + +ADB="${ADB:-adb}" +WECHAT_CACHE="/data/data/com.tencent.mm/cache/hook_voice_src.amr" + +usage() { + echo "Usage: $0 text " + echo " $0 image " + echo " $0 voice [duration_ms]" + exit 1 +} + +[ $# -lt 3 ] && usage + +CMD="$1"; WXID="$2"; shift 2 + +case "$CMD" in +text) + PAYLOAD="$*" + B64_WXID=$(printf '%s' "$WXID" | base64 | tr -d '\n') + B64_TEXT=$(printf '%s' "$PAYLOAD" | base64 | tr -d '\n') + INNER=$(printf 'TO=$(printf "%%s" "%s" | base64 -d); TEXT=$(printf "%%s" "%s" | base64 -d); am broadcast -a com.hermes.wechat.SEND_MSG_INTERNAL --es to "$TO" --es text "$TEXT"' \ + "$B64_WXID" "$B64_TEXT") + B64_INNER=$(printf '%s' "$INNER" | base64 | tr -d '\n') + echo "Sending text to $WXID..." + $ADB shell "su -c 'echo $B64_INNER | base64 -d | sh'" + ;; + +image) + # Internal API: WeChat's own b80.u.oj (insert+prebuild) + v70.w.oj (CDN upload). + # No UI, no contact picker — fire-and-forget to . Verified full-res delivery. + IMAGE_PATH="$1" + [ -z "$IMAGE_PATH" ] && usage + EXT="${IMAGE_PATH##*.}" + # Stage into /sdcard so the WeChat process (runs the hook) can read it, regardless + # of whether the source path is root-only. If already under /sdcard this is a no-op copy. + SHARE="/sdcard/wechat_send_tmp.$EXT" + echo "Staging image to $SHARE..." + $ADB shell "su -c 'cp \"$IMAGE_PATH\" \"$SHARE\" && chmod 644 \"$SHARE\"'" + echo "Sending image to $WXID via internal API..." + $ADB shell "su -c 'am broadcast -a com.hermes.wechat.SEND_IMAGE_INTERNAL --es to \"$WXID\" --es path \"$SHARE\"'" + echo "Sent. (Verify via WeChat UI screenshot; full image uploads async via CDN.)" + ;; + +voice) + # Fully-synthetic voice via the internal uploadvoice cgi (no recording, no hot-swap). + # Uploads the SILK inline + delivers to the recipient AND inserts a sent bubble on the + # sender side. The hook reads src_path AS the WeChat process, so the SILK must live in + # WeChat's own data dir with its uid + app_data_file SELinux context — /sdcard gives + # EACCES and a root cp into the sandbox needs chown+restorecon to the right owner. + SILK_PATH="$1" + DURATION="${2:-3000}" + [ -z "$SILK_PATH" ] && usage + [ ! -f "$SILK_PATH" ] && { echo "File not found: $SILK_PATH"; exit 1; } + + WX_UID=$($ADB shell "su -c 'stat -c %U /data/user/0/com.tencent.mm'" | tr -d '\r') + [ -z "$WX_UID" ] && { echo "Could not resolve WeChat uid"; exit 1; } + STAGE="/data/data/com.tencent.mm/files/hook_voice_cgi.silk" + + echo "Staging SILK into WeChat data dir (uid=$WX_UID)..." + $ADB push "$SILK_PATH" /sdcard/hook_voice_tmp.silk >/dev/null + $ADB shell "su -c 'cp /sdcard/hook_voice_tmp.silk $STAGE'" + $ADB shell "su -c 'chown $WX_UID:$WX_UID $STAGE'" + $ADB shell "su -c 'chmod 600 $STAGE'" + $ADB shell "su -c 'restorecon $STAGE'" 2>/dev/null + $ADB shell "su -c 'rm -f /sdcard/hook_voice_tmp.silk'" + + echo "Sending synthetic voice to $WXID (duration=${DURATION}ms)..." + $ADB shell "su -c 'am broadcast -a com.hermes.wechat.SEND_VOICE_CGI --es to $WXID --es src_path $STAGE --ei duration $DURATION'" + echo "Sent. (Recipient gets a playable voice; sender sees a sent bubble. Verify via WeChat UI.)" + ;; + +*) + echo "Unknown command: $CMD" + usage + ;; +esac diff --git a/xhs_lsposed_a16_fix/skill_wechat_send/SKILL.md b/xhs_lsposed_a16_fix/skill_wechat_send/SKILL.md index 3a72afd..fa88ad3 100644 --- a/xhs_lsposed_a16_fix/skill_wechat_send/SKILL.md +++ b/xhs_lsposed_a16_fix/skill_wechat_send/SKILL.md @@ -48,9 +48,17 @@ adb shell su -c 'am broadcast -a com.hermes.wechat.SEND_IMAGE_INTERNAL --es to y `su` 能读到的路径(如 `/sdcard/...`)。发前若图片在 Mac 上,先 `adb push` 到设备。 2026-07-07 真机验证:filehelper + 真人(火星人)均全图收到。详见技术文档 §3。 -### ✅ 语音(TTS→SILK,半自动 hot-swap) -纯合成卡「发送中」,用 hot-swap:让微信真录一段 + 偷换 SILK 字节。 -端到端脚本 `../tts_voice_swap.sh`(需先打开目标聊天并处语音模式)。详见技术文档 §2。 +### ✅ 语音(TTS→SILK,全合成,无录音/无 hot-swap) +```bash +adb shell su -c 'am broadcast -a com.hermes.wechat.SEND_VOICE_CGI --es to yibooo --es src_path <设备上SILK> --ei duration 3000' +``` +或用封装脚本(自动把 SILK stage 进 WeChat 数据目录):`./wechat_send.sh voice yibooo /path/to.silk 3000` + +走微信内部 `uploadvoice` cgi:音频内联上传 + 投递给接收方,同时插发送方本地已发送气泡。 +真机验证:接收方(火星人/Mac)收到能播 + 发送方本地正常显示。SILK 来源 = edge-tts→ffmpeg +16k mono→silk-wasm。**SILK 须 WeChat 进程可读**——封装脚本已处理(stage 进 +`/data/data/com.tencent.mm/files/` + chown WeChat uid + restorecon);直接 `/sdcard` 会 EACCES。 +旧的半自动 hot-swap(`../tts_voice_swap.sh` + `ARM_VOICE_SWAP`)已被全合成取代,可弃用。 ## 不要做 - **不要用 `dk5.s5.Ni` 发文字**——那是发名片的(会得到失败的空名片)。 diff --git a/xhs_lsposed_a16_fix/wechat_send.sh b/xhs_lsposed_a16_fix/wechat_send.sh index 016e0a1..01eac79 100755 --- a/xhs_lsposed_a16_fix/wechat_send.sh +++ b/xhs_lsposed_a16_fix/wechat_send.sh @@ -11,10 +11,13 @@ # : talker id as stored in WeChat DB (e.g. 'filehelper', 'wxid_xxx'). # IMPORTANT: test-only with 'filehelper' to avoid account risk. # -# Voice notes: -# — local SILK V3 file (Tencent prefix \x02#!SILK_V3). +# Voice notes (SEND_VOICE_CGI — fully synthetic, no recording/hot-swap): +# — local SILK V3 file (Tencent prefix \x02#!SILK_V3), e.g. from +# edge-tts → ffmpeg 16k mono → silk-wasm. # [duration_ms] — playback duration in ms (default: 3000). -# The script pushes the SILK to WeChat's cache dir and fires the broadcast. +# Stages the SILK into WeChat's own data dir (uid + app_data_file ctx) then broadcasts +# the internal uploadvoice cgi: uploads audio inline + delivers to the recipient AND +# inserts a sent bubble on the sender side. Verified end-to-end + both-sides visible. # # Examples: # ./wechat_send.sh text filehelper "hello world" @@ -66,17 +69,31 @@ image) ;; voice) + # Fully-synthetic voice via the internal uploadvoice cgi (no recording, no hot-swap). + # Uploads the SILK inline + delivers to the recipient AND inserts a sent bubble on the + # sender side. The hook reads src_path AS the WeChat process, so the SILK must live in + # WeChat's own data dir with its uid + app_data_file SELinux context — /sdcard gives + # EACCES and a root cp into the sandbox needs chown+restorecon to the right owner. SILK_PATH="$1" DURATION="${2:-3000}" [ -z "$SILK_PATH" ] && usage [ ! -f "$SILK_PATH" ] && { echo "File not found: $SILK_PATH"; exit 1; } - echo "Pushing SILK to WeChat cache..." - $ADB push "$SILK_PATH" /sdcard/hook_voice_tmp.amr - $ADB shell "su -c 'cp /sdcard/hook_voice_tmp.amr $WECHAT_CACHE && chmod 644 $WECHAT_CACHE && chown u0_a330:u0_a330 $WECHAT_CACHE && rm /sdcard/hook_voice_tmp.amr'" + WX_UID=$($ADB shell "su -c 'stat -c %U /data/user/0/com.tencent.mm'" | tr -d '\r') + [ -z "$WX_UID" ] && { echo "Could not resolve WeChat uid"; exit 1; } + STAGE="/data/data/com.tencent.mm/files/hook_voice_cgi.silk" - echo "Sending voice to $WXID (duration=${DURATION}ms)..." - $ADB shell "su -c 'am broadcast -a com.hermes.wechat.SEND_VOICE_INTERNAL --es to $WXID --es src_path $WECHAT_CACHE --ei duration $DURATION'" + echo "Staging SILK into WeChat data dir (uid=$WX_UID)..." + $ADB push "$SILK_PATH" /sdcard/hook_voice_tmp.silk >/dev/null + $ADB shell "su -c 'cp /sdcard/hook_voice_tmp.silk $STAGE'" + $ADB shell "su -c 'chown $WX_UID:$WX_UID $STAGE'" + $ADB shell "su -c 'chmod 600 $STAGE'" + $ADB shell "su -c 'restorecon $STAGE'" 2>/dev/null + $ADB shell "su -c 'rm -f /sdcard/hook_voice_tmp.silk'" + + echo "Sending synthetic voice to $WXID (duration=${DURATION}ms)..." + $ADB shell "su -c 'am broadcast -a com.hermes.wechat.SEND_VOICE_CGI --es to $WXID --es src_path $STAGE --ei duration $DURATION'" + echo "Sent. (Recipient gets a playable voice; sender sees a sent bubble. Verify via WeChat UI.)" ;; *)