当前位置: 首页 > web >正文

Gemini 2.5 Pro 一次测试

您好,您遇到的重定向循环问题,即在 /user/messaging/user/login?return_to=/user/messaging/user/login 之间反复跳转,通常是由于客户端的身份验证状态检查和页面重定向逻辑存在冲突或竞争条件。

在分析了您提供的代码(特别是 main.jsui-messaging.jsworker.js 的相关部分)后,我判断问题主要出在客户端 JavaScript 如何确定用户登录状态以及何时执行页面跳转。当这些逻辑不够集中或执行顺序不当时,就可能出现您描述的循环。

为了解决这个问题,我建议对 main.js 中的 checkLoginStatusdisplayCorrectView 函数进行重构,使其职责更清晰,并调整 /user/messaging 页面的初始化方式。

核心思路:

  1. checkLoginStatus: 此函数将成为判断用户是否登录、是否需要重定向的唯一决策点。它会首先调用 /api/me 检查会话。
    • 如果用户已登录:
      • 若当前在登录/注册页面,则根据 return_to 参数或默认规则跳转到账户页面或目标页面。
      • 若当前在受保护页面(如 /user/account, /user/messaging),则调用 displayCorrectView 更新UI,并继续加载页面内容。
    • 如果用户未登录:
      • 若当前在受保护页面,则重定向到登录页,并附带 return_to 参数。
      • 若当前在公共页面(如登录页),则调用 displayCorrectView 显示相应表单。
  2. displayCorrectView: 此函数将只负责根据 checkLoginStatus 提供的用户数据(或null)来更新页面的显示元素(如顶部栏、侧边栏显隐、表单显隐等),不再执行任何页面跳转逻辑。
  3. /user/messaging 页面初始化: 在 main.jsWorkspaceAppConfigAndInitialize 函数中,将在 checkLoginStatus 完成后,如果确定用户已登录且当前页面是 /user/messaging,才调用 ui-messaging.js 中的 loadMessagingTabData

以下是建议的修改方案:

1. my/src/worker.js
您在 worker.js 中为 /user/messaging 添加的路由和未登录重定向逻辑是正确的,无需更改。

// (现有 worker.js 代码保持不变,特别是 /user/messaging 路由部分)
// ...if (method === 'GET' && path === '/user/messaging') {if (!userEmailFromSession) { // Redirect to login if not authenticatedconst loginUrl = new URL(`${OAUTH_ISSUER_URL(env, request)}/user/login`);loginUrl.searchParams.set('return_to', path); // Return to messaging page after loginreturn Response.redirect(loginUrl.toString(), 302);}return new Response(generateMessagingPageHtml(env), { headers: { 'Content-Type': 'text/html;charset=UTF-8' } });}
// ...

2. cdn/js/main.js
这是主要的修改部分。我们将重构 checkLoginStatusdisplayCorrectView,并调整初始化流程。

