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 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) {
... ...