Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
[![Platform](https://img.shields.io/badge/platform-macOS%20%7C%20Linux%20%7C%20Windows-lightgrey.svg)](#安装)
[![Rust](https://img.shields.io/badge/built%20with-Rust-orange.svg)](https://www.rust-lang.org)

会话 · 聊天记录 · 搜索 · 联系人 · 群成员 · 收藏 · 统计 · 导出
会话 · 聊天记录 · 搜索 · 发送 · 联系人 · 群成员 · 收藏 · 统计 · 导出

</div>

Expand Down Expand Up @@ -38,6 +38,7 @@ npx skills add jackwener/wx-cli -g
- **毫秒级响应** — 后台 daemon 持久缓存解密数据库,mtime 不变则复用
- **AI 友好** — 默认 YAML 输出,更省 token & 易读;`--json` 可切换为 JSON(方便 `jq` 处理等)
- **完全本地** — 数据不出本机,实时解密,无需全量预解密
- **macOS 发送** — 通过微信搜索快捷键打开聊天并发送文本消息(需辅助功能权限)

---

Expand Down Expand Up @@ -150,10 +151,13 @@ wx history "AI群" --since 2026-04-01 --until 2026-04-15
wx search "关键词" # 全库搜索
wx search "关键词" -n 500 # 放宽搜索结果条数
wx search "会议" --in "工作群" --since 2026-01-01
wx send "张三" "你好,今晚 8 点见" # 发送消息(macOS)
```

`history` / `search` / `export` 都支持 `-n` / `--limit` 指定条数。默认值只是为了避免一次性输出过多消息,不是硬上限。

`send` 是 macOS 屏幕自动化命令:它会激活微信,用 `⌘F` 聚焦搜索框,打开目标聊天后发送文本。使用前需保持微信已登录,并给当前终端或 agent 应用开启"辅助功能"权限;发送过程中会临时使用剪贴板。

会话/消息输出里都带 `chat_type` 字段,取值为 `private` / `group` / `official_account` / `folded`。`official_account` 涵盖公众号、订阅号、服务号及 `mphelper` / `qqsafe` 等系统通知;`folded` 对应微信里的"订阅号折叠"和"折叠群聊"两个聚合入口。

### 朋友圈(SNS)
Expand Down
10 changes: 9 additions & 1 deletion SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: wx-cli
description: "wx-cli — 从本地微信数据库查询聊天记录、联系人、会话、收藏等。用户提到微信聊天记录、联系人、消息历史、群成员、收藏内容时,使用此 skill 安装并调用 wx-cli。"
description: "wx-cli — 从本地微信数据库查询聊天记录、联系人、会话、收藏等,并可在 macOS 通过 WeChat 屏幕自动化发送消息。用户提到微信聊天记录、联系人、消息历史、群成员、收藏内容、发送微信消息或回复微信时,使用此 skill 安装并调用 wx-cli。"
---

# wx-cli
Expand All @@ -16,13 +16,16 @@ description: "wx-cli — 从本地微信数据库查询聊天记录、联系人
- wx-cli
- 帮我看看微信里
- 搜索微信消息
- 发送微信消息
- 回复微信

## Prerequisites

- macOS(Apple Silicon / Intel)或 Linux
- 微信桌面版 4.x 已安装并登录
- Node.js >= 14(npm 安装方式)或 curl(shell 安装方式)
- 首次 `wx init` 需要 `sudo`(内存扫描提取密钥)
- macOS 发送消息需要给当前终端或 agent 应用开启"辅助功能"权限

---

Expand Down Expand Up @@ -122,10 +125,15 @@ wx history "AI群" --since 2026-04-01 --until 2026-04-15 -n 100
wx search "关键词"
wx search "关键词" -n 500
wx search "会议" --in "工作群" --since 2026-01-01

# 发送消息(macOS)
wx send "张三" "你好"
```

`history` / `search` / `export` 都支持 `-n` / `--limit` 指定返回条数。默认值只是为了避免一次输出过多,不是硬上限。

`send` 是 macOS 屏幕自动化命令:它会激活微信,用 `⌘F` 聚焦搜索框,打开目标聊天后发送文本。发送前必须由用户明确确认收件人和消息正文;命令会真实发送消息给第三方。发送过程中会临时使用文本剪贴板,并在常见情况下恢复原文本剪贴板内容。

`sessions` / `unread` / `history` / `new-messages` / `stats` 的输出都带 `chat_type` 字段,agent 可据此分流:

| 取值 | 含义 | username 特征 |
Expand Down
10 changes: 10 additions & 0 deletions src/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ mod init;
pub mod sessions;
pub mod history;
pub mod search;
pub mod send;
pub mod contacts;
pub mod export;
pub mod daemon_cmd;
Expand Down Expand Up @@ -92,6 +93,14 @@ enum Commands {
#[arg(long)]
json: bool,
},
/// 发送微信消息(macOS 屏幕自动化)
Send {
/// 聊天对象名称
chat: String,
/// 要发送的消息
#[arg(allow_hyphen_values = true)]
message: String,
},
/// 查看联系人
Contacts {
/// 按名字过滤
Expand Down Expand Up @@ -282,6 +291,7 @@ fn dispatch(cli: Cli) -> Result<()> {
Commands::Search { keyword, chats, limit, since, until, msg_type, json } => {
search::cmd_search(keyword, chats, limit, since, until, msg_type, json)
}
Commands::Send { chat, message } => send::cmd_send(chat, message),
Commands::Contacts { query, limit, json } => contacts::cmd_contacts(query, limit, json),
Commands::Export { chat, since, until, limit, format, output } => {
export::cmd_export(chat, since, until, limit, format, output)
Expand Down
94 changes: 94 additions & 0 deletions src/cli/send.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
use anyhow::Result;

#[cfg(target_os = "macos")]
use anyhow::{bail, Context};
#[cfg(target_os = "macos")]
use std::process::Command;

#[cfg(target_os = "macos")]
const SEND_SCRIPT: &str = r#"
on run argv
if (count of argv) < 2 then error "chat and message are required"
set chatName to item 1 of argv
set messageText to item 2 of argv
set previousClipboard to missing value
try
set previousClipboard to the clipboard as text
end try
try
tell application id "com.tencent.xinWeChat" to activate
delay 0.3
tell application "System Events"
set wxProc to first application process whose bundle identifier is "com.tencent.xinWeChat"
set frontmost of wxProc to true
delay 0.2
keystroke "f" using command down
delay 0.1
keystroke "a" using command down
delay 0.05
my pasteText(chatName)
delay 1.5
key code 36
delay 0.8
my pasteText(messageText)
delay 0.1
key code 36
end tell
my restoreClipboard(previousClipboard)
on error errorMessage number errorNumber
my restoreClipboard(previousClipboard)
error errorMessage number errorNumber
end try
end run

on pasteText(textValue)
tell application "System Events"
set the clipboard to textValue
delay 0.05
keystroke "v" using command down
end tell
end pasteText

on restoreClipboard(previousClipboard)
if previousClipboard is not missing value then set the clipboard to previousClipboard
end restoreClipboard
"#;

#[cfg(target_os = "macos")]
pub fn cmd_send(chat: String, message: String) -> Result<()> {
if chat.trim().is_empty() {
bail!("聊天对象名称不能为空");
}
if message.is_empty() {
bail!("消息不能为空");
}

let output = Command::new("osascript")
.arg("-e")
.arg(SEND_SCRIPT)
.arg(&chat)
.arg(&message)
Comment on lines +59 to +70
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

message 只用 is_empty() 校验会放过仅包含空格/换行的内容,与 PR 描述里“验证空消息”不一致。建议改为基于 message.trim() 判断空白,并(可选)像 chat 一样将 chat.trim() 的结果用于传给脚本/打印,避免用户输入前后空格导致搜索不到会话。

Copilot uses AI. Check for mistakes.
.output()
.context("无法运行 osascript")?;

if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
let reason = if stderr.is_empty() {
format!("osascript exited with status {}", output.status)
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

osascript 失败且 stderr 为空时,这里会生成类似 "osascript exited with status exit status: 1" 的重复文案(ExitStatus 的 Display 已包含 "exit status:")。建议改为基于 output.status.code() 组装更清晰的错误原因,或直接格式化 output.status 而不要再额外加 "exited with status" 前缀。

Suggested change
format!("osascript exited with status {}", output.status)
if let Some(code) = output.status.code() {
format!("osascript exited with code {}", code)
} else {
format!("osascript failed with status {}", output.status)
}

Copilot uses AI. Check for mistakes.
} else {
stderr
};
bail!(
"发送微信消息失败:{}。请确认微信已登录,并已给当前终端/应用开启“辅助功能”权限",
reason
);
}

println!("已发送到 {}", chat);
Ok(())
}

#[cfg(not(target_os = "macos"))]
pub fn cmd_send(_chat: String, _message: String) -> Result<()> {
anyhow::bail!("send 命令目前只支持 macOS");
}