fix(canvas): keep uploaded resources in asset library (#388)

This commit is contained in:
MasterChiefCN
2026-09-02 20:18:49 +08:00
committed by GitHub
parent 85c018adc1
commit a0b65006fb
22 changed files with 1154 additions and 34 deletions

View File

@@ -9,6 +9,7 @@ import type { CanvasProject } from "@/stores/canvas/use-canvas-store";
import { flushCanvasStorePersistence, useCanvasStore } from "@/stores/canvas/use-canvas-store";
import { useSyncProgressStore } from "@/stores/use-sync-progress-store";
import { useCanvasHistoryStore } from "@/stores/canvas/use-canvas-history-store";
import { repairMissingCanvasAssets } from "@/services/canvas-asset-repair";
let activeRemoteUserId = "";
type RemoteUserDataPhase = "inactive" | "hydrating" | "ready" | "failed";
@@ -25,6 +26,7 @@ let acknowledgedProjects = new Map<string, CanvasProject>();
const LOCAL_STORAGE_KEY_PATTERN = /^(image|video|audio|file|video-reference|audio-reference):/;
export async function syncRemoteUserData(userId?: string | null) {
let repairedCanvasAssets = false;
await withRemoteUserDataSyncExclusive(async () => {
activeRemoteUserId = userId || "";
acknowledgedProjects.clear();
@@ -42,6 +44,8 @@ export async function syncRemoteUserData(userId?: string | null) {
// 这里只替换结构化记录,不在登录阶段解析图片/视频/音频 URL;媒体由实际使用方按需解析。
useCanvasStore.getState().replaceProjects(snapshot.projects);
useAssetStore.getState().replaceAssets(snapshot.assets);
const repair = repairMissingCanvasAssets();
repairedCanvasAssets = repair.createdAssets > 0 || repair.updatedProjects > 0;
await Promise.all([flushCanvasStorePersistence(), flushAssetStorePersistence()]);
acknowledgedProjects = new Map(snapshot.projects.map((project) => [project.id, project]));
acknowledgedAssets = new Map(snapshot.assets.map((asset) => [asset.id, asset]));
@@ -51,6 +55,7 @@ export async function syncRemoteUserData(userId?: string | null) {
throw error;
}
});
if (repairedCanvasAssets) await saveRemoteUserDataNow();
}
export function installRemoteUserDataAutoSync() {
@@ -167,6 +172,9 @@ export async function deleteCanvasProjectsWithRemoteSync(ids: string[]) {
for (const node of proj.nodes) {
if (node.metadata?.assetId) activeAssetIds.add(node.metadata.assetId);
}
for (const clip of proj.timeline?.clips || []) {
if (clip.directMedia?.assetId) activeAssetIds.add(clip.directMedia.assetId);
}
}
let assetChanged = false;
// 1. 已存在的关联素材标记为 archived
@@ -314,6 +322,9 @@ async function drainRemoteUserDataChanges() {
}
async function saveRemoteUserDataBatch(uploaded: Map<string, string>) {
// 中央兜底:任何调用方只要把持久媒体写进画布,提交前都会先补齐素材记录与 assetId。
// 页面级入口仍主动入库,以便立即反馈;这里负责阻止遗漏入口形成远端幽灵资源。
repairMissingCanvasAssets();
const currentProjects = useCanvasStore.getState().projects;
const currentAssets = useAssetStore.getState().assets;
const dirtyProjects = currentProjects.filter((project) => !sameEntitySnapshot(acknowledgedProjects.get(project.id), project));
@@ -326,6 +337,13 @@ async function saveRemoteUserDataBatch(uploaded: Map<string, string>) {
// 转换后的 resource: 引用只属于发往服务端的 payload,不能反写整份实时 store。
// 已确认快照记录的是本次上传所依据的本地实体;上传期间的新编辑会在下一轮继续提交。
// 素材先于画布提交。这样画布中的 resource: 引用一旦成为远端事实,
// 对应 Asset 已经存在,刷新或换设备不会出现只占容量、不见素材的窗口。
for (const source of dirtyAssets) {
const remotePayload = await ensureRemoteResourceReferences(assetForRemoteSync(source), uploaded);
await upsertRemoteAsset(remotePayload);
acknowledgedAssets.set(source.id, source);
}
for (const source of dirtyProjects) {
const keysToUpload = collectLocalMediaKeys(source);
const total = keysToUpload.length;
@@ -364,11 +382,6 @@ async function saveRemoteUserDataBatch(uploaded: Map<string, string>) {
throw error;
}
}
for (const source of dirtyAssets) {
const remotePayload = await ensureRemoteResourceReferences(assetForRemoteSync(source), uploaded);
await upsertRemoteAsset(remotePayload);
acknowledgedAssets.set(source.id, source);
}
for (const id of deletedProjectIds) {
await deleteRemoteCanvasProject(id);
acknowledgedProjects.delete(id);