用 Rclone 自动备份小狼毫配置到 WebDAV

Updated:

1. Rclone 配置 webdav

scoop 下载 rclone

scoop install rclone
rclone version
rclone config

我用的是 openlist 自建的 webdav,其他 webdav 参考官方规范填写

name: list
type: webdav
url: https://list.miuo.me/dav/
vendor: other
user: 你的 OpenList 用户名
password: 你的 OpenList 密码
bearer_token>
Enter跳过即可
Edit advanced config?
n
Keep this "list" remote?
y
q

测试

rclone lsd list:

出现全部列表

PS C:\Users\Sakura> rclone lsd list:
          -1 2026-03-04 18:22:52        -1  infini-cloud
          -1 2026-03-29 20:15:30        -1 Cloudflare R2
          -1 2026-05-01 12:09:55        -1 mihon
          -1 2026-05-01 12:20:14        -1 mistn
          -1 2026-01-19 13:10:05        -1 onedrive
          -1 2026-05-01 12:21:05        -1 private
          -1 2026-01-19 13:31:07        -1 quark
PS C:\Users\Sakura>

2. 创建 Rime 备份脚本

ps1脚本,按需自行修改

$ErrorActionPreference = "Stop"

# Paths
$Source = "C:\Users\xxx\AppData\Roaming\Rime"
$TempDir = "C:\Users\xxx\rclone-temp"
$LogDir = "C:\Users\xxx\rclone-logs"
$RemoteDir = "list:backup/rime-archives"

# File names
$Time = Get-Date -Format "yyyyMMdd-HHmmss"
$ZipName = "rime-$Time.zip"
$ZipPath = Join-Path $TempDir $ZipName

# Logs
$LogFile = Join-Path $LogDir "rime-backup.log"
$RcloneLogFile = Join-Path $LogDir "rime-rclone.log"

# Create folders
New-Item -ItemType Directory -Force -Path $TempDir | Out-Null
New-Item -ItemType Directory -Force -Path $LogDir | Out-Null

function Write-Log {
    param([string]$Message)

    $Now = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
    "[$Now] $Message" | Out-File -FilePath $LogFile -Append -Encoding utf8
}

function Write-Step {
    param([string]$Message)

    $Now = Get-Date -Format "HH:mm:ss"
    Write-Host "[$Now] $Message" -ForegroundColor Cyan
    Write-Log $Message
}

function Write-Ok {
    param([string]$Message)

    $Now = Get-Date -Format "HH:mm:ss"
    Write-Host "[$Now] OK: $Message" -ForegroundColor Green
    Write-Log "OK: $Message"
}

function Write-Warn {
    param([string]$Message)

    $Now = Get-Date -Format "HH:mm:ss"
    Write-Host "[$Now] WARN: $Message" -ForegroundColor Yellow
    Write-Log "WARN: $Message"
}

function Write-Fail {
    param([string]$Message)

    $Now = Get-Date -Format "HH:mm:ss"
    Write-Host "[$Now] ERROR: $Message" -ForegroundColor Red
    Write-Log "ERROR: $Message"
}

function Show-TextProgress {
    param(
        [string]$Title,
        [int]$Current,
        [int]$Total
    )

    if ($Total -le 0) {
        return
    }

    $Percent = [math]::Floor(($Current / $Total) * 100)
    $BarLength = 30
    $FilledLength = [math]::Floor($Percent * $BarLength / 100)
    $EmptyLength = $BarLength - $FilledLength

    $Bar = ("#" * $FilledLength) + ("-" * $EmptyLength)
    Write-Host "`r$Title [$Bar] $Percent%  ($Current/$Total files)" -NoNewline -ForegroundColor Yellow
}

