Skip to content

Graila/send target open fix - #171

Open
fieldyyuyang wants to merge 14 commits into
thisnick:mainfrom
fieldyyuyang:graila/send-target-open-fix
Open

Graila/send target open fix#171
fieldyyuyang wants to merge 14 commits into
thisnick:mainfrom
fieldyyuyang:graila/send-target-open-fix

Conversation

@fieldyyuyang

Copy link
Copy Markdown

No description provided.

Fieldy YuYang and others added 14 commits June 17, 2026 02:05
…_dir (issue thisnick#153)

find_account_dir only scanned the main WeChat PID's /proc/<pid>/fd; when the DB
fds are held by a helper/renderer process it returned None, so login never
persisted logged_in_user and /api/chats returned empty. Add: scan all
WeChat-related PIDs, then a filesystem fallback over xwechat_files/* (account
dirs with core DBs, newest mtime wins). Redacted diagnostic logging + unit tests
for the pure helpers. login.rs: loud warn on detection give-up (control flow
unchanged to preserve the ws/login contract).
…in our build network)

The build network has no proxy inside the Docker build, so curl-from-github for
noVNC and sqlcipher truncates. Vendor both tarballs into the build context
(COPY) instead; pre-fetch commands documented in the Dockerfile. WeChat .deb
(Tencent CDN) still downloads in-build fine. gitignore the vendored binaries +
staged rust source.
…-nobell

Recover the data plane when WeChat is logged in but session.logged_in_user
was never persisted (issue thisnick#153). Two real login paths (phone-confirm, and
connecting /api/ws/login while already logged in) bypass the LoginPlan
DetectingUser phase, so find_account_dir is never called and /api/chats etc.
return empty.

- POST /api/sync/rescan (token-protected): resolve wechat pid -> patched
  find_account_dir_with_method -> persist logged_in_user -> trigger key
  extraction. Reuses the exact helpers the LoginPlan uses. Redacted JSON +
  logs (no wxid/path/key/token).
- find_account_dir_with_method: expose detection method for observability.
- x11vnc -nobell in entrypoint.sh + sessions/manager.rs: suppress the
  noVNC browser bell WeChat triggers on notifications.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The WeChat version-update popup is a separate top-level X window that the
a11y dismiss_popup (scoped to the main window tree) does not catch; left
open it obscures the UI and disrupts login/rescan/adapter.

- tools/ui_popups::close_known_popups: enumerate visible windows via the
  no-shell exec_command xdotool wrapper, match a tight whitelist (name
  Weixin/微信/WeChat + geometry ~500-650x350-500) guarded by 'a larger main
  Weixin window also present', click only the top-right close X once each,
  re-verify. Redacted logs/response (no titles/screenshots/chat data).
- POST /api/ui/close-known-popups (token-protected) returns
  {status, popup_detected, popup_closed, closed_count, windows_seen,
   popup_type, warning?}.
- /api/sync/rescan calls the closer first so popups can't block detection.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The frida chat-select fast-path hard-fails on WeChat builds without a
BUILD_PROFILE (e.g. 4.1.1.7 / 7b3f07cc), killing send at 'No action selected'.
Add a version-robust fallback that opens a chat the way a human does:

- tools/ui_open_chat::open_chat_a11y_search: resolve chatId -> display name via
  the decrypted DB (get_chat_by_username), then drive WeChat's search box via
  a11y-dump + xdotool (focus, clear, type name, click first result), and confirm
  via the message composer (Send button) being present. No frida offsets; no
  shell interpolation. Redacted logs/response (never name/id/content).
- POST /api/ui/open-chat (token-protected): {chatId, dryRun} -> redacted
  {status, method, *_present, result_clicked, open_confirmed, error}, with
  specific error codes.
- SendMessagePlan Opening: on chat-select failure, fall back to a11y search and
  return a specific error instead of generic 'No action selected'. Frida hook
  kept as the fast-path when a profile exists.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
WeChat's composer send button is 'Send(S)' on EN clients but '发送(S)' on ZH
clients (the EMDE locale). Hardcoding the English name made open-confirmation
(AW-FORK-7 open_confirmed=false) and the send Confirming phase false-negative.

- ia/selectors::is_send_button_name(name): match 'send' (any case) OR '发送';
  shared helper + unit test.
- Use it in plans/send_message::find_edit_send_pair, plans/chat_open
  find_edit_near_send, and tools/ui_open_chat::chat_is_open (which now also logs
  redacted editable/send-button counts). Dropped the English-only Send(S)
  query_selector gate in send_message. No behavior change beyond locale.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
AW-FORK-7B showed open_confirmed=false because the open clicked the wrong list:
WeChat 4.1.1 renders search results as a SEPARATE deeper list (depth ~15, few
items) vs the main chat-list (shallow depth, many items). The old
find_first_result picked the global topmost list-item -> a main-chat-list row,
so the target never opened.

- find_search_box_node: locate the search box WITH its tree depth (prefer
  FOCUSED editable).
- select_result_first_item: pick the first row of a list that is deeper than the
  search box, has a modest item count (<10), and sits at/below the search box —
  excludes the main chat-list. Redacted diagnostics: lists_count /
  candidate_lists / selected_depth / selected_items.
- keyboard fallback: if no results list is distinguishable, or a click didn't
  confirm, press Down+Return (search box focused) to open the first result.
  Logs keyboard_fallback=true/false. No names/ids/content logged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
AW-FORK-8 produced 2 outgoing messages from 1 authorized send: the send-path
open used the a11y keyboard fallback (Down+Return) which, with the composer
focused, injected a stray message before the real one.

- OpenChatOptions { dry_run, allow_keyboard_fallback, send_safe } +
  open_chat_a11y_search_with_options. Endpoint keeps fallback
  (OpenChatOptions::endpoint); send path uses OpenChatOptions::send_safe
  (open_chat_a11y_search_send_safe): click-only, NEVER presses Return, fails
  closed (keyboard_fallback_suppressed / open_not_confirmed_send_safe).
- SendMessagePlan Opening now requires fb.open_confirmed (not mere
  result_clicked) and uses the send-safe open.
- SendMessagePlanState.send_action_executed guard: a plan run emits at most one
  send action (send_action_guard_triggered). composer_not_found log added.
- Redacted logs only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
AW-FORK-8C: the standalone open-chat keyboard fallback (Down+Return) sent a stray
when a composer was focused; and the send-safe open could not complete when the
chat was already open (candidate_lists=0).

- focused_editable_is_search_box + safe_keyboard_open: NEVER press Return unless
  the FOCUSED editable node is the search box. Both keyboard-fallback sites
  (including the standalone endpoint) route through it; on unsafe focus it returns
  error keyboard_fallback_unsafe_focus and does not press Return.
- SendMessagePlan Opening: if mainWindow=chat_open and a composer pair is already
  present, skip the a11y re-search entirely (already_chat_open skip_reopen) — this
  is the AW-FORK-8C state and avoids both the flaky re-search and re-typing into a
  focused composer. Send path still forbids keyboard fallback.
- Doc note on /api/ui/open-chat: do not use dryRun:false as a pre-send precheck.
- Redacted logs only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
AW-FORK-19: SendMessagePlan no longer reuses whatever chat is already open on a
send. The `already_chat_open => skip_reopen` shortcut assumed any present composer
was the target and mis-routed a group send into a still-open private chat
(AW-FORK-18E). Now every send opens the intended target by name via the send-safe
a11y search (click-only, never Return) and fails closed if the target open can't
be confirmed. Decision routed through decide_send_open_policy() with a regression
test: composer-present-alone => ForceOpenTarget.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
AW-FORK-20: when a send-safe open runs while ANOTHER chat is already open, the
post-type search-results list can't be distinguished from the main chat list
(candidate_lists=0) and the open fails closed (AW-FORK-19C). Before searching,
if a chat is open, press Escape to return to the no-chat-open layout where the
results list is detectable, then re-locate the search box. Escape never sends and
we still never press Enter or type into a composer; if it can't clear, the
candidate_lists=0 guard still fails closed (no mis-route). Gated via
needs_pre_search_unfocus(send_safe, chat_open) with unit tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
AW-FORK-21: when select_result_first_item finds no candidate search-results list
(candidate_lists=0, the chat-open layout that fails closed in AW-FORK-19C/20B),
log each list's depth/rect/item-count and which of the 3 heuristic conditions
(deeper-than-search, items<10, first-at-or-below-search) excluded it, plus
search_depth/search_y. Sanitized (numbers/booleans only). Diagnostic only — no
behaviour change to the selection.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
AW-FORK-21B: find_search_box_node now selects the TOPMOST editable (the WeChat
search field at the top-left) via choose_search_editable(), instead of
focused.or(topmost). While a chat is open the bottom composer is EDITABLE+FOCUSED;
the old logic picked it, so the resolved name was typed into the composer and no
search results appeared (candidate_lists=0, AW-FORK-21). Now a focused bottom
composer never wins. Adds search_box_policy=topmost_editable log + unit tests
(topmost beats focused-bottom; focused doesn't auto-win). Send-safe invariants
unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
AW-FORK-22: the open path now clicks the search-result row whose normalized name
EXACTLY matches the resolved target (select_result_matching_target +
pick_matching_row), instead of the blind first row that strayed a group send to a
wrong private chat (§63). System chats (File Transfer / Weixin Team / 文件传输助手
/ 微信团队) are denylisted. No unique match => fail closed (no click, no keyboard
fallback) in both send-safe and endpoint modes; the keyboard fallback is removed.
Adds normalize_name/is_denied_system_chat/pick_matching_row + unit tests
(exact-not-first-row, denylist-skipped, no-match/ambiguous fail closed).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant