diff --git a/wechat_plain/scripts/auto-reply/README.md b/wechat_plain/scripts/auto-reply/README.md index b9f60bd..ca5dbda 100644 --- a/wechat_plain/scripts/auto-reply/README.md +++ b/wechat_plain/scripts/auto-reply/README.md @@ -48,8 +48,9 @@ - **失败即静默**:生成超时/失败 → 无草稿 → policy skip → 不发。最坏情况是不回,绝不误发。 - **API 发送的两个坑(2026-07-11 硬仗定位,详见 memory `wechat-api-send-onresume-flush`)**: 1. 中文必须走 `--es text_b64`(`--es text` 被 shell/am 弄乱 → 卡 status=1)。 - 2. `z2.b` 只入队(status=1 转圈),**WeChat NetSceneQueue 要一次 onResume(后台→前台切换)才派发** → 发送后自动 `KEYCODE_HOME` + `am start LauncherUI` 触发派发 → status=2。**这是设备机制不是代码 bug**,发送代码跨版本逐字节相同,别去 diff/重编译 hook 找代码差异。 - 3. 送达校验:轮询 `message.status`(1=发送中/2=已达/5=失败);超时仍 1 视为"已入队将送达",不误报失败。 + 2. `z2.b` 只入队(status=1 转圈),**WeChat NetSceneQueue 要一次真实的 app 切换(切走→切回微信 = onPause→onResume)才派发**。**这是设备机制不是代码 bug**,别去 diff/重编译 hook。 + 3. **flush 加固(2026-07-12,`_send_text_verified`)**:发送后 `time.sleep(4)` 让消息稳稳落库(太快切换微信还没登记这条)→ `_onresume_cycle()`(`KEYCODE_HOME` 背景化 2.5s → `am start LauncherUI` 切回停 3s)→ **切回后停在前台多轮查 status,不立刻再切走**(否则打断正在进行的网络发送)→ 没到 2 再来一轮。真机实测:屏幕亮/灭(Dozing)都 **~15s flush 到 status=2**(旧版单次切换+被动等常卡几十秒~分钟)。 + 4. 送达校验:轮询 `message.status`(1=发送中/2=已达/5=失败);超时仍 1 视为"已入队将送达",不误报失败。 ## 运维 diff --git a/wechat_plain/scripts/auto-reply/wechat_reply_orchestrator.py b/wechat_plain/scripts/auto-reply/wechat_reply_orchestrator.py index 48cc552..455c088 100644 --- a/wechat_plain/scripts/auto-reply/wechat_reply_orchestrator.py +++ b/wechat_plain/scripts/auto-reply/wechat_reply_orchestrator.py @@ -159,8 +159,23 @@ def _queue_insert(event, draft, sender_disp, action, reason, status): con.close() -VERIFY_TIMEOUT = 25 # 轮询确认送达的总超时(秒) -VERIFY_POLL = 3 # 轮询间隔 +VERIFY_TIMEOUT = 60 # 送达校验总超时(秒) +VERIFY_POLL = 3 # 前台停留期每次查 status 的间隔 +DWELL_POLLS = 4 # 一次 onResume 后停留前台查几次(给网络发送时间,不立刻再背景化打断) + + +def _onresume_cycle(): + """一次真实的 onPause→onResume:HOME 背景化 → 停顿 → 拉回微信前台。 + 触发 WeChat NetSceneQueue flush 卡在 status=1 的发送。切换要给足时间——太快 + (背景/前台 <2s)微信来不及走完 onPause/onResume+网络 tick,flush 不了。""" + try: + subprocess.run(["input", "keyevent", "KEYCODE_HOME"], capture_output=True, timeout=6) + time.sleep(2.5) # 让 onPause 走完、微信真的进后台 + subprocess.run(["am", "start", "-n", "com.tencent.mm/.ui.LauncherUI"], + capture_output=True, timeout=8) + time.sleep(3) # onResume 后停一下,让网络 scene 开始派发 + except Exception: # noqa: BLE001 + pass def _cleanup_verify(): @@ -219,32 +234,28 @@ def _send_text_verified(to_wxid, text, key): res = send_text_via_hook(to_wxid, text) if res.get("status") != "sent": return {"ok": False, "reason": "broadcast失败:" + str(res.get("error", ""))[:40]} - # 关键机制:z2.b 只把消息入队(status=1 转圈),WeChat 发送队列要一次 onResume - # (后台→前台切换)才真正派发。真机验证:发送后 HOME 切后台再拉回前台,卡住的消息 - # 全部 flush 到 status=2。(等同"重启微信才发出去",但轻得多。) - time.sleep(2) # 让消息先入库 - try: - subprocess.run(["input", "keyevent", "KEYCODE_HOME"], capture_output=True, timeout=6) - time.sleep(1.5) - subprocess.run(["am", "start", "-n", "com.tencent.mm/.ui.LauncherUI"], - capture_output=True, timeout=8) - time.sleep(2) - except Exception: # noqa: BLE001 - pass - deadline = time.time() + VERIFY_TIMEOUT + # 关键机制:z2.b 只把消息入队(status=1 转圈),WeChat 发送队列要一次真实的 app 切换 + # (切走→切回微信 = onPause→onResume)才真正派发。真机确认:发送后等几秒 → 切到别的 app + # → 切回微信,卡住的消息就 flush 到 status=2。这里:先等消息落库,再做完整切换 cycle, + # 切回后停在前台多轮查(不立刻再切走,否则会打断正在进行的网络发送)。 + time.sleep(4) # 让消息先稳稳落库(太快切换时微信还没登记这条,flush 不到它) last = None + deadline = time.time() + VERIFY_TIMEOUT while time.time() < deadline: - time.sleep(VERIFY_POLL) - last = _sent_status(db, key, to_wxid, base, text) - if last == 2: - return {"ok": True, "reason": "verified(status=2)"} - if last == 5: - return {"ok": False, "reason": "status=5(微信报失败)"} - # 超时判定:真机验证 status=1 是"已入队发送中",微信连接慢时会延迟但最终送达 - # (曾观察到十几条 status=1 在微信重连后全部 flush 到 2)→ 视为已发,不误报失败。 - # 仅 status=None(消息压根没创建,hook 没处理/微信没就绪)才算真失败 → 进审核。 + _onresume_cycle() # 一次真实切走→切回 + for _ in range(DWELL_POLLS): # 切回后停留前台查几次,给网络发送时间 + last = _sent_status(db, key, to_wxid, base, text) + if last == 2: + return {"ok": True, "reason": "verified(status=2)"} + if last == 5: + return {"ok": False, "reason": "status=5(微信报失败)"} + if time.time() >= deadline: + break + time.sleep(VERIFY_POLL) + # 超时判定:多轮切换仍卡 status=1 = 微信连接慢/离线,最终多半会 flush(网络恢复时批量 + # 派发)→ 视为已入队不误报失败。仅 status=None(消息没创建,hook 没处理)才算真失败。 if last == 1: - return {"ok": True, "reason": "queued(status=1,已入队将送达)"} + return {"ok": True, "reason": "queued(status=1,多轮切换仍未flush,网络恢复后送达)"} return {"ok": False, "reason": f"消息未创建(status={last})"}