Commit 4586bf6b071c03dbbe9eb35ce5080dd9bc6737e2
1 parent
7e5f1ccf
fix: 修 res.on(end) 残留 flush() 引用 + @ 选人卡死
#1 auto-reply.js: callHermesRunStream 的 res.on('end') 还在调旧的 flush()
(重构成 flushTextBuf/emit 时漏改),触发 ReferenceError: flush is not defined
→ 之前直接崩 node(被 keepalive 重启打断 run)。改用 flushTextBuf/flushReasoningBuf。
(上一个 commit 的 unhandledRejection 兜底捕获到了这个真凶)
#2 HermesAccessibilityService @ 选人卡死:
- 成员名匹配:选人面板节点文本是"昵称: ZZ"等带前缀格式,严格 equals
匹配不上 → 新增 findMentionMember 做包含匹配 + 向上找 clickable 祖先
- 找不到成员时 fallback:之前直接 typeFinalContent 但仍停在"选择提醒的人"
整页/半屏页,找不到 bkk 输入框 → 卡死。改成先 GLOBAL_ACTION_BACK 退出
选人页回到聊天页,800ms 后再 findInnerEditText + typeFinalContent
实测:宜创群回复曾卡在 HalfScreenTransparentActivity(选择提醒的人),
要 @ 的 ZZ 在面板显示为"昵称: ZZ",严格匹配失败 + 无退出逻辑 → 卡死。
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Showing
2 changed files
with
57 additions
and
14 deletions
server/services/wechat/auto-reply.js
| ... | ... | @@ -414,7 +414,9 @@ async function callHermesRunStream(messages, sessionId, onUtterance) { |
| 414 | 414 | } |
| 415 | 415 | }); |
| 416 | 416 | res.on('end', () => { |
| 417 | - flush(true); | |
| 417 | + // SSE 流结束兜底:把残留的 reasoning / content flush 掉 | |
| 418 | + if (CONFIG.streamReasoning) flushReasoningBuf(true); | |
| 419 | + flushTextBuf(true); | |
| 418 | 420 | sendChain.then(() => resolve(finalResult)); |
| 419 | 421 | }); |
| 420 | 422 | res.on('error', reject); | ... | ... |
wechat_plain/scripts/lsposed-wechat-sendmsg/app/src/main/java/com/hermes/wechat/xposed/accessibility/HermesAccessibilityService.java
| ... | ... | @@ -650,7 +650,9 @@ public class HermesAccessibilityService extends AccessibilityService { |
| 650 | 650 | return; |
| 651 | 651 | } |
| 652 | 652 | |
| 653 | - AccessibilityNodeInfo memberNode = findClickableByText(root, pendingAtSender); | |
| 653 | + // 选人面板里成员节点文本可能是 "昵称: ZZ" / "ZZ" / 带其它前缀, | |
| 654 | + // 严格 equals 匹配不上,改用"包含"模糊匹配 | |
| 655 | + AccessibilityNodeInfo memberNode = findMentionMember(root, pendingAtSender); | |
| 654 | 656 | if (memberNode != null) { |
| 655 | 657 | Log.i(TAG, "Clicking @ member: " + pendingAtSender); |
| 656 | 658 | memberNode.performAction(AccessibilityNodeInfo.ACTION_CLICK); |
| ... | ... | @@ -662,7 +664,6 @@ public class HermesAccessibilityService extends AccessibilityService { |
| 662 | 664 | AccessibilityNodeInfo r = getRootInActiveWindow(); |
| 663 | 665 | AccessibilityNodeInfo freshEdit = (r != null) ? findInnerEditText(r) : null; |
| 664 | 666 | if (freshEdit != null) { |
| 665 | - // 拼接 pendingContent 到现有 @xx 后面(不要覆盖 @ chip) | |
| 666 | 667 | appendToEditText(freshEdit, " " + pendingContent); |
| 667 | 668 | freshEdit.recycle(); |
| 668 | 669 | } else if (edit != null) { |
| ... | ... | @@ -679,22 +680,62 @@ public class HermesAccessibilityService extends AccessibilityService { |
| 679 | 680 | }, 400); |
| 680 | 681 | }, 500); |
| 681 | 682 | } else { |
| 682 | - Log.w(TAG, "@ member not found in picker: " + pendingAtSender + " — fallback to plain content"); | |
| 683 | + // 关键:没找到成员时,当前停在"选择提醒的人"整页 / 半屏页上, | |
| 684 | + // 直接 typeFinalContent 会因为找不到 bkk 卡死在选人页。 | |
| 685 | + // 必须先 GLOBAL_ACTION_BACK 退出选人页回到聊天页,再写文本。 | |
| 686 | + Log.w(TAG, "@ member not found in picker: " + pendingAtSender | |
| 687 | + + " — pressing BACK to exit picker, then plain content"); | |
| 683 | 688 | root.recycle(); |
| 684 | 689 | pendingAtSender = null; |
| 685 | - // edit 引用可能在 ACTION_SET_TEXT("@") + postDelayed 后过期, | |
| 686 | - // 重新从当前 root 拿 EditText 再 set text | |
| 687 | 690 | if (edit != null) edit.recycle(); |
| 688 | - AccessibilityNodeInfo r = getRootInActiveWindow(); | |
| 689 | - AccessibilityNodeInfo freshEdit = (r != null) ? findInnerEditText(r) : null; | |
| 690 | - if (r != null) r.recycle(); | |
| 691 | - if (freshEdit != null) { | |
| 692 | - typeFinalContent(freshEdit); | |
| 693 | - } else { | |
| 694 | - Log.w(TAG, "Fresh edit not found after @ fallback, abort"); | |
| 695 | - resetState(); | |
| 691 | + performGlobalAction(GLOBAL_ACTION_BACK); | |
| 692 | + mainHandler.postDelayed(() -> { | |
| 693 | + AccessibilityNodeInfo r = getRootInActiveWindow(); | |
| 694 | + AccessibilityNodeInfo freshEdit = (r != null) ? findInnerEditText(r) : null; | |
| 695 | + if (r != null) r.recycle(); | |
| 696 | + if (freshEdit != null) { | |
| 697 | + // @ 已经把 "@" 字符留在输入框,typeFinalContent 用 SET_TEXT 会整体覆盖 | |
| 698 | + typeFinalContent(freshEdit); | |
| 699 | + } else { | |
| 700 | + Log.w(TAG, "Fresh edit not found after BACK from picker, abort"); | |
| 701 | + resetState(); | |
| 702 | + } | |
| 703 | + }, 800); | |
| 704 | + } | |
| 705 | + } | |
| 706 | + | |
| 707 | + /** | |
| 708 | + * 在 @ 选人面板里找匹配 name 的可点击成员。 | |
| 709 | + * 面板里成员名节点文本形如 "昵称: ZZ" / "ZZ" / "火星人",可能带前缀, | |
| 710 | + * 用包含匹配 + 向上找 clickable 祖先(list item)。 | |
| 711 | + */ | |
| 712 | + private AccessibilityNodeInfo findMentionMember(AccessibilityNodeInfo root, String name) { | |
| 713 | + if (root == null || name == null || name.isEmpty()) return null; | |
| 714 | + // 先试严格的 findClickableByText(最精确) | |
| 715 | + AccessibilityNodeInfo exact = findClickableByText(root, name); | |
| 716 | + if (exact != null) return exact; | |
| 717 | + | |
| 718 | + // 再试包含匹配:遍历 findByText(name) 命中的节点,向上找 clickable | |
| 719 | + List<AccessibilityNodeInfo> nodes = root.findAccessibilityNodeInfosByText(name); | |
| 720 | + if (nodes == null) return null; | |
| 721 | + AccessibilityNodeInfo result = null; | |
| 722 | + for (AccessibilityNodeInfo n : nodes) { | |
| 723 | + if (result == null) { | |
| 724 | + CharSequence t = n.getText(); | |
| 725 | + if (t != null && t.toString().contains(name)) { | |
| 726 | + AccessibilityNodeInfo cur = n; | |
| 727 | + while (cur != null) { | |
| 728 | + if (cur.isClickable()) { result = cur; break; } | |
| 729 | + AccessibilityNodeInfo p = cur.getParent(); | |
| 730 | + if (cur != n) cur.recycle(); | |
| 731 | + cur = p; | |
| 732 | + } | |
| 733 | + if (result != null) continue; // 别 recycle n(在 result 链上) | |
| 734 | + } | |
| 696 | 735 | } |
| 736 | + n.recycle(); | |
| 697 | 737 | } |
| 738 | + return result; | |
| 698 | 739 | } |
| 699 | 740 | |
| 700 | 741 | private void typeFinalContent(AccessibilityNodeInfo edit) { | ... | ... |