泛采专业版插件

泛采系统增强工具 - 提供主题切换、快捷键、自动刷新等功能,让您的工作更高效

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name        泛采专业版插件
// @name:en     FanCai Pro Plugin
// @description 泛采系统增强工具 - 提供主题切换、快捷键、自动刷新等功能,让您的工作更高效
// @description:en Enhanced toolset for FanCai System - Theme switching, keyboard shortcuts, auto-refresh and more
// @namespace   https://www.valuesimplex.com/
// @version     0.0.19
// @author      fangtiansheng <[email protected]>
// @icon        https://www.valuesimplex.com/images/favicon.ico
// @license     MIT
// @match       *://*/*
// @connect     *
// @require     https://cdn.jsdelivr.net/npm/[email protected]/dist/axios.min.js
// @require     https://cdn.jsdelivr.net/npm/[email protected]/dist/axiosGmxhrAdapter.min.js
// @grant       GM.xmlHttpRequest
// @grant       GM.registerMenuCommand
// @grant       GM.unregisterMenuCommand
// @run-at      document-end
// ==/UserScript==

/*
 * 泛采专业版插件
 * ==============
 *
 * 功能特性:
 * 1. 主题切换 - 支持明暗主题自动切换
 * 2. 快捷键支持:
 *    - ESC: 关闭弹窗
 *    - Ctrl+S: 保存
 *    - 自定义快捷键绑定
 * 3. 自动刷新功能
 * 4. 面板切换优化
 * 5. 验证器位置调整
 * 6. 任务列表限制优化
 * 7. 安全特性检查
 *
 * 安装说明:
 * 1. 确保已安装支持用户脚本的浏览器扩展(如Tampermonkey)
 * 2. 点击安装链接即可自动安装
 *
 * 使用说明:
 * - 脚本会自动在泛采系统页面启用
 * - 可通过浏览器扩展的设置面板配置脚本选项
 * - 问题反馈请访问GitHub仓库提交Issue
 *
 * 更新日志:
 * v0.0.18
 * - 新增主题自动切换功能
 * - 优化键盘快捷键支持
 * - 提升性能和稳定性
 */

// 配置项
const CONFIG = {
    REFRESH_INTERVAL: 1000,
    THEME_CHECK_INTERVAL: 1000,
    MENU_ITEMS: [
        {
            name: '财经转到泛采',
            fn: toCrawl,
            accessKey: 'f'
        }
    ]
};

// 工具函数
const utils = {
    async get(url, config = {}) {
        return axios.get(url, {
            adapter: axiosGmxhrAdapter,
            ...config
        });
    },

    async post(url, data, config = {}) {
        return axios.post(url, data, {
            adapter: axiosGmxhrAdapter,
            ...config
        });
    },

    isCrawlPage() {
        return window.location.href.endsWith('#/home/crawl');
    }
};

// 核心功能
class CrawlerTools {
    constructor() {
        this.init();
    }

    async init() {
        // 从UserScript元信息中获取版本号
        const version = GM.info.script.version;
        console.log(`%c 泛采系统专业版插件 %c v${version} %c`,
            "background:#5D5D5D; padding:1px; border-radius:3px 0 0 3px; color:#fff",
            "background:#0D7FBF; padding:1px; border-radius:0 3px 3px 0; color:#fff",
            "background:transparent"
        );

        if (window.location.hash.includes('/home/crawl')) {
            // 爬虫管理页面特定功能
            this.bindKeyboardShortcuts();
            this.initThemeSwitch();
            this.initPanelSwitch();
            this.initAutoRefresh();
            this.initValidatorPosition();
            this.initTaskListLimit();
            console.log('爬虫管理页面功能已初始化');
        } else {
            // 在其他页面执行的功能
            this.checkSecurityFeatures();
            this.checkMetaTags();
            console.log('安全特性检查已初始化');
        }
    }

    bindKeyboardShortcuts() {
    // ESC关闭弹窗
        window.addEventListener("keydown", (e) => {
            if (e.key === "Escape") {
                const popups = Array.from(document.getElementsByClassName("popup_hover")).reverse();
                for (const popup of popups) {
                    if (popup.style.display === "") {
                        popup.querySelector(".popup_head_close_icon")?.click();
                        break;
                    }
                }
            }

            // Ctrl+S保存
            if (e.ctrlKey && e.key === "s") {
                e.preventDefault();
                const saveButton = document.querySelector(".pageDetail_toolBox_item, .planDetail_toolBox_item");
                const buttons = saveButton?.children;
                if (buttons?.length) {
                    buttons[buttons.length - 2].click();
                }
            }
        });
    }

