const AUTH_STORAGE_KEY = 'cyberstrike-auth'; let authToken = null; let authTokenExpiry = null; let authPromise = null; let authPromiseResolvers = []; let isAppInitialized = false; function isTokenValid() { return !!authToken && authTokenExpiry instanceof Date && authTokenExpiry.getTime() > Date.now(); } function saveAuth(token, expiresAt) { const expiry = expiresAt instanceof Date ? expiresAt : new Date(expiresAt); authToken = token; authTokenExpiry = expiry; try { localStorage.setItem(AUTH_STORAGE_KEY, JSON.stringify({ token, expiresAt: expiry.toISOString(), })); } catch (error) { console.warn('无法持久化认证信息:', error); } } function clearAuthStorage() { authToken = null; authTokenExpiry = null; try { localStorage.removeItem(AUTH_STORAGE_KEY); } catch (error) { console.warn('无法清除认证信息:', error); } } function loadAuthFromStorage() { try { const raw = localStorage.getItem(AUTH_STORAGE_KEY); if (!raw) { return false; } const stored = JSON.parse(raw); if (!stored.token || !stored.expiresAt) { clearAuthStorage(); return false; } const expiry = new Date(stored.expiresAt); if (Number.isNaN(expiry.getTime())) { clearAuthStorage(); return false; } authToken = stored.token; authTokenExpiry = expiry; return isTokenValid(); } catch (error) { console.error('读取认证信息失败:', error); clearAuthStorage(); return false; } } function resolveAuthPromises(success) { authPromiseResolvers.forEach(resolve => resolve(success)); authPromiseResolvers = []; authPromise = null; } function showLoginOverlay(message = '') { const overlay = document.getElementById('login-overlay'); const errorBox = document.getElementById('login-error'); const passwordInput = document.getElementById('login-password'); if (!overlay) { return; } overlay.style.display = 'flex'; if (errorBox) { if (message) { errorBox.textContent = message; errorBox.style.display = 'block'; } else { errorBox.textContent = ''; errorBox.style.display = 'none'; } } setTimeout(() => { if (passwordInput) { passwordInput.focus(); } }, 100); } function hideLoginOverlay() { const overlay = document.getElementById('login-overlay'); const errorBox = document.getElementById('login-error'); const passwordInput = document.getElementById('login-password'); if (overlay) { overlay.style.display = 'none'; } if (errorBox) { errorBox.textContent = ''; errorBox.style.display = 'none'; } if (passwordInput) { passwordInput.value = ''; } } function ensureAuthPromise() { if (!authPromise) { authPromise = new Promise(resolve => { authPromiseResolvers.push(resolve); }); } return authPromise; } async function ensureAuthenticated() { if (isTokenValid()) { return true; } showLoginOverlay(); await ensureAuthPromise(); return true; } function handleUnauthorized({ message = '认证已过期,请重新登录', silent = false } = {}) { clearAuthStorage(); authPromise = null; authPromiseResolvers = []; if (!silent) { showLoginOverlay(message); } else { showLoginOverlay(); } return false; } async function apiFetch(url, options = {}) { await ensureAuthenticated(); const opts = { ...options }; const headers = new Headers(options && options.headers ? options.headers : undefined); if (authToken && !headers.has('Authorization')) { headers.set('Authorization', `Bearer ${authToken}`); } opts.headers = headers; const response = await fetch(url, opts); if (response.status === 401) { handleUnauthorized(); throw new Error('未授权访问'); } return response; } async function submitLogin(event) { event.preventDefault(); const passwordInput = document.getElementById('login-password'); const errorBox = document.getElementById('login-error'); const submitBtn = document.querySelector('.login-submit'); if (!passwordInput) { return; } const password = passwordInput.value.trim(); if (!password) { if (errorBox) { errorBox.textContent = '请输入密码'; errorBox.style.display = 'block'; } return; } if (submitBtn) { submitBtn.disabled = true; } try { const response = await fetch('/api/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ password }), }); const result = await response.json().catch(() => ({})); if (!response.ok || !result.token) { if (errorBox) { errorBox.textContent = result.error || '登录失败,请检查密码'; errorBox.style.display = 'block'; } return; } saveAuth(result.token, result.expires_at); hideLoginOverlay(); resolveAuthPromises(true); if (!isAppInitialized) { await bootstrapApp(); } else { await refreshAppData(); } } catch (error) { console.error('登录失败:', error); if (errorBox) { errorBox.textContent = '登录失败,请稍后重试'; errorBox.style.display = 'block'; } } finally { if (submitBtn) { submitBtn.disabled = false; } } } async function refreshAppData(showTaskErrors = false) { await Promise.allSettled([ loadConversations(), loadActiveTasks(showTaskErrors), ]); } async function bootstrapApp() { if (!isAppInitialized) { initializeChatUI(); isAppInitialized = true; } await refreshAppData(); } // 通用工具函数 function getStatusText(status) { const statusMap = { 'pending': '等待中', 'running': '执行中', 'completed': '已完成', 'failed': '失败' }; return statusMap[status] || status; } function formatDuration(ms) { const seconds = Math.floor(ms / 1000); const minutes = Math.floor(seconds / 60); const hours = Math.floor(minutes / 60); if (hours > 0) { return `${hours}小时${minutes % 60}分钟`; } else if (minutes > 0) { return `${minutes}分钟${seconds % 60}秒`; } else { return `${seconds}秒`; } } function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } function formatMarkdown(text) { const sanitizeConfig = { ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'u', 's', 'code', 'pre', 'blockquote', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'ul', 'ol', 'li', 'a', 'img', 'table', 'thead', 'tbody', 'tr', 'th', 'td', 'hr'], ALLOWED_ATTR: ['href', 'title', 'alt', 'src', 'class'], ALLOW_DATA_ATTR: false, }; if (typeof DOMPurify !== 'undefined') { if (typeof marked !== 'undefined' && !/<[a-z][\s\S]*>/i.test(text)) { try { marked.setOptions({ breaks: true, gfm: true, }); let parsedContent = marked.parse(text); return DOMPurify.sanitize(parsedContent, sanitizeConfig); } catch (e) { console.error('Markdown 解析失败:', e); return DOMPurify.sanitize(text, sanitizeConfig); } } else { return DOMPurify.sanitize(text, sanitizeConfig); } } else if (typeof marked !== 'undefined') { try { marked.setOptions({ breaks: true, gfm: true, }); return marked.parse(text); } catch (e) { console.error('Markdown 解析失败:', e); return escapeHtml(text).replace(/\n/g, '
'); } } else { return escapeHtml(text).replace(/\n/g, '
'); } } function setupLoginUI() { const loginForm = document.getElementById('login-form'); if (loginForm) { loginForm.addEventListener('submit', submitLogin); } } async function initializeApp() { setupLoginUI(); const hasStoredAuth = loadAuthFromStorage(); if (hasStoredAuth && isTokenValid()) { try { const response = await apiFetch('/api/auth/validate', { method: 'GET', }); if (response.ok) { hideLoginOverlay(); resolveAuthPromises(true); await bootstrapApp(); return; } } catch (error) { console.warn('本地会话已失效,需重新登录'); } } clearAuthStorage(); showLoginOverlay(); } // 用户菜单控制 function toggleUserMenu() { const dropdown = document.getElementById('user-menu-dropdown'); if (!dropdown) return; const isVisible = dropdown.style.display !== 'none'; dropdown.style.display = isVisible ? 'none' : 'block'; } // 点击页面其他地方时关闭下拉菜单 document.addEventListener('click', function(event) { const dropdown = document.getElementById('user-menu-dropdown'); const avatarBtn = document.querySelector('.user-avatar-btn'); if (dropdown && avatarBtn && !dropdown.contains(event.target) && !avatarBtn.contains(event.target)) { dropdown.style.display = 'none'; } }); // 退出登录 async function logout() { // 关闭下拉菜单 const dropdown = document.getElementById('user-menu-dropdown'); if (dropdown) { dropdown.style.display = 'none'; } try { // 先尝试调用退出API(如果token有效) if (authToken) { const headers = new Headers(); headers.set('Authorization', `Bearer ${authToken}`); await fetch('/api/auth/logout', { method: 'POST', headers: headers, }).catch(() => { // 忽略错误,继续清除本地认证信息 }); } } catch (error) { console.error('退出登录API调用失败:', error); } finally { // 无论如何都清除本地认证信息 clearAuthStorage(); hideLoginOverlay(); showLoginOverlay('已退出登录'); } } // 导出函数供HTML使用 window.toggleUserMenu = toggleUserMenu; window.logout = logout; document.addEventListener('DOMContentLoaded', initializeApp);