fix(canvas): 画布与素材同步 - 修复批量缩略图、远程资源回退与重命名同步

This commit is contained in:
ddCat committed 2026-09-18 23:27:34 +08:00
1 parent 1a79005d25
commit 262540ea36
19 files changed
+635 -513

No files matched your search

+16 -2
View File
@@ -123,6 +123,8 @@ export function useCanvasBatchTable({ nodesRef, connectionsRef, setNodes, setCon
navigateToSettings({ continueCreation: true });
return;
}
const generationConfig = buildGenerationConfig(effectiveConfig, undefined, "image");
const imageResolution = /^(1k|2k|4k)$/i.test(generationConfig.quality) ? generationConfig.quality.toUpperCase() : "由尺寸决定";
const activeNodeIds = new Set((sourceNode.metadata?.generationBatches || []).filter((batch) => batch.mode === "batch_image").flatMap((batch) => batch.items.filter((item) => ["waiting", "submitting", "queued", "running"].includes(item.status)).map((item) => item.nodeId)));
const requested = requestedRowIds?.length ? new Set(requestedRowIds) : null;
const rows = table.rows.filter((row) => {
@@ -139,7 +141,15 @@ export function useCanvasBatchTable({ nodesRef, connectionsRef, setNodes, setCon
if (!rows.length) return message.info("没有可提交的未完成任务,请检查参考图和提示词");
const confirmed = await new Promise<boolean>((resolve) => modal.confirm({
title: `确认提交 ${rows.length} 个批量图片任务`,
content: `模型:${modelDisplayName(effectiveConfig, imageModel)};并发上限:${table.concurrency}。这些任务可能消耗积分或产生外部模型费用。`,
content: [
`模型:${modelDisplayName(effectiveConfig, generationConfig.model) || generationConfig.model}`,
`出图数量:${rows.length} 张(每行 1 张)`,
`尺寸:${generationConfig.size || "默认"}`,
`质量:${generationConfig.quality || "默认"}`,
`分辨率:${imageResolution}`,
`并发:${table.concurrency}`,
"这些任务可能消耗积分或产生外部模型费用。",
].join("\n"),
okText: "确认生成",
cancelText: "取消",
centered: true,
@@ -160,7 +170,11 @@ export function useCanvasBatchTable({ nodesRef, connectionsRef, setNodes, setCon
...(existingIndex >= 0 ? resetGenerationTaskMetadata(nextNodes[existingIndex].metadata) : {}),
prompt,
composerContent: prompt,
model: buildGenerationConfig(effectiveConfig, undefined, "image").model,
model: generationConfig.model,
size: generationConfig.size,
quality: generationConfig.quality,
transparentBackground: generationConfig.transparentBackground,
count: 1,
generationMode: "image" as const,
generationType: "edit" as const,
workflowKind: "final" as const,
@@ -309,7 +309,11 @@ export function useCanvasProjectLifecycle({
const renameCurrentProject = useCallback((title: string) => {
renameProject(projectId, title);
}, [projectId, renameProject]);
// 标题是画布列表和分享入口的元数据,重命名后立即提交,避免只停留在浏览器缓存。
void saveRemoteUserDataNow(projectId).catch((error) => {
message.warning(error instanceof Error ? `名称已更新到本地,云端同步将在后台重试:${error.message}` : "名称已更新到本地,云端同步将在后台重试");
});
}, [message, projectId, renameProject]);
const persistLocalEdits = useCallback(async () => {
const snapshot = { nodes: nodesRef.current, connections: connectionsRef.current, chatSessions, activeChatId, canvasAppearance, backgroundMode, showImageInfo };
@@ -15,12 +15,12 @@ import { useDebouncedValue } from "@/hooks/use-debounced-value";
import { ModelPicker } from "@/components/model-picker";
import { CreditSymbol, requestCreditCost } from "@/constant/credits";
import { modelCapabilityConfigFor, normalizeImageValue, normalizeVideoValue, videoDurationOptions } from "@/lib/model-capabilities";
import { modelQuoteRequest } from "@/lib/model-pricing";
import { modelQuoteDescription, modelQuoteRequest } from "@/lib/model-pricing";
import { customShotTitle, formatShotOrdinal, normalizeDefaultShotTitle } from "@/lib/shot-label";
import { modelCompatibilityError, resolveCompatibleModel, resolveModelVideoBooleanOptions, type ModelRequirements } from "@/lib/model-selection";
import { formatVideoResolutionLabel } from "@/lib/video-generation-options";
import { submitBackendGenerationTask } from "@/services/api/generation-task";
import { quoteLogicalModel } from "@/services/api/logical-models";
import { quoteModel, type LogicalModelQuote } from "@/services/api/logical-models";
import { type GenerationTask } from "@/services/api/task-center";
import {
createUnitWorkflow,
@@ -182,8 +182,8 @@ export default function WorkflowProductionWorkbench(props: Props) {
});
const quoteRequest = useMemo(() => modelQuoteRequest(generationConfig, routedModel, generationCapability, modelRequirements), [generationCapability, generationConfig, modelRequirements, routedModel]);
const quoteRequestKey = JSON.stringify(quoteRequest || null);
const [quotedCredits, setQuotedCredits] = useState<number | null>(null);
const generationCredits = quotedCredits ?? configuredCredits;
const [routeQuote, setRouteQuote] = useState<LogicalModelQuote | null>(null);
const generationCredits = routeQuote ? routeQuote.amountMicrocredits / 1_000_000 : configuredCredits;
const formattedGenerationCredits = generationCredits?.toLocaleString("zh-CN", { maximumFractionDigits: 6 });
const modelSummary = routedModel ? modelDisplayName(effectiveConfig, routedModel) : "未选择模型";
const durationSummary = `${Number(watchedDuration || Math.max(0.5, (selectedShot?.durationMs || 3000) / 1000))}s`;
@@ -212,15 +212,15 @@ export default function WorkflowProductionWorkbench(props: Props) {
useEffect(() => {
if (!creditsEnabled || !quoteRequest) {
setQuotedCredits(null);
setRouteQuote(null);
return;
}
const controller = new AbortController();
setQuotedCredits(null);
quoteLogicalModel(quoteRequest.logicalModelID, quoteRequest.intent, controller.signal)
.then(({ quote }) => setQuotedCredits(quote.amountMicrocredits / 1_000_000))
setRouteQuote(null);
quoteModel(quoteRequest, controller.signal)
.then(({ quote }) => setRouteQuote(quote))
.catch(() => {
if (!controller.signal.aborted) setQuotedCredits(null);
if (!controller.signal.aborted) setRouteQuote(null);
});
return () => controller.abort();
// quoteRequestKey captures the normalized request without retriggering on object identity.
@@ -545,7 +545,7 @@ export default function WorkflowProductionWorkbench(props: Props) {
</div>
<footer className="workflow-editor-actions">
<div className="workflow-generation-cost" aria-live="polite">
{creditsEnabled && formattedGenerationCredits ? <><CreditSymbol /><span>本次预计 {formattedGenerationCredits} 积分</span></> : creditsEnabled && routedModel ? <span>本次费用将在提交时按实际规格计算</span> : null}
{creditsEnabled && formattedGenerationCredits ? <><CreditSymbol /><span title={routeQuote ? modelQuoteDescription(routeQuote) : undefined}>本次预计 {formattedGenerationCredits} 积分</span></> : creditsEnabled && routedModel ? <span>本次费用将在提交时按实际规格计算</span> : null}
</div>
<div className="flex items-center gap-2"><Button danger icon={<Trash2 className="size-4" />} loading={deleteShot.isPending} disabled={saveShot.isPending || selectedShotSubmitting || changeAssetBinding.isPending} onClick={requestDeleteShot}>删除镜头</Button><Button htmlType="submit" icon={<Save className="size-4" />} loading={saveShot.isPending} disabled={!editorDirty || deleteShot.isPending}>保存脚本</Button><Button type="primary" icon={<Play className="size-4" />} loading={selectedShotSubmitting || shotTask?.status === "queued" || shotTask?.status === "running"} disabled={deleteShot.isPending} onClick={() => void generateArtifact()}>{selectedShotSubmitting ? `${stageCopy.action}(正在提交)` : shotTask?.status === "queued" || shotTask?.status === "running" ? `${stageCopy.action}(已运行${shotTaskElapsed})` : shotTask?.status === "failed" ? `${stageCopy.action}(上次失败,可重试)` : shotTask?.status === "succeeded" && !newestArtifact ? `${stageCopy.action}(已完成,正在同步)` : newestArtifact ? `${stageCopy.action}(已生成)` : stageCopy.action}</Button></div>
</footer>
-1
View File
@@ -184,7 +184,6 @@ export default function ProjectsPage() {
const hasInitialError = query.isError && !query.data;
return (
<WorkspacePage className="library-page project-library-page" grid>
<PageHeader title="短剧 Agent" description="你的故事、章节与镜头,都在这里。" meta={<span className="app-projects-header-meta">{totalProjectCount} 个项目</span>} />
<details className="story-launcher-panel" aria-label="开始一部新短剧">
<summary className="story-launcher-head">
<div className="story-launcher-title">