fix: stabilize agent project startup

This commit is contained in:
Your Name 2026-05-18 17:50:33 +08:00
parent 57a1e25921
commit 33f25f4b21
4 changed files with 187 additions and 111 deletions

View File

@ -25,6 +25,30 @@ export const revalidate = 0;
const FAL_IMAGE_TASK_POLL_INTERVAL_MS = 2_000; const FAL_IMAGE_TASK_POLL_INTERVAL_MS = 2_000;
const falTaskLastPolledAt = new Map<string, number>(); const falTaskLastPolledAt = new Map<string, number>();
const falTaskSyncInFlight = new Set<string>(); const falTaskSyncInFlight = new Set<string>();
const AGENT_MODE_SCOPED_CHAT_SUFFIX = "a93e17a60001";
function isAgentModeScopedChatId(chatId: string) {
const normalized = String(chatId || "")
.trim()
.toLowerCase()
.replace(/-/g, "");
return (
/^[0-9a-f]{32}$/.test(normalized) &&
normalized.endsWith(AGENT_MODE_SCOPED_CHAT_SUFFIX)
);
}
function createEmptyMessagesResponse() {
return NextResponse.json(
{ messages: [] },
{
headers: {
"Cache-Control": "no-store, max-age=0",
},
}
);
}
async function markFalTaskFailed(params: { async function markFalTaskFailed(params: {
message: DBMessage; message: DBMessage;
@ -212,7 +236,15 @@ export async function GET(request: NextRequest) {
} }
const selectedChat = await getChatById({ id: chatId }); const selectedChat = await getChatById({ id: chatId });
if (!selectedChat || selectedChat.userId !== session.user.id) { if (!selectedChat) {
if (isAgentModeScopedChatId(chatId)) {
return createEmptyMessagesResponse();
}
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
}
if (selectedChat.userId !== session.user.id) {
return NextResponse.json({ error: "Forbidden" }, { status: 403 }); return NextResponse.json({ error: "Forbidden" }, { status: 403 });
} }

View File

@ -967,38 +967,6 @@ function normalizeImportedCanvasState(payload: unknown): {
}; };
} }
function hasMeaningfulWorkflowState(
workflowState: AgentWorkflowPersistedState | null | undefined
) {
if (!workflowState) return false;
const inputs = workflowState.inputs;
const hasInputs = Boolean(
String(inputs?.primarySku || "").trim() ||
String(inputs?.secondarySku || "").trim() ||
String(inputs?.brand || "").trim() ||
String(inputs?.store || "").trim() ||
String(inputs?.officialSecondCategory || "").trim() ||
String(inputs?.officialThirdCategory || "").trim() ||
String(inputs?.thirdCategory || "").trim() ||
String(inputs?.fourthCategory || "").trim() ||
String(inputs?.fifthCategory || "").trim() ||
String(inputs?.priceBand || "").trim()
);
const hasSelections = Object.values(workflowState.selections || {}).some(
(value) => Boolean(String(value || "").trim())
);
const hasBrandModel = Boolean(
String(workflowState.selectedBrandModel?.imageUrl || "").trim()
);
const hasPreset = Boolean(
String(workflowState.selectedAgentPresetId || "").trim()
);
return hasInputs || hasSelections || hasBrandModel || hasPreset;
}
const FLOATING_PANEL_WIDTH = 460; const FLOATING_PANEL_WIDTH = 460;
const FLOATING_PANEL_INSET = 16; const FLOATING_PANEL_INSET = 16;
const DOCKED_PANEL_INSET = 0; const DOCKED_PANEL_INSET = 0;
@ -1712,12 +1680,14 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
const hasClientPoint = const hasClientPoint =
typeof anchorPoint?.clientX === "number" && typeof anchorPoint?.clientX === "number" &&
typeof anchorPoint?.clientY === "number"; typeof anchorPoint?.clientY === "number";
const clientX = hasClientPoint ? anchorPoint.clientX : null;
const clientY = hasClientPoint ? anchorPoint.clientY : null;
anchorX = hasClientPoint anchorX = hasClientPoint
? Math.min(Math.max(anchorPoint.clientX - rect.left, 0), rect.width) ? Math.min(Math.max((clientX ?? 0) - rect.left, 0), rect.width)
: rect.width / 2; : rect.width / 2;
anchorY = hasClientPoint anchorY = hasClientPoint
? Math.min(Math.max(anchorPoint.clientY - rect.top, 0), rect.height) ? Math.min(Math.max((clientY ?? 0) - rect.top, 0), rect.height)
: rect.height / 2; : rect.height / 2;
} }
@ -2895,14 +2865,6 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
workflowMaterials, workflowMaterials,
sanitizedWorkflowSelections, sanitizedWorkflowSelections,
]); ]);
const currentWorkflowPersistedStateRef = useRef<AgentWorkflowPersistedState>(
buildCurrentWorkflowPersistedState()
);
useEffect(() => {
currentWorkflowPersistedStateRef.current =
buildCurrentWorkflowPersistedState();
}, [buildCurrentWorkflowPersistedState]);
useEffect(() => { useEffect(() => {
void checkCanvasAuthSession(); void checkCanvasAuthSession();
@ -2936,15 +2898,11 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
console.log("[Canvas] Loading project:", id); console.log("[Canvas] Loading project:", id);
const loadProject = async () => { const loadProject = async () => {
const preservedWorkflowState = currentWorkflowPersistedStateRef.current;
const shouldPreserveCurrentWorkflowState =
requestedAgentMode &&
lastLoadedCanvasIdRef.current === id &&
hasMeaningfulWorkflowState(preservedWorkflowState);
setIsLoading(true); setIsLoading(true);
setWorkflowHydrationSource("pending"); if (!requestedAgentMode) {
if (!shouldPreserveCurrentWorkflowState) { setWorkflowHydrationSource("pending");
}
if (!requestedAgentMode) {
setWorkflowInputs({ setWorkflowInputs({
primarySku: "", primarySku: "",
secondarySku: "", secondarySku: "",
@ -2962,13 +2920,15 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
setSelectedBrandModel(null); setSelectedBrandModel(null);
setSelectedAgentPresetId(null); setSelectedAgentPresetId(null);
} }
setAgentModeState(createEmptyAgentModeWorkspaceState()); if (!requestedAgentMode) {
setAgentWorkspaceState(createEmptyAgentGenerationProjectWorkspaceState()); setAgentModeState(createEmptyAgentModeWorkspaceState());
setAgentProjectTitle("未命名生图项目"); setAgentWorkspaceState(createEmptyAgentGenerationProjectWorkspaceState());
setAgentProjectThumbnail(null); setAgentProjectTitle("未命名生图项目");
setAgentProjectResultCollections({}); setAgentProjectThumbnail(null);
setAgentProjectDeletedResultCollections({}); setAgentProjectResultCollections({});
setLoadedAgentProjectId(null); setAgentProjectDeletedResultCollections({});
setLoadedAgentProjectId(null);
}
// 🆕 先清空当前画布状态,确保切换时不会显示旧数据 // 🆕 先清空当前画布状态,确保切换时不会显示旧数据
useCanvasStore.getState().clearCanvas(); useCanvasStore.getState().clearCanvas();
@ -3077,8 +3037,6 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
normalizeAgentModeWorkspaceState(persistedWorkflow.agentModeState) normalizeAgentModeWorkspaceState(persistedWorkflow.agentModeState)
); );
setWorkflowHydrationSource("project"); setWorkflowHydrationSource("project");
} else if (requestedAgentMode && shouldPreserveCurrentWorkflowState) {
setWorkflowHydrationSource("project");
} else { } else {
setWorkflowHydrationSource("empty"); setWorkflowHydrationSource("empty");
} }
@ -3995,32 +3953,42 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
}, [agentProjectId, agentProjectTitle, handleRenameAgentProject]); }, [agentProjectId, agentProjectTitle, handleRenameAgentProject]);
const handleCreateAgentProject = useCallback(async () => { const handleCreateAgentProject = useCallback(async () => {
if (agentProjectId) { if (isAgentProjectSaving) {
await persistAgentProject("switch");
}
const result = await agentProjectService.createProject({
title: buildDefaultAgentProjectTitle(),
projectState: createEmptyAgentGenerationProjectPersistedState(),
});
if (!result.success || !result.data) {
if (isUnauthorizedApiError(result.error)) {
setIsAuthenticated(false);
openLoginDialog(
result.error || "当前登录状态已失效,请重新登录后继续操作。"
);
}
toast.error(result.error || "创建生图项目失败");
return; return;
} }
toast.success("已新建空白项目"); setIsAgentProjectSaving(true);
updateCanvasSearchParams({ try {
mode: "agent", if (agentProjectId) {
agentProjectId: result.data.id, await persistAgentProject("switch");
}); }
const result = await agentProjectService.createProject({
title: buildDefaultAgentProjectTitle(),
projectState: createEmptyAgentGenerationProjectPersistedState(),
});
if (!result.success || !result.data) {
if (isUnauthorizedApiError(result.error)) {
setIsAuthenticated(false);
openLoginDialog(
result.error || "当前登录状态已失效,请重新登录后继续操作。"
);
}
toast.error(result.error || "创建生图项目失败");
return;
}
toast.success("已新建空白项目");
updateCanvasSearchParams({
mode: "agent",
agentProjectId: result.data.id,
});
} finally {
setIsAgentProjectSaving(false);
}
}, [ }, [
agentProjectId, agentProjectId,
isAgentProjectSaving,
openLoginDialog, openLoginDialog,
persistAgentProject, persistAgentProject,
updateCanvasSearchParams, updateCanvasSearchParams,
@ -9034,14 +9002,21 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
<button <button
className="rounded-[28px] border border-emerald-400/20 bg-emerald-500/[0.10] p-5 text-left transition-colors hover:border-emerald-300/35 hover:bg-emerald-500/[0.16]" className="rounded-[28px] border border-emerald-400/20 bg-emerald-500/[0.10] p-5 text-left transition-colors hover:border-emerald-300/35 hover:bg-emerald-500/[0.16]"
disabled={isAgentProjectSaving}
onClick={() => { onClick={() => {
void handleCreateAgentProject(); void handleCreateAgentProject();
}} }}
type="button" type="button"
> >
<div className="flex items-center gap-3 text-emerald-100"> <div className="flex items-center gap-3 text-emerald-100">
<Plus className="h-5 w-5" /> {isAgentProjectSaving ? (
<span className="font-medium text-sm">新建空白项目</span> <Loader2 className="h-5 w-5 animate-spin" />
) : (
<Plus className="h-5 w-5" />
)}
<span className="font-medium text-sm">
{isAgentProjectSaving ? "正在新建项目" : "新建空白项目"}
</span>
</div> </div>
<div className="mt-3 text-emerald-50/70 text-sm leading-6"> <div className="mt-3 text-emerald-50/70 text-sm leading-6">
仅在你要开始一条全新的场景生图流程时再创建,避免项目列表里自动冒出一堆无效项目。 仅在你要开始一条全新的场景生图流程时再创建,避免项目列表里自动冒出一堆无效项目。

View File

@ -1929,6 +1929,13 @@ export function sanitizeAgentWorkflowSelections(params: {
config?: AgentWorkflowUiConfig | null; config?: AgentWorkflowUiConfig | null;
selections: Partial<Record<AgentWorkflowStepKey, string>>; selections: Partial<Record<AgentWorkflowStepKey, string>>;
}) { }) {
const hasSelection = Object.values(params.selections || {}).some((value) =>
Boolean(String(value || "").trim())
);
if (!hasSelection) {
return {};
}
const config = params.config || defaultAgentWorkflowUiConfig; const config = params.config || defaultAgentWorkflowUiConfig;
const enabledSteps = flattenAgentWorkflowSteps( const enabledSteps = flattenAgentWorkflowSteps(
getEnabledAgentWorkflowStepTree(Array.isArray(config.steps) ? config.steps : []) getEnabledAgentWorkflowStepTree(Array.isArray(config.steps) ? config.steps : [])
@ -2089,6 +2096,13 @@ export function getAgentWorkflowBlockedOptionKeysForSelections(params: {
config?: AgentWorkflowUiConfig | null; config?: AgentWorkflowUiConfig | null;
selections: Partial<Record<AgentWorkflowStepKey, string>>; selections: Partial<Record<AgentWorkflowStepKey, string>>;
}) { }) {
const hasSelection = Object.values(params.selections || {}).some((value) =>
Boolean(String(value || "").trim())
);
if (!hasSelection) {
return [];
}
const config = params.config || defaultAgentWorkflowUiConfig; const config = params.config || defaultAgentWorkflowUiConfig;
const constraintSettings = getAgentWorkflowConstraintSettings(config); const constraintSettings = getAgentWorkflowConstraintSettings(config);
const visibleSteps = flattenVisibleAgentWorkflowSteps( const visibleSteps = flattenVisibleAgentWorkflowSteps(

View File

@ -9,22 +9,65 @@ interface ApiResponse<T = unknown> {
error?: string; error?: string;
} }
const AGENT_PROJECT_REQUEST_TIMEOUT_MS = 15_000;
async function fetchAgentProjectApi<T>(
input: RequestInfo | URL,
init?: RequestInit
): Promise<ApiResponse<T>> {
const controller = new AbortController();
const timeoutId = globalThis.setTimeout(
() => controller.abort(),
AGENT_PROJECT_REQUEST_TIMEOUT_MS
);
try {
const response = await fetch(input, {
...init,
signal: controller.signal,
});
return await response.json();
} finally {
globalThis.clearTimeout(timeoutId);
}
}
function getAgentProjectRequestErrorMessage(error: unknown, fallback: string) {
if (error instanceof DOMException && error.name === "AbortError") {
return `${fallback}:请求超时`;
}
return fallback;
}
export const agentProjectService = { export const agentProjectService = {
async getProjects(): Promise<ApiResponse<AgentGenerationProject[]>> { async getProjects(): Promise<ApiResponse<AgentGenerationProject[]>> {
try { try {
const response = await fetch("/api/agent-projects", { cache: "no-store" }); return await fetchAgentProjectApi<AgentGenerationProject[]>(
return await response.json(); "/api/agent-projects",
} catch { { cache: "no-store" }
return { success: false, error: "获取生图项目列表失败" }; );
} catch (error) {
return {
success: false,
error: getAgentProjectRequestErrorMessage(
error,
"获取生图项目列表失败"
),
};
} }
}, },
async getProject(id: string): Promise<ApiResponse<AgentGenerationProject>> { async getProject(id: string): Promise<ApiResponse<AgentGenerationProject>> {
try { try {
const response = await fetch(`/api/agent-projects/${id}`, { cache: "no-store" }); return await fetchAgentProjectApi<AgentGenerationProject>(
return await response.json(); `/api/agent-projects/${id}`,
} catch { { cache: "no-store" }
return { success: false, error: "获取生图项目失败" }; );
} catch (error) {
return {
success: false,
error: getAgentProjectRequestErrorMessage(error, "获取生图项目失败"),
};
} }
}, },
@ -35,14 +78,19 @@ export const agentProjectService = {
projectState?: AgentGenerationProjectPersistedState; projectState?: AgentGenerationProjectPersistedState;
}): Promise<ApiResponse<AgentGenerationProject>> { }): Promise<ApiResponse<AgentGenerationProject>> {
try { try {
const response = await fetch("/api/agent-projects", { return await fetchAgentProjectApi<AgentGenerationProject>(
method: "POST", "/api/agent-projects",
headers: { "Content-Type": "application/json" }, {
body: JSON.stringify(payload), method: "POST",
}); headers: { "Content-Type": "application/json" },
return await response.json(); body: JSON.stringify(payload),
} catch { }
return { success: false, error: "创建生图项目失败" }; );
} catch (error) {
return {
success: false,
error: getAgentProjectRequestErrorMessage(error, "创建生图项目失败"),
};
} }
}, },
@ -56,25 +104,32 @@ export const agentProjectService = {
} }
): Promise<ApiResponse<AgentGenerationProject>> { ): Promise<ApiResponse<AgentGenerationProject>> {
try { try {
const response = await fetch(`/api/agent-projects/${id}`, { return await fetchAgentProjectApi<AgentGenerationProject>(
method: "PUT", `/api/agent-projects/${id}`,
headers: { "Content-Type": "application/json" }, {
body: JSON.stringify(payload), method: "PUT",
}); headers: { "Content-Type": "application/json" },
return await response.json(); body: JSON.stringify(payload),
} catch { }
return { success: false, error: "保存生图项目失败" }; );
} catch (error) {
return {
success: false,
error: getAgentProjectRequestErrorMessage(error, "保存生图项目失败"),
};
} }
}, },
async deleteProject(id: string): Promise<ApiResponse> { async deleteProject(id: string): Promise<ApiResponse> {
try { try {
const response = await fetch(`/api/agent-projects/${id}`, { return await fetchAgentProjectApi(`/api/agent-projects/${id}`, {
method: "DELETE", method: "DELETE",
}); });
return await response.json(); } catch (error) {
} catch { return {
return { success: false, error: "删除生图项目失败" }; success: false,
error: getAgentProjectRequestErrorMessage(error, "删除生图项目失败"),
};
} }
}, },
}; };