SuperAPI AI Directory

wechat

Design & creation

View repository

Updated 10-15 23:58

About this skill

wechat — macOS CLI

Unified CLI for WeChat on macOS. Send messages in pure background (zero UI flash) AND query the encrypted local databases for sessions, contacts, chat history, group members, Moments, favorites.

Fast path (read this first)

Send a WeChat message in one call:

wechat send "早上好" Lisa                 # fuzzy name match (remark / nick / alias)
wechat send "hi" filehelper              # wxid — zero DB lookup, fastest
wechat send "提醒一下" 20590343959@chatroom  # group wxid (ends in @chatroom)

Resolution rules (applied in order):

  1. RECIPIENT matches a wxid shape (wxid_…, …@chatroom, gh_…, biz_…, or reserved like filehelper) → skip all DB work and send directly.
  2. Otherwise, search the local contact DB (remark / nickname / alias / wxid) with session-recency bias:
    • single match → send
    • multiple matches but only one has recent activity (30d) → send to that one
    • otherwise → exit 2 + JSON {"status":"ambiguous","candidates":[...]}; the agent picks and retries with the explicit wxid

On ambiguous, a sample response:

{
  "status": "ambiguous",
  "hint": "Lisa",
  "candidates": [
    {"wxid": "lishuang683451", "display_name": "lisa", "last_seen": "2026-04-20 05:34:55"},
    {"wxid": "wxid_xxx", "display_name": "Lisa (另一个)", "last_seen": ""}
  ],
  "note": "multiple matches; pass one of the wxids explicitly: wechat send <text> <wxid>"
}

Agent should: read candidates[0].wxid, retry wechat send "<text>" <wxid>. Don't ask the user unless the top candidate has no recent activity or multiple candidates do.

HTTP Bridge for agent integration (v1.10+)

wechat-bridge is a separate binary that wraps the daemon's RPCs as a stable localhost HTTP surface. Use this when wiring WeChat into agent platforms (Hermes, n8n, Dify, LangChain, custom bots) — HTTP is cheaper to integrate than spawning the CLI per call.

# Start bridge (binds 127.0.0.1:18400 by default)
wechat-bridge &

# Health + send-readiness
curl http://127.0.0.1:18400/health

# Send
curl -X POST http://127.0.0.1:18400/send \
  -H 'Content-Type: application/json' \
  -d '{"wxid":"filehelper","text":"hi"}'

# SSE message stream — ⚠️ ALWAYS pass ?since=<epoch>
# Without ?since, default is 0 = backfills entire local message history
# (1MB+ in seconds for typical accounts). For agent / long-running flows
# always pass a since timestamp; pick "now" for live-only or last-checkpoint.
SINCE=$(date +%s)
curl -N "http://127.0.0.1:18400/messages/stream?since=$SINCE"

Endpoints:

Method Path Maps to
GET /health ping + send_status
GET /chats sessions
GET /unread unread
GET /contacts contacts (query + limit)
GET /chat/:wxid recent N messages for one chat
GET /chat/:wxid/history history (limit + since + until)
GET /resolve resolve_recipient
POST /send send_text — returns {status: delivered / submitted_unconfirmed / status_unknown / failed, diagnostic, ...}
POST /typing typing indicator (only when --shape hermes)
GET /messages/stream?since=<epoch> new_messages_since polled into SSE; pass since or you'll get the full backlog on first connect

SSE payload shape (v1.10.28 — Wechaty-aligned + isMentioned)

/messages/stream emits event: messages carrying a JSON array of:

{
  messageId: string,
  chatId: string,                // wxid (DM) or groupid@chatroom
  senderId: string,              // in group: sender's wxid; in DM: the other party's wxid
  senderName: string,
  chatName: string,
  isGroup: boolean,
  body: string,                  // human-readable text. For URL / quote / mini_program, body is the title — raw XML is NOT exposed here.
  hasMedia: boolean,
  mediaType: "image"|"voice"|"video"|"file"|"",
  mediaUrls: string[],           // first entry is CDN URL when applicable
  mentionedIds: string[],        // v1.10.25+ — authoritative @-mention list resolved by daemon
  isMentioned: boolean,          // v1.10.28+ — bridge-authoritative "this row @-mentions ME". Self-sent rows are always false.
  quotedParticipant: string,     // v1.10.27+ — populated from refer.fromUser on quote replies
  botIds: string[],              // legacy heuristic self-marker; NEW consumers should rely on fromSelf instead
  fromSelf: boolean,             // v1.10.25+ — bridge-authoritative "this row was produced by our own POST /send"; DROP THESE to avoid self-echo loops
  messageKind: "text"|"image"|"audio"|"video"|"contact"|"emoticon"|"location"|
               "url"|"attachment"|"mini_program"|"chat_history"|"transfer"|
               "red_envelope"|"recalled"|"system"|"unknown",  // v1.10.27+, aligned to Wechaty's MessageType enum
  urlLink?:     { title, description, url, thumbUrl },                             // present iff messageKind=url
  miniProgram?: { title, description, appId, username, pagePath, thumbUrl },        // present iff messageKind=mini_program
  refer?:       { svrId, fromUser, chatUser, displayName, content },              // present on quote replies
  recall?:      { replacedMsgId, text },                                            // present iff messageKind=recalled
  media?:       { aesKey, md5, cdnUrl, cdnThumbUrl, length, durationSeconds, localPath },  // structured metadata for image/audio/video/attachment
  timestamp: number,
}

