feat(nanobot-web): 添加会话创建接口并优化前端会话管理

- 新增 POST /api/sessions/{key} 接口用于立即创建或持久化会话
- 提取 _serialize_session_detail 函数以统一会话数据序列化逻辑
- 前端添加 createSession API 调用函数
- 实现本地存储中的会话ID持久化功能
- 优化 ChatPage 组件中的会话切换逻辑,确保状态正确重置
- 在消息处理中添加会话ID验证,避免跨会话消息混乱
- 新建会话时主动调用创建API并刷新会话列表
This commit is contained in:
2026-03-16 17:37:25 +08:00
parent b3767dd4ab
commit 344aee93fa
4 changed files with 62 additions and 9 deletions

View File

@ -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) => {

View File

@ -509,6 +509,10 @@ export async function listSessions(): Promise<Session[]> {
return fetchJSON('/api/sessions');
}
export async function createSession(key: string): Promise<SessionDetail> {
return fetchJSON(`/api/sessions/${encodeURIComponent(key)}`, { method: 'POST' });
}
export async function getSession(key: string): Promise<SessionDetail> {
return fetchJSON(`/api/sessions/${encodeURIComponent(key)}`);
}

View File

@ -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<ChatStore>((set) => ({
user: null,
isAuthLoading: true,
sessionId: 'web:default',
sessionId: getInitialSessionId(),
messages: [],
isLoading: false,
streamingContent: '',
@ -125,7 +142,10 @@ export const useChatStore = create<ChatStore>((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 }),