Skip to content

Repository files navigation

@yumgjs/yusy

@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 转换:将 .sy JSON 文档转换为更适合 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

快速上手

1. 准备 S3 同步信息

你需要先从思源同步配置或对象存储控制台中准备这些信息:

  • S3 endpoint
  • access key
  • secret key
  • bucket
  • region
  • 用于解密同步数据的 AES key,或可推导出 AES key 的密码

如果已经有 Base64 形式的 32 字节 AES key,推荐直接配置 SIYUAN_RESTORE_AES_KEY

2. 准备输出目录

mkdir siyuan-notes-mirror
cd siyuan-notes-mirror

3. 设置环境变量

Git 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"

4. 执行恢复

yusy

运行后,输出目录中会出现恢复后的文档,以及用于下次增量同步的 .restore-state.json

配置方式

yusy 支持多种配置来源,优先级从高到低通常为:

  1. CLI 参数
  2. 环境变量 / CI Secrets
  3. .env 文件
  4. JSON / JSONC 配置文件
  5. 默认值

默认会尝试读取当前目录下的这些配置文件:

  • siyuan-restore.json
  • siyuan-restore.jsonc
  • .siyuan-restore.json
  • .siyuan-restore.jsonc

也可以显式指定配置文件:

yusy --config ./siyuan-restore.jsonc

配置文件示例:

{
  "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"]
}

更多配置说明见:

常用命令

查看帮助:

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

输出思源原始 .sy JSON 文档,适合保留完整结构:

yusy --format sy

md

.sy 文档转换为 Markdown,适合阅读、搜索和 diff:

yusy --format md --rename title

both

同时输出 .sy.md。如果你希望保留原始数据,又希望在 Git 中查看 Markdown diff,推荐使用这个模式:

yusy --format both --rename title

Markdown 转换细节见 docs/converter.md

增量同步与加速缓存

开启增量同步后,yusy 会在输出目录中维护:

.restore-state.json

这个文件会记录:

  • 上次同步的 snapshot ID
  • 已恢复文档 ID 和路径映射
  • fileMeta metadata cache
  • titleMap / renameMap
  • 与同步范围有关的配置指纹,例如 docsOnly、exclude、notebook scope 等

这样后续执行时可以:

  • 跳过未变化文件。
  • 避免重复下载 chunk。
  • 复用 fileMeta,减少 S3 metadata 请求。
  • 在只同步部分笔记本时保留其他笔记本已有状态。
  • 在 summary 中输出 metadataCacheHitschunksDownloaded 等信息。

日志中会看到类似信息:

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": "工作,个人"
}

多笔记本同步时,单次执行只处理本次指定的笔记本。例如:

  1. 第一次只同步 工作
  2. 第二次同步 工作个人
  3. 第三次只同步 个人

第三次执行时,不会删除或重置 工作 笔记本已有文件,也不会破坏 .restore-state.json 中与 工作 相关的状态、路径映射和缓存。这样你可以按需分批同步多个笔记本,也可以给不同 CI job 分配不同笔记本范围。

搭建自己的定时同步仓库

推荐单独建立一个 笔记镜像仓库,例如 siyuan-notes-mirror。这个仓库只负责保存恢复后的文档和 .restore-state.json

整体流程:

  1. GitHub Actions checkout 这个笔记镜像仓库。
  2. 安装 @yumgjs/yusy CLI。
  3. 执行 yusy,把输出目录设为仓库根目录 .
  4. CLI 根据 .restore-state.json 做增量同步。
  5. workflow 把恢复后的文档变化和 .restore-state.json 一起提交回仓库。

actions/checkout 就是把仓库内容拉到 runner 工作目录。只要 .restore-state.json 被提交回仓库,下次 workflow 执行时就能继续复用上次的增量状态和加速缓存。

1. 初始化仓库

mkdir siyuan-notes-mirror
cd siyuan-notes-mirror
git init

建议 .gitignore 至少包含:

.env
*.log
.tmp/
tmp/

不要忽略 .restore-state.json,它应该被提交到仓库。

2. 添加 GitHub Actions workflow

文件路径:

.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

需要配置的环境变量

必需 Secrets

在 GitHub 仓库的 SettingsSecrets and variablesActionsSecrets 中配置:

变量 说明
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 思源同步密码

常用 Variables

在 GitHub 仓库的 SettingsSecrets and variablesActionsVariables 中配置:

变量 默认值 说明
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 中的变量

这些通常直接写在 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

故障处理

zstd.node 缺失

@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 安装即可,不需要处理这个问题。

TS5112: tsconfig.json is present...

不要直接用 tsc src/index.ts 这类指定文件的方式做类型检查。请使用项目脚本:

pnpm typecheck

CI 没有增量效果

优先检查:

  • workflow 是否 checkout 了保存笔记的仓库。
  • SIYUAN_RESTORE_OUTPUT=. 是否指向仓库根目录。
  • .restore-state.json 是否没有被 .gitignore 忽略。
  • workflow 是否把 .restore-state.json 和文档变化一起 commit + push。

只要 .restore-state.json 能被提交并在下次 checkout 时恢复,CLI 就可以继续复用增量状态和加速缓存。

快照时间少 8 小时

GitHub Actions runner 默认使用 UTC。yusy 默认按 Asia/Shanghai 格式化 summary 和提交信息中的快照时间。如果你想显式配置,可以设置:

SIYUAN_RESTORE_TIME_ZONE=Asia/Shanghai

也可以设置为其他 IANA 时区,例如:

SIYUAN_RESTORE_TIME_ZONE=UTC

更多文档

本地开发

pnpm install
pnpm test
pnpm typecheck
pnpm build

本地运行开发入口:

pnpm dev -- --help

如果 pnpm 提示 native 依赖没有构建,请先执行:

pnpm zstd:approve
pnpm install

开源协议

MIT License,详见 LICENSE

About

用于恢复/镜像思源笔记(SiYuan)S3 同步数据的命令行工具。它会从 S3 兼容对象存储中读取思源同步快照,解密、解压并还原文档,可输出 .sy 原始文档、Markdown,或两者同时输出,非常适合把思源笔记定时同步到 Git 仓库中,获得可搜索、可 diff、可备份的长期归档。

Topics

Resources

Stars

5 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages