mirror of https://github.com/jackwener/wx-cli.git
feat: add macos send command
parent
6659f48984
commit
def157272e
|
|
@ -8,7 +8,7 @@
|
||||||
[](#安装)
|
[](#安装)
|
||||||
[](https://www.rust-lang.org)
|
[](https://www.rust-lang.org)
|
||||||
|
|
||||||
会话 · 聊天记录 · 搜索 · 联系人 · 群成员 · 收藏 · 统计 · 导出
|
会话 · 聊天记录 · 搜索 · 发送 · 联系人 · 群成员 · 收藏 · 统计 · 导出
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -38,6 +38,7 @@ npx skills add jackwener/wx-cli -g
|
||||||
- **毫秒级响应** — 后台 daemon 持久缓存解密数据库,mtime 不变则复用
|
- **毫秒级响应** — 后台 daemon 持久缓存解密数据库,mtime 不变则复用
|
||||||
- **AI 友好** — 默认 YAML 输出,更省 token & 易读;`--json` 可切换为 JSON(方便 `jq` 处理等)
|
- **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 "关键词" # 全库搜索
|
||||||
wx search "关键词" -n 500 # 放宽搜索结果条数
|
wx search "关键词" -n 500 # 放宽搜索结果条数
|
||||||
wx search "会议" --in "工作群" --since 2026-01-01
|
wx search "会议" --in "工作群" --since 2026-01-01
|
||||||
|
wx send "张三" "你好,今晚 8 点见" # 发送消息(macOS)
|
||||||
```
|
```
|
||||||
|
|
||||||
`history` / `search` / `export` 都支持 `-n` / `--limit` 指定条数。默认值只是为了避免一次性输出过多消息,不是硬上限。
|
`history` / `search` / `export` 都支持 `-n` / `--limit` 指定条数。默认值只是为了避免一次性输出过多消息,不是硬上限。
|
||||||
|
|
||||||
|
`send` 是 macOS 屏幕自动化命令:它会激活微信,用 `⌘F` 聚焦搜索框,打开目标聊天后发送文本。使用前需保持微信已登录,并给当前终端或 agent 应用开启"辅助功能"权限;发送过程中会临时使用剪贴板。
|
||||||
|
|
||||||
会话/消息输出里都带 `chat_type` 字段,取值为 `private` / `group` / `official_account` / `folded`。`official_account` 涵盖公众号、订阅号、服务号及 `mphelper` / `qqsafe` 等系统通知;`folded` 对应微信里的"订阅号折叠"和"折叠群聊"两个聚合入口。
|
会话/消息输出里都带 `chat_type` 字段,取值为 `private` / `group` / `official_account` / `folded`。`official_account` 涵盖公众号、订阅号、服务号及 `mphelper` / `qqsafe` 等系统通知;`folded` 对应微信里的"订阅号折叠"和"折叠群聊"两个聚合入口。
|
||||||
|
|
||||||
### 朋友圈(SNS)
|
### 朋友圈(SNS)
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ mod init;
|
||||||
pub mod sessions;
|
pub mod sessions;
|
||||||
pub mod history;
|
pub mod history;
|
||||||
pub mod search;
|
pub mod search;
|
||||||
|
pub mod send;
|
||||||
pub mod contacts;
|
pub mod contacts;
|
||||||
pub mod export;
|
pub mod export;
|
||||||
pub mod daemon_cmd;
|
pub mod daemon_cmd;
|
||||||
|
|
@ -92,6 +93,14 @@ enum Commands {
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
json: bool,
|
json: bool,
|
||||||
},
|
},
|
||||||
|
/// 发送微信消息(macOS 屏幕自动化)
|
||||||
|
Send {
|
||||||
|
/// 聊天对象名称
|
||||||
|
chat: String,
|
||||||
|
/// 要发送的消息
|
||||||
|
#[arg(allow_hyphen_values = true)]
|
||||||
|
message: String,
|
||||||
|
},
|
||||||
/// 查看联系人
|
/// 查看联系人
|
||||||
Contacts {
|
Contacts {
|
||||||
/// 按名字过滤
|
/// 按名字过滤
|
||||||
|
|
@ -282,6 +291,7 @@ fn dispatch(cli: Cli) -> Result<()> {
|
||||||
Commands::Search { keyword, chats, limit, since, until, msg_type, json } => {
|
Commands::Search { keyword, chats, limit, since, until, msg_type, json } => {
|
||||||
search::cmd_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::Contacts { query, limit, json } => contacts::cmd_contacts(query, limit, json),
|
||||||
Commands::Export { chat, since, until, limit, format, output } => {
|
Commands::Export { chat, since, until, limit, format, output } => {
|
||||||
export::cmd_export(chat, since, until, limit, format, output)
|
export::cmd_export(chat, since, until, limit, format, output)
|
||||||
|
|
|
||||||
|
|
@ -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");
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue