feat(nanobot-web): 添加会话创建接口并优化前端会话管理
- 新增 POST /api/sessions/{key} 接口用于立即创建或持久化会话
- 提取 _serialize_session_detail 函数以统一会话数据序列化逻辑
- 前端添加 createSession API 调用函数
- 实现本地存储中的会话ID持久化功能
- 优化 ChatPage 组件中的会话切换逻辑,确保状态正确重置
- 在消息处理中添加会话ID验证,避免跨会话消息混乱
- 新建会话时主动调用创建API并刷新会话列表
This commit is contained in:
@ -1580,11 +1580,8 @@ def _register_routes(app: FastAPI) -> None:
|
|||||||
sm: SessionManager = app.state.session_manager
|
sm: SessionManager = app.state.session_manager
|
||||||
return sm.list_sessions()
|
return sm.list_sessions()
|
||||||
|
|
||||||
@app.get("/api/sessions/{key:path}")
|
def _serialize_session_detail(session: Session) -> dict[str, Any]:
|
||||||
async def get_session(key: str):
|
"""Build the filtered session payload returned to the web UI."""
|
||||||
"""Get a session's message history."""
|
|
||||||
sm: SessionManager = app.state.session_manager
|
|
||||||
session = sm.get_or_create(key)
|
|
||||||
# Filter out tool messages and assistant messages with tool_calls
|
# Filter out tool messages and assistant messages with tool_calls
|
||||||
# (intermediate steps), only keep user messages and final assistant replies
|
# (intermediate steps), only keep user messages and final assistant replies
|
||||||
visible_messages = []
|
visible_messages = []
|
||||||
@ -1616,6 +1613,21 @@ def _register_routes(app: FastAPI) -> None:
|
|||||||
"updated_at": session.updated_at.isoformat(),
|
"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}")
|
@app.delete("/api/sessions/{key:path}")
|
||||||
async def delete_session(key: str):
|
async def delete_session(key: str):
|
||||||
"""Delete a session."""
|
"""Delete a session."""
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import { ScrollArea } from '@/components/ui/scroll-area';
|
|||||||
import { Separator } from '@/components/ui/separator';
|
import { Separator } from '@/components/ui/separator';
|
||||||
import {
|
import {
|
||||||
cancelDelegation,
|
cancelDelegation,
|
||||||
|
createSession,
|
||||||
deleteSession,
|
deleteSession,
|
||||||
getSession,
|
getSession,
|
||||||
getStatus,
|
getStatus,
|
||||||
@ -204,11 +205,14 @@ export default function ChatPage() {
|
|||||||
}, [loadSessions]);
|
}, [loadSessions]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
clearMessages();
|
||||||
|
setIsLoading(false);
|
||||||
|
setIsThinking(false);
|
||||||
resetProcessState();
|
resetProcessState();
|
||||||
const wsSessionId = sessionId.startsWith('web:') ? sessionId.slice(4) : sessionId;
|
const wsSessionId = sessionId.startsWith('web:') ? sessionId.slice(4) : sessionId;
|
||||||
wsManager.connect(wsSessionId);
|
wsManager.connect(wsSessionId);
|
||||||
loadSessionMessages(sessionId);
|
loadSessionMessages(sessionId);
|
||||||
}, [loadSessionMessages, resetProcessState, sessionId]);
|
}, [clearMessages, loadSessionMessages, resetProcessState, sessionId, setIsLoading, setIsThinking]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const unsubStatus = wsManager.onStatusChange(async (status) => {
|
const unsubStatus = wsManager.onStatusChange(async (status) => {
|
||||||
@ -338,6 +342,10 @@ export default function ChatPage() {
|
|||||||
setIsThinking(false);
|
setIsThinking(false);
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
if (result.response) {
|
if (result.response) {
|
||||||
|
if (useChatStore.getState().sessionId !== sessionId) {
|
||||||
|
await loadSessions();
|
||||||
|
return;
|
||||||
|
}
|
||||||
addMessage({
|
addMessage({
|
||||||
role: 'assistant',
|
role: 'assistant',
|
||||||
content: result.response,
|
content: result.response,
|
||||||
@ -351,6 +359,9 @@ export default function ChatPage() {
|
|||||||
} catch {
|
} catch {
|
||||||
setIsThinking(false);
|
setIsThinking(false);
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
|
if (useChatStore.getState().sessionId !== sessionId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
addMessage({
|
addMessage({
|
||||||
role: 'assistant',
|
role: 'assistant',
|
||||||
content: '发送失败,请检查后端服务是否正在运行。',
|
content: '发送失败,请检查后端服务是否正在运行。',
|
||||||
@ -413,11 +424,17 @@ export default function ChatPage() {
|
|||||||
}
|
}
|
||||||
}, [sessionId]);
|
}, [sessionId]);
|
||||||
|
|
||||||
const handleNewSession = () => {
|
const handleNewSession = async () => {
|
||||||
const id = `web:${Date.now()}`;
|
const id = `web:${Date.now()}`;
|
||||||
setSessionId(id);
|
setSessionId(id);
|
||||||
clearMessages();
|
clearMessages();
|
||||||
resetProcessState();
|
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) => {
|
const handleDeleteSession = async (key: string, e: React.MouseEvent) => {
|
||||||
|
|||||||
@ -509,6 +509,10 @@ export async function listSessions(): Promise<Session[]> {
|
|||||||
return fetchJSON('/api/sessions');
|
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> {
|
export async function getSession(key: string): Promise<SessionDetail> {
|
||||||
return fetchJSON(`/api/sessions/${encodeURIComponent(key)}`);
|
return fetchJSON(`/api/sessions/${encodeURIComponent(key)}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,6 +13,23 @@ import type {
|
|||||||
} from '@/types';
|
} from '@/types';
|
||||||
import type { WsStatus } from '@/lib/api';
|
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 {
|
interface ChatStore {
|
||||||
user: AuthUser | null;
|
user: AuthUser | null;
|
||||||
isAuthLoading: boolean;
|
isAuthLoading: boolean;
|
||||||
@ -105,7 +122,7 @@ function createEventId(event: ProcessWsEvent): string {
|
|||||||
export const useChatStore = create<ChatStore>((set) => ({
|
export const useChatStore = create<ChatStore>((set) => ({
|
||||||
user: null,
|
user: null,
|
||||||
isAuthLoading: true,
|
isAuthLoading: true,
|
||||||
sessionId: 'web:default',
|
sessionId: getInitialSessionId(),
|
||||||
messages: [],
|
messages: [],
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
streamingContent: '',
|
streamingContent: '',
|
||||||
@ -125,7 +142,10 @@ export const useChatStore = create<ChatStore>((set) => ({
|
|||||||
|
|
||||||
setUser: (user) => set({ user }),
|
setUser: (user) => set({ user }),
|
||||||
setIsAuthLoading: (loading) => set({ isAuthLoading: loading }),
|
setIsAuthLoading: (loading) => set({ isAuthLoading: loading }),
|
||||||
setSessionId: (id) => set({ sessionId: id }),
|
setSessionId: (id) => {
|
||||||
|
persistSessionId(id);
|
||||||
|
set({ sessionId: id });
|
||||||
|
},
|
||||||
setMessages: (msgs) => set({ messages: msgs }),
|
setMessages: (msgs) => set({ messages: msgs }),
|
||||||
addMessage: (msg) => set((s) => ({ messages: [...s.messages, msg] })),
|
addMessage: (msg) => set((s) => ({ messages: [...s.messages, msg] })),
|
||||||
setIsLoading: (loading) => set({ isLoading: loading }),
|
setIsLoading: (loading) => set({ isLoading: loading }),
|
||||||
|
|||||||
Reference in New Issue
Block a user