Avoid refreshing contacts for daemon requests that do not need names, and invalidate the contact cache entry when reload fails so the next request retries instead of staying stale.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Refresh the in-memory contact cache when contact.db changes so long-running daemon sessions pick up updated names and verification flags without a restart.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Lays down the skeleton for聊天附件 (chat attachment) extraction. This commit
introduces the `attachment` module with:
- `attachment_id`: opaque base64url(json) round-trip handle for CLI/IPC. Carries
`(chat, local_id, create_time, kind)` — `local_id` alone is not unique
(实测同 chat 内最多 7 条同 local_id 的记录), so create_time is required for
disambiguation.
- `decoder/`: dispatch by 6B header magic. Three branches:
- `V2_MAGIC` → AES-128-ECB + raw + XOR (need image AES key)
- `V1_MAGIC` → AES-128-ECB with fixed key `cfcd208495d565ef` (= md5("0")[:16])
- else → legacy single-byte XOR with magic auto-detect
Manual ECB + PKCS7 unpad to avoid pulling in another crate.
- `resolver`: `message_resource.db` lookup chain
`username → ChatName2Id.rowid → MessageResourceInfo.packed_info → md5`
+ on-disk `.dat` selection (full > _h > _t) under
`<wxchat_base>/msg/attach/<md5(chat)>/<YYYY-MM>/Img/<md5>[_t|_h].dat`.
Honors `message_local_type % 2^32` to strip the high flag bits, and orders by
`message_create_time DESC` to handle local_id reuse.
- `image_key/`: stub trait + macOS / Windows placeholders. To be filled by
codex with the V2 image key extraction (kvcomm + brute-force on macOS, memory
scan on Windows).
V1 decoder ships with 6 unit tests covering every supported magic + the BMP
extra validation; resolver ships with packed_info parser + dat-file selection
tests; v2 decoder ships with header validation tests. 21 tests pass.
`cargo check` and `cargo check --target x86_64-pc-windows-gnu` both clean.
The 原理 section previously listed only macOS Mach VM API and Linux /proc/<pid>/mem,
omitting the Windows scanner path that has existed in src/scanner/windows.rs since
the Rust rewrite. Add the Windows API pair and the required process access rights
so the section accurately reflects all three platforms supported in CI/builds.
- daemon: write pid file only after IPC bound; clean sock+pid on normal return
- transport: PidFile JSON metadata + identity verification (ps/QueryFullProcessImageNameW); SIGTERM with poll-timeout; backward-compat read for plain-text pid
- daemon_cmd: status/stop work with both new JSON and legacy plain-text pid file
- config: cwd → exe_dir → ~/.wx-cli config precedence matches `wx init` write order; Windows DB auto-detect picks newest by latest mtime
- crypto: full_decrypt uses read_exact for intermediate pages, zero-pads only the final partial page; tests cover short-chunk reads and early EOF
- scanner/windows: page protect check covers PAGE_READWRITE / PAGE_WRITECOPY / PAGE_EXECUTE_*WRITE* with modifier-bit stripping
Cross-reviewed by @wx-cli-coder. Windows verified via `cargo check --target x86_64-pc-windows-gnu` (no Windows runtime test).
- q_contacts: replaced ad-hoc `gh_*`/`biz_*` prefix filter with
`chat_type_of == "private"`. The old filter leaked groups
(`@chatroom`), folded entries (`brandsessionholder` /
`@placeholder_foldgroup`), verified service accounts
(`verify_flag != 0`), and internal `@xxx` system accounts into
`wx contacts` output.
- q_search: parallelized the per-message-DB blocking phase via
`JoinSet::spawn_blocking`. Previously the `for (db_path, ...) in
by_path { ... .await }` loop ran one DB at a time; users with N
message_*.db shards paid N× latency. Each DB now runs concurrently
on the blocking pool; total latency collapses to a single slow DB.
- q_new_messages: fixed `new_state` reset path so first-run + truncated
sessions don't lock `since_ts` at `fallback_ts` forever. Old code
always wrote `state[uname] = old_since_ts || fallback_ts` for changed
sessions, then advanced only those that appeared in `all_msgs`. On
first run (state=None) truncated sessions ended up with
`state[uname] = now-86400` and stayed there across calls — every
subsequent call re-scanned a window that grew with elapsed time.
New logic separates three cases:
* in_results → advance to returned_max (incremental fetch)
* truncated + state → keep prev since_ts (retry next call)
* truncated + none → advance to session_ts (avoid lock-in; old
messages remain reachable via `wx history`).
macOS TCC binds permissions to (bundle id, csreq) where csreq encodes
the app's code signature. `codesign --force --deep --sign -` on
WeChat changes the csreq, silently invalidating every existing TCC
grant for com.tencent.xinWeChat — yet System Settings still paints
each toggle as ON because the UI only checks bundle id, hiding the
drift. WeChat then reprompts for screen recording / camera /
microphone / file access despite "looking allowed".
Three doc-only updates, no code changes:
- README.md quick start: add the `tccutil reset` loop right after the
codesign step, plus a one-line callout pointing at the deep-dive
section.
- SKILL.md macOS init flow: same loop in the agent-readable order, so
agents executing the steps don't skip it.
- docs/macos-permission-guide.md: new section 五 with first-principles
root cause, the reset loop, the macOS 26 "录屏与系统录音 / 仅系统
录音" UI split footgun, and ad-hoc signature verification.
Builds on the BobbyCat PR #29 — keeps the symptom description and the
macOS 26 UI split note, expands scope from ScreenCapture-only to all
TCC services that re-signing actually breaks (Camera / Microphone /
AppleEvents / AddressBook / Documents / Downloads / Desktop), drops
the misleading TCC.db sqlite query (path varies by macOS version, can
need FDA, and is no more useful than just trying WeChat's screenshot
again), and explicitly leaves the reset as a manual step rather than
auto-running it from `wx init` because it would wipe currently-working
grants.
Co-authored-by: BobbyCat <114374951+BobbyCats@users.noreply.github.com>
* feat: expose url field for link/appmsg messages
Extract <url> from appmsg XML in type-49 messages and append it as
a 'url' field in history/search output. The field is omitted when
the message has no valid URL (non-link types, empty, non-http).
* fix: normalize appmsg urls across query outputs
---------
Co-authored-by: tsinghu <tsinghu@tencent.com>
Co-authored-by: jackwener <jakevingoo@gmail.com>
* feat: support group nicknames
* fix(group): keep duplicate nickname senders separate in stats
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
Clarify that the 500-message behavior is only a default limit, not a hard cap.
Document `-n/--limit` examples for history, search, and export in both README and SKILL.
Was: Arc<std::sync::RwLock<Names>>; each dispatch clone_names() copied
4 HashMaps (~100KB for a user with 2700 contacts) and used std RwLock
which blocks the tokio worker thread during the clone.
Now: Arc<tokio::sync::RwLock<Arc<Names>>>; dispatch takes the read
guard, does Arc::clone (pointer bump), drops the guard, then spawns
the query work. Names is immutable after daemon startup; Arc is ideal.
Smoke tested: `wx sessions --json` returns correct data including
chat_type; 8 concurrent clients finish in 12ms.
Root cause: `wx init` does two conceptually-separate things in one
privileged process: (1) scan WeChat memory for keys (needs root) and
(2) write ~/.wx-cli/{all_keys,config}.json (needs only user). When
run under sudo, the files inherit root ownership, so later the daemon
(forked as the user) can't create daemon.sock/log/pid → silent 15s
timeout.
Also: all_keys.json is the raw AES key; 0644 leaked it to every user
on the system.
Fix in init.rs: after the scan completes, immediately setgid+setuid
back to \$SUDO_UID/\$SUDO_GID and set umask 0o077 before any file I/O.
Files are then created as the real user with 0600 by default. Migrate
old broken installs by chown+chmod-recursive before the setuid call.
Fix in transport.rs: pre-check that ~/.wx-cli/ is writable before
spawning daemon; on EACCES print a clear "sudo chown -R ..." hint
instead of the useless "daemon 启动超时" message.
Server uses interprocess::local_socket, but client was using
std::fs::OpenOptions("\\.\pipe\wx-cli-daemon") which fails to
connect to pipes created by interprocess's tokio listener.
Use the same interprocess client API on both sides for consistency.
Verified with: cargo check --target x86_64-pc-windows-gnu (mingw-w64).
Three related bugs caused "wx init" and daemon startup to fail on Windows:
1. init.rs: create ~/.wx-cli/ before writing all_keys.json (was created
only before config.json, so first write failed with ENOENT)
2. transport.rs (Windows): daemon.log was always empty because stderr
was never redirected, and log file open silently fell back to null
when parent dir didn't exist. Now mirror the Unix version: create
parent dir, try_clone to redirect both stdout and stderr.
3. server.rs (Windows): interprocess GenericNamespaced auto-prepends
\\.\pipe\ on Windows. Passing the full path caused a double-prefixed
pipe name that clients (using raw \\.\pipe\wx-cli-daemon) could
never connect to, leading to the 15s startup timeout.
The `skills` CLI (https://github.com/openclaw/skills) requires a YAML
frontmatter block with `name` and `description` to recognize a SKILL.md
as a valid skill. The current file declares description as a Markdown
blockquote, which causes:
$ npx skills add jackwener/wx-cli -g
No valid skills found. Skills require a SKILL.md with name and description.
Switching to standard frontmatter makes installation work end-to-end.
Verified with `npx skills add . -l`:
Found 1 skill
wx-cli