Zhihu MCN & Date Displayer

在知乎回答页面,查询并显示作者的MCN机构信息。

// ==UserScript==
// @name         Zhihu MCN & Date Displayer
// @namespace    http://tampermonkey.net/
// @version      1.2
// @description  在知乎回答页面,查询并显示作者的MCN机构信息。
// @author       Kiddo
// @match        *://www.zhihu.com/*
// @match        *://zhuanlan.zhihu.com/*
// @icon         https://static.zhihu.com/heifetz/favicon.ico
// @grant        GM_xmlhttpRequest
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    console.log('[MCN & Date Displayer] Script started.');

    const PROCESSED_MARKER = 'data-date-processed-v1'; // 使用新标记以防旧标记冲突
    const MCN_PROCESSED_MARKER = 'data-mcn-processed-v1'; // 使用新标记以防旧标记冲突

    /**
     * 将MCN信息添加到作者头部
     * @param {HTMLElement} authorDiv - AuthorInfo-detail 所在的 div
     * @param {string} mcnCompany - MCN公司名称
     * @param {string} urlToken - 用户urlToken
     */
    function displayMcnInfo(authorDiv, mcnCompany, urlToken) {
        // MCN信息依然添加到 AuthorInfo-head
        const headDiv = authorDiv.closest('.AuthorInfo').querySelector('.AuthorInfo-content .AuthorInfo-head');
        if (headDiv && !headDiv.hasAttribute('data-mcn-added')) { // 检查MCN是否已添加
            const mcnSpan = document.createElement('span');
            mcnSpan.className = 'mcn-info-span';
            mcnSpan.textContent = `MCN: ${mcnCompany}`;
            mcnSpan.style.marginLeft = '8px';
            mcnSpan.style.fontSize = '12px';
            mcnSpan.style.color = '#8590a6';
            mcnSpan.style.backgroundColor = '#f0f2f7';
            mcnSpan.style.padding = '1px 4px';
            mcnSpan.style.borderRadius = '3px';
            mcnSpan.style.whiteSpace = 'nowrap'; // 防止换行

            headDiv.appendChild(mcnSpan);
            headDiv.setAttribute('data-mcn-added', 'true'); // 标记MCN已添加
            console.log(`[MCN & Date Displayer] Added MCN info for ${urlToken}: ${mcnCompany}`);
        }
    }

    /**
     * 将创建和修改日期添加到 AuthorInfo-badgeText
     * @param {HTMLElement} contentItem - 对应的 ContentItem AnswerItem 元素
     */
    function displayDateInfo(contentItem) {
        const dateCreatedMeta = contentItem.querySelector('meta[itemprop="dateCreated"]');
        const dateModifiedMeta = contentItem.querySelector('meta[itemprop="dateModified"]');
        // const badgeTextDiv = contentItem.querySelector('.AuthorInfo-head');
        const badgeTextDiv = contentItem.querySelector('div[itemprop="zhihu:question"]');

        if (badgeTextDiv && !badgeTextDiv.hasAttribute('data-date-added')) { // 检查日期是否已添加
            let dateInfo = '';
            let createdDate = '';
            let modifiedDate = '';

            if (dateCreatedMeta && dateCreatedMeta.getAttribute('content')) {
                // 格式化日期:例如 "2020-04-02"
                createdDate = new Date(dateCreatedMeta.getAttribute('content')).toISOString().split('T')[0];
                dateInfo += `创建: ${createdDate}`;
            }

            if (dateModifiedMeta && dateModifiedMeta.getAttribute('content')) {
                modifiedDate = new Date(dateModifiedMeta.getAttribute('content')).toISOString().split('T')[0];
                if (createdDate !== modifiedDate) { // 只有创建和修改日期不同才显示修改日期
                    if (dateInfo) {
                        dateInfo += ' | ';
                    }
                    dateInfo += `修改: ${modifiedDate}`;
                }
            }

            if (dateInfo) {
                const dateSpan = document.createElement('span');
                dateSpan.className = 'date-info-span';
                // 插入到现有文本前或后,这里选择追加,可以调整
                dateSpan.textContent = ` (${dateInfo})`;
                dateSpan.style.fontSize = '12px';
                dateSpan.style.color = '#8590a6';
                dateSpan.style.whiteSpace = 'nowrap'; // 防止换行

                // 为了不影响原有的徽章文本显示,可以考虑加在徽章文本的后面
                badgeTextDiv.appendChild(dateSpan);
                badgeTextDiv.setAttribute('data-date-added', 'true'); // 标记日期已添加
                console.log(`[MCN & Date Displayer] Added date info for answer: ${createdDate} | ${modifiedDate}`);
            }
        }
    }

    /**
     * 处理页面上未处理的 ContentItem AnswerItem
     */
    function processContentItems() {
        console.log('[MCN & Date Displayer] processContentItems() called.');
        // 查找所有未处理Date的 ContentItem AnswerItem
        const contentItems = document.querySelectorAll(`.ContentItem.AnswerItem:not([${PROCESSED_MARKER}])`);

        if (contentItems.length > 0) {
            console.log(`[Date Displayer] Found ${contentItems.length} new ContentItem AnswerItem blocks to process.`);
            contentItems.forEach(contentItem => {
                // 提取日期信息并显示
                displayDateInfo(contentItem);
                contentItem.setAttribute(PROCESSED_MARKER, 'true'); // 标记为已处理
            });
        } else {
            console.log('[Date Displayer] No new ContentItem AnswerItem blocks found.');
        }
        // 查找所有未处理的MCN ContentItem AnswerItem
        const mcnContentItems = document.querySelectorAll(`.ContentItem.AnswerItem:not([${MCN_PROCESSED_MARKER}])`);

        if (mcnContentItems.length > 0) {
            console.log(`[MCN Displayer] Found ${mcnContentItems.length} new ContentItem AnswerItem blocks to process.`);
            mcnContentItems.forEach(contentItem => {
                // 提取MCN信息并显示
                const authorDiv = contentItem.querySelector('.AuthorInfo');
                if (authorDiv) {
                    fetchAndDisplayMcn(authorDiv);
                    contentItem.setAttribute(MCN_PROCESSED_MARKER, 'true'); // 标记为已处理
                } else {
                    console.warn('[MCN Displayer] Could not find .AuthorInfo in:', contentItem);
                }
            });
        } else {
            console.log('[MCN Displayer] No new ContentItem AnswerItem blocks found.');
        }
    }

    /**
     * 抓取并显示MCN信息 (原函数,略有调整以适应新的流程)
     * @param {HTMLElement} authorDiv - AuthorInfo AnswerItem-authorInfo 元素
     */
    function fetchAndDisplayMcn(authorDiv) {
        // MCN信息现在只在 displayMcnInfo 内部检查是否已添加,
        // 这里的 PROCESSED_MARKER 主要用于标记整个 contentItem
        // if (authorDiv.hasAttribute(PROCESSED_MARKER)) { // 这个标记现在移动到 contentItem 上
        //     return;
        // }
        // authorDiv.setAttribute(PROCESSED_MARKER, 'true'); // 这个标记现在移动到 contentItem 上

        const metaUrlElement = authorDiv.querySelector('meta[itemprop="url"]');
        if (!metaUrlElement) {
            console.warn('[MCN & Date Displayer] Could not find meta itemprop="url" in:', authorDiv);
            return;
        }

        let profileUrl = metaUrlElement.getAttribute('content');
        if (!profileUrl) {
            console.warn('[MCN & Date Displayer] Profile URL is empty in:', authorDiv);
            return;
        }

        if (profileUrl.startsWith('//')) {
            profileUrl = 'https:' + profileUrl;
        }

        const urlParts = profileUrl.split('/');
        const urlToken = urlParts[urlParts.length - 1].split('?')[0]; // 移除URL参数

        if (!urlToken) {
            console.warn('[MCN & Date Displayer] Could not extract urlToken from:', profileUrl);
            return;
        }

        console.log(`[MCN & Date Displayer] Processing author: ${urlToken}, URL: ${profileUrl}`);

        GM_xmlhttpRequest({
            method: "GET",
            url: profileUrl,
            onload: function(response) {
                if (response.status >= 200 && response.status < 300) {
                    try {
                        const parser = new DOMParser();
                        const doc = parser.parseFromString(response.responseText, "text/html");
                        const scriptElement = doc.querySelector('script#js-initialData[type="text/json"]');

                        if (scriptElement && scriptElement.textContent) {
                            const jsonData = JSON.parse(scriptElement.textContent);
                            const mcnCompany = jsonData?.initialState?.entities?.users?.[urlToken]?.mcnCompany;

                            if (mcnCompany) {
                                displayMcnInfo(authorDiv, mcnCompany, urlToken);
                            } else {
                                console.log(`[MCN & Date Displayer] No MCN info found for ${urlToken}.`);
                                displayMcnInfo(authorDiv, 'NoMCN', urlToken); // 明确显示无MCN
                            }
                        } else {
                            // console.warn(`[MCN & Date Displayer] Could not find js-initialData script for ${urlToken} in profile page.`);
                            // 有些用户可能没有这个script,是正常现象
                        }
                    } catch (e) {
                        console.error(`[MCN & Date Displayer] Error parsing JSON for ${urlToken}:`, e, response.responseText.substring(0, 500));
                    }
                } else {
                    console.error(`[MCN & Date Displayer] Error fetching profile for ${urlToken}. Status: ${response.status} ${response.statusText}`);
                }
            },
            onerror: function(response) {
                console.error(`[MCN & Date Displayer] GM_xmlhttpRequest error for ${urlToken}:`, response);
            }
        });
    }

    // 防抖函数
    function debounce(func, wait) {
        let timeout;
        return function executedFunction(...args) {
            const later = () => {
                clearTimeout(timeout);
                func(...args);
            };
            clearTimeout(timeout);
            timeout = setTimeout(later, wait);
        };
    }

    // 使用防抖包装 processContentItems
    const debouncedProcessContentItems = debounce(processContentItems, 500); // 500毫秒的延迟

    // 初始加载时处理一次
    setTimeout(processContentItems, 1000); // 确保页面元素基本加载完毕

    console.log('[MyScript] Setting up MutationObserver...');

    const observer = new MutationObserver((mutationsList) => {
        // 我们不需要详细分析mutation的内容。
        // 只要DOM有变化,就可能出现了我们需要处理的新内容。
        // processContentItems 函数自身的逻辑足够智能去处理。
        // 我们只检查是否有节点增删,以避免因属性变化等无关操作频繁触发。
        for (const mutation of mutationsList) {
            if (mutation.type === 'childList' && (mutation.addedNodes.length > 0 || mutation.removedNodes.length > 0)) {
                // DOM结构发生了变化,调用我们的防抖处理函数
                debouncedProcessContentItems();
                // 只要有一个相关的mutation,就可以触发检查,无需遍历所有
                return;
            }
        }
    });

    const config = { childList: true, subtree: true };
    observer.observe(document.body, config);

    console.log('[MCN & Date Displayer] MutationObserver is now observing document.body.');

})();