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);
|
||||
|
||||
Reference in New Issue
Block a user