skill(obsidian-vault-sync): v1.4.0 — 纠正'配置无法验证'的错判 + 补解码基线与运维地图

1. §1 自相矛盾修正:data.json 是 reverse+base64 混淆【可解】,
   解码读值 = 最强验证(实测 syncConfigDir=true 直接读出);
   远端 PROPFIND 降为交叉验证;mtime 只作"被重写过"的线索(不能定性覆盖成了什么)
2. 补 ignorePaths 字段 + 2026-09-11 解码基线(address/username/方向/冲突策略/间隔)
3. 新增「运维地图」:cross-ref notes-vault-curation / credential-leak-response /
   git-repo-hygiene / obsidian-plugin + 本 vault 三个常驻 cron + 说明书路径
4. description 扩到「Obsidian 管理」语义 + triggers 增 5 项(原先只匹配"同步",
   问"obsidian 怎么管"不会被加载)
This commit is contained in:
小唯 2026-09-11 01:35:55 +08:00
parent 1462301816
commit b1824d13fa
1 changed files with 259 additions and 0 deletions

View File

@ -0,0 +1,259 @@
---
name: obsidian-vault-sync
description: "Use when 管理/排查 Obsidian vault同步·配置·插件·重启·排除文件。remotely-save+WebDAV 全流程data.json 解码、远端取证、命令行重启、负载取舍。"
version: "1.4.0"
date: 2026-09-11§1 **纠正**data.json 是 reverse+base64 混淆**可解**,解码读值即最强验证;补 2026-09-11 解码基线、ignorePaths 字段、运维地图)
triggers:
- 同步笔记
- obsidian 同步
- webdav 同步
- remotely-save
- 同步失败
- vault 备份
- obsidian 管理
- obsidian 配置
- 插件同步
- 排除文件
- 重启 obsidian
---
# Obsidian vault 同步remotely-save + WebDAV
> 把 `~/mc` 这类 Obsidian vault 同步到自建服务器。核心难点不是上传,而是**remotely-save 的配置是混淆存储的**——不会解码就改不了服务器地址。
## 触发条件
- 「Obsidian 怎么管 / 配置/插件/同步/重启」「排除文件怎么配」
- 「同步笔记到服务器」「obsidian 同步」「webdav 同步」
- remotely-save / Remotely Save 插件相关
- vault 跨设备/备份需求,或同步报错、冲突
## 1. remotely-save 配置是混淆存储的 ⭐
`.obsidian/plugins/remotely-save/data.json` **只有一个 `d` 字段**,值是混淆串:
```json
{"readme": "...DO NOT share...", "d": "91nIiojIslWYtVmIs01W6IyclJXd0FW..."}
```
**规则reverse + base64**(编码 = 先 base64 再反转;解码反之):
```python
import json, base64
d = json.load(open(path))
# 解码
cfg = json.loads(base64.b64decode(d['d'][::-1]).decode('utf-8'))
# ... 改配置,例如 cfg['webdav']['address'] = 'http://new-host:6086/'
# 编码写回
d['d'] = base64.b64encode(json.dumps(cfg, ensure_ascii=False).encode('utf-8')).decode()[::-1]
json.dump(d, open(path, 'w'), ensure_ascii=False)
```
⚠️ `base64decode(enc)` 直接解会得到乱码 — **必须先 `[::-1]` 反转**
⚠️ 改前备份 `data.json.bak-<date>`;写完再解码验证一次。
⚠️ **插件运行中改文件 = 会被内存态盖回去** → 必须 `pgrep -f obsidian` 确认已关闭再改。
实证:改完的 `data.json` 写着 mtime 22:22但文件实际 mtime 是 **22:34**Obsidian 从内存重写了一次)→
**mtime 晚于你的编辑时间,就是这个文件被覆写过的铁证**
### ✅ 验证:**先解码读值**再用远端取证2026-09-11 纠正:此前本节写「验证不了」,是错的)⭐
`d` 字段是**混淆reverse+base64而非加密** —— 所以**能直接解出插件真正在读的配置**。
这是最强证据:读到的是插件的实际取值,不是「我以为写进去了」。
```python
import json, base64
d = json.load(open(path))
cfg = json.loads(base64.b64decode(d['d'][::-1]).decode('utf-8')) # ← 注意先 [::-1] 再 b64decode
print(cfg['syncConfigDir'], cfg['webdav']['address'], cfg['ignorePaths'])
```
实测2026-09-11 `~/mc`):解出的 `syncConfigDir = true`(且是那次「疑似被覆写」之后的值)
→ 直接结案PROPFIND 那条路只是**交叉验证**,不是唯一手段。
| 手段 | 强度 | 说明 |
|------|------|------|
| **解码 `d` 读值** | **最高** | 读到插件的真实取值;能证明「写进去了且没被盖回去」(每次读都是当前盘上内容)|
| 服务端取证 | 高 | `PROPFIND` 应出现 `.obsidian/plugins/<插件>/main.js` 且字节数与本机一致 ⇒ 证明**效果**已抵达其他设备 |
| 反向证据 | 高 | 本机删一个大目录 → 等一个周期 → 远端该目录消失 ⇒ 双向删除在工作 |
| GUI 看 | 中 | 设置页开关(人看,不入日志,无法留证)|
| mtime 线索 | 低 | **文件 mtime 晚于你的编辑时间 = 该文件被重写过**;能说明「可能被内存态覆盖」,**但不能说明覆盖成了什么** → 必须解码看内容才能定性2026-09-11 实测mtime 22:34 晚于编辑 22:22但解码后目标值仍在属重序列化而非回退|
⚠️ `.obsidian/` 不在 vault 的 git 里(被 vault `.gitignore` 排除)→ **WebDAV 是插件与同步配置唯一的异地副本**
它没同步成功 = 换设备重新配置一遍。
## 2. 关键配置字段
| 字段 | 含义 | 典型值 |
|------|------|--------|
| `serviceType` | 同步后端 | `webdav` / `s3` / `onedrive` |
| `webdav.address` | 地址(**末尾带 /**| `http://<host>:6086/` |
| `webdav.username` / `password` | 凭证 | 取 `牧尘/claw/key.md`(不硬编码)|
| `webdav.authType` | 认证 | `basic` |
| `syncDirection` | 方向 | `bidirectional` / `incremental-push-only` / `incremental-pull-only` |
| `conflictAction` | 冲突策略 | `keep_newer` |
| `autoRunEveryMilliseconds` | 自动同步间隔 | `600000`10 分钟)|
| `skipSizeLargerThan` | 单文件上限 | `-1` = 不限 |
| `protectModifyPercentage` | 大规模变更保护 | `100` |
| `ignorePaths` | 排除同步的路径(设备相关/含凭证的)| 实测 4 条:`.obsidian/workspace.json`、`.obsidian/workspace-mobile.json`、`.obsidian/plugins/remotely-save/data.json`(防循环+防凭证外泄)、`.obsidian/cache` |
| `obfuscateSettingFile` | 配置是否混淆 | 默认开(所以看到 `d`|
**2026-09-11 实测基线**`~/mc` 解码所得,改配置后可对照):
```
syncConfigDir = true syncDirection = bidirectional conflictAction = keep_newer
autoRunEveryMilliseconds = 60000010min
webdav.address = http://192.168.188.13:6086/ username = admin authType = basic
```
## 3. WebDAV 原生操作(不需要 rclone / cadaver
Python `urllib` 足够,零依赖:
| 操作 | 方法 | 备注 |
|------|------|------|
| 列目录 | `PROPFIND` + `Depth: 1` | 返回 XML正则提 `<D:href>` |
| 递归统计 | `PROPFIND` + `Depth: infinity` | 部分服务器不支持 |
| 建目录 | `MKCOL` | 405 = 已存在(可忽略)|
| 上传 | `PUT` | 201 = 新建成功 |
| 下载 | `GET` | |
| 删目录 | `DELETE`**路径尾带 `/`**| 递归删整个 collection |
⚠️ **中文/空格文件名必须 URL 编码**`urllib.parse.quote(path, safe='/%')`
⚠️ 认证头:`Authorization: Basic base64(user:pass)`
⚠️ `urllib.request` 遇 4xx 抛 `HTTPError` —— 要单独 catch 拿状态码,别当崩溃
## 4. 全量同步(以本机为权威版本)
```
1. PROPFIND /<dir>/ Depth:1 → 列服务器旧条目
2. 逐条 DELETE目录尾带 / → 清空
3. os.walk 收集本机 dirs+files → 排除 .git/.trash/__pycache__/.smart-env
4. MKCOL 目录 → 按 path 深度排序(父目录先建)
5. ThreadPoolExecutor(12) 并发 PUT
6. 验证 → 抽查 md5 + PROPFIND Depth:infinity 计数
```
实测2026-09-10**2590 文件 + 148 目录 / 12 线程 / 118 秒 / 0 失败**;本机 2738 对象 ↔ 远端 2739 ✓
▶ 可复用脚本:`scripts/vault-webdav-sync.py`(清空 + 并发上传 + md5 抽查,凭证走环境变量)
## 5. 陷阱
- **服务器旧目录会「复活」已删数据**NAS 上的旧同步常含本机早已删除的历史目录(迁出的项目、废弃文档)→ 全量同步前**必须清空**,否则旧的会回来
- `bidirectional` 只有双方一致后才安全;单向需求改 `syncDirection`
- **凭证库会随 vault 上传**(如 `牧尘/claw/key.md`)→ 先确认服务器在内网/VPN 且有认证
- **⚠️ 别把「体积大」直接推成「每周期重传」2026-09-11 自我纠正,此前本行写错了)**
remotely-save 是**增量同步**,只传变化的文件。证据:服务器端文件 mtime 停在**文件真正改动的那一刻**
(实测某文件服务器 mtime 22:40 = 本机改动时间,而检查时刻已是 01:30若每周期重传mtime 应为「刚刚」)。
所以 109M 的 assets 只是**首次上传过一次**,之后每轮仅比对 → 同步负担远小于「全量重传」的说法。
- **大目录要不要移出/排除,先 grep 笔记有没有引用它**
`grep -rnE '\]\([^)]*assets/|!\[\[|src="[^"]*assets/' <笔记目录>`
实测三个 assets 目录被笔记**相对链接**引用 **110 + 83 处** → 搬出 vault 会让**本地**链接也失效
Obsidian 只解析 vault 内相对路径)→ 结论是**保持原地、也不加 `ignorePaths`**
(加了 = 其他设备连链接都点不开)。审计口径:`du -sh */ | sort -h` + `find . -type f -size +5M`
- `.obsidian/` 通常被 vault 的 `.gitignore` 排除 → 插件与同步配置**不进 git**WebDAV 是它们唯一的异地副本
- 手动改完同步配置后,记得把新文件也 PUT 一份到服务器(否则下次自动同步可能把服务器旧版拉回来)
## 6. 设置类改动:`app.json`(排除文件)与「生效」的两个前提
remotely-save 的配置在 `data.json`混淆、易被内存覆写Obsidian 自身的设置在 `app.json`**明文、可直接改**)。
**两者行为不同,别套用同一套结论**
| 文件 | 明文? | 运行中改文件 | 关键前提 |
|------|--------|--------------|----------|
| `.obsidian/app.json` | ✅ 明文 | **实测存活**(改完 150s 后 md5 不变)| **要重启 Obsidian 才被读进内存** |
| `.obsidian/plugins/*/data.json` | ❌ 混淆(`d`| **会被内存覆写** | 必须先关 Obsidian见 §1|
**排除文件Excluded files**——作用范围:搜索 / 图谱 / 快速切换 / 链接建议 / 反向链接:
```json
// .obsidian/app.json
"userIgnoreFilters": ["小唯/04-Archive/", "小雪/07-Wiki/archived/", "小雪/07-Wiki/knowledge/team-docs/"]
```
流程:`cp app.json /tmp/app.json.bak-<hhmm>` → 写入 → `md5sum` 记下 → **等 2-3 分钟再 `md5sum` 一次**
→ 变了就是被覆写(改法无效,得走 GUI
**改完必须让用户重启 Obsidian**(不重启则:①规则不生效 ②用户重启前动任何设置都会用内存里的旧副本把文件顶掉)。
### 用户不在电脑前时:命令行重启 Obsidian2026-09-11 实测通过)
用户说「我不在电脑前,无法操作」时**不用等他**——关键是**先从旧进程抓启动环境**,否则 Electron 起不来:
```bash
PID=$(pgrep -f '/opt/Obsidian/obsidian$' | head -1)
# ① 抓环境x11 会话才有)
tr '\0' '\n' < /proc/$PID/environ | grep -E '^(DISPLAY|XAUTHORITY|XDG_RUNTIME_DIR|DBUS_SESSION_BUS_ADDRESS)='
# ② 温和退出 + 清 renderer/gpu 子进程
kill -TERM $PID; sleep 3; pkill -f '/opt/Obsidian/obsidian --type='
# ③ 原环境重开setsid 让它脱离 agent 的 shell 存活)
setsid env DISPLAY=:0 XAUTHORITY=$HOME/.Xauthority XDG_RUNTIME_DIR=/run/user/1000 \
DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus /opt/Obsidian/obsidian \
> /tmp/obsidian-restart.log 2>&1 < /dev/null &
# ④ 验证**窗口**真的回来了(进程数 ≠ 窗口打开了)
wmctrl -l | grep -i obsidian # 期望 "… - <vault> - Obsidian <ver>"
```
- 写成长脚本再 `bash` 跑,别拼成一条巨长内联命令(会撞 hardline 阻断,见 `git-repo-hygiene` §8
- **重启后 `app.json` 的 md5 会变 —— 这是正常重序列化,不是被覆写**:判定要**比 JSON 语义**
`diff <(python3 -m json.tool A) <(python3 -m json.tool B)`),只看 md5 会误判成"规则被顶掉了"
- 重启后 `userIgnoreFilters` 才真正载入内存 → 此时才算锁死;在此之前用户动任何设置都可能把文件顶掉
## 7. 验证「同步真的在跑」:用远端取证,别读本地配置
本地配置说明不了任何事(混淆 + 可能被覆写)。远端才是证据:
```bash
PW=<WebDAV 密码> # 优先从 牧尘/claw/key.md 取key.md 里常缺 WebDAV 条目(只有 NAS root→ 直接问用户,别猜
curl -s -u "admin:$PW" -X PROPFIND -H "Depth: 1" "http://<host>:6086/<dir>/" # 目录层
curl -s -u "admin:$PW" -X PROPFIND -H "Depth: 1" "http://<host>:6086/<dir>/.obsidian/plugins/" # 插件层
curl -s -u "admin:$PW" "http://<host>:6086/<dir>/.obsidian/app.json" # 单文件直读
```
- **正向**:远端出现 `.obsidian/plugins/<插件>/main.js` 且**字节数与本机一致** ⇒ `syncConfigDir` 真生效,
且「修好的插件」已抵达其他设备实测17 个插件 + `zhiyi-memory/main.js` 36956 B 两侧一致)。
这比“看 GUI 开关”更硬——开关是意图,远端文件是效果。
- **反向(最硬)**:本机删掉一个大目录 → 等**一个同步周期**`autoRunEveryMilliseconds`)→ 远端该目录应消失。
实测删除一个 1688 文件的目录后,周期末远端对应子目录确实消失 ⇒ 双向删除也在工作。
- 顺便看**残留**:远端根列表能发现「本机早已删、远端还在」的旧条目(本次发现远端还留着一个 7.7M 的 `.zip`)。
- 查同步配置时别忘了 vault 外的**独立仓库**`.obsidian/` 通常被 vault `.gitignore` 排除 ⇒ 它**不在 git 里**
WebDAV 是插件与同步配置唯一的异地副本(不同步成功 = 换设备要重配一遍)。
## 8. 同步负载:什么不该进来
`du -sh --exclude=.git .` + `du -sh */ && find . -type f -size +5M`
⚠️ **先搞清开销模型再决定动不动它2026-09-11 纠正)**remotely-save **增量同步**——大目录的代价是
**首次上传 + 每周期比对**,不是「每 10 分钟重传一遍」。别把「体积大」当成「必须移走」的理由。
- **人读镜像/冻结历史产物**:没有脚本再写、文件名已不可读 ⇒ 移出 vault`notes-vault-curation` §六.11
- **大二进制**pptx/pdf/设计稿):**先 grep 笔记引用**再决定
| 情况 | 处置 |
|------|------|
| 无笔记引用 | 移出 vault最干净`ignorePaths` |
| **被相对链接引用** | **保持原地** —— 移走断本地链接,`ignorePaths` 断其他设备链接 |
- 移出去之后要**验收两边**:本地 `du -sh` + 远端 PROPFIND 应同步变小
## 📎 相关技能与本 vault 的运维地图
| 场景 | 去哪 |
|------|------|
| 改 remotely-save 配置 / 验证同步生效 / 重启 Obsidian | **本技能** |
| vault 结构整理、编码乱码、凭证散落、排除文件配置 | `notes-vault-curation`(§五 每日密钥扫描)|
| 发现凭证进了 git / 要清历史 / 写入侧脱敏 | `credential-leak-response` + `git-repo-hygiene` §5-7 |
| 本 vault 的 git 推送、凭据外置、每日自动快照 cron | `git-repo-hygiene` §7.1/§7.2 |
| 开发/修 Obsidian 插件 | `obsidian-plugin` |
**本 vault 的常驻机制**`~/mc`
- 结构检查 cron `a4cbdf0cb31a`(每周一 09:00· 密钥扫描 cron `6288e67a9a00`(每天 08:45白名单 `牧尘/`
- 自动快照 cron `a4da57d2c026`(每天 09:05脚本 `~/.hermes/scripts/mc-autopush.sh`,双闸门)
- 人读说明书:`小唯/07-Wiki/06-工具与资源/Obsidian配置与插件说明.md`(配置/插件/同步/版本控制政策)
## 实战记录
| 日期 | 内容 |
|------|------|
| 2026-09-11 | 实证 `syncConfigDir` **生效**`PROPFIND /mc/.obsidian/plugins/` → 17 个插件全在服务器上,`zhiyi-memory/main.js` 36956 B 与本机一致(服务器 mtime 22:40 / 本机 22:29⇒ 之前的「无法验证」结案。反向验证:本机删除 1688 文件目录 → 一个周期后远端该子目录消失。写入 `app.json``userIgnoreFilters` 三条排除规则并同步成功(远端 GET 可读到)。移出冻结镜像后 vault 200M→194M / md 2147→459 |
| 2026-09-10 | `~/mc``http://192.168.188.13:6086/mc/`NAS iStoreOS WebDAVbidirectional + keep_newer + 10min。从旧地址 `192.168.123.125:6086` 改来(同一台 NAS 的 VPN 网段地址)。清空服务器 31 个旧条目后上传 2590 文件,抽查 5 个 md5 全一致 |