fix(canvas): 画布同步 - 素材缺失时可强制覆盖并恢复文本与连线顺序

云端缺少素材时可用本地版强制覆盖,只清失效媒体引用。文本节点不再把资源地址当正文,合并时保留引用槽位顺序。
This commit is contained in:
ddcat666
2026-10-10 20:19:54 +08:00
parent 1bbc95b996
commit dd66be15cb
12 changed files with 442 additions and 46 deletions

View File

@@ -17,6 +17,7 @@ export function CanvasSyncStatus({ projectId, onLoadLatest, onOpenVersions }: {
const localPhase = progress?.localPhase;
const localError = localPhase === "error";
const localPending = localPhase === "pending";
const missingResources = progress?.errorKind === "resources";
const retryScheduled = cloudError && progress?.message?.includes("自动重试");
const saving = phase === "pending" || phase === "saving" || phase === "uploading";
const reconciling = phase === "reconciling";
@@ -41,7 +42,9 @@ export function CanvasSyncStatus({ projectId, onLoadLatest, onOpenVersions }: {
const description = conflict
? progress?.message || "本地与云端存在冲突,当前编辑内容保持不变。"
: cloudError
? progress?.message || "本地内容仍然保留,云端暂未确认。"
? missingResources
? "检测到云端缺失素材;可以先保留原始草稿,再用本地版强制覆盖并移除失效素材引用。"
: progress?.message || "本地内容仍然保留,云端暂未确认。"
: localError
? progress?.localError || "本地保存没有完成,请立即重试;当前编辑内容保持不变。"
: reconciling
@@ -140,7 +143,13 @@ export function CanvasSyncStatus({ projectId, onLoadLatest, onOpenVersions }: {
) : cloudError ? (
<div className="canvas-sync-panel__callout">
<strong>{retryScheduled ? "本地内容仍在,正在等待重试" : "本地内容仍在,未覆盖云端"}</strong>
<span>{retryScheduled ? "网络恢复后会自动重试;也可以点击“立即重试”。" : "请处理上面的错误后点击“立即重试”;在此之前本地内容不会被覆盖。"}</span>
<span>
{missingResources
? "可点击“用本地版强制覆盖云端”;操作前会自动保留本地草稿,失效图片节点会保留布局和提示词。"
: retryScheduled
? "网络恢复后会自动重试;也可以点击“立即重试”。"
: "请处理上面的错误后点击“立即重试”;在此之前本地内容不会被覆盖。"}
</span>
</div>
) : localError ? (
<div className="canvas-sync-panel__callout">
@@ -150,19 +159,39 @@ export function CanvasSyncStatus({ projectId, onLoadLatest, onOpenVersions }: {
) : null}
<div className="canvas-sync-panel__actions">
{cloudError ? (
<Button
type="primary"
block
icon={<RefreshCw className="size-3.5" />}
loading={busy}
onClick={() =>
void run(() => retryRemoteUserDataSync(projectId))
.then(() => setStatusOpen(false))
.catch(() => undefined)
}
>
立即重试云端同步
</Button>
<>
{!missingResources ? (
<Button
type="primary"
block
icon={<RefreshCw className="size-3.5" />}
loading={busy}
onClick={() =>
void run(() => retryRemoteUserDataSync(projectId))
.then(() => setStatusOpen(false))
.catch(() => undefined)
}
>
立即重试云端同步
</Button>
) : null}
{missingResources ? (
<Button
type="primary"
danger
block
icon={<UploadCloud className="size-3.5" />}
loading={busy}
onClick={() =>
void run(() => overwriteRemoteCanvasProject(projectId))
.then(() => setStatusOpen(false))
.catch(() => undefined)
}
>
修复并强制覆盖云端
</Button>
) : null}
</>
) : localError ? (
<Button
type="primary"

View File

@@ -1774,7 +1774,9 @@ function CanvasViewportPage() {
try {
const content = node.metadata?.content?.trim();
const resourceId = resourceIdFromStorageKey(node.metadata?.storageKey);
const resourceId = [CanvasNodeType.Image, CanvasNodeType.Video, CanvasNodeType.Audio].includes(node.type as CanvasNodeType)
? resourceIdFromStorageKey(node.metadata?.storageKey)
: "";
// Resource-backed media must use the central access contract. This keeps
// copy operations on the configured CDN/OSS URL instead of copying the
// platform file endpoint or a stale URL persisted in canvas metadata.

View File

@@ -394,7 +394,7 @@ export function useCanvasProjectLifecycle({
try { await persistLocalEdits(); } catch { message.error("本地保存失败,请重试"); return false; }
try {
const result = await forceOverwriteRemoteCanvasSync(projectId);
message.success(result.reboundNodes > 0 ? `已保存,并修复 ${result.reboundNodes} 处媒体与素材的绑定` : "素材关联已核对,画布已保存");
message.success(result.clearedResources > 0 ? `已强制保存,移除了 ${result.clearedResources} 个失效素材引用;本地草稿已保留` : result.reboundNodes > 0 ? `已保存,并修复 ${result.reboundNodes} 处媒体与素材的绑定` : "素材关联已核对,画布已保存");
} catch (error) {
message.error(`修复并保存失败:${error instanceof Error ? error.message : "未知错误"}`);
return false;