diff --git a/README.md b/README.md index e0f06da..9792e54 100644 --- a/README.md +++ b/README.md @@ -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) -会话 · 聊天记录 · 搜索 · 联系人 · 群成员 · 收藏 · 统计 · 导出 +会话 · 聊天记录 · 搜索 · 发送 · 联系人 · 群成员 · 收藏 · 统计 · 导出 @@ -38,6 +38,7 @@ npx skills add jackwener/wx-cli -g - **毫秒级响应** — 后台 daemon 持久缓存解密数据库,mtime 不变则复用 - **AI 友好** — 默认 YAML 输出,更省 token & 易读;`--json` 可切换为 JSON(方便 `jq` 处理等) - **完全本地** — 数据不出本机,实时解密,无需全量预解密 +- **macOS 发送** — 通过微信搜索快捷键打开聊天并发送文本消息(需辅助功能权限) --- @@ -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) diff --git a/SKILL.md b/SKILL.md index 4ce28c3..05fcc86 100644 --- a/SKILL.md +++ b/SKILL.md @@ -1,6 +1,6 @@ --- name: wx-cli -description: "wx-cli — 从本地微信数据库查询聊天记录、联系人、会话、收藏等。用户提到微信聊天记录、联系人、消息历史、群成员、收藏内容时,使用此 skill 安装并调用 wx-cli。" +description: "wx-cli — 从本地微信数据库查询聊天记录、联系人、会话、收藏等,并可在 macOS 通过 WeChat 屏幕自动化发送消息。用户提到微信聊天记录、联系人、消息历史、群成员、收藏内容、发送微信消息或回复微信时,使用此 skill 安装并调用 wx-cli。" --- # wx-cli @@ -16,6 +16,8 @@ description: "wx-cli — 从本地微信数据库查询聊天记录、联系人 - wx-cli - 帮我看看微信里 - 搜索微信消息 +- 发送微信消息 +- 回复微信 ## Prerequisites @@ -23,6 +25,7 @@ description: "wx-cli — 从本地微信数据库查询聊天记录、联系人 - 微信桌面版 4.x 已安装并登录 - Node.js >= 14(npm 安装方式)或 curl(shell 安装方式) - 首次 `wx init` 需要 `sudo`(内存扫描提取密钥) +- macOS 发送消息需要给当前终端或 agent 应用开启"辅助功能"权限 --- @@ -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 特征 | diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 3a28060..c209352 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -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; @@ -92,6 +93,14 @@ enum Commands { #[arg(long)] json: bool, }, + /// 发送微信消息(macOS 屏幕自动化) + Send { + /// 聊天对象名称 + chat: String, + /// 要发送的消息 + #[arg(allow_hyphen_values = true)] + message: String, + }, /// 查看联系人 Contacts { /// 按名字过滤 @@ -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) diff --git a/src/cli/send.rs b/src/cli/send.rs new file mode 100644 index 0000000..d47a2c7 --- /dev/null +++ b/src/cli/send.rs @@ -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) + .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) + } 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"); +}