From c6c7c703b5575ec019a5495a3e2251abaa486ba7 Mon Sep 17 00:00:00 2001 From: 134 Date: Wed, 5 Aug 2026 00:14:06 +0800 Subject: [PATCH] fix(canvas): handle expired sessions for Claw skills --- app/api/canvas-skill-tags/route.ts | 18 +- app/api/canvas/clawd-chat/cancel/route.ts | 12 +- app/api/canvas/clawd-chat/history/route.ts | 12 +- app/api/canvas/clawd-chat/route.ts | 32 ++- .../[id]/CanvasEditorLegacyPageClient.tsx | 1 + components/canvas/CanvasChatPanel.tsx | 266 +++++++++++------- lib/canvas-skill-tag-order.ts | 35 +++ lib/openclaw-chat/errors.ts | 45 +++ playwright.helpers.config.ts | 2 + tests/routes/canvas-skill-tag-order.test.ts | 38 +++ tests/routes/openclaw-client-errors.test.ts | 57 ++++ 11 files changed, 392 insertions(+), 126 deletions(-) create mode 100644 lib/canvas-skill-tag-order.ts create mode 100644 lib/openclaw-chat/errors.ts create mode 100644 tests/routes/canvas-skill-tag-order.test.ts create mode 100644 tests/routes/openclaw-client-errors.test.ts diff --git a/app/api/canvas-skill-tags/route.ts b/app/api/canvas-skill-tags/route.ts index 22c9ec3c0..1b18b6853 100644 --- a/app/api/canvas-skill-tags/route.ts +++ b/app/api/canvas-skill-tags/route.ts @@ -7,9 +7,9 @@ import { CANVAS_SKILL_TAG_ORDER_SETTING_KEY_PREFIX, CANVAS_SKILL_TAGS_CONFIG_VERSION, CANVAS_SKILL_TAGS_SETTING_KEY, - type CanvasSkillTag, normalizeCanvasSkillTagSettings, } from "@/lib/canvas-skill-tags"; +import { applyCanvasSkillTagOrder } from "@/lib/canvas-skill-tag-order"; function getUserSkillTagOrderKey(userId: string) { return `${CANVAS_SKILL_TAG_ORDER_SETTING_KEY_PREFIX}${userId}`; @@ -32,18 +32,6 @@ function normalizeSkillTagOrder(value: unknown) { }); } -function applySkillTagOrder(tags: CanvasSkillTag[], order: string[]) { - if (tags.length < 2 || order.length === 0) return tags; - - const tagById = new Map(tags.map((tag) => [tag.id, tag])); - const ordered = order - .map((id) => tagById.get(id)) - .filter((tag): tag is CanvasSkillTag => Boolean(tag)); - const orderedIds = new Set(ordered.map((tag) => tag.id)); - const remaining = tags.filter((tag) => !orderedIds.has(tag.id)); - return [...ordered, ...remaining]; -} - export async function GET() { try { const session = await auth().catch(() => null); @@ -71,7 +59,7 @@ export async function GET() { const userOrder = normalizeSkillTagOrder(userOrderSetting?.value); return NextResponse.json({ ...settings, - tags: applySkillTagOrder(settings.tags, userOrder), + tags: applyCanvasSkillTagOrder(settings.tags, userOrder), userOrder, }); } catch (error) { @@ -133,7 +121,7 @@ export async function PATCH(request: NextRequest) { return NextResponse.json({ success: true, userOrder: tagIds, - tags: applySkillTagOrder(settings.tags, tagIds), + tags: applyCanvasSkillTagOrder(settings.tags, tagIds), }); } catch (error) { console.error("[CanvasSkillTags API] Save order error:", error); diff --git a/app/api/canvas/clawd-chat/cancel/route.ts b/app/api/canvas/clawd-chat/cancel/route.ts index 138a372e1..fc9d08fda 100644 --- a/app/api/canvas/clawd-chat/cancel/route.ts +++ b/app/api/canvas/clawd-chat/cancel/route.ts @@ -6,6 +6,10 @@ import { buildClawdCancelKey, cancelClawdRequest, } from "@/lib/openclaw-chat/cancel-registry"; +import { + OPENCLAW_AUTH_REQUIRED_CODE, + OPENCLAW_AUTH_REQUIRED_MESSAGE, +} from "@/lib/openclaw-chat/errors"; const cancelSchema = z.object({ id: z.string().uuid(), @@ -15,7 +19,13 @@ const cancelSchema = z.object({ export async function POST(request: Request) { const session = await auth().catch(() => null); if (!session?.user?.id) { - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + return NextResponse.json( + { + error: OPENCLAW_AUTH_REQUIRED_MESSAGE, + code: OPENCLAW_AUTH_REQUIRED_CODE, + }, + { status: 401 } + ); } const parsed = cancelSchema.safeParse(await request.json().catch(() => null)); diff --git a/app/api/canvas/clawd-chat/history/route.ts b/app/api/canvas/clawd-chat/history/route.ts index 822587749..97bbf669d 100644 --- a/app/api/canvas/clawd-chat/history/route.ts +++ b/app/api/canvas/clawd-chat/history/route.ts @@ -1,6 +1,10 @@ import { NextResponse } from "next/server"; import { auth } from "@/app/(auth)/auth"; import { getClawdChatsByUserId } from "@/lib/db/queries"; +import { + OPENCLAW_AUTH_REQUIRED_CODE, + OPENCLAW_AUTH_REQUIRED_MESSAGE, +} from "@/lib/openclaw-chat/errors"; export const dynamic = "force-dynamic"; export const revalidate = 0; @@ -9,7 +13,13 @@ export async function GET(request: Request) { const session = await auth().catch(() => null); if (!session?.user?.id) { - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + return NextResponse.json( + { + error: OPENCLAW_AUTH_REQUIRED_MESSAGE, + code: OPENCLAW_AUTH_REQUIRED_CODE, + }, + { status: 401 } + ); } const url = new URL(request.url); diff --git a/app/api/canvas/clawd-chat/route.ts b/app/api/canvas/clawd-chat/route.ts index 3602d5e2d..31c2d1d67 100644 --- a/app/api/canvas/clawd-chat/route.ts +++ b/app/api/canvas/clawd-chat/route.ts @@ -13,6 +13,11 @@ import { normalizeOpenClawChatConfig, OPENCLAW_CHAT_CONFIG_KEY, } from "@/lib/openclaw-chat/config"; +import { + OPENCLAW_AUTH_REQUIRED_CODE, + OPENCLAW_AUTH_REQUIRED_MESSAGE, + OPENCLAW_UPGRADE_RETRY_MESSAGE, +} from "@/lib/openclaw-chat/errors"; import { buildClawdCancelKey, clearClawdRequestCancellation, @@ -32,8 +37,6 @@ import { } from "@/lib/openclaw-chat/media-attachments"; import { generateUUID } from "@/lib/utils"; -const CLAW_UPGRADE_RETRY_MESSAGE = "claw正在升级更新稍等5分钟再试"; - const requestSchema = z.object({ id: z.string().uuid(), message: z.string().trim().max(8000).optional().default(""), @@ -364,7 +367,13 @@ export async function POST(request: Request) { const session = await auth().catch(() => null); if (!session?.user?.id) { - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + return NextResponse.json( + { + error: OPENCLAW_AUTH_REQUIRED_MESSAGE, + code: OPENCLAW_AUTH_REQUIRED_CODE, + }, + { status: 401 } + ); } const parsed = await parseClawdChatRequest(request); @@ -384,11 +393,14 @@ export async function POST(request: Request) { const config = await readOpenClawConfig(); if (!config.enabled) { - return NextResponse.json({ error: CLAW_UPGRADE_RETRY_MESSAGE }, { status: 503 }); + return NextResponse.json( + { error: OPENCLAW_UPGRADE_RETRY_MESSAGE }, + { status: 503 } + ); } if (!config.baseUrl || !config.token) { return NextResponse.json( - { error: CLAW_UPGRADE_RETRY_MESSAGE }, + { error: OPENCLAW_UPGRADE_RETRY_MESSAGE }, { status: 503 } ); } @@ -599,7 +611,7 @@ export async function POST(request: Request) { streamController.enqueue( encoder.encode( encodeSseEvent("error", { - error: CLAW_UPGRADE_RETRY_MESSAGE, + error: OPENCLAW_UPGRADE_RETRY_MESSAGE, }) ) ); @@ -610,7 +622,7 @@ export async function POST(request: Request) { streamController.enqueue( encoder.encode( encodeSseEvent("error", { - error: CLAW_UPGRADE_RETRY_MESSAGE, + error: OPENCLAW_UPGRADE_RETRY_MESSAGE, }) ) ); @@ -645,7 +657,7 @@ export async function POST(request: Request) { }); return NextResponse.json( { - error: CLAW_UPGRADE_RETRY_MESSAGE, + error: OPENCLAW_UPGRADE_RETRY_MESSAGE, code: errorCode || undefined, }, { status: response.status } @@ -668,7 +680,7 @@ export async function POST(request: Request) { console.error("[ClawdChat API] OpenClaw request failed:", error); return NextResponse.json( { - error: CLAW_UPGRADE_RETRY_MESSAGE, + error: OPENCLAW_UPGRADE_RETRY_MESSAGE, }, { status: isAbortError ? 504 : upstreamStatus } ); @@ -682,7 +694,7 @@ export async function POST(request: Request) { if (!answer) { console.error("[ClawdChat API] OpenClaw empty answer:", upstreamPayload); return NextResponse.json( - { error: CLAW_UPGRADE_RETRY_MESSAGE }, + { error: OPENCLAW_UPGRADE_RETRY_MESSAGE }, { status: 502 } ); } diff --git a/app/canvas/[id]/CanvasEditorLegacyPageClient.tsx b/app/canvas/[id]/CanvasEditorLegacyPageClient.tsx index 06d23ce15..6e53b45af 100644 --- a/app/canvas/[id]/CanvasEditorLegacyPageClient.tsx +++ b/app/canvas/[id]/CanvasEditorLegacyPageClient.tsx @@ -10868,6 +10868,7 @@ export default function CanvasEditorLegacyPage({ chatScope="canvas" onClose={() => setIsPanelOpen(false)} onOpenFileList={() => setPanelMode("fileList")} + onRequireLogin={openLoginDialog} selectedAgentPresetId={ isCanvasAgentModeEnabled ? selectedAgentPresetId : null } diff --git a/components/canvas/CanvasChatPanel.tsx b/components/canvas/CanvasChatPanel.tsx index 2520c8975..e9ae58b38 100644 --- a/components/canvas/CanvasChatPanel.tsx +++ b/components/canvas/CanvasChatPanel.tsx @@ -2,7 +2,7 @@ import { useChat } from "@ai-sdk/react"; import { DefaultChatTransport } from "ai"; -import { useEffect, useRef, useState, useCallback, useMemo, lazy, Suspense, type ClipboardEvent as ReactClipboardEvent, type ComponentProps, type DragEvent as ReactDragEvent, type MouseEvent as ReactMouseEvent, type PointerEvent as ReactPointerEvent } from "react"; +import { useEffect, useRef, useState, useCallback, useMemo, lazy, Suspense, type ClipboardEvent as ReactClipboardEvent, type ComponentProps, type MouseEvent as ReactMouseEvent, type PointerEvent as ReactPointerEvent } from "react"; import { useSession } from "next-auth/react"; import { useSearchParams } from "next/navigation"; import { createPortal } from "react-dom"; @@ -101,10 +101,20 @@ import { normalizeCanvasSkillTags, type CanvasSkillTag, } from "@/lib/canvas-skill-tags"; +import { + applyCanvasSkillTagOrder, + reorderCanvasSkillTags, +} from "@/lib/canvas-skill-tag-order"; import { isClawdTransportInterruption, shouldRestoreClawdComposerAfterFailure, } from "@/lib/openclaw-chat/request-lifecycle"; +import { + getOpenClawClientErrorMessage, + isOpenClawAuthRequiredError, + OPENCLAW_AUTH_REQUIRED_MESSAGE, + OPENCLAW_UPGRADE_RETRY_MESSAGE, +} from "@/lib/openclaw-chat/errors"; import { isCanvasImageReadyForAi, isCanvasImageUploadPending, @@ -368,12 +378,12 @@ interface CanvasChatPanelProps { workspaceVariant?: "canvas" | "video"; canvasToolbarConfig?: CanvasToolbarConfig; canvasAgentFeatureSettings?: CanvasAgentFeatureSettings; + onRequireLogin?: (message?: string) => void; } type CanvasComposerMode = "agent" | "image" | "video" | "detail" | "clawd"; const CLAW_MODE_LABEL = "Claw"; -const CLAW_UPGRADE_RETRY_MESSAGE = "claw正在升级更新稍等5分钟再试"; const CANVAS_MODEL_SELECTOR_WIDTH_STORAGE_KEY = "ai-chatbot:canvas-model-selector-width"; const DEFAULT_CANVAS_MODEL_SELECTOR_WIDTH = 360; @@ -391,6 +401,24 @@ function clampCanvasModelSelectorWidth(value: number) { ); } +function requestCanvasLogin( + onRequireLogin: CanvasChatPanelProps["onRequireLogin"], + message: string +) { + if (onRequireLogin) { + onRequireLogin(message); + return; + } + if (typeof window === "undefined") { + return; + } + + const callbackUrl = `${window.location.pathname}${window.location.search}${window.location.hash}`; + window.location.assign( + `/login?callbackUrl=${encodeURIComponent(callbackUrl)}` + ); +} + function getCanvasComposerModeSessionLabel(mode: CanvasComposerMode) { if (mode === "clawd") return CLAW_MODE_LABEL; if (mode === "video") return "视频"; @@ -510,34 +538,15 @@ function buildClawdSkillTagPrompt(tag: Pick | ].join("\n"); } -function reorderCanvasSkillTags( - tags: CanvasSkillTag[], - draggedId: string, - targetId: string -) { - if (!draggedId || !targetId || draggedId === targetId) return tags; - const currentIndex = tags.findIndex((tag) => tag.id === draggedId); - const targetIndex = tags.findIndex((tag) => tag.id === targetId); - if (currentIndex < 0 || targetIndex < 0) return tags; +const CANVAS_SKILL_TAG_DRAG_THRESHOLD_PX = 6; - const nextTags = [...tags]; - const [draggedTag] = nextTags.splice(currentIndex, 1); - if (!draggedTag) return tags; - const nextTargetIndex = nextTags.findIndex((tag) => tag.id === targetId); - nextTags.splice(nextTargetIndex < 0 ? targetIndex : nextTargetIndex, 0, draggedTag); - return nextTags; -} - -function applyCanvasSkillTagOrder(tags: CanvasSkillTag[], order: string[]) { - if (tags.length < 2 || order.length === 0) return tags; - - const tagById = new Map(tags.map((tag) => [tag.id, tag])); - const orderedTags = order - .map((id) => tagById.get(id)) - .filter((tag): tag is CanvasSkillTag => Boolean(tag)); - const orderedIds = new Set(orderedTags.map((tag) => tag.id)); - return [...orderedTags, ...tags.filter((tag) => !orderedIds.has(tag.id))]; -} +type CanvasSkillTagPointerDragState = { + pointerId: number; + tagId: string; + startX: number; + startY: number; + moved: boolean; +}; type ClawdComposerSubmissionSnapshot = { chatId: string; @@ -698,7 +707,7 @@ async function readClawdResponse( streamError = data && typeof data === "object" && "error" in data ? String((data as { error?: unknown }).error || "") - : CLAW_UPGRADE_RETRY_MESSAGE; + : OPENCLAW_UPGRADE_RETRY_MESSAGE; } }; @@ -712,7 +721,7 @@ async function readClawdResponse( } if (buffer.trim()) consumeBlock(buffer); if (streamError) throw new Error(streamError); - if (!finalPayload) throw new Error(CLAW_UPGRADE_RETRY_MESSAGE); + if (!finalPayload) throw new Error(OPENCLAW_UPGRADE_RETRY_MESSAGE); return finalPayload; } @@ -2556,6 +2565,7 @@ export function CanvasChatPanel({ workspaceVariant = "canvas", canvasToolbarConfig: rawCanvasToolbarConfig, canvasAgentFeatureSettings: propCanvasAgentFeatureSettings, + onRequireLogin, }: CanvasChatPanelProps) { const isWorkspacePresentation = presentation === "workspace"; const isVideoWorkspace = workspaceVariant === "video"; @@ -2567,7 +2577,7 @@ export function CanvasChatPanel({ normalizeCanvasAgentFeatureSettings(propCanvasAgentFeatureSettings) ); const effectiveAgentEnabled = agentEnabled && !isVideoWorkspace; - const { data: session } = useSession(); + const { data: session, status: sessionStatus } = useSession(); const searchParams = useSearchParams(); const requestedChatSessionId = useMemo( () => normalizeCanvasChatSessionId(searchParams.get(CANVAS_CHAT_QUERY_PARAM)), @@ -2662,7 +2672,9 @@ export function CanvasChatPanel({ null ); const pendingSelectedClawdSkillTagRef = useRef(null); - const draggingSkillTagIdRef = useRef(null); + const canvasSkillTagPointerDragRef = useRef( + null + ); const latestCanvasSkillTagsRef = useRef([]); const pendingCanvasSkillTagOrderSaveRef = useRef(null); const suppressSkillTagClickRef = useRef(false); @@ -4871,6 +4883,12 @@ export function CanvasChatPanel({ }, body: JSON.stringify({ tagIds: userOrder }), }); + if (response.status === 401) { + setCanvasSkillTagOrderOverride(null); + requestCanvasLogin(onRequireLogin, OPENCLAW_AUTH_REQUIRED_MESSAGE); + void mutate("/api/canvas-skill-tags"); + return; + } if (!response.ok) { throw new Error("Failed to save skill tag order"); } @@ -4883,7 +4901,7 @@ export function CanvasChatPanel({ void mutate("/api/canvas-skill-tags"); } }, - [baseCanvasSkillTags, canvasSkillTagsData, mutate] + [baseCanvasSkillTags, canvasSkillTagsData, mutate, onRequireLogin] ); const selectedSkillTag = useMemo( () => canvasSkillTags.find((tag) => tag.id === selectedSkillTagId) || null, @@ -6208,23 +6226,9 @@ export function CanvasChatPanel({ ] ); - const handleCanvasSkillTagDragStart = useCallback( - (event: ReactDragEvent, tagId: string) => { - draggingSkillTagIdRef.current = tagId; - pendingCanvasSkillTagOrderSaveRef.current = null; - setDraggingSkillTagId(tagId); - event.dataTransfer.effectAllowed = "move"; - event.dataTransfer.setData("text/plain", tagId); - }, - [] - ); - - const handleCanvasSkillTagDragEnter = useCallback( - (event: ReactDragEvent, targetId: string) => { - const draggedId = draggingSkillTagIdRef.current; + const updateCanvasSkillTagPointerOrder = useCallback( + (draggedId: string, targetId: string) => { if (!draggedId || draggedId === targetId) return; - - event.preventDefault(); const currentTags = latestCanvasSkillTagsRef.current; const nextTags = reorderCanvasSkillTags(currentTags, draggedId, targetId); if (nextTags === currentTags) return; @@ -6237,53 +6241,88 @@ export function CanvasChatPanel({ [] ); - const handleCanvasSkillTagDragOver = useCallback( - (event: ReactDragEvent, targetId: string) => { - const draggedId = draggingSkillTagIdRef.current; - if (!draggedId || draggedId === targetId) return; - event.preventDefault(); - event.dataTransfer.dropEffect = "move"; - }, - [] - ); - - const handleCanvasSkillTagDrop = useCallback( - (event: ReactDragEvent, targetId: string) => { - event.preventDefault(); - const draggedId = - event.dataTransfer.getData("text/plain") || draggingSkillTagIdRef.current; - if (draggedId && draggedId !== targetId && !pendingCanvasSkillTagOrderSaveRef.current) { - const nextTags = reorderCanvasSkillTags( - latestCanvasSkillTagsRef.current, - draggedId, - targetId - ); - if (nextTags !== latestCanvasSkillTagsRef.current) { - const nextOrder = nextTags.map((tag) => tag.id); - pendingCanvasSkillTagOrderSaveRef.current = nextOrder; - latestCanvasSkillTagsRef.current = nextTags; - setCanvasSkillTagOrderOverride(nextOrder); - } + const handleCanvasSkillTagPointerDown = useCallback( + (event: ReactPointerEvent, tagId: string) => { + if ( + isCanvasSkillTagSearching || + canvasSkillTags.length < 2 || + event.button !== 0 + ) { + return; + } + if (sessionStatus === "loading") { + return; + } + if (!session?.user?.id) { + requestCanvasLogin(onRequireLogin, OPENCLAW_AUTH_REQUIRED_MESSAGE); + return; } - suppressSkillTagClickRef.current = true; - window.setTimeout(() => { - suppressSkillTagClickRef.current = false; - }, 0); + canvasSkillTagPointerDragRef.current = { + pointerId: event.pointerId, + tagId, + startX: event.clientX, + startY: event.clientY, + moved: false, + }; + pendingCanvasSkillTagOrderSaveRef.current = null; + event.currentTarget.setPointerCapture(event.pointerId); }, - [] + [ + canvasSkillTags.length, + isCanvasSkillTagSearching, + onRequireLogin, + session?.user?.id, + sessionStatus, + ] ); - const handleCanvasSkillTagDragEnd = useCallback(() => { + const handleCanvasSkillTagPointerMove = useCallback( + (event: ReactPointerEvent) => { + const dragState = canvasSkillTagPointerDragRef.current; + if (!dragState || dragState.pointerId !== event.pointerId) return; + + if (!dragState.moved) { + const distance = Math.hypot( + event.clientX - dragState.startX, + event.clientY - dragState.startY + ); + if (distance < CANVAS_SKILL_TAG_DRAG_THRESHOLD_PX) return; + + dragState.moved = true; + setDraggingSkillTagId(dragState.tagId); + } + + event.preventDefault(); + const target = document + .elementFromPoint(event.clientX, event.clientY) + ?.closest("[data-canvas-skill-tag-id]"); + const targetId = String(target?.dataset.canvasSkillTagId || "").trim(); + updateCanvasSkillTagPointerOrder(dragState.tagId, targetId); + }, + [updateCanvasSkillTagPointerOrder] + ); + + const handleCanvasSkillTagPointerEnd = useCallback(( + event: ReactPointerEvent + ) => { + const dragState = canvasSkillTagPointerDragRef.current; + if (!dragState || dragState.pointerId !== event.pointerId) return; + const pendingOrder = pendingCanvasSkillTagOrderSaveRef.current; pendingCanvasSkillTagOrderSaveRef.current = null; - draggingSkillTagIdRef.current = null; + canvasSkillTagPointerDragRef.current = null; setDraggingSkillTagId(null); - if (pendingOrder?.length) { + if (event.currentTarget.hasPointerCapture(event.pointerId)) { + event.currentTarget.releasePointerCapture(event.pointerId); + } + if (dragState.moved) { suppressSkillTagClickRef.current = true; window.setTimeout(() => { suppressSkillTagClickRef.current = false; }, 0); + } + if (dragState.moved && pendingOrder?.length) { void persistCanvasSkillTagOrder(pendingOrder); } }, [persistCanvasSkillTagOrder]); @@ -11309,6 +11348,15 @@ export function CanvasChatPanel({ } if (isClawdMode) { + if (sessionStatus === "loading") { + toast.message("正在确认登录状态,请稍后再试"); + return; + } + if (!session?.user?.id) { + requestCanvasLogin(onRequireLogin, OPENCLAW_AUTH_REQUIRED_MESSAGE); + return; + } + const isProgrammaticClawdQuickSend = hasOverrideInput; const textContent = normalizeClawdComposerText(composerInput); const tableFilesSnapshot = isProgrammaticClawdQuickSend ? [] : [...clawdTableFiles]; @@ -11547,6 +11595,8 @@ export function CanvasChatPanel({ clearSelection(); } + let clawdAuthRequired = false; + let clawdFailureMessage = OPENCLAW_UPGRADE_RETRY_MESSAGE; try { const formData = new FormData(); formData.append("id", rawChatId); @@ -11596,7 +11646,15 @@ export function CanvasChatPanel({ status: response.status, responseData, }); - throw new Error(CLAW_UPGRADE_RETRY_MESSAGE); + clawdAuthRequired = isOpenClawAuthRequiredError( + response.status, + responseData + ); + clawdFailureMessage = getOpenClawClientErrorMessage( + response.status, + responseData + ); + throw new Error(clawdFailureMessage); } if (clawdSkillTagPrompt && clawdSkillTagForRequest) { @@ -11655,10 +11713,21 @@ export function CanvasChatPanel({ return; } console.error("[CanvasChat] Claw request failed:", error); + if (clawdAuthRequired) { + setMessages((previousMessages) => + previousMessages.filter((message) => message.id !== userMessageId) + ); + if (pendingClawdRequestRestoreRef.current === clawdRequestRestoreSnapshot) { + pendingClawdRequestRestoreRef.current = null; + restorePendingClawdRequestToComposer(clawdRequestRestoreSnapshot); + } + requestCanvasLogin(onRequireLogin, OPENCLAW_AUTH_REQUIRED_MESSAGE); + return; + } toast.error( isClawdTransportInterruption(error) ? "连接已中断,任务仍可能在后台处理,请稍后查看 Claw 历史" - : CLAW_UPGRADE_RETRY_MESSAGE + : clawdFailureMessage ); } finally { pendingClawdSubmissionRef.current = null; @@ -12071,6 +12140,9 @@ export function CanvasChatPanel({ restorePendingClawdRequestToComposer, workflowContext, messages, + onRequireLogin, + session?.user?.id, + sessionStatus, ]); const handleOpenClawQuickAction = useCallback( @@ -14454,24 +14526,19 @@ export function CanvasChatPanel({