Skip to content

fix: detect @-mentions in type-49 (appmsg) content XML - #145

Merged
thisnick merged 1 commit into
thisnick:mainfrom
kyan-du:fix/type49-mention-detection
Apr 1, 2026
Merged

fix: detect @-mentions in type-49 (appmsg) content XML#145
thisnick merged 1 commit into
thisnick:mainfrom
kyan-du:fix/type49-mention-detection

Conversation

@kyan-du

@kyan-du kyan-du commented Mar 28, 2026

Copy link
Copy Markdown
Contributor

Problem

For reference/quote messages (type=49, i.e. when a user long-presses a message and replies with a quote), WeChat places <atuserlist> inside the content XML column rather than the source column.

The current check_is_mentioned() only inspects the source XML, so @-mentions in quoted replies are silently missed — the message gets buffered but never dispatched.

Fix

After checking the source XML, fall back to checking the content XML for type-49 messages. This ensures quoted replies containing @-mentions are properly detected.

Impact

  • Only affects type-49 (appmsg) messages in group chats
  • No behavior change for normal text messages (type=1) where source XML already works
  • Minimal performance impact: one extra XML tag scan only when source check returns false and message is type-49

Related

Related to #126 — both stem from incomplete handling of type-49 (appmsg) messages. This PR fixes mention detection; #126 covers the broader issue of parsing combined/forwarded message content.

For reference/quote messages (type=49), WeChat may place <atuserlist>
inside the content XML rather than the source column. This caused
@-mentions in quoted replies to be silently missed.

Now falls back to checking content XML for type-49 messages when
source XML has no mention, ensuring quoted replies with @-mentions
are properly detected.

