fix(canvas): open login dialog when Claw session expires

This commit is contained in:
134 2026-08-04 23:54:50 +08:00
parent 4b53f4056a
commit 39e8107f23
8 changed files with 212 additions and 19 deletions

View File

@ -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));

View File

@ -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);

View File

@ -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,
cancelClawdRequest,
@ -33,8 +38,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(""),
@ -636,7 +639,7 @@ function streamOpenClawBackgroundTask(params: {
wasCanceled ? "final" : "error",
wasCanceled
? { canceled: true }
: { error: CLAW_UPGRADE_RETRY_MESSAGE }
: { error: OPENCLAW_UPGRADE_RETRY_MESSAGE }
)
)
);
@ -665,7 +668,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);
@ -685,11 +694,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 }
);
}
@ -906,7 +918,7 @@ export async function POST(request: Request) {
streamController.enqueue(
encoder.encode(
encodeSseEvent("error", {
error: CLAW_UPGRADE_RETRY_MESSAGE,
error: OPENCLAW_UPGRADE_RETRY_MESSAGE,
})
)
);
@ -917,7 +929,7 @@ export async function POST(request: Request) {
streamController.enqueue(
encoder.encode(
encodeSseEvent("error", {
error: CLAW_UPGRADE_RETRY_MESSAGE,
error: OPENCLAW_UPGRADE_RETRY_MESSAGE,
})
)
);
@ -952,7 +964,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 }
@ -1000,7 +1012,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 }
);
@ -1014,7 +1026,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 }
);
}

View File