let TURNSTILE_SITE_KEY = '1x00000000000000000000AA';
const activeTurnstileWidgets = new Map();
let loginEmailFor2FA = null;
let currentUserData = null;
let messageArea, authSection, loggedInSection, loginFormEl, registerFormEl;
let topBarUserEmailEl, topBarUserUsernameEl, topBarUserInfoEl, topBarAuthButtonsEl, topBarLogoutButtonEl, userMenuButtonEl, userDropdownMenuEl, topBarAccountLinkEl;
let sidebarEl, sidebarToggleEl, mainContentContainerEl, sidebarOverlayEl;
let accountTabLinks = [];
let tabPanes = [];
let themeToggleButton, themeToggleDarkIcon, themeToggleLightIcon;
let unreadMessagesIndicator;
let appWrapper;
let topBarMessagingButton;
let userPresenceSocket = null;function renderTurnstile(containerElement) {if (!containerElement || !window.turnstile || typeof window.turnstile.render !== 'function') return;if (!TURNSTILE_SITE_KEY) { return; }if (activeTurnstileWidgets.has(containerElement)) {try { turnstile.remove(activeTurnstileWidgets.get(containerElement)); } catch (e) { }activeTurnstileWidgets.delete(containerElement);}containerElement.innerHTML = '';try {const widgetId = turnstile.render(containerElement, {sitekey: TURNSTILE_SITE_KEY,callback: (token) => {const specificCallbackName = containerElement.getAttribute('data-callback');if (specificCallbackName && typeof window[specificCallbackName] === 'function') {window[specificCallbackName](token);}}});if (widgetId) activeTurnstileWidgets.set(containerElement, widgetId);} catch (e) { }
}
function removeTurnstile(containerElement) {if (containerElement && activeTurnstileWidgets.has(containerElement)) {try { turnstile.remove(activeTurnstileWidgets.get(containerElement)); } catch (e) { }activeTurnstileWidgets.delete(containerElement);}
}
function resetTurnstileInContainer(containerElement) {if (containerElement && activeTurnstileWidgets.has(containerElement)) {try { turnstile.reset(activeTurnstileWidgets.get(containerElement)); } catch (e) { }} else if (containerElement) {renderTurnstile(containerElement);}
}
function clearMessages() { if (messageArea) { messageArea.textContent = ''; messageArea.className = 'message hidden'; } }
function showMessage(text, type = 'error', isHtml = false) {if (messageArea) {if (isHtml) { messageArea.innerHTML = text; } else { messageArea.textContent = text; }messageArea.className = 'message ' + type; messageArea.classList.remove('hidden');const mainContent = document.getElementById('main-content');if (mainContent) {mainContent.scrollTo({ top: 0, behavior: 'smooth' });} else {window.scrollTo({ top: 0, behavior: 'smooth' });}}
}
async function apiCall(endpoint, method = 'GET', body = null) {const options = { method, headers: {}, credentials: 'include' };if (body) { options.headers['Content-Type'] = 'application/json'; options.body = JSON.stringify(body); }try {const response = await fetch(endpoint, options);let resultData = {};const contentType = response.headers.get("content-type");if (contentType && contentType.includes("application/json") && response.status !== 204) {try { resultData = await response.json(); } catch (e) { }}return { ok: response.ok, status: response.status, data: resultData };} catch (e) {showMessage('发生网络或服务器错误,请稍后重试。', 'error');return { ok: false, status: 0, data: { error: '网络错误' } };}
}
function updateUnreadMessagesIndicatorUI(count) {const localUnreadIndicator = document.getElementById('unread-messages-indicator');const localMessagingButton = document.getElementById('top-bar-messaging-button');if (!localUnreadIndicator || !localMessagingButton) return;if (count > 0) {localUnreadIndicator.textContent = count;localUnreadIndicator.classList.remove('hidden');localMessagingButton.classList.add('active');} else {localUnreadIndicator.textContent = '';localUnreadIndicator.classList.add('hidden');localMessagingButton.classList.remove('active');}
}
function connectUserPresenceWebSocket() {if (userPresenceSocket && (userPresenceSocket.readyState === WebSocket.OPEN || userPresenceSocket.readyState === WebSocket.CONNECTING)) {return;}if (!currentUserData || !currentUserData.email) {return;}const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';const wsUrl = `${protocol}//${window.location.host}/api/ws/user`;userPresenceSocket = new WebSocket(wsUrl);userPresenceSocket.onopen = () => {if (userPresenceSocket && userPresenceSocket.readyState === WebSocket.OPEN) {userPresenceSocket.send(JSON.stringify({type: "REQUEST_INITIAL_STATE"}));}};userPresenceSocket.onmessage = (event) => {try {const message = JSON.parse(event.data);if (message.type === "CONVERSATIONS_LIST") {if (typeof window.handleConversationsListUpdate === 'function') {window.handleConversationsListUpdate(message.data);}} else if (message.type === "UNREAD_COUNT_TOTAL") {updateUnreadMessagesIndicatorUI(message.data.unread_count);} else if (message.type === "CONVERSATION_UPDATE") {if (typeof window.handleSingleConversationUpdate === 'function') {window.handleSingleConversationUpdate(message.data);}} else if (message.type === "ERROR") {if (typeof window.showMessage === 'function') {window.showMessage(message.data || '从服务器收到错误消息。', 'error');}if (typeof window.handleConversationsListUpdate === 'function') {window.handleConversationsListUpdate([]);}}} catch (e) {}};userPresenceSocket.onclose = (event) => {userPresenceSocket = null;if (currentUserData && currentUserData.email) {setTimeout(connectUserPresenceWebSocket, 5000);}};userPresenceSocket.onerror = (error) => {};
}
function applyTheme(isDark) {document.body.classList.toggle('dark-mode', isDark);if (themeToggleDarkIcon) themeToggleDarkIcon.style.display = isDark ? 'block' : 'none';if (themeToggleLightIcon) themeToggleLightIcon.style.display = isDark ? 'none' : 'block';const qrCodeDisplay = document.getElementById('qrcode-display');const otpAuthUriTextDisplay = document.getElementById('otpauth-uri-text-display');if (qrCodeDisplay && typeof QRCode !== 'undefined' && otpAuthUriTextDisplay) {const otpauthUri = otpAuthUriTextDisplay.textContent;if (otpauthUri && qrCodeDisplay.innerHTML.includes('canvas')) {qrCodeDisplay.innerHTML = '';new QRCode(qrCodeDisplay, {text: otpauthUri, width: 180, height: 180,colorDark: isDark ? "#e2e8f0" : "#000000",colorLight: "#ffffff",correctLevel: QRCode.CorrectLevel.H});}}
}
function toggleSidebar() {if (sidebarEl && sidebarOverlayEl && appWrapper) {const isOpen = sidebarEl.classList.toggle('open');sidebarOverlayEl.classList.toggle('hidden', !isOpen);appWrapper.classList.toggle('sidebar-open-app', isOpen);}
}function activateTab(tabLinkToActivate) {if (!tabLinkToActivate || window.location.pathname !== '/user/account') {return;}let paneIdToActivate = tabLinkToActivate.dataset.paneId;if (accountTabLinks) {accountTabLinks.forEach(link => link.classList.remove('selected'));}tabLinkToActivate.classList.add('selected');if (!mainContentContainerEl) {return;}tabPanes.forEach(pane => {const turnstileDivsInPane = pane.querySelectorAll('.cf-turnstile');if (pane.id === paneIdToActivate) {pane.classList.remove('hidden');turnstileDivsInPane.forEach(div => renderTurnstile(div));if (pane.id === 'tab-content-api-keys' && typeof window.initializeApiKeysTab === 'function') window.initializeApiKeysTab();if (pane.id === 'tab-content-my-applications' && typeof window.loadOauthAppsTabData === 'function') window.loadOauthAppsTabData();if (pane.id === 'tab-content-security-settings' && typeof window.initializeSecuritySettings === 'function') {if (currentUserData) {window.initializeSecuritySettings(currentUserData);} else {apiCall('/api/me').then(response => {if (response.ok && response.data) {currentUserData = response.data; // Should be already set by checkLoginStatuswindow.initializeSecuritySettings(currentUserData);}});}}} else {turnstileDivsInPane.forEach(div => removeTurnstile(div));pane.classList.add('hidden');}});clearMessages();const newlyCreatedApiKeyDisplayDiv = document.getElementById('newly-created-api-key-display');const newOauthClientCredentialsDiv = document.getElementById('new-oauth-client-credentials');if (newlyCreatedApiKeyDisplayDiv) newlyCreatedApiKeyDisplayDiv.classList.add('hidden');if (newOauthClientCredentialsDiv) newOauthClientCredentialsDiv.classList.add('hidden');if (window.innerWidth < 769 && sidebarEl && sidebarEl.classList.contains('open')) {toggleSidebar();}
}async function checkLoginStatus() {const { ok, data } = await apiCall('/api/me');const isLoggedIn = ok && data && data.email;const currentPath = window.location.pathname;const urlParams = new URLSearchParams(window.location.search);const returnToFromQuery = urlParams.get('return_to');currentUserData = isLoggedIn ? data : null;if (isLoggedIn) {if (currentPath === '/user/login' || currentPath === '/user/register' || currentPath === '/') {if (returnToFromQuery) {window.location.href = decodeURIComponent(returnToFromQuery);} else {window.location.href = '/user/account';}return true; // Indicates a redirect is happening}displayCorrectView(currentUserData); // Update UI for logged-in state} else { // Not logged inconst protectedPaths = ['/user/account', '/user/messaging', '/user/help'];if (protectedPaths.includes(currentPath)) {// Preserve existing query params and hash when redirecting to loginconst fullReturnPath = currentPath + window.location.search + window.location.hash;const loginRedirectURL = `/user/login?return_to=${encodeURIComponent(fullReturnPath)}`;window.location.href = loginRedirectURL;return true; // Indicates a redirect is happening}displayCorrectView(null); // Update UI for logged-out state}// Handle specific query params like 'registered' only if no redirect happenedif (urlParams.has('registered') && currentPath === '/user/login' && !isLoggedIn) {showMessage('注册成功!请使用您的邮箱或用户名登录。', 'success');const newUrl = new URL(window.location);newUrl.searchParams.delete('registered');window.history.replaceState({}, document.title, newUrl.toString());}return false; // No redirect happened based on auth state
}function displayCorrectView(userData) {clearMessages();// currentUserData is already set by checkLoginStatusconst isLoggedIn = !!userData?.email;document.querySelectorAll('.cf-turnstile').forEach(div => removeTurnstile(div)); // Clear all turnstilesif (topBarUserInfoEl) topBarUserInfoEl.classList.toggle('hidden', !isLoggedIn);if (isLoggedIn && topBarUserEmailEl) topBarUserEmailEl.textContent = userData.email || '未知邮箱';if (isLoggedIn && topBarUserUsernameEl) topBarUserUsernameEl.textContent = userData.username || '用户';if (topBarAuthButtonsEl) topBarAuthButtonsEl.classList.toggle('hidden', isLoggedIn);if (topBarMessagingButton) {topBarMessagingButton.classList.toggle('hidden', !isLoggedIn);if(isLoggedIn && window.location.pathname !== '/user/messaging') {topBarMessagingButton.classList.remove('active');} else if (isLoggedIn && window.location.pathname === '/user/messaging') {topBarMessagingButton.classList.add('active');}}if (appWrapper) {appWrapper.classList.toggle('logged-in-layout', isLoggedIn);appWrapper.classList.toggle('logged-out-layout', !isLoggedIn);appWrapper.classList.remove('messaging-page-layout'); // Remove by defaultappWrapper.classList.remove('sidebar-open-app'); // Ensure sidebar closed class is not stuckif (sidebarOverlayEl) sidebarOverlayEl.classList.add('hidden');}if (isLoggedIn) {const currentPath = window.location.pathname;if (sidebarEl) {sidebarEl.classList.toggle('hidden', currentPath === '/user/messaging' || currentPath === '/user/help');}if (sidebarToggleEl) { // Ensure sidebar toggle is visible for account page, hidden for otherssidebarToggleEl.classList.toggle('hidden', currentPath === '/user/messaging' || currentPath === '/user/help');}if(authSection) authSection.classList.add('hidden');if (currentPath === '/user/account') {if(loggedInSection) loggedInSection.classList.remove('hidden');if (typeof window.initializePersonalInfoForm === 'function') window.initializePersonalInfoForm(userData);const defaultTabId = 'tab-personal-info';let tabToActivateId = defaultTabId;if (window.location.hash) {const hashTabId = window.location.hash.substring(1);const potentialTabLink = document.getElementById(hashTabId);if (potentialTabLink && potentialTabLink.classList.contains('sidebar-link')) {tabToActivateId = hashTabId;}}const tabLinkToActivate = document.getElementById(tabToActivateId) || document.getElementById(defaultTabId);if (tabLinkToActivate) activateTab(tabLinkToActivate);} else if (currentPath === '/user/messaging') {if(appWrapper) appWrapper.classList.add('messaging-page-layout');if(loggedInSection) loggedInSection.classList.add('hidden'); // Messaging page has its own content structure} else { // e.g. /user/helpif(loggedInSection) loggedInSection.classList.add('hidden');}if (typeof window.connectUserPresenceWebSocket === 'function') {connectUserPresenceWebSocket();}} else { // Not logged inif (userPresenceSocket && userPresenceSocket.readyState === WebSocket.OPEN) userPresenceSocket.close();if (sidebarEl) sidebarEl.classList.add('hidden');if (sidebarToggleEl) sidebarToggleEl.classList.remove('hidden'); // Show toggle if it was hiddenif(loggedInSection) loggedInSection.classList.add('hidden');if(authSection) authSection.classList.remove('hidden');const loginFormContainer = document.getElementById('login-form');const registerFormContainer = document.getElementById('register-form');const login2FASection = document.getElementById('login-2fa-section');const currentPath = window.location.pathname;if (currentPath === '/' || currentPath === '/user/login') {if(loginFormContainer) { loginFormContainer.classList.remove('hidden'); if(loginFormEl) loginFormEl.reset(); renderTurnstile(loginFormContainer.querySelector('.cf-turnstile')); }if(registerFormContainer) registerFormContainer.classList.add('hidden');} else if (currentPath === '/user/register') {if(loginFormContainer) loginFormContainer.classList.add('hidden');if(registerFormContainer) { registerFormContainer.classList.remove('hidden'); if(registerFormEl) registerFormEl.reset(); renderTurnstile(registerFormContainer.querySelector('.cf-turnstile'));}}if(login2FASection) login2FASection.classList.add('hidden');loginEmailFor2FA = null;updateUnreadMessagesIndicatorUI(0);}
}async function fetchAppConfigAndInitialize() {try {const response = await apiCall('/api/config');if (response.ok && response.data.turnstileSiteKey) {TURNSTILE_SITE_KEY = response.data.turnstileSiteKey;}} catch (error) { }const redirected = await checkLoginStatus(); // Await and check if redirect happenedif (!redirected) { // Only proceed if checkLoginStatus didn't redirectconst currentPath = window.location.pathname;if (currentPath === '/user/messaging' && currentUserData && typeof window.loadMessagingTabData === 'function') {window.loadMessagingTabData();}// If on /user/account, tab activation is handled within displayCorrectView}
}document.addEventListener('DOMContentLoaded', () => {messageArea = document.getElementById('message-area');authSection = document.getElementById('auth-section');loggedInSection = document.getElementById('logged-in-section');loginFormEl = document.getElementById('login-form-el');registerFormEl = document.getElementById('register-form-el');appWrapper = document.getElementById('app-wrapper');topBarUserEmailEl = document.getElementById('top-bar-user-email');topBarUserUsernameEl = document.getElementById('top-bar-user-username');topBarUserInfoEl = document.getElementById('top-bar-user-info');topBarAuthButtonsEl = document.getElementById('top-bar-auth-buttons');topBarLogoutButtonEl = document.getElementById('top-bar-logout-button');userMenuButtonEl = document.getElementById('user-menu-button');userDropdownMenuEl = document.getElementById('user-dropdown-menu');topBarAccountLinkEl = document.getElementById('top-bar-account-link');topBarMessagingButton = document.getElementById('top-bar-messaging-button');sidebarEl = document.getElementById('sidebar');sidebarToggleEl = document.getElementById('sidebar-toggle');const mainContent = document.getElementById('main-content');if (mainContent) {mainContentContainerEl = mainContent.querySelector('.container');}sidebarOverlayEl = document.getElementById('sidebar-overlay');if (document.getElementById('account-tabs')) {accountTabLinks = Array.from(document.querySelectorAll('#account-tabs .sidebar-link'));}if (mainContentContainerEl && window.location.pathname === '/user/account') { // Only query tabPanes if on account pagetabPanes = Array.from(mainContentContainerEl.querySelectorAll('.tab-pane'));}themeToggleButton = document.getElementById('theme-toggle-button');themeToggleDarkIcon = document.getElementById('theme-toggle-dark-icon');themeToggleLightIcon = document.getElementById('theme-toggle-light-icon');unreadMessagesIndicator = document.getElementById('unread-messages-indicator');let isDarkMode = localStorage.getItem('theme') === 'dark' || (!localStorage.getItem('theme') && window.matchMedia('(prefers-color-scheme: dark)').matches);applyTheme(isDarkMode);if (themeToggleButton) {themeToggleButton.addEventListener('click', () => {isDarkMode = !isDarkMode;localStorage.setItem('theme', isDarkMode ? 'dark' : 'light');applyTheme(isDarkMode);});}window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => {if (localStorage.getItem('theme') === null) {isDarkMode = e.matches;applyTheme(isDarkMode);}});accountTabLinks.forEach(link => {link.addEventListener('click', (event) => {event.preventDefault();if (window.location.pathname !== '/user/account') {window.location.href = '/user/account' + event.currentTarget.hash;} else {activateTab(event.currentTarget);window.location.hash = event.currentTarget.hash.substring(1); // use hash from link directly}});});if (topBarMessagingButton) {topBarMessagingButton.addEventListener('click', () => {if (window.location.pathname === '/user/messaging') {if (typeof window.loadMessagingTabData === 'function' && currentUserData) {window.loadMessagingTabData();}} else {window.location.href = '/user/messaging';}});}if (sidebarToggleEl) {sidebarToggleEl.addEventListener('click', toggleSidebar);}if (sidebarOverlayEl) sidebarOverlayEl.addEventListener('click', toggleSidebar);if (userMenuButtonEl && userDropdownMenuEl) {userMenuButtonEl.addEventListener('click', (event) => {event.stopPropagation();userDropdownMenuEl.classList.toggle('hidden');});document.addEventListener('click', (event) => {if (userDropdownMenuEl && !userDropdownMenuEl.classList.contains('hidden') && userMenuButtonEl && !userMenuButtonEl.contains(event.target) && !userDropdownMenuEl.contains(event.target)) {userDropdownMenuEl.classList.add('hidden');}});}if (topBarAccountLinkEl) {topBarAccountLinkEl.addEventListener('click', (e) => {e.preventDefault();if (window.location.pathname !== '/user/account') {window.location.href = '/user/account#tab-personal-info';} else {const personalInfoTabLink = document.getElementById('tab-personal-info');if (personalInfoTabLink) activateTab(personalInfoTabLink);window.location.hash = 'tab-personal-info';}if (userDropdownMenuEl) userDropdownMenuEl.classList.add('hidden');});}if (loginFormEl) loginFormEl.addEventListener('submit', (event) => handleAuth(event, 'login'));if (registerFormEl) registerFormEl.addEventListener('submit', (event) => handleAuth(event, 'register'));if (topBarLogoutButtonEl) topBarLogoutButtonEl.addEventListener('click', handleLogout);fetchAppConfigAndInitialize();window.addEventListener('hashchange', () => {if (window.location.pathname === '/user/account') {const hash = window.location.hash.substring(1);const tabLinkToActivateByHash = document.getElementById(hash);if (tabLinkToActivateByHash && tabLinkToActivateByHash.classList.contains('sidebar-link')) {activateTab(tabLinkToActivateByHash);} else if (!hash) { // If hash is empty, default to personal-infoconst defaultTabLink = document.getElementById('tab-personal-info');if (defaultTabLink) activateTab(defaultTabLink);}}});
});
window.handleAuth = async function(event, type) {event.preventDefault(); clearMessages();const form = event.target;const turnstileContainer = form.querySelector('.cf-turnstile');const turnstileToken = form.querySelector('[name="cf-turnstile-response"]')?.value;const login2FASection = document.getElementById('login-2fa-section');const loginTotpCodeInput = document.getElementById('login-totp-code');if (!turnstileToken && turnstileContainer) {showMessage('人机验证失败,请刷新页面或稍后重试。', 'error');if (turnstileContainer) resetTurnstileInContainer(turnstileContainer);return;}let endpoint = '', requestBody = {};if (type === 'login') {const identifier = form.elements['identifier'].value, password = form.elements['password'].value;const totpCode = loginTotpCodeInput ? loginTotpCodeInput.value : '';if (!identifier || !password) { showMessage('邮箱/用户名和密码不能为空。'); return; }if (loginEmailFor2FA && totpCode) { endpoint = '/api/login/2fa-verify'; requestBody = { email: loginEmailFor2FA, totpCode }; }else { endpoint = '/api/login'; requestBody = { identifier, password, turnstileToken }; }} else { // registerendpoint = '/api/register';const {email, username, password, confirmPassword, phoneNumber} = Object.fromEntries(new FormData(form));if (password !== confirmPassword) { showMessage('两次输入的密码不一致。'); return; }if (!email || !username || !password) { showMessage('邮箱、用户名和密码为必填项。'); return; }if (password.length < 6) { showMessage('密码至少需要6个字符。'); return; }requestBody = { email, username, password, confirmPassword, phoneNumber, turnstileToken };}const { ok, status, data } = await apiCall(endpoint, 'POST', requestBody);if (turnstileContainer) resetTurnstileInContainer(turnstileContainer);if (ok && data.success) {if (data.twoFactorRequired && data.email) {showMessage('需要两步验证。请输入验证码。', 'info'); loginEmailFor2FA = data.email;if(login2FASection) login2FASection.classList.remove('hidden'); if(loginTotpCodeInput) loginTotpCodeInput.focus();} else {form.reset();if(login2FASection) login2FASection.classList.add('hidden'); loginEmailFor2FA = null;const urlParams = new URLSearchParams(window.location.search);const returnTo = urlParams.get('return_to');if (returnTo) {window.location.href = decodeURIComponent(returnTo);} else if (type === 'register') {window.location.href = '/user/login?registered=true';}else {window.location.href = '/user/account#tab-personal-info';}}} else {showMessage(data.error || ('操作失败 (' + status + ')'), 'error', data.details ? true : false);if (type === 'login' && loginEmailFor2FA && status !== 401 && login2FASection) { login2FASection.classList.remove('hidden'); }else if (status === 401 && data.error === '两步验证码无效' && login2FASection) {login2FASection.classList.remove('hidden'); if(loginTotpCodeInput) { loginTotpCodeInput.value = ''; loginTotpCodeInput.focus(); }} else if (login2FASection) { login2FASection.classList.add('hidden'); loginEmailFor2FA = null; }}
};
window.handleLogout = async function() {clearMessages();if (userPresenceSocket && userPresenceSocket.readyState === WebSocket.OPEN) {userPresenceSocket.close();}if (typeof window.closeActiveConversationSocket === 'function') {window.closeActiveConversationSocket();}await apiCall('/api/logout', 'POST');currentUserData = null;window.location.href = '/user/login';
};
window.turnstileCallbackLogin = function(token) { };
window.turnstileCallbackRegister = function(token) { };
window.turnstileCallbackPasteApi = function(token) { };
window.turnstileCallbackCloudPc = function(token) { };
window.turnstileCallbackOauthClient = function(token) { };
window.copyToClipboard = function(text, itemNameToCopy = '内容') {if (!text) { showMessage('没有可复制的'+itemNameToCopy+'。', 'warning'); return; }navigator.clipboard.writeText(text).then(() => {showMessage(itemNameToCopy + '已复制到剪贴板!', 'success');}).catch(err => {showMessage('复制失败: ' + err, 'error');});
};
window.escapeHtml = function(unsafe) {if (unsafe === null || typeof unsafe === 'undefined') return '';return String(unsafe).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
};
window.apiCall = apiCall;
window.showMessage = showMessage;
window.clearMessages = clearMessages;
window.renderTurnstile = renderTurnstile;
window.removeTurnstile = removeTurnstile;
window.resetTurnstileInContainer = resetTurnstileInContainer;
window.checkLoginStatus = checkLoginStatus;
window.isValidEmail = function(email) {if (typeof email !== 'string') return false;const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;return emailRegex.test(email);
};
window.updateUnreadMessagesIndicatorUI = updateUnreadMessagesIndicatorUI;
window.connectUserPresenceWebSocket = connectUserPresenceWebSocket;

3. cdn/js/ui-messaging.js
移除底部的 DOMContentLoaded 监听器,因为初始化现在由 main.js 中的 WorkspaceAppConfigAndInitialize 在确认登录后触发。

let contactSearchInput, btnStartNewConversation;
let conversationsListUl, messagesAreaDiv, messagesListDiv, messageInputAreaDiv, messageInputTextarea, btnSendMessage;
let emptyMessagesPlaceholder;
let newConversationEmailInput;
let currentActiveConversationD1Id = null;
let currentUserEmail = null;
let allConversationsCache = [];
let currentConversationMessages = [];
let displayedMessagesCount = 0;
let conversationSocket = null;
let messageIntersectionObserver = null;
let notificationPermissionGranted = false;
let messagesListWrapper, messageLoader;
let isLoadingMoreMessages = false;
let hasMoreMessagesToLoad = true;function formatMillisecondsTimestamp(timestamp) {const date = new Date(timestamp);const year = date.getFullYear();const month = (date.getMonth() + 1).toString().padStart(2, '0');const day = date.getDate().toString().padStart(2, '0');const hours = date.getHours().toString().padStart(2, '0');const minutes = date.getMinutes().toString().padStart(2, '0');const seconds = date.getSeconds().toString().padStart(2, '0');return `${year}/${month}/${day} ${hours}:${minutes}:${seconds}`;
}async function requestNotificationPermission() {if (!('Notification' in window)) {return;}if (Notification.permission === 'granted') {notificationPermissionGranted = true;return;}if (Notification.permission !== 'denied') {const permission = await Notification.requestPermission();if (permission === 'granted') {notificationPermissionGranted = true;}}
}function showDesktopNotification(title, options, conversationIdToOpen) {if (!notificationPermissionGranted || document.hasFocus()) {return;}const notification = new Notification(title, options);notification.onclick = () => {window.focus();if (window.location.pathname !== '/user/messaging') {window.location.href = '/user/messaging';}const convElement = conversationsListUl?.querySelector(`li[data-conversation-id="${conversationIdToOpen}"]`);if (convElement) {convElement.click();}notification.close();};
}function initializeMessageObserver() {if (messageIntersectionObserver) {messageIntersectionObserver.disconnect();}messageIntersectionObserver = new IntersectionObserver((entries, observer) => {entries.forEach(entry => {if (entry.isIntersecting) {const messageElement = entry.target;const messageId = messageElement.dataset.messageId;const isUnreadForCurrentUser = messageElement.classList.contains('unread-for-current-user');if (messageId && isUnreadForCurrentUser && conversationSocket && conversationSocket.readyState === WebSocket.OPEN) {conversationSocket.send(JSON.stringify({type: "MESSAGE_SEEN",data: { message_id: messageId }}));messageElement.classList.remove('unread-for-current-user');observer.unobserve(messageElement);}}});}, { threshold: 0.8 });
}function observeMessageElement(element) {if (messageIntersectionObserver && element) {messageIntersectionObserver.observe(element);}
}function initializeMessagingTab() { // This name is fine, it initializes elements for the messaging page/tabcontactSearchInput = document.getElementById('contact-search-input');btnStartNewConversation = document.getElementById('btn-start-new-conversation');newConversationEmailInput = document.getElementById('new-conversation-email');conversationsListUl = document.getElementById('conversations-list');messagesAreaDiv = document.getElementById('messages-area');messagesListWrapper = document.getElementById('messages-list-wrapper');messageLoader = document.getElementById('message-loader');messagesListDiv = document.getElementById('messages-list');messageInputAreaDiv = document.getElementById('message-input-area');messageInputTextarea = document.getElementById('message-input');btnSendMessage = document.getElementById('btn-send-message');emptyMessagesPlaceholder = messagesListDiv?.querySelector('.empty-messages-placeholder');if (btnStartNewConversation && newConversationEmailInput) {btnStartNewConversation.removeEventListener('click', handleNewConversationButtonClick);btnStartNewConversation.addEventListener('click', handleNewConversationButtonClick);newConversationEmailInput.removeEventListener('keypress', handleNewConversationInputKeypress);newConversationEmailInput.addEventListener('keypress', handleNewConversationInputKeypress);}if(contactSearchInput) {contactSearchInput.removeEventListener('input', handleContactSearch);contactSearchInput.addEventListener('input', handleContactSearch);}if (btnSendMessage) {btnSendMessage.removeEventListener('click', handleSendMessageClick);btnSendMessage.addEventListener('click', handleSendMessageClick);}if (messageInputTextarea) {messageInputTextarea.removeEventListener('keypress', handleMessageInputKeypress);messageInputTextarea.addEventListener('keypress', handleMessageInputKeypress);messageInputTextarea.removeEventListener('input', handleMessageInputAutosize);messageInputTextarea.addEventListener('input', handleMessageInputAutosize);}if (messagesListWrapper) {messagesListWrapper.removeEventListener('scroll', handleMessageScroll);messagesListWrapper.addEventListener('scroll', handleMessageScroll);}initializeMessageObserver();requestNotificationPermission();
}function handleMessageScroll() {if (messagesListWrapper.scrollTop === 0 && hasMoreMessagesToLoad && !isLoadingMoreMessages && conversationSocket && conversationSocket.readyState === WebSocket.OPEN) {loadMoreMessages();}
}async function loadMoreMessages() {if (isLoadingMoreMessages || !hasMoreMessagesToLoad) return;isLoadingMoreMessages = true;if (messageLoader) messageLoader.classList.remove('hidden');conversationSocket.send(JSON.stringify({type: "LOAD_MORE_MESSAGES",data: { currentlyLoadedCount: displayedMessagesCount }}));
}function handleNewConversationButtonClick() {newConversationEmailInput = newConversationEmailInput || document.getElementById('new-conversation-email');if (newConversationEmailInput) {const emailValue = newConversationEmailInput.value.trim();handleStartNewConversation(emailValue);}
}function handleNewConversationInputKeypress(event) {if (event.key === 'Enter') {newConversationEmailInput = newConversationEmailInput || document.getElementById('new-conversation-email');if (newConversationEmailInput) {event.preventDefault();const emailValue = newConversationEmailInput.value.trim();handleStartNewConversation(emailValue);}}
}function handleMessageInputKeypress(event) {if (event.key === 'Enter' && !event.shiftKey) {event.preventDefault();handleSendMessageClick();}
}
function handleMessageInputAutosize() {this.style.height = 'auto';this.style.height = (this.scrollHeight) + 'px';
}function closeActiveConversationSocket() {if (messageIntersectionObserver) {messageIntersectionObserver.disconnect();}if (conversationSocket && (conversationSocket.readyState === WebSocket.OPEN || conversationSocket.readyState === WebSocket.CONNECTING)) {conversationSocket.close();}conversationSocket = null;currentActiveConversationD1Id = null;currentConversationMessages = [];displayedMessagesCount = 0;hasMoreMessagesToLoad = true;isLoadingMoreMessages = false;if (messageLoader) messageLoader.classList.add('hidden');
}
window.closeActiveConversationSocket = closeActiveConversationSocket;async function loadMessagingTabData() { // This function now loads data for the dedicated /user/messaging pageinitializeMessagingTab(); // Initialize DOM elementsif (typeof window.clearMessages === 'function') window.clearMessages();conversationsListUl = conversationsListUl || document.getElementById('conversations-list');messagesListDiv = messagesListDiv || document.getElementById('messages-list');emptyMessagesPlaceholder = emptyMessagesPlaceholder || messagesListDiv?.querySelector('.empty-messages-placeholder');messageInputAreaDiv = messageInputAreaDiv || document.getElementById('message-input-area');messageLoader = messageLoader || document.getElementById('message-loader');if (conversationsListUl) {conversationsListUl.innerHTML = '<p class="placeholder-text">正在加载对话...</p>';}// currentUserData should be set by main.js's checkLoginStatus before this is calledif (window.currentUserData && window.currentUserData.email) {currentUserEmail = window.currentUserData.email;} else {// This case should ideally not be hit if main.js handles redirects correctlyif (conversationsListUl) conversationsListUl.innerHTML = '<p class="placeholder-text" style="color: var(--danger-color);">无法加载用户信息,请重新登录以查看私信。</p>';resetActiveConversationUIOnly();if(typeof window.showMessage === 'function') window.showMessage("用户未登录或会话已过期,请重新登录。", "error");// Redirect should have happened in main.js, but as a fallback:// setTimeout(() => { window.location.href = '/user/login?return_to=/user/messaging'; }, 2000);return;}if (currentActiveConversationD1Id) {const activeConv = allConversationsCache.find(c => c.conversation_id === currentActiveConversationD1Id);if (activeConv) {await handleConversationClick(currentActiveConversationD1Id, activeConv.other_participant_email);} else {resetActiveConversationUIOnly();}} else {resetActiveConversationUIOnly();}let wasSocketAlreadyOpen = false;if (window.userPresenceSocket && window.userPresenceSocket.readyState === WebSocket.OPEN) {wasSocketAlreadyOpen = true;}if (typeof window.connectUserPresenceWebSocket === 'function') {window.connectUserPresenceWebSocket(); // Ensure user presence socket is connected} else {if (conversationsListUl) conversationsListUl.innerHTML = '<p class="placeholder-text" style="color: var(--danger-color);">消息服务连接功能不可用。</p>';return;}// If user presence socket was already open or just connected, request initial stateif (window.userPresenceSocket && window.userPresenceSocket.readyState === WebSocket.OPEN) {window.userPresenceSocket.send(JSON.stringify({type: "REQUEST_INITIAL_STATE"}));} else { // If it's still connecting, wait a moment and trysetTimeout(() => {if (window.userPresenceSocket && window.userPresenceSocket.readyState === WebSocket.OPEN) {window.userPresenceSocket.send(JSON.stringify({type: "REQUEST_INITIAL_STATE"}));} else {if (conversationsListUl) conversationsListUl.innerHTML = '<p class="placeholder-text" style="color: var(--danger-color);">无法连接到用户状态服务。</p>';}}, 1000);}
}function displayConversations(conversations) {if (typeof window.escapeHtml !== 'function') {window.escapeHtml = (unsafe) => {if (typeof unsafe !== 'string') return '';return unsafe.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");};}conversationsListUl = conversationsListUl || document.getElementById('conversations-list');if (!conversationsListUl) return;if (!currentUserEmail) {if (window.currentUserData && window.currentUserData.email) {currentUserEmail = window.currentUserData.email;} else {conversationsListUl.innerHTML = '<p class="placeholder-text" style="color: var(--text-color-muted);">当前用户信息不可用,无法显示对话列表。</p>';return;}}const currentScrollTop = conversationsListUl.scrollTop;conversationsListUl.innerHTML = '';const sortedConversations = conversations.sort((a,b) => (b.last_message_at || 0) - (a.last_message_at || 0));if (sortedConversations.length === 0) {let emptyMessage = '<p class="placeholder-text" style="color: var(--text-color-muted);">没有对话记录。尝试发起新对话吧!</p>';if (contactSearchInput && contactSearchInput.value.trim() !== '') {emptyMessage = '<p class="placeholder-text" style="color: var(--text-color-muted);">未找到相关联系人。</p>';}conversationsListUl.innerHTML = emptyMessage;return;}let html = '';try {sortedConversations.forEach(conv => {const otherParticipantDisplay = window.escapeHtml(conv.other_participant_username || conv.other_participant_email);let lastMessagePreview = conv.last_message_content ? conv.last_message_content : '<i>开始聊天吧!</i>';if (typeof window.marked === 'function' && conv.last_message_content) {lastMessagePreview = window.marked.parse(conv.last_message_content, { sanitize: true, breaks: true }).replace(/<[^>]*>?/gm, '');}lastMessagePreview = window.escapeHtml(lastMessagePreview);if (lastMessagePreview.length > 25) lastMessagePreview = lastMessagePreview.substring(0, 22) + "...";const lastMessageTimeRaw = conv.last_message_at;let lastMessageTimeFormatted = '';if (lastMessageTimeRaw) {try {const date = new Date(lastMessageTimeRaw);const today = new Date();const yesterday = new Date(today);yesterday.setDate(today.getDate() - 1);if (date.toDateString() === today.toDateString()) {lastMessageTimeFormatted = date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });} else if (date.toDateString() === yesterday.toDateString()) {lastMessageTimeFormatted = '昨天';} else {lastMessageTimeFormatted = date.toLocaleDateString([], { month: '2-digit', day: '2-digit' });}} catch (e) { lastMessageTimeFormatted = ''; }}const unreadCount = conv.unread_count > 0 ? `<span class="unread-badge">${conv.unread_count}</span>` : '';const isActive = conv.conversation_id === currentActiveConversationD1Id ? 'selected' : '';const avatarInitial = otherParticipantDisplay.charAt(0).toUpperCase();html += `
<li data-conversation-id="${conv.conversation_id}" data-other-participant-email="${window.escapeHtml(conv.other_participant_email)}" class="${isActive}" title="与 ${otherParticipantDisplay} 的对话">
<div class="contact-avatar">${avatarInitial}</div>
<div class="contact-info">
<span class="contact-name">${otherParticipantDisplay}</span>
<span class="contact-last-message">${conv.last_message_sender === currentUserEmail ? '你: ' : ''}${lastMessagePreview}</span>
</div>
<div class="contact-meta">
<span class="contact-time">${lastMessageTimeFormatted}</span>
${unreadCount}
</div>
</li>`;});} catch (e) {conversationsListUl.innerHTML = '<p class="placeholder-text" style="color: var(--danger-color);">渲染对话列表时出错。</p>';return;}conversationsListUl.innerHTML = html;conversationsListUl.scrollTop = currentScrollTop;conversationsListUl.querySelectorAll('li').forEach(li => {li.removeEventListener('click', handleConversationLiClick);li.addEventListener('click', handleConversationLiClick);});
}function handleConversationLiClick(event) {const li = event.currentTarget;const convId = li.dataset.conversationId;const otherUserEmail = li.dataset.otherParticipantEmail;handleConversationClick(convId, otherUserEmail);
}function handleContactSearch() {contactSearchInput = contactSearchInput || document.getElementById('contact-search-input');if (!contactSearchInput) return;const searchTerm = contactSearchInput.value.toLowerCase().trim();if (!allConversationsCache || !Array.isArray(allConversationsCache)) {if(conversationsListUl) conversationsListUl.innerHTML = '<p class="placeholder-text" style="color: var(--text-color-muted);">对话缓存未准备好,无法搜索。</p>';return;}if (!searchTerm) {displayConversations(allConversationsCache);return;}const filteredConversations = allConversationsCache.filter(conv => {const otherUserUsername = conv.other_participant_username ? String(conv.other_participant_username).toLowerCase() : '';const otherUserEmail = conv.other_participant_email ? String(conv.other_participant_email).toLowerCase() : '';return otherUserUsername.includes(searchTerm) || otherUserEmail.includes(searchTerm);});displayConversations(filteredConversations);
}async function handleConversationClick(conversationD1Id, otherParticipantEmail) {if (!conversationD1Id) return;closeActiveConversationSocket();if (conversationsListUl) {conversationsListUl.querySelectorAll('li').forEach(li => {li.classList.toggle('selected', li.dataset.conversationId === conversationD1Id);});}if(messageInputTextarea) messageInputTextarea.dataset.receiverEmail = otherParticipantEmail;if (messageLoader) messageLoader.classList.add('hidden');await connectConversationWebSocket(conversationD1Id);
}function appendSingleMessageToUI(msg, prepend = false) {if (!messagesListDiv || !currentUserEmail) return;const isSent = msg.sender_email === currentUserEmail;const senderDisplayName = isSent ? '你' : (window.escapeHtml(msg.sender_username || msg.sender_email));const messageTime = formatMillisecondsTimestamp(msg.sent_at);let messageHtmlContent = '';if (typeof window.marked === 'function' && typeof DOMPurify === 'object' && DOMPurify.sanitize) {messageHtmlContent = DOMPurify.sanitize(window.marked.parse(msg.content || '', { breaks: true, gfm: true }));} else {messageHtmlContent = window.escapeHtml(msg.content || '').replace(/\n/g, '<br>');}const messageItemDiv = document.createElement('div');messageItemDiv.className = `message-item ${isSent ? 'sent' : 'received'}`;messageItemDiv.dataset.messageId = msg.message_id;if (msg.sender_email !== currentUserEmail && msg.is_read === 0) {messageItemDiv.classList.add('unread-for-current-user');}messageItemDiv.innerHTML = `
<span class="message-sender">${senderDisplayName}</span>
<div class="message-content">${messageHtmlContent}</div>
<span class="message-time">${messageTime}</span>`;const oldScrollHeight = messagesListWrapper.scrollHeight;const oldScrollTop = messagesListWrapper.scrollTop;if (prepend) {messagesListDiv.insertBefore(messageItemDiv, messagesListDiv.firstChild);messagesListWrapper.scrollTop = oldScrollTop + (messagesListWrapper.scrollHeight - oldScrollHeight);} else {messagesListDiv.appendChild(messageItemDiv);messagesListWrapper.scrollTop = messagesListWrapper.scrollHeight;}if (msg.sender_email !== currentUserEmail && msg.is_read === 0) {observeMessageElement(messageItemDiv);}
}function connectConversationWebSocket(conversationD1Id) {return new Promise((resolve, reject) => {if (conversationSocket && (conversationSocket.readyState === WebSocket.OPEN || conversationSocket.readyState === WebSocket.CONNECTING)) {if (currentActiveConversationD1Id === conversationD1Id) {resolve();return;}closeActiveConversationSocket();}initializeMessageObserver();currentConversationMessages = [];displayedMessagesCount = 0;hasMoreMessagesToLoad = true;isLoadingMoreMessages = false;if (!window.currentUserData || !window.currentUserData.email) {reject(new Error("User not authenticated for conversation WebSocket."));return;}currentActiveConversationD1Id = conversationD1Id;const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';const wsConvUrl = `${protocol}//${window.location.host}/api/ws/conversation/${conversationD1Id}`;conversationSocket = new WebSocket(wsConvUrl);conversationSocket.onopen = () => {if (messagesListDiv) messagesListDiv.innerHTML = '';if (messageInputAreaDiv) messageInputAreaDiv.classList.remove('hidden');if (emptyMessagesPlaceholder) emptyMessagesPlaceholder.classList.add('hidden');if (messageLoader) messageLoader.classList.add('hidden');resolve();};conversationSocket.onmessage = (event) => {try {const message = JSON.parse(event.data);if (message.type === "HISTORICAL_MESSAGE") {currentConversationMessages.unshift(message.data);} else if (message.type === "INITIAL_MESSAGES_LOADED") {currentConversationMessages.sort((a, b) => a.sent_at - b.sent_at);currentConversationMessages.forEach(msg => appendSingleMessageToUI(msg, false));displayedMessagesCount = currentConversationMessages.length;hasMoreMessagesToLoad = message.data.hasMore;if (!hasMoreMessagesToLoad && messageLoader) messageLoader.classList.add('hidden');messagesListWrapper.scrollTop = messagesListWrapper.scrollHeight;} else if (message.type === "OLDER_MESSAGES_BATCH") {const olderBatch = message.data.messages.sort((a, b) => a.sent_at - b.sent_at);olderBatch.forEach(msg => {currentConversationMessages.unshift(msg);appendSingleMessageToUI(msg, true);});displayedMessagesCount += olderBatch.length;hasMoreMessagesToLoad = message.data.hasMore;isLoadingMoreMessages = false;if (messageLoader) messageLoader.classList.add('hidden');if (!hasMoreMessagesToLoad && messageLoader) messageLoader.classList.add('hidden');} else if (message.type === "NEW_MESSAGE") {currentConversationMessages.push(message.data);appendSingleMessageToUI(message.data, false);displayedMessagesCount++;if (message.data.sender_email !== currentUserEmail && window.userPresenceSocket && window.userPresenceSocket.readyState === WebSocket.OPEN) {window.userPresenceSocket.send(JSON.stringify({type: "REQUEST_CONVERSATIONS_LIST"}));}} else if (message.type === "CONNECTION_ESTABLISHED"){} else if (message.type === "MESSAGES_READ"){} else if (message.type === "ERROR") {if (typeof window.showMessage === 'function') window.showMessage(`对话错误: ${message.data}`, 'error');}} catch (e) { }};conversationSocket.onclose = (event) => {if (currentActiveConversationD1Id === conversationD1Id) {conversationSocket = null;currentActiveConversationD1Id = null;resetActiveConversationUIOnly();}};conversationSocket.onerror = (error) => {if (currentActiveConversationD1Id === conversationD1Id) {resetActiveConversationUIOnly();}reject(error);};});
}function resetActiveConversationUIOnly() {closeActiveConversationSocket();if (messageInputTextarea) {messageInputTextarea.value = '';messageInputTextarea.removeAttribute('data-receiver-email');}if (messageInputAreaDiv) messageInputAreaDiv.classList.add('hidden');if (messagesListDiv) messagesListDiv.innerHTML = '';if (messageLoader) messageLoader.classList.add('hidden');if (emptyMessagesPlaceholder && messagesListDiv) {emptyMessagesPlaceholder.querySelector('p').textContent = '选择一个联系人开始聊天';emptyMessagesPlaceholder.querySelector('span').textContent = '或通过上方输入框发起新的对话。';if (messagesListDiv.firstChild !== emptyMessagesPlaceholder) { // Ensure placeholder is appended if list is emptymessagesListDiv.innerHTML = ''; // Clear list before appending placeholdermessagesListDiv.appendChild(emptyMessagesPlaceholder);}emptyMessagesPlaceholder.classList.remove('hidden');}if (conversationsListUl) {conversationsListUl.querySelectorAll('li.selected').forEach(li => li.classList.remove('selected'));}
}function handleSendMessageClick() {if (!conversationSocket || conversationSocket.readyState !== WebSocket.OPEN) {if(typeof window.showMessage === 'function') window.showMessage('对话连接未建立。', 'error');return;}const content = messageInputTextarea.value.trim();if (!content) {if(typeof window.showMessage === 'function') window.showMessage('消息内容不能为空。', 'warning');return;}conversationSocket.send(JSON.stringify({type: "NEW_MESSAGE",data: { content: content }}));messageInputTextarea.value = '';messageInputTextarea.style.height = 'auto';messageInputTextarea.focus();
}async function handleStartNewConversation(receiverEmailFromInput) {const localReceiverEmail = receiverEmailFromInput.trim();if (!localReceiverEmail) {if (typeof window.showMessage === 'function') window.showMessage('请输入对方的邮箱地址。', 'warning');return;}if (!currentUserEmail) {if (typeof window.showMessage === 'function') window.showMessage('当前用户信息获取失败。', 'error');return;}if (localReceiverEmail === currentUserEmail) {if (typeof window.showMessage === 'function') window.showMessage('不能与自己开始对话。', 'warning');return;}if (typeof window.isValidEmail === 'function' && !window.isValidEmail(localReceiverEmail)) {if (typeof window.showMessage === 'function') window.showMessage('请输入有效的邮箱地址。', 'error');return;}const existingConv = allConversationsCache.find(c => c.other_participant_email === localReceiverEmail);if (existingConv && existingConv.conversation_id) {await handleConversationClick(existingConv.conversation_id, localReceiverEmail);if (newConversationEmailInput) newConversationEmailInput.value = '';if (typeof window.showMessage === 'function') window.showMessage(`已切换到与 ${window.escapeHtml(localReceiverEmail)} 的对话。`, 'info');return;}if (typeof window.apiCall === 'function') {const { ok, data, status } = await window.apiCall('/api/messages', 'POST', {receiverEmail: localReceiverEmail,content: `${currentUserEmail.split('@')[0]} 的对话已开始。`});if (ok && data.success && data.conversationId) {if (newConversationEmailInput) newConversationEmailInput.value = '';if (contactSearchInput) contactSearchInput.value = '';if (window.userPresenceSocket && window.userPresenceSocket.readyState === WebSocket.OPEN) {window.userPresenceSocket.send(JSON.stringify({type: "REQUEST_CONVERSATIONS_LIST"}));}setTimeout(async () => {const newlyCreatedConv = allConversationsCache.find(c => c.other_participant_email === localReceiverEmail && c.conversation_id === data.conversationId);if (newlyCreatedConv) {await handleConversationClick(data.conversationId, localReceiverEmail);} else {await handleConversationClick(data.conversationId, localReceiverEmail);}}, 500);if (typeof window.showMessage === 'function') window.showMessage(`${window.escapeHtml(localReceiverEmail)} 的对话已开始。`, 'success');} else if (data.error === '接收者用户不存在' || status === 404) {if (typeof window.showMessage === 'function') window.showMessage(`无法开始对话:用户 ${window.escapeHtml(localReceiverEmail)} 不存在。`, 'error');}else {if (typeof window.showMessage === 'function') window.showMessage(`无法与 ${window.escapeHtml(localReceiverEmail)} 开始对话: ${ (data && (data.error || data.message)) ? window.escapeHtml(data.error || data.message) : '未知错误, 状态: ' + status}`, 'error');}}
}window.handleConversationsListUpdate = function(conversationsData) {if (!currentUserEmail && window.currentUserData && window.currentUserData.email) {currentUserEmail = window.currentUserData.email;}const oldConversationsSummary = { ...allConversationsCache.reduce((acc, conv) => { acc[conv.conversation_id] = conv; return acc; }, {}) };allConversationsCache = conversationsData.map(conv => ({conversation_id: conv.conversation_id,other_participant_username: conv.other_participant_username,other_participant_email: conv.other_participant_email,last_message_content: conv.last_message_content,last_message_sender: conv.last_message_sender,last_message_at: conv.last_message_at,unread_count: conv.unread_count,}));displayConversations(allConversationsCache);allConversationsCache.forEach(newConv => {const oldConv = oldConversationsSummary[newConv.conversation_id];if (newConv.last_message_sender && newConv.last_message_sender !== currentUserEmail && newConv.unread_count > 0) {if (!oldConv || newConv.last_message_at > (oldConv.last_message_at || 0)) {const isCurrentConversationActive = newConv.conversation_id === currentActiveConversationD1Id;if (!document.hasFocus() || !isCurrentConversationActive) { showDesktopNotification(`来自 ${window.escapeHtml(newConv.other_participant_username || newConv.other_participant_email)} 的新消息`,{body: window.escapeHtml(newConv.last_message_content.substring(0, 50) + (newConv.last_message_content.length > 50 ? "..." : "")),icon: '/favicon.ico',tag: `conversation-${newConv.conversation_id}`},newConv.conversation_id);}}}});if (currentActiveConversationD1Id) {const activeConvStillExists = allConversationsCache.some(c => c.conversation_id === currentActiveConversationD1Id);if (!activeConvStillExists) {resetActiveConversationUIOnly();} else {const selectedLi = conversationsListUl?.querySelector(`li[data-conversation-id="${currentActiveConversationD1Id}"]`);if (selectedLi) selectedLi.classList.add('selected');}}
};window.handleSingleConversationUpdate = function(updatedConvData) {if (!currentUserEmail && window.currentUserData && window.currentUserData.email) {currentUserEmail = window.currentUserData.email;}const index = allConversationsCache.findIndex(c => c.conversation_id === updatedConvData.conversation_id);const oldConvData = index > -1 ? { ...allConversationsCache[index] } : null;const mappedData = {conversation_id: updatedConvData.conversation_id,other_participant_username: updatedConvData.other_participant_username,other_participant_email: updatedConvData.other_participant_email,last_message_content: updatedConvData.last_message_content,last_message_sender: updatedConvData.last_message_sender,last_message_at: updatedConvData.last_message_at,unread_count: updatedConvData.unread_count,};if (index > -1) {allConversationsCache[index] = { ...allConversationsCache[index], ...mappedData };} else {allConversationsCache.unshift(mappedData);}displayConversations(allConversationsCache);if (mappedData.last_message_sender && mappedData.last_message_sender !== currentUserEmail && mappedData.unread_count > 0) {if (!oldConvData || mappedData.last_message_at > (oldConvData.last_message_at || 0)) {const isCurrentConversationActive = mappedData.conversation_id === currentActiveConversationD1Id;if (!document.hasFocus() || !isCurrentConversationActive) {showDesktopNotification(`来自 ${window.escapeHtml(mappedData.other_participant_username || mappedData.other_participant_email)} 的新消息`,{body: window.escapeHtml(mappedData.last_message_content.substring(0, 50) + (mappedData.last_message_content.length > 50 ? "..." : "")),icon: '/favicon.ico',tag: `conversation-${mappedData.conversation_id}`},mappedData.conversation_id);}}}if (currentActiveConversationD1Id === updatedConvData.conversation_id) {const selectedLi = conversationsListUl?.querySelector(`li[data-conversation-id="${currentActiveConversationD1Id}"]`);if (selectedLi) selectedLi.classList.add('selected');}
};window.initializeMessagingTab = initializeMessagingTab;
window.loadMessagingTabData = loadMessagingTabData;// Removed the DOMContentLoaded listener from here as initialization is now triggered from main.js
// after fetchAppConfigAndInitialize -> checkLoginStatus completes.

解释:

  • main.js -> checkLoginStatus():
    • 此函数现在是身份验证和重定向的主要控制点。
    • 它首先调用 /api/me
    • 如果已登录
      • 如果当前在 /user/login, /user/register, 或 / 页面,它会检查 return_to 参数,如果存在则跳转到该参数指定的页面,否则跳转到 /user/account。然后函数返回,阻止后续代码执行。
      • 如果当前在其他页面(如 /user/account, /user/messaging, /user/help),它会调用 displayCorrectView(userData) 更新UI,然后正常继续。
    • 如果未登录
      • 如果当前在受保护的页面 (如 /user/account, /user/messaging, /user/help),它会重定向到 /user/login 并附带 return_to 参数(包含原始页面的完整路径、查询参数和哈希)。然后函数返回。
      • 如果当前在公共页面(如 /user/login),它会调用 displayCorrectView(null) 来显示登录表单。
    • 函数返回一个布尔值,指示是否发生了重定向。
  • main.js -> displayCorrectView(userData):
    • 此函数现在只负责根据传入的 userData 更新UI(显示/隐藏元素,填充用户信息等)。它不再执行任何重定向。
    • 特别为 /user/messaging 页面添加了 messaging-page-layout 类,并相应调整了侧边栏和主内容区域的显隐。
  • main.js -> WorkspaceAppConfigAndInitialize():
    • 现在会 await checkLoginStatus()
    • 如果 checkLoginStatus 没有导致重定向,并且当前页面是 /user/messaging 且用户已登录,它会调用 window.loadMessagingTabData()
  • ui-messaging.js:
    • 移除了底部的 DOMContentLoaded 监听器。loadMessagingTabData 现在由 main.js 中的 WorkspaceAppConfigAndInitialize 在正确的时机调用。
    • loadMessagingTabData 内部依赖 window.currentUserData(应由 main.jscheckLoginStatus 设置)。
  • html-ui.js:
    • _generateTopBarHtml 调整了 siteTitleLink 的逻辑,使其在私信页面能正确链接回账户设置。
    • _generateScriptsgenerateHelpPageHtml, generateConsentScreenHtml, generateErrorPageHtml 中现在总是包含所有UI脚本 (includeAllUiScripts = true),以确保像主题切换这样的全局功能在所有页面上都能正常工作。

通过这些更改,身份验证流程应该更加健壮,可以防止因客户端状态判断不准确或重定向逻辑冲突而导致的循环。当用户访问 /user/messaging 时:

  1. worker.js 首先检查服务器端会话。如果未登录,则重定向到 /user/login?return_to=/user/messaging
  2. 如果已登录(或在登录后),浏览器加载 /user/messaging
  3. 客户端 main.js 中的 WorkspaceAppConfigAndInitialize -> checkLoginStatus 会确认登录状态。
  4. 由于已登录且在 /user/messaging 页面,checkLoginStatus 不会重定向,而是调用 displayCorrectView 设置正确的UI。
  5. 之后,WorkspaceAppConfigAndInitialize 会调用 loadMessagingTabData 来加载私信数据。

请您将这些修改应用到对应的文件中,并测试是否解决了重定向循环的问题。

http://www.xdnf.cn/news/8451.html

相关文章:

  • 高项公式英文解析记忆
  • 大模型量化与双重量化(2)-- 代码示例与解释
  • Neo4j入门第二期(Spring Data Neo4j的使用)
  • Oracle 的 MOVE 操作是否重建表?
  • 【学习笔记】Sophus (Python) 使用文档
  • C#调用第三方C++版本SDK过程
  • windows bat 在目录下(包括子目录)搜索批量指定文件名称复制到另一个文件夹内
  • ES6 新增 API 方法
  • ​​3D 几何建模工具库​Open CASCADE(OCCT)简单介绍。
  • 如何评估物联网框架的交互体验?
  • springboot SnailJob client(客户端) 启动失败
  • 机器学习与深度学习:区别与联系
  • 【数据集】全球首个10米分辨率精细分类土地覆盖数据集GLC_FCS10
  • 鸿蒙ArkTS-发请求第三方接口显示实时新闻列表页面
  • 带您了解工业级网络变压器的浪涌等级测试有哪些条件?
  • mysql底层数据结构
  • 怎么判断一个Android APP使用了React Native 这个跨端框架
  • 【Golang】部分语法格式和规则
  • matlab时间反转镜算法
  • 2025年电工杯A题第一版本Q1-Q4详细思路求解+代码运行
  • day24Node-node的Web框架Express
  • C# Windows Forms应用程序-001
  • 国产矢量网络分析仪怎么样?能用在哪里?
  • 打破传统范式,线上 3D 画展彰显多元亮点
  • C语言---动态内存管理、柔性数组
  • unity控制相机围绕物体旋转移动
  • Maven打包SpringBoot项目,因包含SpringBootTest单元测试和Java预览版特性导致打包失败
  • 【leetcode】3356. 零数组变换②
  • 【uniapp】 iosApp开发xcode原生配置项(iOS平台Capabilities配置)
  • SFP与Unsloth:大模型微调技术全解析