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

This commit is contained in:
ddCat
2026-09-18 23:27:34 +08:00
parent 531019b368
commit b99ad798b9
19 changed files with 635 additions and 513 deletions

View File

@@ -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>

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">