维基连线 League of Bangumi Wiki

关联 番組 WIKI 計画 有史以来所有讨论与其对应条目,并提供全站共用备忘录。

// ==UserScript==
// @name         维基连线 League of Bangumi Wiki
// @description         关联 番組 WIKI 計画 有史以来所有讨论与其对应条目,并提供全站共用备忘录。
// @match        *://bgm.tv/subject/*/edit
// @match        *://bangumi.tv/subject/*/edit
// @match        *://chii.in/subject/*/edit
// @match        *://bgm.tv/person/*/edit
// @match        *://bangumi.tv/person/*/edit
// @match        *://chii.in/person/*/edit
// @match        *://bgm.tv/character/*/edit
// @match        *://bangumi.tv/character/*/edit
// @match        *://chii.in/character/*/edit
// @match        *://bgm.tv/wiki*
// @match        *://bangumi.tv/wiki*
// @match        *://chii.in/wiki*
// @grant        none
// @version 0.0.1.20250926165142
// @namespace https://greasyfork.org/users/1389779
// ==/UserScript==

(function () {
    'use strict';

    const MEMO_TOPIC_ID = '436973';
    const API_BASE_URL = 'https://bgmwiki.ry.mk/api';

    // --- 1. 定义样式 ---
    const styles = `
        /* General Panel Styles */
        #wiki-discussion-panel { margin-top: 15px; height: 500px; display: flex; flex-direction: column; }
        .discussion-tabs { display: flex; border-bottom: 1px solid #E0E0E0; margin: 0 -10px 10px -10px; padding: 0 10px; flex-shrink: 0; }
        .discussion-tabs .tab-item { padding: 8px 15px; cursor: pointer; color: #666; border-bottom: 2px solid transparent; transition: color .2s, border-color .2s; margin-bottom: -1px; display: flex; align-items: center; gap: 6px; }
        .discussion-tabs .tab-item:hover { color: #000; }
        .discussion-tabs .tab-item.active { color: var(--primary-color); border-bottom-color: var(--primary-color); }
        .tab-count { font-size: 11px; font-weight: 600; color: #888; background-color: #0000000d; padding: 1px 6px; border-radius: 8px; transition: color .2s, background-color .2s; }
        .discussion-tabs .tab-item.active .tab-count { color: var(--primary-color); background-color: var(--primary-color-l10); }
        .discussion-content { display: none; flex-grow: 1; min-height: 0; flex-direction: column;}
        .discussion-content.active { display: flex; }
        .history-list { flex-grow: 1; overflow-y: auto; padding: 5px 5px 5px 0; margin-right: -5px; display: flex; flex-direction: column; gap: 12px; overscroll-behavior: contain; }
        .loading-placeholder, .empty-placeholder { text-align: center; padding: 20px; margin: auto; color: #999; }
        /* Memo/Discussion Entry Styles */
        .memo-entry { display: flex; gap: 8px; align-items: flex-start; }
        .memo-entry.is-current-user { flex-direction: row-reverse; }
        .memo-entry .user-avatar { width: 32px; height: 32px; border-radius: 50%; flex-shrink: 0; cursor: pointer; }
        .memo-entry .post-content { max-width: 85%; display: flex; flex-direction: column; position: relative; }
        .memo-entry.is-current-user .post-content { align-items: flex-end; }
        .memo-entry .nickname { font-size: 12px; margin-bottom: 4px; color: #666; }
        .memo-entry .nickname a { color: #000; text-decoration: none; font-weight: 700; }
        .memo-entry .post-bubble { padding: 10px 12px; border-radius: 4px 15px 15px 15px; word-wrap: break-word; overflow-wrap: anywhere; font-size: 14px; line-height: 1.5; white-space: pre-wrap; background-color: #f8f8f8; color: #000; }
        .memo-entry.is-current-user .post-bubble { border-radius: 15px 4px 15px 15px; background: linear-gradient(rgba(255,255,255,.85),rgba(255,255,255,.85)),var(--primary-color); color: #111; }
        .memo-entry .timestamp { font-size: 11px; margin-top: 4px; padding: 0 5px; color: #999; }
        .post-bubble a.is-current-subject-link { color: var(--primary-color); }
        /* Input Area Styles */
        .input-area { padding-top: 10px; border-top: 1px solid #E0E0E0; flex-shrink: 0; }
        .input-area textarea { width: 100%; border: 1px solid #ccc; background: #fcfcfc; color: #000; padding: 10px; resize: none; box-sizing: border-box; font-family: inherit; line-height: 1.5; min-height: 80px; max-height: 150px; border-radius: 5px; }
        .memo-options { display: flex; justify-content: space-between; align-items: center; margin-top: 8px; gap: 10px; }
        .memo-options label { display: flex; align-items: center; gap: 5px; font-size: 12px; cursor: pointer; }
        .memo-options input[type="date"] { padding: 4px 6px; border: 1px solid #ccc; border-radius: 4px; font-size: 12px; background: #fcfcfc; color: #333; }
        .memo-options input[type="date"].hidden { display: none; }
        .input-area button { margin-top: 10px; width: 100%; padding: 8px; color: white; border: none; border-radius: 50px; cursor: pointer; font-size: 14px; font-weight: bold; transition: filter .2s; background-color: var(--primary-color); }
        .input-area button:hover { filter: brightness(1.1); }
        .input-area button:disabled { background-color: #ccc; cursor: not-allowed; }
        /* Todo Item Styles (Optimized) */
        .todo-item { display: flex; align-items: center; gap: 10px; padding: 12px; border-radius: 8px; background-color: #f9f9f9; border: 1px solid #eee; transition: background-color 0.2s, opacity 0.3s; margin-bottom: 8px; }
        .todo-item:hover { background-color: #f0f0f0; }
        .todo-item.is-completed { opacity: 0.6; background-color: #fafafa; }
        .todo-item.is-completed .todo-content-text { text-decoration: line-through; color: #888; }
        .todo-item input[type="checkbox"] { width: 16px; height: 16px; flex-shrink: 0; accent-color: var(--primary-color); cursor: pointer; margin: 0; }
        .todo-details { flex-grow: 1; }
        .todo-content-text { font-size: 14px; color: #333; }
        .todo-meta { font-size: 11px; color: #888; margin-top: 4px; }
        .todo-meta .due-date { font-weight: bold; color: var(--primary-color); }
        /* Wiki Connect Page Styles */
        #wiki-connect-container { padding: 0 15px; }
        .wiki-connect-feed .user-avatar { max-height: 50px; }
        .wiki-connect-feed .feed-item { padding: 12px 0; border-bottom: 1px solid #E0E0E0; display: flex; gap: 12px; }
        .wiki-connect-feed .feed-item:last-child { border-bottom: none; }
        .wiki-connect-feed .avatar-col { flex-shrink: 0; }
        .wiki-connect-feed .content-col { flex-grow: 1; }
        .wiki-connect-feed .feed-header { font-size: 13px; color: #666; margin-bottom: 6px; }
        .wiki-connect-feed .feed-header a { font-weight: bold; color: #000; text-decoration: none; }
        .wiki-connect-feed .feed-header a:hover { text-decoration: underline; }
        .wiki-connect-feed .feed-content { font-size: 14px; }
        .wiki-connect-feed .feed-footer { font-size: 12px; color: #999; margin-top: 8px; }

        /* Dark Theme Adjustments (Official Style) */
        html[data-theme=dark] .discussion-tabs { border-bottom-color: #444; }
        html[data-theme=dark] .discussion-tabs .tab-item { color: #aaa; }
        html[data-theme=dark] .discussion-tabs .tab-item:hover, html[data-theme=dark] .memo-entry .nickname a:hover { color: #fff; }
        html[data-theme=dark] .tab-count { color: #ccc; background-color: rgba(255,255,255,0.1); }
        html[data-theme=dark] .discussion-tabs .tab-item.active .tab-count { background-color: var(--primary-color-d10); }
        html[data-theme=dark] .memo-entry .nickname { color: #888; }
        html[data-theme=dark] .memo-entry .nickname a { color: #e0e0e1; }
        html[data-theme=dark] .memo-entry .post-bubble { background-color: #38393a; color: #e0e0e1; }
        html[data-theme=dark] .memo-entry.is-current-user .post-bubble { color: #fff; background: var(--primary-color); }
        html[data-theme=dark] .input-area { border-top-color: #444; }
        html[data-theme=dark] .input-area textarea, html[data-theme=dark] .memo-options input[type="date"] { background: #303132; color: #e0e0e1; border-color: #555; }
        html[data-theme=dark] .memo-options label { color: #ccc; }
        html[data-theme=dark] .todo-item { background-color: rgba(255, 255, 255, 0.05); border-color: #444; }
        html[data-theme=dark] .todo-item:hover { background-color: rgba(255, 255, 255, 0.1); }
        html[data-theme=dark] .todo-item.is-completed { background-color: transparent; opacity: 0.5; }
        html[data-theme=dark] .todo-content-text { color: #ccc; }
        html[data-theme=dark] .todo-item.is-completed .todo-content-text { color: #777; }
        html[data-theme=dark] .todo-meta { color: #888; }
        html[data-theme=dark] .todo-meta .due-date { color: var(--primary-color); }
        html[data-theme=dark] .wiki-connect-feed .feed-item { border-bottom-color: #444; }
        html[data-theme=dark] .wiki-connect-feed .feed-header { color: #aaa; }
        html[data-theme=dark] .wiki-connect-feed .feed-header a { color: #fff; }
        html[data-theme=dark] .wiki-connect-feed .feed-content { color: #e0e0e1; }
    `;

    // --- 2. HTML 结构 ---
    const panelHTML = `
        <div id="wiki-discussion-panel" class="menu_inner clearit">
            <div class="discussion-tabs">
                <div class="tab-item" data-tab="memo">备忘录 <span class="tab-count" data-type="memos"></span><span class="tab-count" data-type="todos" style="display:none;"></span></div>
                <div class="tab-item active" data-tab="discussion">相关讨论 <span class="tab-count"></span></div>
            </div>
            <div id="memo-content" class="discussion-content">
                <div class="history-list"></div>
                <div class="input-area">
                    <textarea id="memo-textarea" placeholder="记录编辑要点、待办事项..."></textarea>
                    <div class="memo-options">
                        <label> <input type="checkbox" id="is-todo-checkbox"> <span>作为待办事项</span> </label>
                        <input type="date" id="todo-date-picker" class="hidden">
                    </div>
                    <button id="save-memo-btn" class="chiiBtn">发送</button>
                </div>
            </div>
            <div id="discussion-content" class="discussion-content active"> <div class="history-list"></div> </div>
        </div>`;

    // --- 3. 脚本主逻辑 ---
    const dataCache = new Map();

    function addStyles(css) {
        const styleSheet = document.createElement("style");
        styleSheet.type = "text/css";
        styleSheet.innerText = css;
        document.head.appendChild(styleSheet);
    }

    function getCurrentPageInfo() {
        const match = location.pathname.match(/\/(subject|person|character)\/(\d+)/);
        if (!match) return null;
        return { type: match[1], id: match[2] };
    }

    function formatTimestamp(timestamp) {
        const date = new Date(timestamp);
        const pad = (n) => n.toString().padStart(2, '0');
        return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
    }

    function showLoadingPlaceholder(container) {
        if (!container) return;
        container.innerHTML = `<div class="loading-placeholder">正在加载...</div>`;
    }

    // --- 渲染函数 ---
    function renderMemos(data, entityId) {
        const historyContainer = document.querySelector('#memo-content .history-list');
        if (!historyContainer) return;
        if (!data) { showLoadingPlaceholder(historyContainer); return; }
        if (data.length === 0) { historyContainer.innerHTML = '<div class="empty-placeholder">暂无备忘录或待办</div>'; return; }

        const currentUser = window.CHOBITS_USERNAME;
        historyContainer.innerHTML = data.map(item => item.type === 'todo' ? renderTodoItem(item) : renderMemoItem(item, currentUser)).join('');

        historyContainer.querySelectorAll('.todo-item input[type="checkbox"]').forEach(checkbox => {
            checkbox.addEventListener('change', (e) => updateTodoStatus(e.target.closest('.todo-item').dataset.replyId, e.target.checked));
        });
    }

    function renderMemoItem(memo, currentUser) {
        const creator = memo.creator;
        const isCurrentUser = currentUser && creator && creator.username === currentUser;
        const avatarUrl = creator?.avatar?.large || 'https://bgm.tv/img/no_avatar.gif';
        const userName = creator?.nickname || '未知用户';
        const postUrl = `${location.origin}/group/topic/${MEMO_TOPIC_ID}#post_${memo.replyId}`;
        return `<div class="memo-entry ${isCurrentUser ? 'is-current-user' : ''}">
                    <a href="/user/${creator.username}" target="_blank"><img src="${avatarUrl}" alt="${userName}" class="user-avatar"></a>
                    <div class="post-content">
                        <div class="nickname"><a href="/user/${creator.username}" target="_blank">${userName}</a></div>
                        <div class="post-bubble">${memo.content}</div>
                        <div class="timestamp"><a href="${postUrl}" target="_blank" style="color:inherit">${formatTimestamp(memo.createdAt)} (查看来源)</a></div>
                    </div>
                </div>`;
    }

    function renderTodoItem(todo) {
        const creator = todo.creator;
        const userName = creator?.nickname || '未知用户';
        const dueDate = todo.dueDate ? ` | 截止: <span class="due-date">${new Date(todo.dueDate).toLocaleDateString()}</span>` : '';
        const postUrl = `${location.origin}/group/topic/${MEMO_TOPIC_ID}#post_${todo.replyId}`;
        const sourceLink = `<a href="${postUrl}" target="_blank" style="color:inherit; text-decoration:none;">(来源)</a>`;
        return `<div class="todo-item ${todo.isCompleted ? 'is-completed' : ''}" data-reply-id="${todo.replyId}">
                    <input type="checkbox" ${todo.isCompleted ? 'checked' : ''} title="标记完成/未完成">
                    <div class="todo-details">
                        <div class="todo-content-text">${todo.content}</div>
                        <div class="todo-meta">由 ${userName} 创建于 ${formatTimestamp(todo.createdAt)} ${sourceLink}${dueDate}</div>
                    </div>
                </div>`;
    }

    function renderDiscussions(data, entityId) {
        const container = document.querySelector('#discussion-content .history-list');
        if (!container) return;
        if (!data) { showLoadingPlaceholder(container); return; }
        if (data.length === 0) { container.innerHTML = '<div class="empty-placeholder">暂无相关讨论</div>'; return; }
        const currentUser = window.CHOBITS_USERNAME;
        container.innerHTML = data.map(post => {
            const creator = post.creator;
            const isCurrentUser = currentUser && creator && creator.username === currentUser;
            const avatarUrl = creator?.avatar?.large || 'https://bgm.tv/img/no_avatar.gif';
            const userName = creator?.nickname || '未知用户';
            const postUrl = `${location.origin}/group/topic/${post.topicId}#post_${post.replyId}`;
            let postHTML = post.content.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
            const highlightClass = (url) => (url.match(/\/(subject|person|character)\/(\d+)/)?.[2] === entityId) ? 'is-current-subject-link' : '';
            postHTML = postHTML.replace(/\[b\]([\s\S]*?)\[\/b\]/gs, '<strong>$1</strong>').replace(/\[url=(.*?)\]([\s\S]*?)\[\/url\]/gs, (m, url, text) => `<a href="${url}" class="${highlightClass(url)}" target="_blank" rel="noopener noreferrer">${text}</a>`);
            postHTML = postHTML.replace(/\[url\](https?:\/\/[^\[]+)\[\/url\]/gs, (m, url) => `<a href="${url.trim()}" class="${highlightClass(url.trim())}" target="_blank" rel="noopener noreferrer">${url.trim()}</a>`);
            return `<div class="memo-entry ${isCurrentUser ? 'is-current-user' : ''}"><a href="/user/${creator.username}" target="_blank"><img src="${avatarUrl}" alt="${userName}" class="user-avatar"></a><div class="post-content"><div class="nickname"><a href="/user/${creator.username}" target="_blank">${userName}</a></div><div class="post-bubble">${postHTML}</div><div class="timestamp"><a href="${postUrl}" target="_blank" style="color:inherit">${formatTimestamp(post.createdAt)} (${post.topicTitle || '查看来源'})</a></div></div></div>`;
        }).join('');
    }

    // --- 数据获取与操作 ---
    async function fetchData(pageInfo, type) {
        const cacheKey = `${type}-${pageInfo.type}-${pageInfo.id}`;
        if (dataCache.has(cacheKey)) {
            if (type === 'memo') renderMemos(dataCache.get(cacheKey), pageInfo.id);
            else if (type === 'discussion') renderDiscussions(dataCache.get(cacheKey), pageInfo.id);
            return;
        }
        const endpoint = type === 'memo' ? 'memos' : 'discussions';
        const renderFunc = type === 'memo' ? renderMemos : renderDiscussions;
        renderFunc(null, pageInfo.id);
        try {
            const res = await fetch(`${API_BASE_URL}/${pageInfo.type}/${pageInfo.id}/${endpoint}?_=${Date.now()}`);
            if (!res.ok) throw new Error(`服务器错误: ${res.status}`);
            const data = await res.json();
            dataCache.set(cacheKey, data);
            renderFunc(data, pageInfo.id);
        } catch (error) {
            console.error(`获取 ${type} 数据失败:`, error);
            const container = document.querySelector(`#${type}-content .history-list`);
            if (container) container.innerHTML = `<div class="empty-placeholder">加载失败: ${error.message}</div>`;
        }
    }

    async function sendMemo(button, textarea, isTodoCheckbox, datePicker) {
        const content = textarea.value.trim();
        if (!content) return alert('内容不能为空!');
        const isTodo = isTodoCheckbox.checked;
        const dueDate = datePicker.value;
        if (isTodo && !dueDate) return alert('作为待办事项时,必须选择一个截止日期!');
        const pageInfo = getCurrentPageInfo();
        if (!pageInfo) return alert('无法识别当前页面。');
        button.disabled = true;
        button.textContent = '正在发送...';
        try {
            const formhash = document.querySelector('a[href*="/logout/"]')?.href.split('/logout/')[1];
            if (!formhash) throw new Error('无法找到 formhash,请确认您已登录。');
            const subjectTitle = document.querySelector('h1.nameSingle a').textContent.trim();
            const entityUrl = `${location.origin}/${pageInfo.type}/${pageInfo.id}`;
            const wrappedContent = isTodo ? `[todo][due=${dueDate}]${content}[/todo]` : content;
            const postContent = `[b]关联条目:[/b][url=${entityUrl}]${subjectTitle}[/url]\n[b]编辑者:[/b][user]${window.CHOBITS_USERNAME || ''}[/user]\n[quote]\n${wrappedContent}\n[/quote]`;
            const res = await fetch(`${location.origin}/group/topic/${MEMO_TOPIC_ID}/new_reply`, {
                method: 'POST', body: new URLSearchParams({ formhash, content: postContent, submit: '加上去' }), credentials: 'include'
            });
            if (!res.ok || !res.redirected) throw new Error(`发送失败: ${res.status}`);
            textarea.value = '';
            isTodoCheckbox.checked = false;
            datePicker.classList.add('hidden');
            button.textContent = '正在刷新...';
            await fetch(`${API_BASE_URL}/memos/refresh`, { method: 'POST' });
            await new Promise(resolve => setTimeout(resolve, 3000));
            dataCache.delete(`memo-${pageInfo.type}-${pageInfo.id}`);
            await Promise.all([fetchData(pageInfo, 'memo'), fetchAndRenderCounts(pageInfo)]);
            alert('发送成功!');
        } catch (error) {
            console.error('发送时出错:', error);
            alert(`发送失败: ${error.message}`);
        } finally {
            button.disabled = false;
            button.textContent = '发送';
        }
    }

    async function updateTodoStatus(replyId, isCompleted) {
        try {
            const res = await fetch(`${API_BASE_URL}/memos/${replyId}/toggle`, {
                method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ is_completed: isCompleted })
            });
            if (!res.ok) throw new Error('更新状态失败');

            const isWikiPage = location.pathname.startsWith('/wiki');
            const pageInfo = getCurrentPageInfo();

            if (isWikiPage) {
                dataCache.delete('wiki-connect-todos');
                await fetchAndRenderWikiConnectFeed('todos');
            } else if (pageInfo) {
                dataCache.delete(`memo-${pageInfo.type}-${pageInfo.id}`);
                await Promise.all([fetchData(pageInfo, 'memo'), fetchAndRenderCounts(pageInfo)]);
            }
        } catch (error) {
            console.error('更新待办状态失败:', error);
            alert('更新状态失败,请刷新重试。');
        }
    }
    async function fetchAndRenderCounts(pageInfo) {
        if (!pageInfo) return;
        try {
            const res = await fetch(`${API_BASE_URL}/${pageInfo.type}/${pageInfo.id}/counts?_=${Date.now()}`);
            if (!res.ok) return;
            const counts = await res.json();
            const memoTab = document.querySelector('.tab-item[data-tab="memo"] .tab-count[data-type="memos"]');
            const discussionTab = document.querySelector('.tab-item[data-tab="discussion"] .tab-count');
            const todoBadge = document.querySelector('.tab-count[data-type="todos"]');

            if (memoTab) memoTab.textContent = counts.memos > 0 ? counts.memos : '';
            if (discussionTab) discussionTab.textContent = counts.discussions > 0 ? counts.discussions : '';
            if (todoBadge) {
                todoBadge.textContent = counts.todos;
                todoBadge.style.display = counts.todos > 0 ? 'inline-block' : 'none';
            }
        } catch (error) {
            console.error('获取计数失败:', error);
        }
    }

    // --- 初始化函数 ---
    function initPanel() {
        const targetColumn = document.getElementById('columnInSubjectB') || document.getElementById('columnCrtB');
        if (!targetColumn) return;
        addStyles(styles);
        targetColumn.insertAdjacentHTML('afterbegin', panelHTML);
        const pageInfo = getCurrentPageInfo();
        if (!pageInfo) return;
        fetchAndRenderCounts(pageInfo);
        // Event Listeners
        const tabs = document.querySelectorAll('#wiki-discussion-panel .tab-item');
        const contents = document.querySelectorAll('#wiki-discussion-panel .discussion-content');
        tabs.forEach(tab => {
            tab.addEventListener('click', () => {
                tabs.forEach(t => t.classList.remove('active'));
                contents.forEach(c => c.classList.remove('active'));
                tab.classList.add('active');
                document.getElementById(tab.dataset.tab + '-content').classList.add('active');
                fetchData(pageInfo, tab.dataset.tab);
            });
        });
        const saveMemoBtn = document.getElementById('save-memo-btn'), memoTextarea = document.getElementById('memo-textarea');
        const isTodoCheckbox = document.getElementById('is-todo-checkbox'), todoDatePicker = document.getElementById('todo-date-picker');
        isTodoCheckbox.addEventListener('change', () => {
            todoDatePicker.classList.toggle('hidden', !isTodoCheckbox.checked);
            if (isTodoCheckbox.checked && !todoDatePicker.value) todoDatePicker.valueAsDate = new Date();
        });
        saveMemoBtn.addEventListener('click', () => sendMemo(saveMemoBtn, memoTextarea, isTodoCheckbox, todoDatePicker));
        fetchData(pageInfo, 'discussion');
    }

    function initWikiConnectPage() {
        addStyles(styles);
        const navTabs = document.querySelector('.navTabs');
        if (!navTabs) return;

        const navSubTabs = document.querySelector('.navSubTabsWrapper');
        const columnB = document.getElementById('columnB');
        const originalContent = document.querySelector('#columnA');

        const connectTab = document.createElement('li');
        connectTab.innerHTML = `<a href="#wiki-connect">维基连线</a>`;
        navTabs.appendChild(connectTab);

        const handleTabClick = (e) => {
            e.preventDefault();
            const target = e.currentTarget;

            navTabs.querySelectorAll('li a').forEach(a => a.classList.remove('focus'));
            target.classList.add('focus');

            if (target.href.endsWith('#wiki-connect')) {
                document.title = '维基连线 | Bangumi';
                history.pushState(null, '', '/wiki#connect');

                if (originalContent) originalContent.style.display = 'none';
                if (navSubTabs) navSubTabs.style.display = 'none';
                if (columnB) columnB.style.display = 'none';

                let container = document.getElementById('wiki-connect-container');
                if (!container) {
                    container = document.createElement('div');
                    container.id = 'wiki-connect-container';
                    container.style.width = '100%';
                    container.innerHTML = `
                        <h2 class="title">维基连线 <small class="grey">最新动态</small></h2>
                        <div class="discussion-tabs">
                            <div class="tab-item active" data-tab="connect-memos" data-type="memos">备忘录</div>
                            <div class="tab-item" data-tab="connect-todos" data-type="todos">待办事项</div>
                        </div>
                        <div id="connect-memos-content" class="discussion-content active">
                            <div class="wiki-connect-feed history-list"></div>
                        </div>
                        <div id="connect-todos-content" class="discussion-content">
                            <div class="wiki-connect-feed history-list"></div>
                        </div>`;
                    originalContent.parentNode.insertBefore(container, originalContent);

                    container.querySelectorAll('.discussion-tabs .tab-item').forEach(tab => {
                        tab.addEventListener('click', () => {
                            if (tab.classList.contains('active')) return;
                            container.querySelectorAll('.discussion-tabs .tab-item').forEach(t => t.classList.remove('active'));
                            container.querySelectorAll('.discussion-content').forEach(c => c.classList.remove('active'));
                            tab.classList.add('active');
                            document.getElementById(tab.dataset.tab + '-content').classList.add('active');
                            fetchAndRenderWikiConnectFeed(tab.dataset.type);
                        });
                    });
                    fetchAndRenderWikiConnectFeed('memos');
                }
                container.style.display = 'block';
            } else {
                if (location.hash === '#connect') {
                    location.href = target.href;
                }
            }
        };

        connectTab.querySelector('a').addEventListener('click', handleTabClick);

        if (location.hash === '#connect') {
            setTimeout(() => connectTab.querySelector('a').click(), 0);
        }
    }

    async function fetchAndRenderWikiConnectFeed(type = 'memos') {
        const container = document.querySelector(`#connect-${type}-content .wiki-connect-feed`);
        if (!container) return;

        const cacheKey = `wiki-connect-${type}`;
        if (dataCache.has(cacheKey)) {
            renderWikiConnectItems(dataCache.get(cacheKey), container);
            return;
        }

        showLoadingPlaceholder(container);
        try {
            const res = await fetch(`${API_BASE_URL}/memos/latest/${type}?_=${Date.now()}`);
            if (!res.ok) throw new Error(`服务器错误: ${res.status}`);
            const data = await res.json();
            dataCache.set(cacheKey, data);
            renderWikiConnectItems(data, container);
        } catch (error) {
            container.innerHTML = `<div class="empty-placeholder">加载失败: ${error.message}</div>`;
        }
    }

    function renderWikiConnectItems(data, container) {
        if (!container) return;
        if (data.length === 0) {
            container.innerHTML = '<div class="empty-placeholder">暂无内容</div>';
            return;
        }
        container.innerHTML = data.map(item => {
            const creator = item.creator;
            const avatarUrl = creator?.avatar?.large || 'https://bgm.tv/img/no_avatar.gif';
            const userName = creator?.nickname || '未知用户';
            const entityUrl = `/${item.entityType}/${item.entityId}`;
            const entityTitle = item.entityTitle || `${item.entityType} #${item.entityId}`;
            let contentHTML = '';
            if (item.type === 'todo') {
                const dueDate = item.dueDate ? ` (截止: ${new Date(item.dueDate).toLocaleDateString()})` : '';
                contentHTML = `<div class="todo-item ${item.isCompleted ? 'is-completed' : ''}" data-reply-id="${item.replyId}">
                                <input type="checkbox" ${item.isCompleted ? 'checked' : ''} title="标记完成/未完成">
                                <div class="todo-details">
                                    <div class="todo-content-text"><strong>[待办]</strong> ${item.content}${dueDate}</div>
                                </div>
                               </div>`;
            } else {
                contentHTML = `<div class="feed-content">${item.content}</div>`;
            }

            return `<div class="feed-item">
                        <div class="avatar-col">
                            <a href="/user/${creator.username}" target="_blank"><img src="${avatarUrl}" class="user-avatar" alt="${userName}"></a>
                        </div>
                        <div class="content-col">
                            <div class="feed-header">
                                <a href="/user/${creator.username}" target="_blank">${userName}</a>
                                发布于 <a href="${entityUrl}/edit" target="_blank">${entityTitle}</a>
                            </div>
                            ${contentHTML}
                            <div class="feed-footer">${formatTimestamp(item.createdAt)}</div>
                        </div>
                    </div>`;
        }).join('');

        container.querySelectorAll('.todo-item input[type="checkbox"]').forEach(checkbox => {
            checkbox.addEventListener('change', (e) => updateTodoStatus(e.target.closest('.todo-item').dataset.replyId, e.target.checked));
        });
    }


    // --- Main Execution ---
    function main() {
        if (location.pathname.match(/\/(subject|person|character)\/\d+\/edit/)) {
            initPanel();
        } else if (location.pathname.startsWith('/wiki')) {
            initWikiConnectPage();
        }
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', main);
    } else {
        main();
    }
})();