Fixes: group chat replies via quote not triggering bot response
@thisnick
thisnick merged commit 5ab79ba into thisnick:main Apr 1, 2026
4 checks passed
kyan-du added a commit to kyan-du/agent-wechat that referenced this pull request Aug 13, 2026
* Log WeChat crash/recovery in health monitor (#115)

The health monitor silently ignored when WeChat's process disappeared
(segfault, etc.) — no logs were emitted. Now it logs when the process
disappears and when it comes back.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* fix: correct status display for WeChat connection (fixes #74) (#114)

Two changes:

1. status.ts: `!snapshot.connected` → `snapshot.connected === false`
   The CLI-side status check builds snapshots without runtime state, so
   `connected` is undefined. The falsy check treated undefined as
   disconnected, reporting WARN even when WeChat was working.

2. channel.ts: Add status.buildAccountSnapshot hook.
   Merges monitor runtime state (connected, linked, running) into
   daemon-side snapshots. Without this, the gateway only included static
   config fields in the snapshot passed to collectWeChatStatusIssues.

Added status.test.ts with 7 test cases covering connected/disconnected/
undefined/auth/linked scenarios.

Co-authored-by: Kyan Du <kyan.du@icloud.com>

* Add changeset for health monitor crash logging (#115)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Version Packages (#112)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* Move WeChat restart from entrypoint to health monitor (#116)

* Move WeChat restart from entrypoint to health monitor

The entrypoint bash restart loop was unreliable — WeChat would crash
and not get restarted. Move the restart logic into the agent-server
health monitor which already detects when the process disappears.

The health monitor now spawns WeChat directly when it detects the
process is gone, with the same crash loop protection (5 restarts
within 60s triggers a 30s backoff).

The entrypoint still does the initial launch but no longer loops.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Extract shared launch-wechat.sh script for entrypoint and health monitor

Both the entrypoint (initial launch) and health monitor (restart after
crash) now call the same script, so WeChat env vars stay in one place.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Move launch-wechat script to docker/tools/ (on PATH)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* Version Packages (#117)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* Replace x86_64 NativeFunction lookup with pure memory hashmap walk (#118)

The enumerate_sessions step previously called a WeChat function
(0x392c1d0) from Frida's thread to find the session vector. This
risked corrupting shared state and crashing WeChat. Now walks the
unordered_map linked list with pure pointer reads instead.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* Version Packages (#119)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* Move server setup to first step in wechat extension README (#120)

The server needs to be running before installing the extension or
configuring the channel, so it belongs as step 1 in the setup flow.

https://claude.ai/code/session_016yXZUhTChJsbPR7mENH3x9

Co-authored-by: Claude <noreply@anthropic.com>

* Remove port 5900 references from docs — VNC web viewer is served at /vnc/ (#122)

Port 5900 (raw VNC) is no longer exposed to the host. The browser-based
VNC viewer is proxied through the agent server at /vnc/ on port 6174.

https://claude.ai/code/session_019cYxiPfAUR2YYTTz2713W9

Co-authored-by: Claude <noreply@anthropic.com>

* Add Starlight docs site with full documentation structure

Set up an Astro Starlight documentation site in docs/ with pages covering:
- Getting started (overview, quickstart)
- Architecture (how the FSM engine and container work)
- Container setup (CLI, Docker Compose, building from source)
- CLI reference (installation, full command list)
- OpenClaw integration (setup, configuration, local vs container)
- Wechaty integration (puppet setup)
- Operations (VNC, tokens, data/storage, proxy, restarting/recovery)
- Hosting (requirements, self-hosting, managed hosting)
- Reference (REST API, environment variables, troubleshooting)

Moved existing dev-notes (release.md, rust-server-investigation.md) to
dev-notes/ to make room for the docs site.

https://claude.ai/code/session_01EGmcCx8qrMU8kYzAAp7AZt

* Add docs site with GitHub Pages deployment, custom styling, and link fixes

- Starlight docs site with IBM Plex Sans + JetBrains Mono theme
- GitHub Actions workflow for automatic deployment on push to main
- Remark plugin to auto-prefix internal links with base path
- Custom CSS: dark/light themes, table styling, code blocks, sidebar
- Content updates: Docker prerequisites, managed hosting links, remote
  OpenClaw setup, Wechaty gateway mode, simplified QR scan tips
- Documentation links added to root and package READMEs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Update GitHub Actions to latest versions (node22 runtime)

- dorny/paths-filter v3 → v4
- docker/setup-buildx-action v3 → v4
- docker/login-action v3 → v4
- actions/upload-pages-artifact v3 → v4
- Remove pinned pnpm version (auto-detects from packageManager)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix WeChat a11y by using inherited D-Bus session address (#124)

* fix: use inherited DBUS_SESSION_BUS_ADDRESS for WeChat restarts instead of DB value

The health monitor was passing session.dbus_address from the DB when
restarting WeChat, but this could differ from the D-Bus session that
AT-SPI is actually connected to (set up by entrypoint.sh). This caused
restarted WeChat instances to connect to the wrong D-Bus bus, making
the a11y tree empty and triggering a kill loop.

Now spawn_wechat inherits DBUS_SESSION_BUS_ADDRESS from the agent-server
process environment (which got it from the entrypoint), ensuring WeChat
uses the same D-Bus session as AT-SPI.

https://claude.ai/code/session_012RruowqggbS2heNSDuWVGh

* chore: add changeset for DBUS_SESSION_BUS_ADDRESS fix

https://claude.ai/code/session_012RruowqggbS2heNSDuWVGh

* chore: add agent-server to changeset tracking

Add package.json for the Rust binary so changesets can track its version
alongside the other packages. Add it to the fixed version group.

https://claude.ai/code/session_012RruowqggbS2heNSDuWVGh

* revert: remove agent-server from changeset tracking

The Rust binary version is managed by Cargo.toml, not changesets.

https://claude.ai/code/session_012RruowqggbS2heNSDuWVGh

* chore: add agent-server to changeset version tracking

- Add package.json for agent-server-rust so changesets can track its version
- Add it to the fixed version group (all packages version together)
- Sync Cargo.toml to current version (0.11.5)
- Add sync-cargo-version.sh script to update Cargo.toml after changeset bumps
- Update version-packages script to run the sync
- Update changeset to reference @agent-wechat/agent-server

https://claude.ai/code/session_012RruowqggbS2heNSDuWVGh

---------

Co-authored-by: Claude <noreply@anthropic.com>

* Version Packages (#125)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* Add social share preview (Open Graph + Twitter Card) for doc pages (thisnick#127)

Adds an OG image and configures Starlight head meta tags so doc pages
render a branded preview when shared on Twitter/X, LinkedIn, Discord, Slack, etc.

https://claude.ai/code/session_01N6RBY1UsP4sjWe2YbuRtEv

Co-authored-by: Claude <noreply@anthropic.com>

* Add social share preview (Open Graph + Twitter Card) for doc pages (thisnick#128)

* Add social share preview (Open Graph + Twitter Card) for doc pages

Adds an OG image and configures Starlight head meta tags so doc pages
render a branded preview when shared on Twitter/X, LinkedIn, Discord, Slack, etc.

https://claude.ai/code/session_01N6RBY1UsP4sjWe2YbuRtEv

Convert OG image from SVG to PNG at build time using sharp

- Add Astro integration that converts public/og-image.svg → PNG during
  both dev and build via sharp
- Fix duplicate opacity attributes in SVG that broke sharp parsing
- Update meta tags to reference .png instead of .svg for universal
  social platform compatibility
- Add generated og-image.png to .gitignore (build artifact)

https://claude.ai/code/session_01N6RBY1UsP4sjWe2YbuRtEv

Redesign OG image and clean up integration code

- Replace AI-slop SVG (gradients, grid, rainbow pills) with minimal
  design: solid dark bg, WeChat green accent bar, bold type, terminal
  snippet
- Fix integration: remove unused imports, pass file path to sharp
  instead of reading buffer, add mtime check to skip reconversion,
  use fileURLToPath for cross-platform safety, add error handling
- Derive og:image URL from site+base constants instead of hardcoding
- Export OG_WIDTH/OG_HEIGHT from integration to share with config

https://claude.ai/code/session_01N6RBY1UsP4sjWe2YbuRtEv

Generate dynamic per-page OG images with satori at build time

Each doc page now gets its own OG image with its title and description
rendered via satori (JSX→SVG) + sharp (SVG→PNG). Images are generated
during astro:config:setup and cached with mtime checks.

- Add satori dependency
- Rewrite og-image integration to iterate content/docs, parse
  frontmatter, and generate public/og/[slug].png per page
- Add custom Head.astro component override to inject per-page
  og:image/twitter:image based on the current route
- Remove static og-image.svg (design now lives in code template)
- Remove static head entries from Starlight config

https://claude.ai/code/session_01N6RBY1UsP4sjWe2YbuRtEv

Add project context to per-page OG images

Non-root pages now show "agent-wechat — Programmable WeChat for AI
agents" above the page title so shared links have brand context.
Root page is unchanged.

https://claude.ai/code/session_01N6RBY1UsP4sjWe2YbuRtEv

* Add docs build job to CI

Runs `pnpm build` for the docs package on PRs that touch docs/,
pnpm-lock.yaml, or the CI workflow. Uses the same path-filter
pattern as the existing ts and rust jobs.

https://claude.ai/code/session_01N6RBY1UsP4sjWe2YbuRtEv

---------

Co-authored-by: Claude <noreply@anthropic.com>

* Update sync-cargo-version.sh to also update Cargo.lock (thisnick#130)

After syncing the version from package.json to Cargo.toml, run
`cargo update --workspace` so Cargo.lock stays consistent with
the bumped version in the Version PR created by changesets.

https://claude.ai/code/session_012RBQP5qtwp7DkQ5edLUo9e

Co-authored-by: Claude <noreply@anthropic.com>

* Fix noVNC token handling in WebSocket connection (thisnick#129)

* Fix VNC redirect encoding: pass token as separate query param

The injected noVNC script was embedding the token inside the `path`
query parameter (e.g. `path=vnc/websockify?token=...`), but
`searchParams.set()` percent-encodes the value, turning `?` into `%3F`
and `=` into `%3D`. This broke noVNC's ability to parse the token.

Fix: set `path` to just `vnc/websockify` and keep `token` as a
separate URL query parameter. noVNC natively recognizes the `token`
param and passes it to the WebSocket connection.

https://claude.ai/code/session_01TMLTZQ9HssUWawScCmeZGT

* Update Cargo.lock with current version

https://claude.ai/code/session_01TMLTZQ9HssUWawScCmeZGT

* Add changeset for VNC redirect params fix

https://claude.ai/code/session_01TMLTZQ9HssUWawScCmeZGT

---------

Co-authored-by: Claude <noreply@anthropic.com>

* Version Packages (thisnick#131)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* Fix VNC WebSocket auth: embed token in path param for noVNC (thisnick#132)

The upstream fix incorrectly removed the token from the noVNC `path`
query parameter, expecting noVNC to pass the `token` URL param to the
WebSocket connection natively. It doesn't — noVNC only uses the `path`
param to build the WebSocket URL. This broke auth on the websockify
endpoint.

Restore embedding the token inside the `path` param
(vnc/websockify?token=...) so it reaches the auth middleware, and
delete it from the visible URL to keep it out of browser history.

https://claude.ai/code/session_01TMLTZQ9HssUWawScCmeZGT

Co-authored-by: Claude <noreply@anthropic.com>

* Version Packages (thisnick#133)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* Keep token query param in VNC URL for direct access (thisnick#134)

The JS was deleting the top-level `token` param from the URL after
embedding it in the `path` param. This broke direct access to the
transformed URL because the server's `has_token` check looks for a
top-level `token=` query param.

Keep `token` in the URL so the page works when bookmarked or shared.

https://claude.ai/code/session_01TMLTZQ9HssUWawScCmeZGT

Co-authored-by: Claude <noreply@anthropic.com>

* Version Packages (thisnick#135)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* chore: update openclaw dependency to latest (thisnick#137)

* chore: update openclaw dependency to latest

- Update openclaw from >=2026.3.2 to 2026.3.23-2
- Fix breaking import changes in plugin SDK:
  - buildChannelKeyCandidates, resolveChannelEntryMatchWithFallback → openclaw/plugin-sdk/channel-targets
  - resolveAllowlistProviderRuntimeGroupPolicy, resolveDefaultGroupPolicy → openclaw/plugin-sdk/config-runtime
  - resolveSenderCommandAuthorization → openclaw/plugin-sdk/command-auth
  - DEFAULT_ACCOUNT_ID → openclaw/plugin-sdk/account-id
  - createReplyPrefixOptions → createChannelReplyPipeline from openclaw/plugin-sdk/channel-reply-pipeline
  - ChannelMeta type → ChannelPlugin["meta"] (no longer separately exported)
- Remove onboarding adapter (dropped from ChannelPlugin type)

* chore: add changeset for openclaw dependency update

* chore: use semver range for openclaw dependency

* chore: update lockfile

* Version Packages (thisnick#138)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* Add new WeChat build offsets (3eda8254, eba86b80) and detach hook after use (thisnick#139)

- Add chat selection profiles for builds 3eda8254 (aarch64) and eba86b80 (x86_64)
- Add image XOR masks for the same builds in extract-keys
- Detach Frida hook in onLeave instead of count-based guard to restore function prologue

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Version Packages (thisnick#140)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* Return "pending" for voice media not yet available in DB (thisnick#141)

When voice data hasn't been written to media_*.db yet, return
type="pending" instead of "unsupported". The extension's pollMedia
already retries when data is missing — but short-circuits on
"unsupported". This lets voice messages be retried correctly.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Version Packages (thisnick#142)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* fix: handle non-ASCII filenames in file send (thisnick#143)

* fix: handle non-ASCII filenames in file send

Two issues caused file sends with non-ASCII filenames (e.g. CJK
characters) to silently fail:

1. paste-file: The file:// URI passed to xclip contained raw non-ASCII
   bytes. WeChat's Qt under POSIX locale cannot resolve such URIs.
   Fix: detect non-ASCII paths and copy the file to an ASCII-safe temp
   path before pasting.

2. messages.rs: If std::fs::write failed (or base64 decode failed),
   file_path stayed None and the handler returned success:true without
   actually sending anything.
   Fix: sanitize the filename to ASCII for the temp path, and return
   proper error responses on failure instead of silently succeeding.

* address review: portable iconv check, clarify safe_name comment, add changeset

- Replace GNU grep -P with iconv for portable non-ASCII detection (paste-file)
- Add comment clarifying dot preservation in safe_name sanitization (messages.rs)
- Add patch changeset for @agent-wechat/agent-server

---------

Co-authored-by: Kyan Du <kyan.du@icloud.com>

* fix: detect @-mentions in type-49 (appmsg) content XML (thisnick#145)

For reference/quote messages (type=49), WeChat may place <atuserlist>
inside the content XML rather than the source column. This caused
@-mentions in quoted replies to be silently missed.

Now falls back to checking content XML for type-49 messages when
source XML has no mention, ensuring quoted replies with @-mentions
are properly detected.

Fixes: group chat replies via quote not triggering bot response

Co-authored-by: Kyan Du <kyan.du@icloud.com>

* Version Packages (thisnick#147)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* chore: remove dead onboarding adapter

The onboarding adapter was deprecated and removed from imports in v0.11.10
but the file was never deleted. No code references it.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: add changeset for onboarding removal

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(ci): remove npm upgrade step that breaks on Node 22.22.2

The bundled npm on Node 22 already supports OIDC provenance.
The `npm install -g npm@latest` step fails with a missing
`promise-retry` module on certain GitHub Actions runner images.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Version Packages

* fix(ci): pin npm@11.12.1 for OIDC trusted publishing

The bundled npm 10.x on Node 22 doesn't support OIDC trusted publishing.
Pin to npm@11.12.1 instead of `npm@latest` to avoid the broken
`promise-retry` module issue on certain runner images.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Version Packages

* feat(openclaw): migrate plugin to openclaw 2026.5 contracts

- Add channelConfigs metadata to openclaw.plugin.json so the gateway can
  validate config and load setup surfaces before the plugin runtime
  imports (silences "channel plugin manifest declares wechat without
  channelConfigs metadata" warning).
- Replace deprecated runtime.config.loadConfig() with config.current().
- Add a message adapter via createChannelMessageAdapterFromOutbound from
  openclaw/plugin-sdk/channel-message; keep the legacy outbound adapter
  for older openclaw versions.
- Update access-control test scenario to match openclaw 2026.5's
  intentional new semantics (pairing-store allowFrom is consulted only
  for non-"open"/"allowlist" DM policies).
- Bump openclaw devDep floor to ^2026.5.12.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(ci): skip npm self-install when runner ships compatible npm

The previous unconditional `npm install -g npm@11.12.1` step failed on
ubuntu-latest with Node 22.22.2, which already ships a compatible npm
11.x but breaks during self-install with `Cannot find module
'promise-retry'`. Only install when the active npm is older than the
OIDC-required floor (11.5).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(ci): activate npm 11.12.1 via Corepack

The previous version-checked install still failed: the runner ships
npm 10.9.7, and any `npm install` (including self-install) hits
`MODULE_NOT_FOUND: 'promise-retry'` at the arborist rebuild step. Use
Corepack to activate npm 11.12.1, which downloads the package manager
independently of the broken bundled npm.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: establish downstream maintenance baseline

* feat: parse merged-forward (chat history) messages (thisnick#126)

Parse appmsg subtype 19 (merged forward / chat history) by extracting
the XML-escaped <recorditem> content and rendering each <dataitem> as
'sender: message'. Previously only the title was shown, making these
messages opaque to agents.

Closes thisnick#126

(cherry picked from commit 36c9c30)

* fix: sync /api/status loginState with actual logged_in_user from DB

Before this fix, /api/status always returned loginState.status as
'logged_out' regardless of actual login state. This caused
inconsistency where:
- /api/status → logged_out
- /api/status/auth → logged_in
- sending messages failed with NOT_LOGGED_IN

Now get_status() reads the session's logged_in_user from the database
and derives the loginState.status from that, which is consistent with
how auth_status() determines login state.

Fixes: #110 (similar issue with login state inconsistency)
(cherry picked from commit a29517e)

* fix(send_message): pick active composer when ghost frames exist

WeChat's accessibility tree can contain multiple edit+send pairs: the
live main-window composer plus stale "ghost" frames left behind by chats
previously detached into separate windows. The old depth-first "take the
first pair" logic grabbed the wrong (ghost) composer, whose input never
received text and whose Send button stayed DISABLED forever, causing the
plan to loop and ultimately fail with "No action selected".

Collect every candidate edit+send pair and rank them so the genuinely
active composer wins:
  1. editable currently FOCUSED        (strongest signal)
  2. Send button NOT disabled          (composer already has text)
  3. pair under the main "Weixin" frame (not a ghost/detached frame)
DFS order breaks any remaining ties.

This makes 'wx messages send' reliable even when stale frames are present,
removing the need to restart the container to clear ghost windows.

(cherry picked from commit 88cbbb6)

* fix(chat-select): bounded non-blocking reads and shared composer finder

- chat-select.py: replace blocking readline() loops with a select()-based
  read_lines_until() so deadlines hold even when the frida child stays
  alive but silent; short-circuit when the target chat is already
  selected regardless of force, since clicking the selected item never
  fires the selectSession hook
- move the ghost-frame-aware edit+send composer finder from
  send_message.rs into ia::helpers and reuse it in chat_open.rs, ranking
  candidates lexicographically (focused > send-enabled > main frame)
- send_message: always run chat-select even in "chat" state; the open
  chat may not be the target and chat-select skips when it already is

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit 209cb02)

* test: cover adopted message and composer fixes

* fix: decode nested entities in merged-forward items

* Disable downstream release publishing

---------

Co-authored-by: Nick Yu <wiseyu@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Kyan Du <kyan.du@icloud.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: OpenClaw Agent <openclaw@agent.local>
Co-authored-by: longxboy <longxboyhi@gmail.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.

2 participants