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 { ErrorBoundary } from '@/components/ErrorBoundary'; import { useAuth } from '@/hooks/useAuth'; import { useChat } from '@/hooks/useChat'; import { useSessionStore, isAdminUser } from '@/store/sessionStore'; import { useChatStore } from '@/store/chatStore'; import { usePageStore } from '@/store/pageStore'; import { fetchMessages } from '@/api/sessions'; import { registerServiceWorker } from '@/hooks/usePWA'; import { ModelsAdminPage } from '@/components/admin/ModelsAdminPage'; import { AdminDashboard } from '@/components/admin/AdminDashboard'; import { ProfilePage } from '@/components/profile/ProfilePage'; /** 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, userId } = useAuth(); const { send, sendVoiceStreamMessage } = useChat(); const { loadSessionsFromServer, ensureMainSession, setCurrentSessionId, setMessages, loadMessagesFromServer, sessions, currentSessionId } = useSessionStore(); const [authMode, setAuthMode] = useState<'login' | 'register'>('login'); const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); const [email, setEmail] = useState(''); const [nickname, setNickname] = useState(''); const [verifyCode, setVerifyCode] = useState(''); const [error, setError] = useState(''); const [successMsg, setSuccessMsg] = useState(''); const initializedRef = useRef(false); // ========== PWA Service Worker ========== // 注册 Service Worker(在 main.tsx 移除内联注册后,由 App 组件负责) useEffect(() => { registerServiceWorker(); }, []); // ========== URL Hash 路由 ========== /** 根据 hash 恢复或选择初始会话 */ const initSession = useCallback(async () => { if (!userId || initializedRef.current) return; initializedRef.current = true; try { 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); setHashSessionId(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); setHashSessionId(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); } } catch (error) { console.error('[App] initSession failed:', error); // 重置状态避免死锁,允许后续重试 initializedRef.current = false; } }, [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); if (!result.success) { setError(result.error || '登录失败'); } }; const handleRegister = async () => { setError(''); setSuccessMsg(''); if (!email) { setError('请输入邮箱'); return; } if (!nickname) { setError('请输入昵称'); return; } const result = await register(username, password, email, nickname, verifyCode || '000000'); if (!result.success) { setError(result.error || '注册失败'); } else { setSuccessMsg('注册成功!正在进入...'); } }; const switchMode = (mode: 'login' | 'register') => { setAuthMode(mode); setError(''); setSuccessMsg(''); }; // 登录/注册页面 if (!isLoggedIn) { return (
{/* Logo */}
🌸

昔涟

永远在你身边的伙伴 ♪

{/* 登录/注册表单 */}
{/* 模式切换 */}
setUsername(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && (authMode === 'login' ? handleLogin() : handleRegister())} className="w-full px-4 py-2.5 rounded-xl border border-pink-200 dark:border-pink-800 bg-white dark:bg-gray-800 text-sm text-gray-700 dark:text-gray-200 placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-pink-400" /> {authMode === 'register' && ( setEmail(e.target.value)} className="w-full px-4 py-2.5 rounded-xl border border-pink-200 dark:border-pink-800 bg-white dark:bg-gray-800 text-sm text-gray-700 dark:text-gray-200 placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-pink-400" /> )} {authMode === 'register' && ( setNickname(e.target.value)} className="w-full px-4 py-2.5 rounded-xl border border-pink-200 dark:border-pink-800 bg-white dark:bg-gray-800 text-sm text-gray-700 dark:text-gray-200 placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-pink-400" /> )} setPassword(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && (authMode === 'login' ? handleLogin() : handleRegister())} className="w-full px-4 py-2.5 rounded-xl border border-pink-200 dark:border-pink-800 bg-white dark:bg-gray-800 text-sm text-gray-700 dark:text-gray-200 placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-pink-400" /> {authMode === 'register' && ( setVerifyCode(e.target.value)} maxLength={6} className="w-full px-4 py-2.5 rounded-xl border border-pink-200 dark:border-pink-800 bg-white dark:bg-gray-800 text-sm text-gray-700 dark:text-gray-200 placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-pink-400" /> )} {error && (

{error}

)} {successMsg && (

{successMsg}

)} {authMode === 'login' ? ( ) : ( )}

{authMode === 'register' ? '开发阶段 · 验证码统一使用 000000' : '欢迎回来 ♪'}

); } // 聊天界面 return ( ); } type SendFn = (content: string, mode?: import('@/types/chat').ChatMode, attachments?: import('@/types/chat').MessageAttachment[]) => void; type SendVoiceStreamFn = (msg: import('@/types/chat').WSClientMessage) => void; function PageRouter({ onSend, onSendVoiceStream }: { onSend: SendFn; onSendVoiceStream: SendVoiceStreamFn }) { const currentPage = usePageStore((s) => s.currentPage); const isAdmin = isAdminUser(localStorage.getItem('user_id') || ''); switch (currentPage) { case 'admin-models': if (!isAdmin) return ; return ; case 'admin-dashboard': if (!isAdmin) return ; return ; case 'profile': return ; case 'chat': default: return ; } } function ChatPage({ onSend, onSendVoiceStream }: { onSend: SendFn; onSendVoiceStream: SendVoiceStreamFn }) { return (
); }