use serde::{Deserialize, Serialize}; use serde_json::Value; use std::collections::HashMap; /// CLI 向 daemon 发送的请求(换行符分隔 JSON,与 Python 版兼容) #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "cmd", rename_all = "snake_case")] pub enum Request { Ping, Sessions { #[serde(default = "default_limit_20")] limit: usize, #[serde(default, skip_serializing_if = "is_false")] with_meta: bool, #[serde(default, skip_serializing_if = "is_false")] debug_source: bool, }, History { chat: String, #[serde(default = "default_limit_50")] limit: usize, #[serde(default)] offset: usize, #[serde(skip_serializing_if = "Option::is_none")] since: Option, #[serde(skip_serializing_if = "Option::is_none")] until: Option, #[serde(skip_serializing_if = "Option::is_none")] msg_type: Option, #[serde(default, skip_serializing_if = "is_false")] with_meta: bool, #[serde(default, skip_serializing_if = "is_false")] debug_source: bool, }, Search { keyword: String, #[serde(skip_serializing_if = "Option::is_none")] chats: Option>, #[serde(default = "default_limit_20")] limit: usize, #[serde(skip_serializing_if = "Option::is_none")] since: Option, #[serde(skip_serializing_if = "Option::is_none")] until: Option, #[serde(skip_serializing_if = "Option::is_none")] msg_type: Option, #[serde(default, skip_serializing_if = "is_false")] with_meta: bool, #[serde(default, skip_serializing_if = "is_false")] debug_source: bool, }, Contacts { #[serde(skip_serializing_if = "Option::is_none")] query: Option, #[serde(default = "default_limit_50")] limit: usize, }, Unread { #[serde(default = "default_limit_20")] limit: usize, /// 按会话类型过滤:private / group / official / folded / all,支持多选 #[serde(default, skip_serializing_if = "Option::is_none")] filter: Option>, #[serde(default, skip_serializing_if = "is_false")] with_meta: bool, #[serde(default, skip_serializing_if = "is_false")] debug_source: bool, }, Members { chat: String, }, NewMessages { /// 上次检查时各会话的 last_timestamp 快照(username -> ts) /// None 表示首次运行,会返回 new_state 供下次使用 #[serde(skip_serializing_if = "Option::is_none")] state: Option>, #[serde(default = "default_limit_200")] limit: usize, #[serde(default, skip_serializing_if = "is_false")] with_meta: bool, #[serde(default, skip_serializing_if = "is_false")] debug_source: bool, }, Stats { chat: String, #[serde(skip_serializing_if = "Option::is_none")] since: Option, #[serde(skip_serializing_if = "Option::is_none")] until: Option, #[serde(default, skip_serializing_if = "is_false")] with_meta: bool, #[serde(default, skip_serializing_if = "is_false")] debug_source: bool, }, Favorites { #[serde(default = "default_limit_50")] limit: usize, /// 类型过滤:1=文本,2=图片,5=文章,19=名片,20=视频 #[serde(skip_serializing_if = "Option::is_none")] fav_type: Option, /// 内容关键词搜索 #[serde(skip_serializing_if = "Option::is_none")] query: Option, }, /// 朋友圈互动通知(点赞 + 评论) SnsNotifications { #[serde(default = "default_limit_50")] limit: usize, #[serde(skip_serializing_if = "Option::is_none")] since: Option, #[serde(skip_serializing_if = "Option::is_none")] until: Option, /// 包含已读通知(默认仅未读) #[serde(default)] include_read: bool, }, /// 朋友圈时间线(按时间 / 作者筛选帖子) SnsFeed { #[serde(default = "default_limit_20")] limit: usize, #[serde(skip_serializing_if = "Option::is_none")] since: Option, #[serde(skip_serializing_if = "Option::is_none")] until: Option, /// 作者昵称 / 备注名 / 微信 username,模糊匹配 #[serde(skip_serializing_if = "Option::is_none")] user: Option, }, /// 查询公众号文章推送(biz_message_*.db 分片) BizArticles { #[serde(default = "default_limit_50")] limit: usize, /// 公众号名称过滤(模糊匹配 display name,None = 全部) #[serde(skip_serializing_if = "Option::is_none")] account: Option, #[serde(skip_serializing_if = "Option::is_none")] since: Option, #[serde(skip_serializing_if = "Option::is_none")] until: Option, /// 只看有未读消息的公众号,每个公众号取最新 1 篇 #[serde(default)] unread: bool, }, /// 朋友圈全文搜索(匹配 contentDesc) SnsSearch { keyword: String, #[serde(default = "default_limit_20")] limit: usize, #[serde(skip_serializing_if = "Option::is_none")] since: Option, #[serde(skip_serializing_if = "Option::is_none")] until: Option, #[serde(skip_serializing_if = "Option::is_none")] user: Option, }, /// 重新加载配置和密钥(init --force 后 daemon 不会自动重读) ReloadConfig, /// 列出某个会话里的图片附件 /// 输出每条带 `attachment_id`(不透明 base64url 句柄),传给 `Extract` 时取回本体 Attachments { chat: String, /// 类型过滤:当前仅支持 image #[serde(default, skip_serializing_if = "Option::is_none")] kinds: Option>, #[serde(default = "default_limit_50")] limit: usize, #[serde(default)] offset: usize, #[serde(skip_serializing_if = "Option::is_none")] since: Option, #[serde(skip_serializing_if = "Option::is_none")] until: Option, #[serde(default, skip_serializing_if = "is_false")] with_meta: bool, #[serde(default, skip_serializing_if = "is_false")] debug_source: bool, }, /// 提取(解密)单个附件的本体到指定路径 Extract { /// `Attachments` 返回的不透明 ID attachment_id: String, /// 写入的绝对路径(daemon 直接写盘,不经 socket 传 binary) output: String, /// 已存在时是否覆盖 #[serde(default)] overwrite: bool, }, /// 解密本地朋友圈 Sns/Img 缓存图片到目录 SnsExtract { /// 输出目录 output_dir: String, /// 只扫描指定月份,如 2026-06;为空则扫描所有月份 #[serde(skip_serializing_if = "Option::is_none")] month: Option, /// 最多导出多少张 #[serde(default = "default_limit_50")] limit: usize, /// 已存在时是否覆盖 #[serde(default)] overwrite: bool, /// SNS V2 tail XOR key;Windows 4.x 实测默认 0xe7 #[serde(default = "default_sns_xor_key")] xor_key: u8, }, } fn default_sns_xor_key() -> u8 { 0xe7 } /// daemon 的响应 #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Response { pub ok: bool, #[serde(skip_serializing_if = "Option::is_none")] pub error: Option, #[serde(flatten)] pub data: Value, } impl Response { pub fn ok(data: Value) -> Self { Self { ok: true, error: None, data, } } pub fn err(msg: impl Into) -> Self { Self { ok: false, error: Some(msg.into()), data: Value::Null, } } pub fn to_json_line(&self) -> anyhow::Result { let s = serde_json::to_string(self)?; Ok(s + "\n") } } fn default_limit_20() -> usize { 20 } fn default_limit_50() -> usize { 50 } fn default_limit_200() -> usize { 200 } fn is_false(v: &bool) -> bool { !*v }