The full JSON Schema is committed at wx/schema/sse-payload-v1.10.28.schema.json and enforced by a contract test in the daemon build.

Consumer checklist:

  • Filter self-echo with fromSelf === true. Do NOT use senderId === myWxid — in DM both directions share the same senderId.
  • In groups, only respond when isGroup && isMentioned — the daemon already resolves the authoritative mention comparison, so don't reimplement mentionedIds.includes(myWxid) yourself (your wxid may be a remark / lookup that the daemon resolves correctly). The bridge will also drop non-@ group rows automatically when WECHAT_BRIDGE_GROUP_MENTION_ONLY=1.
  • Need the URL only? mediaUrls[0]. Need aesKey + md5 to decrypt or verify? media.cdnUrl / media.aesKey / ….
  • For messageKind: "image", do not inline base64 image bytes in chat responses. Call wechat image get <messageId> --chat <chatId> --json, parse absolutePath, then use the host agent's file/image Read capability on that path. Default --from auto (since v1.13.11) tries the daemon's in-memory lookup first (fast, works when the user has opened the image at least once in WeChat) and falls back to CDN replay only on miss. If the result is image not yet viewed in WeChat (cache empty), and CDN fallback failed, ask the user to open the image once in WeChat and retry. cdn-expired or needs local-decrypt adaptation means neither path can recover this image — surface that to the user instead of guessing.
  • Expect body for URL / quote / mini_program to be the human title. If you were previously parsing raw <appmsg> XML from body, migrate to the dedicated urlLink / miniProgram / refer objects.
  • Backward compatible: every pre-v1.10.25 field is preserved in name + type. New fields are additive.

Security notes for agents:

  • Bridge binds 127.0.0.1 — not exposed to LAN without tunnelling.
  • Set WECHAT_BRIDGE_BEARER=<secret> env var to require Authorization: Bearer <secret> on non-/health routes. Use this if tunnelling via Tailscale / SSH.
  • Activation gating is enforced inside wechatd, not in the bridge. A missing / expired wxp_act_ token → HTTP 401 / 402 on /send. Bridge cannot bypass activation.

Command groups

Group Commands First-time requirement
Diagnostics doctor — (run first; checks AX permission, daemon status, WeChat binary fingerprint)
Setup init requires user to click Enter WeChat during the ~5 min window
Send send first send after each WeChat restart fails with delivery_verify_timeout until the user manually types + Enters one message in WeChat to warm up the send pipeline (~5 s)
Query (messaging) sessions, unread, new-messages, contacts, history, search, members, stats, export, image, sent (v1.16.12+, cross-chat self-sent) init first; daemon auto-starts on demand (v1.7.5)
Saved items favorites init first; daemon auto-starts on demand
Realtime (v1.3+) listen daemon auto-starts on demand (v1.7.5)
Daemon (v1.2+) daemon start|stop|status|ping optional — query/listen commands pull it up automatically when needed
HTTP Bridge (v1.10+) wechat-bridge (separate binary) agent / Hermes / n8n integration over localhost HTTP — see section below
Wechaty Puppet gateway (v1.10.32+) wechat-wechaty-gateway (separate binary, gRPC :18401) for the human writing a wechaty bot — NOT used by this skill. If the user asks "can I run my wechaty bot on this?", point them to https://github.com/leeguooooo/wechat-skill#接-ai-agent and stop. Don't try to write wechaty TS from this skill.
wechat tunnel (v1.11+) wechat tunnel setup Expose local REST bridge to a remote service via Cloudflare Tunnel; details in docs/remote-gateway.md, do NOT inline the full setup flow in this skill.
wechat orchestrate (v1.12+) wechat orchestrate setup --outbox-url= --webhook-url= --bearer= --webhook-secret= Long-running worker that polls a SaaS outbox API and pushes SSE inbound events to a SaaS webhook. NAT-friendly (Mac all-outbound, no public IP / domain). Used by SaaS integrations (cherry-class). Protocol: docs/v1.12-orchestrate-protocol.md. Don't inline the SaaS-side endpoint design here.
Auth (v1.9.1+) auth activate | status | renew mandatory activation before send — code from @WechatCliBot on Telegram
update-guard (v1.16.33+) update-guard status | disable | enable Lock WeChat background auto hot-update channel (chflags uchg MacUpdate path), Tencent pushing a new dylib won't replace it. Recommended to run once after init completes disable to make the current build's adaptation stable; the CLI will self-check and warn at startup, without blocking
probe-build (v1.16.32+) probe-build Run when WeChat upgrades to a new build the tool doesn't recognize. Local literal_scan + pattern-match routing anchor outputs the shift value, POST /v2/probe to the adaptation queue.Don't pass binary, only pass derived values. read-side commands (history / sessions etc.) are immediately available via literal_scan fallback

All query commands default to YAML output (agent-friendly, low token). Add --json to get JSON.


🛑 Safety rules (CRITICAL — read before calling send)

Every send call must resolve to a known wxid. No silent default to "current chat" — that flag (--current-chat) was removed pre-1.13; if the resolver can't find a recipient, stop and ask the user.

