您好,您遇到的重定向循环问题,即在 /user/messaging
、/user/login?return_to=/user/messaging
和 /user/login
之间反复跳转,通常是由于客户端的身份验证状态检查和页面重定向逻辑存在冲突或竞争条件。
在分析了您提供的代码(特别是 main.js
、ui-messaging.js
和 worker.js
的相关部分)后,我判断问题主要出在客户端 JavaScript 如何确定用户登录状态以及何时执行页面跳转。当这些逻辑不够集中或执行顺序不当时,就可能出现您描述的循环。
为了解决这个问题,我建议对 main.js
中的 checkLoginStatus
和 displayCorrectView
函数进行重构,使其职责更清晰,并调整 /user/messaging
页面的初始化方式。
核心思路:
checkLoginStatus
: 此函数将成为判断用户是否登录、是否需要重定向的唯一决策点。它会首先调用/api/me
检查会话。- 如果用户已登录:
- 若当前在登录/注册页面,则根据
return_to
参数或默认规则跳转到账户页面或目标页面。 - 若当前在受保护页面(如
/user/account
,/user/messaging
),则调用displayCorrectView
更新UI,并继续加载页面内容。
- 若当前在登录/注册页面,则根据
- 如果用户未登录:
- 若当前在受保护页面,则重定向到登录页,并附带
return_to
参数。 - 若当前在公共页面(如登录页),则调用
displayCorrectView
显示相应表单。
- 若当前在受保护页面,则重定向到登录页,并附带
- 如果用户已登录:
displayCorrectView
: 此函数将只负责根据checkLoginStatus
提供的用户数据(或null)来更新页面的显示元素(如顶部栏、侧边栏显隐、表单显隐等),不再执行任何页面跳转逻辑。/user/messaging
页面初始化: 在main.js
的WorkspaceAppConfigAndInitialize
函数中,将在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 authenticated
const loginUrl = new URL(`${OAUTH_ISSUER_URL(env, request)}/user/login`);
loginUrl.searchParams.set('return_to', path); // Return to messaging page after login
return Response.redirect(loginUrl.toString(), 302);
}
return new Response(generateMessagingPageHtml(env), { headers: { 'Content-Type': 'text/html;charset=UTF-8' } });
}
// ...
2. cdn/js/main.js
这是主要的修改部分。我们将重构 checkLoginStatus
和 displayCorrectView
,并调整初始化流程。
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 checkLoginStatus
window.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 in
const protectedPaths = ['/user/account', '/user/messaging', '/user/help'];
if (protectedPaths.includes(currentPath)) {
// Preserve existing query params and hash when redirecting to login
const 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 happened
if (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 checkLoginStatus
const isLoggedIn = !!userData?.email;
document.querySelectorAll('.cf-turnstile').forEach(div => removeTurnstile(div)); // Clear all turnstiles
if (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 default
appWrapper.classList.remove('sidebar-open-app'); // Ensure sidebar closed class is not stuck
if (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 others
sidebarToggleEl.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/help
if(loggedInSection) loggedInSection.classList.add('hidden');
}
if (typeof window.connectUserPresenceWebSocket === 'function') {
connectUserPresenceWebSocket();
}
} else { // Not logged in
if (userPresenceSocket && userPresenceSocket.readyState === WebSocket.OPEN) userPresenceSocket.close();
if (sidebarEl) sidebarEl.classList.add('hidden');
if (sidebarToggleEl) sidebarToggleEl.classList.remove('hidden'); // Show toggle if it was hidden
if(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 happened
if (!redirected) { // Only proceed if checkLoginStatus didn't redirect
const 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 page
tabPanes = 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-info
const 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 { // register
endpoint = '/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, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
};
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/tab
contactSearchInput = 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 page
initializeMessagingTab(); // Initialize DOM elements
if (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 called
if (window.currentUserData && window.currentUserData.email) {
currentUserEmail = window.currentUserData.email;
} else {
// This case should ideally not be hit if main.js handles redirects correctly
if (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 state
if (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 try
setTimeout(() => {
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
};
}
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 empty
messagesListDiv.innerHTML = ''; // Clear list before appending placeholder
messagesListDiv.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.js
的checkLoginStatus
设置)。
- 移除了底部的
html-ui.js
:_generateTopBarHtml
调整了siteTitleLink
的逻辑,使其在私信页面能正确链接回账户设置。_generateScripts
在generateHelpPageHtml
,generateConsentScreenHtml
,generateErrorPageHtml
中现在总是包含所有UI脚本 (includeAllUiScripts = true
),以确保像主题切换这样的全局功能在所有页面上都能正常工作。
通过这些更改,身份验证流程应该更加健壮,可以防止因客户端状态判断不准确或重定向逻辑冲突而导致的循环。当用户访问 /user/messaging
时:
worker.js
首先检查服务器端会话。如果未登录,则重定向到/user/login?return_to=/user/messaging
。- 如果已登录(或在登录后),浏览器加载
/user/messaging
。 - 客户端
main.js
中的WorkspaceAppConfigAndInitialize
->checkLoginStatus
会确认登录状态。 - 由于已登录且在
/user/messaging
页面,checkLoginStatus
不会重定向,而是调用displayCorrectView
设置正确的UI。 - 之后,
WorkspaceAppConfigAndInitialize
会调用loadMessagingTabData
来加载私信数据。
请您将这些修改应用到对应的文件中,并测试是否解决了重定向循环的问题。