* docs(macos): document codesign side-effect popup ("微信" 想访问其他 App 的数据)
After `codesign --force --deep --sign - /Applications/WeChat.app`, macOS
treats the re-signed WeChat as a different code identity from the
original. When WeChat then accesses its own container / cache / app-group
data (notably triggered when opening 公众号 articles), macOS fires the
"'微信' 想访问其他 App 的数据" popup.
This is a known side-effect of the current macOS invasive init path,
not a "wx-cli is reading other apps' data" issue and not a 公众号-only
problem — 公众号 is just a high-frequency trigger surface because of
WebView / cache access.
Document this in 3 places per agreed scope:
- README.md macOS init: add "副作用提示" callout linking to the guide
- docs/macos-permission-guide.md: new §六 with first-principles
explanation, mitigation options, and long-term direction
- src/cli/init.rs: print a short macOS-only warning at the end of
`wx init` so users see it right when they finish the invasive setup
* review: stop overstating the trade-off and condition the init warning
Per codex review on PR #64:
1. src/cli/init.rs warning was unconditional but the wording presumed
the user had taken the ad-hoc re-sign path. If init goes through the
tier 2 path (Apple-signed WeChat + GUI Terminal + Developer Tools TCC
authorization), the warning would mis-fire. Reword conditionally and
point to the GitHub URL of the doc instead of a relative path that
release-binary / npm-installed users won't have on disk.
2. docs/macos-permission-guide.md §六 and the matching README callout
said "restoring official WeChat = giving up macOS memory-scan". This
contradicts the same guide's §一 实测表 which shows
"Apple 签名 + 本机 Terminal sudo = ✅". Restoring the official
signature only gives up the default re-sign path; the local-Terminal
+ Developer-Tools route still works on Apple-signed WeChat. Only
SSH + Apple-signed WeChat actually requires re-signing.
* review (round 2): caveat empirical gap + drop emoji
Self-review found two issues both LGTMs missed:
1. The "tier 2 仍走通" claim (README + §六) leans on §一 实测表 row
"Apple 签名 + 本机 Terminal sudo = ✅". But that data only covers
macOS 10.15 (Catalina) and 11.1 (Big Sur). macOS 14/15 — the exact
versions where the popup behavior originates — were never tested
for that path in this project. Add an explicit caveat instead of
silently extrapolating across major macOS versions.
2. `init.rs` warning used a ⚠️ emoji prefix, which violates the
project + global "no emojis in files unless requested" rule. README
and the rest of init.rs have no emoji. Replace with `[macOS]`.
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.
- 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`).
* 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>
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.
- server.rs: add handle_connection_windows for named pipe connections
- transport.rs: import CommandExt trait for creation_flags on Windows
- release.yml: mkdir -p before binary copy to npm bin dirs