mirror of https://github.com/jackwener/wx-cli.git
555 lines
16 KiB
Rust
555 lines
16 KiB
Rust
pub mod attachments;
|
||
pub mod biz_articles;
|
||
pub mod contacts;
|
||
pub mod daemon_cmd;
|
||
pub mod export;
|
||
pub mod extract;
|
||
pub mod favorites;
|
||
pub mod history;
|
||
mod init;
|
||
pub mod members;
|
||
pub mod new_messages;
|
||
pub mod output;
|
||
pub mod search;
|
||
pub mod sessions;
|
||
pub mod sns_feed;
|
||
pub mod sns_notifications;
|
||
pub mod sns_search;
|
||
pub mod stats;
|
||
pub mod transport;
|
||
pub mod unread;
|
||
|
||
use self::output::OutputOpts;
|
||
use anyhow::Result;
|
||
use clap::{Parser, Subcommand};
|
||
|
||
/// wx — 微信本地数据 CLI
|
||
#[derive(Parser)]
|
||
#[command(name = "wx", version = env!("CARGO_PKG_VERSION"), about = "wx — 微信本地数据 CLI")]
|
||
pub struct Cli {
|
||
/// 返回更重的 freshness/source 元数据(如 per-shard latest、cache modes)
|
||
#[arg(long, global = true)]
|
||
with_meta: bool,
|
||
/// 在 meta 里暴露真实 shard 路径(调试用)
|
||
#[arg(long, global = true, hide = true)]
|
||
debug_source: bool,
|
||
#[command(subcommand)]
|
||
command: Commands,
|
||
}
|
||
|
||
#[derive(Subcommand)]
|
||
enum Commands {
|
||
/// 初始化:检测数据目录并扫描加密密钥
|
||
Init {
|
||
/// 强制重新扫描(覆盖已有配置)
|
||
#[arg(long)]
|
||
force: bool,
|
||
},
|
||
/// 列出最近会话
|
||
Sessions {
|
||
/// 会话数量
|
||
#[arg(short = 'n', long, default_value = "20")]
|
||
limit: usize,
|
||
/// 输出 JSON(默认 YAML)
|
||
#[arg(long)]
|
||
json: bool,
|
||
},
|
||
/// 查看聊天记录
|
||
History {
|
||
/// 聊天对象名称(支持模糊匹配)
|
||
chat: String,
|
||
/// 消息数量
|
||
#[arg(short = 'n', long, default_value = "50")]
|
||
limit: usize,
|
||
/// 分页偏移
|
||
#[arg(long, default_value = "0")]
|
||
offset: usize,
|
||
/// 起始时间 YYYY-MM-DD
|
||
#[arg(long)]
|
||
since: Option<String>,
|
||
/// 结束时间 YYYY-MM-DD
|
||
#[arg(long)]
|
||
until: Option<String>,
|
||
/// 消息类型过滤 [text|image|voice|video|sticker|location|link|file|call|system]
|
||
#[arg(long = "type", value_name = "TYPE",
|
||
value_parser = ["text","image","voice","video","sticker","location","link","file","call","system"])]
|
||
msg_type: Option<String>,
|
||
/// 输出 JSON(默认 YAML)
|
||
#[arg(long)]
|
||
json: bool,
|
||
},
|
||
/// 搜索消息
|
||
Search {
|
||
/// 搜索关键词
|
||
keyword: String,
|
||
/// 限定聊天(可多次指定)
|
||
#[arg(long = "in", value_name = "CHAT")]
|
||
chats: Vec<String>,
|
||
/// 结果数量
|
||
#[arg(short = 'n', long, default_value = "20")]
|
||
limit: usize,
|
||
/// 起始时间 YYYY-MM-DD
|
||
#[arg(long)]
|
||
since: Option<String>,
|
||
/// 结束时间 YYYY-MM-DD
|
||
#[arg(long)]
|
||
until: Option<String>,
|
||
/// 消息类型过滤 [text|image|voice|video|sticker|location|link|file|call|system]
|
||
#[arg(long = "type", value_name = "TYPE",
|
||
value_parser = ["text","image","voice","video","sticker","location","link","file","call","system"])]
|
||
msg_type: Option<String>,
|
||
/// 输出 JSON(默认 YAML)
|
||
#[arg(long)]
|
||
json: bool,
|
||
},
|
||
/// 查看联系人
|
||
Contacts {
|
||
/// 按名字过滤
|
||
#[arg(short = 'q', long)]
|
||
query: Option<String>,
|
||
/// 显示数量
|
||
#[arg(short = 'n', long, default_value = "50")]
|
||
limit: usize,
|
||
/// 输出 JSON(默认 YAML)
|
||
#[arg(long)]
|
||
json: bool,
|
||
},
|
||
/// 导出聊天记录到文件
|
||
Export {
|
||
/// 聊天对象名称
|
||
chat: String,
|
||
/// 起始时间 YYYY-MM-DD
|
||
#[arg(long)]
|
||
since: Option<String>,
|
||
/// 结束时间 YYYY-MM-DD
|
||
#[arg(long)]
|
||
until: Option<String>,
|
||
/// 最多导出条数
|
||
#[arg(short = 'n', long, default_value = "500")]
|
||
limit: usize,
|
||
/// 输出格式 [markdown|txt|json|yaml]
|
||
#[arg(short = 'f', long, default_value = "markdown", value_parser = ["markdown", "txt", "json", "yaml"])]
|
||
format: String,
|
||
/// 输出文件(默认 stdout)
|
||
#[arg(short = 'o', long)]
|
||
output: Option<String>,
|
||
},
|
||
/// 显示有未读消息的会话
|
||
Unread {
|
||
/// 显示数量
|
||
#[arg(short = 'n', long, default_value = "20")]
|
||
limit: usize,
|
||
/// 按会话类型过滤,逗号分隔。示例:--filter private,group 只看真人的未读
|
||
#[arg(long, value_name = "TYPES", value_delimiter = ',',
|
||
value_parser = ["all", "private", "group", "official", "folded"])]
|
||
filter: Vec<String>,
|
||
/// 输出 JSON(默认 YAML)
|
||
#[arg(long)]
|
||
json: bool,
|
||
},
|
||
/// 查看群成员
|
||
Members {
|
||
/// 群聊名称(支持模糊匹配)
|
||
chat: String,
|
||
/// 输出 JSON(默认 YAML)
|
||
#[arg(long)]
|
||
json: bool,
|
||
},
|
||
/// 获取自上次检查以来的新消息
|
||
NewMessages {
|
||
/// 显示数量上限
|
||
#[arg(short = 'n', long, default_value = "200")]
|
||
limit: usize,
|
||
/// 输出 JSON(默认 YAML)
|
||
#[arg(long)]
|
||
json: bool,
|
||
},
|
||
/// 聊天统计分析
|
||
Stats {
|
||
/// 聊天对象名称(支持模糊匹配)
|
||
chat: String,
|
||
/// 起始时间 YYYY-MM-DD
|
||
#[arg(long)]
|
||
since: Option<String>,
|
||
/// 结束时间 YYYY-MM-DD
|
||
#[arg(long)]
|
||
until: Option<String>,
|
||
/// 输出 JSON(默认 YAML)
|
||
#[arg(long)]
|
||
json: bool,
|
||
},
|
||
/// 查看微信收藏内容
|
||
Favorites {
|
||
/// 显示数量
|
||
#[arg(short = 'n', long, default_value = "50")]
|
||
limit: usize,
|
||
/// 类型过滤 [text|image|article|card|video]
|
||
#[arg(long = "type", value_name = "TYPE",
|
||
value_parser = ["text","image","article","card","video"])]
|
||
fav_type: Option<String>,
|
||
/// 内容关键词搜索
|
||
#[arg(short = 'q', long)]
|
||
query: Option<String>,
|
||
/// 输出 JSON(默认 YAML)
|
||
#[arg(long)]
|
||
json: bool,
|
||
},
|
||
/// 朋友圈互动通知:别人对我的朋友圈点赞/评论 + 我评过的帖子下的跟帖
|
||
SnsNotifications {
|
||
/// 显示数量
|
||
#[arg(short = 'n', long, default_value = "50")]
|
||
limit: usize,
|
||
/// 起始时间 YYYY-MM-DD
|
||
#[arg(long)]
|
||
since: Option<String>,
|
||
/// 结束时间 YYYY-MM-DD
|
||
#[arg(long)]
|
||
until: Option<String>,
|
||
/// 包含已读通知(默认仅未读)
|
||
#[arg(long)]
|
||
include_read: bool,
|
||
/// 输出 JSON(默认 YAML)
|
||
#[arg(long)]
|
||
json: bool,
|
||
},
|
||
/// 朋友圈时间线:按时间/作者筛选本地缓存的朋友圈
|
||
SnsFeed {
|
||
/// 显示数量
|
||
#[arg(short = 'n', long, default_value = "20")]
|
||
limit: usize,
|
||
/// 起始时间 YYYY-MM-DD
|
||
#[arg(long)]
|
||
since: Option<String>,
|
||
/// 结束时间 YYYY-MM-DD
|
||
#[arg(long)]
|
||
until: Option<String>,
|
||
/// 只看指定作者(昵称 / 备注名 / 微信 ID,模糊匹配)
|
||
#[arg(long)]
|
||
user: Option<String>,
|
||
/// 输出 JSON(默认 YAML)
|
||
#[arg(long)]
|
||
json: bool,
|
||
},
|
||
/// 查询公众号文章推送(本地缓存)
|
||
BizArticles {
|
||
/// 显示数量
|
||
#[arg(short = 'n', long, default_value = "50")]
|
||
limit: usize,
|
||
/// 限定公众号(名称模糊匹配)
|
||
#[arg(long)]
|
||
account: Option<String>,
|
||
/// 起始时间 YYYY-MM-DD
|
||
#[arg(long)]
|
||
since: Option<String>,
|
||
/// 结束时间 YYYY-MM-DD
|
||
#[arg(long)]
|
||
until: Option<String>,
|
||
/// 只看有未读的公众号,每个公众号取最新 1 篇
|
||
#[arg(long)]
|
||
unread: bool,
|
||
/// 输出 JSON(默认 YAML)
|
||
#[arg(long)]
|
||
json: bool,
|
||
},
|
||
/// 朋友圈全文搜索:匹配正文关键词
|
||
SnsSearch {
|
||
/// 关键词
|
||
keyword: String,
|
||
/// 结果数量
|
||
#[arg(short = 'n', long, default_value = "20")]
|
||
limit: usize,
|
||
/// 起始时间 YYYY-MM-DD
|
||
#[arg(long)]
|
||
since: Option<String>,
|
||
/// 结束时间 YYYY-MM-DD
|
||
#[arg(long)]
|
||
until: Option<String>,
|
||
/// 限定作者(昵称 / 备注名 / 微信 ID)
|
||
#[arg(long)]
|
||
user: Option<String>,
|
||
/// 输出 JSON(默认 YAML)
|
||
#[arg(long)]
|
||
json: bool,
|
||
},
|
||
/// 解密本地朋友圈 Sns/Img 缓存图片到目录
|
||
SnsExtract {
|
||
/// 输出目录
|
||
#[arg(short = 'o', long)]
|
||
output_dir: String,
|
||
/// 只扫描指定月份,例如 2026-06;默认扫描所有月份
|
||
#[arg(long)]
|
||
month: Option<String>,
|
||
/// 最多导出多少张
|
||
#[arg(short = 'n', long, default_value = "50")]
|
||
limit: usize,
|
||
/// 已存在时覆盖
|
||
#[arg(long)]
|
||
overwrite: bool,
|
||
/// SNS V2 tail XOR key;Windows 4.x 实测默认 0xe7
|
||
#[arg(long, default_value = "0xe7")]
|
||
xor_key: String,
|
||
/// 输出 JSON(默认 YAML)
|
||
#[arg(long)]
|
||
json: bool,
|
||
},
|
||
/// 列出某会话的图片附件,返回不透明 attachment_id
|
||
Attachments {
|
||
/// 会话名称(联系人显示名 / wxid / @chatroom username 都可以)
|
||
chat: String,
|
||
/// 类型(当前仅支持 image)
|
||
#[arg(long = "kind", value_name = "KIND",
|
||
value_parser = ["image", "img"])]
|
||
kinds: Vec<String>,
|
||
/// 显示数量
|
||
#[arg(short = 'n', long, default_value = "50")]
|
||
limit: usize,
|
||
/// 分页偏移
|
||
#[arg(long, default_value = "0")]
|
||
offset: usize,
|
||
/// 起始时间 YYYY-MM-DD
|
||
#[arg(long)]
|
||
since: Option<String>,
|
||
/// 结束时间 YYYY-MM-DD
|
||
#[arg(long)]
|
||
until: Option<String>,
|
||
/// 输出 JSON(默认 YAML)
|
||
#[arg(long)]
|
||
json: bool,
|
||
},
|
||
/// 把单个 attachment_id 对应的资源解密写到指定文件路径
|
||
Extract {
|
||
/// 由 `wx attachments` 输出的不透明 ID(base64url 字符串)
|
||
attachment_id: String,
|
||
/// 输出文件路径(绝对或相对当前工作目录均可;扩展名建议保留为 .jpg 等)
|
||
#[arg(short = 'o', long)]
|
||
output: String,
|
||
/// 目标已存在时覆盖
|
||
#[arg(long)]
|
||
overwrite: bool,
|
||
/// 输出 JSON(默认 YAML)
|
||
#[arg(long)]
|
||
json: bool,
|
||
},
|
||
/// 管理 wx-daemon
|
||
Daemon {
|
||
#[command(subcommand)]
|
||
cmd: DaemonCommands,
|
||
},
|
||
}
|
||
|
||
#[derive(Subcommand)]
|
||
pub enum DaemonCommands {
|
||
/// 查看 daemon 运行状态
|
||
Status,
|
||
/// 停止 daemon
|
||
Stop,
|
||
/// 查看 daemon 日志
|
||
Logs {
|
||
/// 持续输出(tail -f)
|
||
#[arg(short = 'f', long)]
|
||
follow: bool,
|
||
/// 显示最近 N 行
|
||
#[arg(short = 'n', long, default_value = "50")]
|
||
lines: usize,
|
||
},
|
||
}
|
||
|
||
pub fn run() {
|
||
let cli = Cli::parse();
|
||
if let Err(e) = dispatch(cli) {
|
||
eprintln!("错误: {}", e);
|
||
std::process::exit(1);
|
||
}
|
||
}
|
||
|
||
fn dispatch(cli: Cli) -> Result<()> {
|
||
let base_with_meta = cli.with_meta;
|
||
let base_debug_source = cli.debug_source;
|
||
match cli.command {
|
||
Commands::Init { force } => init::cmd_init(force),
|
||
Commands::Sessions { limit, json } => sessions::cmd_sessions(
|
||
limit,
|
||
OutputOpts {
|
||
json,
|
||
with_meta: base_with_meta,
|
||
debug_source: base_debug_source,
|
||
},
|
||
),
|
||
Commands::History {
|
||
chat,
|
||
limit,
|
||
offset,
|
||
since,
|
||
until,
|
||
msg_type,
|
||
json,
|
||
} => history::cmd_history(
|
||
chat,
|
||
limit,
|
||
offset,
|
||
since,
|
||
until,
|
||
msg_type,
|
||
OutputOpts {
|
||
json,
|
||
with_meta: base_with_meta,
|
||
debug_source: base_debug_source,
|
||
},
|
||
),
|
||
Commands::Search {
|
||
keyword,
|
||
chats,
|
||
limit,
|
||
since,
|
||
until,
|
||
msg_type,
|
||
json,
|
||
} => search::cmd_search(
|
||
keyword,
|
||
chats,
|
||
limit,
|
||
since,
|
||
until,
|
||
msg_type,
|
||
OutputOpts {
|
||
json,
|
||
with_meta: base_with_meta,
|
||
debug_source: base_debug_source,
|
||
},
|
||
),
|
||
Commands::Contacts { query, limit, json } => contacts::cmd_contacts(query, limit, json),
|
||
Commands::Export {
|
||
chat,
|
||
since,
|
||
until,
|
||
limit,
|
||
format,
|
||
output,
|
||
} => {
|
||
let export_json = format == "json";
|
||
export::cmd_export(
|
||
chat,
|
||
since,
|
||
until,
|
||
limit,
|
||
format,
|
||
output,
|
||
OutputOpts {
|
||
json: export_json,
|
||
with_meta: base_with_meta,
|
||
debug_source: base_debug_source,
|
||
},
|
||
)
|
||
}
|
||
Commands::Unread {
|
||
limit,
|
||
filter,
|
||
json,
|
||
} => unread::cmd_unread(
|
||
limit,
|
||
filter,
|
||
OutputOpts {
|
||
json,
|
||
with_meta: base_with_meta,
|
||
debug_source: base_debug_source,
|
||
},
|
||
),
|
||
Commands::Members { chat, json } => members::cmd_members(chat, json),
|
||
Commands::NewMessages { limit, json } => new_messages::cmd_new_messages(
|
||
limit,
|
||
OutputOpts {
|
||
json,
|
||
with_meta: base_with_meta,
|
||
debug_source: base_debug_source,
|
||
},
|
||
),
|
||
Commands::Stats {
|
||
chat,
|
||
since,
|
||
until,
|
||
json,
|
||
} => stats::cmd_stats(
|
||
chat,
|
||
since,
|
||
until,
|
||
OutputOpts {
|
||
json,
|
||
with_meta: base_with_meta,
|
||
debug_source: base_debug_source,
|
||
},
|
||
),
|
||
Commands::Favorites {
|
||
limit,
|
||
fav_type,
|
||
query,
|
||
json,
|
||
} => favorites::cmd_favorites(limit, fav_type, query, json),
|
||
Commands::SnsNotifications {
|
||
limit,
|
||
since,
|
||
until,
|
||
include_read,
|
||
json,
|
||
} => sns_notifications::cmd_sns_notifications(limit, since, until, include_read, json),
|
||
Commands::SnsFeed {
|
||
limit,
|
||
since,
|
||
until,
|
||
user,
|
||
json,
|
||
} => sns_feed::cmd_sns_feed(limit, since, until, user, json),
|
||
Commands::SnsSearch {
|
||
keyword,
|
||
limit,
|
||
since,
|
||
until,
|
||
user,
|
||
json,
|
||
} => sns_search::cmd_sns_search(keyword, limit, since, until, user, json),
|
||
Commands::BizArticles {
|
||
limit,
|
||
account,
|
||
since,
|
||
until,
|
||
unread,
|
||
json,
|
||
} => biz_articles::cmd_biz_articles(limit, account, since, until, unread, json),
|
||
Commands::Attachments {
|
||
chat,
|
||
kinds,
|
||
limit,
|
||
offset,
|
||
since,
|
||
until,
|
||
json,
|
||
} => attachments::cmd_attachments(
|
||
chat,
|
||
kinds,
|
||
limit,
|
||
offset,
|
||
since,
|
||
until,
|
||
OutputOpts {
|
||
json,
|
||
with_meta: base_with_meta,
|
||
debug_source: base_debug_source,
|
||
},
|
||
),
|
||
Commands::SnsExtract {
|
||
output_dir,
|
||
month,
|
||
limit,
|
||
overwrite,
|
||
xor_key,
|
||
json,
|
||
} => extract::cmd_sns_extract(output_dir, month, limit, overwrite, xor_key, json),
|
||
Commands::Extract {
|
||
attachment_id,
|
||
output,
|
||
overwrite,
|
||
json,
|
||
} => extract::cmd_extract(attachment_id, output, overwrite, json),
|
||
Commands::Daemon { cmd } => daemon_cmd::cmd_daemon(cmd),
|
||
}
|
||
}
|