Correct flows for "send YYY to XXX":

  1. Just try it: wechat send "YYY" XXX. Fast-path resolver (see top of this doc) handles wxid-shaped targets instantly and fuzzy-matches names against the local contact DB with session-recency bias.
  2. On exit 2 + status: "ambiguous": if candidates[0] has last_seen within ~30 days and others are stale/empty, the CLI already auto-picked it and returned success. If it truly was ambiguous (multiple candidates with recent activity), pick one yourself by asking the user — don't guess.
  3. On no contact matches "XXX": ask the user for the wxid (or have them confirm a candidate from wechat contacts --query XXX --brief).

Hard rules (the agent MUST follow):

  • DO NOT guess or fabricate a wxid. If resolution fails, escalate to the user.
  • DO NOT scan the filesystem / grep logs / use AppleScript to hunt for a wxid. The CLI already searches the local contact DB via the fast path — trust it. If it can't find the recipient, stop and ask the user.
  • DO NOT invoke wechat contacts followed by wechat send as two separate calls unless the first fast-path send already told you it was ambiguous. The one-liner saves ~400ms and one agent round-trip.

Capability matrix

Capability Status Command
Extract DB key, cache layout (required first step for query commands) ✅ wechat init
Send text to a specific wxid / group name / nickname ✅ wechat send "..." <recipient>
Any Unicode / emoji / CJK / length ✅ built-in
Zero UI flash (no focus steal) ✅ default for send
List recent chat sessions ✅ wechat sessions
Sessions with unread messages ✅ wechat unread
Incremental new messages since last check ✅ wechat new-messages
Contact lookup / fuzzy search ✅ wechat contacts [--query KW]
Chat history (private / group) ✅ wechat history <chat> [-n 500]
LLM-ready group digest ✅ v1.13.33 (--with-id v1.16.19+) wechat digest <chat> [--json --with-id]
User alias map for chats ✅ v1.16.19+ wechat alias add/list/rm
Full-DB keyword search (FTS5 trigram, v1.16.21+) ✅ wechat search <kw> [--in CHAT] [--since TIME]
Group members ✅ wechat members <group>
Chat statistics (senders / types / hours) ✅ wechat stats <chat>
Export chat → Markdown / JSON ✅ wechat export <chat> --format markdown -o ...
Favorites (text/image/article/...) ✅ wechat favorites [--type ...] [--query KW]
Image media (local in-memory lookup + CDN fallback) ✅ wechat image get <messageId> --chat <id>
Voice media (raw SILK_V3) ✅ wechat audio get <svr_id> (1.13.21+)
Voice transcribe (whisper.cpp + SILK pipeline) ✅ wechat audio setup once + wechat audio transcribe <svr_id> (1.13.25+)
first-send warmup (manual, once per WeChat session) required first send errors with delivery_verify_timeout; user types one msg in WeChat then re-runs wechat send
Realtime inbound stream (v1.3) ✅ wechat listen — watches new messages, push to stdout
Inbound callback → shell command (v1.3) ✅ wechat listen --on-message "handler.sh" (WECHAT_MSG_* env vars)
Server-side wxid filter (v1.3) ✅ wechat listen --wxid filehelper
Background daemon (v1.2+, lazy-start v1.7.5) ✅ wechat daemon start — or auto-spawn by any query command
WeChat binary fingerprint verification (v1.7.2+) ✅ wechat doctor surfaces drift after WeChat hot-fix updates
Send image / file ⏳ roadmap —
Group broadcast ❌ disallowed anti-abuse; LICENSE forbids
Linux / Windows / Intel Mac ❌ macOS arm64 only
unverified WeChat build ⚠️ unverified adaptation data may drift; wechat doctor flags it

Agent: first-use setup

Step 1 — Check wechat is on PATH:

command -v wechat

If missing:

curl -fsSL https://raw.githubusercontent.com/leeguooooo/wechat-skill/main/install.sh | bash
# Ensure ~/.local/bin is on PATH
case "${SHELL##*/}" in
  fish) fish_add_path "$HOME/.local/bin" ;;
  zsh)  grep -q '.local/bin' ~/.zshrc  2>/dev/null || echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.zshrc ;;
  bash) grep -q '.local/bin' ~/.bashrc 2>/dev/null || echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc ;;
esac
export PATH="$HOME/.local/bin:$PATH"

Step 2 — Run wechat init (required before any query command):

wechat init

This restarts WeChat (closes current session + relaunches) in order to capture the decryption key at login. Tell the user:

"Going to briefly close and relaunch WeChat to extract the local database key. Any draft messages in WeChat will be lost — confirm before proceeding. After WeChat relaunches, you must click "Enter WeChat" (or scan QR if no cached account) within ~5 minutes — the key is only written during that sign-in."

Key material is only written during the login moment, so init uses the macOS local debugging interface once during that window and waits up to 300 s. If the user misses the window or WeChat was already logged in before init ran, capture won't trigger — rerun wechat init --force.

Result is saved to ~/.wx-rs/ (mode 0600 files) + ~/.wx-rs/config.json. Re-run init whenever WeChat restarts.

init also prints the detected WeChat version/build and a binary fingerprint check. If the fingerprint isn't in the verified set (e.g. a WeChat hot-fix shipped a new build), send/query may silently fail — reinstall the official dmg from https://mac.weixin.qq.com/en and verify the auto-update toggle at WeChat → Settings → General → "Auto-upgrade when updates available" is off.

Step 3 — (For send only) Accessibility permission:

Run wechat doctor. If the terminal hasn't been granted Accessibility yet, this pops the native macOS dialog and opens the Privacy & Security → Accessibility pane directly — no hunting. Toggle the terminal app ON, then quit + relaunch the terminal (macOS requires a restart for the permission to take effect).

