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