feat: 新增 S3 兼容云存储同步 (#1351)

* Add S3 Cloud Sync design document

Design for adding AWS S3 as a new Cloud Sync backend alongside WebDAV.
Hybrid approach: extract shared sync protocol, add independent S3 transport.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add S3 cloud sync implementation design (reqwest + Sig V4)

Updated design based on 2026-03-06 draft: switches from rust-s3 crate
to hand-rolled AWS Sig V4 on existing reqwest for broader S3-compatible
service support (AWS, MinIO, R2, Alibaba OSS, Tencent COS, Huawei OBS).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add S3 cloud sync implementation plan (11 tasks, TDD)

Detailed step-by-step plan covering: sync_protocol extraction, S3 Sig V4
transport, settings, sync/auto-sync modules, Tauri commands, frontend
presets/dynamic form, and i18n.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* deps: add hmac crate for S3 Sig V4 signing

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: extract sync_protocol.rs from webdav_sync.rs for shared use

Move transport-agnostic sync protocol logic (constants, types, snapshot
building, manifest validation, artifact verification, snapshot application,
utilities) into a new shared sync_protocol module so both WebDAV and the
upcoming S3 transport can reuse it.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: use transport-neutral error keys in sync_protocol

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add S3 transport layer with AWS Sig V4 signing

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add S3SyncSettings to AppSettings

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add S3 sync module with upload/download/fetch

Implements the S3 sync protocol layer (s3_sync.rs) that combines the
shared sync_protocol with the S3 transport. Mirrors the WebDAV sync
module structure with independent sync mutex, connection check,
upload, download, fetch_remote_info, and sync status persistence.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add S3 auto sync worker with debounce

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add S3 sync Tauri commands and auto sync worker startup

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add S3 sync TypeScript types and API layer

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add S3 sync i18n translations (en/zh/ja)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add S3 sync presets and dynamic form to sync settings

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: preserve HTTP scheme for S3 custom endpoints (MinIO support)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* test: add live S3 integration tests (env-var driven, --ignored)

Run with: S3_TEST_AK=... S3_TEST_SK=... S3_TEST_BUCKET=... cargo test --lib services::s3::integration_tests -- --ignored

Verifies test_connection, put_object, get_object, head_object, and 404
handling against a real S3 bucket using the project's own Sig V4 signing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: remove internal design docs before PR

* fix: wire S3 auto-sync to DB hook & sync UI state on async load

- P1: Add s3_auto_sync::notify_db_changed call in SQLite update_hook
  so S3 auto-sync worker receives DB change signals (was only wired
  for WebDAV, leaving S3 worker idle)

- P2: Add useEffect to update syncType selector when s3Config loads
  asynchronously, preventing stale "webdav" default for S3 users

* fix: satisfy clippy for s3 sync

* fix: address s3 sync review feedback

---------

Co-authored-by: Keith (via OpenClaw) <keithyt06@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Jason <farion1231@gmail.com>
This commit is contained in:
Keith Yu
2026-06-04 22:18:51 +08:00
committed by GitHub
parent ea95f39adf
commit 2a24da517f
26 changed files with 4393 additions and 795 deletions
+18 -2
View File
@@ -107,7 +107,7 @@ type View =
| "openclawAgents"
| "hermesMemory";
interface WebDavSyncStatusUpdatedPayload {
interface SyncStatusUpdatedPayload {
source?: string;
status?: string;
error?: string;
@@ -371,7 +371,7 @@ function App() {
}
});
useTauriEvent<WebDavSyncStatusUpdatedPayload | null | undefined>(
useTauriEvent<SyncStatusUpdatedPayload | null | undefined>(
"webdav-sync-status-updated",
async (payload) => {
const statusPayload = payload ?? {};
@@ -387,6 +387,22 @@ function App() {
},
);
useTauriEvent<SyncStatusUpdatedPayload | null | undefined>(
"s3-sync-status-updated",
async (payload) => {
const statusPayload = payload ?? {};
await queryClient.invalidateQueries({ queryKey: ["settings"] });
if (statusPayload.source !== "auto" || statusPayload.status !== "error") {
return;
}
toast.error(
t("settings.s3Sync.autoSyncFailedToast", {
error: statusPayload.error || t("common.unknown"),
}),
);
},
);
useTauriEvent<{ appType: string; providerName: string }>(
"proxy-official-warning",
(payload) => {
+1
View File
@@ -415,6 +415,7 @@ export function SettingsPage({
<AccordionContent className="px-6 pb-6 pt-4 border-t border-border/50">
<WebdavSyncSection
config={settings?.webdavSync}
s3Config={settings?.s3Sync}
settings={settings}
onAutoSave={handleAutoSave}
/>
File diff suppressed because it is too large Load Diff
+10 -4
View File
@@ -193,8 +193,11 @@ export function useSettings(): UseSettingsResult {
const sanitizedOpenclawDir = sanitizeDir(
mergedSettings.openclawConfigDir,
);
const { webdavSync: _ignoredWebdavSync, ...restSettings } =
mergedSettings;
const {
webdavSync: _ignoredWebdavSync,
s3Sync: _ignoredS3Sync,
...restSettings
} = mergedSettings;
const payload: Settings = {
...restSettings,
@@ -327,8 +330,11 @@ export function useSettings(): UseSettingsResult {
const previousGeminiDir = sanitizeDir(data?.geminiConfigDir);
const previousOpencodeDir = sanitizeDir(data?.opencodeConfigDir);
const previousOpenclawDir = sanitizeDir(data?.openclawConfigDir);
const { webdavSync: _ignoredWebdavSync, ...restSettings } =
mergedSettings;
const {
webdavSync: _ignoredWebdavSync,
s3Sync: _ignoredS3Sync,
...restSettings
} = mergedSettings;
const payload: Settings = {
...restSettings,
+101
View File
@@ -546,6 +546,107 @@
"confirm": "Confirm Upload"
}
},
"syncType": {
"label": "Sync Type",
"webdav": "WebDAV",
"s3": "S3 Compatible"
},
"s3Sync": {
"presets": {
"label": "Provider",
"awsS3": "AWS S3",
"awsS3Hint": "Use IAM Access Key with s3:PutObject/GetObject/HeadObject permissions. Region: us-east-1, ap-northeast-1, etc.",
"minio": "MinIO",
"minioHint": "Custom endpoint required, e.g. http://minio.local:9000. Region can be 'us-east-1'.",
"r2": "Cloudflare R2",
"r2Hint": "Endpoint: https://<account_id>.r2.cloudflarestorage.com. Region: 'auto'.",
"oss": "Alibaba Cloud OSS",
"ossHint": "Endpoint: https://oss-<region>.aliyuncs.com. Region: cn-hangzhou, cn-shanghai, etc.",
"cos": "Tencent Cloud COS",
"cosHint": "Endpoint: https://cos.<region>.myqcloud.com. Region: ap-guangzhou, ap-beijing, etc.",
"obs": "Huawei OBS",
"obsHint": "Endpoint: https://obs.<region>.myhuaweicloud.com. Region: cn-north-4, cn-east-3, etc.",
"custom": "S3 Compatible (Custom)",
"customHint": "Any S3-compatible service. Custom endpoint required."
},
"region": "Region",
"regionPlaceholder": "e.g. us-east-1",
"bucket": "Bucket",
"bucketPlaceholder": "e.g. my-cc-switch-bucket",
"accessKeyId": "Access Key ID",
"accessKeyIdPlaceholder": "e.g. AKIA...",
"secretAccessKey": "Secret Access Key",
"secretAccessKeyPlaceholder": "Leave unchanged to keep the existing secret",
"endpoint": "Endpoint (optional for AWS)",
"endpointHint": "Required for most S3-compatible services",
"endpointPlaceholder": "e.g. https://s3-compatible.example.com",
"remoteRoot": "Remote Root Directory",
"remoteRootDefault": "Default: cc-switch-sync",
"profile": "Sync Profile Name",
"profileDefault": "Default: default",
"autoSync": "Auto Sync",
"autoSyncHint": "When enabled, each database change triggers an automatic S3 upload.",
"enabled": "Enable S3 Sync",
"enabledHint": "Turn on S3 sync for manual and automatic cloud operations.",
"mutualExclusionTitle": "Switch Sync Type",
"mutualExclusionMessage": "Enabling S3 sync will disable the current WebDAV sync. Continue?",
"mutualExclusionMessageReverse": "Enabling WebDAV sync will disable the current S3 sync. Continue?",
"mutualExclusionFailed": "Failed to switch sync type: {{error}}",
"testConnection": "Test Connection",
"connectionOk": "S3 connection successful",
"test": "Test Connection",
"testing": "Testing...",
"testSuccess": "S3 connection successful",
"testFailed": "S3 connection failed: {{error}}",
"save": "Save Config",
"saving": "Saving...",
"saveFailed": "Failed to save config: {{error}}",
"saveAndTestSuccess": "Config saved, S3 connection OK",
"saveAndTestFailed": "Config saved, but S3 connection test failed: {{error}}",
"upload": "Upload to Cloud",
"uploading": "Uploading...",
"uploadSuccess": "Uploaded to S3",
"uploadFailed": "S3 upload failed: {{error}}",
"autoSyncFailedToast": "S3 auto sync failed: {{error}}",
"download": "Download from Cloud",
"downloading": "Downloading...",
"downloadSuccess": "Downloaded and restored from S3",
"downloadFailed": "S3 download failed: {{error}}",
"lastSync": "Last sync: {{time}}",
"autoSyncLastErrorTitle": "Last S3 auto sync failed",
"autoSyncLastErrorHint": "Please check network or S3 settings. Auto sync will retry on future changes.",
"missingBucket": "Please enter the S3 bucket",
"noRemoteData": "No sync data found in the S3 bucket",
"incompatibleVersion": "Remote data is incompatible (version {{version}}). This client supports protocol v2 / db-v6.",
"unsaved": "Unsaved",
"saved": "Saved",
"unsavedChanges": "Please save config first",
"saveBeforeSync": "Save configuration first to enable upload/download.",
"fetchingRemote": "Fetching remote info...",
"fetchRemoteFailed": "Failed to fetch remote info. Please check configuration and network.",
"notConfigured": "S3 sync is not configured",
"disabled": "S3 sync is disabled",
"confirmDownload": {
"title": "Restore from S3",
"deviceName": "Uploaded by",
"createdAt": "Uploaded at",
"artifacts": "Contents",
"warning": "This will overwrite all local data and skill configurations",
"confirm": "Confirm Restore"
},
"confirmUpload": {
"title": "Upload to S3",
"content": "The following will be synced to the S3 bucket:",
"dbItem": "Database (all provider configs and data)",
"skillsItem": "Skills (all custom skills)",
"targetPath": "Target object path",
"existingData": "Existing cloud data",
"deviceName": "Uploaded by",
"createdAt": "Uploaded at",
"warning": "This will overwrite existing sync data at the S3 path",
"confirm": "Confirm Upload"
}
},
"autoReload": "Data refreshed",
"languageOptionChinese": "简体中文",
"languageOptionTraditionalChinese": "繁體中文",
+101
View File
@@ -546,6 +546,107 @@
"confirm": "アップロードを実行"
}
},
"syncType": {
"label": "同期方式",
"webdav": "WebDAV",
"s3": "S3互換ストレージ"
},
"s3Sync": {
"presets": {
"label": "サービス",
"awsS3": "AWS S3",
"awsS3Hint": "IAM Access Keyを使用。s3:PutObject/GetObject/HeadObject権限が必要です。Region例: us-east-1, ap-northeast-1",
"minio": "MinIO",
"minioHint": "カスタムEndpointが必要です(例: http://minio.local:9000)。Regionは us-east-1 に設定可能",
"r2": "Cloudflare R2",
"r2Hint": "Endpoint: https://<account_id>.r2.cloudflarestorage.com、Regionは auto に設定",
"oss": "Alibaba Cloud OSS",
"ossHint": "Endpoint: https://oss-<region>.aliyuncs.com、Region例: cn-hangzhou, cn-shanghai",
"cos": "Tencent Cloud COS",
"cosHint": "Endpoint: https://cos.<region>.myqcloud.com、Region例: ap-guangzhou, ap-beijing",
"obs": "Huawei OBS",
"obsHint": "Endpoint: https://obs.<region>.myhuaweicloud.com、Region例: cn-north-4, cn-east-3",
"custom": "S3互換サービス(カスタム)",
"customHint": "任意のS3互換サービス。カスタムEndpointが必要です"
},
"region": "リージョン",
"regionPlaceholder": "例: us-east-1",
"bucket": "バケット",
"bucketPlaceholder": "例: my-cc-switch-bucket",
"accessKeyId": "Access Key ID",
"accessKeyIdPlaceholder": "例: AKIA...",
"secretAccessKey": "Secret Access Key",
"secretAccessKeyPlaceholder": "既存のシークレットを保持する場合は変更しないでください",
"endpoint": "エンドポイント(AWSは空欄可)",
"endpointHint": "多くの S3 互換サービスでは必須です",
"endpointPlaceholder": "例: https://s3-compatible.example.com",
"remoteRoot": "リモートルートディレクトリ",
"remoteRootDefault": "デフォルト: cc-switch-sync",
"profile": "同期プロファイル名",
"profileDefault": "デフォルト: default",
"autoSync": "自動同期",
"autoSyncHint": "有効にすると、データベース変更のたびに S3 へ自動アップロードします。",
"enabled": "S3 同期を有効化",
"enabledHint": "S3 のアップロード/ダウンロードと自動同期を有効にします。",
"mutualExclusionTitle": "同期方式の切り替え",
"mutualExclusionMessage": "S3同期を有効にすると、現在のWebDAV同期が無効になります。続行しますか?",
"mutualExclusionMessageReverse": "WebDAV同期を有効にすると、現在のS3同期が無効になります。続行しますか?",
"mutualExclusionFailed": "同期方式の切り替えに失敗しました:{{error}}",
"testConnection": "接続テスト",
"connectionOk": "S3接続成功",
"test": "接続テスト",
"testing": "テスト中...",
"testSuccess": "S3 接続成功",
"testFailed": "S3 接続失敗:{{error}}",
"save": "設定を保存",
"saving": "保存中...",
"saveFailed": "設定の保存に失敗しました:{{error}}",
"saveAndTestSuccess": "設定を保存しました。S3 接続正常です",
"saveAndTestFailed": "設定を保存しましたが、S3 接続テストに失敗しました:{{error}}",
"upload": "クラウドにアップロード",
"uploading": "アップロード中...",
"uploadSuccess": "S3 にアップロードしました",
"uploadFailed": "S3 アップロードに失敗しました:{{error}}",
"autoSyncFailedToast": "S3 自動同期に失敗しました:{{error}}",
"download": "クラウドからダウンロード",
"downloading": "ダウンロード中...",
"downloadSuccess": "S3 からダウンロード・復元しました",
"downloadFailed": "S3 ダウンロードに失敗しました:{{error}}",
"lastSync": "前回の同期:{{time}}",
"autoSyncLastErrorTitle": "前回の S3 自動同期に失敗しました",
"autoSyncLastErrorHint": "ネットワークまたは S3 設定を確認してください。次回の変更時に自動再試行されます。",
"missingBucket": "S3 バケットを入力してください",
"noRemoteData": "S3 バケットに同期データが見つかりません",
"incompatibleVersion": "リモートデータに互換性がありません(バージョン {{version}})。このクライアントは protocol v2 / db-v6 をサポートしています。",
"unsaved": "未保存",
"saved": "保存済み",
"unsavedChanges": "先に設定を保存してください",
"saveBeforeSync": "アップロード/ダウンロードを有効にするには、先に設定を保存してください。",
"fetchingRemote": "リモート情報を取得中...",
"fetchRemoteFailed": "リモート情報の取得に失敗しました。設定とネットワークを確認してください。",
"notConfigured": "S3同期が設定されていません",
"disabled": "S3同期が無効です",
"confirmDownload": {
"title": "S3 から復元",
"deviceName": "アップロード元",
"createdAt": "アップロード日時",
"artifacts": "内容",
"warning": "ローカルのすべてのデータとスキル設定が上書きされます",
"confirm": "復元を実行"
},
"confirmUpload": {
"title": "S3 にアップロード",
"content": "以下の内容を S3 バケットに同期します:",
"dbItem": "データベース(すべてのプロバイダー設定とデータ)",
"skillsItem": "スキル(すべてのカスタムスキル)",
"targetPath": "保存先オブジェクトパス",
"existingData": "クラウドの既存データ",
"deviceName": "アップロード元",
"createdAt": "アップロード日時",
"warning": "この S3 パスにある既存の同期データが上書きされます",
"confirm": "アップロードを実行"
}
},
"autoReload": "データを更新しました",
"languageOptionChinese": "简体中文",
"languageOptionTraditionalChinese": "繁體中文",
+101
View File
@@ -546,6 +546,107 @@
"confirm": "確認上傳"
}
},
"syncType": {
"label": "同步方式",
"webdav": "WebDAV",
"s3": "S3 相容儲存"
},
"s3Sync": {
"presets": {
"label": "服務商",
"awsS3": "AWS S3",
"awsS3Hint": "使用 IAM Access Key,需要 s3:PutObject/GetObject/HeadObject 權限。Region 範例:us-east-1, ap-northeast-1",
"minio": "MinIO",
"minioHint": "需要自訂 Endpoint,如 http://minio.local:9000。Region 可設為 us-east-1",
"r2": "Cloudflare R2",
"r2Hint": "Endpoint: https://<account_id>.r2.cloudflarestorage.comRegion 設為 auto",
"oss": "阿里雲 OSS",
"ossHint": "Endpoint: https://oss-<region>.aliyuncs.comRegion 範例:cn-hangzhou, cn-shanghai",
"cos": "騰訊雲 COS",
"cosHint": "Endpoint: https://cos.<region>.myqcloud.comRegion 範例:ap-guangzhou, ap-beijing",
"obs": "華為雲 OBS",
"obsHint": "Endpoint: https://obs.<region>.myhuaweicloud.comRegion 範例:cn-north-4, cn-east-3",
"custom": "S3 相容服務(自訂)",
"customHint": "任意 S3 相容服務,需要自訂 Endpoint"
},
"region": "區域 (Region)",
"regionPlaceholder": "例如 us-east-1",
"bucket": "儲存桶 (Bucket)",
"bucketPlaceholder": "例如 my-cc-switch-bucket",
"accessKeyId": "Access Key ID",
"accessKeyIdPlaceholder": "例如 AKIA...",
"secretAccessKey": "Secret Access Key",
"secretAccessKeyPlaceholder": "留空則保持現有金鑰",
"endpoint": "EndpointAWS 可留空)",
"endpointHint": "多數 S3 相容服務需要填寫",
"endpointPlaceholder": "例如 https://s3-compatible.example.com",
"remoteRoot": "遠端根目錄",
"remoteRootDefault": "預設: cc-switch-sync",
"profile": "同步設定檔名稱",
"profileDefault": "預設: default",
"autoSync": "自動同步",
"autoSyncHint": "開啟後每次資料庫變更都會自動上傳到 S3。",
"enabled": "啟用 S3 同步",
"enabledHint": "開啟後可使用 S3 上傳/下載和自動同步。",
"mutualExclusionTitle": "切換同步方式",
"mutualExclusionMessage": "啟用 S3 同步將停用目前的 WebDAV 同步,是否繼續?",
"mutualExclusionMessageReverse": "啟用 WebDAV 同步將停用目前的 S3 同步,是否繼續?",
"mutualExclusionFailed": "切換同步方式失敗:{{error}}",
"testConnection": "測試連線",
"connectionOk": "S3 連線成功",
"test": "測試連線",
"testing": "測試中...",
"testSuccess": "S3 連線成功",
"testFailed": "S3 連線失敗:{{error}}",
"save": "儲存設定",
"saving": "儲存中...",
"saveFailed": "儲存設定失敗:{{error}}",
"saveAndTestSuccess": "設定已儲存,S3 連線正常",
"saveAndTestFailed": "設定已儲存,但 S3 連線測試失敗:{{error}}",
"upload": "上傳至雲端",
"uploading": "上傳中...",
"uploadSuccess": "已上傳到 S3",
"uploadFailed": "S3 上傳失敗:{{error}}",
"autoSyncFailedToast": "S3 自動同步失敗:{{error}}",
"download": "從雲端下載",
"downloading": "下載中...",
"downloadSuccess": "已從 S3 下載並還原",
"downloadFailed": "S3 下載失敗:{{error}}",
"lastSync": "上次同步:{{time}}",
"autoSyncLastErrorTitle": "上次 S3 自動同步失敗",
"autoSyncLastErrorHint": "請檢查網路或 S3 設定,系統會在後續變更時繼續自動重試。",
"missingBucket": "請填寫 S3 儲存桶",
"noRemoteData": "S3 儲存桶中沒有找到同步資料",
"incompatibleVersion": "遠端資料版本不相容(版本 {{version}}),目前支援協議 v2 / db-v6",
"unsaved": "未儲存",
"saved": "已儲存",
"unsavedChanges": "請先儲存設定",
"saveBeforeSync": "請先儲存設定,再使用上傳/下載。",
"fetchingRemote": "取得遠端資訊...",
"fetchRemoteFailed": "取得遠端資訊失敗,請檢查設定和網路後重試。",
"notConfigured": "未設定 S3 同步",
"disabled": "S3 同步未啟用",
"confirmDownload": {
"title": "從 S3 還原",
"deviceName": "上傳裝置",
"createdAt": "上傳時間",
"artifacts": "包含內容",
"warning": "還原將覆寫本地所有資料和技能設定",
"confirm": "確認還原"
},
"confirmUpload": {
"title": "上傳到 S3",
"content": "將同步以下內容到 S3 儲存桶:",
"dbItem": "資料庫(所有 Provider 設定和資料)",
"skillsItem": "技能包(所有自訂技能)",
"targetPath": "目標物件路徑",
"existingData": "雲端已有資料",
"deviceName": "上傳裝置",
"createdAt": "上傳時間",
"warning": "將覆寫該 S3 路徑下已有的同步資料",
"confirm": "確認上傳"
}
},
"autoReload": "資料已重新整理",
"languageOptionChinese": "简体中文",
"languageOptionTraditionalChinese": "繁體中文",
+101
View File
@@ -546,6 +546,107 @@
"confirm": "确认上传"
}
},
"syncType": {
"label": "同步方式",
"webdav": "WebDAV",
"s3": "S3 兼容存储"
},
"s3Sync": {
"presets": {
"label": "服务商",
"awsS3": "AWS S3",
"awsS3Hint": "使用 IAM Access Key,需要 s3:PutObject/GetObject/HeadObject 权限。Region 示例:us-east-1, ap-northeast-1",
"minio": "MinIO",
"minioHint": "需要自定义 Endpoint,如 http://minio.local:9000。Region 可设为 us-east-1",
"r2": "Cloudflare R2",
"r2Hint": "Endpoint: https://<account_id>.r2.cloudflarestorage.comRegion 设为 auto",
"oss": "阿里云 OSS",
"ossHint": "Endpoint: https://oss-<region>.aliyuncs.comRegion 示例:cn-hangzhou, cn-shanghai",
"cos": "腾讯云 COS",
"cosHint": "Endpoint: https://cos.<region>.myqcloud.comRegion 示例:ap-guangzhou, ap-beijing",
"obs": "华为云 OBS",
"obsHint": "Endpoint: https://obs.<region>.myhuaweicloud.comRegion 示例:cn-north-4, cn-east-3",
"custom": "S3 兼容服务(自定义)",
"customHint": "任意 S3 兼容服务,需要自定义 Endpoint"
},
"region": "区域 (Region)",
"regionPlaceholder": "例如 us-east-1",
"bucket": "存储桶 (Bucket)",
"bucketPlaceholder": "例如 my-cc-switch-bucket",
"accessKeyId": "Access Key ID",
"accessKeyIdPlaceholder": "例如 AKIA...",
"secretAccessKey": "Secret Access Key",
"secretAccessKeyPlaceholder": "留空则保持现有密钥",
"endpoint": "EndpointAWS 可留空)",
"endpointHint": "多数 S3 兼容服务需要填写",
"endpointPlaceholder": "例如 https://s3-compatible.example.com",
"remoteRoot": "远程根目录",
"remoteRootDefault": "默认: cc-switch-sync",
"profile": "同步配置名",
"profileDefault": "默认: default",
"autoSync": "自动同步",
"autoSyncHint": "开启后每次数据库变更都会自动上传到 S3。",
"enabled": "启用 S3 同步",
"enabledHint": "开启后可使用 S3 上传/下载和自动同步。",
"mutualExclusionTitle": "切换同步方式",
"mutualExclusionMessage": "启用 S3 同步将禁用当前的 WebDAV 同步,是否继续?",
"mutualExclusionMessageReverse": "启用 WebDAV 同步将禁用当前的 S3 同步,是否继续?",
"mutualExclusionFailed": "切换同步方式失败:{{error}}",
"testConnection": "测试连接",
"connectionOk": "S3 连接成功",
"test": "测试连接",
"testing": "测试中...",
"testSuccess": "S3 连接成功",
"testFailed": "S3 连接失败:{{error}}",
"save": "保存配置",
"saving": "保存中...",
"saveFailed": "保存配置失败:{{error}}",
"saveAndTestSuccess": "配置已保存,S3 连接正常",
"saveAndTestFailed": "配置已保存,但 S3 连接测试失败:{{error}}",
"upload": "上传到云端",
"uploading": "上传中...",
"uploadSuccess": "已上传到 S3",
"uploadFailed": "S3 上传失败:{{error}}",
"autoSyncFailedToast": "S3 自动同步失败:{{error}}",
"download": "从云端下载",
"downloading": "下载中...",
"downloadSuccess": "已从 S3 下载并恢复",
"downloadFailed": "S3 下载失败:{{error}}",
"lastSync": "上次同步:{{time}}",
"autoSyncLastErrorTitle": "上次 S3 自动同步失败",
"autoSyncLastErrorHint": "请检查网络或 S3 配置,系统会在后续变更时继续自动重试。",
"missingBucket": "请填写 S3 存储桶",
"noRemoteData": "S3 存储桶中没有找到同步数据",
"incompatibleVersion": "远端数据版本不兼容(版本 {{version}}),当前支持协议 v2 / db-v6",
"unsaved": "未保存",
"saved": "已保存",
"unsavedChanges": "请先保存配置",
"saveBeforeSync": "请先保存配置,再使用上传/下载。",
"fetchingRemote": "获取远端信息...",
"fetchRemoteFailed": "获取远端信息失败,请检查配置和网络后重试。",
"notConfigured": "未配置 S3 同步",
"disabled": "S3 同步未启用",
"confirmDownload": {
"title": "从 S3 恢复",
"deviceName": "上传设备",
"createdAt": "上传时间",
"artifacts": "包含内容",
"warning": "恢复将覆盖本地所有数据和技能配置",
"confirm": "确认恢复"
},
"confirmUpload": {
"title": "上传到 S3",
"content": "将同步以下内容到 S3 存储桶:",
"dbItem": "数据库(所有 Provider 配置和数据)",
"skillsItem": "技能包(所有自定义技能)",
"targetPath": "目标对象路径",
"existingData": "云端已有数据",
"deviceName": "上传设备",
"createdAt": "上传时间",
"warning": "将覆盖该 S3 路径下已有的同步数据",
"confirm": "确认上传"
}
},
"autoReload": "数据已刷新",
"languageOptionChinese": "简体中文",
"languageOptionTraditionalChinese": "繁體中文",
+40 -1
View File
@@ -1,5 +1,10 @@
import { invoke } from "@tauri-apps/api/core";
import type { Settings, WebDavSyncSettings, RemoteSnapshotInfo } from "@/types";
import type {
Settings,
WebDavSyncSettings,
S3SyncSettings,
RemoteSnapshotInfo,
} from "@/types";
import type { AppId } from "./types";
export interface ConfigTransferResult {
@@ -142,6 +147,40 @@ export const settingsApi = {
return await invoke("webdav_sync_fetch_remote_info");
},
// ===== S3 Sync API =====
async s3TestConnection(
settings: S3SyncSettings,
preserveEmptyPassword = true,
): Promise<WebDavTestResult> {
return await invoke("s3_test_connection", {
settings,
preserveEmptyPassword,
});
},
async s3SyncUpload(): Promise<WebDavSyncResult> {
return await invoke("s3_sync_upload");
},
async s3SyncDownload(): Promise<WebDavSyncResult> {
return await invoke("s3_sync_download");
},
async s3SyncSaveSettings(
settings: S3SyncSettings,
passwordTouched: boolean,
): Promise<{ success: boolean }> {
return await invoke("s3_sync_save_settings", {
settings,
passwordTouched,
});
},
async s3SyncFetchRemoteInfo(): Promise<RemoteSnapshotInfo | { empty: true }> {
return await invoke("s3_sync_fetch_remote_info");
},
async syncCurrentProvidersLive(): Promise<void> {
const result = (await invoke("sync_current_providers_live")) as {
success?: boolean;
+17
View File
@@ -289,6 +289,20 @@ export interface WebDavSyncSettings {
status?: WebDavSyncStatus;
}
// S3 同步配置
export interface S3SyncSettings {
enabled?: boolean;
autoSync?: boolean;
region?: string;
bucket?: string;
accessKeyId?: string;
secretAccessKey?: string;
endpoint?: string;
remoteRoot?: string;
profile?: string;
status?: WebDavSyncStatus;
}
export type RemoteSnapshotLayout = "current" | "legacy";
// 远端快照信息(下载前预览)
@@ -382,6 +396,9 @@ export interface Settings {
// ===== WebDAV v2 同步设置 =====
webdavSync?: WebDavSyncSettings;
// ===== S3 同步设置 =====
s3Sync?: S3SyncSettings;
// ===== 备份策略设置 =====
// Auto-backup interval in hours (0=disabled, default 24)
backupIntervalHours?: number;