mirror of https://github.com/jackwener/wx-cli.git
Merge branch 'jackwener:main' into main
commit
1d8c184c3c
33
README.md
33
README.md
|
|
@ -8,7 +8,7 @@
|
||||||
[](#安装)
|
[](#安装)
|
||||||
[](https://www.rust-lang.org)
|
[](https://www.rust-lang.org)
|
||||||
|
|
||||||
会话 · 聊天记录 · 搜索 · 联系人 · 群成员 · 收藏 · 统计 · 导出
|
会话 · 聊天记录 · 搜索 · 联系人 · 群成员 · 群昵称 · 收藏 · 统计 · 导出
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -100,10 +100,16 @@ cargo build --release
|
||||||
# 1. 签名(只需做一次,WeChat 更新后重做)
|
# 1. 签名(只需做一次,WeChat 更新后重做)
|
||||||
codesign --force --deep --sign - /Applications/WeChat.app
|
codesign --force --deep --sign - /Applications/WeChat.app
|
||||||
|
|
||||||
# 2. 重启微信,等待完全登录
|
# 2. 清理旧 TCC 授权记录(重签名后必做,否则微信截图/通话权限可能 silent 失效)
|
||||||
|
for s in ScreenCapture Camera Microphone AppleEvents AddressBook \
|
||||||
|
SystemPolicyDocumentsFolder SystemPolicyDownloadsFolder SystemPolicyDesktopFolder; do
|
||||||
|
tccutil reset "$s" com.tencent.xinWeChat
|
||||||
|
done
|
||||||
|
|
||||||
|
# 3. 重启微信,等待完全登录
|
||||||
killall WeChat && open /Applications/WeChat.app
|
killall WeChat && open /Applications/WeChat.app
|
||||||
|
|
||||||
# 3. 初始化
|
# 4. 初始化
|
||||||
sudo wx init
|
sudo wx init
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -112,6 +118,8 @@ sudo wx init
|
||||||
> codesign --remove-signature "/Applications/WeChat.app/Contents/Frameworks/vlc_plugins/librtp_mpeg4_plugin.dylib"
|
> codesign --remove-signature "/Applications/WeChat.app/Contents/Frameworks/vlc_plugins/librtp_mpeg4_plugin.dylib"
|
||||||
> codesign --force --deep --sign - /Applications/WeChat.app
|
> codesign --force --deep --sign - /Applications/WeChat.app
|
||||||
> ```
|
> ```
|
||||||
|
>
|
||||||
|
> 重签名后 macOS 的 TCC 隐私授权按新 code signature 重新校验,旧记录会失效。如果跳过 `tccutil reset`,微信截图/视频通话/麦克风等权限可能"看起来已开启但实际拒绝"。详见 [macOS 权限与签名指南](docs/macos-permission-guide.md#五重签名后微信权限-silent-失效)。
|
||||||
|
|
||||||
**Linux**
|
**Linux**
|
||||||
|
|
||||||
|
|
@ -158,6 +166,17 @@ wx search "会议" --in "工作群" --since 2026-01-01
|
||||||
|
|
||||||
会话/消息输出里都带 `chat_type` 字段,取值为 `private` / `group` / `official_account` / `folded`。`official_account` 涵盖公众号、订阅号、服务号及 `mphelper` / `qqsafe` 等系统通知;`folded` 对应微信里的"订阅号折叠"和"折叠群聊"两个聚合入口。
|
会话/消息输出里都带 `chat_type` 字段,取值为 `private` / `group` / `official_account` / `folded`。`official_account` 涵盖公众号、订阅号、服务号及 `mphelper` / `qqsafe` 等系统通知;`folded` 对应微信里的"订阅号折叠"和"折叠群聊"两个聚合入口。
|
||||||
|
|
||||||
|
群聊里的 `last_sender`、`sender` 和 `stats` 的 `top_senders` 会优先使用群昵称(群名片)。如果本地数据库里没有对应群昵称,则回退到联系人备注、微信昵称或 username。
|
||||||
|
|
||||||
|
引用消息会在 `history` / `search` / `new-messages` 输出中显示当前回复和被引用原文:
|
||||||
|
|
||||||
|
```text
|
||||||
|
[引用] 当前回复
|
||||||
|
↳ 发送者: 被引用内容
|
||||||
|
```
|
||||||
|
|
||||||
|
`--type link` / `--type file` 会包含微信 appmsg 里的链接、文件、合并聊天记录和引用消息等变体;搜索时也会匹配解压后可见的引用原文。
|
||||||
|
|
||||||
### 朋友圈(SNS)
|
### 朋友圈(SNS)
|
||||||
|
|
||||||
三个独立命令,区分"通知"和"帖子":
|
三个独立命令,区分"通知"和"帖子":
|
||||||
|
|
@ -187,6 +206,14 @@ wx contacts --query "李" # 按名字搜索
|
||||||
wx members "AI交流群" # 群成员列表
|
wx members "AI交流群" # 群成员列表
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`wx members --json` 返回的成员字段包括:
|
||||||
|
|
||||||
|
- `username`:微信内部 username
|
||||||
|
- `display`:用于展示的名称,优先使用群昵称
|
||||||
|
- `contact_display`:联系人备注或微信昵称
|
||||||
|
- `group_nickname`:群昵称;本地没有记录时为空字符串
|
||||||
|
- `is_owner`:是否群主
|
||||||
|
|
||||||
### 收藏 & 统计
|
### 收藏 & 统计
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
|
||||||
45
SKILL.md
45
SKILL.md
|
|
@ -11,6 +11,7 @@ description: "wx-cli — 从本地微信数据库查询聊天记录、联系人
|
||||||
- 微信消息历史
|
- 微信消息历史
|
||||||
- 微信联系人
|
- 微信联系人
|
||||||
- 微信群成员
|
- 微信群成员
|
||||||
|
- 微信群昵称 / 群名片
|
||||||
- 微信收藏
|
- 微信收藏
|
||||||
- wechat history / messages / contacts
|
- wechat history / messages / contacts
|
||||||
- wx-cli
|
- wx-cli
|
||||||
|
|
@ -65,14 +66,33 @@ codesign --remove-signature "/Applications/WeChat.app/Contents/Frameworks/vlc_pl
|
||||||
codesign --force --deep --sign - /Applications/WeChat.app
|
codesign --force --deep --sign - /Applications/WeChat.app
|
||||||
```
|
```
|
||||||
|
|
||||||
**第二步:重启 WeChat**
|
**第二步:清理 WeChat 在 macOS TCC 隐私数据库里的旧授权记录**(重签名后必做)
|
||||||
|
|
||||||
|
macOS TCC 按 `bundle id + csreq` 联合校验权限;csreq 编码自代码签名。重签名后旧 csreq 和新签名不再匹配,旧授权记录会 silent 失效(System Settings 仍把开关画成"已允许",运行时实际拒绝)。把 WeChat 在 TCC 里的旧记录抹掉,让 macOS 在下次微信请求权限时按新签名重新生成 csreq:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tccutil reset ScreenCapture com.tencent.xinWeChat # 截图 / 屏幕共享
|
||||||
|
tccutil reset Camera com.tencent.xinWeChat # 视频通话 / 扫码
|
||||||
|
tccutil reset Microphone com.tencent.xinWeChat # 语音消息 / 通话
|
||||||
|
tccutil reset AppleEvents com.tencent.xinWeChat # 自动化 / 输入法
|
||||||
|
tccutil reset AddressBook com.tencent.xinWeChat # 通讯录
|
||||||
|
tccutil reset SystemPolicyDocumentsFolder com.tencent.xinWeChat
|
||||||
|
tccutil reset SystemPolicyDownloadsFolder com.tencent.xinWeChat
|
||||||
|
tccutil reset SystemPolicyDesktopFolder com.tencent.xinWeChat
|
||||||
|
```
|
||||||
|
|
||||||
|
`tccutil` 对没有授权过的 service 会报 "No such bundle identifier",是 no-op,不影响其他 service 的 reset。
|
||||||
|
|
||||||
|
**第三步:重启 WeChat**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
killall WeChat && open /Applications/WeChat.app
|
killall WeChat && open /Applications/WeChat.app
|
||||||
# 等待微信完全登录后再继续
|
# 等待微信完全登录后再继续
|
||||||
```
|
```
|
||||||
|
|
||||||
**第三步:初始化**
|
之后微信触发权限请求时按 GUI 提示重新允许即可。在 macOS 26 上,把 WeChat 加进 **隐私与安全 → 录屏与系统录音** 的上半区,**不要**只勾下半区的"仅系统录音"——后者不能授予截图权限。
|
||||||
|
|
||||||
|
**第四步:初始化**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo wx init
|
sudo wx init
|
||||||
|
|
@ -137,6 +157,17 @@ wx search "会议" --in "工作群" --since 2026-01-01
|
||||||
|
|
||||||
`wx unread --filter` 支持 `private` / `group` / `official` / `folded` / `all`,逗号分隔多选。默认 `all`。
|
`wx unread --filter` 支持 `private` / `group` / `official` / `folded` / `all`,逗号分隔多选。默认 `all`。
|
||||||
|
|
||||||
|
群聊消息里的 `last_sender`、`sender` 和 `stats.top_senders` 会优先显示群昵称(群名片)。如果本地数据库没有群昵称,再回退到联系人备注、微信昵称或 username。
|
||||||
|
|
||||||
|
引用消息(appmsg `type=57`)在 `history` / `search` / `new-messages` 输出里会展开为两行:第一行是当前回复,第二行以 `↳` 开头显示被引用原文,例如:
|
||||||
|
|
||||||
|
```text
|
||||||
|
[引用] 当前回复
|
||||||
|
↳ 发送者: 被引用内容
|
||||||
|
```
|
||||||
|
|
||||||
|
`--type link` / `--type file` 会覆盖微信 appmsg 的链接、文件、合并聊天记录和引用消息等变体;`search --type link` 也会匹配解压并格式化后的引用原文。
|
||||||
|
|
||||||
### 联系人与群组
|
### 联系人与群组
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
@ -148,6 +179,16 @@ wx contacts --query "李"
|
||||||
wx members "AI交流群"
|
wx members "AI交流群"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`wx members --json` 每个成员包含:
|
||||||
|
|
||||||
|
- `username`:微信内部 username
|
||||||
|
- `display`:推荐展示名,优先使用群昵称
|
||||||
|
- `contact_display`:联系人备注或微信昵称
|
||||||
|
- `group_nickname`:群昵称;没有记录时为空字符串
|
||||||
|
- `is_owner`:是否群主
|
||||||
|
|
||||||
|
Agent 展示群成员时优先用 `display`。需要区分群昵称和联系人名时,再读取 `group_nickname` 与 `contact_display`。
|
||||||
|
|
||||||
### 朋友圈(SNS)
|
### 朋友圈(SNS)
|
||||||
|
|
||||||
三个命令,作用各不同:
|
三个命令,作用各不同:
|
||||||
|
|
|
||||||
|
|
@ -196,3 +196,79 @@ open /Applications/WeChat.app
|
||||||
| "SIP 阻止了调试微信" | ❌ SIP 只保护系统进程,微信不受 SIP 保护 |
|
| "SIP 阻止了调试微信" | ❌ SIP 只保护系统进程,微信不受 SIP 保护 |
|
||||||
| "加了 sshd 到 FDA 就行" | ❌ 还需要加 `sshd-keygen-wrapper`,且要重连 SSH |
|
| "加了 sshd 到 FDA 就行" | ❌ 还需要加 `sshd-keygen-wrapper`,且要重连 SSH |
|
||||||
| "微信开着也能重签名" | ❌ 运行中的 binary/dylib 被占用,codesign 会失败 |
|
| "微信开着也能重签名" | ❌ 运行中的 binary/dylib 被占用,codesign 会失败 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、重签名后微信权限 silent 失效
|
||||||
|
|
||||||
|
### 现象
|
||||||
|
|
||||||
|
完成 ad-hoc 重签名后,微信任意以下功能都可能"看起来已授权但实际被拒绝":
|
||||||
|
|
||||||
|
- 截图 / 屏幕共享(`ScreenCapture`)
|
||||||
|
- 视频通话 / 扫码(`Camera`)
|
||||||
|
- 语音消息 / 通话(`Microphone`)
|
||||||
|
- 自动化、第三方输入法(`AppleEvents`)
|
||||||
|
- 同步通讯录(`AddressBook`)
|
||||||
|
- 文件发送 / 接收(`SystemPolicyDocumentsFolder` / `Downloads` / `Desktop`)
|
||||||
|
|
||||||
|
System Settings 里通常仍看到"微信.app"开关是 ON,但运行时权限校验失败。微信会反复弹"需要开启 X 权限"。
|
||||||
|
|
||||||
|
### 根因(第一性原理)
|
||||||
|
|
||||||
|
macOS TCC(Transparency, Consent, and Control)按 **bundle id + csreq** 联合校验权限。`csreq`(code requirement)是从 app 的 code signature 推导出的二进制 blob,存在 `/Library/Application Support/com.apple.TCC/TCC.db` 的 `access` 表里,每条 ~160 字节。
|
||||||
|
|
||||||
|
`codesign --force --deep --sign -` 把 WeChat 从官方签名换成 ad-hoc 签名(甚至 ad-hoc → ad-hoc 重签也会变),新进程的 csreq 跟旧记录里那条对不上 —— tccd 拒绝。
|
||||||
|
|
||||||
|
System Settings UI 只按 client 显示开关、不重算 csreq,所以视觉上是"已授权",运行时实际拒绝。这是 silent drift。
|
||||||
|
|
||||||
|
### 修复步骤
|
||||||
|
|
||||||
|
把 WeChat 在 TCC 里的旧记录全部抹掉,让 macOS 在下次微信请求权限时按新签名重新生成 csreq:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
for s in ScreenCapture Camera Microphone AppleEvents AddressBook \
|
||||||
|
SystemPolicyDocumentsFolder SystemPolicyDownloadsFolder SystemPolicyDesktopFolder; do
|
||||||
|
tccutil reset "$s" com.tencent.xinWeChat
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
|
`tccutil` 对没有授权过的 service 会报 "No such bundle identifier",这是 no-op,不影响其他 service 的 reset。
|
||||||
|
|
||||||
|
之后退出并重新打开微信,按 GUI 提示重新允许:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
killall WeChat
|
||||||
|
open /Applications/WeChat.app
|
||||||
|
```
|
||||||
|
|
||||||
|
> 这一步**应当由用户/agent 手动执行**,不在 `wx init` 里自动跑——TCC 重置会让用户的现有授权失效,需要由人决定时机。
|
||||||
|
|
||||||
|
#### macOS 26 的 UI 拆分
|
||||||
|
|
||||||
|
在 macOS 26 上,**隐私与安全 → 录屏与系统录音** 显示为两块,容易踩坑:
|
||||||
|
|
||||||
|
| 区域 | 作用 |
|
||||||
|
|------|------|
|
||||||
|
| **录屏与系统录音**(上半区) | 录制屏幕内容 + 系统音频;微信截图、屏幕共享需要这一项 |
|
||||||
|
| **仅系统录音**(下半区) | 只录系统音频;只打开这一项**不能**修复微信截图 |
|
||||||
|
|
||||||
|
把 WeChat 加进上半区;只勾下半区的"仅系统录音"无效。
|
||||||
|
|
||||||
|
### 验证
|
||||||
|
|
||||||
|
确认 WeChat 当前是 ad-hoc 签名(这是修复前提):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
codesign -dv --verbose=4 /Applications/WeChat.app 2>&1 | grep -E "Signature|flags|TeamIdentifier"
|
||||||
|
```
|
||||||
|
|
||||||
|
期望看到:
|
||||||
|
|
||||||
|
```text
|
||||||
|
flags=0x2(adhoc)
|
||||||
|
Signature=adhoc
|
||||||
|
TeamIdentifier=not set
|
||||||
|
```
|
||||||
|
|
||||||
|
最直接的功能验证:在微信里使用截图、视频通话、麦克风等功能,按 GUI 弹窗的"允许"重新授权一次,之后正常工作。
|
||||||
|
|
|
||||||
|
|
@ -118,6 +118,10 @@ pub fn cmd_init(force: bool) -> Result<()> {
|
||||||
std::fs::write(&config_path, serde_json::to_string_pretty(&cfg)?)
|
std::fs::write(&config_path, serde_json::to_string_pretty(&cfg)?)
|
||||||
.context("写入 config.json 失败")?;
|
.context("写入 config.json 失败")?;
|
||||||
println!("配置已保存: {}", config_path.display());
|
println!("配置已保存: {}", config_path.display());
|
||||||
|
|
||||||
|
// init 之后必须停掉旧 daemon(它用的是旧 config),下次调用会自动重启
|
||||||
|
let _ = crate::cli::transport::stop_daemon();
|
||||||
|
|
||||||
println!("初始化完成,可以使用 wx sessions / wx history 等命令了");
|
println!("初始化完成,可以使用 wx sessions / wx history 等命令了");
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
||||||
|
|
@ -124,6 +124,31 @@ pub fn ensure_daemon() -> Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 停止 daemon(如果正在运行)
|
||||||
|
pub fn stop_daemon() -> Result<()> {
|
||||||
|
let pid_path = config::pid_path();
|
||||||
|
if let Ok(pid_str) = std::fs::read_to_string(&pid_path) {
|
||||||
|
if let Ok(pid) = pid_str.trim().parse::<u32>() {
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
let _ = std::process::Command::new("kill")
|
||||||
|
.arg("-TERM")
|
||||||
|
.arg(pid.to_string())
|
||||||
|
.spawn();
|
||||||
|
}
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
|
let _ = std::process::Command::new("taskkill")
|
||||||
|
.args(["/F", "/PID", &pid.to_string()])
|
||||||
|
.spawn();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let _ = std::fs::remove_file(config::sock_path());
|
||||||
|
let _ = std::fs::remove_file(&pid_path);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// 启动 daemon 前检查 `~/.wx-cli/` 可写,给出比"超时"更明确的错误。
|
/// 启动 daemon 前检查 `~/.wx-cli/` 可写,给出比"超时"更明确的错误。
|
||||||
///
|
///
|
||||||
/// 典型坑:旧版本 `sudo wx init` 把目录留成 root 属主,非 root 的 daemon
|
/// 典型坑:旧版本 `sudo wx init` 把目录留成 root 属主,非 root 的 daemon
|
||||||
|
|
|
||||||
109
src/config.rs
109
src/config.rs
|
|
@ -71,7 +71,8 @@ fn find_config_file() -> Result<PathBuf> {
|
||||||
return Ok(cwd);
|
return Ok(cwd);
|
||||||
}
|
}
|
||||||
// 3. ~/.wx-cli/config.json
|
// 3. ~/.wx-cli/config.json
|
||||||
if let Some(home) = dirs::home_dir() {
|
let home = cli_home_dir();
|
||||||
|
if home != PathBuf::from("/tmp") {
|
||||||
let p = home.join(".wx-cli").join("config.json");
|
let p = home.join(".wx-cli").join("config.json");
|
||||||
if p.exists() {
|
if p.exists() {
|
||||||
return Ok(p);
|
return Ok(p);
|
||||||
|
|
@ -87,9 +88,44 @@ fn find_config_file() -> Result<PathBuf> {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn cli_dir() -> PathBuf {
|
pub fn cli_dir() -> PathBuf {
|
||||||
dirs::home_dir()
|
cli_home_dir().join(".wx-cli")
|
||||||
.unwrap_or_else(|| PathBuf::from("/tmp"))
|
}
|
||||||
.join(".wx-cli")
|
|
||||||
|
fn cli_home_dir() -> PathBuf {
|
||||||
|
resolve_cli_home(
|
||||||
|
dirs::home_dir().unwrap_or_else(|| PathBuf::from("/tmp")),
|
||||||
|
sudo_user_home_dir(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_cli_home(default_home: PathBuf, sudo_home: Option<PathBuf>) -> PathBuf {
|
||||||
|
sudo_home.unwrap_or(default_home)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
fn sudo_user_home_dir() -> Option<PathBuf> {
|
||||||
|
use std::ffi::{CStr, CString};
|
||||||
|
|
||||||
|
let sudo_user = std::env::var("SUDO_USER").ok()?;
|
||||||
|
let sudo_user = sudo_user.trim();
|
||||||
|
if sudo_user.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let c_user = CString::new(sudo_user).ok()?;
|
||||||
|
unsafe {
|
||||||
|
let pwd = libc::getpwnam(c_user.as_ptr());
|
||||||
|
if pwd.is_null() || (*pwd).pw_dir.is_null() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let dir = CStr::from_ptr((*pwd).pw_dir).to_str().ok()?;
|
||||||
|
Some(PathBuf::from(dir))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(unix))]
|
||||||
|
fn sudo_user_home_dir() -> Option<PathBuf> {
|
||||||
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn sock_path() -> PathBuf {
|
pub fn sock_path() -> PathBuf {
|
||||||
|
|
@ -154,17 +190,7 @@ pub fn auto_detect_db_dir() -> Option<PathBuf> {
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
fn detect_db_dir_impl() -> Option<PathBuf> {
|
fn detect_db_dir_impl() -> Option<PathBuf> {
|
||||||
let home = dirs::home_dir()?;
|
let home = sudo_user_home_dir().or_else(dirs::home_dir)?;
|
||||||
// 支持 sudo 环境
|
|
||||||
let home = if let Ok(sudo_user) = std::env::var("SUDO_USER") {
|
|
||||||
if !sudo_user.is_empty() {
|
|
||||||
PathBuf::from("/Users").join(&sudo_user)
|
|
||||||
} else {
|
|
||||||
home
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
home
|
|
||||||
};
|
|
||||||
|
|
||||||
let base = home.join("Library/Containers/com.tencent.xinWeChat/Data/Documents/xwechat_files");
|
let base = home.join("Library/Containers/com.tencent.xinWeChat/Data/Documents/xwechat_files");
|
||||||
if !base.exists() {
|
if !base.exists() {
|
||||||
|
|
@ -190,9 +216,7 @@ fn detect_db_dir_impl() -> Option<PathBuf> {
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
fn detect_db_dir_impl() -> Option<PathBuf> {
|
fn detect_db_dir_impl() -> Option<PathBuf> {
|
||||||
let home = dirs::home_dir()?;
|
let home = dirs::home_dir()?;
|
||||||
let sudo_home = std::env::var("SUDO_USER").ok()
|
let sudo_home = sudo_user_home_dir();
|
||||||
.filter(|s| !s.is_empty())
|
|
||||||
.map(|u| PathBuf::from("/home").join(u));
|
|
||||||
|
|
||||||
let mut candidates: Vec<PathBuf> = Vec::new();
|
let mut candidates: Vec<PathBuf> = Vec::new();
|
||||||
for base_home in [Some(home.clone()), sudo_home].into_iter().flatten() {
|
for base_home in [Some(home.clone()), sudo_home].into_iter().flatten() {
|
||||||
|
|
@ -213,13 +237,32 @@ fn detect_db_dir_impl() -> Option<PathBuf> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
candidates.sort_by_key(|p| {
|
candidates.sort_by_key(|p| {
|
||||||
std::fs::metadata(p)
|
// 排序:取 db_storage 目录下所有 .db 文件的最新 mtime,而非目录自身的 mtime
|
||||||
.and_then(|m| m.modified())
|
// 这样当收到新消息时(只有 .db 文件被更新),能正确识别最新目录
|
||||||
.unwrap_or(std::time::SystemTime::UNIX_EPOCH)
|
latest_db_mtime(p).unwrap_or(std::time::SystemTime::UNIX_EPOCH)
|
||||||
});
|
});
|
||||||
candidates.into_iter().next_back()
|
candidates.into_iter().next_back()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 递归查找 db_storage 目录下所有 .db 文件的最新 mtime
|
||||||
|
fn latest_db_mtime(dir: &Path) -> Option<std::time::SystemTime> {
|
||||||
|
let mut latest = None;
|
||||||
|
if let Ok(entries) = std::fs::read_dir(dir) {
|
||||||
|
for entry in entries.flatten() {
|
||||||
|
let path = entry.path();
|
||||||
|
let mtime = if path.is_dir() {
|
||||||
|
latest_db_mtime(&path).unwrap_or(std::time::SystemTime::UNIX_EPOCH)
|
||||||
|
} else if path.extension().and_then(|s| s.to_str()) == Some("db") {
|
||||||
|
entry.metadata().and_then(|m| m.modified()).unwrap_or(std::time::SystemTime::UNIX_EPOCH)
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
latest = Some(latest.map_or(mtime, |cur| if mtime > cur { mtime } else { cur }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
latest
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
fn detect_db_dir_impl() -> Option<PathBuf> {
|
fn detect_db_dir_impl() -> Option<PathBuf> {
|
||||||
let appdata = std::env::var("APPDATA").ok()?;
|
let appdata = std::env::var("APPDATA").ok()?;
|
||||||
|
|
@ -257,3 +300,27 @@ fn detect_db_dir_impl() -> Option<PathBuf> {
|
||||||
fn detect_db_dir_impl() -> Option<PathBuf> {
|
fn detect_db_dir_impl() -> Option<PathBuf> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::resolve_cli_home;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resolve_cli_home_prefers_sudo_home_when_present() {
|
||||||
|
let home = resolve_cli_home(
|
||||||
|
PathBuf::from("/root"),
|
||||||
|
Some(PathBuf::from("/Users/alice")),
|
||||||
|
);
|
||||||
|
assert_eq!(home, PathBuf::from("/Users/alice"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resolve_cli_home_falls_back_to_default_home() {
|
||||||
|
let home = resolve_cli_home(
|
||||||
|
PathBuf::from("/root"),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
assert_eq!(home, PathBuf::from("/root"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
1239
src/daemon/query.rs
1239
src/daemon/query.rs
File diff suppressed because it is too large
Load Diff
|
|
@ -231,5 +231,8 @@ async fn dispatch(
|
||||||
Err(e) => Response::err(e.to_string()),
|
Err(e) => Response::err(e.to_string()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
ReloadConfig => {
|
||||||
|
Response::ok(serde_json::json!({ "reloading": true }))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -114,6 +114,8 @@ pub enum Request {
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
user: Option<String>,
|
user: Option<String>,
|
||||||
},
|
},
|
||||||
|
/// 重新加载配置和密钥(init --force 后 daemon 不会自动重读)
|
||||||
|
ReloadConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
/// 通过 /proc/<pid>/maps 枚举内存区域,
|
/// 通过 /proc/<pid>/maps 枚举内存区域,
|
||||||
/// 通过 /proc/<pid>/mem 读取内存内容,
|
/// 通过 /proc/<pid>/mem 读取内存内容,
|
||||||
/// 搜索 x'<64hex><32hex>' 格式的 SQLCipher 密钥
|
/// 搜索 x'<64hex><32hex>' 格式的 SQLCipher 密钥
|
||||||
use anyhow::{bail, Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use std::io::{Read, Seek, SeekFrom};
|
use std::io::{Read, Seek, SeekFrom};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue