fix(claw): prevent new chat history rebound
This commit is contained in:
parent
a7d994d93b
commit
dcff190c62
@ -275,8 +275,11 @@ import {
|
||||
MAX_CHAT_AUTO_CONTEXT_LENGTH,
|
||||
MAX_CHAT_HIDDEN_PROMPT_LENGTH,
|
||||
MAX_CHAT_TEXT_PART_LENGTH,
|
||||
normalizeCanvasChatSessionId,
|
||||
normalizeChatAttachmentName,
|
||||
normalizeChatMediaType,
|
||||
replaceCanvasChatSessionHistory,
|
||||
resolveCanvasChatSessionNavigationTarget,
|
||||
type CanvasChatSessionRecord,
|
||||
type ClawdChatHistoryRecord,
|
||||
type PickedObjectAttachment,
|
||||
@ -3055,15 +3058,24 @@ type CanvasChatSession = Omit<CanvasChatSessionRecord, "mode"> & {
|
||||
|
||||
type ClawdHistoryItem = ClawdChatHistoryRecord;
|
||||
|
||||
const CANVAS_CHAT_QUERY_PARAM = "chat";
|
||||
const CANVAS_IMAGE_NODE_MANUAL_UPLOAD_EVENT = "canvas:image-node-manual-upload";
|
||||
const CANVAS_CHAT_QUERY_PARAM = "chat";
|
||||
|
||||
function normalizeCanvasChatSessionId(value?: string | null) {
|
||||
const raw = String(value || "").trim();
|
||||
if (!raw) return null;
|
||||
type CanvasHistoryMessagesResponse = {
|
||||
chatId: string;
|
||||
messages: ChatMessage[];
|
||||
};
|
||||
|
||||
const rawUuid = raw.startsWith("canvas-") ? raw.slice("canvas-".length) : raw;
|
||||
return isValidChatUuid(rawUuid) ? `canvas-${rawUuid}` : null;
|
||||
async function fetchCanvasHistoryMessages(
|
||||
url: string
|
||||
): Promise<CanvasHistoryMessagesResponse> {
|
||||
const payload = (await fetcher(url)) as { messages?: ChatMessage[] };
|
||||
const requestUrl = new URL(url, "http://canvas.local");
|
||||
|
||||
return {
|
||||
chatId: String(requestUrl.searchParams.get("chatId") || ""),
|
||||
messages: Array.isArray(payload?.messages) ? payload.messages : [],
|
||||
};
|
||||
}
|
||||
|
||||
type CopiedUserMessagePayload = {
|
||||
@ -3365,6 +3377,8 @@ export function CanvasChatPanel({
|
||||
const pendingLaunchRef = useRef<CanvasLaunchPayload | null>(null);
|
||||
const [launchAutoSendTick, setLaunchAutoSendTick] = useState(0);
|
||||
const hasInitializedChatStateRef = useRef(false);
|
||||
const initializedChatSessionStorageKeyRef = useRef<string | null>(null);
|
||||
const chatSessionRootIdRef = useRef(chatId);
|
||||
const [loadedChatSessionStorageKey, setLoadedChatSessionStorageKey] = useState<string | null>(
|
||||
null
|
||||
);
|
||||
@ -3392,12 +3406,12 @@ export function CanvasChatPanel({
|
||||
|
||||
const replaceChatSessionQuery = useCallback(
|
||||
(sessionId: string) => {
|
||||
const normalized = normalizeCanvasChatSessionId(sessionId);
|
||||
if (!normalized || typeof window === "undefined") return;
|
||||
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set(CANVAS_CHAT_QUERY_PARAM, normalized);
|
||||
window.history.replaceState(window.history.state, "", url);
|
||||
if (typeof window === "undefined") return;
|
||||
replaceCanvasChatSessionHistory({
|
||||
currentUrl: window.location.href,
|
||||
history: window.history,
|
||||
sessionId,
|
||||
});
|
||||
},
|
||||
[]
|
||||
);
|
||||
@ -3736,6 +3750,7 @@ export function CanvasChatPanel({
|
||||
forceHistoryHydrationRef.current = null;
|
||||
releaseSendGuard(activeChatId);
|
||||
updateChatSessions((prevSessions) => [newSession, ...prevSessions]);
|
||||
activeChatIdRef.current = newChatSessionId;
|
||||
setActiveChatId(newChatSessionId);
|
||||
replaceChatSessionQuery(newChatSessionId);
|
||||
toast.success(options?.toastMessage || "已新建聊天窗口");
|
||||
@ -4169,6 +4184,11 @@ export function CanvasChatPanel({
|
||||
useEffect(() => {
|
||||
const now = Date.now();
|
||||
let rootChatId = chatId;
|
||||
const sessionStorageKey = getSessionStorageKey(canvasId, chatScope);
|
||||
|
||||
if (initializedChatSessionStorageKeyRef.current === sessionStorageKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (chatScope === "agent") {
|
||||
try {
|
||||
@ -4186,6 +4206,11 @@ export function CanvasChatPanel({
|
||||
}
|
||||
}
|
||||
|
||||
chatSessionRootIdRef.current = rootChatId;
|
||||
const initialRequestedChatSessionId = normalizeCanvasChatSessionId(
|
||||
new URL(window.location.href).searchParams.get(CANVAS_CHAT_QUERY_PARAM)
|
||||
);
|
||||
|
||||
const fallbackSession: CanvasChatSession = {
|
||||
id: rootChatId,
|
||||
title: "对话 1",
|
||||
@ -4194,17 +4219,17 @@ export function CanvasChatPanel({
|
||||
updatedAt: now,
|
||||
};
|
||||
const requestedSession: CanvasChatSession | null =
|
||||
requestedChatSessionId && requestedChatSessionId !== rootChatId
|
||||
initialRequestedChatSessionId &&
|
||||
initialRequestedChatSessionId !== rootChatId
|
||||
? {
|
||||
id: requestedChatSessionId,
|
||||
id: initialRequestedChatSessionId,
|
||||
title: "新对话",
|
||||
mode: "image",
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
}
|
||||
: null;
|
||||
const initialChatId = requestedChatSessionId || rootChatId;
|
||||
const sessionStorageKey = getSessionStorageKey(canvasId, chatScope);
|
||||
const initialChatId = initialRequestedChatSessionId || rootChatId;
|
||||
|
||||
try {
|
||||
const saved = localStorage.getItem(sessionStorageKey);
|
||||
@ -4248,6 +4273,7 @@ export function CanvasChatPanel({
|
||||
|
||||
const sorted = [...nextSessions].sort((a, b) => b.updatedAt - a.updatedAt);
|
||||
setChatSessions(sorted);
|
||||
activeChatIdRef.current = initialChatId;
|
||||
setActiveChatId(initialChatId);
|
||||
localStorage.setItem(sessionStorageKey, JSON.stringify(sorted));
|
||||
setLoadedChatSessionStorageKey(sessionStorageKey);
|
||||
@ -4256,10 +4282,52 @@ export function CanvasChatPanel({
|
||||
setChatSessions(
|
||||
requestedSession ? [requestedSession, fallbackSession] : [fallbackSession]
|
||||
);
|
||||
activeChatIdRef.current = initialChatId;
|
||||
setActiveChatId(initialChatId);
|
||||
setLoadedChatSessionStorageKey(sessionStorageKey);
|
||||
} finally {
|
||||
initializedChatSessionStorageKeyRef.current = sessionStorageKey;
|
||||
}
|
||||
}, [canvasId, chatId, chatScope, requestedChatSessionId]);
|
||||
}, [canvasId, chatId, chatScope]);
|
||||
|
||||
useEffect(() => {
|
||||
const sessionStorageKey = getSessionStorageKey(canvasId, chatScope);
|
||||
if (initializedChatSessionStorageKeyRef.current !== sessionStorageKey) return;
|
||||
|
||||
const browserRequestedChatSessionId = normalizeCanvasChatSessionId(
|
||||
new URL(window.location.href).searchParams.get(CANVAS_CHAT_QUERY_PARAM)
|
||||
);
|
||||
const targetChatId = resolveCanvasChatSessionNavigationTarget({
|
||||
activeSessionId: activeChatIdRef.current,
|
||||
browserSessionId: browserRequestedChatSessionId,
|
||||
rootSessionId: chatSessionRootIdRef.current,
|
||||
routerSessionId: requestedChatSessionId,
|
||||
});
|
||||
if (!targetChatId) return;
|
||||
|
||||
const now = Date.now();
|
||||
updateChatSessions((previousSessions) => {
|
||||
if (previousSessions.some((session) => session.id === targetChatId)) {
|
||||
return previousSessions;
|
||||
}
|
||||
return [
|
||||
{
|
||||
id: targetChatId,
|
||||
title: "新对话",
|
||||
mode: "image",
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
},
|
||||
...previousSessions,
|
||||
];
|
||||
});
|
||||
|
||||
forceHistoryHydrationRef.current = requestedChatSessionId
|
||||
? toRawCanvasChatId(targetChatId)
|
||||
: null;
|
||||
activeChatIdRef.current = targetChatId;
|
||||
setActiveChatId(targetChatId);
|
||||
}, [canvasId, chatScope, requestedChatSessionId, updateChatSessions]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isSessionMenuOpen) return;
|
||||
@ -4417,13 +4485,18 @@ export function CanvasChatPanel({
|
||||
}, []);
|
||||
|
||||
// 从 API 加载历史消息
|
||||
const { data: historyData, mutate: mutateHistoryData } = useSWR<{
|
||||
messages: ChatMessage[];
|
||||
}>(historyMessagesSWRKey, fetcher, {
|
||||
revalidateOnFocus: false,
|
||||
revalidateOnReconnect: false,
|
||||
dedupingInterval: 10000,
|
||||
});
|
||||
const { data: fetchedHistoryData, mutate: mutateHistoryData } =
|
||||
useSWR<CanvasHistoryMessagesResponse>(
|
||||
historyMessagesSWRKey,
|
||||
fetchCanvasHistoryMessages,
|
||||
{
|
||||
revalidateOnFocus: false,
|
||||
revalidateOnReconnect: false,
|
||||
dedupingInterval: 10000,
|
||||
}
|
||||
);
|
||||
const historyData =
|
||||
fetchedHistoryData?.chatId === rawChatId ? fetchedHistoryData : undefined;
|
||||
const initialCanvasMessageCount =
|
||||
activeChatId === chatId ? initialMessages.length : 0;
|
||||
const voteSWRKey =
|
||||
@ -15316,6 +15389,7 @@ export function CanvasChatPanel({
|
||||
}
|
||||
forceHistoryHydrationRef.current = toRawCanvasChatId(sessionId);
|
||||
releaseSendGuard(activeChatId);
|
||||
activeChatIdRef.current = sessionId;
|
||||
setActiveChatId(sessionId);
|
||||
replaceChatSessionQuery(sessionId);
|
||||
setIsSessionMenuOpen(false);
|
||||
@ -15365,6 +15439,7 @@ export function CanvasChatPanel({
|
||||
|
||||
forceHistoryHydrationRef.current = toRawCanvasChatId(sessionId);
|
||||
releaseSendGuard(activeChatId);
|
||||
activeChatIdRef.current = sessionId;
|
||||
setActiveChatId(sessionId);
|
||||
setComposerModeOverride("clawd");
|
||||
setIsDetailGenerationMode(false);
|
||||
|
||||
@ -9,6 +9,7 @@ export const MAX_CHAT_ATTACHMENT_NAME_LENGTH = 100;
|
||||
|
||||
const CHAT_UUID_REGEX =
|
||||
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
||||
const CANVAS_CHAT_QUERY_PARAM = "chat";
|
||||
|
||||
const ALLOWED_CHAT_MEDIA_TYPES = new Set([
|
||||
"image/jpeg",
|
||||
@ -70,6 +71,42 @@ export function isValidChatUuid(value?: string | null): value is string {
|
||||
return Boolean(value && CHAT_UUID_REGEX.test(String(value).trim()));
|
||||
}
|
||||
|
||||
export function normalizeCanvasChatSessionId(value?: string | null) {
|
||||
const raw = String(value || "").trim();
|
||||
if (!raw) return null;
|
||||
|
||||
const rawUuid = raw.startsWith("canvas-") ? raw.slice("canvas-".length) : raw;
|
||||
return isValidChatUuid(rawUuid) ? `canvas-${rawUuid}` : null;
|
||||
}
|
||||
|
||||
export function replaceCanvasChatSessionHistory(params: {
|
||||
currentUrl: string;
|
||||
history: Pick<History, "replaceState">;
|
||||
sessionId: string;
|
||||
}) {
|
||||
const normalized = normalizeCanvasChatSessionId(params.sessionId);
|
||||
if (!normalized) return null;
|
||||
|
||||
const url = new URL(params.currentUrl);
|
||||
url.searchParams.set(CANVAS_CHAT_QUERY_PARAM, normalized);
|
||||
|
||||
// Reusing Next.js' existing history state bypasses its URL synchronization.
|
||||
params.history.replaceState(null, "", url);
|
||||
return normalized;
|
||||
}
|
||||
|
||||
export function resolveCanvasChatSessionNavigationTarget(params: {
|
||||
activeSessionId: string;
|
||||
browserSessionId: string | null;
|
||||
rootSessionId: string;
|
||||
routerSessionId: string | null;
|
||||
}) {
|
||||
if (params.browserSessionId !== params.routerSessionId) return null;
|
||||
|
||||
const targetSessionId = params.routerSessionId || params.rootSessionId;
|
||||
return targetSessionId !== params.activeSessionId ? targetSessionId : null;
|
||||
}
|
||||
|
||||
export function isValidChatHttpUrl(value?: string | null): value is string {
|
||||
if (!value) return false;
|
||||
try {
|
||||
|
||||
@ -237,7 +237,10 @@ import {
|
||||
} from "@/components/canvas/chat-panel/canvas-upload-helpers";
|
||||
import {
|
||||
mergeClawdHistoryIntoCanvasSessions,
|
||||
normalizeCanvasChatSessionId,
|
||||
normalizeChatMediaType,
|
||||
replaceCanvasChatSessionHistory,
|
||||
resolveCanvasChatSessionNavigationTarget,
|
||||
sanitizeChatAttachment,
|
||||
type PickedObjectAttachment,
|
||||
} from "@/components/canvas/chat-panel/canvas-chat-session";
|
||||
@ -258,6 +261,55 @@ function dbTextMessage(
|
||||
}
|
||||
|
||||
test.describe("canvas chat pure helpers", () => {
|
||||
test("updates the chat query without reusing Next.js internal history state", () => {
|
||||
const calls: Array<{ data: unknown; url: string | URL | null }> = [];
|
||||
const sessionId = "canvas-44444444-4444-4444-8444-444444444444";
|
||||
const history = {
|
||||
replaceState(data: unknown, _unused: string, url?: string | URL | null) {
|
||||
calls.push({ data, url: url ?? null });
|
||||
},
|
||||
} as Pick<History, "replaceState">;
|
||||
|
||||
expect(
|
||||
replaceCanvasChatSessionHistory({
|
||||
currentUrl: "https://ai.yydd.ai/canvas/project-id?mode=agent",
|
||||
history,
|
||||
sessionId,
|
||||
})
|
||||
).toBe(sessionId);
|
||||
expect(calls).toHaveLength(1);
|
||||
expect(calls[0]?.data).toBeNull();
|
||||
|
||||
const nextUrl = new URL(String(calls[0]?.url));
|
||||
expect(nextUrl.searchParams.get("mode")).toBe("agent");
|
||||
expect(nextUrl.searchParams.get("chat")).toBe(sessionId);
|
||||
expect(normalizeCanvasChatSessionId(nextUrl.searchParams.get("chat"))).toBe(
|
||||
sessionId
|
||||
);
|
||||
});
|
||||
|
||||
test("ignores stale router chat state until it matches the browser URL", () => {
|
||||
const oldSessionId = "canvas-55555555-5555-4555-8555-555555555555";
|
||||
const newSessionId = "canvas-66666666-6666-4666-8666-666666666666";
|
||||
|
||||
expect(
|
||||
resolveCanvasChatSessionNavigationTarget({
|
||||
activeSessionId: newSessionId,
|
||||
browserSessionId: newSessionId,
|
||||
rootSessionId: oldSessionId,
|
||||
routerSessionId: oldSessionId,
|
||||
})
|
||||
).toBeNull();
|
||||
expect(
|
||||
resolveCanvasChatSessionNavigationTarget({
|
||||
activeSessionId: newSessionId,
|
||||
browserSessionId: oldSessionId,
|
||||
rootSessionId: oldSessionId,
|
||||
routerSessionId: oldSessionId,
|
||||
})
|
||||
).toBe(oldSessionId);
|
||||
});
|
||||
|
||||
test("rebuilds Claw session history from persisted database records", () => {
|
||||
const localSession = {
|
||||
id: "canvas-11111111-1111-4111-8111-111111111111",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user