refactor(*): 稳定期治理 - 收敛核心架构、错误语义并修复跨项目数据竞态

This commit is contained in:
ddCat
2026-09-09 22:02:10 +08:00
parent bacd502f5c
commit b5f3413ded
298 changed files with 12681 additions and 26428 deletions

View File

@@ -3,7 +3,7 @@ import localforage from "localforage";
import { nanoid } from "nanoid";
import { readImageMeta } from "@/lib/image-utils";
import { getActiveUserScope } from "@/lib/user-scope";
import { importResourceFromUrl, isResourceUrl, resourceFileUrl, resourceIdFromStorageKey, resourceStorageKey, uploadResourceFile } from "@/services/api/resources";
import { importResourceFromUrl, isResourceUrl, resourceFileUrl, resourceIdFromStorageKey, resourceStorageKey, ResourceUploadError, uploadResourceFile } from "@/services/api/resources";
import { cacheResourceObjectUrl, getCachedResourceBlob, getCachedResourceObjectUrl, primeResourceBlobCache } from "@/services/resource-blob-cache";
export type UploadedImage = {
@@ -13,6 +13,14 @@ export type UploadedImage = {
height: number;
bytes: number;
mimeType: string;
/**
* true 表示直传失败、文件当前只存在于本机 IndexedDB。
* 云端数据同步会用同一幂等键重传,但在那之前它不是一份已持久化的服务端资源:
* `url` 是页面级 objectURL,刷新即失效。UI 不得把这种结果说成"已保存"。
*/
pendingRemoteUpload?: boolean;
/** 直传失败原因,仅在 pendingRemoteUpload 为 true 时有值,供 UI 如实告知用户。 */
remoteUploadError?: string;
};
const store = localforage.createInstance({ name: "infinite-canvas", storeName: "image_files" });
@@ -40,6 +48,7 @@ export async function uploadImage(input: string | Blob, onProgress?: (uploadedBy
const blob = typeof input === "string" ? await (await fetch(input)).blob() : input;
const previewUrl = URL.createObjectURL(blob);
const meta = await readImageMeta(previewUrl);
let remoteUploadError = "";
try {
const resource = await uploadResourceFile(blob, "image", { width: meta.width, height: meta.height, fileName: input instanceof File ? input.name : undefined, idempotencyKey: storageKey }, onProgress);
await primeResourceBlobCache(resourceStorageKey(resource.id), blob).catch(() => "");
@@ -52,13 +61,16 @@ export async function uploadImage(input: string | Blob, onProgress?: (uploadedBy
bytes: resource.size || blob.size,
mimeType: resource.mimeType || blob.type || meta.mimeType,
};
} catch {
// OSS is optional during local/self-hosted setup. Keep the existing local fallback.
} catch (error) {
// 鉴权失效、越权、体积超限这类失败重传也是同样结果,不能退化成"稍后自动同步"。
if (error instanceof ResourceUploadError && error.permanent) throw error;
remoteUploadError = error instanceof Error ? error.message : "图片直传失败";
}
// 瞬时失败退回本机:文件仍可用,且云端数据同步会用同一幂等键重传。
await store.setItem(storageKey, blob);
const url = previewUrl;
objectUrls.set(storageKey, url);
return { url, storageKey, width: meta.width, height: meta.height, bytes: blob.size, mimeType: blob.type || meta.mimeType };
return { url, storageKey, width: meta.width, height: meta.height, bytes: blob.size, mimeType: blob.type || meta.mimeType, pendingRemoteUpload: true, remoteUploadError };
}
function shouldImportRemoteImage(input: string) {