求职助手

为相关的求职平台(BOSS直聘、拉勾、智联招聘、猎聘)添加一些实用的小功能,如自定义薪资范围、过滤黑名单公司等。

目前为 2025-03-25 提交的版本。查看 最新版本

// ==UserScript==
// @name         求职助手
// @namespace    job_seeking_helper
// @author       Gloduck
// @license MIT
// @version      1.0
// @description  为相关的求职平台(BOSS直聘、拉勾、智联招聘、猎聘)添加一些实用的小功能,如自定义薪资范围、过滤黑名单公司等。
// @match        https://www.zhipin.com/*
// @grant        GM_registerMenuCommand
// @grant        GM_setValue
// @grant        GM_getValue
// ==/UserScript==

(function () {
    'use strict';

    // 策略枚举
    const JobPlatform = {
        BOSS_ZHIPIN: Symbol('Boss'), LAGOU: Symbol('Lagou'), UNKNOWN: Symbol('未知网站')
    };

    const JobPageType = {
        SEARCH: Symbol('Search'), RECOMMEND: Symbol('Recommend')
    }

    const JobFilterType = {
        BLACKLIST: Symbol('Blacklist'), VIEWED: Symbol('Viewed'), MISMATCH_CONDITION: Symbol('MismatchCondition'),
    }

    let curPageHash = null;
    // 默认设置
    const defaults = {
        blacklist: [],
        minSalary: 0,
        maxSalary: Infinity,
        maxDailyHours: 8,
        maxMonthlyDays: 22,
        maxWeeklyCount: 4,
        viewedAction: 'mark',
        blacklistAction: 'hide',
        conditionAction: 'hide'
    };

    // 加载用户设置
    const settings = {
        blacklist: GM_getValue('blacklist', defaults.blacklist),
        minSalary: GM_getValue('minSalary', defaults.minSalary),
        maxSalary: GM_getValue('maxSalary', defaults.maxSalary),
        maxDailyHours: GM_getValue('maxDailyHours', defaults.maxDailyHours),
        maxMonthlyDays: GM_getValue('maxMonthlyDays', defaults.maxMonthlyDays),
        maxWeeklyCount: GM_getValue('maxWeeklyCount', defaults.maxWeeklyCount),
        viewedAction: GM_getValue('viewedAction', defaults.viewedAction),
        blacklistAction: GM_getValue('blacklistAction', defaults.blacklistAction),
        conditionAction: GM_getValue('conditionAction', defaults.conditionAction)
    };

    // 注册(不可用)设置菜单
    GM_registerMenuCommand('职位过滤设置', showSettings);

    function showSettings() {
        const dialog = document.createElement('div');
        dialog.style = `
            position: fixed;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            background: #ffffff;
            padding: 25px;
            border-radius: 12px;
            box-shadow: 0 8px 24px rgba(0,0,0,0.15);
            z-index: 9999;
            min-width: 380px;
            font-family: 'Segoe UI', sans-serif;
        `;

        dialog.innerHTML = `
            <h2 style="color: #2c3e50; margin: 0 0 20px; font-size: 1.4em;">职位过滤设置</h2>
            
            <!-- 基础设置 -->
            <div style="margin-bottom: 20px;">
                <label style="display: block; margin-bottom: 8px; font-weight: 500;">黑名单关键词(逗号分隔)</label>
                <input type="text" id="blacklist" 
                    style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px;"
                    value="${settings.blacklist.join(',')}">
            </div>

            <!-- 薪资设置 -->
            <div style="margin-bottom: 20px;">
                <label style="display: block; margin-bottom: 8px; font-weight: 500;">薪资范围(元/月)</label>
                <div style="display: flex; gap: 10px;">
                    <input type="number" id="minSalary" placeholder="最低" 
                        style="flex: 1; padding: 8px;" value="${settings.minSalary}">
                    <span style="align-self: center;">-</span>
                    <input type="number" id="maxSalary" placeholder="最高" 
                        style="flex: 1; padding: 8px;" value="${settings.maxSalary}">
                </div>
            </div>
            
                <div style="margin-bottom: 20px; padding: 15px; background: #f8f9fa; border-radius: 8px;">
                <h3 style="color: #2c3e50; margin: 0 0 15px; font-size: 1.1em;">工作时间限制</h3>
                
                <div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 15px;">
                    <div>
                        <label style="display: block; margin-bottom: 6px;">日工作小时</label>
                        <input type="number" id="maxDailyHours" 
                            min="1" max="24" step="0.5"
                            style="width: 100%; padding: 8px;"
                            value="${settings.maxDailyHours}">
                    </div>
                    
                    <div>
                        <label style="display: block; margin-bottom: 6px;">月工作天数</label>
                        <input type="number" id="maxMonthlyDays" 
                            min="1" max="31" step="1"
                            style="width: 100%; padding: 8px;"
                            value="${settings.maxMonthlyDays}">
                    </div>
                    
                    <div>
                        <label style="display: block; margin-bottom: 6px;">月工作周数</label>
                        <input type="number" id="maxWeeklyCount" 
                            min="1" max="6" step="0.5"
                            style="width: 100%; padding: 8px;"
                            value="${settings.maxWeeklyCount}">
                    </div>
                </div>
            </div>

            <!-- 整合后的处理方式设置 -->
            <div style="margin-bottom: 20px; padding: 15px; background: #f8f9fa; border-radius: 8px;">
                <h3 style="color: #2c3e50; margin: 0 0 15px; font-size: 1.1em;">处理方式设置</h3>
                
                <div style="margin-bottom: 12px;">
                    <label style="display: block; margin-bottom: 6px;">黑名单职位:</label>
                    ${createRadioGroup('blacklistAction', ['delete', 'hide', 'mark'], settings.blacklistAction)}
                </div>

                <div style="margin-bottom: 12px;">
                    <label style="display: block; margin-bottom: 6px;">已查看职位:</label>
                    ${createRadioGroup('viewedAction', ['delete', 'hide', 'mark'], settings.viewedAction)}
                </div>

                <div>
                    <label style="display: block; margin-bottom: 6px;">不符合条件职位:</label>
                    ${createRadioGroup('conditionAction', ['delete', 'hide', 'mark'], settings.conditionAction)}
                </div>
            </div>
            
            <div style="margin-top: 25px; padding-top: 20px; border-top: 1px solid #eee;">
                <button id="clearCacheBtn" 
                    style="
                        background: #95a5a6;
                        color: white;
                        border: none;
                        padding: 8px 16px;
                        border-radius: 4px;
                        cursor: pointer;
                        transition: opacity 0.2s;
                    ">
                    清理已查看职位
                </button>
                <span style="color: #7f8c8d; font-size: 0.9em; margin-left: 10px;">
                    将重置当前平台所有已经查看的职位
                </span>
            </div>

            <!-- 操作按钮 -->
            <div style="display: flex; gap: 10px; justify-content: flex-end; border-top: 1px solid #eee; padding-top: 20px;">
                <button id="saveBtn" class="dialog-btn primary">保存</button>
                <button id="cancelBtn" class="dialog-btn secondary">取消</button>
            </div>
        `;

        // 添加样式
        const style = document.createElement('style');
        style.textContent = `
            .dialog-btn {
                padding: 8px 20px;
                border: none;
                border-radius: 6px;
                cursor: pointer;
                transition: all 0.2s;
                font-size: 14px;
            }
            .primary {
                background: #3498db;
                color: white;
            }
            .secondary {
                background: #f0f0f0;
                color: #666;
            }
            .radio-group {
                display: flex;
                gap: 15px;
                margin-top: 5px;
            }
            .radio-item label {
                display: flex;
                align-items: center;
                gap: 6px;
                cursor: pointer;
            }
        `;
        dialog.appendChild(style);
        document.body.appendChild(dialog);

        // 事件绑定
        dialog.querySelector('#saveBtn').addEventListener('click', saveSettings);
        dialog.querySelector('#cancelBtn').addEventListener('click', () => dialog.remove());
        dialog.querySelector('#clearCacheBtn').addEventListener('click', () => {
            if (confirm('确定要清理已查看职位吗?\n这将重置当前平台所有已经查看的职位!')) {
                clearViewedJob(choosePlatForm());
                alert('清理完成!');
                location.reload();
            }
        });
    }

    // 生成单选按钮组
    function createRadioGroup(name, options, selected) {
        return `
            <div class="radio-group">
                ${options.map(opt => `
                    <div class="radio-item">
                        <label>
                            <input type="radio" name="${name}" value="${opt}" ${opt === selected ? 'checked' : ''}>
                            ${getActionLabel(opt)}
                        </label>
                    </div>
                `).join('')}
            </div>
        `;
    }

    // 获取操作标签
    function getActionLabel(action) {
        const labels = {
            delete: '删除', hide: '屏蔽', mark: '标识'
        };
        return labels[action] || action;
    }

    function saveSettings() {
        settings.blacklist = document.querySelector('#blacklist').value
            .split(',')
            .map(s => s.trim().toLowerCase())
            .filter(Boolean);

        settings.minSalary = parseInt(document.querySelector('#minSalary').value) || 0;
        settings.maxSalary = parseInt(document.querySelector('#maxSalary').value) || Infinity;
        settings.viewedAction = document.querySelector('input[name="viewedAction"]:checked').value;
        settings.blacklistAction = document.querySelector('input[name="blacklistAction"]:checked').value;
        settings.conditionAction = document.querySelector('input[name="conditionAction"]:checked').value;
        settings.maxDailyHours = parseFloat(document.querySelector('#maxDailyHours').value) || 0;
        settings.maxMonthlyDays = parseInt(document.querySelector('#maxMonthlyDays').value) || 0;
        settings.maxWeeklyCount = parseFloat(document.querySelector('#maxWeeklyCount').value) || 0;

        // 保存设置
        GM_setValue('blacklist', settings.blacklist);
        GM_setValue('minSalary', settings.minSalary);
        GM_setValue('maxSalary', settings.maxSalary);
        GM_setValue('viewedAction', settings.viewedAction);
        GM_setValue('blacklistAction', settings.blacklistAction);
        GM_setValue('conditionAction', settings.conditionAction);
        GM_setValue('maxDailyHours', settings.maxDailyHours);
        GM_setValue('maxMonthlyDays', settings.maxMonthlyDays);
        GM_setValue('maxWeeklyCount', settings.maxWeeklyCount);

        document.querySelector('div').remove();
        alert('设置已保存!');
        location.reload();
    }

    /**
     *
     * @return {symbol}
     */
    function choosePlatForm() {
        const href = window.location.href;
        if (href.includes('zhipin.com')) {
            return JobPlatform.BOSS_ZHIPIN;
        } else {
            return JobPlatform.UNKNOWN;
        }
    }

    /**
     *
     * @returns {PlatFormStrategy}
     */
    function getStrategy() {
        switch (choosePlatForm()) {
            case JobPlatform.BOSS_ZHIPIN:
                return new BossStrategy();
            default:
                throw new Error('Unsupported platform')
        }
    }


    /**
     *
     * @param {String} salaryRange
     */
    function parseSlaryToMontyly(salaryRange) {
        const regex = /(\d+(\.\d+)?)\s*-\s*(\d+(\.\d+)?)/;
        const match = salaryRange.match(regex);

        if (!match) {
            throw new Error('Invalid salary range format');
        }

        // 提取最小值和最大值
        let minSalary = parseFloat(match[1]);
        let maxSalary = parseFloat(match[3]);
        if (salaryRange.includes('K') || salaryRange.includes('k')) {
            minSalary = minSalary * 1000;
            maxSalary = maxSalary * 1000;
        }
        if (salaryRange.includes('周')) {
            minSalary = minSalary * settings.maxWeeklyCount;
            maxSalary = maxSalary * settings.maxWeeklyCount;
        } else if (salaryRange.includes('天')) {
            minSalary = minSalary * settings.maxMonthlyDays;
            maxSalary = maxSalary * settings.maxMonthlyDays;
        } else if (salaryRange.includes('时')) {
            minSalary = minSalary * settings.maxMonthlyDays * settings.maxDailyHours;
            maxSalary = maxSalary * settings.maxMonthlyDays * settings.maxDailyHours;
        }
        return {
            min: minSalary, max: maxSalary,
        }
    }

    /**
     *
     * @param {Symbol} jobPlatform
     */
    function clearViewedJob(jobPlatform) {
        let jobViewKey = getJobViewKey(jobPlatform);
        GM_setValue(jobViewKey, []);
    }

    /**
     *
     * @param {Symbol} jobPlatform
     * @param {String} uniqueKey
     */
    function setJobViewed(jobPlatform, uniqueKey) {
        let jobViewKey = getJobViewKey(jobPlatform);
        const jobViewedSet = getJobViewedSet(jobPlatform);
        if (jobViewedSet.has(uniqueKey)) {
            return;
        }
        jobViewedSet.add(uniqueKey);
        GM_setValue(jobViewKey, [...jobViewedSet]);
    }

    /**
     *
     * @param {Symbol} jobPlatform
     * @return {Set<String>}
     */
    function getJobViewedSet(jobPlatform) {
        let jobViewKey = getJobViewKey(jobPlatform);
        return new Set(GM_getValue(jobViewKey, []));
    }

    /**
     *
     * @param {Symbol} jobPlatform
     * @return {string}
     */
    function getJobViewKey(jobPlatform) {
        return jobPlatform.description + "ViewHistory";
    }


    class PlatFormStrategy {
        /**
         * @returns {JobPageType}
         */
        fetchJobPageType() {
            throw new Error('Method not implemented')
        }

        /**
         * @param {JobPageType} jobPageType
         * @returns {NodeListOf<Element>}
         */
        fetchJobElements(jobPageType) {
            throw new Error('Method not implemented')

        }

        /**
         * @param {Element} jobElement
         * @param {JobPageType} jobPageType
         * @returns {String|null}
         */
        fetchJobUniqueKey(jobElement, jobPageType) {
            throw new Error('Method not implemented')
        }

        /**
         *
         * @param {Element} jobElement
         * @param {JobPageType} jobPageType
         * @returns {{min: number, max: number}}
         */
        parseSalary(jobElement, jobPageType) {
            throw new Error('Method not implemented')

        }

        /**
         *
         * @param {Element} jobElement
         * @param {JobPageType} jobPageType
         * @returns {String}
         */
        parseJobName(jobElement, jobPageType) {
            throw new Error('Method not implemented')
        }

        /**
         *
         * @param {Element} jobElement
         * @param {JobPageType} jobPageType
         * @returns {String}
         */
        parseCompanyName(jobElement, jobPageType) {
            throw new Error('Method not implemented')
        }

        /**
         *
         * @param {Element} jobElement
         * @param {JobPageType} jobPageType
         * @param {JobFilterType[]} jobFilterTypes
         * @returns {void}
         */
        markCurJobElement(jobElement, jobPageType, jobFilterTypes) {
            throw new Error('Method not implemented')
        }

        /**
         *
         * @param {Element} jobElement
         * @param {JobPageType} jobPageType
         * @param {JobFilterType[]} jobFilterTypes
         * @returns {void}
         */
        blockCurJobElement(jobElement, jobPageType, jobFilterTypes) {
            throw new Error('Method not implemented')
        }

        /**
         *
         * @param {Element} jobElement
         * @param {JobPageType} jobPageType
         * @returns {void}
         */
        removeCurJobElement(jobElement, jobPageType) {
            throw new Error('Method not implemented')
        }

        /**
         *
         * @param {Element} jobElement
         * @param {JobPageType} jobPageType
         * @param {Function} eventCallback
         * @returns {void}
         */
        addViewedCallback(jobElement, jobPageType, eventCallback) {
            throw new Error('Method not implemented')
        }

        /**
         *
         * @param {Element} element
         */
        addDeleteLine(element) {
            const delElement = document.createElement('del');

            while (element.firstChild) {
                delElement.appendChild(element.firstChild);
            }

            element.appendChild(delElement);
        }

        /**
         * @param {JobFilterType} jobFilterType
         * @returns {String}
         */
        convertFilterTypeToMessage(jobFilterType) {
            if (jobFilterType === JobFilterType.BLACKLIST) {
                return '黑名单';
            } else if (jobFilterType === JobFilterType.VIEWED) {
                return '已查看';
            } else if (jobFilterType === JobFilterType.MISMATCH_CONDITION) {
                return '条件不符';
            } else {
                return '未知';
            }
        }

    }

    class BossStrategy extends PlatFormStrategy {
        fetchJobPageType() {
            if (document.querySelector('.search-job-result') != null) {
                return JobPageType.SEARCH;
            }
            return null;
        }

        fetchJobElements(jobPageType) {
            if (jobPageType === JobPageType.SEARCH) {
                return document.querySelectorAll('ul.job-list-box > li.job-card-wrapper');
            } else {
                throw new Error('Not a job element')
            }
        }

        fetchJobUniqueKey(jobElement, jobPageType) {
            if (jobPageType === JobPageType.SEARCH) {
                const element = jobElement.querySelector('.job-card-left');
                if (element == null) {
                    return null;
                }
                const url = element.href;
                if (url == null) {
                    return null;
                }
                return url.split('/job_detail/')[1].split('.html')[0];
            } else {
                throw new Error('Not a job element')
            }
        }

        parseSalary(jobElement, jobPageType) {
            if (jobPageType === JobPageType.SEARCH) {
                const salary = jobElement.querySelector('.salary').textContent;
                return parseSlaryToMontyly(salary);
            } else {
                throw new Error('Not a job element')
            }
        }

        parseCompanyName(jobElement, jobPageType) {
            if (jobPageType === JobPageType.SEARCH) {
                return jobElement.querySelector('.company-name > a').textContent
            } else {
                throw new Error('Not a job element')
            }
        }

        parseJobName(jobElement, jobPageType) {
            if (jobPageType === JobPageType.SEARCH) {
                return jobElement.querySelector('.job-name').textContent
            } else {
                throw new Error('Not a job element')
            }
        }

        addViewedCallback(jobElement, jobPageType, eventCallback) {
            jobElement.addEventListener('click', eventCallback, true);
        }

        markCurJobElement(jobElement, jobPageType, jobFilterTypes) {
            if (jobPageType === JobPageType.SEARCH) {
                const titleElement = jobElement.querySelector('.job-title');
                let markSpan = titleElement.querySelector('.mark');
                if (markSpan === null) {
                    markSpan = document.createElement('span');
                    markSpan.classList.add('mark');
                    markSpan.style.color = 'red';
                    titleElement.insertBefore(markSpan, titleElement.firstChild);
                }
                markSpan.textContent = '(' + jobFilterTypes.map(jobFilterType => this.convertFilterTypeToMessage(jobFilterType)).join('|') + ')';
                this.changeJobElementColor(jobElement, jobPageType);

            } else {
                throw new Error('Not a job element')
            }
        }

        blockCurJobElement(jobElement, jobPageType, jobFilterTypes) {
            const message = jobFilterTypes.map(jobFilterType => this.convertFilterTypeToMessage(jobFilterType)).join('|');
            if (jobPageType === JobPageType.SEARCH) {
                const cardBody = jobElement.querySelector('.job-card-body');
                cardBody.innerHTML = `
                    <div class="job-card-left"></div>
                    <div class="tip" style="color: dimgray; font-weight: bold; font-size: large; padding-top: 20px">已屏蔽</div>
                    <div class="job-card-right"></div>
                    `;
                const cardFooter = jobElement.querySelector('.job-card-footer');
                cardFooter.innerHTML = `
                    <div class="info-desc">${message}</div>
                    `;
                this.changeJobElementColor(jobElement, jobPageType);
            } else {
                throw new Error('Not a job element')
            }
        }

        removeCurJobElement(jobElement, jobPageType) {
            jobElement.parentElement.removeChild(jobElement);
            // jobElement.style.display = 'none';
        }

        /**
         *
         * @param {Element} jobElement
         * @param {JobPageType} jobPageType
         */
        changeJobElementColor(jobElement, jobPageType) {
            if (jobPageType === JobPageType.SEARCH) {
                jobElement.style.backgroundColor = '#e1e1e1';
            } else {
                throw new Error('Not a job element')
            }
        }

    }

    setInterval(() => {
        const strategy = getStrategy();
        if (strategy == null) {
            return;
        }
        let jobPageType = strategy.fetchJobPageType();
        if (jobPageType == null) {
            return;
        }
        const pageHash = getPageHash();
        if (pageHash !== curPageHash) {
            const jobPlatform = choosePlatForm();
            const viewedJobIds = getJobViewedSet(jobPlatform);
            const elements = strategy.fetchJobElements(jobPageType);
            for (let i = 0; i < elements.length; i++) {
                const job = elements[i];
                const id = strategy.fetchJobUniqueKey(job, jobPageType);
                if (id == null) {
                    continue;
                }
                initEventHandler(jobPlatform, strategy, jobPageType, job);

                const salary = strategy.parseSalary(job, jobPageType);
                const companyName = strategy.parseCompanyName(job, jobPageType);
                let jobName = strategy.parseJobName(job, jobPageType);
                console.log(`Id:${id},公司:${companyName},岗位:${jobName},月薪: ${salary.min} - ${salary.max}`);

                const jobFilterType = filterElement(strategy, job, jobPageType, viewedJobIds);
                handleElement(strategy, job, jobPageType, jobFilterType);

            }

            // 元素可能变动了,重新计算Hash
            curPageHash = getPageHash();
        }
    }, 1000)

    /**
     * @param {PlatFormStrategy} strategy
     * @param {Element} jobElement
     * @param {JobPageType} jobPageType
     * @param {Set<String>} viewedJobs
     * @returns {JobFilterType[]}
     */
    function filterElement(strategy, jobElement, jobPageType, viewedJobs) {
        const filterTypes = [];
        const companyName = strategy.parseCompanyName(jobElement, jobPageType).toLowerCase();
        for (let i = 0; i < settings.blacklist.length; i++) {
            if (companyName.includes(settings.blacklist[i])) {
                filterTypes.push(JobFilterType.BLACKLIST);
                break
            }
        }
        const jobId = strategy.fetchJobUniqueKey(jobElement, jobPageType);
        if (viewedJobs.has(jobId)) {
            filterTypes.push(JobFilterType.VIEWED);
        }

        const companySalary = strategy.parseSalary(jobElement, jobPageType);
        if (companySalary.min < settings.minSalary || companySalary.max > settings.maxSalary) {
            filterTypes.push(JobFilterType.MISMATCH_CONDITION);
        }
        return filterTypes;
    }

    /**
     *
     * @param {PlatFormStrategy} strategy
     * @param {Element} jobElement
     * @param {JobPageType} jobPageType
     * @param {JobFilterType[]} jobFilterTypes
     */
    function handleElement(strategy, jobElement, jobPageType, jobFilterTypes) {
        if (jobFilterTypes.length === 0) {
            return;
        }
        let filter = jobFilterTypes.map(filterType => {
            if (filterType === JobFilterType.BLACKLIST) {
                return settings.blacklistAction;
            } else if (filterType === JobFilterType.VIEWED) {
                return settings.viewedAction;
            } else if (filterType === JobFilterType.MISMATCH_CONDITION) {
                return settings.conditionAction;
            } else {
                return null;
            }
        }).filter(action => action != null && typeof action === 'string');
        if (filter.includes('delete')) {
            strategy.removeCurJobElement(jobElement, jobPageType);
        } else if (filter.includes('hide')) {
            strategy.blockCurJobElement(jobElement, jobPageType, jobFilterTypes);
        } else if (filter.includes('mark')) {
            strategy.markCurJobElement(jobElement, jobPageType, jobFilterTypes);
        }
    }

    function getPageHash() {
        const strategy = getStrategy();
        let jobPageType = strategy.fetchJobPageType();
        const elements = strategy.fetchJobElements(jobPageType);
        let keys = '';
        for (let i = 0; i < elements.length; i++) {
            const id = strategy.fetchJobUniqueKey(elements[i], jobPageType);
            if (id === null) {
                continue;
            }
            keys += id;
        }
        return hashCode(keys);
    }

    /**
     *@param {Symbol} jobPlatform
     * @param {PlatFormStrategy} strategy
     * @param {JobPageType} jobPageType
     * @param {Element} element
     */
    function initEventHandler(jobPlatform, strategy, jobPageType, element) {
        const callBack = () => {
            const id = strategy.fetchJobUniqueKey(element, jobPageType);
            setJobViewed(jobPlatform, id);
            // 重置PageHash,来刷新
            curPageHash = null;
        };
        strategy.addViewedCallback(element, jobPageType, callBack);
    }

    /**
     *
     * @param {String} str
     * @returns {number}
     */
    function hashCode(str) {
        let hash = 0;
        if (str.length === 0) return hash;
        for (let i = 0; i < str.length; i++) {
            hash = (hash << 5) - hash + str.charCodeAt(i);
            hash |= 0;
        }
        return hash;
    }

})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址