function Compress-FolderWithTextProgress {
    param(
        [string]$SourceDir,
        [string]$DestinationZip
    )

    Add-Type -AssemblyName System.IO.Compression
    Add-Type -AssemblyName System.IO.Compression.FileSystem

    if (Test-Path $DestinationZip) {
        Remove-Item $DestinationZip -Force
    }

    $Files = Get-ChildItem -Path $SourceDir -Recurse -File -Force
    $Total = $Files.Count

    if ($Total -eq 0) {
        throw "No files found in source folder: $SourceDir"
    }

    $Zip = [System.IO.Compression.ZipFile]::Open($DestinationZip, [System.IO.Compression.ZipArchiveMode]::Create)

    try {
        $Index = 0

        foreach ($File in $Files) {
            $Index++

            $RelativePath = $File.FullName.Substring($SourceDir.Length).TrimStart("\")
            $EntryName = $RelativePath -replace "\\", "/"

            [System.IO.Compression.ZipFileExtensions]::CreateEntryFromFile(
                $Zip,
                $File.FullName,
                $EntryName,
                [System.IO.Compression.CompressionLevel]::Optimal
            ) | Out-Null

            if (($Index % 5 -eq 0) -or ($Index -eq $Total)) {
                Show-TextProgress -Title "Compressing" -Current $Index -Total $Total
            }
        }

        Write-Host ""
    }
    finally {
        $Zip.Dispose()
    }
}

Clear-Host
Write-Host ""
Write-Host "========================================" -ForegroundColor DarkCyan
Write-Host " Rime Backup to Cloud Storage - Keep 3" -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor DarkCyan
Write-Host ""

Write-Log "========== Rime backup start =========="
Write-Log "Source: $Source"
Write-Log "Remote: $RemoteDir"
Write-Log "ZipPath: $ZipPath"
Write-Log "RcloneLog: $RcloneLogFile"

if (!(Test-Path $Source)) {
    Write-Fail "Rime folder not found: $Source"
    throw "Rime folder not found: $Source"
}

# Detect WeaselServer path
$WeaselPath = $null
$WeaselProcess = Get-Process -Name "WeaselServer" -ErrorAction SilentlyContinue | Select-Object -First 1

if ($WeaselProcess -and $WeaselProcess.Path) {
    $WeaselPath = $WeaselProcess.Path
    Write-Log "Found WeaselServer: $WeaselPath"
}

# Fallback paths
if (!$WeaselPath) {
    $PossiblePaths = @(
        "C:\Program Files\Rime\weasel-0.17.4\WeaselServer.exe",
        "C:\Program Files\Rime\weasel-0.16.3\WeaselServer.exe",
        "C:\Program Files (x86)\Rime\weasel-0.17.4\WeaselServer.exe",
        "C:\Program Files (x86)\Rime\weasel-0.16.3\WeaselServer.exe",
        "C:\Program Files\Rime\WeaselServer.exe",
        "C:\Program Files (x86)\Rime\WeaselServer.exe"
    )

    foreach ($Path in $PossiblePaths) {
        if (Test-Path $Path) {
            $WeaselPath = $Path
            Write-Log "Using fallback WeaselServer path: $WeaselPath"
            break
        }
    }
}

$UploadSucceeded = $false

try {
    Write-Step "[1/6] Stopping Weasel processes..."
    Get-Process -Name "WeaselServer" -ErrorAction SilentlyContinue | Stop-Process -Force
    Get-Process -Name "WeaselDeployer" -ErrorAction SilentlyContinue | Stop-Process -Force
    Start-Sleep -Seconds 2
    Write-Ok "Weasel processes stopped"

    Write-Step "[2/6] Preparing temporary zip..."
    if (Test-Path $ZipPath) {
        Remove-Item $ZipPath -Force
        Write-Log "Old temp zip removed"
    }
    Write-Ok "Temp path ready"

    Write-Step "[3/6] Compressing Rime folder..."
    Compress-FolderWithTextProgress -SourceDir $Source -DestinationZip $ZipPath

    $ZipSizeMB = [math]::Round((Get-Item $ZipPath).Length / 1MB, 2)
    Write-Ok "Compress done: $ZipName ($ZipSizeMB MB)"

    Write-Step "[4/6] Uploading backup to cloud storage..."
    Write-Host "File size: $ZipSizeMB MB" -ForegroundColor DarkGray
    Write-Host ""

    rclone copy $ZipPath $RemoteDir `
        --progress `
        --stats 1s `
        --stats-one-line `
        --log-file $RcloneLogFile `
        --log-level INFO

    if ($LASTEXITCODE -ne 0) {
        throw "rclone upload failed with exit code $LASTEXITCODE"
    }

    $UploadSucceeded = $true
    Write-Ok "Upload done"

    Write-Step "[5/6] Keeping only latest 3 backups..."
    $Files = rclone lsf $RemoteDir --files-only | Where-Object { $_ -like "rime-*.zip" } | Sort-Object -Descending

    if ($LASTEXITCODE -ne 0) {
        throw "rclone lsf failed with exit code $LASTEXITCODE"
    }

    $FilesToDelete = $Files | Select-Object -Skip 3

    if ($FilesToDelete.Count -eq 0) {
        Write-Ok "No old backups need deleting"
    } else {
        foreach ($File in $FilesToDelete) {
            Write-Warn "Deleting old backup: $File"

            rclone deletefile "$RemoteDir/$File" `
                --log-file $RcloneLogFile `
                --log-level INFO

            if ($LASTEXITCODE -ne 0) {
                throw "rclone deletefile failed for $File with exit code $LASTEXITCODE"
            }
        }

        Write-Ok "Old backups deleted"
    }

    Write-Step "[6/6] Cleaning local temp file..."

    if ($UploadSucceeded -and (Test-Path $ZipPath)) {
        Remove-Item $ZipPath -Force
        Write-Ok "Local temp zip removed"
    } else {
        Write-Warn "Upload failed, local zip kept: $ZipPath"
    }

    Write-Host ""
    Write-Host "Backup success!" -ForegroundColor Green
    Write-Host "Remote folder: $RemoteDir" -ForegroundColor DarkGray
    Write-Host "Script log: $LogFile" -ForegroundColor DarkGray
    Write-Host "Rclone log: $RcloneLogFile" -ForegroundColor DarkGray
    Write-Host ""

    Write-Log "========== Rime backup success =========="
}
catch {
    Write-Fail "Backup failed: $($_.Exception.Message)"
    Write-Log "BACKUP FAILED: $($_.Exception.Message)"

    if (Test-Path $ZipPath) {
        Write-Warn "Local zip kept for retry: $ZipPath"
        Write-Log "Local zip kept for retry: $ZipPath"
    }

    throw
}
finally {
    if ($WeaselPath -and (Test-Path $WeaselPath)) {
        Write-Step "Restarting WeaselServer..."
        Start-Process $WeaselPath
        Write-Ok "WeaselServer started"
    } else {
        Write-Warn "WeaselServer path not found, skipped restart"
    }

    Write-Log "========== Rime backup end =========="
}

3. 设置 windows 定时备份任务

每周日 22:10 跑一次,错过后下次开机补跑,失败后每 10 分钟重试一次最多 3 次,超过 1 小时停止,不启动重复实例

$TaskName = "Backup Rime to R2 Keep3"
$ScriptPath = "C:\Users\Sakura\Scripts\backup_rime_to_r2_keep3.ps1"

# Delete old task if exists
schtasks /Delete /TN $TaskName /F 2>$null

# Create action
$Action = New-ScheduledTaskAction `
    -Execute "powershell.exe" `
    -Argument "-NoProfile -ExecutionPolicy Bypass -File `"$ScriptPath`""

# Run every Sunday at 22:10
$Trigger = New-ScheduledTaskTrigger `
    -Weekly `
    -DaysOfWeek Sunday `
    -At 22:10

# Settings
$Settings = New-ScheduledTaskSettingsSet `
    -AllowStartIfOnBatteries `
    -DontStopIfGoingOnBatteries `
    -StartWhenAvailable `
    -RestartCount 3 `
    -RestartInterval (New-TimeSpan -Minutes 10) `
    -ExecutionTimeLimit (New-TimeSpan -Hours 1) `
    -MultipleInstances IgnoreNew

# Register task
Register-ScheduledTask `
    -TaskName $TaskName `
    -Action $Action `
    -Trigger $Trigger `
    -Settings $Settings `
    -Description "Backup Rime config to Cloudflare R2 weekly, keep latest 3 archives." `
    -Force

检查

schtasks /Query /TN "Backup Rime to R2 Keep3" /V /FO LIST

4. Rime 具体配置

4.1 使用 [ / ] 切换候选项

配置文件路径:

C:\Users\Sakura\AppData\Roaming\Rime\default.custom.yaml

如果没有 default.custom.yaml 就新建一个。不要直接改 default.yaml,更新方案时更容易被覆盖。

patch:
  # 原本 [ ] 被 Lua 的“以词定字”占用
  # 如果要用 [ ] 翻页,需要先把“以词定字”改成别的快捷键
  key_binder/select_first_character: "Control+bracketleft"
  key_binder/select_last_character: "Control+bracketright"

  key_binder/bindings/+:
    - { when: has_menu, accept: bracketleft, send: Page_Up }
    - { when: has_menu, accept: bracketright, send: Page_Down }

4.2 小狼毫皮肤配置

配置文件路径:

C:\Users\Sakura\AppData\Roaming\Rime\weasel.custom.yaml

如果没有 weasel.custom.yaml 就新建一个。不要直接改 weasel.yaml,更新配置时更容易被覆盖。

patch:
  style/color_scheme: purity_of_form_custom

  preset_color_schemes/purity_of_form_custom:
    name: "uo"
    author: "miuo"

    # 小狼毫颜色是 BGR,不是网页常见的 RGB;黑白灰不受影响。
    text_color: 0x000000
    back_color: 0xFFFFFF
    border_color: 0xE5E5E5
    shadow_color: 0x20000000

    label_color: 0x666666
    candidate_text_color: 0x333333
    comment_text_color: 0x888888

    hilited_text_color: 0x000000
    hilited_candidate_text_color: 0x000000
    hilited_candidate_label_color: 0x333333
    hilited_comment_text_color: 0x666666
    hilited_candidate_back_color: 0xFFFFFF
    hilited_candidate_border_color: 0xFFFFFF

    # 左侧高亮标记,0xD47800 对应接近 Windows 蓝色。
    hilited_mark_color: 0xD47800

重新部署即可

5. 雾凇词库更新

git status
git pull

重新部署即可

Comments