fix(*): 质量检查 - 修复预演保存、增量同步与缺失资源恢复
This commit is contained in:
@@ -8,13 +8,7 @@ import { resolveCompatibleModel, type ModelRequirements } from "@/lib/model-sele
|
||||
import type { AgentMediaSettings } from "@/services/api/agent";
|
||||
import { PUBLIC_MODEL_CATALOG_ID, useEffectiveConfig } from "@/stores/use-config-store";
|
||||
|
||||
export function CanvasAgentVideoApprovalSettings({ initial, value, theme, disabled, onChange }: {
|
||||
initial: AgentVideoApproval;
|
||||
value?: AgentMediaSettings;
|
||||
theme: CanvasTheme;
|
||||
disabled: boolean;
|
||||
onChange: (value: AgentMediaSettings) => void;
|
||||
}) {
|
||||
export function CanvasAgentVideoApprovalSettings({ initial, value, theme, disabled, onChange }: { initial: AgentVideoApproval; value?: AgentMediaSettings; theme: CanvasTheme; disabled: boolean; onChange: (value: AgentMediaSettings) => void }) {
|
||||
const effective = useEffectiveConfig();
|
||||
const config = useMemo(() => ({ ...effective, channels: effective.channels.filter((channel) => channel.scope === "system" || channel.id === PUBLIC_MODEL_CATALOG_ID) }), [effective]);
|
||||
const settings = value || initial;
|
||||
@@ -42,7 +36,15 @@ export function CanvasAgentVideoApprovalSettings({ initial, value, theme, disabl
|
||||
const profile = modelCapabilityConfigFor(config, next).video;
|
||||
if (!profile) return;
|
||||
const normalized = normalizeVideoValue(profile, { seconds: duration, ratio: settings.size, resolution: settings.quality });
|
||||
onChange({ ...agentApprovalModelSelection(config, next), size: normalized.ratio, quality: normalized.resolution, durationSeconds: Number(normalized.seconds), videoGenerateAudio: settings.videoGenerateAudio, videoWatermark: settings.videoWatermark, videoArkPrivateAssetUpload: settings.videoArkPrivateAssetUpload });
|
||||
onChange({
|
||||
...agentApprovalModelSelection(config, next),
|
||||
size: normalized.ratio,
|
||||
quality: normalized.resolution,
|
||||
durationSeconds: Number(normalized.seconds),
|
||||
videoGenerateAudio: settings.videoGenerateAudio,
|
||||
videoWatermark: settings.videoWatermark,
|
||||
videoArkPrivateAssetUpload: settings.videoArkPrivateAssetUpload,
|
||||
});
|
||||
};
|
||||
|
||||
const changeOption = (key: "vquality" | "size" | "videoSeconds" | "videoGenerateAudio" | "videoWatermark" | "videoArkPrivateAssetUpload", next: string) => {
|
||||
@@ -75,12 +77,32 @@ export function CanvasAgentVideoApprovalSettings({ initial, value, theme, disabl
|
||||
return (
|
||||
<fieldset disabled={disabled} className="min-w-0 space-y-3 border-0 p-0" data-canvas-no-zoom data-canvas-wheel-scroll aria-label="视频生成设置">
|
||||
<div className="space-y-1.5">
|
||||
<div className="text-xs" style={{ color: theme.node.muted }}>生成模型</div>
|
||||
<div className="text-xs" style={{ color: theme.node.muted }}>
|
||||
生成模型
|
||||
</div>
|
||||
<ModelPicker config={config} capability="video" value={model} onChange={changeModel} requirements={requirements} variant="creation" fullWidth showOptionPrices placeholder="选择生成模型" popoverClassName="agent-model-picker-popover" />
|
||||
</div>
|
||||
{model ? <VideoSettingsPanel config={{ ...config, model, size: settings.size, videoSeconds: duration, vquality: settings.quality, videoGenerateAudio: audio, videoWatermark: watermark, videoArkPrivateAssetUpload: privateAssetUpload }} onConfigChange={changeOption} theme={theme} showTitle={false} className="min-w-0 space-y-3" /> : <p className="text-xs" style={{ color: theme.node.muted }}>当前模型不在可选目录中,可重新选择;提交时将重新校验模型与规格。</p>}
|
||||
<p className="text-xs" style={{ color: theme.node.text }}>本次规格:{settings.size}{settings.quality ? ` · ${settings.quality}` : ""}{settings.durationSeconds ? ` · ${settings.durationSeconds} 秒` : ""}</p>
|
||||
<p className="text-xs" style={{ color: theme.node.muted }}>修改仅用于本次生成,费用按最终模型和规格计算。</p>
|
||||
{model ? (
|
||||
<VideoSettingsPanel
|
||||
config={{ ...config, model, size: settings.size, videoSeconds: duration, vquality: settings.quality, videoGenerateAudio: audio, videoWatermark: watermark, videoArkPrivateAssetUpload: privateAssetUpload }}
|
||||
onConfigChange={changeOption}
|
||||
theme={theme}
|
||||
showTitle={false}
|
||||
className="min-w-0 space-y-3"
|
||||
/>
|
||||
) : (
|
||||
<p className="text-xs" style={{ color: theme.node.muted }}>
|
||||
当前模型不在可选目录中,可重新选择;提交时将重新校验模型与规格。
|
||||
</p>
|
||||
)}
|
||||
<p className="text-xs" style={{ color: theme.node.text }}>
|
||||
本次规格:{settings.size}
|
||||
{settings.quality ? ` · ${settings.quality}` : ""}
|
||||
{settings.durationSeconds ? ` · ${settings.durationSeconds} 秒` : ""}
|
||||
</p>
|
||||
<p className="text-xs" style={{ color: theme.node.muted }}>
|
||||
修改仅用于本次生成,费用按最终模型和规格计算。
|
||||
</p>
|
||||
</fieldset>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -571,7 +571,11 @@ export function ApprovalCard({
|
||||
无法确认具体目标,继续前请重新读取画布。
|
||||
</div>
|
||||
)}
|
||||
{imageApproval ? <CanvasAgentImageApprovalSettings initial={imageApproval} value={mediaSettings} onChange={setMediaSettings} theme={theme} disabled={submitting} /> : videoApproval ? <CanvasAgentVideoApprovalSettings initial={videoApproval} value={mediaSettings} onChange={setMediaSettings} theme={theme} disabled={submitting} /> : null}
|
||||
{imageApproval ? (
|
||||
<CanvasAgentImageApprovalSettings initial={imageApproval} value={mediaSettings} onChange={setMediaSettings} theme={theme} disabled={submitting} />
|
||||
) : videoApproval ? (
|
||||
<CanvasAgentVideoApprovalSettings initial={videoApproval} value={mediaSettings} onChange={setMediaSettings} theme={theme} disabled={submitting} />
|
||||
) : null}
|
||||
<button type="button" className="canvas-agent-approval-reason-toggle" aria-expanded={showReason} onClick={() => setShowReason((value) => !value)} disabled={submitting}>
|
||||
{showReason ? "收起拒绝理由" : "填写拒绝理由(可选)"}
|
||||
</button>
|
||||
|
||||
@@ -79,7 +79,7 @@ export function CanvasPrevisWorkbench({
|
||||
onDeleteImageNode: (nodeId: string) => void;
|
||||
onFlush?: () => void | Promise<void>;
|
||||
}) {
|
||||
const { message } = App.useApp();
|
||||
const { message, modal } = App.useApp();
|
||||
const theme = canvasThemes[useActiveTheme()];
|
||||
const effectiveCanvasId = useMemo(() => {
|
||||
const explicit = canvasId?.trim();
|
||||
@@ -241,6 +241,7 @@ export function CanvasPrevisWorkbench({
|
||||
resetWorkbench();
|
||||
}, [open, resetWorkbench, scene, writeDraft]);
|
||||
|
||||
// 打开会话时检查合法本地恢复候选
|
||||
// 父级项目收到 Agent 的预演台语义补丁后,scene.id 不变但 updatedAt 会变化;
|
||||
// 不能只按 scene.id 忽略,否则打开着的工作台永远看不到 Agent 的修改。
|
||||
useEffect(() => {
|
||||
@@ -257,7 +258,7 @@ export function CanvasPrevisWorkbench({
|
||||
setSelectedBone(null);
|
||||
setPlayhead(0);
|
||||
message.info("Agent 已更新预演场景");
|
||||
}, [message, open, scene, setPlayhead, setSelectedBone, setSelectedLightId, setSelectedObjectId, writeDraft]);
|
||||
}, [message, modal, open, scene, writeDraft]);
|
||||
|
||||
const activeShot = draft?.shots?.find((item) => item.id === draft.activeShotId) || draft?.shots?.[0] || null;
|
||||
const activeCamera = draft?.cameras?.find((item) => item.id === activeShot?.cameraId) || draft?.cameras?.[0] || null;
|
||||
@@ -355,6 +356,7 @@ export function CanvasPrevisWorkbench({
|
||||
[writeAndPublish],
|
||||
);
|
||||
|
||||
// 打开会话时检查合法本地恢复候选之后,挂载暂存手势生命周期。
|
||||
// 暂存手势的终止生命周期:常驻安装,非活跃时 end 为空操作。
|
||||
useEffect(
|
||||
() =>
|
||||
@@ -466,7 +468,14 @@ export function CanvasPrevisWorkbench({
|
||||
return;
|
||||
}
|
||||
recordPrevisDiagnostic("PREVIS_CLOSE_BLOCKED", { sceneId: draftRef.current?.id, saveOutcome: "stay", revision: saveController.progress.revision });
|
||||
message.error(decision.message);
|
||||
modal.confirm({
|
||||
title: "保存未完成",
|
||||
content: decision.message,
|
||||
okText: "放弃修改并关闭",
|
||||
cancelText: "留在预演台",
|
||||
okButtonProps: { danger: true },
|
||||
onOk: onClose,
|
||||
});
|
||||
closingRef.current = false;
|
||||
})();
|
||||
};
|
||||
@@ -1246,6 +1255,12 @@ export function CanvasPrevisWorkbench({
|
||||
<span className={`previs-desk-save ${saveStatusClass}`} aria-live="polite">
|
||||
{saveIndicator.label}
|
||||
</span>
|
||||
{saveController.progress.status === "error" ? (
|
||||
<button type="button" className="previs-desk-header-action" onClick={() => void saveController.retrySave()}>
|
||||
<Save className="size-3.5" />
|
||||
重试保存
|
||||
</button>
|
||||
) : null}
|
||||
{onboardingScope ? (
|
||||
<button type="button" className="previs-desk-icon-button" aria-label="重新开始引导" title="重新开始引导" onClick={() => setOnboardingRestartSignal((value) => value + 1)}>
|
||||
<Lightbulb className="size-4" />
|
||||
|
||||
@@ -8,6 +8,7 @@ import type { PrevisScene } from "@/types/previs";
|
||||
export type PrevisSaveController = {
|
||||
progress: PrevisSaveProgress;
|
||||
commitScene: (scene: PrevisScene) => void;
|
||||
retrySave: () => Promise<boolean>;
|
||||
prepareClose: () => Promise<PrevisCloseDecision>;
|
||||
handlePageHide: () => Promise<boolean>;
|
||||
};
|
||||
@@ -59,7 +60,10 @@ export function usePrevisSaveCoordinator(input: { sceneId: string | null; initia
|
||||
coordinatorRef.current = null;
|
||||
const pending = coordinator.getSnapshot();
|
||||
if (pending.confirmedRevision < pending.revision) {
|
||||
void coordinator.flushLatest().catch(() => undefined).finally(() => coordinator.dispose());
|
||||
void coordinator
|
||||
.flushLatest()
|
||||
.catch(() => undefined)
|
||||
.finally(() => coordinator.dispose());
|
||||
return;
|
||||
}
|
||||
coordinator.dispose();
|
||||
@@ -79,6 +83,14 @@ export function usePrevisSaveCoordinator(input: { sceneId: string | null; initia
|
||||
[syncProgress],
|
||||
);
|
||||
|
||||
const retrySave = useCallback(async () => {
|
||||
const coordinator = coordinatorRef.current;
|
||||
if (!coordinator) return false;
|
||||
const success = await coordinator.flushLatest();
|
||||
syncProgress();
|
||||
return success;
|
||||
}, [syncProgress]);
|
||||
|
||||
const prepareClose = useCallback(async () => {
|
||||
const coordinator = coordinatorRef.current;
|
||||
if (!coordinator) return "close" as PrevisCloseDecision;
|
||||
@@ -107,5 +119,5 @@ export function usePrevisSaveCoordinator(input: { sceneId: string | null; initia
|
||||
}
|
||||
}, [syncProgress]);
|
||||
|
||||
return { progress, commitScene, prepareClose, handlePageHide };
|
||||
return { progress, commitScene, retrySave, prepareClose, handlePageHide };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user