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:
parent
1462301816
commit
b1824d13fa
|
|
@ -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 = 600000(10min)
|
||||
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**(不重启则:①规则不生效 ②用户重启前动任何设置都会用内存里的旧副本把文件顶掉)。
|
||||
|
||||
### 用户不在电脑前时:命令行重启 Obsidian(2026-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 WebDAV,bidirectional + keep_newer + 10min)。从旧地址 `192.168.123.125:6086` 改来(同一台 NAS 的 VPN 网段地址)。清空服务器 31 个旧条目后上传 2590 文件,抽查 5 个 md5 全一致 |
|
||||
Loading…
Reference in New Issue