fix agent project state sync and workflow prompt freshness
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled

This commit is contained in:
Your Name 2026-03-25 23:02:16 +08:00
parent 785177bed4
commit 2e87ab91d3
3 changed files with 117 additions and 30 deletions

View File

@ -103,6 +103,7 @@ import {
} from "@/lib/agents/mode-workspace-state";
import {
createEmptyAgentGenerationProjectPersistedState,
createEmptyAgentWorkflowPersistedState,
createEmptyAgentGenerationProjectWorkspaceState,
normalizeAgentGenerationProjectPersistedState,
} from "@/lib/agents/generation-project-state";
@ -840,7 +841,7 @@ export default function CanvasEditorPage({ params }: PageProps) {
try {
const result = await agentProjectService.createProject({
title: buildDefaultAgentProjectTitle(),
projectState: persistedAgentProjectStateRef.current,
projectState: createEmptyAgentGenerationProjectPersistedState(),
});
if (!result.success || !result.data) {
throw new Error(result.error || "创建生图项目失败");
@ -889,12 +890,8 @@ export default function CanvasEditorPage({ params }: PageProps) {
const persistedState = normalizeAgentGenerationProjectPersistedState(
result.data.projectState
);
const currentWorkflowState = currentWorkflowPersistedStateRef.current;
const persistedWorkflow = hasMeaningfulWorkflowState(persistedState.workflowState)
? persistedState.workflowState
: hasMeaningfulWorkflowState(currentWorkflowState)
? currentWorkflowState
: persistedState.workflowState;
const persistedWorkflow =
persistedState.workflowState || createEmptyAgentWorkflowPersistedState();
setWorkflowInputs({
primarySku: String(persistedWorkflow.inputs?.primarySku || "").trim(),
@ -1883,7 +1880,7 @@ export default function CanvasEditorPage({ params }: PageProps) {
return (
<AgentGenerationWorkspaceDarkV2
chatId={agentModeChatId}
projectId={id}
projectId={agentProjectId || id}
selectedAgentPresetId={isAgentWorkspaceEnabled ? selectedAgentPresetId : null}
selectedBrandModel={selectedBrandModel}
workflowContext={workflowContext}

View File

@ -894,6 +894,11 @@ export function AgentGenerationWorkspaceDarkV2({
const lastSyncedDetailModelSlotKeyRef = useRef<string | null>(null);
const detailGenerationCancelRef = useRef(false);
const workspaceStateSignatureRef = useRef<string>("");
const generationRequestSnapshotRef = useRef<{
hiddenPrompt: string | null;
}>({
hiddenPrompt: null,
});
const [localAgentModeState, setLocalAgentModeState] = useState<AgentModeWorkspaceState>(
() => createEmptyAgentModeWorkspaceState()
);
@ -1379,7 +1384,10 @@ export function AgentGenerationWorkspaceDarkV2({
body: {
id: rawChatId,
message: request.messages.at(-1),
hiddenPrompt: hiddenPromptRef.current || undefined,
hiddenPrompt:
generationRequestSnapshotRef.current.hiddenPrompt ||
hiddenPromptRef.current ||
undefined,
agentPresetId:
transportState.selectedAgentPresetId || selectedAgentPresetId || undefined,
selectedChatModel:
@ -1439,6 +1447,25 @@ export function AgentGenerationWorkspaceDarkV2({
]);
// 初始化附件
useEffect(() => {
workspaceStateSignatureRef.current = "";
lastSyncedProductSlotKeysRef.current = [];
lastSyncedBrandModelUrlRef.current = null;
lastSyncedDetailProductSlotKeysRef.current = [];
lastSyncedDetailBundleSlotKeysRef.current = [];
lastSyncedDetailModelSlotKeyRef.current = null;
setInput("");
setAttachments([]);
setSelectedResultUrl(null);
setFullscreenPreview(null);
setPendingGeneration(null);
setImageParams({
aspectRatio: "auto",
imageSize: "2K",
imageCount: 1,
});
}, [projectId]);
useEffect(() => {
if (!hasAgentUploadSlots) {
setAttachments((current) => (current.length === 0 ? current : []));
@ -1471,20 +1498,12 @@ export function AgentGenerationWorkspaceDarkV2({
(_, index) => current[index] || null
);
for (let index = 0; index < productSlotCount; index += 1) {
const currentSlot = result[index];
const nextAttachment = nextSlots[index];
const currentSlotKey = getAttachmentIdentityKey(currentSlot);
const previousSyncedKey = lastSyncedProductSlotKeysRef.current[index] || null;
const currentIsPreviousAutoSelection =
Boolean(previousSyncedKey) && currentSlotKey === previousSyncedKey;
const currentIsBrandModel =
currentSlotKey ? brandModelAttachmentKeys.has(currentSlotKey) : false;
if (nextAttachment) {
if (!currentSlot || currentIsPreviousAutoSelection || currentIsBrandModel) {
result[index] = nextAttachment as PickedAttachment;
}
} else if (currentIsPreviousAutoSelection || currentIsBrandModel) {
result[index] = nextAttachment as PickedAttachment;
} else if (previousSyncedKey) {
result[index] = null;
}
}
@ -1528,11 +1547,7 @@ export function AgentGenerationWorkspaceDarkV2({
const currentSlot = modelSlotIndex >= 0 ? slots[modelSlotIndex] ?? null : null;
const previousSyncedUrl = lastSyncedBrandModelUrlRef.current;
if (
modelSlotIndex >= 0 &&
nextAttachment &&
(!currentSlot || currentSlot.url === previousSyncedUrl)
) {
if (modelSlotIndex >= 0 && nextAttachment) {
slots[modelSlotIndex] = nextAttachment;
} else if (modelSlotIndex >= 0 && previousSyncedUrl && currentSlot?.url === previousSyncedUrl) {
slots[modelSlotIndex] = null;
@ -2457,9 +2472,13 @@ export function AgentGenerationWorkspaceDarkV2({
toast.error("请至少补充要求或上传图片");
return;
}
hiddenPromptRef.current = workflowContext?.promptText
const nextHiddenPrompt = workflowContext?.promptText
? `【工作流设定】\n${workflowContext.promptText}\n\n请严格遵循以上工作流设定输出内容,并保持整体结果一致。`
: null;
hiddenPromptRef.current = nextHiddenPrompt;
generationRequestSnapshotRef.current = {
hiddenPrompt: nextHiddenPrompt,
};
setActiveResultModeType(currentResultModeType);
setSelectedResultUrl(null);
setPendingGeneration({
@ -2478,6 +2497,9 @@ export function AgentGenerationWorkspaceDarkV2({
sendMessage({ role: "user", parts });
setInput("");
} catch (error) {
generationRequestSnapshotRef.current = {
hiddenPrompt: null,
};
setPendingGeneration(null);
toast.error(error instanceof Error ? error.message : "发送生成请求失败");
}

View File

@ -417,6 +417,11 @@ export function AgentWorkflowPanelDarkV2({
] as const);
return new Map(entries);
}, [resolvedConfig.steps, selections]);
const [autoAdvanceNotice, setAutoAdvanceNotice] = useState<{
completedStepTitle: string;
completedSummary: string;
nextStepTitle: string;
} | null>(null);
const [activeEmbeddedStepKey, setActiveEmbeddedStepKey] = useState<string>("");
const [activeEmbeddedOptionDepth, setActiveEmbeddedOptionDepth] = useState(1);
const [pendingAutoAdvanceStepKey, setPendingAutoAdvanceStepKey] = useState<string | null>(null);
@ -519,6 +524,12 @@ export function AgentWorkflowPanelDarkV2({
);
if (nextStep && nextStep.step.key !== activeEmbeddedStepKey) {
setAutoAdvanceNotice({
completedStepTitle: activeEmbeddedStepEntry.step.title,
completedSummary:
stepSelectionSummaryMap[activeEmbeddedStepEntry.step.key] || "已完成当前步骤选择",
nextStepTitle: nextStep.step.title,
});
setActiveEmbeddedStepKey(nextStep.step.key);
setActiveEmbeddedOptionDepth(1);
}
@ -532,8 +543,21 @@ export function AgentWorkflowPanelDarkV2({
flattenedResolvedSteps,
pendingAutoAdvanceStepKey,
selections,
stepSelectionSummaryMap,
]);
useEffect(() => {
if (!autoAdvanceNotice) {
return;
}
const timer = window.setTimeout(() => {
setAutoAdvanceNotice(null);
}, 3200);
return () => window.clearTimeout(timer);
}, [autoAdvanceNotice]);
const renderOptionLevels = (
step: AgentWorkflowUiConfig["steps"][number],
options: AgentWorkflowUiConfig["steps"][number]["options"],
@ -700,6 +724,7 @@ export function AgentWorkflowPanelDarkV2({
type="button"
onClick={() => {
setPendingAutoAdvanceStepKey(null);
setAutoAdvanceNotice(null);
setActiveEmbeddedStepKey(step.key);
}}
className="group relative flex min-w-0 flex-col items-start pr-2 text-left transition-transform duration-200 hover:-translate-y-[1px]"
@ -717,11 +742,11 @@ export function AgentWorkflowPanelDarkV2({
>
{stepIndex + 1}
</span>
<span
className={cn(
"truncate text-[13px] font-semibold transition-colors",
active
? "text-white"
<span
className={cn(
"truncate text-[13px] font-semibold transition-colors",
active
? "text-white"
: completed
? "text-white/82"
: "text-white/52 group-hover:text-white/76"
@ -729,6 +754,11 @@ export function AgentWorkflowPanelDarkV2({
>
{step.title}
</span>
{active ? (
<span className="rounded-full border border-emerald-300/30 bg-emerald-400/10 px-2 py-0.5 text-[10px] font-medium text-emerald-200">
当前步骤
</span>
) : null}
</div>
<span
className={cn(
@ -773,6 +803,44 @@ export function AgentWorkflowPanelDarkV2({
{activeEmbeddedOptionLevel ? (
<div className="space-y-4">
{autoAdvanceNotice ? (
<motion.div
key={`${autoAdvanceNotice.completedStepTitle}-${autoAdvanceNotice.nextStepTitle}`}
initial={{ opacity: 0, y: -10, scale: 0.98 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0 }}
className="rounded-2xl border border-emerald-300/16 bg-[linear-gradient(135deg,rgba(16,185,129,0.12),rgba(6,10,12,0.86))] px-4 py-3 shadow-[0_14px_40px_rgba(0,0,0,0.28)]"
>
<div className="flex flex-wrap items-center gap-2 text-sm font-semibold text-white">
<span className="flex h-7 w-7 items-center justify-center rounded-full bg-emerald-300 text-black">
<Check className="h-4 w-4" />
</span>
<span>已完成「{autoAdvanceNotice.completedStepTitle}」</span>
<ArrowRight className="h-4 w-4 text-emerald-200/80" />
<span className="text-emerald-200">已自动切换到「{autoAdvanceNotice.nextStepTitle}」</span>
</div>
<div className="mt-1 pl-9 text-xs leading-5 text-emerald-50/72">
刚刚完成的内容:{autoAdvanceNotice.completedSummary}
</div>
</motion.div>
) : null}
<div className="rounded-2xl border border-emerald-300/12 bg-emerald-400/[0.05] px-4 py-3">
<div className="flex flex-wrap items-center gap-2">
<span className="rounded-full border border-emerald-300/24 bg-emerald-400/10 px-2.5 py-1 text-[11px] font-medium text-emerald-200">
当前正在选择
</span>
<span className="text-sm font-semibold text-white">
{activeEmbeddedStepEntry.step.title}
</span>
</div>
<div className="mt-1 text-xs leading-5 text-emerald-50/68">
{activeEmbeddedOptionLevel.depth === 1
? "当前区域展示的是这个步骤的主卡片。选中后系统会继续切到该步骤的下一级,或自动进入下一个步骤。"
: `当前区域展示的是「${activeEmbeddedStepEntry.step.title}」的第 ${activeEmbeddedOptionLevel.depth} 级子卡片,不是上一个已完成步骤的卡片。`}
</div>
</div>
<div className="flex items-center gap-2">
<span className="rounded-full border border-white/12 px-2.5 py-1 text-[11px] font-medium text-white/45">
{activeEmbeddedOptionLevel.depth === 1