From 0e8f419d800a9f0cf6342acbde95c7b0dd7422c0 Mon Sep 17 00:00:00 2001 From: yibo Date: Sun, 24 May 2026 21:30:33 +0800 Subject: [PATCH] feat: A11y 走 chat list 优先路径 + 群 @ 用昵称定位选人面板 --- server/services/wechat/auto-reply.js | 11 ++++++++++- wechat_plain/scripts/lsposed-wechat-sendmsg/app/src/main/java/com/hermes/wechat/xposed/accessibility/HermesAccessibilityService.java | 100 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 101 insertions(+), 10 deletions(-) diff --git a/server/services/wechat/auto-reply.js b/server/services/wechat/auto-reply.js index bf9b8d3..96d5744 100644 --- a/server/services/wechat/auto-reply.js +++ b/server/services/wechat/auto-reply.js @@ -562,7 +562,16 @@ class AutoReplyBot { // 取前若干字符作为 substring 匹配;含 XML/特殊字符的部分(如 refermsg)跳过 const trimmed = bubbleText.replace(/<[^>]+>/g, '').trim(); if (trimmed) sendOpts.quoteText = trimmed.substring(0, 30); - if (senderName) sendOpts.atSender = senderName; + // senderName 是 wxid(如 'yibooo'),群里 @ 面板显示的是昵称(如 '火星人'), + // 必须查 rcontact 拿到昵称,否则 A11y 在选人面板里搜不到 + if (senderName) { + try { + const senderNick = await this.tracker.getContactNickname(senderName); + sendOpts.atSender = senderNick || senderName; + } catch (_) { + sendOpts.atSender = senderName; + } + } log('[GROUP] quote=' + (sendOpts.quoteText || '-') + ' atSender=' + (sendOpts.atSender || '-')); } 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 e8301c6..3f0af15 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 @@ -41,6 +41,8 @@ public class HermesAccessibilityService extends AccessibilityService { private static final String ID_CHAT_SEND = "com.tencent.mm:id/bql"; // 发送按钮 (Button, text="发送") private static final String ID_CHAT_TITLE = "com.tencent.mm:id/obn"; // 聊天界面标题栏 (TextView) private static final String ID_BUBBLE_TEXT = "com.tencent.mm:id/bkl"; // 消息气泡 TextView (long-clickable=true) + private static final String ID_CHATLIST_ROW = "com.tencent.mm:id/cj1"; // 主界面 chat list 每行的 clickable 容器 (LinearLayout) + private static final String ID_CHATLIST_NAME = "com.tencent.mm:id/kbq"; // chat list 行里显示会话名 / 群名 的 TextView // 长按气泡后弹出的菜单项文案(WeChat 8.0.72 简中) private static final String MENU_QUOTE_TEXT = "引用"; @@ -140,11 +142,6 @@ public class HermesAccessibilityService extends AccessibilityService { } else { if (edit != null) { Log.i(TAG, "In wrong chat (title mismatch), restarting WeChat main screen"); - } else { - Log.i(TAG, "Not in chat, starting search"); - } - // If in wrong chat, relaunch WeChat main screen first - if (edit != null) { Intent relaunch = getPackageManager().getLaunchIntentForPackage("com.tencent.mm"); if (relaunch != null) { relaunch.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); @@ -153,12 +150,12 @@ public class HermesAccessibilityService extends AccessibilityService { mainHandler.postDelayed(() -> { AccessibilityNodeInfo mainRoot = getRootInActiveWindow(); if (mainRoot != null) { - performSearch(mainRoot); + enterChatViaListOrSearch(mainRoot); mainRoot.recycle(); } }, 1500); } else { - performSearch(root); + enterChatViaListOrSearch(root); } } root.recycle(); @@ -167,7 +164,81 @@ public class HermesAccessibilityService extends AccessibilityService { }; // ===================================================================== - // Search for contact + // Enter chat via chat list (preferred) — fallback to search + // ===================================================================== + + /** + * 优先方案:在 LauncherUI 的 chat list 里找显示名匹配的行,直接点击进入。 + * 比搜索路径稳得多 — 不会触发"创建群聊"歧义(搜索"丁鹏、🍋火星人、火星人" + * 会被 WeChat 解读成"创建包含这 3 人的新群"建议)。 + * + * 失败时(chat 不在可见列表里 / 不在 LauncherUI 等)回退到搜索路径。 + */ + private void enterChatViaListOrSearch(AccessibilityNodeInfo root) { + // 1) 找 chat list 里所有 kbq TextView,按 text 严格匹配 displayName + List names = root.findAccessibilityNodeInfosByViewId(ID_CHATLIST_NAME); + Log.d(TAG, "ChatList scan: found " + (names == null ? 0 : names.size()) + " kbq names, target=" + pendingToUserDisplayName); + AccessibilityNodeInfo matchedRow = null; + if (names != null) { + for (AccessibilityNodeInfo n : names) { + CharSequence t = n.getText(); + if (matchedRow == null && t != null && pendingToUserDisplayName.equals(t.toString())) { + // 向上找 cj1 (clickable row container) + AccessibilityNodeInfo cur = n; + while (cur != null) { + CharSequence id = cur.getViewIdResourceName(); + if (id != null && ID_CHATLIST_ROW.equals(id.toString())) { + matchedRow = cur; + break; + } + AccessibilityNodeInfo parent = cur.getParent(); + if (cur != n) cur.recycle(); + cur = parent; + } + // fallback: 没找到 cj1 就用任意 clickable 祖先 + if (matchedRow == null) { + AccessibilityNodeInfo c = n; + while (c != null) { + if (c.isClickable()) { matchedRow = c; break; } + AccessibilityNodeInfo p = c.getParent(); + if (c != n) c.recycle(); + c = p; + } + } + continue; // matched, don't recycle n + } + n.recycle(); + } + } + + if (matchedRow != null) { + Log.i(TAG, "ChatList: clicking row for " + pendingToUserDisplayName); + matchedRow.performAction(AccessibilityNodeInfo.ACTION_CLICK); + matchedRow.recycle(); + + // 等聊天页加载,bkk 出现后开始 quote/@/text 流程 + mainHandler.postDelayed(() -> { + AccessibilityNodeInfo chatRoot = getRootInActiveWindow(); + if (chatRoot != null) { + if (findNodeById(chatRoot, ID_CHAT_EDIT) != null) { + Log.i(TAG, "Chat UI loaded from list, proceeding to type/send"); + performTypeAndSend(chatRoot); + } else { + Log.w(TAG, "Chat UI not loaded after list click, falling back to search"); + performSearch(chatRoot); + } + chatRoot.recycle(); + } + }, 2200); + return; + } + + Log.i(TAG, "ChatList: no match for " + pendingToUserDisplayName + " in visible list, falling back to search"); + performSearch(root); + } + + // ===================================================================== + // Search for contact (fallback path) // ===================================================================== private void performSearch(AccessibilityNodeInfo root) { @@ -508,7 +579,18 @@ public class HermesAccessibilityService extends AccessibilityService { Log.w(TAG, "@ member not found in picker: " + pendingAtSender + " — fallback to plain content"); root.recycle(); pendingAtSender = null; - typeFinalContent(edit); + // 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(); + } } } -- libgit2 0.21.4