feat: improve upload resilience and release 0.1.23

This commit is contained in:
2026-02-20 20:21:42 +08:00
parent 6618de1aed
commit 01384a2215
9 changed files with 435 additions and 115 deletions

View File

@@ -153,7 +153,7 @@ const syncState = reactive({
nextRunAt: "",
});
const updateState = reactive({
currentVersion: "0.1.22",
currentVersion: "0.1.23",
latestVersion: "",
available: false,
mandatory: false,
@@ -241,6 +241,7 @@ let unlistenNativeDownloadProgress: UnlistenFn | null = null;
let unlistenNativeUploadProgress: UnlistenFn | null = null;
let syncTimer: ReturnType<typeof setInterval> | null = null;
let hasCheckedUpdateAfterAuth = false;
let authRefreshPromise: Promise<boolean> | null = null;
const toast = reactive({
visible: false,
@@ -806,12 +807,40 @@ function toggleFileSortOrder() {
fileViewState.sortOrder = fileViewState.sortOrder === "asc" ? "desc" : "asc";
}
async function invokeBridge(command: string, payload: Record<string, any>) {
function isAuthBridgeCommand(command: string) {
return command === "api_login" || command === "api_refresh_token";
}
async function refreshAccessToken() {
if (authRefreshPromise) {
return authRefreshPromise;
}
authRefreshPromise = (async () => {
try {
const response = await invoke<BridgeResponse>("api_refresh_token", {
baseUrl: appConfig.baseUrl,
});
return Boolean(response.ok && response.data?.success);
} catch {
return false;
} finally {
authRefreshPromise = null;
}
})();
return authRefreshPromise;
}
async function invokeBridge(
command: string,
payload: Record<string, any>,
allowAuthRetry = true,
) {
let response: BridgeResponse;
try {
return await invoke<BridgeResponse>(command, payload);
response = await invoke<BridgeResponse>(command, payload);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
return {
response = {
ok: false,
status: 0,
data: {
@@ -820,6 +849,20 @@ async function invokeBridge(command: string, payload: Record<string, any>) {
},
} satisfies BridgeResponse;
}
if (
response.status === 401
&& allowAuthRetry
&& !isAuthBridgeCommand(command)
&& authenticated.value
) {
const refreshed = await refreshAccessToken();
if (refreshed) {
return invokeBridge(command, payload, false);
}
}
return response;
}
async function initClientVersion() {
@@ -1106,34 +1149,78 @@ async function chooseSyncDirectory() {
}
}
async function uploadFileWithResume(filePath: string, targetPath: string, taskId?: string) {
const resumableResponse = await invokeBridge("api_upload_file_resumable", {
baseUrl: appConfig.baseUrl,
filePath,
targetPath,
chunkSize: 4 * 1024 * 1024,
taskId: taskId || null,
});
if (resumableResponse.ok && resumableResponse.data?.success) {
return resumableResponse;
function isRetryableUploadResponse(response: BridgeResponse) {
const status = Number(response.status || 0);
if ([0, 408, 425, 429, 500, 502, 503, 504].includes(status)) {
return true;
}
const message = String(response.data?.message || "").toLowerCase();
return (
message.includes("timeout")
|| message.includes("timed out")
|| message.includes("network")
|| message.includes("connection")
|| message.includes("超时")
|| message.includes("稍后重试")
);
}
const message = String(resumableResponse.data?.message || "");
if (
message.includes("当前存储模式不支持分片上传")
|| message.includes("分片上传会话")
|| message.includes("上传会话")
) {
return await invokeBridge("api_upload_file", {
async function waitMs(ms: number) {
await new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
}
async function uploadFileWithResume(filePath: string, targetPath: string, taskId?: string) {
const maxAttempts = 3;
let lastResponse: BridgeResponse | null = null;
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
const resumableResponse = await invokeBridge("api_upload_file_resumable", {
baseUrl: appConfig.baseUrl,
filePath,
targetPath,
chunkSize: 4 * 1024 * 1024,
taskId: taskId || null,
});
if (resumableResponse.ok && resumableResponse.data?.success) {
return resumableResponse;
}
const message = String(resumableResponse.data?.message || "");
if (
message.includes("当前存储模式不支持分片上传")
|| message.includes("分片上传会话")
|| message.includes("上传会话")
) {
const fallbackResponse = await invokeBridge("api_upload_file", {
baseUrl: appConfig.baseUrl,
filePath,
targetPath,
taskId: taskId || null,
});
if (fallbackResponse.ok && fallbackResponse.data?.success) {
return fallbackResponse;
}
lastResponse = fallbackResponse;
} else {
lastResponse = resumableResponse;
}
if (attempt < maxAttempts - 1 && lastResponse && isRetryableUploadResponse(lastResponse)) {
await waitMs(600 * (attempt + 1));
continue;
}
break;
}
return resumableResponse;
return lastResponse || {
ok: false,
status: 0,
data: {
success: false,
message: "上传失败",
},
};
}
function rebuildSyncScheduler() {