feat: add dual-layer versioning to WebDAV sync (protocol v2 + db-v6)

Separate protocol version from database compatibility version in WebDAV
sync paths. Upload writes to v2/db-v6/<profile>, download falls back to
legacy v2/<profile> when current path has no data. Extend manifest with
optional dbCompatVersion field and add legacy layout detection to UI.
This commit is contained in:
Jason
2026-03-08 19:33:20 +08:00
parent bf40b0138c
commit c0737f2cfe
8 changed files with 330 additions and 67 deletions
+55 -4
View File
@@ -92,6 +92,10 @@ function formatDate(rfc3339: string): string {
return Number.isNaN(d.getTime()) ? rfc3339 : d.toLocaleString();
}
function formatDbCompatVersion(version?: number | null): string | null {
return typeof version === "number" ? `db-v${version}` : null;
}
// ─── Types ──────────────────────────────────────────────────
type ActionState =
@@ -395,7 +399,10 @@ export function WebdavSyncSection({ config }: WebdavSyncSectionProps) {
if (!info.compatible) {
toast.error(
t("settings.webdavSync.incompatibleVersion", {
version: info.version,
protocolVersion: info.protocolVersion,
dbCompatVersion:
formatDbCompatVersion(info.dbCompatVersion) ??
t("common.unknown"),
}),
);
return;
@@ -450,6 +457,9 @@ export function WebdavSyncSection({ config }: WebdavSyncSectionProps) {
const lastError = config?.status?.lastError?.trim();
const showAutoSyncError =
!!lastError && config?.status?.lastErrorSource === "auto";
const currentRemotePath = `/${form.remoteRoot.trim() || "cc-switch-sync"}/v2/db-v6/${form.profile.trim() || "default"}`;
const remoteDbCompatDisplay = formatDbCompatVersion(remoteInfo?.dbCompatVersion);
const remoteIsLegacy = remoteInfo?.layout === "legacy";
// ─── Render ─────────────────────────────────────────────
@@ -720,8 +730,7 @@ export function WebdavSyncSection({ config }: WebdavSyncSectionProps) {
{t("settings.webdavSync.confirmUpload.targetPath")}
{": "}
<code className="ml-1 text-xs bg-muted px-1.5 py-0.5 rounded">
/{form.remoteRoot.trim() || "cc-switch-sync"}/v2/
{form.profile.trim() || "default"}
{currentRemotePath}
</code>
</p>
{remoteInfo && (
@@ -742,14 +751,35 @@ export function WebdavSyncSection({ config }: WebdavSyncSectionProps) {
{t("settings.webdavSync.confirmUpload.createdAt")}
</dt>
<dd>{formatDate(remoteInfo.createdAt)}</dd>
<dt className="font-medium text-foreground">
{t("settings.webdavSync.confirmUpload.path")}
</dt>
<dd>
<code className="bg-muted px-1.5 py-0.5 rounded">
{remoteInfo.remotePath}
</code>
</dd>
{remoteDbCompatDisplay && (
<>
<dt className="font-medium text-foreground">
{t("settings.webdavSync.confirmUpload.dbCompat")}
</dt>
<dd>{remoteDbCompatDisplay}</dd>
</>
)}
</dl>
</div>
)}
{remoteInfo && (
{remoteInfo && !remoteIsLegacy && (
<p className="text-destructive font-medium">
{t("settings.webdavSync.confirmUpload.warning")}
</p>
)}
{remoteInfo && remoteIsLegacy && (
<p className="font-medium text-amber-600 dark:text-amber-400">
{t("settings.webdavSync.confirmUpload.legacyNotice")}
</p>
)}
</div>
</DialogDescription>
</DialogHeader>
@@ -793,12 +823,33 @@ export function WebdavSyncSection({ config }: WebdavSyncSectionProps) {
{t("settings.webdavSync.confirmDownload.createdAt")}
</dt>
<dd>{formatDate(remoteInfo.createdAt)}</dd>
<dt className="font-medium text-foreground">
{t("settings.webdavSync.confirmDownload.path")}
</dt>
<dd>
<code className="text-xs bg-muted px-1.5 py-0.5 rounded">
{remoteInfo.remotePath}
</code>
</dd>
{remoteDbCompatDisplay && (
<>
<dt className="font-medium text-foreground">
{t("settings.webdavSync.confirmDownload.dbCompat")}
</dt>
<dd>{remoteDbCompatDisplay}</dd>
</>
)}
<dt className="font-medium text-foreground">
{t("settings.webdavSync.confirmDownload.artifacts")}
</dt>
<dd>{remoteInfo.artifacts.join(", ")}</dd>
</dl>
)}
{remoteInfo?.layout === "legacy" && (
<p className="font-medium text-amber-600 dark:text-amber-400">
{t("settings.webdavSync.confirmDownload.legacyNotice")}
</p>
)}
<p className="text-destructive font-medium">
{t("settings.webdavSync.confirmDownload.warning")}
</p>
+7 -1
View File
@@ -397,7 +397,7 @@
"saveAndTestSuccess": "Config saved, connection OK",
"saveAndTestFailed": "Config saved, but connection test failed: {{error}}",
"noRemoteData": "No sync data found on the remote server",
"incompatibleVersion": "Remote data version incompatible (v{{version}}), current supports v2",
"incompatibleVersion": "Remote data is incompatible (protocol v{{protocolVersion}}, database {{dbCompatVersion}}). This client supports protocol v2 / db-v6.",
"unsaved": "Unsaved",
"saved": "Saved",
"unsavedChanges": "Please save config first",
@@ -408,7 +408,10 @@
"title": "Restore from Cloud",
"deviceName": "Uploaded by",
"createdAt": "Uploaded at",
"path": "Remote path",
"dbCompat": "DB compatibility",
"artifacts": "Contents",
"legacyNotice": "A legacy remote path was detected. After restoring, the next upload will write to the new v2/db-v6 path.",
"warning": "This will overwrite all local data and skill configurations",
"confirm": "Confirm Restore"
},
@@ -421,7 +424,10 @@
"existingData": "Existing cloud data",
"deviceName": "Uploaded by",
"createdAt": "Uploaded at",
"path": "Remote path",
"dbCompat": "DB compatibility",
"warning": "This will overwrite existing sync data on the remote server",
"legacyNotice": "Legacy remote data was detected. This upload will write to the new v2/db-v6 path and will not overwrite the legacy path.",
"confirm": "Confirm Upload"
}
},
+7 -1
View File
@@ -397,7 +397,7 @@
"saveAndTestSuccess": "設定を保存しました。接続正常です",
"saveAndTestFailed": "設定を保存しましたが、接続テストに失敗しました:{{error}}",
"noRemoteData": "クラウドに同期データが見つかりません",
"incompatibleVersion": "リモートデータのバージョンに互換性がありません(v{{version}})。現在 v2 をサポートしています",
"incompatibleVersion": "リモートデータに互換性がありません(プロトコル v{{protocolVersion}}、データベース {{dbCompatVersion}})。このクライアントは protocol v2 / db-v6 をサポートしています",
"unsaved": "未保存",
"saved": "保存済み",
"unsavedChanges": "先に設定を保存してください",
@@ -408,7 +408,10 @@
"title": "クラウドから復元",
"deviceName": "アップロード元",
"createdAt": "アップロード日時",
"path": "リモートパス",
"dbCompat": "DB 互換レイヤー",
"artifacts": "内容",
"legacyNotice": "旧レイアウトのリモートパスを検出しました。復元後、次回のアップロードは新しい v2/db-v6 パスに書き込まれます。",
"warning": "ローカルのすべてのデータとスキル設定が上書きされます",
"confirm": "復元を実行"
},
@@ -421,7 +424,10 @@
"existingData": "クラウドの既存データ",
"deviceName": "アップロード元",
"createdAt": "アップロード日時",
"path": "リモートパス",
"dbCompat": "DB 互換レイヤー",
"warning": "リモートの既存同期データが上書きされます",
"legacyNotice": "旧レイアウトのリモートデータを検出しました。今回のアップロードは新しい v2/db-v6 パスに書き込み、旧パスは上書きしません。",
"confirm": "アップロードを実行"
}
},
+7 -1
View File
@@ -397,7 +397,7 @@
"saveAndTestSuccess": "配置已保存,连接正常",
"saveAndTestFailed": "配置已保存,但连接测试失败:{{error}}",
"noRemoteData": "云端没有找到同步数据",
"incompatibleVersion": "远端数据版本不兼容(v{{version}}),当前支持 v2",
"incompatibleVersion": "远端数据版本不兼容(协议 v{{protocolVersion}},数据库 {{dbCompatVersion}}),当前支持协议 v2 / db-v6",
"unsaved": "未保存",
"saved": "已保存",
"unsavedChanges": "请先保存配置",
@@ -408,7 +408,10 @@
"title": "从云端恢复",
"deviceName": "上传设备",
"createdAt": "上传时间",
"path": "远端路径",
"dbCompat": "数据库兼容层",
"artifacts": "包含内容",
"legacyNotice": "检测到旧版云端路径。恢复完成后,下次上传将写入新路径 v2/db-v6。",
"warning": "恢复将覆盖本地所有数据和技能配置",
"confirm": "确认恢复"
},
@@ -421,7 +424,10 @@
"existingData": "云端已有数据",
"deviceName": "上传设备",
"createdAt": "上传时间",
"path": "远端路径",
"dbCompat": "数据库兼容层",
"warning": "将覆盖云端已有的同步数据",
"legacyNotice": "检测到旧版云端路径数据。本次上传将写入新路径 v2/db-v6,不会覆盖旧路径。",
"confirm": "确认上传"
}
},
+1 -1
View File
@@ -102,7 +102,7 @@ export const settingsApi = {
return await invoke("import_config_from_file", { filePath });
},
// ─── WebDAV v2 sync ───────────────────────────────────────
// ─── WebDAV sync ──────────────────────────────────────────
async webdavTestConnection(
settings: WebDavSyncSettings,
+8 -2
View File
@@ -169,7 +169,7 @@ export interface VisibleApps {
openclaw: boolean;
}
// WebDAV v2 同步状态
// WebDAV 同步状态
export interface WebDavSyncStatus {
lastSyncAt?: number | null;
lastError?: string | null;
@@ -179,7 +179,7 @@ export interface WebDavSyncStatus {
lastRemoteManifestHash?: string | null;
}
// WebDAV v2 同步配置
// WebDAV 同步配置
export interface WebDavSyncSettings {
enabled?: boolean;
autoSync?: boolean;
@@ -191,14 +191,20 @@ export interface WebDavSyncSettings {
status?: WebDavSyncStatus;
}
export type RemoteSnapshotLayout = "current" | "legacy";
// 远端快照信息(下载前预览)
export interface RemoteSnapshotInfo {
deviceName: string;
createdAt: string;
snapshotId: string;
version: number;
protocolVersion: number;
dbCompatVersion?: number | null;
compatible: boolean;
artifacts: string[];
layout: RemoteSnapshotLayout;
remotePath: string;
}
// 应用设置类型(用于设置对话框与 Tauri API)