fix: persist Claw composer clear guard

This commit is contained in:
134 2026-06-24 21:44:22 +08:00
parent 754209ee53
commit 4de04234d0

View File

@ -333,6 +333,9 @@ const CLAWD_TABLE_MAX_FILES = 3;
const CLAWD_TABLE_MAX_BYTES = 10 * 1024 * 1024;
const CLAWD_PROCESSING_HINT =
"已收到,这个任务稍微繁重一些,处理时间可能会长一点,请耐心等候。";
const CLAWD_SUBMITTED_COMPOSER_RECOVERY_TTL_MS = 30 * 60 * 1000;
const CLAWD_SUBMITTED_COMPOSER_STORAGE_KEY =
"ai-chatbot:clawd-submitted-composer";
const CLAWD_SIMPLE_PROMPT_PATTERNS = [
/^(hi|hello|hey|你好|您好|哈喽|嗨|在吗|早上好|下午好|晚上好)[\s!.。!??~~]*$/i,
/^(你是谁|你叫什么|你叫什么名字|你能做什么|你可以做什么|有什么功能|介绍一下自己|帮我介绍一下你)[\s!.。!??~~]*$/,
@ -359,6 +362,85 @@ function normalizeClawdComposerText(value: string) {
.trim();
}
type ClawdComposerSubmissionSnapshot = {
chatId: string;
expiresAt: number;
startedAt: number;
submittedTexts: string[];
};
function isClawdSubmittedComposerText(
snapshot: ClawdComposerSubmissionSnapshot,
value: string
) {
const normalizedValue = normalizeClawdComposerText(value);
if (!normalizedValue) return false;
return snapshot.submittedTexts.some(
(text) => normalizeClawdComposerText(text) === normalizedValue
);
}
function parseClawdComposerSubmissionSnapshot(
value: unknown
): ClawdComposerSubmissionSnapshot | null {
if (!value || typeof value !== "object") return null;
const snapshot = value as Partial<ClawdComposerSubmissionSnapshot>;
const chatId = String(snapshot.chatId || "").trim();
const startedAt = Number(snapshot.startedAt);
const expiresAt = Number(snapshot.expiresAt);
const submittedTexts = Array.isArray(snapshot.submittedTexts)
? snapshot.submittedTexts
.map((text) => normalizeClawdComposerText(String(text || "")))
.filter(Boolean)
: [];
if (!chatId || !Number.isFinite(startedAt) || !Number.isFinite(expiresAt)) {
return null;
}
if (submittedTexts.length === 0) {
return null;
}
return {
chatId,
expiresAt,
startedAt,
submittedTexts,
};
}
function readStoredClawdComposerSubmissionSnapshot() {
if (typeof window === "undefined") return null;
try {
const raw = window.sessionStorage.getItem(
CLAWD_SUBMITTED_COMPOSER_STORAGE_KEY
);
if (!raw) return null;
return parseClawdComposerSubmissionSnapshot(JSON.parse(raw));
} catch {
return null;
}
}
function writeStoredClawdComposerSubmissionSnapshot(
snapshot: ClawdComposerSubmissionSnapshot | null
) {
if (typeof window === "undefined") return;
try {
if (!snapshot) {
window.sessionStorage.removeItem(CLAWD_SUBMITTED_COMPOSER_STORAGE_KEY);
return;
}
window.sessionStorage.setItem(
CLAWD_SUBMITTED_COMPOSER_STORAGE_KEY,
JSON.stringify(snapshot)
);
} catch {
// Ignore session storage failures; the in-memory guard still handles normal tab switches.
}
}
function shouldShowClawdProcessingHint(
message: string,
hasTableFiles: boolean,
@ -1650,6 +1732,7 @@ export function CanvasChatPanel({
[searchParams]
);
const [input, setInput] = useState("");
const inputRef = useRef(input);
const [attachments, setAttachments] = useState<PickedObjectAttachment[]>([]);
const [selectedModelId, setSelectedModelId] = useState<string>(
effectiveAgentEnabled ? "auto" : ""
@ -1762,11 +1845,11 @@ export function CanvasChatPanel({
const historyScrollTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const forceHistoryHydrationRef = useRef<string | null>(null);
const copiedUserMessageRef = useRef<CopiedUserMessagePayload | null>(null);
const pendingClawdSubmissionRef = useRef<{
chatId: string;
submittedTexts: string[];
startedAt: number;
} | null>(null);
const composerUserEditedAtRef = useRef(0);
const pendingClawdSubmissionRef =
useRef<ClawdComposerSubmissionSnapshot | null>(null);
const recentClawdSubmissionRef =
useRef<ClawdComposerSubmissionSnapshot | null>(null);
const replaceChatSessionQuery = useCallback(
(sessionId: string) => {
@ -2104,6 +2187,10 @@ export function CanvasChatPanel({
setSelectedSkillTagId(null);
}, [activeChatId]);
useEffect(() => {
inputRef.current = input;
}, [input]);
const clearComposerEditor = useCallback(() => {
setInput("");
const editableDiv = textareaRef.current as HTMLDivElement | null;
@ -2112,37 +2199,68 @@ export function CanvasChatPanel({
}
}, []);
const clearRestoredClawdComposerText = useCallback(() => {
const getActiveClawdSubmissionSnapshot = useCallback(() => {
const now = Date.now();
const pendingSubmission = pendingClawdSubmissionRef.current;
if (pendingSubmission) {
return pendingSubmission;
}
const recentSubmission =
recentClawdSubmissionRef.current ||
readStoredClawdComposerSubmissionSnapshot();
if (!recentSubmission) {
return null;
}
recentClawdSubmissionRef.current = recentSubmission;
if (recentSubmission.expiresAt <= now) {
recentClawdSubmissionRef.current = null;
writeStoredClawdComposerSubmissionSnapshot(null);
return null;
}
return recentSubmission;
}, []);
const clearRestoredClawdComposerText = useCallback(() => {
const submissionSnapshot = getActiveClawdSubmissionSnapshot();
const editableDiv = textareaRef.current as HTMLDivElement | null;
if (!pendingSubmission || !editableDiv) return;
if (pendingSubmission.chatId !== activeChatIdRef.current) return;
if (!submissionSnapshot || !editableDiv) return;
if (submissionSnapshot.chatId !== activeChatIdRef.current) return;
if (composerUserEditedAtRef.current > submissionSnapshot.startedAt) return;
const editorText = normalizeClawdComposerText(
getComposerPlainTextFromElement(editableDiv)
);
if (!editorText) {
if (editableDiv.childNodes.length > 0) {
const stateText = normalizeClawdComposerText(inputRef.current);
if (!editorText && !stateText) {
if (pendingClawdSubmissionRef.current && editableDiv.childNodes.length > 0) {
clearComposerEditor();
}
return;
}
const submittedTextSet = new Set(
pendingSubmission.submittedTexts
.map((text) => normalizeClawdComposerText(text))
.filter(Boolean)
const editorWasRestored = isClawdSubmittedComposerText(
submissionSnapshot,
editorText
);
if (!submittedTextSet.has(editorText)) return;
const stateWasRestored = isClawdSubmittedComposerText(
submissionSnapshot,
stateText
);
if (!editorWasRestored && !stateWasRestored) return;
clearComposerEditor();
}, [clearComposerEditor]);
}, [clearComposerEditor, getActiveClawdSubmissionSnapshot]);
useEffect(() => {
if (typeof window === "undefined") return;
const clearAfterRestore = () => {
window.setTimeout(clearRestoredClawdComposerText, 0);
[0, 120, 500, 1500].forEach((delay) => {
window.setTimeout(clearRestoredClawdComposerText, delay);
});
};
const handleVisibilityChange = () => {
if (document.visibilityState === "visible") {
@ -9452,16 +9570,22 @@ export function CanvasChatPanel({
signature: sendSignature,
timestamp: Date.now(),
});
pendingClawdSubmissionRef.current = {
const clawdSubmissionStartedAt = Date.now();
const clawdSubmissionSnapshot: ClawdComposerSubmissionSnapshot = {
chatId: activeChatId,
expiresAt:
clawdSubmissionStartedAt + CLAWD_SUBMITTED_COMPOSER_RECOVERY_TTL_MS,
startedAt: clawdSubmissionStartedAt,
submittedTexts: [
textContent,
baseRequestMessage,
requestMessage,
optimisticMessageText,
],
startedAt: Date.now(),
};
pendingClawdSubmissionRef.current = clawdSubmissionSnapshot;
recentClawdSubmissionRef.current = clawdSubmissionSnapshot;
writeStoredClawdComposerSubmissionSnapshot(clawdSubmissionSnapshot);
setClawdProcessingHint(nextProcessingHint);
setIsClawdSubmitting(true);
setMessages((previousMessages) =>
@ -9471,6 +9595,9 @@ export function CanvasChatPanel({
])
);
clearComposerEditor();
[0, 120, 500, 1500].forEach((delay) => {
window.setTimeout(clearRestoredClawdComposerText, delay);
});
setAttachments([]);
setClawdTableFiles([]);
clearSelection();
@ -9536,6 +9663,11 @@ export function CanvasChatPanel({
hasHydratedHistoryRef.current = true;
} catch (error) {
console.error("[CanvasChat] Claw request failed:", error);
pendingClawdSubmissionRef.current = null;
if (recentClawdSubmissionRef.current === clawdSubmissionSnapshot) {
recentClawdSubmissionRef.current = null;
writeStoredClawdComposerSubmissionSnapshot(null);
}
setMessages((previousMessages) =>
previousMessages.filter((message) => message.id !== userMessageId)
);
@ -9565,6 +9697,14 @@ export function CanvasChatPanel({
}
} finally {
pendingClawdSubmissionRef.current = null;
if (recentClawdSubmissionRef.current === clawdSubmissionSnapshot) {
const refreshedSubmissionSnapshot = {
...clawdSubmissionSnapshot,
expiresAt: Date.now() + CLAWD_SUBMITTED_COMPOSER_RECOVERY_TTL_MS,
};
recentClawdSubmissionRef.current = refreshedSubmissionSnapshot;
writeStoredClawdComposerSubmissionSnapshot(refreshedSubmissionSnapshot);
}
setIsClawdSubmitting(false);
setClawdProcessingHint(null);
releaseSendGuard(activeChatId);
@ -9899,6 +10039,7 @@ export function CanvasChatPanel({
setSendGuard,
setMessages,
clearComposerEditor,
clearRestoredClawdComposerText,
shouldBlockDuplicateSend,
status,
mutate,
@ -13309,6 +13450,18 @@ export function CanvasChatPanel({
.replace(/\u200b/g, " ")
.replace(/\s+/g, " ")
.trim();
const activeClawdSubmission =
getActiveClawdSubmissionSnapshot();
const isRestoredSubmittedClawdText =
activeClawdSubmission !== null &&
activeClawdSubmission.chatId === activeChatIdRef.current &&
isClawdSubmittedComposerText(
activeClawdSubmission,
textContent
);
if (!isRestoredSubmittedClawdText) {
composerUserEditedAtRef.current = Date.now();
}
setInput(textContent);
if ((!isVideoModel || videoFrameSlots.length === 0) && !hasAgentUploadSlots) {
syncAttachmentsWithEditor(target);