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 falTaskLastPolledAt = new Map<string, number>();
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: {
message: DBMessage;
@ -212,7 +236,15 @@ export async function GET(request: NextRequest) {
}
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 });
}

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_INSET = 16;
const DOCKED_PANEL_INSET = 0;
@ -1712,12 +1680,14 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
const hasClientPoint =
typeof anchorPoint?.clientX === "number" &&
typeof anchorPoint?.clientY === "number";
const clientX = hasClientPoint ? anchorPoint.clientX : null;
const clientY = hasClientPoint ? anchorPoint.clientY : null;
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;
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;
}
@ -2895,14 +2865,6 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
workflowMaterials,
sanitizedWorkflowSelections,
]);
const currentWorkflowPersistedStateRef = useRef<AgentWorkflowPersistedState>(
buildCurrentWorkflowPersistedState()
);
useEffect(() => {
currentWorkflowPersistedStateRef.current =
buildCurrentWorkflowPersistedState();
}, [buildCurrentWorkflowPersistedState]);
useEffect(() => {
void checkCanvasAuthSession();
@ -2936,15 +2898,11 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
console.log("[Canvas] Loading project:", id);
const loadProject = async () => {
const preservedWorkflowState = currentWorkflowPersistedStateRef.current;
const shouldPreserveCurrentWorkflowState =
requestedAgentMode &&
lastLoadedCanvasIdRef.current === id &&
hasMeaningfulWorkflowState(preservedWorkflowState);
setIsLoading(true);
setWorkflowHydrationSource("pending");
if (!shouldPreserveCurrentWorkflowState) {
if (!requestedAgentMode) {
setWorkflowHydrationSource("pending");
}
if (!requestedAgentMode) {
setWorkflowInputs({
primarySku: "",
secondarySku: "",
@ -2962,13 +2920,15 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
setSelectedBrandModel(null);
setSelectedAgentPresetId(null);
}
setAgentModeState(createEmptyAgentModeWorkspaceState());
setAgentWorkspaceState(createEmptyAgentGenerationProjectWorkspaceState());
setAgentProjectTitle("未命名生图项目");
setAgentProjectThumbnail(null);
setAgentProjectResultCollections({});
setAgentProjectDeletedResultCollections({});
setLoadedAgentProjectId(null);
if (!requestedAgentMode) {
setAgentModeState(createEmptyAgentModeWorkspaceState());
setAgentWorkspaceState(createEmptyAgentGenerationProjectWorkspaceState());
setAgentProjectTitle("未命名生图项目");
setAgentProjectThumbnail(null);
setAgentProjectResultCollections({});
setAgentProjectDeletedResultCollections({});
setLoadedAgentProjectId(null);
}
// 🆕 先清空当前画布状态,确保切换时不会显示旧数据
useCanvasStore.getState().clearCanvas();
@ -3077,8 +3037,6 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
normalizeAgentModeWorkspaceState(persistedWorkflow.agentModeState)
);
setWorkflowHydrationSource("project");
} else if (requestedAgentMode && shouldPreserveCurrentWorkflowState) {
setWorkflowHydrationSource("project");
} else {
setWorkflowHydrationSource("empty");
}
@ -3995,32 +3953,42 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
}, [agentProjectId, agentProjectTitle, handleRenameAgentProject]);
const handleCreateAgentProject = useCallback(async () => {
if (agentProjectId) {
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 || "创建生图项目失败");
if (isAgentProjectSaving) {
return;
}
toast.success("已新建空白项目");
updateCanvasSearchParams({
mode: "agent",
agentProjectId: result.data.id,
});
setIsAgentProjectSaving(true);
try {
if (agentProjectId) {
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,
isAgentProjectSaving,
openLoginDialog,
persistAgentProject,
updateCanvasSearchParams,
@ -9034,14 +9002,21 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
<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]"
disabled={isAgentProjectSaving}
onClick={() => {
void handleCreateAgentProject();
}}
type="button"
>
<div className="flex items-center gap-3 text-emerald-100">
<Plus className="h-5 w-5" />
<span className="font-medium text-sm">新建空白项目</span>
{isAgentProjectSaving ? (
<Loader2 className="h-5 w-5 animate-spin" />
) : (
<Plus className="h-5 w-5" />
)}
<span className="font-medium text-sm">
{isAgentProjectSaving ? "正在新建项目" : "新建空白项目"}
</span>
</div>
<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;
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 enabledSteps = flattenAgentWorkflowSteps(
getEnabledAgentWorkflowStepTree(Array.isArray(config.steps) ? config.steps : [])
@ -2089,6 +2096,13 @@ export function getAgentWorkflowBlockedOptionKeysForSelections(params: {
config?: AgentWorkflowUiConfig | null;
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 constraintSettings = getAgentWorkflowConstraintSettings(config);
const visibleSteps = flattenVisibleAgentWorkflowSteps(

View File

@ -9,22 +9,65 @@ interface ApiResponse<T = unknown> {
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 = {
async getProjects(): Promise<ApiResponse<AgentGenerationProject[]>> {
try {
const response = await fetch("/api/agent-projects", { cache: "no-store" });
return await response.json();
} catch {
return { success: false, error: "获取生图项目列表失败" };
return await fetchAgentProjectApi<AgentGenerationProject[]>(
"/api/agent-projects",
{ cache: "no-store" }
);
} catch (error) {
return {
success: false,
error: getAgentProjectRequestErrorMessage(
error,
"获取生图项目列表失败"
),
};
}
},
async getProject(id: string): Promise<ApiResponse<AgentGenerationProject>> {
try {
const response = await fetch(`/api/agent-projects/${id}`, { cache: "no-store" });
return await response.json();
} catch {
return { success: false, error: "获取生图项目失败" };
return await fetchAgentProjectApi<AgentGenerationProject>(
`/api/agent-projects/${id}`,
{ cache: "no-store" }
);
} catch (error) {
return {
success: false,
error: getAgentProjectRequestErrorMessage(error, "获取生图项目失败"),
};
}
},
@ -35,14 +78,19 @@ export const agentProjectService = {
projectState?: AgentGenerationProjectPersistedState;
}): Promise<ApiResponse<AgentGenerationProject>> {
try {
const response = await fetch("/api/agent-projects", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
return await response.json();
} catch {
return { success: false, error: "创建生图项目失败" };
return await fetchAgentProjectApi<AgentGenerationProject>(
"/api/agent-projects",
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
}
);
} catch (error) {
return {
success: false,
error: getAgentProjectRequestErrorMessage(error, "创建生图项目失败"),
};
}
},
@ -56,25 +104,32 @@ export const agentProjectService = {
}
): Promise<ApiResponse<AgentGenerationProject>> {
try {
const response = await fetch(`/api/agent-projects/${id}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
return await response.json();
} catch {
return { success: false, error: "保存生图项目失败" };
return await fetchAgentProjectApi<AgentGenerationProject>(
`/api/agent-projects/${id}`,
{
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
}
);
} catch (error) {
return {
success: false,
error: getAgentProjectRequestErrorMessage(error, "保存生图项目失败"),
};
}
},
async deleteProject(id: string): Promise<ApiResponse> {
try {
const response = await fetch(`/api/agent-projects/${id}`, {
return await fetchAgentProjectApi(`/api/agent-projects/${id}`, {
method: "DELETE",
});
return await response.json();
} catch {
return { success: false, error: "删除生图项目失败" };
} catch (error) {
return {
success: false,
error: getAgentProjectRequestErrorMessage(error, "删除生图项目失败"),
};
}
},
};