From ebfc7589a99b8db89770b8bec727c00b63fdc065 Mon Sep 17 00:00:00 2001 From: 134 Date: Tue, 7 Jul 2026 22:26:04 +0800 Subject: [PATCH] feat: add global claw chat history --- app/api/canvas/clawd-chat/history/route.ts | 42 +++ components/canvas/CanvasChatPanel.tsx | 321 +++++++++++++++++---- lib/db/queries.ts | 38 +++ 3 files changed, 341 insertions(+), 60 deletions(-) create mode 100644 app/api/canvas/clawd-chat/history/route.ts diff --git a/app/api/canvas/clawd-chat/history/route.ts b/app/api/canvas/clawd-chat/history/route.ts new file mode 100644 index 000000000..822587749 --- /dev/null +++ b/app/api/canvas/clawd-chat/history/route.ts @@ -0,0 +1,42 @@ +import { NextResponse } from "next/server"; +import { auth } from "@/app/(auth)/auth"; +import { getClawdChatsByUserId } from "@/lib/db/queries"; + +export const dynamic = "force-dynamic"; +export const revalidate = 0; + +export async function GET(request: Request) { + const session = await auth().catch(() => null); + + if (!session?.user?.id) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const url = new URL(request.url); + const limit = Math.max( + 1, + Math.min(100, Math.round(Number(url.searchParams.get("limit") || 50))) + ); + + const chats = await getClawdChatsByUserId({ + userId: session.user.id, + limit, + }); + + return NextResponse.json( + { + chats: chats.map((chat) => ({ + id: chat.id, + title: chat.title, + createdAt: chat.createdAt?.toISOString?.() || chat.createdAt, + updatedAt: chat.updatedAt?.toISOString?.() || chat.updatedAt, + messageCount: Number(chat.messageCount || 0), + })), + }, + { + headers: { + "Cache-Control": "no-store, max-age=0", + }, + } + ); +} diff --git a/components/canvas/CanvasChatPanel.tsx b/components/canvas/CanvasChatPanel.tsx index 292dc228d..24e6b11e8 100644 --- a/components/canvas/CanvasChatPanel.tsx +++ b/components/canvas/CanvasChatPanel.tsx @@ -1971,6 +1971,14 @@ interface CanvasChatSession { clawdSkillTagPromptApplied?: boolean; } +interface ClawdHistoryItem { + id: string; + title: string; + createdAt: string | Date; + updatedAt: string | Date; + messageCount: number; +} + const CANVAS_CHAT_QUERY_PARAM = "chat"; const CANVAS_IMAGE_NODE_MANUAL_UPLOAD_EVENT = "canvas:image-node-manual-upload"; @@ -2194,6 +2202,7 @@ export function CanvasChatPanel({ const inFlightRawChatIdRef = useRef(null); const [chatSessions, setChatSessions] = useState([]); const [isSessionMenuOpen, setIsSessionMenuOpen] = useState(false); + const [isClawdHistoryMenuOpen, setIsClawdHistoryMenuOpen] = useState(false); const [isComposerModeMenuOpen, setIsComposerModeMenuOpen] = useState(false); const [selectedSkillTagId, setSelectedSkillTagId] = useState(null); const [areCanvasSkillTagsExpanded, setAreCanvasSkillTagsExpanded] = useState(false); @@ -2239,6 +2248,7 @@ export function CanvasChatPanel({ ); const hasInitializedVideoWorkspaceRef = useRef(false); const sessionMenuRef = useRef(null); + const clawdHistoryMenuRef = useRef(null); const historyScrollTimerRef = useRef | null>(null); const forceHistoryHydrationRef = useRef(null); const copiedUserMessageRef = useRef(null); @@ -2963,6 +2973,17 @@ export function CanvasChatPanel({ return () => document.removeEventListener("mousedown", handleClickOutside); }, [isSessionMenuOpen]); + useEffect(() => { + if (!isClawdHistoryMenuOpen) return; + const handleClickOutside = (event: MouseEvent) => { + if (!clawdHistoryMenuRef.current?.contains(event.target as Node)) { + setIsClawdHistoryMenuOpen(false); + } + }; + document.addEventListener("mousedown", handleClickOutside); + return () => document.removeEventListener("mousedown", handleClickOutside); + }, [isClawdHistoryMenuOpen]); + useEffect(() => { const sessionStorageKey = getSessionStorageKey(canvasId, chatScope); if (loadedChatSessionStorageKey !== sessionStorageKey) return; @@ -4545,6 +4566,19 @@ export function CanvasChatPanel({ activeChatSession, activeComposerMode ); + const clawdHistorySWRKey = + isClawdMode && isClawdHistoryMenuOpen + ? "/api/canvas/clawd-chat/history?limit=80" + : null; + const { + data: clawdHistoryData, + isLoading: isClawdHistoryLoading, + mutate: mutateClawdHistory, + } = useSWR<{ chats: ClawdHistoryItem[] }>(clawdHistorySWRKey, fetcher, { + revalidateOnFocus: false, + revalidateOnReconnect: false, + dedupingInterval: 5000, + }); useEffect(() => { if (activeChatSessionMode !== "clawd") return; if (!isClawdModeEnabled) return; @@ -10785,6 +10819,7 @@ export function CanvasChatPanel({ if (rawChatId) { void mutate(buildHistoryMessagesUrl(rawChatId, { cacheBust: true })); } + void mutateClawdHistory(); hasHydratedHistoryRef.current = true; } catch (error) { console.error("[CanvasChat] Claw request failed:", error); @@ -11203,6 +11238,7 @@ export function CanvasChatPanel({ shouldBlockDuplicateSend, status, mutate, + mutateClawdHistory, markSessionClawdSkillTagPromptApplied, updateChatSessions, buildHistoryMessagesUrl, @@ -12598,6 +12634,70 @@ export function CanvasChatPanel({ toast.success("已切换聊天窗口"); }, [activeChatId, releaseSendGuard, replaceChatSessionQuery]); + const switchToClawdHistorySession = useCallback( + (historyItem: ClawdHistoryItem) => { + const sessionId = normalizeCanvasChatSessionId(historyItem.id); + if (!sessionId) return; + + const updatedAt = Date.parse(String(historyItem.updatedAt || "")) || Date.now(); + const createdAt = Date.parse(String(historyItem.createdAt || "")) || updatedAt; + const title = String(historyItem.title || "").trim() || `${CLAW_MODE_LABEL} 对话`; + + updateChatSessions((prevSessions) => { + const currentIndex = prevSessions.findIndex( + (session) => session.id === sessionId + ); + + if (currentIndex < 0) { + return [ + { + id: sessionId, + title, + mode: "clawd", + createdAt, + updatedAt, + }, + ...prevSessions, + ]; + } + + const nextSessions = [...prevSessions]; + nextSessions[currentIndex] = { + ...nextSessions[currentIndex], + title, + mode: "clawd", + updatedAt: Math.max( + Number(nextSessions[currentIndex]?.updatedAt) || 0, + updatedAt + ), + }; + nextSessions.sort((a, b) => b.updatedAt - a.updatedAt); + return nextSessions; + }); + + forceHistoryHydrationRef.current = toRawCanvasChatId(sessionId); + releaseSendGuard(activeChatId); + setActiveChatId(sessionId); + setComposerModeOverride("clawd"); + setIsDetailGenerationMode(false); + replaceChatSessionQuery(sessionId); + setIsClawdHistoryMenuOpen(false); + toast.success("已打开 Claw 历史对话"); + }, + [activeChatId, releaseSendGuard, replaceChatSessionQuery, updateChatSessions] + ); + + const formatClawdHistoryTime = useCallback((value: string | Date) => { + const timestamp = Date.parse(String(value || "")); + if (!Number.isFinite(timestamp)) return ""; + return new Date(timestamp).toLocaleString("zh-CN", { + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + }); + }, []); + const activeSessionTitle = useMemo(() => { const current = chatSessions.find((session) => session.id === activeChatId); return String(current?.title || "").trim(); @@ -12726,72 +12826,173 @@ export function CanvasChatPanel({ > -
- - {isSessionMenuOpen && ( -
+ - ); - }) + + Claw + + {isClawdHistoryMenuOpen && ( +
+
+
+ Claw 对话历史 +
+ +
+
+ {isClawdHistoryLoading ? ( +
+ + 正在加载 Claw 历史 +
+ ) : (clawdHistoryData?.chats || []).length === 0 ? ( +
+ 暂无 Claw 历史 +
+ ) : ( + (clawdHistoryData?.chats || []).map((item) => { + const sessionId = normalizeCanvasChatSessionId(item.id); + const isActive = sessionId === activeChatId; + const timeLabel = formatClawdHistoryTime(item.updatedAt); + return ( + + ); + }) + )} +
-
- )} -
+ )} + + ) : ( +
+ + {isSessionMenuOpen && ( +
+
聊天窗口记录
+
+ {currentModeChatSessions.length === 0 ? ( +
暂无记录
+ ) : ( + currentModeChatSessions.map((session, index) => { + const isActive = session.id === activeChatId; + const timeLabel = new Date(session.updatedAt).toLocaleTimeString([], { + hour: "2-digit", + minute: "2-digit", + }); + return ( + + ); + }) + )} +
+
+ )} +
+ )}