improve canvas agent session handling
This commit is contained in:
parent
88f3a5453b
commit
9d2a1eab68
@ -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;
|
||||
}>({
|
||||
}>>({});
|
||||
const storyboardNodeMapRef = useRef<Record<string, Record<number, string>>>({});
|
||||
const getSendGuard = useCallback((chatSessionId: string) => {
|
||||
return (
|
||||
sendGuardRef.current[chatSessionId] || {
|
||||
inFlight: false,
|
||||
signature: "",
|
||||
timestamp: 0,
|
||||
});
|
||||
const storyboardNodeMapRef = useRef<Record<string, Record<number, string>>>({});
|
||||
const releaseSendGuard = useCallback(() => {
|
||||
sendGuardRef.current.inFlight = false;
|
||||
}
|
||||
);
|
||||
}, []);
|
||||
|
||||
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) => {
|
||||
@ -2944,6 +2994,18 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
||||
: 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);
|
||||
|
||||
1341
docs/Agent模式-低延迟响应升级规划-2026-03-08.md
Normal file
1341
docs/Agent模式-低延迟响应升级规划-2026-03-08.md
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user