Commit 4586bf6b071c03dbbe9eb35ce5080dd9bc6737e2

Authored by yibo
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>
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) {