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:
2026-05-17 17:18:02 +08:00
parent 745b1c6aad
commit e7b7eff0d8
21 changed files with 1735 additions and 284 deletions
+134 -2
View File
@@ -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);