If you prefer the manual path: System Settings → Privacy & Security → Accessibility → add the terminal app you're using (Terminal / iTerm / Warp / …).

Step 4 — (For send only) One-time first-send warmup per WeChat session:

send needs WeChat's send pipeline to be fully wired, which only happens after a real user-initiated send. The first wechat send after each WeChat restart fails with:

error: 消息发送路径已执行,但数据库核查窗口内没有找到匹配新消息。常见原因:WeChat 输入框还没 warmup,首次 send 之前需要在 WeChat 里手动发一条让发送路径建好。

Resolution (the error itself prints these steps):

  1. Open WeChat
  2. Click "File Transfer Helper" (filehelper)
  3. Type any short message (e.g. hi)
  4. Press Enter to send
  5. Re-run wechat send — succeeds on retry, and stays warm until WeChat restarts.

This is intrinsic to WeChat's internal event loop; no automation can substitute. If wechat doctor reports ax_trusted: true and warmup still doesn't help, file an issue.


Send failure modes (agent self-recovery table)

When wechat send returns {ok: false, …} or stderr matches the reason: slot in the support payload, agents should map the reason to the playbook below before asking the user to debug. The first three are recoverable inline; the rest need a human action that the agent should ask for explicitly.

reason: slot Root cause Agent action
delivery_verify_timeout / slot_send_bp_armed_no_fire WeChat (re)launched but its internal send pipeline not yet wired Tell user: open WeChat → click "File Transfer Helper" → type any message → press Enter. Then retry wechat send. (See Step 4 above.)
wechat_not_running WeChat.app not launched Run open -a WeChat. If from a non-aqua SSH session, fall back to osascript -e 'tell application "WeChat" to activate'. Wait ~5s before retrying.
slot_send_bp_failed_to_arm (detail: send adapter install did not complete within 30s) WeChat has get-task-allow=false (official default signing); wechatd's local debugging interface can't connect Run wechat doctor to confirm wechat_get_task_allow ✗. Then ask the user to copy-run the merge-mode re-sign block that install.sh prints (or wechat doctor suggests). Needs sudo, must be run by the user. Do NOT use a replace-mode codesign --entitlements ... command — it strips WeChat's existing system entitlements and breaks TCC for WeChat itself.
daemon_accessibility FAIL / ax_trusted=false after a recent install/upgrade Multiple ai.wechat.* LaunchAgents on the machine — orchestrate (KeepAlive) re-spawned wechatd with a stale launchd responsibility chain Re-run curl -fsSL https://raw.githubusercontent.com/leeguooooo/wechat-skill/main/install.sh | bash. v1.16.12+ install.sh bootouts every ai.wechat.*.plist and bootstraps in the right order. Don't try to surgically launchctl kickstart — it doesn't reset the responsibility chain.
tcc_accessibility_denied (wechat-bridge or wechatd untrusted) User hasn't dragged binaries into System Settings → Privacy & Security → Accessibility, or dragged them but didn't toggle ON Open the pane: open "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility" + open ~/.local/bin. Tell user to drag both wechatd and wechat-bridge in and toggle both to ON. macOS strictly requires user-initiated drag — no automation can substitute.
activation_* / subscription_* / unauthorized Activation code expired / not redeemed wechat auth status to confirm. Then wechat auth activate <code>. Direct user to https://t.me/WechatCliBot to request an activation code (research / personal use only — no commercial inquiries answered).
dylib_fingerprint_unverified WeChat auto-updated to a build not in our verified set wechat doctor shows the new fingerprint. Tell user to turn off WeChat auto-update (WeChat → Settings → General → "Auto-upgrade when updates available") and reinstall from https://mac.weixin.qq.com/en. New build support is a maintainer task — file an issue with the fingerprint reported by wechat doctor.

Key principle: every reason: slot is stable enum; the support payload includes ax_trusted, input_monitoring, binary_fingerprint, cli_version, daemon_version — match on those exact field names, not on the human-readable Chinese error message (it may evolve across releases).

