fix: 第二轮修复 — 数据库启动检查、会话持久化、URL路由、设备排序等
1. DevTools 启动前检查数据库状态,失败时自动尝试启动 2. ai-core 添加数据库断线重连机制 (30秒间隔) 3. Dashboard 添加数据库状态卡片 (启动/停止/重启) 4. Gateway 会话空闲超时管理 (30分钟标记空闲) 5. 会话/消息 PostgreSQL 持久化 (SessionStore + REST API) 6. 前端服务端会话持久化 + URL hash 路由 + 侧边栏管理 7. 管理员回到主对话按钮 8. IoT 设备卡片固定排序 9. 更新相关文档
This commit is contained in:
+134
-2
@@ -1,13 +1,37 @@
|
||||
import { useState } from 'react';
|
||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { AppLayout } from '@/components/layout/AppLayout';
|
||||
import { ChatContainer } from '@/components/chat/ChatContainer';
|
||||
import { ChatInput } from '@/components/chat/ChatInput';
|
||||
import { useAuth } from '@/hooks/useAuth';
|
||||
import { useChat } from '@/hooks/useChat';
|
||||
import { useSessionStore, isAdminUser } from '@/store/sessionStore';
|
||||
import { useChatStore } from '@/store/chatStore';
|
||||
import { fetchMessages } from '@/api/sessions';
|
||||
|
||||
/** URL Hash 工具 */
|
||||
const SESSION_HASH_PREFIX = 'session=';
|
||||
|
||||
function getSessionIdFromHash(): string | null {
|
||||
const hash = window.location.hash.slice(1); // 去掉 #
|
||||
if (hash.startsWith(SESSION_HASH_PREFIX)) {
|
||||
return hash.slice(SESSION_HASH_PREFIX.length);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function setHashSessionId(sessionId: string | null) {
|
||||
if (sessionId) {
|
||||
window.location.hash = SESSION_HASH_PREFIX + sessionId;
|
||||
} else {
|
||||
// 清除 hash
|
||||
history.replaceState(null, '', window.location.pathname + window.location.search);
|
||||
}
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
const { isLoggedIn, login, register, loading: authLoading } = useAuth();
|
||||
const { isLoggedIn, login, register, loading: authLoading, userId } = useAuth();
|
||||
const { send } = useChat();
|
||||
const { loadSessionsFromServer, ensureMainSession, setCurrentSessionId, setMessages, loadMessagesFromServer, sessions, currentSessionId } = useSessionStore();
|
||||
|
||||
const [authMode, setAuthMode] = useState<'login' | 'register'>('login');
|
||||
const [username, setUsername] = useState('');
|
||||
@@ -17,6 +41,114 @@ export default function App() {
|
||||
const [error, setError] = useState('');
|
||||
const [successMsg, setSuccessMsg] = useState('');
|
||||
|
||||
const initializedRef = useRef(false);
|
||||
|
||||
// ========== URL Hash 路由 ==========
|
||||
|
||||
/** 根据 hash 恢复或选择初始会话 */
|
||||
const initSession = useCallback(async () => {
|
||||
if (!userId || initializedRef.current) return;
|
||||
initializedRef.current = true;
|
||||
|
||||
const admin = isAdminUser(userId);
|
||||
|
||||
// 1. 从服务端加载会话列表
|
||||
await loadSessionsFromServer(userId);
|
||||
const currentSessions = useSessionStore.getState().sessions;
|
||||
|
||||
// 2. 检查 URL hash
|
||||
const hashId = getSessionIdFromHash();
|
||||
|
||||
if (hashId) {
|
||||
// 尝试加载 hash 指定的会话
|
||||
const found = currentSessions.find((s) => s.id === hashId);
|
||||
if (found) {
|
||||
setCurrentSessionId(found.id);
|
||||
await loadMessagesFromServer(found.id);
|
||||
return;
|
||||
}
|
||||
// 会话可能已被删除,尝试从 API 获取消息(404 时 catch)
|
||||
try {
|
||||
const resp = await fetchMessages(hashId);
|
||||
if (resp.messages && resp.messages.length > 0) {
|
||||
// 消息存在说明会话仍有效(虽然不在列表里,可能是刚创建的)
|
||||
setCurrentSessionId(hashId);
|
||||
const msgs = resp.messages.map((m: any, i: number) => ({
|
||||
id: m.id ? String(m.id) : `hist_${i}_${Date.now()}`,
|
||||
role: m.role,
|
||||
content: m.content,
|
||||
timestamp: typeof m.created_at === 'number' ? m.created_at : Date.now(),
|
||||
isStreaming: false,
|
||||
}));
|
||||
setMessages(msgs);
|
||||
useChatStore.getState().setMessages(msgs);
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
// 会话不存在,回退
|
||||
}
|
||||
// 回退:清除 hash,加载最新/主对话
|
||||
setHashSessionId(null);
|
||||
}
|
||||
|
||||
// 3. 无 hash 或 hash 无效:加载最新会话
|
||||
if (admin) {
|
||||
// 管理员:确保主对话存在
|
||||
const mainSession = await ensureMainSession(userId);
|
||||
if (mainSession) {
|
||||
setCurrentSessionId(mainSession.id);
|
||||
setHashSessionId(mainSession.id);
|
||||
await loadMessagesFromServer(mainSession.id);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 普通用户:选择最新会话
|
||||
if (currentSessions.length > 0) {
|
||||
const latest = currentSessions[0]; // 已按 updated_at DESC 排序
|
||||
setCurrentSessionId(latest.id);
|
||||
setHashSessionId(latest.id);
|
||||
await loadMessagesFromServer(latest.id);
|
||||
}
|
||||
}, [userId, loadSessionsFromServer, ensureMainSession, setCurrentSessionId, setMessages, loadMessagesFromServer]);
|
||||
|
||||
// 登录后初始化
|
||||
useEffect(() => {
|
||||
if (isLoggedIn && userId) {
|
||||
initSession();
|
||||
}
|
||||
}, [isLoggedIn, userId, initSession]);
|
||||
|
||||
// 监听 hashchange 事件 (浏览器前进/后退)
|
||||
useEffect(() => {
|
||||
if (!isLoggedIn) return;
|
||||
|
||||
const handleHashChange = async () => {
|
||||
const hashId = getSessionIdFromHash();
|
||||
const currentId = useSessionStore.getState().currentSessionId;
|
||||
|
||||
if (hashId && hashId !== currentId) {
|
||||
// hash 变化,切换会话
|
||||
setCurrentSessionId(hashId);
|
||||
await loadMessagesFromServer(hashId);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('hashchange', handleHashChange);
|
||||
return () => window.removeEventListener('hashchange', handleHashChange);
|
||||
}, [isLoggedIn, setCurrentSessionId, loadMessagesFromServer]);
|
||||
|
||||
// 当前会话变化时更新 URL hash(仅在登录后、非 hashchange 驱动时)
|
||||
useEffect(() => {
|
||||
if (!isLoggedIn || !currentSessionId) return;
|
||||
const hashId = getSessionIdFromHash();
|
||||
if (hashId !== currentSessionId) {
|
||||
setHashSessionId(currentSessionId);
|
||||
}
|
||||
}, [isLoggedIn, currentSessionId]);
|
||||
|
||||
// ========== 认证相关 ==========
|
||||
|
||||
const handleLogin = async () => {
|
||||
setError('');
|
||||
const result = await login(username, password);
|
||||
|
||||
@@ -1,8 +1,110 @@
|
||||
// 会话API
|
||||
export {
|
||||
createSession,
|
||||
listSessions,
|
||||
getSession,
|
||||
deleteSession,
|
||||
fetchSessionMessages,
|
||||
} from './client';
|
||||
// 会话持久化 API — 对接 Gateway REST API
|
||||
|
||||
import { request } from './client';
|
||||
import type { Session, SessionListResponse, SessionMessagesResponse, SessionResponse } from '@/types/session';
|
||||
|
||||
/**
|
||||
* 获取用户的会话列表
|
||||
* GET /api/v1/sessions?user_id={userId}
|
||||
*/
|
||||
export async function fetchSessions(userId: string): Promise<Session[]> {
|
||||
const resp = await request<SessionListResponse>(
|
||||
`/sessions?user_id=${encodeURIComponent(userId)}`
|
||||
);
|
||||
if (resp.error) {
|
||||
console.error('[sessions] 获取会话列表失败:', resp.error);
|
||||
return [];
|
||||
}
|
||||
// Gateway 返回 { sessions: [...] }
|
||||
return (resp.data as SessionListResponse)?.sessions || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取会话的消息历史
|
||||
* GET /api/v1/sessions/{sessionId}/messages?limit={limit}
|
||||
*/
|
||||
export async function fetchMessages(
|
||||
sessionId: string,
|
||||
limit: number = 50
|
||||
): Promise<SessionMessagesResponse> {
|
||||
const resp = await request<SessionMessagesResponse>(
|
||||
`/sessions/${encodeURIComponent(sessionId)}/messages?limit=${limit}`
|
||||
);
|
||||
if (resp.error) {
|
||||
console.error('[sessions] 获取消息失败:', resp.error);
|
||||
return { messages: [] };
|
||||
}
|
||||
return (resp.data as SessionMessagesResponse) || { messages: [] };
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建新会话
|
||||
* POST /api/v1/sessions
|
||||
*/
|
||||
export async function createSession(
|
||||
userId: string,
|
||||
sessionId: string,
|
||||
title: string = '新的对话',
|
||||
isMain: boolean = false
|
||||
): Promise<SessionResponse | null> {
|
||||
const resp = await request<SessionResponse>('/sessions', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
user_id: userId,
|
||||
session_id: sessionId,
|
||||
title,
|
||||
is_main: isMain,
|
||||
},
|
||||
});
|
||||
if (resp.error) {
|
||||
console.error('[sessions] 创建会话失败:', resp.error);
|
||||
return null;
|
||||
}
|
||||
return resp.data as SessionResponse;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除单个会话 (不删除记忆)
|
||||
* DELETE /api/v1/sessions/{sessionId}
|
||||
*/
|
||||
export async function deleteSession(sessionId: string): Promise<boolean> {
|
||||
const resp = await request(`/sessions/${encodeURIComponent(sessionId)}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
if (resp.error) {
|
||||
console.error('[sessions] 删除会话失败:', resp.error);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除用户所有会话 (不删除记忆)
|
||||
* DELETE /api/v1/sessions?user_id={userId}
|
||||
*/
|
||||
export async function deleteAllSessions(userId: string): Promise<boolean> {
|
||||
const resp = await request(`/sessions?user_id=${encodeURIComponent(userId)}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
if (resp.error) {
|
||||
console.error('[sessions] 删除所有会话失败:', resp.error);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空会话消息但不删除会话本身
|
||||
* DELETE /api/v1/sessions/{sessionId}/messages
|
||||
*/
|
||||
export async function clearSessionMessages(sessionId: string): Promise<boolean> {
|
||||
const resp = await request(
|
||||
`/sessions/${encodeURIComponent(sessionId)}/messages`,
|
||||
{ method: 'DELETE' }
|
||||
);
|
||||
if (resp.error) {
|
||||
console.error('[sessions] 清空消息失败:', resp.error);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import { useSession } from '@/hooks/useSession';
|
||||
import { useSessionStore } from '@/store/sessionStore';
|
||||
import { CyreneAvatar } from '@/components/persona/CyreneAvatar';
|
||||
import type { Session } from '@/types/session';
|
||||
|
||||
interface SidebarProps {
|
||||
onClose?: () => void;
|
||||
@@ -11,12 +13,25 @@ export function Sidebar({ onClose }: SidebarProps) {
|
||||
sessions,
|
||||
createSession,
|
||||
deleteSession,
|
||||
deleteAllSessions,
|
||||
clearMainSession,
|
||||
setCurrentSession,
|
||||
isAdmin,
|
||||
} = useSession();
|
||||
const currentSessionId = useSessionStore((s) => s.currentSessionId);
|
||||
|
||||
const displaySessions = sessions;
|
||||
const activeSessionId = currentSessionId;
|
||||
// 确认弹窗状态
|
||||
const [confirmAction, setConfirmAction] = useState<{
|
||||
type: 'delete' | 'clearMain' | 'deleteAll';
|
||||
sessionId?: string;
|
||||
} | null>(null);
|
||||
|
||||
// 按 updated_at 降序排列
|
||||
const displaySessions = [...sessions].sort((a, b) => {
|
||||
const ta = typeof a.updated_at === 'string' ? parseInt(a.updated_at, 10) : (a.updated_at as unknown as number);
|
||||
const tb = typeof b.updated_at === 'string' ? parseInt(b.updated_at, 10) : (b.updated_at as unknown as number);
|
||||
return (tb || 0) - (ta || 0);
|
||||
});
|
||||
|
||||
const handleNewChat = async () => {
|
||||
const session = await createSession();
|
||||
@@ -28,11 +43,43 @@ export function Sidebar({ onClose }: SidebarProps) {
|
||||
if (onClose) onClose();
|
||||
};
|
||||
|
||||
const handleDeleteSession = (e: { stopPropagation: () => void }, id: string) => {
|
||||
e.stopPropagation();
|
||||
deleteSession(id);
|
||||
const handleMainSession = async () => {
|
||||
// 找到主对话
|
||||
const mainSession = displaySessions.find((s) => s.is_main);
|
||||
if (mainSession) {
|
||||
setCurrentSession(mainSession.id);
|
||||
if (onClose) onClose();
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteClick = (e: React.MouseEvent, id: string) => {
|
||||
e.stopPropagation();
|
||||
const session = displaySessions.find((s) => s.id === id);
|
||||
// 管理员主对话不可删除
|
||||
if (isAdmin && session?.is_main) return;
|
||||
setConfirmAction({ type: 'delete', sessionId: id });
|
||||
};
|
||||
|
||||
const handleConfirmAction = async () => {
|
||||
if (!confirmAction) return;
|
||||
switch (confirmAction.type) {
|
||||
case 'delete':
|
||||
if (confirmAction.sessionId) {
|
||||
await deleteSession(confirmAction.sessionId);
|
||||
}
|
||||
break;
|
||||
case 'clearMain':
|
||||
await clearMainSession();
|
||||
break;
|
||||
case 'deleteAll':
|
||||
await deleteAllSessions();
|
||||
break;
|
||||
}
|
||||
setConfirmAction(null);
|
||||
};
|
||||
|
||||
const cancelConfirm = () => setConfirmAction(null);
|
||||
|
||||
/** 格式化时间戳为可读字符串 */
|
||||
const formatTime = (ts: string | number): string => {
|
||||
if (!ts) return '';
|
||||
@@ -52,7 +99,28 @@ export function Sidebar({ onClose }: SidebarProps) {
|
||||
|
||||
return (
|
||||
<aside className="h-full bg-white/90 dark:bg-gray-900/90 border-r border-pink-100 dark:border-pink-900 flex flex-col">
|
||||
{/* 侧边栏头部 */}
|
||||
{/* 主对话按钮 (仅管理员可见) */}
|
||||
{isAdmin && (
|
||||
<div className="p-3 border-b border-pink-100 dark:border-pink-900 flex gap-2">
|
||||
<button
|
||||
onClick={handleMainSession}
|
||||
className="flex-1 flex items-center justify-center gap-1.5 px-3 py-2 bg-amber-50 hover:bg-amber-100 dark:bg-amber-900/20 dark:hover:bg-amber-900/30 text-amber-600 dark:text-amber-400 rounded-xl text-sm font-medium transition-colors border border-amber-200 dark:border-amber-800"
|
||||
title="回到主对话"
|
||||
>
|
||||
<span>🏠</span>
|
||||
<span>主对话</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setConfirmAction({ type: 'clearMain' })}
|
||||
className="flex items-center justify-center px-2 py-2 bg-red-50 hover:bg-red-100 dark:bg-red-900/20 dark:hover:bg-red-900/30 text-red-400 hover:text-red-500 rounded-xl text-sm transition-colors border border-red-200 dark:border-red-800"
|
||||
title="清空主对话消息"
|
||||
>
|
||||
<span>🗑️</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 新对话按钮 */}
|
||||
<div className="p-4 border-b border-pink-100 dark:border-pink-900">
|
||||
<button
|
||||
onClick={handleNewChat}
|
||||
@@ -70,46 +138,80 @@ export function Sidebar({ onClose }: SidebarProps) {
|
||||
还没有对话哦,开始和昔涟聊天吧 ♪
|
||||
</p>
|
||||
) : (
|
||||
displaySessions.map((session) => (
|
||||
<div
|
||||
key={session.id}
|
||||
onClick={() => handleSelectSession(session.id)}
|
||||
className={`
|
||||
group flex items-center justify-between px-4 py-2.5 mx-2 rounded-lg cursor-pointer transition-colors
|
||||
${
|
||||
activeSessionId === session.id
|
||||
? 'bg-pink-50 dark:bg-pink-900/30 text-pink-600'
|
||||
: 'hover:bg-gray-50 dark:hover:bg-gray-800 text-gray-600 dark:text-gray-300'
|
||||
}
|
||||
`}
|
||||
>
|
||||
<div className="flex items-center gap-2 min-w-0 flex-1">
|
||||
<CyreneAvatar size="sm" />
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm font-medium truncate">{session.title || '新的对话'}</p>
|
||||
<p className="text-xs text-gray-400 truncate">
|
||||
{session.message_count != null ? `${session.message_count} 条消息` : ''}
|
||||
{session.updated_at ? ` · ${formatTime(session.updated_at)}` : ''}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={(e) => handleDeleteSession(e, session.id)}
|
||||
className="opacity-0 group-hover:opacity-100 p-1 text-gray-400 hover:text-red-400 transition-all"
|
||||
title="删除会话"
|
||||
displaySessions.map((session) => {
|
||||
const isMainSession = session.is_main;
|
||||
const isActive = currentSessionId === session.id;
|
||||
// 管理员主对话不可删除
|
||||
const canDelete = !(isAdmin && isMainSession);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={session.id}
|
||||
onClick={() => handleSelectSession(session.id)}
|
||||
className={`
|
||||
group flex items-center justify-between px-4 py-2.5 mx-2 rounded-lg cursor-pointer transition-colors
|
||||
${
|
||||
isActive
|
||||
? 'bg-pink-50 dark:bg-pink-900/30 text-pink-600'
|
||||
: 'hover:bg-gray-50 dark:hover:bg-gray-800 text-gray-600 dark:text-gray-300'
|
||||
}
|
||||
`}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
))
|
||||
<div className="flex items-center gap-2 min-w-0 flex-1">
|
||||
<CyreneAvatar size="sm" />
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm font-medium truncate">
|
||||
{isMainSession ? '🏠 ' : ''}
|
||||
{session.title || '新的对话'}
|
||||
</p>
|
||||
<p className="text-xs text-gray-400 truncate">
|
||||
{session.message_count != null
|
||||
? `${session.message_count} 条消息`
|
||||
: ''}
|
||||
{session.updated_at
|
||||
? `${session.message_count != null ? ' · ' : ''}${formatTime(session.updated_at)}`
|
||||
: ''}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{canDelete && (
|
||||
<button
|
||||
onClick={(e) => handleDeleteClick(e, session.id)}
|
||||
className="opacity-0 group-hover:opacity-100 p-1 text-gray-400 hover:text-red-400 transition-all"
|
||||
title="删除会话"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-4 w-4"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 底部信息 */}
|
||||
<div className="p-4 border-t border-pink-100 dark:border-pink-900">
|
||||
<div className="flex items-center gap-2 text-xs text-gray-400">
|
||||
{/* 底部:一键清空所有对话 */}
|
||||
<div className="p-3 border-t border-pink-100 dark:border-pink-900">
|
||||
<button
|
||||
onClick={() => setConfirmAction({ type: 'deleteAll' })}
|
||||
className="w-full flex items-center justify-center gap-1.5 px-3 py-2 bg-red-50 hover:bg-red-100 dark:bg-red-900/20 dark:hover:bg-red-900/30 text-red-400 hover:text-red-500 rounded-xl text-xs font-medium transition-colors border border-red-200 dark:border-red-800"
|
||||
>
|
||||
<span>🗑️</span>
|
||||
<span>清空所有对话</span>
|
||||
</button>
|
||||
<div className="flex items-center gap-2 mt-3 text-xs text-gray-400">
|
||||
<CyreneAvatar size="sm" />
|
||||
<div>
|
||||
<p className="font-medium text-pink-400">昔涟 AI</p>
|
||||
@@ -117,6 +219,42 @@ export function Sidebar({ onClose }: SidebarProps) {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 确认弹窗 */}
|
||||
{confirmAction && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
|
||||
<div className="bg-white dark:bg-gray-800 rounded-2xl shadow-xl p-6 m-4 max-w-sm w-full border border-pink-100 dark:border-pink-700">
|
||||
<h3 className="text-lg font-semibold text-gray-800 dark:text-gray-200 mb-2">
|
||||
{confirmAction.type === 'delete'
|
||||
? '删除会话'
|
||||
: confirmAction.type === 'clearMain'
|
||||
? '清空主对话'
|
||||
: '清空所有对话'}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 mb-6">
|
||||
{confirmAction.type === 'delete'
|
||||
? '确定要删除这个会话吗?此操作不会删除记忆。'
|
||||
: confirmAction.type === 'clearMain'
|
||||
? '确定要清空主对话的所有消息吗?此操作不会删除记忆。'
|
||||
: '确定要删除所有对话吗?此操作不会删除记忆。管理员将回到主对话,普通用户进入新对话。'}
|
||||
</p>
|
||||
<div className="flex justify-end gap-3">
|
||||
<button
|
||||
onClick={cancelConfirm}
|
||||
className="px-4 py-2 rounded-lg text-sm text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
onClick={handleConfirmAction}
|
||||
className="px-4 py-2 rounded-lg text-sm bg-red-400 hover:bg-red-500 text-white font-medium transition-colors"
|
||||
>
|
||||
确定
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,24 @@
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { useSessionStore } from '@/store/sessionStore';
|
||||
import { useSessionStore, isAdminUser } from '@/store/sessionStore';
|
||||
import { useChatStore } from '@/store/chatStore';
|
||||
import { listSessions as apiListSessions, createSession as apiCreateSession, deleteSession as apiDeleteSession } from '@/api/client';
|
||||
import { createSession as apiCreateSession } from '@/api/sessions';
|
||||
import type { Session } from '@/types/session';
|
||||
|
||||
/** 生成简易随机ID */
|
||||
function randomID(n: number = 12): string {
|
||||
const letters = 'abcdefghijklmnopqrstuvwxyz0123456789';
|
||||
let result = '';
|
||||
for (let i = 0; i < n; i++) {
|
||||
result += letters.charAt(Math.floor(Math.random() * letters.length));
|
||||
}
|
||||
return `session_${result}`;
|
||||
}
|
||||
|
||||
/** 获取当前用户ID */
|
||||
function getUserId(): string {
|
||||
return localStorage.getItem('user_id') || '';
|
||||
}
|
||||
|
||||
export function useSession() {
|
||||
const {
|
||||
sessions,
|
||||
@@ -14,69 +29,105 @@ export function useSession() {
|
||||
removeSession,
|
||||
setCurrentSessionId,
|
||||
setLoading,
|
||||
loadSessionsFromServer,
|
||||
loadMessagesFromServer,
|
||||
clearMainSessionMessages,
|
||||
deleteSessionAndRefresh,
|
||||
deleteAllSessionsAndReset,
|
||||
ensureMainSession,
|
||||
} = useSessionStore();
|
||||
|
||||
const { clearMessages } = useChatStore();
|
||||
const userId = getUserId();
|
||||
const isAdmin = isAdminUser(userId);
|
||||
|
||||
/** 从服务端加载会话列表 */
|
||||
const loadSessions = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const resp = await apiListSessions();
|
||||
if (resp.data) {
|
||||
const data = resp.data as { sessions: Session[] };
|
||||
setSessions(data.sessions || []);
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [setSessions, setLoading]);
|
||||
if (!userId) return;
|
||||
await loadSessionsFromServer(userId);
|
||||
}, [userId, loadSessionsFromServer]);
|
||||
|
||||
// 初始加载
|
||||
useEffect(() => {
|
||||
loadSessions();
|
||||
}, [loadSessions]);
|
||||
|
||||
const createSession = useCallback(async (title?: string) => {
|
||||
const resp = await apiCreateSession(title);
|
||||
if (resp.data) {
|
||||
const session = resp.data as Session;
|
||||
addSession(session);
|
||||
// setCurrentSessionId 内部已处理消息清空和历史加载,无需额外 clearMessages
|
||||
await setCurrentSessionId(session.id);
|
||||
return session;
|
||||
}
|
||||
return null;
|
||||
}, [addSession, setCurrentSessionId]);
|
||||
|
||||
const deleteSession = useCallback(async (id: string) => {
|
||||
await apiDeleteSession(id);
|
||||
// 如果删除的是当前活跃会话,先切换到其他会话
|
||||
if (currentSessionId === id) {
|
||||
const remaining = useSessionStore.getState().sessions.filter((s: Session) => s.id !== id);
|
||||
if (remaining.length > 0) {
|
||||
// 切换到列表中的第一个会话
|
||||
await setCurrentSessionId(remaining[0].id);
|
||||
} else {
|
||||
clearMessages();
|
||||
setCurrentSessionId(null);
|
||||
/** 创建新会话 (isMain 默认为 false) */
|
||||
const createSession = useCallback(
|
||||
async (title?: string, isMain: boolean = false) => {
|
||||
const sid = randomID();
|
||||
const created = await apiCreateSession(
|
||||
userId,
|
||||
sid,
|
||||
title || '新的对话',
|
||||
isMain
|
||||
);
|
||||
if (created) {
|
||||
const newSession: Session = {
|
||||
id: created.id,
|
||||
user_id: created.user_id,
|
||||
title: created.title,
|
||||
is_main: created.is_main,
|
||||
created_at: String(created.created_at),
|
||||
updated_at: String(created.updated_at),
|
||||
message_count: 0,
|
||||
};
|
||||
addSession(newSession);
|
||||
setCurrentSessionId(newSession.id);
|
||||
return newSession;
|
||||
}
|
||||
}
|
||||
removeSession(id);
|
||||
}, [removeSession, currentSessionId, clearMessages, setCurrentSessionId]);
|
||||
return null;
|
||||
},
|
||||
[userId, addSession, setCurrentSessionId]
|
||||
);
|
||||
|
||||
const setCurrentSession = useCallback((id: string) => {
|
||||
// setCurrentSessionId 内部已处理消息清空和历史加载
|
||||
setCurrentSessionId(id);
|
||||
}, [setCurrentSessionId]);
|
||||
/** 清空主对话消息 */
|
||||
const clearMainSession = useCallback(async () => {
|
||||
const mainSession = useSessionStore
|
||||
.getState()
|
||||
.sessions.find((s: Session) => s.is_main);
|
||||
if (!mainSession) {
|
||||
// 尝试确保主对话存在
|
||||
const ensured = await ensureMainSession(userId);
|
||||
if (!ensured) return false;
|
||||
return clearMainSessionMessages(ensured.id);
|
||||
}
|
||||
return clearMainSessionMessages(mainSession.id);
|
||||
}, [userId, clearMainSessionMessages, ensureMainSession]);
|
||||
|
||||
/** 删除单个会话 */
|
||||
const deleteSession = useCallback(
|
||||
async (id: string) => {
|
||||
await deleteSessionAndRefresh(id, userId);
|
||||
},
|
||||
[userId, deleteSessionAndRefresh]
|
||||
);
|
||||
|
||||
/** 删除所有会话 */
|
||||
const deleteAllSessions = useCallback(async () => {
|
||||
await deleteAllSessionsAndReset(userId);
|
||||
}, [userId, deleteAllSessionsAndReset]);
|
||||
|
||||
/** 切换当前会话 */
|
||||
const setCurrentSession = useCallback(
|
||||
async (id: string) => {
|
||||
setCurrentSessionId(id);
|
||||
// 加载该会话的消息历史
|
||||
await loadMessagesFromServer(id);
|
||||
},
|
||||
[setCurrentSessionId, loadMessagesFromServer]
|
||||
);
|
||||
|
||||
return {
|
||||
sessions,
|
||||
currentSessionId,
|
||||
loading,
|
||||
isAdmin,
|
||||
userId,
|
||||
loadSessions,
|
||||
createSession,
|
||||
clearMainSession,
|
||||
deleteSession,
|
||||
deleteAllSessions,
|
||||
setCurrentSession,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useEffect, useRef, useCallback, useState } from 'react';
|
||||
import { useChatStore } from '@/store/chatStore';
|
||||
import { useSessionStore } from '@/store/sessionStore';
|
||||
import { getToken } from '@/api/client';
|
||||
import { fetchMessages } from '@/api/sessions';
|
||||
import type { WSClientMessage, WSServerMessage } from '@/types/chat';
|
||||
|
||||
const WS_BASE_URL =
|
||||
@@ -12,7 +13,8 @@ export function useWebSocket() {
|
||||
const wsRef = useRef<WebSocket | null>(null);
|
||||
const reconnectTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const shouldReconnectRef = useRef(true);
|
||||
const activeSessionRef = useRef<string | null>(null); // 追踪当前活跃会话,防止竞态
|
||||
const activeSessionRef = useRef<string | null>(null);
|
||||
const loadingRef = useRef(false); // 防止重复加载消息
|
||||
|
||||
// 订阅 sessionStore 中的 currentSessionId 变化
|
||||
const currentSessionId = useSessionStore((s) => s.currentSessionId);
|
||||
@@ -40,7 +42,7 @@ export function useWebSocket() {
|
||||
shouldReconnectRef.current = true;
|
||||
console.log('[WS] 已连接, session_id:', sessionID);
|
||||
|
||||
// 连接后发送会话恢复消息,恢复历史上下文
|
||||
// 连接后发送会话恢复消息,恢复后端上下文
|
||||
const sid = useSessionStore.getState().currentSessionId;
|
||||
if (sid) {
|
||||
const resumeMsg: WSClientMessage = {
|
||||
@@ -81,10 +83,39 @@ export function useWebSocket() {
|
||||
wsRef.current = ws;
|
||||
}, []);
|
||||
|
||||
// 初始连接 + 会话切换时重连
|
||||
// 会话切换时:先通过 REST API 加载历史消息,再建立 WS 连接
|
||||
useEffect(() => {
|
||||
activeSessionRef.current = currentSessionId;
|
||||
connect();
|
||||
|
||||
const loadAndConnect = async () => {
|
||||
// 如果是从 URL 恢复的 session,先加载历史消息
|
||||
if (currentSessionId && !loadingRef.current) {
|
||||
loadingRef.current = true;
|
||||
try {
|
||||
const resp = await fetchMessages(currentSessionId);
|
||||
const rawMessages = resp.messages || [];
|
||||
const msgs = rawMessages.map((m: any, i: number) => ({
|
||||
id: m.id ? String(m.id) : `hist_${i}_${Date.now()}`,
|
||||
role: m.role,
|
||||
content: m.content,
|
||||
timestamp:
|
||||
typeof m.created_at === 'number' ? m.created_at : Date.now(),
|
||||
isStreaming: false,
|
||||
}));
|
||||
useSessionStore.getState().setMessages(msgs);
|
||||
useChatStore.getState().setMessages(msgs);
|
||||
} catch {
|
||||
// 加载失败不影响后续连接
|
||||
} finally {
|
||||
loadingRef.current = false;
|
||||
}
|
||||
}
|
||||
|
||||
connect();
|
||||
};
|
||||
|
||||
loadAndConnect();
|
||||
|
||||
return () => {
|
||||
if (reconnectTimerRef.current) {
|
||||
clearTimeout(reconnectTimerRef.current);
|
||||
@@ -96,12 +127,13 @@ export function useWebSocket() {
|
||||
|
||||
const sendMessage = useCallback((msg: WSClientMessage) => {
|
||||
if (wsRef.current?.readyState === WebSocket.OPEN) {
|
||||
// 自动附上 session_id
|
||||
const sessionID = useSessionStore.getState().currentSessionId;
|
||||
wsRef.current.send(JSON.stringify({
|
||||
...msg,
|
||||
session_id: msg.session_id || sessionID || undefined,
|
||||
}));
|
||||
wsRef.current.send(
|
||||
JSON.stringify({
|
||||
...msg,
|
||||
session_id: msg.session_id || sessionID || undefined,
|
||||
})
|
||||
);
|
||||
}
|
||||
}, []);
|
||||
|
||||
@@ -109,7 +141,8 @@ export function useWebSocket() {
|
||||
}
|
||||
|
||||
function handleServerMessage(msg: WSServerMessage) {
|
||||
const { addMessage, appendToLastMessage, finishStreaming, setTyping } = useChatStore.getState();
|
||||
const { addMessage, appendToLastMessage, finishStreaming, setTyping } =
|
||||
useChatStore.getState();
|
||||
const { setMessages } = useSessionStore.getState();
|
||||
const chatState = useChatStore.getState();
|
||||
|
||||
@@ -128,21 +161,22 @@ function handleServerMessage(msg: WSServerMessage) {
|
||||
|
||||
case 'stream_chunk':
|
||||
if (msg.content) {
|
||||
// 首个 chunk 到达时创建消息并隐藏 typing indicator
|
||||
const { messages } = useChatStore.getState();
|
||||
const lastMsg = messages[messages.length - 1];
|
||||
if (!lastMsg || lastMsg.role !== 'assistant' || !lastMsg.isStreaming) {
|
||||
// 创建新的流式消息
|
||||
if (
|
||||
!lastMsg ||
|
||||
lastMsg.role !== 'assistant' ||
|
||||
!lastMsg.isStreaming
|
||||
) {
|
||||
addMessage({
|
||||
id: msg.message_id || ('msg_' + Date.now()),
|
||||
id: msg.message_id || 'msg_' + Date.now(),
|
||||
role: 'assistant',
|
||||
content: msg.content,
|
||||
timestamp: msg.timestamp,
|
||||
isStreaming: true,
|
||||
});
|
||||
setTyping(false); // 首个 chunk 到达,隐藏 typing 指示器
|
||||
setTyping(false);
|
||||
} else {
|
||||
// 追加到现有流式消息
|
||||
appendToLastMessage(msg.content);
|
||||
}
|
||||
}
|
||||
@@ -154,28 +188,23 @@ function handleServerMessage(msg: WSServerMessage) {
|
||||
|
||||
case 'history_response':
|
||||
if (msg.messages) {
|
||||
// 确保每条消息都有 id
|
||||
const msgsWithIds = msg.messages.map((m: any, i: number) => ({
|
||||
...m,
|
||||
id: m.id || `hist_${i}_${Date.now()}`,
|
||||
}));
|
||||
// 同步历史消息到两个 store
|
||||
setMessages(msgsWithIds);
|
||||
useChatStore.getState().setMessages(msgsWithIds);
|
||||
}
|
||||
// 确保历史加载后 typing indicator 关闭
|
||||
setTyping(false);
|
||||
break;
|
||||
|
||||
case 'device_update':
|
||||
// 处理 IoT 设备状态更新
|
||||
if (msg.devices && msg.devices.length > 0) {
|
||||
chatState.setIoTDevices(msg.devices);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'background_thinking':
|
||||
// 处理后端推送的后台思考状态
|
||||
if (msg.thinking_status) {
|
||||
chatState.setBackgroundThinkingStatus(msg.thinking_status);
|
||||
}
|
||||
@@ -187,7 +216,6 @@ function handleServerMessage(msg: WSServerMessage) {
|
||||
break;
|
||||
|
||||
case 'pong':
|
||||
// 忽略心跳响应
|
||||
break;
|
||||
|
||||
default:
|
||||
|
||||
@@ -1,15 +1,38 @@
|
||||
import { create } from 'zustand';
|
||||
import type { Session } from '@/types/session';
|
||||
import type { Message } from '@/types/chat';
|
||||
import { fetchSessionMessages as apiFetchMessages } from '@/api/client';
|
||||
import {
|
||||
fetchSessions,
|
||||
fetchMessages,
|
||||
createSession as apiCreateSession,
|
||||
deleteSession as apiDeleteSession,
|
||||
deleteAllSessions as apiDeleteAllSessions,
|
||||
clearSessionMessages as apiClearMessages,
|
||||
} from '@/api/sessions';
|
||||
import { useChatStore } from '@/store/chatStore';
|
||||
|
||||
/** 生成简易随机ID */
|
||||
function randomID(n: number = 12): string {
|
||||
const letters = 'abcdefghijklmnopqrstuvwxyz0123456789';
|
||||
let result = '';
|
||||
for (let i = 0; i < n; i++) {
|
||||
result += letters.charAt(Math.floor(Math.random() * letters.length));
|
||||
}
|
||||
return `session_${result}`;
|
||||
}
|
||||
|
||||
/** 判断是否为管理员用户 (user_id 以 "admin_" 开头) */
|
||||
export function isAdminUser(userId: string | null): boolean {
|
||||
return userId?.startsWith('admin_') ?? false;
|
||||
}
|
||||
|
||||
interface SessionStore {
|
||||
sessions: Session[];
|
||||
currentSessionId: string | null;
|
||||
loading: boolean;
|
||||
messages: Message[];
|
||||
|
||||
// 基础操作
|
||||
setSessions: (sessions: Session[]) => void;
|
||||
addSession: (session: Session) => void;
|
||||
removeSession: (id: string) => void;
|
||||
@@ -17,9 +40,17 @@ interface SessionStore {
|
||||
setLoading: (loading: boolean) => void;
|
||||
setMessages: (messages: Message[]) => void;
|
||||
clearMessages: () => void;
|
||||
|
||||
// 服务端持久化操作
|
||||
loadSessionsFromServer: (userId: string) => Promise<void>;
|
||||
loadMessagesFromServer: (sessionId: string) => Promise<void>;
|
||||
clearMainSessionMessages: (sessionId: string) => Promise<boolean>;
|
||||
deleteSessionAndRefresh: (id: string, userId: string) => Promise<void>;
|
||||
deleteAllSessionsAndReset: (userId: string) => Promise<void>;
|
||||
ensureMainSession: (userId: string) => Promise<Session | null>;
|
||||
}
|
||||
|
||||
export const useSessionStore = create<SessionStore>((set) => ({
|
||||
export const useSessionStore = create<SessionStore>((set, get) => ({
|
||||
sessions: [],
|
||||
currentSessionId: null,
|
||||
loading: false,
|
||||
@@ -34,46 +65,143 @@ export const useSessionStore = create<SessionStore>((set) => ({
|
||||
currentSessionId: state.currentSessionId === id ? null : state.currentSessionId,
|
||||
messages: state.currentSessionId === id ? [] : state.messages,
|
||||
})),
|
||||
setCurrentSessionId: async (id) => {
|
||||
// 立即清除旧消息,防止闪旧数据
|
||||
set({ currentSessionId: id, messages: [], loading: true });
|
||||
useChatStore.getState().clearMessages();
|
||||
|
||||
// 清除旧消息(同时清 chatStore)
|
||||
if (id === null) {
|
||||
set({ messages: [], loading: false });
|
||||
return;
|
||||
}
|
||||
|
||||
// 从后端加载历史消息
|
||||
try {
|
||||
const resp = await apiFetchMessages(id);
|
||||
if (resp.data) {
|
||||
const data = resp.data as { messages: Message[] };
|
||||
const msgs = (data.messages || []).map((m: Message, i: number) => ({
|
||||
...m,
|
||||
id: m.id || `hist_${i}_${Date.now()}`,
|
||||
}));
|
||||
set({ messages: msgs, loading: false });
|
||||
// 同步到 chatStore 以便 ChatContainer 渲染
|
||||
useChatStore.getState().setMessages(msgs);
|
||||
} else {
|
||||
set({ messages: [], loading: false });
|
||||
useChatStore.getState().clearMessages();
|
||||
}
|
||||
} catch {
|
||||
set({ messages: [], loading: false });
|
||||
setCurrentSessionId: (id) => {
|
||||
set({ currentSessionId: id });
|
||||
// 切换会话时清空旧消息,等待加载
|
||||
if (id !== get().currentSessionId) {
|
||||
set({ messages: [], loading: true });
|
||||
useChatStore.getState().clearMessages();
|
||||
}
|
||||
},
|
||||
setLoading: (loading) => set({ loading }),
|
||||
setMessages: (messages) => {
|
||||
set({ messages });
|
||||
// 同步到 chatStore
|
||||
useChatStore.getState().setMessages(messages);
|
||||
},
|
||||
clearMessages: () => {
|
||||
set({ messages: [] });
|
||||
useChatStore.getState().clearMessages();
|
||||
},
|
||||
|
||||
// ========== 服务端持久化操作 ==========
|
||||
|
||||
/**
|
||||
* 从服务端加载会话列表
|
||||
*/
|
||||
loadSessionsFromServer: async (userId: string) => {
|
||||
set({ loading: true });
|
||||
try {
|
||||
const sessions = await fetchSessions(userId);
|
||||
set({ sessions, loading: false });
|
||||
} catch {
|
||||
set({ loading: false });
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 从服务端加载指定会话的消息历史
|
||||
*/
|
||||
loadMessagesFromServer: async (sessionId: string) => {
|
||||
set({ loading: true });
|
||||
try {
|
||||
const resp = await fetchMessages(sessionId);
|
||||
const rawMessages = resp.messages || [];
|
||||
const msgs: Message[] = rawMessages.map((m: any, i: number) => ({
|
||||
id: m.id ? String(m.id) : `hist_${i}_${Date.now()}`,
|
||||
role: m.role,
|
||||
content: m.content,
|
||||
timestamp: typeof m.created_at === 'number' ? m.created_at : Date.now(),
|
||||
isStreaming: false,
|
||||
}));
|
||||
set({ messages: msgs, loading: false });
|
||||
useChatStore.getState().setMessages(msgs);
|
||||
} catch {
|
||||
set({ messages: [], loading: false });
|
||||
useChatStore.getState().clearMessages();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 清空主对话的消息但保留会话本身
|
||||
*/
|
||||
clearMainSessionMessages: async (sessionId: string) => {
|
||||
const ok = await apiClearMessages(sessionId);
|
||||
if (ok) {
|
||||
set({ messages: [] });
|
||||
useChatStore.getState().clearMessages();
|
||||
}
|
||||
return ok;
|
||||
},
|
||||
|
||||
/**
|
||||
* 删除会话并自动切换到下一个可用会话
|
||||
*/
|
||||
deleteSessionAndRefresh: async (id: string, userId: string) => {
|
||||
const ok = await apiDeleteSession(id);
|
||||
if (!ok) return;
|
||||
|
||||
const state = get();
|
||||
const remaining = state.sessions.filter((s) => s.id !== id);
|
||||
const wasCurrent = state.currentSessionId === id;
|
||||
|
||||
// 更新本地列表
|
||||
set({ sessions: remaining });
|
||||
|
||||
if (wasCurrent) {
|
||||
if (remaining.length > 0) {
|
||||
// 切换到列表中的第一个会话
|
||||
const nextId = remaining[0].id;
|
||||
set({ currentSessionId: nextId });
|
||||
await get().loadMessagesFromServer(nextId);
|
||||
} else {
|
||||
// 没有会话了:管理员回到主对话,普通用户创建新对话
|
||||
set({ currentSessionId: null, messages: [] });
|
||||
useChatStore.getState().clearMessages();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 删除所有会话并重置状态
|
||||
*/
|
||||
deleteAllSessionsAndReset: async (userId: string) => {
|
||||
const ok = await apiDeleteAllSessions(userId);
|
||||
if (!ok) return;
|
||||
|
||||
set({ sessions: [], currentSessionId: null, messages: [] });
|
||||
useChatStore.getState().clearMessages();
|
||||
},
|
||||
|
||||
/**
|
||||
* 确保主对话存在(管理员用户)。若不存在则创建并返回。
|
||||
*/
|
||||
ensureMainSession: async (userId: string) => {
|
||||
const state = get();
|
||||
// 先检查本地列表
|
||||
const existing = state.sessions.find((s) => s.is_main);
|
||||
if (existing) return existing;
|
||||
|
||||
// 本地没有,尝试从服务端加载
|
||||
await get().loadSessionsFromServer(userId);
|
||||
const refreshed = get().sessions.find((s) => s.is_main);
|
||||
if (refreshed) return refreshed;
|
||||
|
||||
// 服务端也没有,创建主对话
|
||||
const sid = randomID();
|
||||
const created = await apiCreateSession(userId, sid, '主对话', true);
|
||||
if (created) {
|
||||
const newSession: Session = {
|
||||
id: created.id,
|
||||
user_id: created.user_id,
|
||||
title: created.title,
|
||||
is_main: created.is_main,
|
||||
created_at: String(created.created_at),
|
||||
updated_at: String(created.updated_at),
|
||||
message_count: 0,
|
||||
};
|
||||
set((s) => ({ sessions: [newSession, ...s.sessions] }));
|
||||
return newSession;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -5,15 +5,16 @@ export interface Session {
|
||||
id: string;
|
||||
user_id: string;
|
||||
title: string;
|
||||
is_main: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
message_count: number;
|
||||
is_active: boolean;
|
||||
message_count?: number;
|
||||
}
|
||||
|
||||
/** 创建会话参数 */
|
||||
export interface CreateSessionParams {
|
||||
title?: string;
|
||||
is_main?: boolean;
|
||||
}
|
||||
|
||||
/** 会话列表响应 */
|
||||
@@ -26,6 +27,16 @@ export interface SessionMessagesResponse {
|
||||
messages: import('@/types/chat').Message[];
|
||||
}
|
||||
|
||||
/** 单个会话响应 */
|
||||
export interface SessionResponse {
|
||||
id: string;
|
||||
user_id: string;
|
||||
title: string;
|
||||
is_main: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
/** 认证相关 */
|
||||
export interface AuthResponse {
|
||||
user_id: string;
|
||||
|
||||
Reference in New Issue
Block a user