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