    initThemeSwitch() {
        const head = document.querySelector(".head");
        if (!head) {
            console.log("未找到.head元素,等待DOM加载...");
            setTimeout(() => this.initThemeSwitch(), 1000);
            return;
        }

        // 检查是否已经存在主题切换按钮
        if (document.querySelector('[role="switch_theme_option"]')) {
            console.log("主题切换按钮已存在");
            return;
        }

        const switchHtml = `
            <label for="switch_theme_option" class="nav" style="color:#fff; position:absolute; right:400px;">亮色</label>
            <div class="el-form-item__content" style="margin-left:15px; margin-bottom:2px; position:absolute; right:350px;">
                <div role="switch_theme_option" aria-checked="true" class="el-switch is-checked">
                    <input type="checkbox" name="" true-value="true" class="el-switch__input">
                    <span class="el-switch__core" style="width:40px;"></span>
                </div>
            </div>
        `;

        const switchWrapper = document.createElement("div");
        switchWrapper.innerHTML = switchHtml;

        // 确保switchWrapper的内容被正确插入
        const fragment = document.createDocumentFragment();
        while (switchWrapper.firstChild) {
            fragment.appendChild(switchWrapper.firstChild);
        }
        head.appendChild(fragment);

        const switchElement = document.querySelector('[role="switch_theme_option"]');
        if (switchElement) {
            switchElement.addEventListener("click", this.toggleTheme.bind(this));
            console.log("主题切换按钮已添加并绑定事件");
        } else {
            console.log("未能找到主题切换按钮元素");
        }
    }

    toggleTheme(e) {
        const isDark = e.currentTarget.classList.contains("is-checked");
        e.currentTarget.classList.toggle("is-checked");

        const html = document.querySelector("html");
        const label = document.querySelector('label[for="switch_theme_option"]');

        if (isDark) {
            html.style = `mix-blend-mode:difference; filter:invert(80%) hue-rotate(0deg) brightness(110%);`;
            ['.cell', 'span', '.iconfont'].forEach(selector => {
                document.querySelectorAll(selector).forEach(el => el.style.color = '#111');
            });
            label.innerText = "暗色";
        } else {
            html.style = "";
            ['.cell', 'span', '.iconfont'].forEach(selector => {
                document.querySelectorAll(selector).forEach(el => el.style.color = '');
            });
            label.innerText = "亮色";
        }
    }

    initPanelSwitch() {
        if (!window.location.hash.includes('/home/crawl')) return;

        const checkAndInitSwitch = () => {
            const head = document.querySelector(".head");
            const button_switch = document.querySelector('div[role="switch_option"]');
            const label_switch = document.querySelector('label[for="switch_option"]');
            const main_box = document.querySelector(".pageDetail_mainBox");

            if (main_box && !button_switch && head) {
                const switch_option = document.createElement("div");
                head.insertBefore(switch_option, head.lastChild);
                switch_option.outerHTML = `
                    <label for="switch_option" class="nav" style="color: #fff;position: absolute;right: 300px;">显示</label>
                    <div class="el-form-item__content" style="margin-left: 15px; margin-bottom: 2px;position: absolute;right: 250px;">
                        <div role="switch_option" aria-checked="true" class="el-switch is-checked">
                            <input type="checkbox" name="" true-value="true" class="el-switch__input">
                            <span class="el-switch__core" style="width: 40px;"></span>
                        </div>
                    </div>
                `;

                const button_switch = document.querySelector('div[role="switch_option"]');
                if (button_switch) {
                    button_switch.addEventListener("click", this.handlePanelSwitch.bind(this));
                }
            }

            if (!main_box && button_switch) {
                button_switch.remove();
                label_switch?.remove();
            }
        };

        // 初始检查并每秒更新
        checkAndInitSwitch();
        setInterval(checkAndInitSwitch, 1000);
    }

    handlePanelSwitch(e) {
        const main_box = document.querySelector(".pageDetail_mainBox");
        const label = document.querySelector('label[for="switch_option"]');
        if (!main_box || !label) return;

        const isChecked = e.currentTarget.classList.contains("is-checked");
        e.currentTarget.classList.toggle("is-checked");

        const display = isChecked ? "none" : "block";
        const text = isChecked ? "隐藏" : "显示";

        // 更新所有子元素的显示状态
        Array.from(main_box.children)
            .slice(0, -2)
            .forEach(child => child.style.display = display);

        label.innerText = text;
    }

    async checkSecurityFeatures() {
        // 检查 Cloudflare
        try {
            const res = await fetch(location.origin + "/cdn-cgi/trace", {
                method: "HEAD",
                mode: "cors",
                credentials: "include"
            });
            if (res.ok) {
                console.log("网站使用了Cloudflare CDN服务");
            }
        } catch (e) {}

        // 检查加速乐
        if (document.cookie.includes('__jsl_clearance_s')) {
            console.log('网站启用了加速乐');
        }

        // 检查robots.txt
        try {
            const robotsRes = await utils.get("/robots.txt");
            if (robotsRes.status === 200) {
                console.log(`发现网站有robots.txt文件: ${robotsRes.request.responseURL}`);
                document.title = "" + document.title;
            }
        } catch (e) {}

        // 检查sitemap.xml
        try {
            const sitemapRes = await utils.get("/sitemap.xml");
            if (sitemapRes.status === 200) {
                console.log(`发现网站有sitemap.xml文件: ${sitemapRes.request.responseURL}`);
                document.title = "" + document.title;
            }
        } catch (e) {}
    }