LaunchAgent / TCC handbook for agents: if you ever need to manually reset responsibility chains (e.g. user's machine has a customized LaunchAgent layout), use launchctl bootout + launchctl bootstrap — never launchctl kickstart -k. kickstart doesn't re-read plist EnvironmentVariables and doesn't reset launchd's responsibility-chain cache. The install.sh upgrade flow is the reference implementation.


Usage — send

# Recipient resolves wxid / 群名 / 昵称 / 备注 (fuzzy match against local contact DB)
wechat send "你好 🎉" filehelper
wechat send "会议 5 分钟后开始" lishuang683451
wechat send "早上好" Lisa                # 找不到 → friendly error + 候选

# Group send (resolver also handles group display names)
wechat send "今天 19:00 团建" "AI 星球"

# JSON output for agents that parse responses
wechat send "ok" filehelper --json

# Dry-run: resolve recipient + validate args, do NOT actually send. Useful when
# the agent wants to verify a fuzzy name → expected wxid before committing.
wechat send "draft" "李工" --dry-run --json

send arguments

Arg Required Description
<TEXT> (positional) yes Message body. Any length, any Unicode.
<RECIPIENT> (positional) or --wxid yes Target wxid / chatroom id / nickname / group name / remark. Resolver picks the most-recently-active match if hint is fuzzy.
--mention <wxid> no Visual @<name> prefix (text-only, no real ping ack — see issue #4).
--dry-run no Resolve recipient + validate but don't send. Pairs well with --json for agent dry-checks.
--json no JSON output

send --json three-state contract (v1.13.20+)

All --json outputs carry a top-level ok: bool, agent directly if (r.ok) {...} else {...} no need to parse three sets of schemas:

Status Trigger shape (top-level fields)
success wechat send TEXT RECIPIENT --json actually sent successfully {ok: true, sent: true, reason: null, diagnostic: {…SendResult 全字段…}}
dry-run --dry-run --json(resolver OK + not actually sent) {ok: true, dry_run: true, text, resolved_wxid}
error (early) --json + wrong params / resolver can't find / ambiguous / network down {ok: false, exit_code: <int>, error: "<msg>"}
error (send fail) actual send failed (first-send warmup miss / TCC missing / WeChat version not in adaptation set) {ok: false, sent: false, reason: "<reason>", diagnostic: {…}}
# 推荐:agent 用 jq 分支
wechat send "hi" filehelper --dry-run --json | jq -e '.ok' && echo "✓ resolved" || echo "✗ failed"

stderr still has human-readable error descriptions (for terminal users); agent only needs to parse stdout JSON.


Usage — query

# Sessions (recent conversations)
wechat sessions -n 20                           # full yaml
wechat sessions --brief -n 20                   # 单行/会话, 带未读数
wechat sessions --filter group --json -n 20     # 只看群聊 (chat_type: group / private / official_account / folded / other)
# JSON 字段命名:未读数是 `unread_count`(下划线全名), 不是裸 `unread`。brief 视图渲染成 [N unread] 仅是显示, 实际字段是 unread_count。

# Contacts
wechat contacts --query 李                      # fuzzy match nickname/remark/wxid
wechat contacts --brief -n 50                   # 单行/联系人 (姓名 + wxid)

# Unread
wechat unread -n 5

# History (chat positional or --chat flag, both accepted)
wechat history "张三" -n 2000
wechat history --chat 21263894984@chatroom -n 200
wechat history "AI 星球" --since "2026-04-01" --until "2026-04-15" -n 200   # ISO date OK
wechat history "AI 星球" --since 1719793200 --until 1720484400 -n 200       # epoch OK too

# Search (FTS5 trigram, v1.16.21+)
wechat search "会议纪要"                              # 全局,毫秒级
wechat search "报销" --in "财务群"                    # 群限定
wechat search "claude" --since "3 days ago"          # 加时间窗
wechat search "report" --since "2026-05-01" --until "2026-05-10"

# Search agent UX (v1.16.22+):
wechat search "desktop" --in "Helm" --context 5m     # 每条命中带前后 5 分钟同群上下文,文本模式 `>` 标记命中
wechat search "report" --timeout-ms 5000             # 硬上限 5s,超时 exit 124 + `search_timeout`
# `--json` meta 多带: elapsed_ms / row_count / chat_resolved / chat_hint / context_secs

# HTML 证据页 (v1.16.23+, v1.16.24 重做 WeChat 主题 UI + 默认 inline 图片):
wechat search "desktop" --in "Helm" --report                    # 单文件 HTML,默认 --context 5m,图片 base64 inline
wechat search "房价" --in "立水桥" --report --no-media           # 跳过图片(escape hatch,渲染成占位卡片)
wechat search "通知" --in "群" --report --out report.html       # 自定义路径,无 sidecar 目录,转发邮件/微信直接带走
# stdout = 写入的文件路径(text 模式)或 {ok, report_path, hit_count, ...}(--json)
# 输出文件 self-contained:inline CSS,无 JS,无 CDN,可直接邮件/微信/Notion 转发

# Search caveats (FTS5 trigram limits):
# - 查询 < 3 字符: 全局会报 `query_too_short`,加 `--in <chat>` 则
#   fallback 到 LIKE 单 shard。例: `wechat search 卡 --in <chat>`
# - 首次升级后 daemon 在后台 backfill (266k 消息 ~86s 本机);期间
#   查询会报 `search_index_unavailable`,等一两分钟后再查
# - mirror DB at `~/.wx-rs/search-index.db` (SQLCipher 加密,key 从
#   现有 WeChat key HKDF 派生),mirror 跟 WeChat 数据库一起在 home 目录
# - `--context` 需要配合 `--in <chat>` 才生效(无 chat 时被 daemon 跳过)

# Group members
wechat members "AI 星球"

# Stats
wechat stats "AI 星球"

history --json payload shape (stable contract for agents)

Top-level wrapper (history / sessions / unread / search / digest all consistent):

{
  "meta": {
    "chat_latest_timestamp": 1778981425,
    "shards_scanned": 2,
    "shards_hit": 2,
    "status": "ok",
    "order": "desc",
    "now_unix": 1779178200,
    "since_resolved": 1778573400,
    "until_resolved": 1779178200
  },
  "rows": [ {...row...}, {...row...} ]
}

Not a bare array.jq want .rows[] not .[]。meta.order is a field added in v1.16.12+, telling you rows are "desc"(new→old, history default) or "asc"(old→new, digest default / wechat history --order asc)。Don't take rows[0] as "newest" or "oldest", first check meta.order。

meta.status values:ok / windowed(passed --since/--until)/ possibly_stale (SessionTable is > 24h ahead of history, likely shard drift) / possibly_stale_unknown_shards (disk has new shard daemon doesn't recognize, need to rerun wechat init)。

v1.16.19+ time window backfill (history): meta.now_unix is the local epoch at query time; meta.since_resolved / meta.until_resolved is the --since / --until actual epoch after natural language parsing (doesn't appear if not passed). After natural language --since "2 days ago" is parsed, agent doesn't need to recompute timezone, just use these three values to restate to the user "actually queried is X to Y, current Z".

Fields of each message row (snake_case):

Field Type Description
local_id int DB row primary key (monotonic per chat).image get <local_id> --chat <wxid> Use this to fetch images.
server_id int WeChat server-side msg id (referenced when recalling replacedMsgId)。
local_type int Original type code. After masking the low 16 bits = 1 text / 3 image / 34 voice / 43 video / 49 appmsg / etc.
message_kind string enum: text / image / audio / video / url / mini_program / recalled / appmsg / etc. Wechaty-aligned.
display_text string Cleaned human-readable body (text directly = body; image/url extracts title; recalled gives replacement text).
message_content string Raw body (may be raw XML / group message with <sender>:\n prefix). For debug use; for production logic use display_text。
sender_wxid string | null Group message = real sender wxid;DM both sides are null(WeChat DB does not record sender wxid in 1:1 chat).Cannot determine self-sent from this alone(DM would misjudge both sides + system messages are also null).
sender_display_name string | null daemon-resolved display name (group nickname in groups / contact remark / nickname).Changed in v1.16.19+: When the contact DB is unreachable / this wxid cannot be found, fallback to the bare wxid instead of null (agents do not need to handle both has/has-not shapes at once). DM self-sent / system messages are still null.
real_sender_id string per-chat auto-increment ID (string,never empty). WeChat assigns the current account a fixed id (empirically on this machine it is "2", may differ per account), other integers = the other party/group member.Use this to determine self-sent: scan filehelper history to get your own id (filehelper is 100% self-sent, the one with the largest share is the self id), other chats use the same id for filtering.
chat_id / username string Conversation wxid (DM) or xxxx@chatroom(group).
chat_display_name string Group name / contact remark / nickname (auto-resolved in v1.13.9+).xxxx@chatroom Directly readable.
create_time int epoch seconds。
created_at string ISO local timezone (2026-05-18T01:30:45+09:00), derived in v1.13.30+, humans can just use this directly.
is_mentioned bool The current account was @ in the group (authoritatively resolved on the daemon side, clients should not compute it again).
media object image / voice / video / file only then: {aesKey, md5, cdnUrl, cdnThumbUrl, length, durationSeconds, localPath, dat_path?, dat_md5?, dat_exists?}。
urlLink / miniProgram / refer / recall object type-specific structured fields (see SSE schema).

Field stability: additions = default null / absent;will notrename / change type (the contract is guarded by SSE schema unit tests since v1.10.27).

Want to pull "what I said" across chats: use wechat sent --since "7 days ago" --json(v1.16.12+), much cleaner than manually iterating sessions + filter.

Group name ambiguity: wechat history "AI 星球" may match multiple (same-named groups + same-named contacts). On ambiguity the CLI lists candidates + errors, switch to --chat <wxid> or --chat <chatroom_id@chatroom> to specify explicitly.

v1.16.19+ user alias: Long group names / emoji-prefixed group names often miss on fuzzy (詹密群 / 季景铭郡业主三群), use an alias map once and for all:

wechat alias add 詹密群 17765974862@chatroom
wechat alias add AI星球 20590343959@chatroom
wechat alias list                # 看现有
wechat alias rm 詹密群           # 删

Resolver priority: wxid-shape > alias > contact.db fuzzy。wechat send / history / digest / search / recalled / sent / listen --wxid all share the same lookup (written to ~/.wx-rs/aliases.json)。


# Export
wechat export "张三" --format markdown -o zhang.md
wechat export "AI 星球" --format json -o ai.json -n 5000

# Incremental (since last checkpoint saved in ~/.wx-rs/cursor.json)
wechat new-messages -n 50      # advances checkpoint
wechat new-messages --reset    # rewind checkpoint to "now" so next call starts fresh

# Favorites
wechat favorites                          # all locally-cached items

# Image media (local in-memory lookup first, CDN fallback)
wechat image get <local_id> --chat <chat_id>            # decrypts + writes to ~/.wechat/media-cache/<md5>.jpg
wechat image inspect <local_id> --chat <chat_id>        # dump CDN metadata (no key/url leak)

# Voice media — history auto-transcribes by default (v1.13.25+)
wechat audio setup [--model small|medium|large]         # one-time: install deps + download model
wechat audio transcribe <svr_id> [--language zh]        # single-file pipeline
wechat audio get <svr_id>                               # raw SILK_V3 bytes, no decode

Voice — agents reading group history just work (v1.13.25+)

After running wechat audio setup once (~2-3 minutes downloads ~1.5GB medium model + builds silk-decoder), wechat history automatically transcribes voice messages so the agent sees the spoken content in display_text (and structured in media.transcript) — no more [语音消息] placeholders breaking conversation context. Transcripts are cached by SHA-256 of the audio blob, so re-reading the same chat is near-instant.

# One-time setup
wechat audio setup

# Read a chat — voice messages already transcribed inline
# (Output is {meta, rows} not a bare array — use `.rows[]` not `.[]`.)
wechat history <chat> --json | jq '.rows[] | {kind: .message_kind, text: .display_text}'

Opt-out (skip transcribe to keep history fast / private):

wechat history <chat> --no-transcribe                   # skip transcribe entirely
wechat history <chat> --transcribe-model small          # smaller / faster model
wechat history <chat> --quiet                           # silence stderr progress lines
                                                        # (auto-on in --json mode)

The media.transcript_status field on each audio row tells the agent where the text came from: cached / transcribed / no_deps (run wechat audio setup) / failed / skipped_svr_id_zero / invalid_input.

wechat doctor reports audio readiness in two rows so machine consumers can check default-model status without parsing strings:

  • audio_transcribe_setup — overall ffmpeg / whisper-cli / silk-decoder presence. Always ok: true (audio is optional; missing tools must not flip overall doctor status to needs_init).
  • audio_transcribe_default_model — ggml-medium.bin exists. ok reflects reality, but excluded from the overall status calculation so a user without medium still sees status: "ok". Read this row's ok field directly to know whether wechat history default transcribe will work.

For a single voice file outside history:

SVR=$(wechat history <chat> --json | jq -r '.rows[] | select(.message_kind=="audio") | .server_id' | head -1)
wechat audio transcribe "$SVR"           # prints transcript directly

wechat audio transcribe --json defaults to redacting the transcript text (only metadata in JSON output) so agents can't accidentally log private conversation text. Pass --include-transcript to opt in.

Voice — manual decode if you skip audio setup

wechat audio get <svr_id> writes the raw .silk bytes locally (pure local DB read; no debugging interface / in-memory lookup / CDN). To play / share without going through audio transcribe:

# One-time decoder build
git clone https://github.com/kn007/silk-v3-decoder /tmp/silk-v3-decoder
cd /tmp/silk-v3-decoder/silk && make

# Per-file
SVR=691336177198502815
wechat audio get "$SVR"
/tmp/silk-v3-decoder/silk/decoder ~/.wechat/audio-cache/$SVR.silk /tmp/$SVR.pcm
ffmpeg -y -f s16le -ar 24000 -ac 1 -i /tmp/$SVR.pcm /tmp/$SVR.wav

Note: SNS / Moments commands (sns-feed, sns-search, sns-notifications) and the legacy bootstrap subcommand were removed in the v1.13 line. The data is still in sns.db if you query it directly with sqlcipher, but no first-class CLI surface yet — track via roadmap.


Usage — realtime listen (v1.3)

wechat listen streams new incoming WeChat messages to stdout as they arrive (latency <500ms). Requires the background daemon.

# One-time: start the daemon (keep running in a separate terminal or `&`)
wechat daemon start

# Stream all new messages
wechat listen

# Stream only messages from one chat (server-side filter)
wechat listen --wxid filehelper

# JSONL output for agent consumption
# IMPORTANT: pipe stdout only — daemon spawn / errors go to stderr.
#   wechat listen --format json | your-adapter      ← OK
#   wechat listen --format json 2>&1 | your-adapter ← WRONG, stderr 混流会把 [daemon] 字样混进 stdin
wechat listen --format json

# Trigger a shell command per message — the handler sees WECHAT_MSG_* env vars
# Handler stdout is routed to /dev/null (so it doesn't pollute the JSONL stream
# that agents pipe to jq); use stderr or write to a file for logging.
wechat listen --on-message "./ai-reply.sh"
wechat listen --wxid lisa --on-message 'echo "[$(date +%H:%M)] $WECHAT_MSG_SENDER_WXID: $WECHAT_MSG_TEXT" >> log.txt'

--on-message env vars

Variable Meaning
WECHAT_MSG_TEXT Message body (already cleaned: compressed content decompressed, group <sender>:\n prefix stripped)
WECHAT_MSG_SENDER_WXID Sender wxid for group messages (empty string for private chats — there the chat wxid = sender)
WECHAT_MSG_TABLE Msg_<md5(chat_wxid)> — internal table name
WECHAT_MSG_CREATE_TIME Unix epoch seconds (as string)
WECHAT_MSG_LOCAL_ID / WECHAT_MSG_LOCAL_TYPE Internal message id + type code
WECHAT_MSG_SENDER_ID DB real_sender_id (numeric; rarely needed — use SENDER_WXID instead)
WECHAT_MSG_DB Absolute path of the message DB the message came from

Safety notes:

  • Content is passed via env vars, not shell-interpolated into the command. Safe against $(rm -rf) style injection.
  • Handler runs async (one subprocess per message); if it takes longer than messages arrive, handlers will pile up. Keep handlers fast or add your own queueing.

Daemon lifecycle

wechat daemon start              # foreground; or `wechat daemon start &` for background
wechat daemon status             # socket + pid + uptime
wechat daemon ping               # round-trip latency sanity check
wechat daemon stop               # graceful shutdown

The daemon caches each encrypted local DB connection so wechat sessions / contacts / history / unread run in <30ms (vs 400-500ms without it). It also powers wechat listen by watching message_*.db-wal file changes.

Fuzzy chat resolution

history / search --in / stats / export / members accept a <chat> argument that is matched against (in order): exact wxid → remark → nick_name → alias. If ambiguous, the most-recently-active match is picked. Use wechat contacts --query ... first if you need to disambiguate.

Output format

All query commands emit YAML by default. Add --json for JSON:

wechat sessions --json | jq '.rows[] | select(.chat_type=="private" and .unread_count>0)'
wechat new-messages --json                # ideal for agents consuming incremental updates

When to invoke this skill (agent triggers)

Send:

  • "Send a message to Lisa: ..."
  • "Send a WeChat notification to my mom 'I'm home'"
  • "Remind me that the XXX meeting starts in 5 minutes"
  • "send to filehelper ..."

Query:

  • "What did Lisa say recently in WeChat" → wechat history Lisa
  • "Search for who mentioned reimbursement in the group" → wechat search 报销
  • "How many people are in the AI Planet group / who speaks the most" → wechat members + wechat stats
  • "What unread messages are there" → wechat unread
  • "Export my chat history with Zhang San" → wechat export 张三 -o ...
  • "What is that image in the XX group" → wechat history "XX群" -n 50 (find the local_id of message_kind: image) → wechat image get <local_id> --chat <chat_id>
  • "What have I recently saved" → wechat favorites

Realtime:

  • "Help me keep an eye on messages from Lisa, and automatically reply XXX when received" → wechat daemon start then wechat listen --wxid <lisa-wxid> --on-message "..."
  • "Connect WeChat messages to my AI assistant" → wechat listen --format json --on-message "curl -X POST ..."
  • "Monitor who mentions 'meeting' in this group, and notify me immediately" → wechat listen --wxid <group> + handler that greps

Example user utterances and the right first call:

  • "Send a message to Lisa: the meeting starts in 5 minutes" → wechat contacts --query Lisa → wechat send --wxid ... --text ...
  • "send to filehelper today's summary" → wechat send --text ... --wxid filehelper
  • "Check who has spoken the most recently in the XXX group" → wechat stats "XXX"

🔐 Security / data scope

  • Everything runs 100% locally — no data leaves the machine.
  • wechat init caches the raw DB key in ~/.wechat/keys.json (mode 0600). Treat that key like a password — anyone with keys.json + a copy of ~/Library/Containers/com.tencent.xinWeChat/... can decrypt all your WeChat data.
  • Never commit ~/.wechat/ to git. Never paste the key into chat windows. If leaked: logout + re-login WeChat to rotate the key.

Mechanism (brief)

init — relaunches WeChat and uses the macOS public debugging interface once during the login moment to capture the local DB decryption material. No codesign --force --deep on WeChat.app, no sudo. The debugging interface detaches immediately after capture.

Query commands — load the captured material + discovered DB paths from local config and read the encrypted local databases directly. When the background daemon is running, queries are routed over a local Unix socket to a persistent connection pool — cuts latency 5-10×.

listen — watches the on-disk message databases for changes and pushes each new row to subscribed CLI processes over the daemon socket. Zero network traffic; runs entirely locally.

send — uses macOS Accessibility API to silently target the chat input, then drives WeChat's normal send pipeline via the local debugging interface. Zero window activation / focus steal.


Caveats

  • macOS arm64 only. WeChat 4.0.x / 4.1.x series verified — run wechat doctor to confirm your specific build is in the adaptation set. Other versions may need adaptation work.
  • Binary is a standalone native executable (GitHub Releases). install.sh auto-clears macOS Gatekeeper quarantine attribute.
  • Not a WeChat API. Userland research artifact. Can break on any WeChat update.
  • LICENSE forbids commercial use — see LICENSE + DISCLAIMER.md.

Updating the CLI

Before starting a session, the agent should check that wechat is reasonably current. The binary is self-contained; upgrading just means pulling a newer release.

Check current version:

wechat --version

Upgrade to latest (safe, idempotent) — re-run install.sh with --force, or without it (it overwrites by default):

curl -fsSL https://raw.githubusercontent.com/leeguooooo/wechat-skill/main/install.sh | bash

That pulls the latest tagged release binary from GitHub, re-installs to ~/.local/bin/wechat, and re-clears any Gatekeeper quarantine attribute. No data is lost — ~/.wechat/keys.json + state.json are untouched.

When to upgrade:

Stopping WeChat's auto-update (highly recommended, v1.16.33+):

After wechat init succeeds + wechat send works on the current build, run:

wechat update-guard disable

This locks the MacUpdate directory under WeChat's container with chflags uchg so Tencent's silent dylib hot-update can't replace the binary. The current adaptation profile stays valid until the user explicitly runs wechat update-guard enable to restore. wechat doctor shows wechat_update_guard as ✓ when disabled. Pure user-level reversible — does not touch the WeChat binary, NSUserDefaults, or TCC.

Adapting a brand-new WeChat build that isn't in the verified list (v1.16.32+):

If wechat doctor shows wechat_dylib_fingerprint ✗ (not in verified list) AND wechat send fails with "build not supported":

wechat probe-build

This hashes the local dylib, runs literal_scan to verify SQLCipher key extraction, pattern-matches three routing anchors against the ARM64 slice, and POSTs the derived numerical shifts to the adaptation queue. Does not upload binary bytes. Read-side commands (history / sessions / unread / sent) keep working via the literal_scan fallback while waiting for send adaptation.

Updating the skill metadata (this SKILL.md itself, when the agent is installed via skills.sh):

# refresh skill files (including this SKILL.md) from the public repo
npx skills update leeguooooo/wechat-skill -g

If the agent sees wechat: command not found after an npx skills update, it still needs to run install.sh — skill updates do not include the binary.

Support