Commit 266b0b95662995256f7930c90b446ca9fe74e928
1 parent
4f519e39
fix(wechat-auto-reply): 加固发送 flush — 延迟从分钟级降到 ~15s
用户实测发现:触发 NetSceneQueue 派发的是一次真实 app 切换(切走→切回微信), 且要给足时间。旧版单次切换+被动等 25s 常卡几十秒~分钟。三个经验值: - 发送后 sleep(4) 让消息稳稳落库(切太快微信还没登记这条,flush 不到) - _onresume_cycle: HOME 背景化 2.5s → am start LauncherUI 切回停 3s(切换给足时间) - 切回后停前台连查 DWELL_POLLS 轮,不立刻再切走(否则打断正在进行的网络发送); 没到 2 再来一轮,总超时 60s 真机实测:屏幕亮/灭(Dozing)都 ~15s flush 到 status=2。memory wechat-api-send-onresume-flush 已补加固经验。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Showing
2 changed files
with
39 additions
and
27 deletions
wechat_plain/scripts/auto-reply/README.md
| @@ -48,8 +48,9 @@ | @@ -48,8 +48,9 @@ | ||
| 48 | - **失败即静默**:生成超时/失败 → 无草稿 → policy skip → 不发。最坏情况是不回,绝不误发。 | 48 | - **失败即静默**:生成超时/失败 → 无草稿 → policy skip → 不发。最坏情况是不回,绝不误发。 |
| 49 | - **API 发送的两个坑(2026-07-11 硬仗定位,详见 memory `wechat-api-send-onresume-flush`)**: | 49 | - **API 发送的两个坑(2026-07-11 硬仗定位,详见 memory `wechat-api-send-onresume-flush`)**: |
| 50 | 1. 中文必须走 `--es text_b64`(`--es text` 被 shell/am 弄乱 → 卡 status=1)。 | 50 | 1. 中文必须走 `--es text_b64`(`--es text` 被 shell/am 弄乱 → 卡 status=1)。 |
| 51 | - 2. `z2.b` 只入队(status=1 转圈),**WeChat NetSceneQueue 要一次 onResume(后台→前台切换)才派发** → 发送后自动 `KEYCODE_HOME` + `am start LauncherUI` 触发派发 → status=2。**这是设备机制不是代码 bug**,发送代码跨版本逐字节相同,别去 diff/重编译 hook 找代码差异。 | ||
| 52 | - 3. 送达校验:轮询 `message.status`(1=发送中/2=已达/5=失败);超时仍 1 视为"已入队将送达",不误报失败。 | 51 | + 2. `z2.b` 只入队(status=1 转圈),**WeChat NetSceneQueue 要一次真实的 app 切换(切走→切回微信 = onPause→onResume)才派发**。**这是设备机制不是代码 bug**,别去 diff/重编译 hook。 |
| 52 | + 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**(旧版单次切换+被动等常卡几十秒~分钟)。 | ||
| 53 | + 4. 送达校验:轮询 `message.status`(1=发送中/2=已达/5=失败);超时仍 1 视为"已入队将送达",不误报失败。 | ||
| 53 | 54 | ||
| 54 | ## 运维 | 55 | ## 运维 |
| 55 | 56 |
wechat_plain/scripts/auto-reply/wechat_reply_orchestrator.py
| @@ -159,8 +159,23 @@ def _queue_insert(event, draft, sender_disp, action, reason, status): | @@ -159,8 +159,23 @@ def _queue_insert(event, draft, sender_disp, action, reason, status): | ||
| 159 | con.close() | 159 | con.close() |
| 160 | 160 | ||
| 161 | 161 | ||
| 162 | -VERIFY_TIMEOUT = 25 # 轮询确认送达的总超时(秒) | ||
| 163 | -VERIFY_POLL = 3 # 轮询间隔 | 162 | +VERIFY_TIMEOUT = 60 # 送达校验总超时(秒) |
| 163 | +VERIFY_POLL = 3 # 前台停留期每次查 status 的间隔 | ||
| 164 | +DWELL_POLLS = 4 # 一次 onResume 后停留前台查几次(给网络发送时间,不立刻再背景化打断) | ||
| 165 | + | ||
| 166 | + | ||
| 167 | +def _onresume_cycle(): | ||
| 168 | + """一次真实的 onPause→onResume:HOME 背景化 → 停顿 → 拉回微信前台。 | ||
| 169 | + 触发 WeChat NetSceneQueue flush 卡在 status=1 的发送。切换要给足时间——太快 | ||
| 170 | + (背景/前台 <2s)微信来不及走完 onPause/onResume+网络 tick,flush 不了。""" | ||
| 171 | + try: | ||
| 172 | + subprocess.run(["input", "keyevent", "KEYCODE_HOME"], capture_output=True, timeout=6) | ||
| 173 | + time.sleep(2.5) # 让 onPause 走完、微信真的进后台 | ||
| 174 | + subprocess.run(["am", "start", "-n", "com.tencent.mm/.ui.LauncherUI"], | ||
| 175 | + capture_output=True, timeout=8) | ||
| 176 | + time.sleep(3) # onResume 后停一下,让网络 scene 开始派发 | ||
| 177 | + except Exception: # noqa: BLE001 | ||
| 178 | + pass | ||
| 164 | 179 | ||
| 165 | 180 | ||
| 166 | def _cleanup_verify(): | 181 | def _cleanup_verify(): |
| @@ -219,32 +234,28 @@ def _send_text_verified(to_wxid, text, key): | @@ -219,32 +234,28 @@ def _send_text_verified(to_wxid, text, key): | ||
| 219 | res = send_text_via_hook(to_wxid, text) | 234 | res = send_text_via_hook(to_wxid, text) |
| 220 | if res.get("status") != "sent": | 235 | if res.get("status") != "sent": |
| 221 | return {"ok": False, "reason": "broadcast失败:" + str(res.get("error", ""))[:40]} | 236 | return {"ok": False, "reason": "broadcast失败:" + str(res.get("error", ""))[:40]} |
| 222 | - # 关键机制:z2.b 只把消息入队(status=1 转圈),WeChat 发送队列要一次 onResume | ||
| 223 | - # (后台→前台切换)才真正派发。真机验证:发送后 HOME 切后台再拉回前台,卡住的消息 | ||
| 224 | - # 全部 flush 到 status=2。(等同"重启微信才发出去",但轻得多。) | ||
| 225 | - time.sleep(2) # 让消息先入库 | ||
| 226 | - try: | ||
| 227 | - subprocess.run(["input", "keyevent", "KEYCODE_HOME"], capture_output=True, timeout=6) | ||
| 228 | - time.sleep(1.5) | ||
| 229 | - subprocess.run(["am", "start", "-n", "com.tencent.mm/.ui.LauncherUI"], | ||
| 230 | - capture_output=True, timeout=8) | ||
| 231 | - time.sleep(2) | ||
| 232 | - except Exception: # noqa: BLE001 | ||
| 233 | - pass | ||
| 234 | - deadline = time.time() + VERIFY_TIMEOUT | 237 | + # 关键机制:z2.b 只把消息入队(status=1 转圈),WeChat 发送队列要一次真实的 app 切换 |
| 238 | + # (切走→切回微信 = onPause→onResume)才真正派发。真机确认:发送后等几秒 → 切到别的 app | ||
| 239 | + # → 切回微信,卡住的消息就 flush 到 status=2。这里:先等消息落库,再做完整切换 cycle, | ||
| 240 | + # 切回后停在前台多轮查(不立刻再切走,否则会打断正在进行的网络发送)。 | ||
| 241 | + time.sleep(4) # 让消息先稳稳落库(太快切换时微信还没登记这条,flush 不到它) | ||
| 235 | last = None | 242 | last = None |
| 243 | + deadline = time.time() + VERIFY_TIMEOUT | ||
| 236 | while time.time() < deadline: | 244 | while time.time() < deadline: |
| 237 | - time.sleep(VERIFY_POLL) | ||
| 238 | - last = _sent_status(db, key, to_wxid, base, text) | ||
| 239 | - if last == 2: | ||
| 240 | - return {"ok": True, "reason": "verified(status=2)"} | ||
| 241 | - if last == 5: | ||
| 242 | - return {"ok": False, "reason": "status=5(微信报失败)"} | ||
| 243 | - # 超时判定:真机验证 status=1 是"已入队发送中",微信连接慢时会延迟但最终送达 | ||
| 244 | - # (曾观察到十几条 status=1 在微信重连后全部 flush 到 2)→ 视为已发,不误报失败。 | ||
| 245 | - # 仅 status=None(消息压根没创建,hook 没处理/微信没就绪)才算真失败 → 进审核。 | 245 | + _onresume_cycle() # 一次真实切走→切回 |
| 246 | + for _ in range(DWELL_POLLS): # 切回后停留前台查几次,给网络发送时间 | ||
| 247 | + last = _sent_status(db, key, to_wxid, base, text) | ||
| 248 | + if last == 2: | ||
| 249 | + return {"ok": True, "reason": "verified(status=2)"} | ||
| 250 | + if last == 5: | ||
| 251 | + return {"ok": False, "reason": "status=5(微信报失败)"} | ||
| 252 | + if time.time() >= deadline: | ||
| 253 | + break | ||
| 254 | + time.sleep(VERIFY_POLL) | ||
| 255 | + # 超时判定:多轮切换仍卡 status=1 = 微信连接慢/离线,最终多半会 flush(网络恢复时批量 | ||
| 256 | + # 派发)→ 视为已入队不误报失败。仅 status=None(消息没创建,hook 没处理)才算真失败。 | ||
| 246 | if last == 1: | 257 | if last == 1: |
| 247 | - return {"ok": True, "reason": "queued(status=1,已入队将送达)"} | 258 | + return {"ok": True, "reason": "queued(status=1,多轮切换仍未flush,网络恢复后送达)"} |
| 248 | return {"ok": False, "reason": f"消息未创建(status={last})"} | 259 | return {"ok": False, "reason": f"消息未创建(status={last})"} |
| 249 | 260 | ||
| 250 | 261 |