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,7 +414,9 @@ async function callHermesRunStream(messages, sessionId, onUtterance) { | ||
| 414 | } | 414 | } |
| 415 | }); | 415 | }); |
| 416 | res.on('end', () => { | 416 | res.on('end', () => { |
| 417 | - flush(true); | 417 | + // SSE 流结束兜底:把残留的 reasoning / content flush 掉 |
| 418 | + if (CONFIG.streamReasoning) flushReasoningBuf(true); | ||
| 419 | + flushTextBuf(true); | ||
| 418 | sendChain.then(() => resolve(finalResult)); | 420 | sendChain.then(() => resolve(finalResult)); |
| 419 | }); | 421 | }); |
| 420 | res.on('error', reject); | 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,7 +650,9 @@ public class HermesAccessibilityService extends AccessibilityService { | ||
| 650 | return; | 650 | return; |
| 651 | } | 651 | } |
| 652 | 652 | ||
| 653 | - AccessibilityNodeInfo memberNode = findClickableByText(root, pendingAtSender); | 653 | + // 选人面板里成员节点文本可能是 "昵称: ZZ" / "ZZ" / 带其它前缀, |
| 654 | + // 严格 equals 匹配不上,改用"包含"模糊匹配 | ||
| 655 | + AccessibilityNodeInfo memberNode = findMentionMember(root, pendingAtSender); | ||
| 654 | if (memberNode != null) { | 656 | if (memberNode != null) { |
| 655 | Log.i(TAG, "Clicking @ member: " + pendingAtSender); | 657 | Log.i(TAG, "Clicking @ member: " + pendingAtSender); |
| 656 | memberNode.performAction(AccessibilityNodeInfo.ACTION_CLICK); | 658 | memberNode.performAction(AccessibilityNodeInfo.ACTION_CLICK); |
| @@ -662,7 +664,6 @@ public class HermesAccessibilityService extends AccessibilityService { | @@ -662,7 +664,6 @@ public class HermesAccessibilityService extends AccessibilityService { | ||
| 662 | AccessibilityNodeInfo r = getRootInActiveWindow(); | 664 | AccessibilityNodeInfo r = getRootInActiveWindow(); |
| 663 | AccessibilityNodeInfo freshEdit = (r != null) ? findInnerEditText(r) : null; | 665 | AccessibilityNodeInfo freshEdit = (r != null) ? findInnerEditText(r) : null; |
| 664 | if (freshEdit != null) { | 666 | if (freshEdit != null) { |
| 665 | - // 拼接 pendingContent 到现有 @xx 后面(不要覆盖 @ chip) | ||
| 666 | appendToEditText(freshEdit, " " + pendingContent); | 667 | appendToEditText(freshEdit, " " + pendingContent); |
| 667 | freshEdit.recycle(); | 668 | freshEdit.recycle(); |
| 668 | } else if (edit != null) { | 669 | } else if (edit != null) { |
| @@ -679,22 +680,62 @@ public class HermesAccessibilityService extends AccessibilityService { | @@ -679,22 +680,62 @@ public class HermesAccessibilityService extends AccessibilityService { | ||
| 679 | }, 400); | 680 | }, 400); |
| 680 | }, 500); | 681 | }, 500); |
| 681 | } else { | 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 | root.recycle(); | 688 | root.recycle(); |
| 684 | pendingAtSender = null; | 689 | pendingAtSender = null; |
| 685 | - // edit 引用可能在 ACTION_SET_TEXT("@") + postDelayed 后过期, | ||
| 686 | - // 重新从当前 root 拿 EditText 再 set text | ||
| 687 | if (edit != null) edit.recycle(); | 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 | private void typeFinalContent(AccessibilityNodeInfo edit) { | 741 | private void typeFinalContent(AccessibilityNodeInfo edit) { |