improve canvas agent session handling
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-09 00:40:35 +08:00
parent 88f3a5453b
commit 9d2a1eab68
2 changed files with 1496 additions and 53 deletions

View File

@ -1650,6 +1650,7 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
const hasInitializedChatStateRef = useRef(false);
const sessionMenuRef = useRef<HTMLDivElement>(null);
const historyScrollTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const forceHistoryHydrationRef = useRef<string | null>(null);
// 🆕 标签悬停预览状态
const [tagPreview, setTagPreview] = useState<TagPreviewInfo | null>(null);
@ -1680,34 +1681,83 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
unitCost: number;
imageCount: number;
} | null>(null);
const [streamImageGenerationState, setStreamImageGenerationState] =
useState<ImageGenerationStatusStreamPayload | null>(null);
const [streamImageGenerationStateByChatId, setStreamImageGenerationStateByChatId] =
useState<Record<string, ImageGenerationStatusStreamPayload | null>>({});
const streamImageGenerationState = useMemo(
() => streamImageGenerationStateByChatId[activeChatId] || null,
[activeChatId, streamImageGenerationStateByChatId]
);
const imageParamsRef = useRef(imageParams);
const videoParamsRef = useRef(videoParams);
const deepThinkingEnabledRef = useRef(deepThinkingEnabled);
const imageAutoTitleCacheRef = useRef<Map<string, string>>(new Map());
const sendGuardRef = useRef<{
const sendGuardRef = useRef<Record<string, {
inFlight: boolean;
signature: string;
timestamp: number;
}>({
inFlight: false,
signature: "",
timestamp: 0,
});
}>>({});
const storyboardNodeMapRef = useRef<Record<string, Record<number, string>>>({});
const releaseSendGuard = useCallback(() => {
sendGuardRef.current.inFlight = false;
const getSendGuard = useCallback((chatSessionId: string) => {
return (
sendGuardRef.current[chatSessionId] || {
inFlight: false,
signature: "",
timestamp: 0,
}
);
}, []);
const shouldBlockDuplicateSend = useCallback((signature: string) => {
const setSendGuard = useCallback(
(
chatSessionId: string,
next: {
inFlight: boolean;
signature: string;
timestamp: number;
}
) => {
sendGuardRef.current[chatSessionId] = next;
},
[]
);
const releaseSendGuard = useCallback(
(chatSessionId: string) => {
sendGuardRef.current[chatSessionId] = {
...getSendGuard(chatSessionId),
inFlight: false,
};
},
[getSendGuard]
);
const setStreamImageGenerationStateForChat = useCallback(
(chatSessionId: string, next: ImageGenerationStatusStreamPayload | null) => {
setStreamImageGenerationStateByChatId((prev) => ({
...prev,
[chatSessionId]: next,
}));
},
[]
);
const clearStreamImageGenerationStateForChat = useCallback((chatSessionId: string) => {
setStreamImageGenerationStateByChatId((prev) => {
if (!(chatSessionId in prev)) return prev;
const next = { ...prev };
delete next[chatSessionId];
return next;
});
}, []);
const shouldBlockDuplicateSend = useCallback((chatSessionId: string, signature: string) => {
const now = Date.now();
const previous = sendGuardRef.current;
const previous = getSendGuard(chatSessionId);
return (
previous.signature === signature &&
now - previous.timestamp < 4500
);
}, []);
}, [getSendGuard]);
const handleTypewriterActiveChange = useCallback((typingKey: string, active: boolean) => {
setActiveTypingSections((prev) => {
@ -2941,9 +2991,21 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
? "视频"
: activeComposerMode === "detail"
? "AI详情"
: activeComposerMode === "image"
? "生图"
: "Agent";
: activeComposerMode === "image"
? "生图"
: "Agent";
const shouldMaskAgentRuntimeModelName =
activeComposerMode === "agent" || selectedModelId === "auto";
const getAgentSafeModelName = useCallback(
(modelName?: string | null) => {
const normalized = String(modelName || "").trim();
if (!shouldMaskAgentRuntimeModelName) {
return normalized || undefined;
}
return normalized ? "agent model" : "agent model";
},
[shouldMaskAgentRuntimeModelName]
);
// 默认选择模型(根据系统设置)
useEffect(() => {
@ -3426,7 +3488,6 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
setMessages,
sendMessage,
status,
stop,
} = useChat<ChatMessage>({
id: activeChatId,
messages: mergedInitialMessages,
@ -3494,13 +3555,13 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
typeof dataPart.data === "string" ? JSON.parse(dataPart.data) : dataPart.data;
if (!payload || typeof payload !== "object") return;
if (payload.phase === "completed") {
setStreamImageGenerationState(null);
clearStreamImageGenerationStateForChat(activeChatId);
return;
}
setStreamImageGenerationState({
setStreamImageGenerationStateForChat(activeChatId, {
phase: payload.phase || "processing",
providerType: payload.providerType,
modelName: payload.modelName,
modelName: getAgentSafeModelName(payload.modelName),
imageCount: Number(payload.imageCount || 0) || undefined,
});
} catch (e) {
@ -3543,28 +3604,59 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
part.text.includes("<!-- IMAGE_GENERATION_END -->")
)
: false;
if (hasImageGenStartMarker) {
setStreamImageGenerationState((prev) => ({
phase: "processing",
providerType:
prev?.providerType ||
(activeComposerMode === "agent" ? "alibaba-image" : currentModel?.providerType),
modelName:
prev?.modelName ||
(activeComposerMode === "agent"
? "智能处理"
: currentModel?.displayName || undefined),
imageCount:
prev?.imageCount || Math.max(1, Number(imageParamsRef.current?.imageCount || 1)),
}));
}
if (hasImageGenEndMarker) {
setStreamImageGenerationState(null);
}
const normalizedParts = Array.isArray(messageData.parts) ? messageData.parts : [];
const normalizedAttachments = Array.isArray(messageData.attachments)
? messageData.attachments
: [];
const emptyNearestUserImageKeys = new Set<string>();
const incomingImageUrls = normalizedParts.flatMap((part: any) =>
part?.type === "text" && typeof part?.text === "string"
? extractAssistantImageUrls(part.text, emptyNearestUserImageKeys)
: typeof part?.url === "string" && /^https?:\/\//i.test(part.url)
? [part.url]
: []
);
const incomingVideoUrls = normalizedParts.flatMap((part: any) =>
part?.type === "text" && typeof part?.text === "string"
? extractVideoUrls(part.text)
: typeof part?.url === "string" &&
/^https?:\/\//i.test(part.url) &&
/\.(mp4|mov|webm|m4v)(?:[?#].*)?$/i.test(part.url)
? [part.url]
: []
);
normalizedAttachments.forEach((attachment: any) => {
const rawUrl = String(attachment?.url || "").trim();
if (!rawUrl || !/^https?:\/\//i.test(rawUrl)) return;
const contentType = String(attachment?.contentType || "").toLowerCase();
if (
contentType.startsWith("video/") ||
/\.(mp4|mov|webm|m4v)(?:[?#].*)?$/i.test(rawUrl)
) {
incomingVideoUrls.push(rawUrl);
return;
}
incomingImageUrls.push(rawUrl);
});
if (hasImageGenStartMarker) {
const previousState =
streamImageGenerationStateByChatId[activeChatId] || null;
setStreamImageGenerationStateForChat(activeChatId, {
phase: "processing",
providerType:
previousState?.providerType ||
(activeComposerMode === "agent" ? "alibaba-image" : currentModel?.providerType),
modelName:
previousState?.modelName ||
getAgentSafeModelName(currentModel?.displayName),
imageCount:
previousState?.imageCount ||
Math.max(1, Number(imageParamsRef.current?.imageCount || 1)),
});
}
if (hasImageGenEndMarker || incomingImageUrls.length > 0 || incomingVideoUrls.length > 0) {
clearStreamImageGenerationStateForChat(activeChatId);
}
setMessages((prevMessages) => {
const existingIndex = prevMessages.findIndex(
@ -3728,6 +3820,8 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
}
},
onError: (error) => {
clearStreamImageGenerationStateForChat(activeChatId);
releaseSendGuard(activeChatId);
const fallbackMessage =
error instanceof Error ? error.message : "生成失败";
const responseData = (error as any)?.data;
@ -3757,9 +3851,9 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
useEffect(() => {
if (status === "ready") {
releaseSendGuard();
releaseSendGuard(activeChatId);
}
}, [releaseSendGuard, status]);
}, [activeChatId, releaseSendGuard, status]);
useEffect(() => {
const launchPayload = pendingLaunchRef.current;
@ -3817,7 +3911,8 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
}
if (!historyData || !rawChatId) return;
if (status === "streaming" || status === "submitted") return;
const shouldForceHydrate = forceHistoryHydrationRef.current === rawChatId;
if ((status === "streaming" || status === "submitted") && !shouldForceHydrate) return;
const historyMessages = historyData.messages || [];
const lastMessageId = historyMessages.at(-1)?.id || "none";
@ -3828,6 +3923,7 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
loadedHistorySignatureRef.current = signature;
processedMessageIds.current = new Set();
hasHydratedHistoryRef.current = true;
forceHistoryHydrationRef.current = null;
setMessages(historyMessages);
setMessageViewportKey((prev) => prev + 1);
console.log("[CanvasChat] 已同步历史消息:", historyMessages.length, signature);
@ -6223,20 +6319,21 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
// 处理发送(图片已在上传时添加到画布,这里只发送消息)
const handleSend = useCallback(async () => {
if (sendGuardRef.current.inFlight || status === "streaming" || status === "submitted") {
const currentSendGuard = getSendGuard(activeChatId);
if (currentSendGuard.inFlight || status === "streaming" || status === "submitted") {
return;
}
if (isDetailGenerationMode && aiDetailGenerationEnabled) {
sendGuardRef.current = {
setSendGuard(activeChatId, {
inFlight: true,
signature: `detail:${activeChatId}:${Date.now()}`,
timestamp: Date.now(),
};
});
try {
await handleDetailGeneration();
} finally {
releaseSendGuard();
releaseSendGuard(activeChatId);
}
return;
}
@ -6389,17 +6486,17 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
mode: activeComposerMode,
});
if (shouldBlockDuplicateSend(sendSignature)) {
if (shouldBlockDuplicateSend(activeChatId, sendSignature)) {
toast.message("检测到重复发送,已自动忽略");
return;
}
sendGuardRef.current = {
setSendGuard(activeChatId, {
inFlight: true,
signature: sendSignature,
timestamp: Date.now(),
};
setStreamImageGenerationState(null);
});
clearStreamImageGenerationStateForChat(activeChatId);
// 使用与 multimodal-input 相同的格式发送消息
try {
@ -6408,7 +6505,7 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
parts: parts,
});
} catch (error) {
releaseSendGuard();
releaseSendGuard(activeChatId);
console.error("[CanvasChat] sendMessage failed:", error);
toast.error("发送失败,请重试");
return;
@ -6448,8 +6545,10 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
aiDetailGenerationEnabled,
handleDetailGeneration,
ensureImagePointsBeforeSend,
isVideoModel,
getSendGuard,
clearStreamImageGenerationStateForChat,
releaseSendGuard,
setSendGuard,
shouldBlockDuplicateSend,
status,
]);
@ -7001,20 +7100,23 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
updatedAt: now,
};
releaseSendGuard(activeChatId);
setChatSessions((prevSessions) => [newSession, ...prevSessions]);
setActiveChatId(newChatSessionId);
toast.success("已新建聊天窗口");
}, [chatSessions.length]);
}, [activeChatId, chatSessions.length, releaseSendGuard]);
const switchToSession = useCallback((sessionId: string) => {
if (!sessionId || sessionId === activeChatId) {
setIsSessionMenuOpen(false);
return;
}
forceHistoryHydrationRef.current = toRawCanvasChatId(sessionId);
releaseSendGuard(activeChatId);
setActiveChatId(sessionId);
setIsSessionMenuOpen(false);
toast.success("已切换聊天窗口");
}, [activeChatId]);
}, [activeChatId, releaseSendGuard]);
const activeSessionTitle = useMemo(() => {
const current = chatSessions.find((session) => session.id === activeChatId);

File diff suppressed because it is too large Load Diff