feat: allow canceling claw submissions
This commit is contained in:
parent
fd5b483bad
commit
e1f514536b
40
app/api/canvas/clawd-chat/cancel/route.ts
Normal file
40
app/api/canvas/clawd-chat/cancel/route.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { z } from "zod";
|
||||
import { auth } from "@/app/(auth)/auth";
|
||||
import { getChatById } from "@/lib/db/queries";
|
||||
import {
|
||||
buildClawdCancelKey,
|
||||
cancelClawdRequest,
|
||||
} from "@/lib/openclaw-chat/cancel-registry";
|
||||
|
||||
const cancelSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
userMessageId: z.string().uuid(),
|
||||
});
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const session = await auth().catch(() => null);
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
const parsed = cancelSchema.safeParse(await request.json().catch(() => null));
|
||||
if (!parsed.success) {
|
||||
return NextResponse.json({ error: "请求参数不正确" }, { status: 400 });
|
||||
}
|
||||
|
||||
const existingChat = await getChatById({ id: parsed.data.id }).catch(() => null);
|
||||
if (existingChat && existingChat.userId !== session.user.id) {
|
||||
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
|
||||
}
|
||||
|
||||
const active = cancelClawdRequest(
|
||||
buildClawdCancelKey({
|
||||
userId: session.user.id,
|
||||
chatId: parsed.data.id,
|
||||
userMessageId: parsed.data.userMessageId,
|
||||
})
|
||||
);
|
||||
|
||||
return NextResponse.json({ canceled: true, active });
|
||||
}
|
||||
@ -13,6 +13,14 @@ import {
|
||||
normalizeOpenClawChatConfig,
|
||||
OPENCLAW_CHAT_CONFIG_KEY,
|
||||
} from "@/lib/openclaw-chat/config";
|
||||
import {
|
||||
buildClawdCancelKey,
|
||||
cancelClawdRequest,
|
||||
clearClawdRequestCancellation,
|
||||
isClawdRequestCanceled,
|
||||
registerActiveClawdRequest,
|
||||
unregisterActiveClawdRequest,
|
||||
} from "@/lib/openclaw-chat/cancel-registry";
|
||||
import {
|
||||
buildOpenClawDisplayMessage,
|
||||
buildOpenClawMessageWithSpreadsheetMarkdown,
|
||||
@ -141,6 +149,10 @@ function getMetadataString(
|
||||
return typeof value === "string" ? value.trim() : "";
|
||||
}
|
||||
|
||||
function createCanceledResponse() {
|
||||
return NextResponse.json({ canceled: true }, { status: 499 });
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const session = await auth().catch(() => null);
|
||||
|
||||
@ -172,6 +184,16 @@ export async function POST(request: Request) {
|
||||
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
|
||||
}
|
||||
|
||||
const userMessageId = parsed.userMessageId || generateUUID();
|
||||
const cancelKey = buildClawdCancelKey({
|
||||
userId: session.user.id,
|
||||
chatId: parsed.id,
|
||||
userMessageId,
|
||||
});
|
||||
if (isClawdRequestCanceled(cancelKey)) {
|
||||
return createCanceledResponse();
|
||||
}
|
||||
|
||||
let spreadsheetMarkdownResult: Awaited<
|
||||
ReturnType<typeof buildOpenClawSpreadsheetMarkdown>
|
||||
> | null = null;
|
||||
@ -200,6 +222,10 @@ export async function POST(request: Request) {
|
||||
spreadsheetMarkdown: spreadsheetMarkdownResult?.markdown || "",
|
||||
});
|
||||
|
||||
if (isClawdRequestCanceled(cancelKey)) {
|
||||
return createCanceledResponse();
|
||||
}
|
||||
|
||||
if (!existingChat) {
|
||||
await saveChat({
|
||||
id: parsed.id,
|
||||
@ -209,7 +235,6 @@ export async function POST(request: Request) {
|
||||
});
|
||||
}
|
||||
|
||||
const userMessageId = parsed.userMessageId || generateUUID();
|
||||
const assistantMessageId = generateUUID();
|
||||
const conversationId = createConversationId(session.user.id, parsed.id);
|
||||
const userProfile = await getUserById({ id: session.user.id }).catch(() => null);
|
||||
@ -226,9 +251,18 @@ export async function POST(request: Request) {
|
||||
let upstreamPayload: unknown = null;
|
||||
let upstreamStatus = 502;
|
||||
|
||||
if (isClawdRequestCanceled(cancelKey)) {
|
||||
return createCanceledResponse();
|
||||
}
|
||||
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
registerActiveClawdRequest(cancelKey, controller, config.timeoutMs);
|
||||
const timeout = setTimeout(() => controller.abort(), config.timeoutMs);
|
||||
const handleRequestAbort = () => {
|
||||
cancelClawdRequest(cancelKey);
|
||||
};
|
||||
request.signal.addEventListener("abort", handleRequestAbort, { once: true });
|
||||
const response = await fetch(endpoint, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
@ -266,7 +300,11 @@ export async function POST(request: Request) {
|
||||
},
|
||||
}),
|
||||
signal: controller.signal,
|
||||
}).finally(() => clearTimeout(timeout));
|
||||
}).finally(() => {
|
||||
clearTimeout(timeout);
|
||||
request.signal.removeEventListener("abort", handleRequestAbort);
|
||||
unregisterActiveClawdRequest(cancelKey);
|
||||
});
|
||||
|
||||
upstreamStatus = response.status;
|
||||
upstreamPayload = await response.json().catch(() => null);
|
||||
@ -287,8 +325,19 @@ export async function POST(request: Request) {
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
const isAbortError =
|
||||
error instanceof Error && error.name === "AbortError";
|
||||
unregisterActiveClawdRequest(cancelKey);
|
||||
const errorName =
|
||||
error && typeof error === "object" && "name" in error
|
||||
? String((error as { name?: unknown }).name || "")
|
||||
: "";
|
||||
const isAbortError = errorName === "AbortError";
|
||||
if (isClawdRequestCanceled(cancelKey)) {
|
||||
console.info("[ClawdChat API] OpenClaw request canceled:", {
|
||||
chatId: parsed.id,
|
||||
userMessageId,
|
||||
});
|
||||
return createCanceledResponse();
|
||||
}
|
||||
console.error("[ClawdChat API] OpenClaw request failed:", error);
|
||||
return NextResponse.json(
|
||||
{
|
||||
@ -311,6 +360,10 @@ export async function POST(request: Request) {
|
||||
);
|
||||
}
|
||||
|
||||
if (isClawdRequestCanceled(cancelKey)) {
|
||||
return createCanceledResponse();
|
||||
}
|
||||
|
||||
const existingMessages = await getMessagesByChatId({ id: parsed.id }).catch(
|
||||
() => []
|
||||
);
|
||||
@ -366,6 +419,7 @@ export async function POST(request: Request) {
|
||||
};
|
||||
|
||||
await saveMessages({ messages: [userMessage, assistantMessage] });
|
||||
clearClawdRequestCancellation(cancelKey);
|
||||
|
||||
return NextResponse.json({
|
||||
conversationId,
|
||||
|
||||
@ -25,7 +25,7 @@ import {
|
||||
type UiSurfaceSnapshot,
|
||||
} from "@/lib/ui-surface-protocol";
|
||||
import useSWR, { useSWRConfig } from "swr";
|
||||
import { Loader2, ImagePlus, X, Plus, History, LayoutGrid, ArrowRightToLine, RefreshCw, Globe, ArrowUp, Video, Sparkles, Check, ChevronDown, ChevronUp, AlertCircle, CheckCircle2, Brain, ThumbsUp, ThumbsDown, Eye, Trash2, Download, MessageCircle, Clapperboard, Bot, FileSpreadsheet, Paperclip, Copy } from "lucide-react";
|
||||
import { Loader2, ImagePlus, X, Plus, History, LayoutGrid, ArrowRightToLine, RefreshCw, Globe, ArrowUp, Video, Sparkles, Check, ChevronDown, ChevronUp, AlertCircle, CheckCircle2, Brain, ThumbsUp, ThumbsDown, Eye, Trash2, Download, MessageCircle, Clapperboard, Bot, FileSpreadsheet, Paperclip, Copy, Square } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Collapsible,
|
||||
@ -458,6 +458,16 @@ type ClawdComposerSubmissionSnapshot = {
|
||||
submittedTexts: string[];
|
||||
};
|
||||
|
||||
type PendingClawdRequestRestoreSnapshot = {
|
||||
chatId: string;
|
||||
userMessageId: string;
|
||||
textContent: string;
|
||||
tableFiles: File[];
|
||||
attachments: PickedObjectAttachment[];
|
||||
skillTag: CanvasSkillTag | null;
|
||||
shouldRestoreSkillTag: boolean;
|
||||
};
|
||||
|
||||
function isClawdSubmittedComposerText(
|
||||
snapshot: ClawdComposerSubmissionSnapshot,
|
||||
value: string
|
||||
@ -1600,9 +1610,13 @@ function OpenClawQuickActions({
|
||||
function CanvasAssistantTypingPlaceholder({
|
||||
hint,
|
||||
label = "分析中",
|
||||
onCancel,
|
||||
cancelLabel = "停止",
|
||||
}: {
|
||||
hint?: string | null;
|
||||
label?: string;
|
||||
onCancel?: () => void;
|
||||
cancelLabel?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex w-full justify-start">
|
||||
@ -1618,6 +1632,19 @@ function CanvasAssistantTypingPlaceholder({
|
||||
>
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
<span>{label}</span>
|
||||
{onCancel ? (
|
||||
<button
|
||||
type="button"
|
||||
className="-mr-1 ml-1 rounded-full px-2 py-0.5 text-[12px] font-semibold text-[#5d6675] transition-colors hover:bg-black/5 hover:text-[#242a33] dark:text-zinc-300 dark:hover:bg-white/10 dark:hover:text-white"
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
onCancel();
|
||||
}}
|
||||
>
|
||||
{cancelLabel}
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -2259,6 +2286,10 @@ export function CanvasChatPanel({
|
||||
useRef<ClawdComposerSubmissionSnapshot | null>(null);
|
||||
const recentClawdSubmissionRef =
|
||||
useRef<ClawdComposerSubmissionSnapshot | null>(null);
|
||||
const clawdAbortControllerRef = useRef<AbortController | null>(null);
|
||||
const pendingClawdRequestRestoreRef =
|
||||
useRef<PendingClawdRequestRestoreSnapshot | null>(null);
|
||||
const canceledClawdMessageIdsRef = useRef<Set<string>>(new Set());
|
||||
|
||||
const replaceChatSessionQuery = useCallback(
|
||||
(sessionId: string) => {
|
||||
@ -7120,6 +7151,76 @@ export function CanvasChatPanel({
|
||||
},
|
||||
});
|
||||
|
||||
const restorePendingClawdRequestToComposer = useCallback(
|
||||
(snapshot: PendingClawdRequestRestoreSnapshot) => {
|
||||
setMessages((previousMessages) =>
|
||||
previousMessages.filter((message) => message.id !== snapshot.userMessageId)
|
||||
);
|
||||
setClawdTableFiles(snapshot.tableFiles);
|
||||
setAttachments(snapshot.attachments);
|
||||
|
||||
if (snapshot.skillTag && snapshot.shouldRestoreSkillTag) {
|
||||
setSessionClawdSkillTag(snapshot.chatId, snapshot.skillTag.id, {
|
||||
promptApplied: false,
|
||||
});
|
||||
setSelectedSkillTagId(snapshot.skillTag.id);
|
||||
pendingSelectedClawdSkillTagRef.current = snapshot.skillTag;
|
||||
}
|
||||
|
||||
composerUserEditedAtRef.current = Date.now();
|
||||
inputRef.current = snapshot.textContent;
|
||||
setInput(snapshot.textContent);
|
||||
|
||||
const editableDiv = textareaRef.current as HTMLDivElement | null;
|
||||
if (editableDiv) {
|
||||
editableDiv.innerHTML = "";
|
||||
editableDiv.textContent = snapshot.textContent;
|
||||
window.setTimeout(() => {
|
||||
snapshot.attachments.forEach((attachment, index) => {
|
||||
if (!attachment.url) return;
|
||||
insertImageTagInEditor(
|
||||
attachment.url,
|
||||
normalizeChatAttachmentName(attachment.name),
|
||||
index
|
||||
);
|
||||
});
|
||||
editableDiv.focus();
|
||||
}, 0);
|
||||
}
|
||||
},
|
||||
[insertImageTagInEditor, setMessages, setSessionClawdSkillTag]
|
||||
);
|
||||
|
||||
const handleCancelClawdSubmission = useCallback(() => {
|
||||
const snapshot = pendingClawdRequestRestoreRef.current;
|
||||
if (!snapshot) return;
|
||||
|
||||
canceledClawdMessageIdsRef.current.add(snapshot.userMessageId);
|
||||
pendingClawdRequestRestoreRef.current = null;
|
||||
pendingClawdSubmissionRef.current = null;
|
||||
recentClawdSubmissionRef.current = null;
|
||||
writeStoredClawdComposerSubmissionSnapshot(null);
|
||||
|
||||
void fetch("/api/canvas/clawd-chat/cancel", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
id: snapshot.chatId,
|
||||
userMessageId: snapshot.userMessageId,
|
||||
}),
|
||||
}).catch((error) => {
|
||||
console.warn("[CanvasChat] Claw cancel request failed:", error);
|
||||
});
|
||||
|
||||
clawdAbortControllerRef.current?.abort();
|
||||
clawdAbortControllerRef.current = null;
|
||||
setIsClawdSubmitting(false);
|
||||
setClawdProcessingHint(null);
|
||||
releaseSendGuard(snapshot.chatId);
|
||||
restorePendingClawdRequestToComposer(snapshot);
|
||||
toast.message("已停止处理,可以重新编辑后再发送");
|
||||
}, [releaseSendGuard, restorePendingClawdRequestToComposer]);
|
||||
|
||||
useEffect(() => {
|
||||
activeMessagesRef.current = messages;
|
||||
}, [messages]);
|
||||
@ -10731,6 +10832,19 @@ export function CanvasChatPanel({
|
||||
pendingClawdSubmissionRef.current = clawdSubmissionSnapshot;
|
||||
recentClawdSubmissionRef.current = clawdSubmissionSnapshot;
|
||||
writeStoredClawdComposerSubmissionSnapshot(clawdSubmissionSnapshot);
|
||||
const clawdAbortController = new AbortController();
|
||||
const clawdRequestRestoreSnapshot: PendingClawdRequestRestoreSnapshot = {
|
||||
chatId: activeChatId,
|
||||
userMessageId,
|
||||
textContent,
|
||||
tableFiles: tableFilesSnapshot,
|
||||
attachments: attachmentsSnapshot,
|
||||
skillTag: clawdSkillTagForRequest || null,
|
||||
shouldRestoreSkillTag: hasPendingClawdSkillTagPrompt,
|
||||
};
|
||||
canceledClawdMessageIdsRef.current.delete(userMessageId);
|
||||
clawdAbortControllerRef.current = clawdAbortController;
|
||||
pendingClawdRequestRestoreRef.current = clawdRequestRestoreSnapshot;
|
||||
setClawdProcessingHint(nextProcessingHint);
|
||||
setIsClawdSubmitting(true);
|
||||
setMessages((previousMessages) =>
|
||||
@ -10773,8 +10887,13 @@ export function CanvasChatPanel({
|
||||
const response = await fetch("/api/canvas/clawd-chat", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
signal: clawdAbortController.signal,
|
||||
});
|
||||
const responseData = await response.json().catch(() => null);
|
||||
if (response.status === 499 || responseData?.canceled === true) {
|
||||
canceledClawdMessageIdsRef.current.add(userMessageId);
|
||||
throw new DOMException("Claw request canceled", "AbortError");
|
||||
}
|
||||
if (!response.ok) {
|
||||
console.error("[CanvasChat] Claw API returned error:", {
|
||||
status: response.status,
|
||||
@ -10824,6 +10943,17 @@ export function CanvasChatPanel({
|
||||
void mutateClawdHistory();
|
||||
hasHydratedHistoryRef.current = true;
|
||||
} catch (error) {
|
||||
const wasCanceled =
|
||||
canceledClawdMessageIdsRef.current.has(userMessageId) ||
|
||||
(error instanceof Error && error.name === "AbortError");
|
||||
if (wasCanceled) {
|
||||
if (pendingClawdRequestRestoreRef.current === clawdRequestRestoreSnapshot) {
|
||||
pendingClawdRequestRestoreRef.current = null;
|
||||
restorePendingClawdRequestToComposer(clawdRequestRestoreSnapshot);
|
||||
toast.message("已停止处理,可以重新编辑后再发送");
|
||||
}
|
||||
return;
|
||||
}
|
||||
console.error("[CanvasChat] Claw request failed:", error);
|
||||
pendingClawdSubmissionRef.current = null;
|
||||
if (recentClawdSubmissionRef.current === clawdSubmissionSnapshot) {
|
||||
@ -10865,13 +10995,25 @@ export function CanvasChatPanel({
|
||||
}
|
||||
} finally {
|
||||
pendingClawdSubmissionRef.current = null;
|
||||
if (pendingClawdRequestRestoreRef.current === clawdRequestRestoreSnapshot) {
|
||||
pendingClawdRequestRestoreRef.current = null;
|
||||
}
|
||||
if (clawdAbortControllerRef.current === clawdAbortController) {
|
||||
clawdAbortControllerRef.current = null;
|
||||
}
|
||||
const wasCanceled = canceledClawdMessageIdsRef.current.delete(userMessageId);
|
||||
if (recentClawdSubmissionRef.current === clawdSubmissionSnapshot) {
|
||||
if (wasCanceled) {
|
||||
recentClawdSubmissionRef.current = null;
|
||||
writeStoredClawdComposerSubmissionSnapshot(null);
|
||||
} else {
|
||||
const refreshedSubmissionSnapshot = {
|
||||
...clawdSubmissionSnapshot,
|
||||
expiresAt: Date.now() + CLAWD_SUBMITTED_COMPOSER_RECOVERY_TTL_MS,
|
||||
};
|
||||
recentClawdSubmissionRef.current = refreshedSubmissionSnapshot;
|
||||
writeStoredClawdComposerSubmissionSnapshot(refreshedSubmissionSnapshot);
|
||||
}
|
||||
}
|
||||
setIsClawdSubmitting(false);
|
||||
setClawdProcessingHint(null);
|
||||
@ -11244,6 +11386,7 @@ export function CanvasChatPanel({
|
||||
markSessionClawdSkillTagPromptApplied,
|
||||
updateChatSessions,
|
||||
buildHistoryMessagesUrl,
|
||||
restorePendingClawdRequestToComposer,
|
||||
workflowContext,
|
||||
messages,
|
||||
]);
|
||||
@ -12092,6 +12235,7 @@ export function CanvasChatPanel({
|
||||
isClawdMode && activeClawdSkillTag
|
||||
? activeClawdSkillTag.placeholder?.trim() || ""
|
||||
: "";
|
||||
const isClawdStopAvailable = isClawdMode && isClawdSubmitting;
|
||||
const handleClawdPresetPlaceholderDoubleClick = useCallback(
|
||||
(event: ReactMouseEvent<HTMLDivElement>) => {
|
||||
if (isSubmitting || !isClawdMode || !clawdEditablePresetPlaceholder) return;
|
||||
@ -13878,7 +14022,12 @@ export function CanvasChatPanel({
|
||||
) : null}
|
||||
|
||||
{isClawdMode && isClawdSubmitting ? (
|
||||
<CanvasAssistantTypingPlaceholder hint={clawdProcessingHint} label="分析中" />
|
||||
<CanvasAssistantTypingPlaceholder
|
||||
cancelLabel="停止并编辑"
|
||||
hint={clawdProcessingHint}
|
||||
label="分析中"
|
||||
onCancel={handleCancelClawdSubmission}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<div ref={messagesEndRef} />
|
||||
@ -15756,7 +15905,9 @@ export function CanvasChatPanel({
|
||||
isWorkspacePresentation
|
||||
? "h-10 rounded-full px-4 transition-all duration-200 flex-shrink-0"
|
||||
: "h-8 w-8 rounded-full transition-all duration-200 flex-shrink-0",
|
||||
isVideoToolbarDark
|
||||
isClawdStopAvailable
|
||||
? "bg-rose-500 text-white shadow-sm hover:bg-rose-600 dark:bg-rose-500 dark:text-white dark:hover:bg-rose-400"
|
||||
: isVideoToolbarDark
|
||||
? canSend
|
||||
? "border border-[#e7ff1a]/20 bg-[#e7ff1a] text-zinc-950 shadow-sm hover:bg-[#dff40f]"
|
||||
: "border border-white/10 bg-[#242529] text-zinc-500 hover:bg-[#2c2d33]"
|
||||
@ -15768,15 +15919,28 @@ export function CanvasChatPanel({
|
||||
? "bg-[#eef0f3] text-[#20242a] shadow-sm ring-1 ring-inset ring-[#d5dae2] hover:bg-[#e4e7ec] dark:bg-white dark:text-zinc-950 dark:ring-white/15 dark:hover:bg-zinc-100"
|
||||
: "bg-[#f1f1f2] text-[#b6bac2] hover:bg-[#ececee] dark:bg-[#242529] dark:text-[#777d88] dark:hover:bg-[#2c2d33]"
|
||||
)}
|
||||
onClick={() => void handleSend()}
|
||||
disabled={isSubmitting || !canSend}
|
||||
onClick={() => {
|
||||
if (isClawdStopAvailable) {
|
||||
handleCancelClawdSubmission();
|
||||
return;
|
||||
}
|
||||
void handleSend();
|
||||
}}
|
||||
disabled={isClawdStopAvailable ? false : isSubmitting || !canSend}
|
||||
title={
|
||||
isWorkspacePresentation || isDetailGenerationMode
|
||||
isClawdStopAvailable
|
||||
? "停止处理并重新编辑"
|
||||
: isWorkspacePresentation || isDetailGenerationMode
|
||||
? getToolbarItem("chatStartGeneration").label
|
||||
: getToolbarItem("chatSend").label
|
||||
}
|
||||
>
|
||||
{isSubmitting ? (
|
||||
{isClawdStopAvailable ? (
|
||||
<>
|
||||
<Square className="h-3.5 w-3.5 fill-current" />
|
||||
{isWorkspacePresentation ? <span className="ml-2">停止</span> : null}
|
||||
</>
|
||||
) : isSubmitting ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
{isWorkspacePresentation ? <span className="ml-2">生成中</span> : null}
|
||||
|
||||
76
lib/openclaw-chat/cancel-registry.ts
Normal file
76
lib/openclaw-chat/cancel-registry.ts
Normal file
@ -0,0 +1,76 @@
|
||||
const CANCELED_REQUEST_TTL_MS = 10 * 60 * 1000;
|
||||
|
||||
type ActiveClawdRequest = {
|
||||
controller: AbortController;
|
||||
expiresAt: number;
|
||||
};
|
||||
|
||||
const activeRequests = new Map<string, ActiveClawdRequest>();
|
||||
const canceledRequests = new Map<string, number>();
|
||||
|
||||
function cleanupExpiredClawdRequests(now = Date.now()) {
|
||||
for (const [key, request] of activeRequests.entries()) {
|
||||
if (request.expiresAt <= now) {
|
||||
activeRequests.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
for (const [key, expiresAt] of canceledRequests.entries()) {
|
||||
if (expiresAt <= now) {
|
||||
canceledRequests.delete(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function buildClawdCancelKey({
|
||||
userId,
|
||||
chatId,
|
||||
userMessageId,
|
||||
}: {
|
||||
userId: string;
|
||||
chatId: string;
|
||||
userMessageId: string;
|
||||
}) {
|
||||
return `${userId}:${chatId}:${userMessageId}`;
|
||||
}
|
||||
|
||||
export function registerActiveClawdRequest(
|
||||
key: string,
|
||||
controller: AbortController,
|
||||
timeoutMs: number
|
||||
) {
|
||||
cleanupExpiredClawdRequests();
|
||||
activeRequests.set(key, {
|
||||
controller,
|
||||
expiresAt: Date.now() + Math.max(timeoutMs, CANCELED_REQUEST_TTL_MS),
|
||||
});
|
||||
}
|
||||
|
||||
export function unregisterActiveClawdRequest(key: string) {
|
||||
activeRequests.delete(key);
|
||||
}
|
||||
|
||||
export function cancelClawdRequest(key: string) {
|
||||
cleanupExpiredClawdRequests();
|
||||
canceledRequests.set(key, Date.now() + CANCELED_REQUEST_TTL_MS);
|
||||
const activeRequest = activeRequests.get(key);
|
||||
if (activeRequest && !activeRequest.controller.signal.aborted) {
|
||||
activeRequest.controller.abort();
|
||||
}
|
||||
return Boolean(activeRequest);
|
||||
}
|
||||
|
||||
export function isClawdRequestCanceled(key: string) {
|
||||
cleanupExpiredClawdRequests();
|
||||
const expiresAt = canceledRequests.get(key);
|
||||
if (!expiresAt) return false;
|
||||
if (expiresAt <= Date.now()) {
|
||||
canceledRequests.delete(key);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export function clearClawdRequestCancellation(key: string) {
|
||||
canceledRequests.delete(key);
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user