Gemini 2.5 Pro 一次测试

发布于:2025-05-27 ⋅ 阅读:(186) ⋅ 点赞:(0)

您好!您遇到的问题——在非账户设置页面(例如 /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_read
                     FROM messages m
                     JOIN users u_sender ON m.sender_email = u_sender.email
                     WHERE 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(
                 `SELECT
                    c.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_sender
                  FROM conversations c
                  LEFT JOIN users u1 ON c.participant1_email = u1.email
                  LEFT JOIN users u2 ON c.participant2_email = u2.email
                  WHERE c.participant1_email = ?1 OR c.participant2_email = ?1
                  ORDER 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 authenticated
                    const loginUrl = new URL(`${OAUTH_ISSUER_URL(env, request)}/user/login`);
                    loginUrl.searchParams.set('return_to', path); // Return to messaging page after login
                    return Response.redirect(loginUrl.toString(), 302);
                }
                return new Response(generateMessagingPageHtml(env), { headers: { 'Content-Type': 'text/html;charset=UTF-8' } });
            }
            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, "&")
                 .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.js
            const 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 split
                    const 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 pages
             if (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 in
        if (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 first
        window.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 page
            sidebarToggleEl.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 page
                const 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 { // register
                 window.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 UserPresenceDO
                const newlyCreatedConv = allConversationsCache.find(c => c.other_participant_email === localReceiverEmail && c.conversation_id === data.conversationId);
                if (newlyCreatedConv) { // if list updated, click it
                    await handleConversationClick(data.conversationId, localReceiverEmail);
                } else { // if not found in cache, force open with provided data
                    await handleConversationClick(data.conversationId, localReceiverEmail);
                }
            }, 500);

            if (typeof window.showMessage === 'function') window.showMessage(`${window.escapeHtml(localReceiverEmail)} 的对话已开始。`, 'success');
        } else if (data.error === '接收者用户不存在' || status === 404) {
            if (typeof window.showMessage === 'function') window.showMessage(`无法开始对话:用户 ${window.escapeHtml(localReceiverEmail)} 不存在。`, 'error');
        }
        else {
            if (typeof window.showMessage === 'function') window.showMessage(`无法与 ${window.escapeHtml(localReceiverEmail)} 开始对话: ${ (data && (data.error || data.message)) ? window.escapeHtml(data.error || data.message) : '未知错误, 状态: ' + status}`, 'error');
        }
    }
}

window.handleConversationsListUpdate = function(conversationsData) {
    if (!currentUserEmail && window.currentUserData && window.currentUserData.email) {
        currentUserEmail = window.currentUserData.email;
    }
    const oldConversationsSummary = { ...allConversationsCache.reduce((acc, conv) => { acc[conv.conversation_id] = conv; return acc; }, {}) };

    allConversationsCache = conversationsData.map(conv => ({
        conversation_id: conv.conversation_id,
        other_participant_username: conv.other_participant_username,
        other_participant_email: conv.other_participant_email,
        last_message_content: conv.last_message_content,
        last_message_sender: conv.last_message_sender,
        last_message_at: conv.last_message_at,
        unread_count: conv.unread_count,
    }));

    displayConversations(allConversationsCache);

    allConversationsCache.forEach(newConv => {
        const oldConv = oldConversationsSummary[newConv.conversation_id];
        if (newConv.last_message_sender && newConv.last_message_sender !== currentUserEmail && newConv.unread_count > 0) {
            if (!oldConv || newConv.last_message_at > (oldConv.last_message_at || 0)) {
                 const isCurrentConversationActive = newConv.conversation_id === currentActiveConversationD1Id;
                if (!document.hasFocus() || !isCurrentConversationActive) { // Only notify if not focused or not the active conversation
                    showDesktopNotification(
                        `来自 ${window.escapeHtml(newConv.other_participant_username || newConv.other_participant_email)} 的新消息`,
                        {
                            body: window.escapeHtml(newConv.last_message_content.substring(0, 50) + (newConv.last_message_content.length > 50 ? "..." : "")),
                            icon: '/favicon.ico',
                            tag: `conversation-${newConv.conversation_id}`
                        },
                        newConv.conversation_id
                    );
                }
            }
        }
    });


    if (currentActiveConversationD1Id) {
        const activeConvStillExists = allConversationsCache.some(c => c.conversation_id === currentActiveConversationD1Id);
        if (!activeConvStillExists) {
            resetActiveConversationUIOnly();
        } else {
            const selectedLi = conversationsListUl?.querySelector(`li[data-conversation-id="${currentActiveConversationD1Id}"]`);
            if (selectedLi) selectedLi.classList.add('selected');
        }
    }
};

window.handleSingleConversationUpdate = function(updatedConvData) {
    if (!currentUserEmail && window.currentUserData && window.currentUserData.email) {
        currentUserEmail = window.currentUserData.email;
    }

    const index = allConversationsCache.findIndex(c => c.conversation_id === updatedConvData.conversation_id);
    const oldConvData = index > -1 ? { ...allConversationsCache[index] } : null;

    const mappedData = {
        conversation_id: updatedConvData.conversation_id,
        other_participant_username: updatedConvData.other_participant_username,
        other_participant_email: updatedConvData.other_participant_email,
        last_message_content: updatedConvData.last_message_content,
        last_message_sender: updatedConvData.last_message_sender,
        last_message_at: updatedConvData.last_message_at,
        unread_count: updatedConvData.unread_count,
    };

    if (index > -1) {
        allConversationsCache[index] = { ...allConversationsCache[index], ...mappedData };
    } else {
        allConversationsCache.unshift(mappedData);
    }

    displayConversations(allConversationsCache);

    if (mappedData.last_message_sender && mappedData.last_message_sender !== currentUserEmail && mappedData.unread_count > 0) {
        if (!oldConvData || mappedData.last_message_at > (oldConvData.last_message_at || 0)) {
            const isCurrentConversationActive = mappedData.conversation_id === currentActiveConversationD1Id;
             if (!document.hasFocus() || !isCurrentConversationActive) {
                showDesktopNotification(
                    `来自 ${window.escapeHtml(mappedData.other_participant_username || mappedData.other_participant_email)} 的新消息`,
                    {
                        body: window.escapeHtml(mappedData.last_message_content.substring(0, 50) + (mappedData.last_message_content.length > 50 ? "..." : "")),
                        icon: '/favicon.ico',
                        tag: `conversation-${mappedData.conversation_id}`
                    },
                    mappedData.conversation_id
                );
            }
        }
    }

    if (currentActiveConversationD1Id === updatedConvData.conversation_id) {
        const selectedLi = conversationsListUl?.querySelector(`li[data-conversation-id="${currentActiveConversationD1Id}"]`);
        if (selectedLi) selectedLi.classList.add('selected');
    }
};


window.initializeMessagingTab = initializeMessagingTab;
window.loadMessagingTabData = loadMessagingTabData;

// 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 currentUserData
                if (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 的大部分逻辑可以被移除,侧边栏链接也将变为直接的页面跳转。


微信公众号

今日签到

点亮在社区的每一天
去签到