    initAutoRefresh() {
        setInterval(async () => {
            try {
                const res = await utils.get("/crawl/crawl/get-user-list");
                if (res.status === 200) {
                    document.querySelectorAll(".head").forEach(head => {
                        head.style.backgroundColor = "";
                    });
                    document.title = "爬虫管理系统";
                }

                // 更新描述时间戳
                const descTextarea = document.querySelector('textarea');
                if (descTextarea) {
                    const timestamp = new Date().toLocaleString('sv-SE').replaceAll("/", "-") + '\n';
                    const timePattern = /\d+\-\d+\-\d+\s\d+:\d+:\d+\n/;
                    descTextarea.value = timePattern.test(descTextarea.value)
                        ? descTextarea.value.replace(timePattern, timestamp)
                        : timestamp + descTextarea.value;
                }
            } catch (e) {
                console.error("自动刷新失败:", e);
            }
        }, CONFIG.REFRESH_INTERVAL * 60);
    }

    initValidatorPosition() {
        setInterval(() => {
            const validatorDiv = document.querySelector('.crawl_validatorTable');
            if (validatorDiv && validatorDiv.parentNode.firstChild !== validatorDiv) {
                validatorDiv.parentNode.insertBefore(validatorDiv, validatorDiv.parentNode.firstChild);
            }
        }, CONFIG.REFRESH_INTERVAL);
    }

    // 修改任务列表数量限制
    initTaskListLimit() {
        // 保存原始的XMLHttpRequest.open方法
        const originalOpen = XMLHttpRequest.prototype.open;

        // 重写XMLHttpRequest.open方法
        XMLHttpRequest.prototype.open = function(method, url, async, user, password) {
            // 检查URL是否匹配特定模式
            if (url.includes("/crawl/crawl/plan-task-select/") && url.includes("limit=10")) {
                // 将limit参数从10修改为100
                url = url.replace("limit=10", "limit=100");
            }

            // 使用修改后的URL调用原始的open方法
            originalOpen.call(this, method, url, async, user, password);
        };
    }

    // 检测页面meta标签
    checkMetaTags() {
        const title = window.document.title;
        const metaTags = {
            "ArticleTitle": './/*[@name="ArticleTitle"]/@content',
            "PubDate": './/*[translate(@name, "PUBDATE", "pubdate")="pubdate" and contains(@content, "20")]/@content',
            "ColumnName": './/*[translate(@name, "COLUMNNAME", "columnname")="columnname"]/@content',
            "ContentSource": './/*[translate(@name, "CONTENTSOURCE", "contentsource")="contentsource"]/@content',
            "Author": './/*[translate(@name, "AUTHOR", "author")="author"]/@content',
            "Keywords": './/*[translate(@name, "KEYWORDS", "keywords")="keywords"]/@content'
        };

        // 检查常规meta标签
        for (const [tag, xpath] of Object.entries(metaTags)) {
            const elements = document.getElementsByName(tag) || document.getElementsByName(tag.toLowerCase());
            if (elements.length || (tag === "PubDate" && document.getElementsByName("pubdate").length)) {
                console.log(xpath);
                if (tag === "ArticleTitle") {
                    window.document.title = "❤️" + window.document.title;
                    this.startTitleAnimation("❤️", "🔔", title, 600);
                }
            }
        }

        // 检查WordPress日期meta标签
        const metaPubDates = document.querySelectorAll('meta[content^="202"]');
        if (metaPubDates.length > 0) {
            metaPubDates.forEach((meta, index) => {
                console.log(`带有时间格式的meta标签${index + 1}:`, meta);

                if (meta.property?.startsWith("article:")) {
                    window.document.title = "💚" + window.document.title;
                    this.startTitleAnimation("💚", "🔔", title, 600);
                    console.log('.//*[contains(@property, "article:published")]/@content');
                }
            });
        }
    }

    // 标题动画辅助函数
    startTitleAnimation(emoji1, emoji2, baseTitle, interval) {
        let isFirstEmoji = true;
        const updateTitle = () => {
            window.document.title = isFirstEmoji ? emoji1 + baseTitle : emoji2 + baseTitle;
            isFirstEmoji = !isFirstEmoji;
        };

        updateTitle(); // 立即执行一次
        return setInterval(updateTitle, interval);
    }
}

// 初始化
async function toCrawl() {
    window.location = window.location.origin + "/#/home/crawl";
}

// 启动脚本
(async function main() {
    try {
        new CrawlerTools();
        CONFIG.MENU_ITEMS.forEach(item => {
            GM.registerMenuCommand(item.name, item.fn, item.accessKey);
        });
    } catch (e) {
        console.error("脚本初始化失败:", e);
    }
})();