diff --git a/server/services/wechat/auto-reply.js b/server/services/wechat/auto-reply.js index 1a7e32a..d3d1a35 100644 --- a/server/services/wechat/auto-reply.js +++ b/server/services/wechat/auto-reply.js @@ -414,7 +414,9 @@ async function callHermesRunStream(messages, sessionId, onUtterance) { } }); res.on('end', () => { - flush(true); + // SSE 流结束兜底:把残留的 reasoning / content flush 掉 + if (CONFIG.streamReasoning) flushReasoningBuf(true); + flushTextBuf(true); sendChain.then(() => resolve(finalResult)); }); res.on('error', reject); diff --git a/wechat_plain/scripts/lsposed-wechat-sendmsg/app/src/main/java/com/hermes/wechat/xposed/accessibility/HermesAccessibilityService.java b/wechat_plain/scripts/lsposed-wechat-sendmsg/app/src/main/java/com/hermes/wechat/xposed/accessibility/HermesAccessibilityService.java index 6b37a57..7be720c 100644 --- a/wechat_plain/scripts/lsposed-wechat-sendmsg/app/src/main/java/com/hermes/wechat/xposed/accessibility/HermesAccessibilityService.java +++ b/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 { return; } - AccessibilityNodeInfo memberNode = findClickableByText(root, pendingAtSender); + // 选人面板里成员节点文本可能是 "昵称: ZZ" / "ZZ" / 带其它前缀, + // 严格 equals 匹配不上,改用"包含"模糊匹配 + AccessibilityNodeInfo memberNode = findMentionMember(root, pendingAtSender); if (memberNode != null) { Log.i(TAG, "Clicking @ member: " + pendingAtSender); memberNode.performAction(AccessibilityNodeInfo.ACTION_CLICK); @@ -662,7 +664,6 @@ public class HermesAccessibilityService extends AccessibilityService { AccessibilityNodeInfo r = getRootInActiveWindow(); AccessibilityNodeInfo freshEdit = (r != null) ? findInnerEditText(r) : null; if (freshEdit != null) { - // 拼接 pendingContent 到现有 @xx 后面(不要覆盖 @ chip) appendToEditText(freshEdit, " " + pendingContent); freshEdit.recycle(); } else if (edit != null) { @@ -679,22 +680,62 @@ public class HermesAccessibilityService extends AccessibilityService { }, 400); }, 500); } else { - Log.w(TAG, "@ member not found in picker: " + pendingAtSender + " — fallback to plain content"); + // 关键:没找到成员时,当前停在"选择提醒的人"整页 / 半屏页上, + // 直接 typeFinalContent 会因为找不到 bkk 卡死在选人页。 + // 必须先 GLOBAL_ACTION_BACK 退出选人页回到聊天页,再写文本。 + Log.w(TAG, "@ member not found in picker: " + pendingAtSender + + " — pressing BACK to exit picker, then plain content"); root.recycle(); pendingAtSender = null; - // edit 引用可能在 ACTION_SET_TEXT("@") + postDelayed 后过期, - // 重新从当前 root 拿 EditText 再 set text if (edit != null) edit.recycle(); - AccessibilityNodeInfo r = getRootInActiveWindow(); - AccessibilityNodeInfo freshEdit = (r != null) ? findInnerEditText(r) : null; - if (r != null) r.recycle(); - if (freshEdit != null) { - typeFinalContent(freshEdit); - } else { - Log.w(TAG, "Fresh edit not found after @ fallback, abort"); - resetState(); + performGlobalAction(GLOBAL_ACTION_BACK); + mainHandler.postDelayed(() -> { + AccessibilityNodeInfo r = getRootInActiveWindow(); + AccessibilityNodeInfo freshEdit = (r != null) ? findInnerEditText(r) : null; + if (r != null) r.recycle(); + if (freshEdit != null) { + // @ 已经把 "@" 字符留在输入框,typeFinalContent 用 SET_TEXT 会整体覆盖 + typeFinalContent(freshEdit); + } else { + Log.w(TAG, "Fresh edit not found after BACK from picker, abort"); + resetState(); + } + }, 800); + } + } + + /** + * 在 @ 选人面板里找匹配 name 的可点击成员。 + * 面板里成员名节点文本形如 "昵称: ZZ" / "ZZ" / "火星人",可能带前缀, + * 用包含匹配 + 向上找 clickable 祖先(list item)。 + */ + private AccessibilityNodeInfo findMentionMember(AccessibilityNodeInfo root, String name) { + if (root == null || name == null || name.isEmpty()) return null; + // 先试严格的 findClickableByText(最精确) + AccessibilityNodeInfo exact = findClickableByText(root, name); + if (exact != null) return exact; + + // 再试包含匹配:遍历 findByText(name) 命中的节点,向上找 clickable + List nodes = root.findAccessibilityNodeInfosByText(name); + if (nodes == null) return null; + AccessibilityNodeInfo result = null; + for (AccessibilityNodeInfo n : nodes) { + if (result == null) { + CharSequence t = n.getText(); + if (t != null && t.toString().contains(name)) { + AccessibilityNodeInfo cur = n; + while (cur != null) { + if (cur.isClickable()) { result = cur; break; } + AccessibilityNodeInfo p = cur.getParent(); + if (cur != n) cur.recycle(); + cur = p; + } + if (result != null) continue; // 别 recycle n(在 result 链上) + } } + n.recycle(); } + return result; } private void typeFinalContent(AccessibilityNodeInfo edit) {