From 344aee93fa02b04897dc2e94d2c2f368270db264 Mon Sep 17 00:00:00 2001 From: steven_li Date: Mon, 16 Mar 2026 17:37:25 +0800 Subject: [PATCH] =?UTF-8?q?feat(nanobot-web):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E4=BC=9A=E8=AF=9D=E5=88=9B=E5=BB=BA=E6=8E=A5=E5=8F=A3=E5=B9=B6?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=89=8D=E7=AB=AF=E4=BC=9A=E8=AF=9D=E7=AE=A1?= =?UTF-8?q?=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 POST /api/sessions/{key} 接口用于立即创建或持久化会话 - 提取 _serialize_session_detail 函数以统一会话数据序列化逻辑 - 前端添加 createSession API 调用函数 - 实现本地存储中的会话ID持久化功能 - 优化 ChatPage 组件中的会话切换逻辑,确保状态正确重置 - 在消息处理中添加会话ID验证,避免跨会话消息混乱 - 新建会话时主动调用创建API并刷新会话列表 --- app-instance/backend/nanobot/web/server.py | 22 +++++++++++++++----- app-instance/frontend/app/(app)/page.tsx | 21 +++++++++++++++++-- app-instance/frontend/lib/api.ts | 4 ++++ app-instance/frontend/lib/store.ts | 24 ++++++++++++++++++++-- 4 files changed, 62 insertions(+), 9 deletions(-) diff --git a/app-instance/backend/nanobot/web/server.py b/app-instance/backend/nanobot/web/server.py index 226b7e9..ca4eec5 100644 --- a/app-instance/backend/nanobot/web/server.py +++ b/app-instance/backend/nanobot/web/server.py @@ -1580,11 +1580,8 @@ def _register_routes(app: FastAPI) -> None: sm: SessionManager = app.state.session_manager return sm.list_sessions() - @app.get("/api/sessions/{key:path}") - async def get_session(key: str): - """Get a session's message history.""" - sm: SessionManager = app.state.session_manager - session = sm.get_or_create(key) + def _serialize_session_detail(session: Session) -> dict[str, Any]: + """Build the filtered session payload returned to the web UI.""" # Filter out tool messages and assistant messages with tool_calls # (intermediate steps), only keep user messages and final assistant replies visible_messages = [] @@ -1616,6 +1613,21 @@ def _register_routes(app: FastAPI) -> None: "updated_at": session.updated_at.isoformat(), } + @app.post("/api/sessions/{key:path}") + async def create_session(key: str): + """Create or persist a session immediately.""" + sm: SessionManager = app.state.session_manager + session = sm.get_or_create(key) + sm.save(session) + return _serialize_session_detail(session) + + @app.get("/api/sessions/{key:path}") + async def get_session(key: str): + """Get a session's message history.""" + sm: SessionManager = app.state.session_manager + session = sm.get_or_create(key) + return _serialize_session_detail(session) + @app.delete("/api/sessions/{key:path}") async def delete_session(key: str): """Delete a session.""" diff --git a/app-instance/frontend/app/(app)/page.tsx b/app-instance/frontend/app/(app)/page.tsx index fafca08..89f72db 100644 --- a/app-instance/frontend/app/(app)/page.tsx +++ b/app-instance/frontend/app/(app)/page.tsx @@ -9,6 +9,7 @@ import { ScrollArea } from '@/components/ui/scroll-area'; import { Separator } from '@/components/ui/separator'; import { cancelDelegation, + createSession, deleteSession, getSession, getStatus, @@ -204,11 +205,14 @@ export default function ChatPage() { }, [loadSessions]); useEffect(() => { + clearMessages(); + setIsLoading(false); + setIsThinking(false); resetProcessState(); const wsSessionId = sessionId.startsWith('web:') ? sessionId.slice(4) : sessionId; wsManager.connect(wsSessionId); loadSessionMessages(sessionId); - }, [loadSessionMessages, resetProcessState, sessionId]); + }, [clearMessages, loadSessionMessages, resetProcessState, sessionId, setIsLoading, setIsThinking]); useEffect(() => { const unsubStatus = wsManager.onStatusChange(async (status) => { @@ -338,6 +342,10 @@ export default function ChatPage() { setIsThinking(false); setIsLoading(false); if (result.response) { + if (useChatStore.getState().sessionId !== sessionId) { + await loadSessions(); + return; + } addMessage({ role: 'assistant', content: result.response, @@ -351,6 +359,9 @@ export default function ChatPage() { } catch { setIsThinking(false); setIsLoading(false); + if (useChatStore.getState().sessionId !== sessionId) { + return; + } addMessage({ role: 'assistant', content: '发送失败,请检查后端服务是否正在运行。', @@ -413,11 +424,17 @@ export default function ChatPage() { } }, [sessionId]); - const handleNewSession = () => { + const handleNewSession = async () => { const id = `web:${Date.now()}`; setSessionId(id); clearMessages(); resetProcessState(); + try { + await createSession(id); + } catch { + // ignore transient create failures; first message can still create the session server-side + } + void loadSessions(); }; const handleDeleteSession = async (key: string, e: React.MouseEvent) => { diff --git a/app-instance/frontend/lib/api.ts b/app-instance/frontend/lib/api.ts index 2760b00..d9cbdfa 100644 --- a/app-instance/frontend/lib/api.ts +++ b/app-instance/frontend/lib/api.ts @@ -509,6 +509,10 @@ export async function listSessions(): Promise { return fetchJSON('/api/sessions'); } +export async function createSession(key: string): Promise { + return fetchJSON(`/api/sessions/${encodeURIComponent(key)}`, { method: 'POST' }); +} + export async function getSession(key: string): Promise { return fetchJSON(`/api/sessions/${encodeURIComponent(key)}`); } diff --git a/app-instance/frontend/lib/store.ts b/app-instance/frontend/lib/store.ts index 188bd4f..a4639a4 100644 --- a/app-instance/frontend/lib/store.ts +++ b/app-instance/frontend/lib/store.ts @@ -13,6 +13,23 @@ import type { } from '@/types'; import type { WsStatus } from '@/lib/api'; +const ACTIVE_SESSION_STORAGE_KEY = 'nanobot_active_session_id'; + +function getInitialSessionId(): string { + if (typeof window === 'undefined') { + return 'web:default'; + } + const saved = window.localStorage.getItem(ACTIVE_SESSION_STORAGE_KEY)?.trim(); + return saved || 'web:default'; +} + +function persistSessionId(id: string): void { + if (typeof window === 'undefined') { + return; + } + window.localStorage.setItem(ACTIVE_SESSION_STORAGE_KEY, id); +} + interface ChatStore { user: AuthUser | null; isAuthLoading: boolean; @@ -105,7 +122,7 @@ function createEventId(event: ProcessWsEvent): string { export const useChatStore = create((set) => ({ user: null, isAuthLoading: true, - sessionId: 'web:default', + sessionId: getInitialSessionId(), messages: [], isLoading: false, streamingContent: '', @@ -125,7 +142,10 @@ export const useChatStore = create((set) => ({ setUser: (user) => set({ user }), setIsAuthLoading: (loading) => set({ isAuthLoading: loading }), - setSessionId: (id) => set({ sessionId: id }), + setSessionId: (id) => { + persistSessionId(id); + set({ sessionId: id }); + }, setMessages: (msgs) => set({ messages: msgs }), addMessage: (msg) => set((s) => ({ messages: [...s.messages, msg] })), setIsLoading: (loading) => set({ isLoading: loading }),