@ -10997,6 +10997,7 @@ export default function CanvasEditorLegacyPage({
chatScope="canvas"
onClose={() => setIsPanelOpen(false)}
onOpenFileList={() => setPanelMode("fileList")}
onRequireLogin={openLoginDialog}
selectedAgentPresetId={
isCanvasAgentModeEnabled ? selectedAgentPresetId : null
}

View File

@ -87,6 +87,12 @@ import {
normalizeVideoCanvasStoryboardReference,
type VideoCanvasStoryboardReferenceRequest,
} from "@/lib/video-canvas/storyboard-reference";
import {
getOpenClawClientErrorMessage,
isOpenClawAuthRequiredError,
OPENCLAW_AUTH_REQUIRED_MESSAGE,
OPENCLAW_UPGRADE_RETRY_MESSAGE,
} from "@/lib/openclaw-chat/errors";
import type {
AgentPreset,
AgentWorkflowComposerContext,
@ -344,6 +350,7 @@ interface CanvasChatPanelProps {
canvasAgentFeatureSettings?: CanvasAgentFeatureSettings;
composerReferenceRequest?: VideoCanvasStoryboardReferenceRequest | null;
onComposerReferenceHandled?: (requestId: string) => void;
onRequireLogin?: (message?: string) => void;
onStoryboardReferenceActivate?: (
nodeId: string,
mediaType: "audio" | "image" | "text" | "video",
@ -353,7 +360,6 @@ interface CanvasChatPanelProps {
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;
@ -371,6 +377,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 "视频";
@ -721,7 +745,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;
}
};
@ -735,7 +759,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;
}
@ -2617,6 +2641,7 @@ export function CanvasChatPanel({
canvasAgentFeatureSettings: propCanvasAgentFeatureSettings,
composerReferenceRequest = null,
onComposerReferenceHandled,
onRequireLogin,
onStoryboardReferenceActivate,
}: CanvasChatPanelProps) {
const isWorkspacePresentation = presentation === "workspace";
@ -2629,7 +2654,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)),
@ -11615,6 +11640,15 @@ export function CanvasChatPanel({
}
if (isClawdMode) {
if (sessionStatus === "loading") {
toast.message("正在确认登录状态,请稍后再试");
return;
}
if (!session?.user?.id) {
requestCanvasLogin(onRequireLogin, OPENCLAW_AUTH_REQUIRED_MESSAGE);
return;
}
const requestChatId = activeChatId;
const requestRawChatId = rawChatId;
const isProgrammaticClawdQuickSend = hasOverrideInput;
@ -11861,6 +11895,8 @@ export function CanvasChatPanel({
}
let clawdRequestAccepted = false;
let clawdAuthRequired = false;
let clawdFailureMessage = OPENCLAW_UPGRADE_RETRY_MESSAGE;
try {
const formData = new FormData();
formData.append("id", requestRawChatId);
@ -11930,7 +11966,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) {
@ -12021,7 +12065,11 @@ export function CanvasChatPanel({
previousMessages.filter((message) => message.id !== userMessageId)
);
}
toast.error(CLAW_UPGRADE_RETRY_MESSAGE);
if (clawdAuthRequired) {
requestCanvasLogin(onRequireLogin, OPENCLAW_AUTH_REQUIRED_MESSAGE);
} else {
toast.error(clawdFailureMessage);
}
if (
!isProgrammaticClawdQuickSend &&
activeChatIdRef.current === requestChatId
@ -12512,6 +12560,9 @@ export function CanvasChatPanel({
clearClawdRequestUi,
workflowContext,
messages,
onRequireLogin,
session?.user?.id,
sessionStatus,
]);
const handleOpenClawQuickAction = useCallback(

View File

@ -0,0 +1,49 @@
export const OPENCLAW_UPGRADE_RETRY_MESSAGE =
"claw正在升级更新稍等5分钟再试";
export const OPENCLAW_AUTH_REQUIRED_CODE = "AUTH_REQUIRED";
export const OPENCLAW_AUTH_REQUIRED_MESSAGE =
"登录状态已失效,请重新登录后使用 Claw";
export const OPENCLAW_FORBIDDEN_MESSAGE = "当前账号无权访问该 Claw 对话";
function readPayloadString(payload: unknown, key: "code" | "error") {
if (!payload || typeof payload !== "object" || !(key in payload)) {
return "";
}
const value = (payload as Record<string, unknown>)[key];
return typeof value === "string" ? value.trim() : "";
}
export function getOpenClawClientErrorMessage(
status: number,
payload: unknown
) {
const error = readPayloadString(payload, "error");
if (isOpenClawAuthRequiredError(status, payload)) {
return OPENCLAW_AUTH_REQUIRED_MESSAGE;
}
if (status === 403) {
return OPENCLAW_FORBIDDEN_MESSAGE;
}
if (status >= 400 && status < 500 && status !== 401 && error) {
return error;
}
return OPENCLAW_UPGRADE_RETRY_MESSAGE;
}
export function isOpenClawAuthRequiredError(
status: number,
payload: unknown
) {
const code = readPayloadString(payload, "code");
const error = readPayloadString(payload, "error");
return (
status === 401 &&
(code === OPENCLAW_AUTH_REQUIRED_CODE || /^Unauthorized$/i.test(error))
);
}

View File

@ -45,6 +45,7 @@ export default defineConfig({
/routes\/video-mark-prompt\.test\.ts/,
/routes\/video-subtitle-erase-tools\.test\.ts/,
/routes\/home-sidebar-auth\.test\.ts/,
/routes\/openclaw-client-errors\.test\.ts/,
/routes\/openclaw-video-attachments\.test\.ts/,
],
fullyParallel: true,

View File

@ -0,0 +1,59 @@
import { expect, test } from "@playwright/test";
import {
getOpenClawClientErrorMessage,
isOpenClawAuthRequiredError,
OPENCLAW_AUTH_REQUIRED_CODE,
OPENCLAW_AUTH_REQUIRED_MESSAGE,
OPENCLAW_FORBIDDEN_MESSAGE,
OPENCLAW_UPGRADE_RETRY_MESSAGE,
} from "@/lib/openclaw-chat/errors";
test.describe("OpenClaw client error messages", () => {
test("reports a signed-out website session as a login error", () => {
const response = {
code: OPENCLAW_AUTH_REQUIRED_CODE,
error: OPENCLAW_AUTH_REQUIRED_MESSAGE,
};
expect(getOpenClawClientErrorMessage(401, response)).toBe(
OPENCLAW_AUTH_REQUIRED_MESSAGE
);
expect(isOpenClawAuthRequiredError(401, response)).toBe(true);
});
test("supports the legacy website unauthorized response", () => {
expect(
getOpenClawClientErrorMessage(401, { error: "Unauthorized" })
).toBe(OPENCLAW_AUTH_REQUIRED_MESSAGE);
});
test("does not confuse an upstream 401 with a signed-out website session", () => {
const response = {
code: "UPSTREAM_AUTH_ERROR",
error: OPENCLAW_UPGRADE_RETRY_MESSAGE,
};
expect(getOpenClawClientErrorMessage(401, response)).toBe(
OPENCLAW_UPGRADE_RETRY_MESSAGE
);
expect(isOpenClawAuthRequiredError(401, response)).toBe(false);
});
test("reports a forbidden conversation separately", () => {
expect(getOpenClawClientErrorMessage(403, { error: "Forbidden" })).toBe(
OPENCLAW_FORBIDDEN_MESSAGE
);
});
test("preserves actionable client validation errors", () => {
expect(
getOpenClawClientErrorMessage(400, { error: "请输入问题或上传附件" })
).toBe("请输入问题或上传附件");
});
test("uses the upgrade message for service failures", () => {
expect(getOpenClawClientErrorMessage(503, null)).toBe(
OPENCLAW_UPGRADE_RETRY_MESSAGE
);
});
});