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

Gemini 2.5 Pro 一次测试

您好!您遇到的问题——在非账户设置页面(例如 /user/help)点击顶部栏的“私信”按钮时,私信界面可能无法正确加载并卡在“正在加载对话…”——我分析后认为主要原因如下:

  1. JavaScript 文件缺失:在 /user/help 这样的独立页面上,专门处理私信逻辑的 JavaScript 文件 (ui-messaging.js) 可能没有被加载。目前您的 generateHelpPageHtml 函数在生成页面时,并未包含所有UI脚本。
  2. HTML 结构缺失:私信界面的 HTML 结构(包括显示对话列表的容器)是作为 /user/account 页面内的一个“标签页” (tab pane) 生成的。当您在 /user/help 页面时,这部分 HTML 结构不存在于当前页面,因此即使相关脚本被加载,也无法找到并操作对应的界面元素。

您提出的将用户中心的各个主要功能模块(如 API 密钥、个人设置、安全设置、我的应用以及私信)拆分成独立的页面,而不是全部聚合在 /user/account 下的标签页中,这是一个非常好的建议。这种方式更符合多页面应用(MPA)的思路,可以有效地解决上述问题。为每个功能区创建专门的页面路由和 HTML,能确保在访问该功能时,所有必需的资源(HTML、CSS、JavaScript)都能被正确加载和初始化。

针对私信功能,我们可以将其改造成一个独立的页面,例如 /user/messaging。这将涉及以下修改:

1. my/src/worker.js (Cloudflare Worker 入口文件)

  • 需要为新的私信页面添加路由。
  • 当用户访问 /user/messaging 时,调用一个新的 HTML 生成函数。
