fix: stabilize agent project startup
This commit is contained in:
parent
57a1e25921
commit
33f25f4b21
@ -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 });
|
||||
}
|
||||
|
||||
|
||||
@ -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">
|
||||
仅在你要开始一条全新的场景生图流程时再创建,避免项目列表里自动冒出一堆无效项目。
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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, "删除生图项目失败"),
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user