fix(canvas): open login dialog when Claw session expires
This commit is contained in:
parent
4b53f4056a
commit
39e8107f23
@ -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));
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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 }
|
||||
);
|
||||
}
|
||||
|
||||
@ -10997,6 +10997,7 @@ export default function CanvasEditorLegacyPage({
|
||||
chatScope="canvas"
|
||||
onClose={() => setIsPanelOpen(false)}
|
||||
onOpenFileList={() => setPanelMode("fileList")}
|
||||
onRequireLogin={openLoginDialog}
|
||||
selectedAgentPresetId={
|
||||
isCanvasAgentModeEnabled ? selectedAgentPresetId : null
|
||||
}
|
||||
|
||||
@ -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(
|
||||
|
||||
49
lib/openclaw-chat/errors.ts
Normal file
49
lib/openclaw-chat/errors.ts
Normal 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))
|
||||
);
|
||||
}
|
||||
@ -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,
|
||||
|
||||
59
tests/routes/openclaw-client-errors.test.ts
Normal file
59
tests/routes/openclaw-client-errors.test.ts
Normal 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
|
||||
);
|
||||
});
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user