fix: allow scoped canvas chat history empty state
This commit is contained in:
parent
de457b5dc3
commit
16ebbeb6ac
@ -18,6 +18,12 @@ import {
|
||||
getFalImageTaskStatus,
|
||||
resolveFalImageTaskToMarkdown,
|
||||
} from "@/lib/ai/providers/fal-image";
|
||||
import {
|
||||
isAgentModeScopedChatId,
|
||||
isScopedEmptyHistoryRequest,
|
||||
isValidMessagesChatId,
|
||||
normalizeMessagesChatId,
|
||||
} from "@/lib/chat/messages-route-access";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
export const revalidate = 0;
|
||||
@ -25,19 +31,6 @@ export const revalidate = 0;
|
||||
const FAL_IMAGE_TASK_POLL_INTERVAL_MS = 2_000;
|
||||
const falTaskLastPolledAt = new Map<string, number>();
|
||||
const falTaskSyncInFlight = new Set<string>();
|
||||
const AGENT_MODE_SCOPED_CHAT_SUFFIX = "a93e17a60001";
|
||||
|
||||
function isAgentModeScopedChatId(chatId: string) {
|
||||
const normalized = String(chatId || "")
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/-/g, "");
|
||||
|
||||
return (
|
||||
/^[0-9a-f]{32}$/.test(normalized) &&
|
||||
normalized.endsWith(AGENT_MODE_SCOPED_CHAT_SUFFIX)
|
||||
);
|
||||
}
|
||||
|
||||
function createEmptyMessagesResponse() {
|
||||
return NextResponse.json(
|
||||
@ -226,18 +219,27 @@ export async function GET(request: NextRequest) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
const chatId = request.nextUrl.searchParams.get("chatId");
|
||||
const requestedChatId = request.nextUrl.searchParams.get("chatId");
|
||||
|
||||
if (!chatId) {
|
||||
if (!requestedChatId) {
|
||||
return NextResponse.json(
|
||||
{ error: "Missing required parameter: chatId" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const chatId = normalizeMessagesChatId(requestedChatId);
|
||||
if (!isValidMessagesChatId(chatId)) {
|
||||
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
|
||||
}
|
||||
|
||||
const scope = request.nextUrl.searchParams.get("scope");
|
||||
const selectedChat = await getChatById({ id: chatId });
|
||||
if (!selectedChat) {
|
||||
if (isAgentModeScopedChatId(chatId)) {
|
||||
if (
|
||||
isAgentModeScopedChatId(chatId) ||
|
||||
isScopedEmptyHistoryRequest({ chatId, scope })
|
||||
) {
|
||||
return createEmptyMessagesResponse();
|
||||
}
|
||||
|
||||
|
||||
@ -1677,6 +1677,23 @@ export function CanvasChatPanel({
|
||||
|
||||
// 获取纯 UUID(去掉 canvas- 前缀)用于 API 调用
|
||||
const rawChatId = toRawCanvasChatId(activeChatId);
|
||||
const buildHistoryMessagesUrl = useCallback(
|
||||
(targetRawChatId: string, options?: { cacheBust?: boolean }) => {
|
||||
const params = new URLSearchParams({
|
||||
chatId: targetRawChatId,
|
||||
scope: chatScope,
|
||||
});
|
||||
if (options?.cacheBust) {
|
||||
params.set("_t", String(Date.now()));
|
||||
}
|
||||
return `/api/messages?${params.toString()}`;
|
||||
},
|
||||
[chatScope]
|
||||
);
|
||||
const historyMessagesSWRKey = useMemo(
|
||||
() => (rawChatId ? buildHistoryMessagesUrl(rawChatId) : null),
|
||||
[buildHistoryMessagesUrl, rawChatId]
|
||||
);
|
||||
const { mutate } = useSWRConfig();
|
||||
const [votingMessageId, setVotingMessageId] = useState<string | null>(null);
|
||||
const handleDownloadOriginalImage = useCallback(async (imageUrl: string) => {
|
||||
@ -1718,7 +1735,7 @@ export function CanvasChatPanel({
|
||||
// 从 API 加载历史消息
|
||||
const { data: historyData, mutate: mutateHistoryData } = useSWR<{
|
||||
messages: ChatMessage[];
|
||||
}>(rawChatId ? `/api/messages?chatId=${rawChatId}` : null, fetcher, {
|
||||
}>(historyMessagesSWRKey, fetcher, {
|
||||
revalidateOnFocus: false,
|
||||
revalidateOnReconnect: false,
|
||||
dedupingInterval: 10000,
|
||||
@ -4779,7 +4796,7 @@ export function CanvasChatPanel({
|
||||
|
||||
const syncMessagesFromDatabase = (dbMessages: ChatMessage[]) => {
|
||||
if (finishedChatId !== activeChatIdRef.current) {
|
||||
void mutate(`/api/messages?chatId=${finishedRawChatId}`);
|
||||
void mutate(buildHistoryMessagesUrl(finishedRawChatId));
|
||||
return;
|
||||
}
|
||||
|
||||
@ -4798,9 +4815,12 @@ export function CanvasChatPanel({
|
||||
};
|
||||
|
||||
const reloadMessagesOnce = async () => {
|
||||
const response = await fetch(`/api/messages?chatId=${finishedRawChatId}&_t=${Date.now()}`, {
|
||||
cache: "no-store",
|
||||
});
|
||||
const response = await fetch(
|
||||
buildHistoryMessagesUrl(finishedRawChatId, { cacheBust: true }),
|
||||
{
|
||||
cache: "no-store",
|
||||
}
|
||||
);
|
||||
if (!response.ok) {
|
||||
return { hasResult: false, hasError: false };
|
||||
}
|
||||
|
||||
42
lib/chat/messages-route-access.ts
Normal file
42
lib/chat/messages-route-access.ts
Normal file
@ -0,0 +1,42 @@
|
||||
const AGENT_MODE_SCOPED_CHAT_SUFFIX = "a93e17a60001";
|
||||
const CHAT_UUID_REGEX =
|
||||
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
||||
const EMPTY_HISTORY_SCOPES = new Set(["canvas", "agent"]);
|
||||
|
||||
export function normalizeMessagesChatId(chatId: string) {
|
||||
const normalized = String(chatId || "").trim();
|
||||
const canvasPrefix = "canvas-";
|
||||
if (normalized.toLowerCase().startsWith(canvasPrefix)) {
|
||||
const rawCanvasId = normalized.slice(canvasPrefix.length);
|
||||
return CHAT_UUID_REGEX.test(rawCanvasId) ? rawCanvasId : normalized;
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
export function isValidMessagesChatId(chatId: string) {
|
||||
return CHAT_UUID_REGEX.test(String(chatId || "").trim());
|
||||
}
|
||||
|
||||
export function isAgentModeScopedChatId(chatId: string) {
|
||||
const normalized = String(chatId || "")
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/-/g, "");
|
||||
|
||||
return (
|
||||
/^[0-9a-f]{32}$/.test(normalized) &&
|
||||
normalized.endsWith(AGENT_MODE_SCOPED_CHAT_SUFFIX)
|
||||
);
|
||||
}
|
||||
|
||||
export function isScopedEmptyHistoryRequest(params: {
|
||||
chatId: string;
|
||||
scope?: string | null;
|
||||
}) {
|
||||
const scope = String(params.scope || "")
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
return (
|
||||
EMPTY_HISTORY_SCOPES.has(scope) && isValidMessagesChatId(params.chatId)
|
||||
);
|
||||
}
|
||||
@ -27,6 +27,12 @@ import {
|
||||
} from "@/lib/canvas-toolbar-config";
|
||||
import { shouldBypassCanvasAgentMissingTargetClarification } from "@/lib/ai/runtime/canvas-agent-clarification";
|
||||
import { hasCanvasAgentRoutingContext } from "@/lib/ai/canvas-agent-routing";
|
||||
import {
|
||||
isAgentModeScopedChatId,
|
||||
isScopedEmptyHistoryRequest,
|
||||
isValidMessagesChatId,
|
||||
normalizeMessagesChatId,
|
||||
} from "@/lib/chat/messages-route-access";
|
||||
import {
|
||||
CANVAS_LOCAL_EDIT_REGION_ANNOTATION_SOURCE,
|
||||
CANVAS_NODE_NOTE_ANNOTATION_SOURCE,
|
||||
@ -131,6 +137,35 @@ import type { CanvasAgentContext } from "@/types/canvas/agent-protocol";
|
||||
import type { ChatMessage } from "@/lib/types";
|
||||
|
||||
test.describe("canvas chat pure helpers", () => {
|
||||
test("keeps canvas message empty-history access scoped and explicit", () => {
|
||||
const canvasChatId = "11111111-1111-4111-8111-111111111111";
|
||||
const agentScopedChatId = "11111111-1111-4111-8111-a93e17a60001";
|
||||
|
||||
expect(normalizeMessagesChatId(`canvas-${canvasChatId}`)).toBe(
|
||||
canvasChatId
|
||||
);
|
||||
expect(normalizeMessagesChatId("canvas-not-a-uuid")).toBe(
|
||||
"canvas-not-a-uuid"
|
||||
);
|
||||
expect(isValidMessagesChatId(canvasChatId)).toBe(true);
|
||||
expect(isValidMessagesChatId("canvas-not-a-uuid")).toBe(false);
|
||||
expect(isScopedEmptyHistoryRequest({ chatId: canvasChatId })).toBe(false);
|
||||
expect(
|
||||
isScopedEmptyHistoryRequest({ chatId: canvasChatId, scope: "canvas" })
|
||||
).toBe(true);
|
||||
expect(
|
||||
isScopedEmptyHistoryRequest({ chatId: canvasChatId, scope: " AGENT " })
|
||||
).toBe(true);
|
||||
expect(
|
||||
isScopedEmptyHistoryRequest({
|
||||
chatId: "canvas-not-a-uuid",
|
||||
scope: "canvas",
|
||||
})
|
||||
).toBe(false);
|
||||
expect(isAgentModeScopedChatId(agentScopedChatId)).toBe(true);
|
||||
expect(isAgentModeScopedChatId(canvasChatId)).toBe(false);
|
||||
});
|
||||
|
||||
test("rejects stale autosave bases without blocking fresh or missing revisions", () => {
|
||||
const currentUpdatedAt = new Date("2026-06-22T00:00:05.500Z");
|
||||
|
||||
|
||||
@ -39,6 +39,37 @@ test.describe
|
||||
expect(message).toEqual(getMessageByErrorCode("bad_request:api"));
|
||||
});
|
||||
|
||||
test("Ada can load an empty canvas-scoped message history", async ({
|
||||
adaContext,
|
||||
}) => {
|
||||
const chatId = generateUUID();
|
||||
|
||||
const response = await adaContext.request.get(
|
||||
`/api/messages?chatId=${chatId}&scope=canvas`
|
||||
);
|
||||
|
||||
expect(response.status()).toBe(200);
|
||||
expect(await response.json()).toEqual({ messages: [] });
|
||||
|
||||
const prefixedResponse = await adaContext.request.get(
|
||||
`/api/messages?chatId=canvas-${generateUUID()}&scope=canvas`
|
||||
);
|
||||
|
||||
expect(prefixedResponse.status()).toBe(200);
|
||||
expect(await prefixedResponse.json()).toEqual({ messages: [] });
|
||||
});
|
||||
|
||||
test("Ada cannot load missing unscoped message history", async ({
|
||||
adaContext,
|
||||
}) => {
|
||||
const response = await adaContext.request.get(
|
||||
`/api/messages?chatId=${generateUUID()}`
|
||||
);
|
||||
|
||||
expect(response.status()).toBe(403);
|
||||
expect(await response.json()).toEqual({ error: "Forbidden" });
|
||||
});
|
||||
|
||||
test("Ada can invoke chat generation", async ({ adaContext }) => {
|
||||
const chatId = generateUUID();
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user