@yumgjs/yusy 是一个用于恢复/镜像思源笔记(SiYuan)S3 同步数据的命令行工具。它会从 S3 兼容对象存储中读取思源同步快照,解密、解压并还原文档,可输出 .sy 原始文档、Markdown,或两者同时输出,非常适合把思源笔记定时同步到 Git 仓库中,获得可搜索、可 diff、可备份的长期归档。
- S3 兼容存储读取:支持 AWS S3、腾讯云 COS、阿里云 OSS、MinIO、Cloudflare R2 等兼容 S3 协议的对象存储。
- 思源同步快照恢复:可恢复最新快照,也可指定具体 snapshot ID。
- 解密与解压:支持思源 S3 同步数据使用的 AES key、zstd 压缩数据处理。
- 多种输出格式:支持输出
.sy、Markdown,或.sy + .md同时输出。 - Markdown 转换:将
.syJSON 文档转换为更适合 Git diff 和阅读的 Markdown。 - 标题重命名:可按文档标题重命名输出文件,便于在仓库中浏览。
- 增量同步:通过
.restore-state.json保存同步状态,后续运行只处理变化内容。 - metadata cache:保存文件元数据缓存,减少重复检查和下载,提升 CI 中的同步速度。
- 多笔记本同步:支持一次同步一个或多个笔记本;只同步指定笔记本,不破坏其他笔记本的状态、文件和缓存。
- CI 友好:支持非交互模式、summary 文件、精简提交信息、定时任务。
- 时区稳定:默认使用
Asia/Shanghai格式化快照时间,也可通过环境变量覆盖。
这个工具适合你想把思源笔记同步数据变成一个独立的 Git 仓库时使用,例如:
- 每天定时把思源笔记增量拉取到 GitHub 仓库。
- 在 GitHub 上查看每次笔记变化的 diff。
- 给思源笔记做一份额外的可审计备份。
- 将
.sy文档转换为 Markdown 方便全文搜索、归档或二次处理。 - 只同步某一个或某几个笔记本,避免一次处理整个工作空间。
这个工具不会连接你的思源本地客户端,也不会直接修改思源工作空间。它只读取 S3 同步数据,并把恢复结果写入你指定的输出目录。
不想全局安装时,可以直接使用 npx:
npx @yumgjs/yusy --help推荐在 CI 或长期使用的机器上全局安装:
npm i -g @yumgjs/yusy
yusy --help工具同时保留旧命令名:
siyuan-restore --help你需要先从思源同步配置或对象存储控制台中准备这些信息:
- S3 endpoint
- access key
- secret key
- bucket
- region
- 用于解密同步数据的 AES key,或可推导出 AES key 的密码
如果已经有 Base64 形式的 32 字节 AES key,推荐直接配置 SIYUAN_RESTORE_AES_KEY。
mkdir siyuan-notes-mirror
cd siyuan-notes-mirrorGit Bash / Linux / macOS:
export SIYUAN_RESTORE_ENDPOINT="https://cos.ap-guangzhou.myqcloud.com"
export SIYUAN_RESTORE_ACCESS_KEY="your-access-key"
export SIYUAN_RESTORE_SECRET_KEY="your-secret-key"
export SIYUAN_RESTORE_BUCKET="your-bucket"
export SIYUAN_RESTORE_REGION="ap-guangzhou"
export SIYUAN_RESTORE_AES_KEY="your-base64-32-byte-aes-key"
export SIYUAN_RESTORE_OUTPUT="./restored"
export SIYUAN_RESTORE_SNAPSHOT_ID="latest"
export SIYUAN_RESTORE_FORMAT="both"
export SIYUAN_RESTORE_RENAME="title"
export SIYUAN_RESTORE_DOCS_ONLY="true"
export SIYUAN_RESTORE_INCREMENTAL="true"
export SIYUAN_RESTORE_INTERACTIVE="false"PowerShell:
$env:SIYUAN_RESTORE_ENDPOINT = "https://cos.ap-guangzhou.myqcloud.com"
$env:SIYUAN_RESTORE_ACCESS_KEY = "your-access-key"
$env:SIYUAN_RESTORE_SECRET_KEY = "your-secret-key"
$env:SIYUAN_RESTORE_BUCKET = "your-bucket"
$env:SIYUAN_RESTORE_REGION = "ap-guangzhou"
$env:SIYUAN_RESTORE_AES_KEY = "your-base64-32-byte-aes-key"
$env:SIYUAN_RESTORE_OUTPUT = "./restored"
$env:SIYUAN_RESTORE_SNAPSHOT_ID = "latest"
$env:SIYUAN_RESTORE_FORMAT = "both"
$env:SIYUAN_RESTORE_RENAME = "title"
$env:SIYUAN_RESTORE_DOCS_ONLY = "true"
$env:SIYUAN_RESTORE_INCREMENTAL = "true"
$env:SIYUAN_RESTORE_INTERACTIVE = "false"yusy运行后,输出目录中会出现恢复后的文档,以及用于下次增量同步的 .restore-state.json。
yusy 支持多种配置来源,优先级从高到低通常为:
- CLI 参数
- 环境变量 / CI Secrets
.env文件- JSON / JSONC 配置文件
- 默认值
默认会尝试读取当前目录下的这些配置文件:
siyuan-restore.jsonsiyuan-restore.jsonc.siyuan-restore.json.siyuan-restore.jsonc
也可以显式指定配置文件:
yusy --config ./siyuan-restore.jsonc配置文件示例:
更多配置说明见:
查看帮助:
yusy --help恢复最新快照:
yusy --snapshot-id latest只同步一个笔记本:
yusy --notebook "工作"同步多个笔记本:
yusy --notebooks "工作,个人"只输出 Markdown,并按标题重命名:
yusy --format md --rename title同时输出 .sy 和 .md:
yusy --format both --rename title只恢复文档内容,排除历史、临时等非文档数据:
yusy --docs-only启用增量同步:
yusy --incremental输出 CI summary 文件:
yusy --summary-file "$RUNNER_TEMP/siyuan-restore-summary.json"输出思源原始 .sy JSON 文档,适合保留完整结构:
yusy --format sy把 .sy 文档转换为 Markdown,适合阅读、搜索和 diff:
yusy --format md --rename title同时输出 .sy 和 .md。如果你希望保留原始数据,又希望在 Git 中查看 Markdown diff,推荐使用这个模式:
yusy --format both --rename titleMarkdown 转换细节见 docs/converter.md。
开启增量同步后,yusy 会在输出目录中维护:
.restore-state.json
这个文件会记录:
- 上次同步的 snapshot ID
- 已恢复文档 ID 和路径映射
fileMetametadata cachetitleMap/renameMap- 与同步范围有关的配置指纹,例如 docsOnly、exclude、notebook scope 等
这样后续执行时可以:
- 跳过未变化文件。
- 避免重复下载 chunk。
- 复用
fileMeta,减少 S3 metadata 请求。 - 在只同步部分笔记本时保留其他笔记本已有状态。
- 在 summary 中输出
metadataCacheHits、chunksDownloaded等信息。
日志中会看到类似信息:
Cache: 128 metadata hits, 12 chunks downloaded
CI 提交信息也可以使用缓存命中数量,例如:
c:2 m:4 d:0 cache:128 2026/06/17 23:21:26 [skip ci]
更多 CI summary 和提交信息示例见 docs/CI.md。
你可以通过 CLI、环境变量或配置文件指定一个或多个笔记本。
CLI:
yusy --notebooks "工作,个人"环境变量:
SIYUAN_RESTORE_NOTEBOOKS=工作,个人配置文件:
{
"notebooks": ["工作", "个人"]
}为了兼容旧配置,配置文件中的单个 notebook 也支持逗号分隔:
{
"notebook": "工作,个人"
}多笔记本同步时,单次执行只处理本次指定的笔记本。例如:
- 第一次只同步
工作。 - 第二次同步
工作和个人。 - 第三次只同步
个人。
第三次执行时,不会删除或重置 工作 笔记本已有文件,也不会破坏 .restore-state.json 中与 工作 相关的状态、路径映射和缓存。这样你可以按需分批同步多个笔记本,也可以给不同 CI job 分配不同笔记本范围。
推荐单独建立一个 笔记镜像仓库,例如 siyuan-notes-mirror。这个仓库只负责保存恢复后的文档和 .restore-state.json。
整体流程:
- GitHub Actions checkout 这个笔记镜像仓库。
- 安装
@yumgjs/yusyCLI。 - 执行
yusy,把输出目录设为仓库根目录.。 - CLI 根据
.restore-state.json做增量同步。 - workflow 把恢复后的文档变化和
.restore-state.json一起提交回仓库。
actions/checkout就是把仓库内容拉到 runner 工作目录。只要.restore-state.json被提交回仓库,下次 workflow 执行时就能继续复用上次的增量状态和加速缓存。
mkdir siyuan-notes-mirror
cd siyuan-notes-mirror
git init建议 .gitignore 至少包含:
.env
*.log
.tmp/
tmp/不要忽略 .restore-state.json,它应该被提交到仓库。
文件路径:
.github/workflows/restore-siyuan-notes.yml
示例内容:
name: restore-siyuan-notes
on:
workflow_dispatch:
schedule:
# 北京时间每天 00:00 = UTC 前一天 16:00
- cron: "0 16 * * *"
permissions:
contents: write
concurrency:
group: restore-siyuan-notes
cancel-in-progress: false
jobs:
restore:
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- name: Checkout notes repository
uses: actions/checkout@v4
with:
fetch-depth: 0
persist-credentials: true
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
- name: Install yusy CLI
run: |
npm i -g @yumgjs/yusy
yusy --help
- name: Restore SiYuan notes incrementally
env:
SIYUAN_RESTORE_ENDPOINT: ${{ secrets.SIYUAN_RESTORE_ENDPOINT }}
SIYUAN_RESTORE_ACCESS_KEY: ${{ secrets.SIYUAN_RESTORE_ACCESS_KEY }}
SIYUAN_RESTORE_SECRET_KEY: ${{ secrets.SIYUAN_RESTORE_SECRET_KEY }}
SIYUAN_RESTORE_BUCKET: ${{ secrets.SIYUAN_RESTORE_BUCKET }}
SIYUAN_RESTORE_REGION: ${{ secrets.SIYUAN_RESTORE_REGION }}
SIYUAN_RESTORE_AES_KEY: ${{ secrets.SIYUAN_RESTORE_AES_KEY }}
SIYUAN_RESTORE_NOTEBOOKS: ${{ vars.SIYUAN_RESTORE_NOTEBOOKS }}
SIYUAN_RESTORE_OUTPUT: .
SIYUAN_RESTORE_SNAPSHOT_ID: latest
SIYUAN_RESTORE_FORMAT: both
SIYUAN_RESTORE_RENAME: title
SIYUAN_RESTORE_DOCS_ONLY: "true"
SIYUAN_RESTORE_INCREMENTAL: "true"
SIYUAN_RESTORE_INTERACTIVE: "false"
SIYUAN_RESTORE_SUMMARY_FILE: ${{ runner.temp }}/siyuan-restore-summary.json
SIYUAN_RESTORE_TIME_ZONE: ${{ vars.SIYUAN_RESTORE_TIME_ZONE || 'Asia/Shanghai' }}
SIYUAN_RESTORE_EXCLUDE: history,temp
run: yusy
- name: Build commit message from restore summary
shell: bash
env:
SUMMARY_FILE: ${{ runner.temp }}/siyuan-restore-summary.json
SIYUAN_RESTORE_TIME_ZONE: ${{ vars.SIYUAN_RESTORE_TIME_ZONE || 'Asia/Shanghai' }}
run: |
node <<'NODE'
const fs = require('fs');
const summary = JSON.parse(fs.readFileSync(process.env.SUMMARY_FILE, 'utf8'));
const formatSnapshotTime = (value) => {
const date = value ? new Date(value) : null;
if (!date || Number.isNaN(date.getTime())) return 'unknown-time';
const parts = new Intl.DateTimeFormat('zh-CN', {
timeZone: process.env.SIYUAN_RESTORE_TIME_ZONE || 'Asia/Shanghai',
year: 'numeric', month: '2-digit', day: '2-digit',
hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false,
}).formatToParts(date);
const get = (type) => parts.find((part) => part.type === type)?.value || '';
return `${get('year')}/${get('month')}/${get('day')} ${get('hour')}:${get('minute')}:${get('second')}`;
};
const c = summary.counts;
const title = `c:${c.created} m:${c.modified} d:${c.deleted} cache:${c.metadataCacheHits || 0} ${formatSnapshotTime(summary.snapshot.createdAtIso || summary.snapshot.created)} [skip ci]`;
fs.writeFileSync(`${process.env.RUNNER_TEMP}/commit-message.txt`, `${title}\n\n${summary.commit.body}\n`);
console.log(title);
NODE
- name: Commit and push changes
shell: bash
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add -A
if git diff --cached --quiet; then
echo "No SiYuan note changes."
exit 0
fi
git commit -F "$RUNNER_TEMP/commit-message.txt"
git push更完整的 CI 说明见 docs/CI.md。
在 GitHub 仓库的 Settings → Secrets and variables → Actions → Secrets 中配置:
| 变量 | 说明 |
|---|---|
SIYUAN_RESTORE_ENDPOINT |
S3 endpoint,例如 https://cos.ap-guangzhou.myqcloud.com |
SIYUAN_RESTORE_ACCESS_KEY |
S3 access key |
SIYUAN_RESTORE_SECRET_KEY |
S3 secret key |
SIYUAN_RESTORE_BUCKET |
bucket 名称 |
SIYUAN_RESTORE_REGION |
S3 region |
SIYUAN_RESTORE_AES_KEY |
Base64 形式的 32 字节 AES key |
如果不使用 AES key,而是用密码派生密钥,可改用:
| 变量 | 说明 |
|---|---|
SIYUAN_RESTORE_PASSWORD |
思源同步密码 |
在 GitHub 仓库的 Settings → Secrets and variables → Actions → Variables 中配置:
| 变量 | 默认值 | 说明 |
|---|---|---|
SIYUAN_RESTORE_NOTEBOOKS |
空 | 要同步的笔记本名称或 ID,多个用英文逗号分隔 |
SIYUAN_RESTORE_PATH_STYLE |
false |
MinIO 等需要 path-style endpoint 时设为 true |
SIYUAN_RESTORE_CONCURRENCY |
8 |
并发下载数量 |
SIYUAN_RESTORE_TIME_ZONE |
Asia/Shanghai |
summary 和提交信息中的快照时间时区 |
这些通常直接写在 workflow 里即可:
SIYUAN_RESTORE_OUTPUT=.
SIYUAN_RESTORE_SNAPSHOT_ID=latest
SIYUAN_RESTORE_FORMAT=both
SIYUAN_RESTORE_RENAME=title
SIYUAN_RESTORE_DOCS_ONLY=true
SIYUAN_RESTORE_INCREMENTAL=true
SIYUAN_RESTORE_INTERACTIVE=false
SIYUAN_RESTORE_EXCLUDE=history,temp
@mongodb-js/zstd 是 native addon。如果使用 pnpm 进行本地开发,pnpm 默认可能禁止依赖自动运行构建脚本,导致启动时报类似错误:
Cannot find module '../build/Debug/zstd.node'
处理方式:
pnpm zstd:approve
pnpm install
pnpm zstd:check在 approve-builds 中选择 @mongodb-js/zstd。
如果你只是使用已发布的 CLI,通常直接用 npm 安装即可,不需要处理这个问题。
不要直接用 tsc src/index.ts 这类指定文件的方式做类型检查。请使用项目脚本:
pnpm typecheck优先检查:
- workflow 是否 checkout 了保存笔记的仓库。
SIYUAN_RESTORE_OUTPUT=.是否指向仓库根目录。.restore-state.json是否没有被.gitignore忽略。- workflow 是否把
.restore-state.json和文档变化一起 commit + push。
只要 .restore-state.json 能被提交并在下次 checkout 时恢复,CLI 就可以继续复用增量状态和加速缓存。
GitHub Actions runner 默认使用 UTC。yusy 默认按 Asia/Shanghai 格式化 summary 和提交信息中的快照时间。如果你想显式配置,可以设置:
SIYUAN_RESTORE_TIME_ZONE=Asia/Shanghai
也可以设置为其他 IANA 时区,例如:
SIYUAN_RESTORE_TIME_ZONE=UTC
CLI_USE.md:CLI 参数、配置来源和常见命令。docs/USAGE.md:更完整的使用说明。docs/CI.md:如何搭建定时同步仓库、summary、提交信息和缓存说明。docs/converter.md:.sy转 Markdown 的转换细节。docs/DESIGN.md:整体设计、S3 快照和恢复流程。
pnpm install
pnpm test
pnpm typecheck
pnpm build本地运行开发入口:
pnpm dev -- --help如果 pnpm 提示 native 依赖没有构建,请先执行:
pnpm zstd:approve
pnpm installMIT License,详见 LICENSE。
{ "endpoint": "https://cos.ap-guangzhou.myqcloud.com", "accessKey": "your-access-key", "secretKey": "your-secret-key", "bucket": "your-bucket", "region": "ap-guangzhou", "aesKey": "your-base64-32-byte-aes-key", "output": "./restored", "snapshotId": "latest", "format": "both", "rename": "title", "notebooks": ["工作", "个人"], "docsOnly": true, "incremental": true, "interactive": false, "exclude": ["history", "temp"] }