export class ConversationDurableObject {constructor(state, env) {this.state = state;this.env = env;this.sessions = [];this.messages = [];this.conversationId = null;this.participants = [];this.initialized = false;this.state.blockConcurrencyWhile(async () => {let stored = await this.state.storage.get(["conversationId", "participants", "messages"]);this.conversationId = stored.get("conversationId");this.participants = stored.get("participants") || [];this.messages = stored.get("messages") || [];if (this.conversationId && this.participants.length > 0) {this.initialized = true;}});}async initialize(conversationId, participant1Email, participant2Email) {if (this.initialized) return;this.conversationId = conversationId;this.participants = [participant1Email, participant2Email].sort();if (this.messages.length === 0 && this.env.DB && this.conversationId) {try {const { results } = await this.env.DB.prepare(`SELECT m.message_id, m.conversation_id, m.sender_email, u_sender.username as sender_username, m.content, m.sent_at, m.is_readFROM messages mJOIN users u_sender ON m.sender_email = u_sender.emailWHERE m.conversation_id = ?ORDER BY m.sent_at ASC`).bind(this.conversationId).all();if (results) {this.messages = results;}} catch (e) {}}await this.state.storage.put({"conversationId": this.conversationId,"participants": this.participants,"messages": this.messages});this.initialized = true;}static getConversationDOName(user1Email, user2Email) {const participants = [user1Email, user2Email].sort();return `conv-${participants[0]}-${participants[1]}`;}async fetch(request) {const url = new URL(request.url);const userEmail = request.headers.get("X-User-Email");const requestD1ConversationId = request.headers.get("X-Conversation-D1-Id");const requestP1 = request.headers.get("X-Participant1-Email");const requestP2 = request.headers.get("X-Participant2-Email");if (!this.initialized && requestD1ConversationId && requestP1 && requestP2) {await this.initialize(requestD1ConversationId, requestP1, requestP2);}if (!this.initialized) {return new Response("DO 未初始化。请调用 /initialize 或提供头部信息。", { status: 500 });}if (!userEmail) {return new Response("需要 X-User-Email 头部信息。", { status: 400 });}if (!this.participants.includes(userEmail)) {return new Response("禁止访问:用户不是参与者。", { status: 403 });}if (url.pathname === "/websocket") {if (request.headers.get("Upgrade") !== "websocket") {return new Response("期望 WebSocket 升级", { status: 426 });}const pair = new WebSocketPair();const [client, server] = Object.values(pair);server.accept();this.sessions.push({ ws: server, userEmail: userEmail });const initialBatchSize = 10;const startIndex = Math.max(0, this.messages.length - initialBatchSize);const initialMessages = this.messages.slice(startIndex);initialMessages.forEach(msg => {server.send(JSON.stringify({ type: "HISTORICAL_MESSAGE", data: msg }));});server.send(JSON.stringify({type: "INITIAL_MESSAGES_LOADED",data: {count: initialMessages.length,totalMessagesInConversation: this.messages.length,hasMore: this.messages.length > initialBatchSize}}));server.send(JSON.stringify({ type: "CONNECTION_ESTABLISHED", data: { conversationId: this.conversationId, participants: this.participants }}));server.addEventListener("message", async event => {try {const messageData = JSON.parse(event.data);if (messageData.type === "NEW_MESSAGE") {const { content } = messageData.data;const senderEmail = userEmail;const receiverEmail = this.participants.find(p => p !== senderEmail);if (!receiverEmail) {server.send(JSON.stringify({ type: "ERROR", data: "无法确定接收者。"}));return;}const now = Date.now();const messageId = crypto.randomUUID();let senderUsername = senderEmail.split('@')[0];try {const userDetails = await this.env.DB.prepare("SELECT username FROM users WHERE email = ?").bind(senderEmail).first();if (userDetails && userDetails.username) senderUsername = userDetails.username;} catch (e) { }const newMessage = {message_id: messageId,conversation_id: this.conversationId,sender_email: senderEmail,sender_username: senderUsername,content: content,sent_at: now,is_read: 0,};if (this.env.DB) {try {await this.env.DB.prepare("INSERT INTO messages (message_id, conversation_id, sender_email, receiver_email, content, sent_at, is_read) VALUES (?, ?, ?, ?, ?, ?, 0)").bind(messageId, this.conversationId, senderEmail, receiverEmail, content, now).run();await this.env.DB.prepare("UPDATE conversations SET last_message_at = ? WHERE conversation_id = ?").bind(now, this.conversationId).run();} catch (e) {server.send(JSON.stringify({ type: "ERROR", data: "保存消息失败。" }));return;}}this.messages.push(newMessage);await this.state.storage.put("messages", this.messages);this.broadcast({ type: "NEW_MESSAGE", data: newMessage });if (this.env.USER_PRESENCE_DO) {try {const presenceDOId = this.env.USER_PRESENCE_DO.idFromName(`user-${receiverEmail}`);const presenceStub = this.env.USER_PRESENCE_DO.get(presenceDOId);await presenceStub.fetch(new Request(`https://do-internal/updateConversationState`, {method: 'POST',headers: {'Content-Type': 'application/json'},body: JSON.stringify({conversationId: this.conversationId,action: 'newMessage',senderEmail: senderEmail,messageTimestamp: now,messageContent: content,otherParticipantEmail: senderEmail,otherParticipantUsername: senderUsername})}));} catch(e) { }try {const senderPresenceDOId = this.env.USER_PRESENCE_DO.idFromName(`user-${senderEmail}`);const senderPresenceStub = this.env.USER_PRESENCE_DO.get(senderPresenceDOId);await senderPresenceStub.fetch(new Request(`https://do-internal/updateConversationState`, {method: 'POST',headers: {'Content-Type': 'application/json'},body: JSON.stringify({conversationId: this.conversationId,action: 'messageSent',receiverEmail: receiverEmail,messageTimestamp: now,messageContent: content,})}));} catch(e) { }}} else if (messageData.type === "MESSAGE_SEEN") {const { message_id: seenMessageId } = messageData.data;if (this.env.DB && this.conversationId) {const messagesRead = await this.env.DB.prepare("UPDATE messages SET is_read = 1 WHERE conversation_id = ? AND receiver_email = ? AND is_read = 0").bind(this.conversationId, userEmail).run();this.messages.forEach(msg => {if (msg.receiver_email === userEmail && msg.conversation_id === this.conversationId && !msg.is_read) {msg.is_read = 1;}});await this.state.storage.put("messages", this.messages);this.broadcast({ type: "MESSAGES_READ", data: { reader: userEmail, conversationId: this.conversationId, count: messagesRead.meta.changes }});if (this.env.USER_PRESENCE_DO) {try {const presenceDOId = this.env.USER_PRESENCE_DO.idFromName(`user-${userEmail}`);const presenceStub = this.env.USER_PRESENCE_DO.get(presenceDOId);await presenceStub.fetch(new Request(`https://do-internal/updateConversationState`, {method: 'POST',headers: {'Content-Type': 'application/json'},body: JSON.stringify({ conversationId: this.conversationId, action: 'markRead' })}));} catch(e) { }}}} else if (messageData.type === "LOAD_MORE_MESSAGES") {const { currentlyLoadedCount } = messageData.data;const batchSize = 10;const remainingMessagesCount = this.messages.length - currentlyLoadedCount;const endIndexForSlice = this.messages.length - currentlyLoadedCount;const startIndexForSlice = Math.max(0, endIndexForSlice - batchSize);const olderMessages = this.messages.slice(startIndexForSlice, endIndexForSlice);server.send(JSON.stringify({type: "OLDER_MESSAGES_BATCH",data: {messages: olderMessages,hasMore: startIndexForSlice > 0}}));}} catch (e) {server.send(JSON.stringify({ type: "ERROR", data: "无效的消息格式或处理错误。" }));}});server.addEventListener("close", () => {this.sessions = this.sessions.filter(s => s.ws !== server);});server.addEventListener("error", (err) => {this.sessions = this.sessions.filter(s => s.ws !== server);});return new Response(null, { status: 101, webSocket: client });} else if (url.pathname === "/initialize" && request.method === "POST") {try {const { conversationId, participant1Email, participant2Email } = await request.json();if (conversationId && participant1Email && participant2Email) {if (!this.initialized) {await this.initialize(conversationId, participant1Email, participant2Email);}return new Response("DO 已初始化或之前已初始化", { status: 200 });}return new Response("DO初始化缺少数据", { status: 400 });} catch (e) {return new Response(`DO初始化错误: ${e.message}`, { status: 500 });}}return new Response("在ConversationDurableObject中未找到", { status: 404 });}broadcast(message) {const serializedMessage = JSON.stringify(message);this.sessions = this.sessions.filter(session => {try {if (session.ws.readyState === WebSocket.OPEN) {session.ws.send(serializedMessage);return true;} else {return false;}} catch (e) {return false;}});}
}
export class UserPresenceDurableObject {constructor(state, env) {this.state = state;this.env = env;this.userEmail = null;this.webSocket = null;this.conversationsSummary = {};this.initialized = false;this.state.blockConcurrencyWhile(async () => {const storedUserEmail = await this.state.storage.get("userEmail");if (storedUserEmail) {this.userEmail = storedUserEmail;const storedSummary = await this.state.storage.get("conversationsSummary");if (storedSummary) {this.conversationsSummary = storedSummary;}this.initialized = true;}});}async initialize(userEmail) {if (this.initialized && this.userEmail === userEmail) return;this.userEmail = userEmail;await this.state.storage.put("userEmail", this.userEmail);await this.fetchConversationsSummaryFromD1();this.initialized = true;}async fetchConversationsSummaryFromD1() {if (!this.userEmail || !this.env.DB) return;try {const convResults = await this.env.DB.prepare(`SELECTc.conversation_id, c.last_message_at, c.participant1_email, c.participant2_email,u1.username as p1_username, u2.username as p2_username,(SELECT COUNT(*) FROM messages m WHERE m.conversation_id = c.conversation_id AND m.receiver_email = ?1 AND m.is_read = 0) as unread_count,(SELECT content FROM messages m WHERE m.conversation_id = c.conversation_id ORDER BY m.sent_at DESC LIMIT 1) as last_message_content,(SELECT sender_email FROM messages m WHERE m.conversation_id = c.conversation_id ORDER BY m.sent_at DESC LIMIT 1) as last_message_senderFROM conversations cLEFT JOIN users u1 ON c.participant1_email = u1.emailLEFT JOIN users u2 ON c.participant2_email = u2.emailWHERE c.participant1_email = ?1 OR c.participant2_email = ?1ORDER BY c.last_message_at DESC`).bind(this.userEmail).all();const newSummary = {};let totalUnread = 0;if (convResults && convResults.results) {convResults.results.forEach(conv => {const otherParticipantEmail = conv.participant1_email === this.userEmail ? conv.participant2_email : conv.participant1_email;const otherParticipantUsername = conv.participant1_email === this.userEmail ? conv.p2_username : conv.p1_username;newSummary[conv.conversation_id] = {conversation_id: conv.conversation_id,unread_count: conv.unread_count || 0,last_message_at: conv.last_message_at,last_message_content: conv.last_message_content,last_message_sender: conv.last_message_sender,other_participant_email: otherParticipantEmail,other_participant_username: otherParticipantUsername || otherParticipantEmail.split('@')[0],};totalUnread += (conv.unread_count || 0);});}this.conversationsSummary = newSummary;await this.state.storage.put("conversationsSummary", this.conversationsSummary);if (this.webSocket && this.webSocket.readyState === WebSocket.OPEN) {this.webSocket.send(JSON.stringify({ type: "CONVERSATIONS_LIST", data: Object.values(this.conversationsSummary) }));this.webSocket.send(JSON.stringify({ type: "UNREAD_COUNT_TOTAL", data: { unread_count: totalUnread } }));}} catch (e) {if (this.webSocket && this.webSocket.readyState === WebSocket.OPEN) {this.webSocket.send(JSON.stringify({ type: "ERROR", data: "无法加载对话列表。" }));this.webSocket.send(JSON.stringify({ type: "CONVERSATIONS_LIST", data: [] }));}}}async fetch(request) {const url = new URL(request.url);const requestUserEmail = request.headers.get("X-User-Email");if (!this.initialized && requestUserEmail) {await this.initialize(requestUserEmail);} else if (!this.initialized && this.state.id && this.state.id.name && this.state.id.name.startsWith("user-")) {await this.initialize(this.state.id.name.substring(5));}if (!this.initialized || !this.userEmail) {return new Response("UserPresenceDO 未正确初始化。", { status: 500 });}if (url.pathname === "/websocket") {if (request.headers.get("Upgrade") !== "websocket") {return new Response("期望 WebSocket 升级", { status: 426 });}if (this.webSocket && this.webSocket.readyState === WebSocket.OPEN) {this.webSocket.close(1000, "新连接已建立");}const pair = new WebSocketPair();const [client, server] = Object.values(pair);this.webSocket = server;this.webSocket.accept();this.webSocket.addEventListener("message", async event => {try {const messageData = JSON.parse(event.data);if (messageData.type === "REQUEST_INITIAL_STATE" || messageData.type === "REQUEST_CONVERSATIONS_LIST") {await this.fetchConversationsSummaryFromD1();}} catch(e) {}});this.webSocket.addEventListener("close", () => { this.webSocket = null; });this.webSocket.addEventListener("error", (err) => {this.webSocket = null;});return new Response(null, { status: 101, webSocket: client });} else if (url.pathname === "/updateConversationState" && request.method === "POST") {const updateData = await request.json();const { conversationId, action, senderEmail, messageTimestamp, messageContent, otherParticipantEmail, otherParticipantUsername, receiverEmail } = updateData;let changed = false;if (!this.conversationsSummary[conversationId]) {this.conversationsSummary[conversationId] = {conversation_id: conversationId,unread_count: 0,last_message_at: 0,last_message_content: "",last_message_sender: "",other_participant_email: action === 'newMessage' ? (otherParticipantEmail || senderEmail) : (action === 'messageSent' ? receiverEmail : ""),other_participant_username: action === 'newMessage' ? (otherParticipantUsername || (otherParticipantEmail || senderEmail || "").split('@')[0]) : (action === 'messageSent' ? (receiverEmail || "").split('@')[0] : "")};changed = true;}const currentConv = this.conversationsSummary[conversationId];if (action === 'newMessage') {currentConv.unread_count = (currentConv.unread_count || 0) + 1;currentConv.last_message_at = messageTimestamp;currentConv.last_message_content = messageContent;currentConv.last_message_sender = senderEmail;if (!currentConv.other_participant_email && senderEmail) {currentConv.other_participant_email = senderEmail;currentConv.other_participant_username = otherParticipantUsername || senderEmail.split('@')[0];}changed = true;} else if (action === 'markRead') {if (currentConv.unread_count > 0) {currentConv.unread_count = 0;changed = true;}} else if (action === 'messageSent') {currentConv.last_message_at = messageTimestamp;currentConv.last_message_content = messageContent;currentConv.last_message_sender = this.userEmail;if (!currentConv.other_participant_email && receiverEmail) {currentConv.other_participant_email = receiverEmail;currentConv.other_participant_username = receiverEmail.split('@')[0];}changed = true;}if (changed) {await this.state.storage.put("conversationsSummary", this.conversationsSummary);let totalUnread = 0;Object.values(this.conversationsSummary).forEach(conv => totalUnread += (conv.unread_count || 0));if (this.webSocket && this.webSocket.readyState === WebSocket.OPEN) {const sortedSummary = Object.values(this.conversationsSummary).sort((a,b) => (b.last_message_at || 0) - (a.last_message_at || 0));this.webSocket.send(JSON.stringify({ type: "CONVERSATIONS_LIST", data: sortedSummary }));this.webSocket.send(JSON.stringify({ type: "UNREAD_COUNT_TOTAL", data: { unread_count: totalUnread } }));}}return new Response("用户状态已更新", { status: 200 });} else if (url.pathname === "/getConversationsList" && request.method === "GET") {await this.fetchConversationsSummaryFromD1();const sortedSummary = Object.values(this.conversationsSummary).sort((a,b) => (b.last_message_at || 0) - (a.last_message_at || 0));return new Response(JSON.stringify(sortedSummary), { headers: { 'Content-Type': 'application/json'}});} else if (url.pathname === "/getTotalUnreadCount" && request.method === "GET") {await this.fetchConversationsSummaryFromD1();let totalUnread = 0;Object.values(this.conversationsSummary).forEach(conv => totalUnread += (conv.unread_count || 0));return new Response(JSON.stringify({ unread_count: totalUnread }), { headers: { 'Content-Type': 'application/json'}});}return new Response("在UserPresenceDurableObject中未找到", { status: 404 });}
}
import {authenticateApiRequest,handleGetMe,handleRegister,handleLogin,handleLogin2FAVerify,handleChangePassword,handleUpdateProfile,handleLogout,handle2FAGenerateSecret,handle2FAEnable,handle2FADisable,handleCreatePasteApiKey,handleGetCloudPcKey,handleCreateCloudPcKey,handleGetGreenHubKeys,
} from './api-handlers.js';
import {handleOpenIDConfiguration,handleJwks,handleOAuthAuthorize,handleOAuthToken,handleOAuthUserInfo,handleRegisterOauthClient,handleListOauthClients,handleUpdateOauthClient,handleDeleteOauthClient
} from './oauth-server.js';
import { generateHtmlUi, generateErrorPageHtml, generateHelpPageHtml, generateMessagingPageHtml } from './html-ui.js';
import { jsonResponse, OAUTH_ISSUER_URL } from './helpers.js';
export default {async fetch(request, env, ctx) {const url = new URL(request.url);const path = url.pathname;const method = request.method;const rayId = request.headers.get('cf-ray') || crypto.randomUUID();let userEmailFromSession = null;const cookieHeader = request.headers.get('Cookie');let sessionIdFromCookie = null;if (cookieHeader) {const authCookieString = cookieHeader.split(';').find(row => row.trim().startsWith('AuthToken='));if (authCookieString) sessionIdFromCookie = authCookieString.split('=')[1]?.trim();}if (sessionIdFromCookie && env.DB) {userEmailFromSession = await authenticateApiRequest(sessionIdFromCookie, env);}try {if (method === 'GET' && path === '/.well-known/openid-configuration') {return handleOpenIDConfiguration(request, env);}if (method === 'GET' && path === '/.well-known/jwks.json') {return handleJwks(request, env);}if (path.startsWith('/oauth/')) {if (path === '/oauth/authorize') {return handleOAuthAuthorize(request, env);}if (path === '/oauth/token' && method === 'POST') {return handleOAuthToken(request, env);}if (path === '/oauth/userinfo' && (method === 'GET' || method === 'POST')) {return handleOAuthUserInfo(request, env);}return jsonResponse({ error: 'OAuth/OIDC 端点未找到或方法不允许' }, 404);}if (path.startsWith('/api/ws/user')) {if (!userEmailFromSession) return jsonResponse({ error: 'WebSocket 需要身份验证' }, 401);const doId = env.USER_PRESENCE_DO.idFromName(`user-${userEmailFromSession}`);const stub = env.USER_PRESENCE_DO.get(doId);const forwardRequestHeaders = new Headers(request.headers);forwardRequestHeaders.set('X-User-Email', userEmailFromSession);return stub.fetch(new Request(`${url.origin}/websocket`, new Request(request, {headers: forwardRequestHeaders})));}const wsConversationMatch = path.match(/^\/api\/ws\/conversation\/([a-fA-F0-9-]+)$/);if (wsConversationMatch) {if (!userEmailFromSession) return jsonResponse({ error: 'WebSocket 需要身份验证' }, 401);const conversationD1Id = wsConversationMatch[1];const convDetails = await env.DB.prepare("SELECT participant1_email, participant2_email FROM conversations WHERE conversation_id = ?").bind(conversationD1Id).first();if (!convDetails) return jsonResponse({ error: '对话未找到' }, 404);if (userEmailFromSession !== convDetails.participant1_email && userEmailFromSession !== convDetails.participant2_email) {return jsonResponse({ error: '禁止访问' }, 403);}const doName = ConversationDurableObject.getConversationDOName(convDetails.participant1_email, convDetails.participant2_email);const doId = env.CONVERSATION_DO.idFromName(doName);const stub = env.CONVERSATION_DO.get(doId);const forwardRequestHeaders = new Headers(request.headers);forwardRequestHeaders.set('X-User-Email', userEmailFromSession);forwardRequestHeaders.set('X-Conversation-D1-Id', conversationD1Id);forwardRequestHeaders.set('X-Participant1-Email', convDetails.participant1_email);forwardRequestHeaders.set('X-Participant2-Email', convDetails.participant2_email);return stub.fetch(new Request(`${url.origin}/websocket`, new Request(request, {headers: forwardRequestHeaders})));}if (path.startsWith('/api/')) {if (path === '/api/config' && method === 'GET') {return jsonResponse({ turnstileSiteKey: env.TURNSTILE_SITE_KEY });}if (method === 'POST' && path === '/api/logout') {return handleLogout(request, env, sessionIdFromCookie, url.hostname);}const authenticatedRoutes = ['/api/me', '/api/change-password', '/api/update-profile','/api/2fa/','/api/paste-keys', '/api/cloudpc-key','/api/greenhub-keys', '/api/oauth/clients','/api/messages', '/api/conversations', '/api/messages/unread-count'];const oauthClientSpecificPathMatchForAuth = path.match(/^\/api\/oauth\/clients\/([\w-]+)$/);let needsAuth = authenticatedRoutes.some(p => path.startsWith(p) || (p.endsWith('/') && path.startsWith(p.slice(0,-1))) || path === p );if (oauthClientSpecificPathMatchForAuth && (method === 'PUT' || method === 'DELETE')) {needsAuth = true;}if (needsAuth && !userEmailFromSession) {return jsonResponse({ error: '未授权或会话无效' }, 401);}if (method === 'POST' && path === '/api/register') {return handleRegister(request, env, request.headers.get('CF-Connecting-IP'));}if (method === 'POST' && path === '/api/login') {return handleLogin(request, env, url.protocol, url.hostname, request.headers.get('CF-Connecting-IP'));}if (method === 'POST' && path === '/api/login/2fa-verify') {return handleLogin2FAVerify(request, env, url.protocol, url.hostname);}if (path === '/api/me' && method === 'GET') {return handleGetMe(request, env, userEmailFromSession);}if (path === '/api/change-password' && method === 'POST') {return handleChangePassword(request, env, userEmailFromSession);}if (path === '/api/update-profile' && method === 'POST') {return handleUpdateProfile(request, env, userEmailFromSession);}if (path.startsWith('/api/2fa/')) {if (path === '/api/2fa/generate-secret' && method === 'GET') {return handle2FAGenerateSecret(request, env, userEmailFromSession);}if (path === '/api/2fa/enable' && method === 'POST') {return handle2FAEnable(request, env, userEmailFromSession);}if (path === '/api/2fa/disable' && method === 'POST') {return handle2FADisable(request, env, userEmailFromSession);}}if (path === '/api/paste-keys' && method === 'POST') {return handleCreatePasteApiKey(request, env, userEmailFromSession, request.headers.get('CF-Connecting-IP'));}if (path.startsWith('/api/cloudpc-key')) {if (method === 'GET') {return handleGetCloudPcKey(request, env, userEmailFromSession);}if (method === 'POST') {return handleCreateCloudPcKey(request, env, userEmailFromSession, request.headers.get('CF-Connecting-IP'));}}if (path === '/api/greenhub-keys' && method === 'GET') {return handleGetGreenHubKeys(request, env, userEmailFromSession);}if (path === '/api/oauth/clients') {if (method === 'POST') {return handleRegisterOauthClient(request, env, userEmailFromSession);}if (method === 'GET') {return handleListOauthClients(request, env, userEmailFromSession);}}if (oauthClientSpecificPathMatchForAuth) {const clientIdFromPath = oauthClientSpecificPathMatchForAuth[1];if (method === 'PUT') {return handleUpdateOauthClient(request, env, userEmailFromSession, clientIdFromPath);}if (method === 'DELETE') {return handleDeleteOauthClient(request, env, userEmailFromSession, clientIdFromPath);}}if (path === '/api/messages' && method === 'POST') {if (!userEmailFromSession) return jsonResponse({ error: '用户未认证' }, 401);const reqBody = await request.json();const { receiverEmail, content } = reqBody;if (!receiverEmail || !content) return jsonResponse({ error: '接收者和内容为必填项' }, 400);if (receiverEmail === userEmailFromSession) return jsonResponse({ error: '不能给自己发送消息' }, 400);const receiverUser = await env.DB.prepare("SELECT email FROM users WHERE email = ?").bind(receiverEmail).first();if (!receiverUser) return jsonResponse({ error: '接收用户不存在' }, 404);const p1Sorted = [userEmailFromSession, receiverEmail].sort()[0];const p2Sorted = [userEmailFromSession, receiverEmail].sort()[1];let conversation = await env.DB.prepare("SELECT conversation_id FROM conversations WHERE participant1_email = ? AND participant2_email = ?").bind(p1Sorted, p2Sorted).first();let conversationD1Id;if (!conversation) {conversationD1Id = crypto.randomUUID();const nowDb = Date.now();await env.DB.prepare("INSERT INTO conversations (conversation_id, participant1_email, participant2_email, last_message_at, created_at) VALUES (?, ?, ?, ?, ?)").bind(conversationD1Id, p1Sorted, p2Sorted, nowDb, nowDb).run();} else {conversationD1Id = conversation.conversation_id;}const doName = ConversationDurableObject.getConversationDOName(p1Sorted, p2Sorted);const doId = env.CONVERSATION_DO.idFromName(doName);const stub = env.CONVERSATION_DO.get(doId);const initResponse = await stub.fetch(new Request(`${url.origin}/initialize`, {method: 'POST',headers: {'Content-Type': 'application/json', 'X-User-Email': userEmailFromSession },body: JSON.stringify({conversationId: conversationD1Id,participant1Email: p1Sorted,participant2Email: p2Sorted})}));return jsonResponse({success: true,message: '对话已就绪。请通过WebSocket发送实际消息。',conversationId: conversationD1Id});}if (path === '/api/conversations' && method === 'GET') {if (!userEmailFromSession) return jsonResponse({ error: '用户未认证' }, 401);const doId = env.USER_PRESENCE_DO.idFromName(`user-${userEmailFromSession}`);const stub = env.USER_PRESENCE_DO.get(doId);const forwardRequestHeaders = new Headers(request.headers);forwardRequestHeaders.set('X-User-Email', userEmailFromSession);return stub.fetch(new Request(`${url.origin}/getConversationsList`, new Request(request, {headers: forwardRequestHeaders})));}if (path === '/api/messages/unread-count' && method === 'GET') {if (!userEmailFromSession) return jsonResponse({ error: '用户未认证' }, 401);const doId = env.USER_PRESENCE_DO.idFromName(`user-${userEmailFromSession}`);const stub = env.USER_PRESENCE_DO.get(doId);const forwardRequestHeaders = new Headers(request.headers);forwardRequestHeaders.set('X-User-Email', userEmailFromSession);return stub.fetch(new Request(`${url.origin}/getTotalUnreadCount`, new Request(request, {headers: forwardRequestHeaders})));}return jsonResponse({ error: 'API 端点未找到或请求方法不允许' }, 404);}if (method === 'GET' && path === '/user/help') {return new Response(generateHelpPageHtml(env), { headers: { 'Content-Type': 'text/html;charset=UTF-8' } });}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' } });}if (method === 'GET' && (path === '/' || path === '/user/login' || path === '/user/register' || path === '/user/account')) {return new Response(generateHtmlUi(path, env), { headers: { 'Content-Type': 'text/html;charset=UTF-8' } });}let errorPageIssuerUrl = '/';try { errorPageIssuerUrl = OAUTH_ISSUER_URL(env, request); } catch(e) { }return new Response(generateErrorPageHtml({title: "页面未找到", message: `请求的资源 ${path} 不存在。`, issuerUrl: errorPageIssuerUrl, env}), { status: 404, headers: { 'Content-Type': 'text/html;charset=UTF-8' }});} catch (error) {let errorPageIssuerUrl = '/';try { errorPageIssuerUrl = OAUTH_ISSUER_URL(env, request); } catch (e) { }return new Response(generateErrorPageHtml({title: "服务器内部错误", message: "处理您的请求时发生意外错误。", issuerUrl: errorPageIssuerUrl, env}), { status: 500, headers: { 'Content-Type': 'text/html;charset=UTF-8' }});}},
};

2. my/src/html-ui.js

  • 创建一个新的导出函数 generateMessagingPageHtml(env)
  • 这个函数将负责生成私信页面的完整 HTML。其内容将基于原先账户设置页面中私信标签页的 HTML 结构。
  • _generateAccountPanesHtml 函数中移除原有的私信 (tab-content-messaging) 部分。
  • 确保 generateMessagingPageHtml 包含所有必要的 JavaScript 文件,特别是 main.jsui-messaging.js
  • generateHelpPageHtml_generateScripts的最后一个参数从 false 改为 true,以确保在帮助页面也能加载所有UI脚本,这有助于顶部栏按钮(如主题切换、未来的用户菜单等)在所有页面行为一致。
function _escapeHtml(unsafe) {if (typeof unsafe !== 'string') return '';return unsafe.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
}function _getIssuerUrl(env, req) {let baseUrl = "https://qmwneb946.dpdns.org";if (req && req.url) {try {const urlObj = new URL(req.url);baseUrl = `${urlObj.protocol}//${urlObj.hostname}`;if (baseUrl.endsWith("my.qmwneb946.dpdns.org")) {baseUrl = baseUrl.replace("my.qmwneb946.dpdns.org", "qmwneb946.dpdns.org");}} catch (e) { }} else if (env && env.EXPECTED_ISSUER_URL) {baseUrl = env.EXPECTED_ISSUER_URL;}return baseUrl;
};function _generateIcons() {return {menuIcon: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="h-6 w-6"><path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" /></svg>`,userIcon: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="h-5 w-5 mr-1"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-5.5-2.5a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0zM10 12a5.99 5.99 0 00-4.793 2.39A6.483 6.483 0 0010 16.5a6.483 6.483 0 004.793-2.11A5.99 5.99 0 0010 12z" clip-rule="evenodd" /></svg>`,sunIcon: `<svg id="theme-toggle-light-icon" class="h-5 w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M12 3v1m0 16v1m8.66-12.66l-.707.707M5.05 5.05l-.707.707M21 12h-1M4 12H3m15.66 8.66l-.707-.707M6.757 17.243l-.707-.707M12 6a6 6 0 100 12 6 6 0 000-12z"></path></svg>`,moonIcon: `<svg id="theme-toggle-dark-icon" class="h-5 w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"></path></svg>`,messageIcon: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="h-5 w-5"><path stroke-linecap="round" stroke-linejoin="round" d="M2.25 12.76c0 1.6 1.123 2.994 2.707 3.227 1.087.16 2.185.283 3.293.369V21l4.076-4.076a1.526 1.526 0 011.037-.443 48.282 48.282 0 005.68-.494c1.584-.233 2.707-1.626 2.707-3.228V6.741c0-1.602-1.123-2.995-2.707-3.228A48.394 48.394 0 0012 3c-2.392 0-4.744.175-7.043.513C3.373 3.746 2.25 5.14 2.25 6.741v6.018z" /></svg>`,warningIconLarge: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-16 h-16 text-yellow-500"><path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.008v.008H12v-.008z" /></svg>`,mailIcon: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="h-6 w-6 mr-2"><path stroke-linecap="round" stroke-linejoin="round" d="M21.75 6.75v10.5a2.25 2.25 0 01-2.25 2.25h-15a2.25 2.25 0 01-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0019.5 4.5h-15a2.25 2.25 0 00-2.25 2.25m19.5 0v.243a2.25 2.25 0 01-1.07 1.916l-7.5 4.615a2.25 2.25 0 01-2.36 0L3.32 8.91a2.25 2.25 0 01-1.07-1.916V6.75" /></svg>`};
}function _generateHead(title, cdnBaseUrl, faviconDataUri, pageSpecificStyles = '', loadMonacoEditor = false) {let monacoLoaderScript = loadMonacoEditor ? `<script src="https://cdn.jsdelivr.net/npm/monaco-editor@0.47.0/min/vs/loader.js"></script>` : '';return `
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>${_escapeHtml(title)}</title><link rel="icon" href="${_escapeHtml(faviconDataUri)}"><link rel="stylesheet" href="${_escapeHtml(cdnBaseUrl)}/css/style.css"><script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script><script src="https://cdn.jsdelivr.net/npm/qrcodejs@1.0.0/qrcode.min.js"></script><script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js" defer></script><script src="https://cdn.jsdelivr.net/npm/dompurify/dist/purify.min.js" defer></script>${monacoLoaderScript}<style>.hidden { display: none !important; }.modal {position: fixed; top: 0; left: 0; width: 100%; height: 100%;background-color: rgba(0,0,0,0.6); display: flex;justify-content: center; align-items: center; z-index: 2000;opacity: 0; visibility: hidden; transition: opacity 0.3s ease, visibility 0s linear 0.3s;}.modal.active { opacity: 1; visibility: visible; transition: opacity 0.3s ease; }.modal-content {background-color: var(--current-surface-color); color: var(--current-text-color);padding: 25px 30px; border-radius: var(--border-radius);box-shadow: 0 5px 15px rgba(0,0,0,0.3); width: 90%; max-width: 500px;transform: translateY(-20px); transition: transform 0.3s ease;}.modal.active .modal-content { transform: translateY(0); }.modal-content h4 { margin-top: 0; text-align: left; font-size: 1.4em; color: var(--current-heading-color); }.modal-buttons { margin-top: 25px; text-align: right; }.modal-buttons button { margin-left: 10px; }.license-code-list { list-style-type: none; padding-left: 0; max-height: 200px; overflow-y: auto; border: 1px solid var(--current-border-color); border-radius: var(--border-radius); padding: 10px; background-color: var(--current-bg-color); }.license-code-list li { padding: 5px 0; border-bottom: 1px dashed var(--current-border-color); font-family: var(--font-family-mono); font-size: 0.9em;}.license-code-list li:last-child { border-bottom: none; }${pageSpecificStyles}</style>
</head>`;
}function _generateScripts(cdnBaseUrl, pageSpecificScripts = '', includeAllUiScripts = true) {let scripts = '';if (includeAllUiScripts) {scripts += `<script src="${_escapeHtml(cdnBaseUrl)}/js/main.js" defer></script><script src="${_escapeHtml(cdnBaseUrl)}/js/ui-personal-info.js" defer></script><script src="${_escapeHtml(cdnBaseUrl)}/js/ui-security-settings.js" defer></script><script src="${_escapeHtml(cdnBaseUrl)}/js/ui-api-keys.js" defer></script><script src="${_escapeHtml(cdnBaseUrl)}/js/ui-oauth-apps.js" defer></script><script src="${_escapeHtml(cdnBaseUrl)}/js/ui-messaging.js" defer></script>`;} else {scripts += `<script src="${_escapeHtml(cdnBaseUrl)}/js/main.js" defer></script>`;}if (pageSpecificScripts) {scripts += `<script>${pageSpecificScripts}</script>`;}return scripts;
}function _generateTopBarHtml(siteName, icons, isHelpPageLayout = false, isMessagingPageLayout = false) {let siteTitleLink = `<span class="site-title">${_escapeHtml(siteName)}</span>`;if (isHelpPageLayout) {siteTitleLink = `<a href="/" class="site-title" style="margin-left:0;">${_escapeHtml(siteName)}</a>`;} else if (isMessagingPageLayout) {siteTitleLink = `<a href="/user/account" class="site-title">${_escapeHtml(siteName)}</a>`;}return `<header id="top-bar" class="top-bar"><div class="top-bar-left"><button id="sidebar-toggle" class="sidebar-toggle-button" aria-label="切换导航菜单">${icons.menuIcon}</button>${siteTitleLink}</div><div class="top-bar-right"><button type="button" id="theme-toggle-button" aria-label="切换主题" class="theme-toggle-button">${icons.sunIcon}${icons.moonIcon}</button><button type="button" id="top-bar-messaging-button" aria-label="私信" class="messaging-button hidden">${icons.messageIcon}<span id="unread-messages-indicator" class="unread-badge hidden"></span></button><div id="top-bar-auth-buttons" class="auth-actions hidden"><a href="/user/register" class="button secondary small">注册</a><a href="/user/login" class="button primary small">登录</a></div><div id="top-bar-user-info" class="user-info-dropdown hidden"><button id="user-menu-button" class="user-menu-button">${icons.userIcon}<span class="username-text" id="top-bar-user-username"></span></button><div id="user-dropdown-menu" class="dropdown-menu hidden"><div class="dropdown-user-email" id="top-bar-user-email"></div><hr class="dropdown-divider"><a href="/user/account" id="top-bar-account-link" class="dropdown-item">账户设置</a><a href="/user/help" class="dropdown-item">帮助与API示例</a><button type="button" class="dropdown-item logout" id="top-bar-logout-button">安全登出</button></div></div></div></header>`;
}function _generateSidebarHtml() {return `<aside id="sidebar" class="sidebar"><nav class="sidebar-nav"><ul id="account-tabs"><li><a href="/user/account#tab-personal-info" id="tab-personal-info" class="sidebar-link" data-pane-id="tab-content-personal-info">个人信息</a></li><li><a href="/user/account#tab-security-settings" id="tab-security-settings" class="sidebar-link" data-pane-id="tab-content-security-settings">安全设置</a></li><li><a href="/user/account#tab-api-keys" id="tab-api-keys" class="sidebar-link" data-pane-id="tab-content-api-keys">获取/查看密钥</a></li><li><a href="/user/account#tab-my-applications" id="tab-my-applications" class="sidebar-link" data-pane-id="tab-content-my-applications">我的应用</a></li></ul></nav></aside>`;
}function _generateAuthFormsHtml(showLogin, showRegister, env, authSectionVisible) {const turnstileSiteKey = env.TURNSTILE_SITE_KEY || '1x00000000000000000000AA';return `<div id="auth-section" class="auth-section ${authSectionVisible ? '' : 'hidden'}"><div id="login-form" class="form-container ${showLogin ? '' : 'hidden'}"><h2>用户登录</h2><form id="login-form-el"><div class="form-group"><label for="login-identifier">邮箱或用户名</label><input type="text" id="login-identifier" name="identifier" required autocomplete="username email" placeholder="请输入您的邮箱或用户名"></div><div class="form-group"><label for="login-password">密码</label><input type="password" id="login-password" name="password" required autocomplete="current-password" placeholder="请输入密码"></div><div id="login-2fa-section" class="form-group hidden"><label for="login-totp-code">两步验证码</label><input type="text" id="login-totp-code" name="totpCode" pattern="\\d{6}" maxlength="6" placeholder="请输入6位验证码"></div><div class="cf-turnstile" data-sitekey="${_escapeHtml(turnstileSiteKey)}" data-callback="turnstileCallbackLogin"></div><button type="submit" class="button primary full-width">登录</button></form><div class="toggle-link">还没有账户? <a href="/user/register">立即注册</a></div></div><div id="register-form" class="form-container ${showRegister ? '' : 'hidden'}"><h2>新用户注册</h2><form id="register-form-el"><div class="form-group"><label for="register-username">用户名</label><input type="text" id="register-username" name="username" required minlength="3" maxlength="30" placeholder="3-30位字符,可包含字母、数字、_-"></div><div class="form-group"><label for="register-email">邮箱地址</label><input type="email" id="register-email" name="email" required autocomplete="email" placeholder="例如:user@example.com"></div><div class="form-group"><label for="register-phone">手机号码 (可选)</label><input type="tel" id="register-phone" name="phoneNumber" autocomplete="tel" placeholder="例如:+8613800138000"></div><div class="form-group"><label for="register-password">设置密码 (至少6位)</label><input type="password" id="register-password" name="password" required minlength="6" autocomplete="new-password" placeholder="请输入至少6位密码"></div><div class="form-group"><label for="register-confirm-password">确认密码</label><input type="password" id="register-confirm-password" name="confirmPassword" required minlength="6" autocomplete="new-password" placeholder="请再次输入密码"></div><div class="cf-turnstile" data-sitekey="${_escapeHtml(turnstileSiteKey)}" data-callback="turnstileCallbackRegister"></div><button type="submit" class="button primary full-width">创建账户</button></form><div class="toggle-link">已经有账户了? <a href="/user/login">返回登录</a></div></div></div>`;
}function _generateAccountPanesHtml(env, icons) {const turnstileSiteKey = env.TURNSTILE_SITE_KEY || '1x00000000000000000000AA';return `<div id="tab-content-personal-info" class="tab-pane"><h3>个人信息修改</h3><form id="update-profile-form" class="content-form"><div class="form-group"><label for="profile-username">用户名</label><input type="text" id="profile-username" name="username" required minlength="3" maxlength="30" placeholder="3-30位字符"></div><div class="form-group"><label for="profile-phone">手机号码 (可选)</label><input type="tel" id="profile-phone" name="phoneNumber" placeholder="例如:+8613800138000"></div><div class="form-group"><label for="profile-email">邮箱地址 (不可修改)</label><input type="email" id="profile-email" name="email" readonly disabled></div><button type="submit" class="button primary">保存个人信息</button></form></div><div id="tab-content-security-settings" class="tab-pane hidden"><h3>安全设置</h3><ul class="security-settings-list"><li class="security-setting-item"><div class="setting-entry" data-target="change-password-content-panel"><span class="entry-title">修改密码</span><span class="entry-arrow">▼</span></div><div id="change-password-content-panel" class="setting-content-panel hidden"><form id="change-password-form" class="content-form"><div class="form-group"><label for="current-password">当前密码</label><input type="password" id="current-password" name="currentPassword" required autocomplete="current-password" placeholder="请输入您当前的密码"></div><div class="form-group"><label for="new-password">新密码 (至少6位)</label><input type="password" id="new-password" name="newPassword" required minlength="6" autocomplete="new-password" placeholder="请输入新的密码"></div><button type="submit" class="button secondary">确认修改密码</button></form></div></li><li class="security-setting-item"><div class="setting-entry" data-target="2fa-content-panel"><span class="entry-title">两步验证 (2FA)</span><span class="entry-status" id="2fa-entry-status">(未知)</span><span class="entry-arrow">▼</span></div><div id="2fa-content-panel" class="setting-content-panel hidden"><p class="description-text">使用两步验证码可以帮您双重保护账户安全。</p><div id="2fa-status-section" class="status-display">当前状态: <span id="2fa-current-status">未知</span></div><div id="2fa-controls" style="margin-bottom: 15px;"><button type="button" id="btn-init-enable-2fa" class="button success small hidden">启用两步验证</button><button type="button" id="btn-disable-2fa" class="button danger small hidden">禁用两步验证</button></div><div id="2fa-setup-section" class="hidden" style="margin-top:20px;"><p>1. 使用您的身份验证器应用扫描二维码或手动输入密钥。</p><div style="text-align: center; margin: 15px 0;"><div id="qrcode-display"></div></div><p>密钥链接: <code id="otpauth-uri-text-display" class="otpauth-uri-text"></code></p><input type="hidden" id="2fa-temp-secret"><div class="form-group" style="margin-top:10px;"><label for="2fa-setup-code">6位验证码</label><input type="text" id="2fa-setup-code" name="totpCode" pattern="\\d{6}" maxlength="6" placeholder="请输入验证码"></div><div class="form-actions"><button type="button" id="btn-complete-enable-2fa" class="button success">验证并启用</button><button type="button" id="btn-cancel-2fa-setup" class="button secondary">取消</button></div></div></div></li></ul></div><div id="tab-content-api-keys" class="tab-pane hidden"><h3>获取/查看密钥</h3><ul class="security-settings-list"><li class="security-setting-item"><div class="setting-entry" data-target="paste-api-key-content-panel"><span class="entry-title">云剪贴板 API 密钥</span><span class="entry-arrow">▼</span></div><div id="paste-api-key-content-panel" class="setting-content-panel hidden"><p class="description-text">API 密钥名称将自动生成,仅拥有文本和文件权限。</p><p class="description-text">访问云剪贴板服务:<a href="http://go.qmwneb946.dpdns.org/?LinkId=37" target="_blank" rel="noopener noreferrer" class="external-link">http://go.qmwneb946.dpdns.org/?LinkId=37</a></p><form id="create-paste-api-key-form" class="content-form" style="margin-top:15px;"><div class="cf-turnstile" data-sitekey="${_escapeHtml(turnstileSiteKey)}" data-callback="turnstileCallbackPasteApi"></div><button type="submit" class="button success">创建云剪贴板 API 密钥</button></form><div id="newly-created-api-key-display" class="hidden api-key-display" style="margin-top:15px;"><h5>新创建的云剪贴板 API 密钥:</h5><div class="api-key-value-container"><input type="text" id="new-api-key-value" readonly><button type="button" class="button small secondary" onclick="copyToClipboard(document.getElementById('new-api-key-value').value, '云剪贴板 API 密钥')">复制</button></div></div></div></li><li class="security-setting-item"><div class="setting-entry" data-target="cloud-pc-key-content-panel"><span class="entry-title">Cloud PC 密钥</span><span class="entry-status" id="cloud-pc-key-entry-status">(点击展开查看状态)</span><span class="entry-arrow">▼</span></div><div id="cloud-pc-key-content-panel" class="setting-content-panel hidden"><div id="cloud-pc-key-status-area" class="status-display">正在加载 Cloud PC 密钥状态...</div><p class="description-text">访问 Cloud PC 服务:<a href="http://go.qmwneb946.dpdns.org/?LinkId=17" target="_blank" rel="noopener noreferrer" class="external-link">http://go.qmwneb946.dpdns.org/?LinkId=17</a></p><form id="create-cloud-pc-key-form" class="content-form hidden" style="margin-top:15px;"><p class="description-text">每位用户仅可创建一次,获得 1 次使用次数。</p><div class="cf-turnstile" data-sitekey="${_escapeHtml(turnstileSiteKey)}" data-callback="turnstileCallbackCloudPc"></div><button type="submit" class="button success">创建 Cloud PC 密钥</button></form><div id="existing-cloud-pc-key-display" class="hidden api-key-display" style="margin-top:15px;"><h5>您的 Cloud PC 密钥:</h5><div class="api-key-value-container"><input type="text" id="cloud-pc-api-key-value" readonly><button type="button" class="button small secondary" onclick="copyToClipboard(document.getElementById('cloud-pc-api-key-value').value, 'Cloud PC API 密钥')">复制</button></div><p>剩余使用次数: <strong id="cloud-pc-usage-count"></strong></p></div></div></li><li class="security-setting-item"><div class="setting-entry" data-target="greenhub-key-content-panel"><span class="entry-title">GreenHub 激活码</span><span class="entry-arrow">▼</span></div><div id="greenhub-key-content-panel" class="setting-content-panel hidden"><p class="description-text">查看您已获取的 GreenHub 激活码。</p><button type="button" id="btn-fetch-greenhub-keys" class="button primary">获取 GreenHub 激活码</button><div id="greenhub-codes-display" style="margin-top:15px;"><p class="placeholder-text">点击按钮获取激活码。</p></div></div></li></ul></div><div id="tab-content-my-applications" class="tab-pane hidden"><h3>我的 OAuth 应用</h3><div class="setting-block"><h4>注册新应用</h4><form id="register-oauth-client-form" class="content-form"><div class="form-group"><label for="oauth-client-name">应用名称</label><input type="text" id="oauth-client-name" name="clientName" required maxlength="50" placeholder="例如:我的博客评论系统"></div><div class="form-group"><label for="oauth-client-website">应用主页 (可选)</label><input type="url" id="oauth-client-website" name="clientWebsite" maxlength="200" placeholder="例如:https://myblog.com"></div><div class="form-group"><label for="oauth-client-description">应用描述 (可选)</label><input type="text" id="oauth-client-description" name="clientDescription" maxlength="200" placeholder="简要描述您的应用"></div><div class="form-group"><label for="oauth-client-redirect-uri">回调地址 (Redirect URI)</label><input type="url" id="oauth-client-redirect-uri" name="redirectUri" required placeholder="例如:https://myblog.com/oauth/callback"><p class="input-hint">必须是 HTTPS 地址。</p></div><div class="cf-turnstile" data-sitekey="${_escapeHtml(turnstileSiteKey)}" data-callback="turnstileCallbackOauthClient"></div><button type="submit" class="button success">注册应用</button></form><div id="new-oauth-client-credentials" class="hidden" style="margin-top: 20px;"><h5 style="color: var(--primary-color);">应用注册成功!</h5><p class="description-text">请妥善保管您的应用凭据,特别是 <strong>客户端密钥 (Client Secret)</strong>,它将仅显示这一次。</p><div class="application-card"><p><strong>客户端 ID:</strong> <code id="new-client-id-display"></code> <button type="button" class="button small secondary" onclick="copyToClipboard(document.getElementById('new-client-id-display').textContent, '客户端 ID')">复制</button></p><p><strong>客户端密钥:</strong> <code id="new-client-secret-display"></code> <button type="button" class="button small secondary" onclick="copyToClipboard(document.getElementById('new-client-secret-display').textContent, '客户端密钥')">复制</button></p></div><div class="new-client-secret-warning"><strong>重要提示:</strong> 客户端密钥非常敏感,请立即复制并安全存储。</div></div></div><hr class="section-divider"><h4>已注册的应用</h4><div id="registered-oauth-clients-list"><p>正在加载应用列表...</p></div></div>`;
}function _generateModalsHtml() {return `<div id="edit-oauth-client-modal" class="modal hidden"><div class="modal-content"><h4 id="edit-oauth-client-modal-title">编辑应用信息</h4><form id="edit-oauth-client-form"><input type="hidden" id="edit-client-id" name="clientId"><div class="form-group"><label for="edit-oauth-client-name">应用名称</label><input type="text" id="edit-oauth-client-name" name="clientName" required maxlength="50"></div><div class="form-group"><label for="edit-oauth-client-website">应用主页 (可选)</label><input type="url" id="edit-oauth-client-website" name="clientWebsite" maxlength="200"></div><div class="form-group"><label for="edit-oauth-client-description">应用描述 (可选)</label><input type="text" id="edit-oauth-client-description" name="clientDescription" maxlength="200"></div><div class="form-group"><label for="edit-oauth-client-redirect-uri">回调地址</label><input type="url" id="edit-oauth-client-redirect-uri" name="redirectUri" required><p class="input-hint">必须是 HTTPS 地址。</p></div><div class="modal-buttons"><button type="button" id="btn-cancel-edit-oauth-client" class="button secondary">取消</button><button type="submit" class="button success">保存更改</button></div></form></div></div>`;
}export function generateHtmlUi(currentPath = '/', env = {}) {const icons = _generateIcons();const cdnBaseUrl = env.CDN_BASE_URL || "https://cdn.qmwneb946.dpdns.org";const siteName = "用户中心";const faviconDataUri = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Ctext y='.9em' font-size='90'%3E👤%3C/text%3E%3C/svg%3E";const showLogin = currentPath === '/' || currentPath === '/user/login';const showRegister = currentPath === '/user/register';const showAccount = currentPath === '/user/account';const authSectionVisible = showLogin || showRegister;const appWrapperClass = authSectionVisible ? 'logged-out-layout' : (showAccount ? 'logged-in-layout' : '');return `
<!DOCTYPE html>
<html lang="zh-CN">
${_generateHead(siteName, cdnBaseUrl, faviconDataUri)}
<body class="font-sans antialiased"><div id="app-wrapper" class="app-wrapper ${appWrapperClass}">${_generateTopBarHtml(siteName, icons)}${_generateSidebarHtml()}<div id="sidebar-overlay" class="sidebar-overlay hidden"></div><main id="main-content" class="main-content"><div class="container"><div id="message-area" class="message hidden" role="alert"></div>${_generateAuthFormsHtml(showLogin, showRegister, env, authSectionVisible)}<div id="logged-in-section" class="account-content ${showAccount ? '' : 'hidden'}">${_generateAccountPanesHtml(env, icons)}</div></div></main></div>${_generateModalsHtml()}${_generateScripts(cdnBaseUrl, '', true)}
</body>
</html>`;
}export function generateMessagingPageHtml(env = {}) {const icons = _generateIcons();const cdnBaseUrl = env.CDN_BASE_URL || "https://cdn.qmwneb946.dpdns.org";const siteName = "用户中心";const pageTitle = "个人私信 - " + siteName;const faviconDataUri = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Ctext y='.9em' font-size='90'%3E👤%3C/text%3E%3C/svg%3E";const messagingPageStyles = `.app-wrapper.messaging-page-layout .sidebar { display: none; } /* Or manage display via main.js based on screen size */.app-wrapper.messaging-page-layout .main-content { margin-left: 0; }.app-wrapper.messaging-page-layout .main-content .container { padding: 0; max-width: 100%; background-color: var(--current-bg-color); }.messaging-page-container { display: flex; flex-direction: column; height: calc(100vh - var(--top-bar-height)); }`;const messagingPageSpecificScripts = `document.addEventListener('DOMContentLoaded', () => {if (typeof window.checkLoginStatus === 'function') {window.checkLoginStatus().then(() => {if (window.currentUserData && typeof window.loadMessagingTabData === 'function') {window.loadMessagingTabData();} else if (!window.currentUserData) {window.location.href = '/user/login?return_to=/user/messaging';}});}// Adjust layout for messaging page, sidebar might be hidden by default on this page or handled by main.jsconst appWrapper = document.getElementById('app-wrapper');if(appWrapper) appWrapper.classList.add('messaging-page-layout');const sidebarToggle = document.getElementById('sidebar-toggle');if(sidebarToggle) sidebarToggle.classList.add('hidden'); // Hide sidebar toggle on dedicated messaging page});`;return `
<!DOCTYPE html>
<html lang="zh-CN">
${_generateHead(pageTitle, cdnBaseUrl, faviconDataUri, messagingPageStyles)}
<body class="font-sans antialiased"><div id="app-wrapper" class="app-wrapper logged-in-layout messaging-page-layout">${_generateTopBarHtml(siteName, icons, false, true)}<aside id="sidebar" class="sidebar hidden"></aside> {/* Sidebar can be empty or hidden by CSS/JS */}<div id="sidebar-overlay" class="sidebar-overlay hidden"></div><main id="main-content" class="main-content"><div class="container"><div id="message-area" class="message hidden" role="alert"></div><div class="messaging-page-container"> {/* Wrapper for messaging content */}<div class="messaging-tab-header" style="padding: 15px 20px 10px; margin-bottom:0;">${icons.mailIcon}<h3 style="margin-bottom:0; border-bottom:none; padding-bottom:0; font-size: 1.75rem;">个人私信</h3></div><div class="new-conversation-trigger" id="new-conversation-area-messaging" style="padding: 0px 20px 15px; border-bottom: 1px solid var(--current-border-color);"><input type="email" id="new-conversation-email" placeholder="输入对方邮箱开始新对话..."><button type="button" id="btn-start-new-conversation" class="button primary small">开始</button></div><div class="messaging-layout-new" style="height: calc(100% - 130px); border:none; box-shadow:none; border-radius:0;"> {/* Adjusted height and remove borders */}<div class="messaging-contacts-panel"><div class="contact-search-bar"><input type="search" id="contact-search-input" placeholder="搜索联系人..."></div><h4 class="recent-contacts-title">最近联系</h4><ul id="conversations-list" class="contact-list"><p class="placeholder-text">正在加载对话...</p></ul></div><div id="messages-area" class="message-display-panel"><div id="messages-list-wrapper"><div id="message-loader" class="message-loader hidden"><div class="spinner"></div></div><div id="messages-list" class="messages-list"><div class="empty-messages-placeholder"><p>选择一个联系人开始聊天</p><span>或通过上方搜索框发起新的对话。</span></div></div></div><div id="message-input-area" class="message-input-area hidden"><textarea id="message-input" placeholder="输入消息..." rows="1"></textarea><button type="button" id="btn-send-message" class="button primary">发送</button></div></div></div></div></div></main></div>${_generateScripts(cdnBaseUrl, messagingPageSpecificScripts, true)}
</body>
</html>`;
}export function generateHelpPageHtml(env = {}) {const icons = _generateIcons();const siteName = "用户中心";const faviconDataUri = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Ctext y='.9em' font-size='90'%3E👤%3C/text%3E%3C/svg%3E";const cdnBaseUrl = env.CDN_BASE_URL || "https://cdn.qmwneb946.dpdns.org";const currentIssuerUrl = _getIssuerUrl(env, { url: (typeof window !== 'undefined' && window.location.href) ? window.location.href : 'https://my.qmwneb946.dpdns.org' });const oauthRedirectUrlExample = `${currentIssuerUrl}/oauth/authorize?response_type=code&client_id=YOUR_CLIENT_ID&redirect_uri=YOUR_REGISTERED_REDIRECT_URI&scope=openid%20profile%20email&state=YOUR_OPAQUE_STATE_VALUE&nonce=YOUR_UNIQUE_NONCE_VALUE`;const curlTokenExample = `curl -X POST "${currentIssuerUrl}/oauth/token" \\-H "Content-Type: application/x-www-form-urlencoded" \\-d "grant_type=authorization_code" \\-d "code=THE_AUTHORIZATION_CODE_YOU_RECEIVED" \\-d "redirect_uri=YOUR_REGISTERED_REDIRECT_URI" \\-d "client_id=YOUR_CLIENT_ID" \\-d "client_secret=YOUR_CLIENT_SECRET"`;const curlUserInfoExample = `curl -X GET "${currentIssuerUrl}/oauth/userinfo" \\-H "Authorization: Bearer YOUR_ACCESS_TOKEN"`;const helpPageStyles = `.help-content { padding: 20px; }.monaco-editor-container {border: 1px solid var(--current-border-color);margin-bottom: 1em;overflow: hidden;}.api-usage-section :not(pre) > code {background-color: color-mix(in srgb, var(--current-text-color) 10%, transparent);padding: 2px 5px;border-radius: 3px;font-family: 'Monaco', var(--font-family-mono);}hr.section-divider { border: none; border-top: 1px solid var(--current-border-color); margin: 30px 0; }.app-wrapper.help-page-layout .sidebar { display: none; }.app-wrapper.help-page-layout .main-content { margin-left: 0; }.input-hint { font-size: 0.8rem; color: var(--current-text-muted-color); margin-top: 4px; }.external-link { color: var(--accent-color); font-weight: 500; }.external-link:hover { color: color-mix(in srgb, var(--accent-color) 80%, black); }`;const helpPageScripts = `document.addEventListener('DOMContentLoaded', function () {const isDarkMode = document.body.classList.contains('dark-mode');const editorTheme = isDarkMode ? 'vs-dark' : 'vs';require.config({ paths: { 'vs': 'https://cdn.jsdelivr.net/npm/monaco-editor@0.47.0/min/vs' }});require(['vs/editor/editor.main'], function () {const editorContainers = document.querySelectorAll('.monaco-editor-container');editorContainers.forEach(container => {const codeContent = container.dataset.content || '';let language = container.dataset.language || 'plaintext';if (language === 'uri') language = 'plaintext';if (language === 'bash') language = 'shell';const lines = codeContent.split('\\\\n').length; // Use escaped newline for splitconst lineHeight = 19;let editorHeight = Math.max(60, Math.min(400, lines * lineHeight + 20));if (lines === 1 && codeContent.length > 80) {editorHeight = Math.max(40, Math.min(120, Math.ceil(codeContent.length / 80) * lineHeight + 10) );} else if (lines === 1) {editorHeight = 40;}container.style.height = editorHeight + 'px';monaco.editor.create(container, {value: codeContent,language: language,readOnly: true,theme: editorTheme,minimap: { enabled: false },automaticLayout: true,scrollBeyondLastLine: false,wordWrap: 'on',renderWhitespace: "boundary",fontSize: 13,fontFamily: "'Monaco', Consolas, 'Courier New', monospace",scrollbar: {verticalScrollbarSize: 8,horizontalScrollbarSize: 8,alwaysConsumeMouseWheel: false},lineNumbers: (lines > 1) ? 'on' : 'off',overviewRulerLanes: (lines > 1) ? 2 : 0,hideCursorInOverviewRuler: true,renderLineHighlight: 'none'});});});const themeToggleButton = document.getElementById('theme-toggle-button');if (themeToggleButton) {let currentIsDarkMode = document.body.classList.contains('dark-mode');function applyEditorTheme(dark) {const newEditorTheme = dark ? 'vs-dark' : 'vs';if (typeof monaco !== 'undefined' && monaco.editor && monaco.editor.getEditors) {const editors = monaco.editor.getEditors();editors.forEach(editor => {monaco.editor.setTheme(newEditorTheme);});}}if (typeof monaco !== 'undefined' && monaco.editor) {applyEditorTheme(currentIsDarkMode);}themeToggleButton.addEventListener('click', () => {currentIsDarkMode = document.body.classList.contains('dark-mode'); if (typeof monaco !== 'undefined' && monaco.editor) {applyEditorTheme(currentIsDarkMode);}});}const observer = new MutationObserver(mutationsList => {for (let mutation of mutationsList) {if (mutation.type === 'attributes' && mutation.attributeName === 'class') {const currentIsDarkModeForObserver = document.body.classList.contains('dark-mode');if (typeof monaco !== 'undefined' && monaco.editor && monaco.editor.getEditors && monaco.editor.getEditors().length > 0) {const editors = monaco.editor.getEditors();const newEditorTheme = currentIsDarkModeForObserver ? 'vs-dark' : 'vs';if (editors[0] && editors[0].getOptions().get(monaco.editor.EditorOption.theme) !== newEditorTheme) {monaco.editor.setTheme(newEditorTheme);}}}}});observer.observe(document.body, { attributes: true });});`;return `
<!DOCTYPE html>
<html lang="zh-CN">
${_generateHead(`帮助与API示例 - ${siteName}`, cdnBaseUrl, faviconDataUri, helpPageStyles, true)}
<body class="font-sans antialiased"><div id="app-wrapper" class="app-wrapper help-page-layout">${_generateTopBarHtml(siteName, icons, true)}<aside id="sidebar" class="sidebar hidden"> <nav class="sidebar-nav"><ul id="account-tabs"><li><a href="/user/account#tab-personal-info" class="sidebar-link">返回账户</a></li></ul></nav></aside><div id="sidebar-overlay" class="sidebar-overlay hidden"></div><main id="main-content" class="main-content"><div class="container"><div id="message-area" class="message hidden" role="alert"></div><div id="help-page-content" class="help-content api-usage-section"><h3>欢迎来到 API 使用帮助中心</h3><p>本页面提供关于如何使用本用户中心提供的各种 API 和服务的示例与说明。</p><hr class="section-divider"><h3>OAuth 2.0 / OpenID Connect (OIDC)</h3><p>本用户中心支持作为 OAuth 2.0 和 OpenID Connect 的身份提供方 (IdP)。开发者可以使用它来为自己的应用添加用户认证和授权功能。</p><h4>1. 获取授权码 (Authorization Code)</h4><p>首先,您的应用需要将用户重定向到本用户中心的授权端点 (<code>${_escapeHtml(currentIssuerUrl)}/oauth/authorize</code>)。用户登录并同意授权后,本系统会将用户重定向回您应用注册的回调地址 (Redirect URI),并附带一个授权码。</p><p>例如,重定向 URL 可能如下:</p><div class="monaco-editor-container" data-language="uri" data-content="${_escapeHtml(oauthRedirectUrlExample.replace(/\n/g, '\\n'))}"></div><p class="input-hint">请确保替换上述 URL 中的占位符 (<code>YOUR_CLIENT_ID</code>, <code>YOUR_REGISTERED_REDIRECT_URI</code>, <code>YOUR_OPAQUE_STATE_VALUE</code>, <code>YOUR_UNIQUE_NONCE_VALUE</code>) 为您的实际值。</p><h4>2. 使用授权码交换令牌 (Access Token & ID Token)</h4><p>获得授权码后,您的应用后端服务需要向令牌端点 (<code>${_escapeHtml(currentIssuerUrl)}/oauth/token</code>) 发起 POST 请求,以授权码交换访问令牌 (Access Token) 和身份令牌 (ID Token)。</p><div class="monaco-editor-container" data-language="shell" data-content="${_escapeHtml(curlTokenExample.replace(/\n/g, '\\n'))}"></div><p>成功的响应将是一个 JSON 对象,其中包含 <code>access_token</code>, <code>id_token</code>, <code>token_type</code>, <code>expires_in</code>, 以及可能的 <code>refresh_token</code> (如果请求了 <code>offline_access</code> 范围并且客户端允许)。</p><h4>3. 使用访问令牌获取用户信息</h4><p>获得访问令牌后,您的应用可以使用它向用户信息端点 (<code>${_escapeHtml(currentIssuerUrl)}/oauth/userinfo</code>) 请求已授权的用户信息。</p><div class="monaco-editor-container" data-language="shell" data-content="${_escapeHtml(curlUserInfoExample.replace(/\n/g, '\\n'))}"></div><p>响应内容将是一个 JSON 对象,包含用户的相关信息,具体取决于授权时请求的范围 (scopes),例如 <code>sub</code> (用户唯一标识), <code>email</code>, <code>name</code>, <code>username</code> 等。</p><p class="input-hint">有关 OAuth 和 OIDC 的更多详细信息,请参阅相关的官方规范文档。您也可以在本站的 <code>/.well-known/openid-configuration</code> 路径查看 OpenID Connect 发现文档。</p></div></div></main></div>${_generateScripts(cdnBaseUrl, helpPageScripts, true)}
</body>
</html>`;
}export function generateConsentScreenHtml(data) {const { clientName, requestedScopes, user, formAction, clientId, redirectUri, scope, state, nonce, responseType, issuerUrl: consentIssuerUrl, cdnBaseUrl: consentCdnBaseUrl, env = {} } = data;const finalCdnBaseUrl = consentCdnBaseUrl || env.CDN_BASE_URL || "https://cdn.qmwneb946.dpdns.org";const siteName = "用户中心授权";const faviconDataUri = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Ctext y='.9em' font-size='90'%3E👤%3C/text%3E%3C/svg%3E";const scopesHtml = requestedScopes.map(s => `<li>${_escapeHtml(s)}</li>`).join('');const consentScreenStyles = `body { display: flex; justify-content: center; align-items: center; min-height: 100vh; padding: 20px; background-color: var(--current-bg-color); color: var(--current-text-color); }.consent-container { background-color: var(--current-surface-color); padding: 30px 40px; border-radius: var(--border-radius); box-shadow: var(--box-shadow-md); max-width: 480px; width:100%; text-align: center; }.consent-container h2 { margin-top: 0; color: var(--current-heading-color); font-size: 1.8em; margin-bottom: 20px; }.consent-container p { margin-bottom: 15px; line-height: 1.6; font-size: 1.05em; }.client-name { font-weight: bold; color: var(--primary-color); }.user-identifier { font-weight: bold; }.scopes-list { list-style: inside disc; text-align: left; margin: 20px auto; padding-left: 25px; max-width: fit-content; }.scopes-list li { margin-bottom: 8px; }.consent-buttons { margin-top: 30px; display: flex; justify-content: space-between; gap: 15px; }.consent-buttons button { flex-grow: 1; }`;const consentScreenScripts = `if (localStorage.getItem('theme') === 'dark' || (!localStorage.getItem('theme') && window.matchMedia('(prefers-color-scheme: dark)').matches)) {document.body.classList.add('dark-mode');}`;return `
<!DOCTYPE html>
<html lang="zh-CN">
${_generateHead(`授权请求 - ${siteName}`, finalCdnBaseUrl, faviconDataUri, consentScreenStyles)}
<body class="font-sans antialiased"><div class="consent-container"><h2>授权请求</h2><p>应用 <strong class="client-name">${_escapeHtml(clientName)}</strong> (${_escapeHtml(clientId)})</p><p>正在请求访问您 <strong class="user-identifier">(${_escapeHtml(user.username || user.email)})</strong> 的以下信息:</p><ul class="scopes-list">${scopesHtml}</ul><p>您是否允许此应用访问?</p><form method="POST" action="${_escapeHtml(formAction)}"><input type="hidden" name="client_id" value="${_escapeHtml(clientId)}"><input type="hidden" name="redirect_uri" value="${_escapeHtml(redirectUri)}"><input type="hidden" name="scope" value="${_escapeHtml(scope)}"><input type="hidden" name="state" value="${_escapeHtml(state || '')}"><input type="hidden" name="nonce" value="${_escapeHtml(nonce || '')}"><input type="hidden" name="response_type" value="${_escapeHtml(responseType)}"><div class="consent-buttons"><button type="submit" name="decision" value="deny" class="button secondary">拒绝</button><button type="submit" name="decision" value="allow" class="button primary">允许</button></div></form></div>${_generateScripts(finalCdnBaseUrl, consentScreenScripts, true)}
</body>
</html>`;
}export function generateErrorPageHtml(data) {const { title, message, issuerUrl, cdnBaseUrl: dataCdnBaseUrl, env = {} } = data;const cdnBaseUrl = dataCdnBaseUrl || env.CDN_BASE_URL || "https://cdn.qmwneb946.dpdns.org";const siteName = "用户中心";const faviconDataUri = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Ctext y='.9em' font-size='90'%3E👤%3C/text%3E%3C/svg%3E";const finalIssuerUrl = issuerUrl || _getIssuerUrl(env, { url: (typeof window !== 'undefined' && window.location.href) ? window.location.href : 'https://my.qmwneb946.dpdns.org' });const errorPageStyles = `body { display: flex; justify-content: center; align-items: center; min-height: 100vh; padding: 20px; background-color: var(--current-bg-color); color: var(--current-text-color); }.error-container { background-color: var(--current-surface-color); padding: 30px 40px; border-radius: var(--border-radius); box-shadow: var(--box-shadow-md); max-width: 450px; width: 100%; text-align: center;}.error-container h1 { color: var(--danger-color); margin-top: 0; font-size: 2em; margin-bottom: 15px;}.error-container p { font-size: 1.1em; margin-bottom: 25px; }.error-container a { color: var(--primary-color); text-decoration: none; font-weight: 500; }.error-container a:hover { text-decoration: underline; }`;const errorPageScripts = `if (localStorage.getItem('theme') === 'dark' || (!localStorage.getItem('theme') && window.matchMedia('(prefers-color-scheme: dark)').matches)) {document.body.classList.add('dark-mode');}`;return `
<!DOCTYPE html>
<html lang="zh-CN">
${_generateHead(`错误 - ${siteName}`, cdnBaseUrl, faviconDataUri, errorPageStyles)}
<body class="font-sans antialiased"><div class="error-container"><h1>${_escapeHtml(title)}</h1><p>${_escapeHtml(message)}</p><p><a href="${_escapeHtml(finalIssuerUrl || '/user/login')}">返回登录或首页</a></p></div>${_generateScripts(cdnBaseUrl, errorPageScripts, true)}
</body>
</html>`;
}

3. cdn/js/main.js

  • 修改顶部私信按钮 (topBarMessagingButton) 的事件监听器。之前它调用 activateTab,现在应该直接导航到 /user/messaging
  • 由于私信功能现在是独立页面,原先在 activateTab 中专门为 tab-content-messaging 加载数据的逻辑 (if (pane.id === 'tab-content-messaging' && typeof window.loadMessagingTabData === 'function')) 可以移除或调整。
  • loadMessagingTabData 函数(在 ui-messaging.js 中)现在将作为 /user/messaging 页面的主要初始化函数。它应该在 DOMContentLoaded 事件后,并且在 checkLoginStatus 确认用户已登录后被调用。

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) {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;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();}
}
function displayCorrectView(userData) {clearMessages();currentUserData = userData;const isLoggedIn = !!userData?.email;document.querySelectorAll('.cf-turnstile').forEach(div => removeTurnstile(div));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) topBarMessagingButton.classList.remove('active'); }const currentPath = window.location.pathname;if (isLoggedIn) {if (currentPath !== '/user/messaging') { // Only show sidebar for non-messaging logged-in pagesif (sidebarEl) sidebarEl.classList.remove('hidden');} else {if (sidebarEl) sidebarEl.classList.add('hidden');}if (appWrapper) appWrapper.classList.add('logged-in-layout');if (appWrapper) appWrapper.classList.remove('logged-out-layout');if (['/', '/user/login', '/user/register'].includes(currentPath)) { window.location.pathname = '/user/account'; return; }if(authSection) authSection.classList.add('hidden');if(loggedInSection && currentPath === '/user/account') {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') && hashTabId !== 'tab-messaging') {tabToActivateId = hashTabId;}}const tabLinkToActivate = document.getElementById(tabToActivateId) || document.getElementById(defaultTabId);if (tabLinkToActivate) activateTab(tabLinkToActivate);} else if (loggedInSection && currentPath !== '/user/account' && currentPath !== '/user/messaging') {loggedInSection.classList.add('hidden'); // Hide if on other logged-in pages like /user/help}connectUserPresenceWebSocket();} else { // Not logged inif (userPresenceSocket && userPresenceSocket.readyState === WebSocket.OPEN) {userPresenceSocket.close();userPresenceSocket = null;}if (sidebarEl) sidebarEl.classList.add('hidden');if (appWrapper) appWrapper.classList.remove('logged-in-layout');if (appWrapper) appWrapper.classList.add('logged-out-layout');if (mainContentContainerEl && window.location.pathname !== '/user/messaging') mainContentContainerEl.classList.remove('messaging-active');if (currentPath === '/user/account' || currentPath === '/user/messaging' || currentPath === '/user/help') {const returnTo = encodeURIComponent(currentPath + window.location.hash + window.location.search);window.location.pathname = `/user/login?return_to=${returnTo}`;return;}if(loggedInSection) loggedInSection.classList.add('hidden');if(authSection) authSection.classList.remove('hidden');const login2FASection = document.getElementById('login-2fa-section');const loginFormContainer = document.getElementById('login-form');const registerFormContainer = document.getElementById('register-form');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) { }await checkLoginStatus();
}
async function checkLoginStatus() {const { ok, status, data } = await apiCall('/api/me');displayCorrectView(ok && data.email ? data : null);const urlParams = new URLSearchParams(window.location.search);if (urlParams.has('registered') && (window.location.pathname === '/user/login' || window.location.pathname === '/')) {showMessage('注册成功!请使用您的邮箱或用户名登录。', 'success');const newUrl = new URL(window.location);newUrl.searchParams.delete('registered');window.history.replaceState({}, document.title, newUrl.toString());}if (urlParams.has('return_to') && window.location.pathname === '/user/login' && ok && data.email) {const returnToPath = decodeURIComponent(urlParams.get('return_to'));const newUrl = new URL(window.location);newUrl.searchParams.delete('return_to');window.history.replaceState({}, document.title, newUrl.pathname + newUrl.search); // clean up URL firstwindow.location.href = returnToPath; // then redirect}
}
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) {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();activateTab(event.currentTarget);window.location.hash = event.currentTarget.id;});});if (topBarMessagingButton) {topBarMessagingButton.addEventListener('click', () => {if (window.location.pathname === '/user/messaging') {if (typeof window.loadMessagingTabData === 'function') {window.loadMessagingTabData(); // Reload data if already on page}} else {window.location.href = '/user/messaging';}});}if (sidebarToggleEl) {sidebarToggleEl.addEventListener('click', toggleSidebar);if (window.location.pathname === '/user/messaging') { // Hide toggle on dedicated messaging pagesidebarToggleEl.classList.add('hidden');}}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.classList.contains('hidden') && !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);if (hash && hash !== 'tab-messaging') { // Ignore messaging hash for account pageconst tabLinkToActivateByHash = document.getElementById(hash);if (tabLinkToActivateByHash && tabLinkToActivateByHash.classList.contains('sidebar-link')) {activateTab(tabLinkToActivateByHash);} else {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 {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 (type === 'login' || (type === 'login' && loginEmailFor2FA) || (data.twoFactorRequired === undefined)) {if (returnTo) {window.location.href = decodeURIComponent(returnTo);} else {window.location.href = '/user/account#tab-personal-info';}} else { // registerwindow.location.href = '/user/login?registered=true' + (returnTo ? '&return_to=' + encodeURIComponent(returnTo) : '');}}} 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;

4. cdn/js/ui-messaging.js

  • loadMessagingTabData 应该仍然是主要的入口点,但它现在是为 /user/messaging 页面加载时调用的。它可以直接在 DOMContentLoaded (或在 checkLoginStatus 成功后) 被触发,而不是通过 activateTab

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() {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() {initializeMessagingTab();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>';}if (window.currentUserData && window.currentUserData.email) {currentUserEmail = window.currentUserData.email;} else {const { ok, data } = await window.apiCall('/api/me');if (ok && data && data.email) {currentUserEmail = data.email;window.currentUserData = data;} else {if (conversationsListUl) conversationsListUl.innerHTML = '<p class="placeholder-text" style="color: var(--danger-color);">无法加载用户信息,请重新登录以查看私信。</p>';resetActiveConversationUIOnly();if(typeof window.showMessage === 'function') window.showMessage("用户未登录或会话已过期,请重新登录。", "error");setTimeout(() => { window.location.href = '/user/login?return_to=/user/messaging'; }, 2000);return;}}if (!currentUserEmail) {if (conversationsListUl) conversationsListUl.innerHTML = '<p class="placeholder-text" style="color: var(--danger-color);">用户信息最终未能确认,无法加载对话。</p>';resetActiveConversationUIOnly();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();} else {if (conversationsListUl) conversationsListUl.innerHTML = '<p class="placeholder-text" style="color: var(--danger-color);">消息服务连接功能不可用。</p>';return;}if (wasSocketAlreadyOpen) {if (window.userPresenceSocket && window.userPresenceSocket.readyState === WebSocket.OPEN) {window.userPresenceSocket.send(JSON.stringify({type: "REQUEST_INITIAL_STATE"}));}}
}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) {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]} 的对话已开始。` // This initial message might be optional});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 () => { // Give a moment for the conversations list to potentially update via UserPresenceDOconst newlyCreatedConv = allConversationsCache.find(c => c.other_participant_email === localReceiverEmail && c.conversation_id === data.conversationId);if (newlyCreatedConv) { // if list updated, click itawait handleConversationClick(data.conversationId, localReceiverEmail);} else { // if not found in cache, force open with provided dataawait 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) { // Only notify if not focused or not the active conversationshowDesktopNotification(`来自 ${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;// If this is the dedicated messaging page, initialize it after user check.
if (window.location.pathname === '/user/messaging') {document.addEventListener('DOMContentLoaded', () => {if (typeof window.checkLoginStatus === 'function') {window.checkLoginStatus().then(() => { // checkLoginStatus in main.js sets currentUserDataif (window.currentUserData && typeof loadMessagingTabData === 'function') {loadMessagingTabData();} else if (!window.currentUserData) { // If checkLoginStatus determined not logged in// main.js's displayCorrectView should handle redirect to login}});}});
}

总结与后续步骤

这些更改将私信功能迁移到一个独立的 /user/messaging 页面。当用户点击顶部导航栏的私信按钮时,会直接跳转到这个新页面,确保所有相关的 HTML 和 JavaScript 都被正确加载。这应该能解决从其他页面(如 /user/help)访问私信时遇到的加载问题。

您可以按照同样的模式,将账户设置中的其他部分(如API密钥管理、我的应用等)也拆分为独立的页面,以进一步模块化您的用户中心。

请注意,上述代码修改仅针对分离私信功能。main.js 中的 activateTab 函数以及相关的标签页逻辑仍然对 /user/account 页面内的其他标签页有效。如果未来您将所有标签页都改为独立页面,那么 activateTab 的大部分逻辑可以被移除,侧边栏链接也将变为直接的页面跳转。

相关文章:

  • TCP 三次握手,第二次握手报文丢失会发生什么?
  • FFmpeg 安装包全攻略:gpl、lgpl、shared、master 区别详解
  • macOs系统M1芯片执行source ~/.zshrc报错503
  • SurfaceFlinger及Android应用RenderThread角度观察Jank丢帧卡顿
  • 调度关键路径里调整优先级导致hardlockup
  • 5.23本日总结
  • AI编辑器规则
  • 非关系型数据库NoSQL
  • SpringBoot项目中Redis的使用
  • Linux PXE批量装机+无人值守技术(自动化装机)
  • 2025期中考复现
  • vue3样式穿透用法
  • 25年上半年五月之软考之设计模式
  • vue2中,codemirror编辑器的使用
  • C++:动态刷新打印内容
  • 《计算机组成原理》——第二章-6 总线定时:同步定时(同步通信)
  • PyTorch高阶技巧:构建非线性分类器与梯度优化全解析​
  • 工业RTOS生态重构:从PLC到“端 - 边 - 云”协同调度
  • 解决用input选择文件不能选择同一个文件
  • webpack学习笔记
  • 香港做网站公司/手机怎么制作网页
  • 旅游网站的网页设计素材/网络推广和网站推广
  • html怎么制作动态网站/今日新闻事件
  • 杭州企业seo网站优化/seo网页优化公司
  • wordpress分类页首页调用分类描述/seo优化及推广如何运营
  • 网站建设公司的出路/郑州网站优化公司