跳转到内容

User:PexEric/InCatAssess.js

维基百科,自由的百科全书
注意:保存之后,你必须清除浏览器缓存才能看到做出的更改。Google ChromeFirefoxMicrosoft EdgeSafari:按住⇧ Shift键并单击工具栏的“刷新”按钮。参阅Help:绕过浏览器缓存以获取更多帮助。
(function () {
    'use strict';

    // 配置项
    const CONFIG = {
        maxConcurrentSaves: 2,      // 最大并发保存数
        prefetchCount: 3,            // 预取条目数
        linkThreshold: 2,            // 省级专题链接出现阈值
        retryLimit: 1,               // 保存失败重试次数
        enableAutoWPBS: false,        // 允许自动新建WPBS
        enableImportanceFill: false,  // 允许补全importance=Low
        enableProvinceDetection: false, // 启用省级专题自动识别
        markAsMinorEdit: false,       // 将编辑标记为小编辑
    };

    // 省级行政区映射表
    const PROVINCE_MAP = {
        // 直辖市
        '北京': '北京', '北京市': '北京',
        '天津': '天津', '天津市': '天津',
        '上海': '上海', '上海市': '上海',
        '重庆': '重庆', '重庆市': '重庆',
        // 省
        '河北': '河北', '河北省': '河北',
        '山西': '山西', '山西省': '山西',
        '辽宁': '辽宁', '辽宁省': '辽宁',
        '吉林': '吉林', '吉林省': '吉林',
        '黑龙江': '黑龙江', '黑龙江省': '黑龙江',
        '江苏': '江苏', '江苏省': '江苏',
        '浙江': '浙江', '浙江省': '浙江',
        '安徽': '安徽', '安徽省': '安徽',
        '福建': '福建', '福建省': '福建',
        '江西': '江西', '江西省': '江西',
        '山东': '山东', '山东省': '山东',
        '河南': '河南', '河南省': '河南',
        '湖北': '湖北', '湖北省': '湖北',
        '湖南': '湖南', '湖南省': '湖南',
        '广东': '广东', '广东省': '广东',
        '海南': '海南', '海南省': '海南',
        '四川': '四川', '四川省': '四川',
        '贵州': '贵州', '贵州省': '贵州',
        '云南': '云南', '云南省': '云南',
        '陕西': '陕西', '陕西省': '陕西',
        '甘肃': '甘肃', '甘肃省': '甘肃',
        '青海': '青海', '青海省': '青海',
        //'台湾': '台湾', '台湾省': '台湾', '臺灣': '台湾',
        // 自治区
        '内蒙古': '内蒙古', '内蒙古自治区': '内蒙古',
        '广西': '广西', '广西壮族自治区': '广西',
        '西藏': '西藏', '西藏自治区': '西藏',
        '宁夏': '宁夏', '宁夏回族自治区': '宁夏',
        '新疆': '新疆', '新疆维吾尔自治区': '新疆'
        // 特别行政区
        //'香港': '香港', '香港特别行政区': '香港',
        //'澳门': '澳门', '澳门特别行政区': '澳门', '澳門': '澳门'
    };

    /**
     * 异步保存队列管理器
     */
    class SaveQueueManager {
        constructor(api, maxConcurrent = 2) {
            this.api = api;
            this.maxConcurrent = maxConcurrent;
            this.queue = [];              // 待保存队列
            this.processing = new Map();  // 正在处理的任务
            this.completed = [];          // 已完成任务
            this.failed = [];            // 失败任务
            this.onStatusChange = null;  // 状态变化回调
        }

        async add(task) {
            this.queue.push({
                ...task,
                retryCount: 0,
                status: 'pending'
            });

            this.processNext();
            this.notifyStatusChange();
        }

        async processNext() {
            if (this.processing.size >= this.maxConcurrent) {
                return;
            }

            const task = this.queue.shift();
            if (!task) {
                return;
            }

            task.status = 'processing';
            this.processing.set(task.item.talkTitle, task);
            this.notifyStatusChange();

            try {
                await this.executeTask(task);

                task.status = 'completed';
                this.completed.push(task);
                this.processing.delete(task.item.talkTitle);

                mw.notify(`已保存: ${task.summary}`, {
                    type: 'success',
                    autoHide: true,
                    tag: 'rating-save'
                });

            } catch (error) {
                task.error = error.message;

                if (task.retryCount < CONFIG.retryLimit) {
                    task.retryCount++;
                    task.status = 'retrying';
                    this.queue.unshift(task);
                    this.processing.delete(task.item.talkTitle);

                    setTimeout(() => {
                        this.processNext();
                    }, 2000 * task.retryCount);

                } else {
                    task.status = 'failed';
                    this.failed.push(task);
                    this.processing.delete(task.item.talkTitle);

                    mw.notify(`保存失败 (${task.item.talkTitle}): ${error.message}`, {
                        type: 'error',
                        autoHide: false,
                        tag: 'rating-error'
                    });
                }
            }

            this.notifyStatusChange();
            this.processNext();
        }

        async executeTask(task) {
            const talkContent = await this.getTalkPageContent(task.item.talkTitle);

            let province = null;
            if (CONFIG.enableProvinceDetection) {
                province = await this.identifyProvince(task.item.mainTitle);
            }

            const processor = new WPBSProcessor();
            const result = processor.process(talkContent.content, {
                classRating: task.rating,
                province: province,
                enableImportanceFill: CONFIG.enableImportanceFill
            });

            const summary = this.buildEditSummary(task.rating, result.addedProvince, result.importanceFilled);
            task.summary = summary;

            await this.api.postWithToken('csrf', {
                action: 'edit',
                title: task.item.talkTitle,
                text: result.text,
                summary: summary,
                minor: CONFIG.markAsMinorEdit,
                watchlist: 'nochange',
                basetimestamp: talkContent.timestamp,
                starttimestamp: talkContent.starttimestamp,
                format: 'json'
            });

            return result;
        }

        async getTalkPageContent(title) {
            const response = await this.api.get({
                action: 'query',
                prop: 'revisions|info',
                titles: title,
                rvslots: 'main',
                rvprop: 'content|timestamp',
                format: 'json'
            });

            const page = Object.values(response.query.pages)[0];

            if (page.missing) {
                return {
                    content: '',
                    timestamp: null,
                    starttimestamp: new Date().toISOString()
                };
            }

            return {
                content: page.revisions[0].slots.main['*'],
                timestamp: page.revisions[0].timestamp,
                starttimestamp: new Date().toISOString()
            };
        }

        async identifyProvince(articleTitle) {
            try {
                const wikitextResponse = await this.api.get({
                    action: 'query',
                    prop: 'revisions',
                    titles: articleTitle,
                    rvslots: 'main',
                    rvprop: 'content',
                    format: 'json'
                });

                const pages = wikitextResponse.query.pages;
                const page = Object.values(pages)[0];

                if (page.missing || !page.revisions || !page.revisions[0]) {
                    return null;
                }

                const wikitext = page.revisions[0].slots.main['*'];
                const provinceCounts = this.analyzeProvinceLinks(wikitext);

                let maxProvince = null;
                let maxCount = 0;

                for (const [province, count] of Object.entries(provinceCounts)) {
                    if (count > maxCount && count >= CONFIG.linkThreshold) {
                        maxProvince = province;
                        maxCount = count;
                    }
                }

                return maxProvince;
            } catch (error) {
                return null;
            }
        }

        analyzeProvinceLinks(wikitext) {
            const provinceCounts = {};

            const linkRegex = /\[\[([^\]|]+)(?:\|[^\]]+)?\]\]/g;
            let match;

            while ((match = linkRegex.exec(wikitext)) !== null) {
                const linkTarget = match[1].trim();
                const normalized = this.normalizeProvinceName(linkTarget);

                if (normalized) {
                    provinceCounts[normalized] = (provinceCounts[normalized] || 0) + 1;
                }
            }

            const textCounts = this.analyzeProvinceText(wikitext);

            for (const [province, count] of Object.entries(textCounts)) {
                if (provinceCounts[province]) {
                    provinceCounts[province] += Math.floor(count * 0.3);
                } else {
                    provinceCounts[province] = Math.floor(count * 0.3);
                }
            }

            return provinceCounts;
        }

        analyzeProvinceText(wikitext) {
            const provinceCounts = {};

            let cleanText = wikitext
                .replace(/\[\[[^\]]+\]\]/g, '')
                .replace(/\{\{[^}]+\}\}/g, '')
                .replace(/<[^>]+>/g, '')
                .replace(/\{\|[\s\S]*?\|\}/g, '');

            for (const [fullName, shortName] of Object.entries(PROVINCE_MAP)) {
                const regex = new RegExp(`\\b${this.escapeRegex(fullName)}\\b`, 'g');
                const matches = cleanText.match(regex);

                if (matches) {
                    provinceCounts[shortName] = (provinceCounts[shortName] || 0) + matches.length;
                }
            }

            return provinceCounts;
        }

        escapeRegex(string) {
            return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
        }



        normalizeProvinceName(title) {
            let cleanTitle = title.replace(/^[^:]+:/, '').trim();

            if (PROVINCE_MAP[cleanTitle]) {
                return PROVINCE_MAP[cleanTitle];
            }

            for (const [key, value] of Object.entries(PROVINCE_MAP)) {
                if (cleanTitle === key) {
                    return value;
                }

                if (cleanTitle.includes(key) || key.includes(cleanTitle)) {
                    const keyWords = ['省', '市', '自治区', '特别行政区', '行政区'];
                    const hasAdminSuffix = keyWords.some(suffix =>
                        cleanTitle.includes(key + suffix) || cleanTitle === key
                    );

                    if (hasAdminSuffix || cleanTitle === key) {
                        return value;
                    }
                }
            }

            const specialPatterns = [
                { pattern: /^(.+?)(?:省|市|自治区|特别行政区)/, extract: 1 },
                { pattern: /^(.+?)(?:地区|盟|州)/, extract: 1 }
            ];

            for (const { pattern, extract } of specialPatterns) {
                const match = cleanTitle.match(pattern);
                if (match && PROVINCE_MAP[match[extract]]) {
                    return PROVINCE_MAP[match[extract]];
                }
            }

            return null;
        }

        buildEditSummary(rating, addedProvince, importanceCount) {
            const parts = [`[[User:PexEric/InCatAssess|分类内评级]]:${this.getRatingLabel(rating)}级`];

            if (addedProvince) {
                parts.push(`+{{${addedProvince}专题}}`);
            }

            if (importanceCount > 0) {
                parts.push(`补全${importanceCount}个专题重要度`);
            }

            return parts.join(';');
        }

        getRatingLabel(rating) {
            const reverseMap = {
                'BList': '乙级列表',
                'CList': '丙级列表',
                'List': '列表',
                'SList': '小列表',
                'B': '乙',
                'C': '丙',
                'D': '丁',
                'Start': '初',
                'Stub': '小作品'
            };
            return reverseMap[rating] || rating;
        }

        notifyStatusChange() {
            if (this.onStatusChange) {
                this.onStatusChange({
                    pending: this.queue.length,
                    processing: this.processing.size,
                    completed: this.completed.length,
                    failed: this.failed.length
                });
            }
        }

        getStatus() {
            return {
                pending: this.queue.length,
                processing: this.processing.size,
                completed: this.completed.length,
                failed: this.failed.length,
                total: this.queue.length + this.processing.size + this.completed.length + this.failed.length
            };
        }
    }

    class CategoryRatingTool {
        constructor() {
            this.api = new mw.Api();
            this.queue = [];           // 待处理队列
            this.currentIndex = 0;     // 当前处理索引
            this.processedCount = 0;   // 已处理计数
            this.prefetchCache = {};   // 预取缓存
            this.vueApp = null;        // Vue应用实例
            this.currentItem = null;   // 当前处理的条目
            this.saveQueue = null;     // 保存队列管理器
        }

        canRun() {
            const ns = mw.config.get('wgNamespaceNumber');
            const title = mw.config.get('wgTitle');

            if (ns !== 14) return false;

            const pattern = /^(未评级|未評級).+(条|條)目$/;
            return pattern.test(title);
        }

        addPortletLink() {
            const self = this;
            const link = mw.util.addPortletLink(
                'p-cactions',
                '#',
                '分类内评级',
                'ca-category-rating',
                '为分类内条目批量评级'
            );

            if (link) {
                $(link).click(function (e) {
                    e.preventDefault();
                    self.start();
                });
            }
        }

        async start() {
            try {
                await mw.loader.using([
                    'mediawiki.api',
                    'mediawiki.util',
                    '@wikimedia/codex',
                    'vue'
                ]);

                this.saveQueue = new SaveQueueManager(this.api, CONFIG.maxConcurrentSaves);
                this.saveQueue.onStatusChange = (status) => {
                    this.updateSaveStatus(status);
                };

                mw.notify('正在加载分类成员...', { type: 'info' });
                await this.loadCategoryMembers();

                if (this.queue.length === 0) {
                    mw.notify('该分类没有待处理的条目', { type: 'warning' });
                    return;
                }

                await this.createDialog();
                this.startPrefetching();
                await this.showNextItem();

            } catch (error) {
                mw.notify('启动评级工具失败: ' + error.message, { type: 'error' });
            }
        }

        async loadCategoryMembers() {
            const categoryTitle = mw.config.get('wgPageName');
            let cmcontinue = null;
            this.queue = [];

            do {
                const params = {
                    action: 'query',
                    list: 'categorymembers',
                    cmtitle: categoryTitle,
                    cmnamespace: 1,
                    cmtype: 'page',
                    cmlimit: 'max',
                    format: 'json'
                };

                if (cmcontinue) {
                    params.cmcontinue = cmcontinue;
                }

                const response = await this.api.get(params);

                if (response.query && response.query.categorymembers) {
                    for (const member of response.query.categorymembers) {
                        this.queue.push({
                            talkTitle: member.title,
                            mainTitle: member.title.replace(/^Talk:/, '').replace(/^讨论:/, '')
                        });
                    }
                }

                cmcontinue = response.continue?.cmcontinue;

            } while (cmcontinue);
        }

        async createDialog() {
            const self = this;

            return new Promise((resolve) => {
                mw.loader.using(['@wikimedia/codex', 'vue'], function (require) {
                    const Vue = require('vue');
                    const Codex = require('@wikimedia/codex');

                    self._createDialogWithModules(Vue, Codex);
                    resolve();
                });
            });
        }

        _createDialogWithModules(Vue, Codex) {
            const self = this;

            const mountPoint = document.body.appendChild(document.createElement('div'));
            mountPoint.id = 'rating-dialog-mount';

            this.vueApp = Vue.createMwApp({
                data() {
                    return {
                        showDialog: true,
                        currentIndex: 0,
                        totalItems: self.queue.length,
                        currentItem: null,
                        currentItemInfo: null, // XTools信息
                        previewContent: '加载中...',
                        statusText: '准备就绪',
                        saveStatusText: '保存队列:待处理 0 | 处理中 0 | 已完成 0 | 失败 0',
                        progress: 0,
                        buttonsDisabled: false,
                        showEditDialog: false,
                        customListText: '',
                        isCustomList: false,
                        ratings: [
                            { label: '小作品', value: 'Stub' },
                            { label: '初', value: 'Start' },
                            { label: '丁', value: 'D' },
                            { label: '丙', value: 'C' },
                            { label: '乙', value: 'B' },
                            { label: '小列表', value: 'SList' },
                            { label: '列表', value: 'List' },
                            { label: '丙级列表', value: 'CList' },
                            { label: '乙级列表', value: 'BList' }
                        ]
                    };
                },
                computed: {
                    dialogTitle() {
                        if (this.isCustomList) {
                            return '分类内评级 - 自定义列表';
                        }
                        return '分类内评级 - ' + mw.config.get('wgTitle');
                    },
                    itemInfoHtml() {
                        if (!this.currentItem) return '';

                        const articleUrl = mw.util.getUrl(this.currentItem.mainTitle);
                        const talkUrl = mw.util.getUrl(this.currentItem.talkTitle);
                        const editUrl = mw.util.getUrl(this.currentItem.mainTitle, { action: 'edit' });
                        const historyUrl = mw.util.getUrl(this.currentItem.mainTitle, { action: 'history' });

                        let basicInfo = `<strong>当前条目:</strong>` +
                            `<a href="${articleUrl}" target="_blank">${mw.html.escape(this.currentItem.mainTitle)}</a> ` +
                            `<span style="font-size: 0.9em;">` +
                            `(<a href="${talkUrl}" target="_blank">论</a>` +
                            ` · <a href="${editUrl}" target="_blank">编</a>` +
                            ` · <a href="${historyUrl}" target="_blank">历</a>)` +
                            `</span>`;

                        if (this.currentItemInfo) {
                            if (this.currentItemInfo.loading) {
                                basicInfo += `<span style="font-size: 0.9em; color: #999; margin-left: 8px;">` +
                                    `正在获取条目统计信息...` +
                                    `</span>`;
                            } else if (this.currentItemInfo.error) {
                                basicInfo += `<span style="font-size: 0.9em; color: #d33; margin-left: 8px;">` +
                                    `统计信息获取失败: ${this.currentItemInfo.errorMessage}` +
                                    `</span>`;
                            } else {
                                const info = this.currentItemInfo;
                                basicInfo += `<span style="font-size: 0.9em; color: #666; margin-left: 8px;">` +
                                    `由<a href="https://${mw.config.get('wgServerName')}/wiki/User:${info.creator}" target="_blank">${info.creator}</a>` +
                                    `于${info.createdDate}创建,共${info.revisions}次修订,` +
                                    `最近修订于${info.daysSinceLastEdit}天前。` +
                                    `共${info.editors}位编辑者,` +
                                    `最近${info.pageviewsOffset}天共${info.pageviews}浏览数。` +
                                    `<a href="${info.pageinfoUrl}" target="_blank">查看完整统计</a>。` +
                                    `</span>`;
                            }
                        }

                        return basicInfo;
                    }
                },
                watch: {
                    currentItemInfo: {
                        handler(newVal, oldVal) {
                            this.$forceUpdate();
                        },
                        deep: true,
                        immediate: true
                    }
                },
                template: `
                    <cdx-dialog 
                        v-model:open="showDialog"
                        :title="dialogTitle"
                        :use-close-button="true"
                        class="rating-dialog"
                    >
                        <div class="rating-content">
                            <cdx-button @click="openEditDialog" style="float: right; margin-bottom: 10px;">编辑列表</cdx-button>
                            <!-- 状态信息区 -->
                            <div class="status-section">
                                <div class="status-text">{{ statusText }}</div>
                                <div class="save-status-text">{{ saveStatusText }}</div>
                                <div class="progress-container">
                                    <div class="progress-bar" :style="{ width: progress + '%' }"></div>
                                </div>
                                <div class="item-info" v-html="itemInfoHtml"></div>
                            </div>
                            
                            <!-- 预览区域 -->
                            <div class="preview-section">
                                <div class="preview-content" v-html="previewContent"></div>
                            </div>
                            
                            <!-- 评级按钮区 -->
                            <div class="rating-buttons">
                                <cdx-button 
                                    v-for="rating in ratings"
                                    :key="rating.value"
                                    :disabled="buttonsDisabled"
                                    action="progressive"
                                    @click="handleRating(rating.value)"
                                    class="rating-btn"
                                >
                                    {{ rating.label }}
                                </cdx-button>
                                <cdx-button 
                                    :disabled="buttonsDisabled"
                                    action="destructive"
                                    @click="handleSkip"
                                    class="skip-btn"
                                >
                                    跳过
                                </cdx-button>
                            </div>
                            
                            <div class="shortcut-hint">
                                <small>快捷键:0-8 对应评级由左到右,S 跳过<!-- ,← → 切换条目--></small>
                            </div>
                        </div>
                        <cdx-dialog v-model:open="showEditDialog" title="编辑自定义列表" :primary-action="{ label: '保存', actionType: 'progressive' }" :default-action="{ label: '取消' }" @primary="saveCustomList" @default="closeEditDialog">
                            <p>每行输入一个项目</p>
                            <cdx-text-area v-model="customListText" rows="10" style="width: 100%;"></cdx-text-area>
                        </cdx-dialog>
                    </cdx-dialog>
                `,
                methods: {
                    handleRating(rating) {
                        self.handleRating(rating);
                    },
                    handleSkip() {
                        self.handleSkip();
                    },
                    openEditDialog() {
                        this.customListText = self.queue.map(item => item.mainTitle).join('\n');
                        this.showEditDialog = true;
                    },
                    saveCustomList() {
                        const titles = this.customListText.split('\n').map(t => t.trim()).filter(t => t);
                        self.queue = titles.map(title => ({
                            talkTitle: 'Talk:' + title,
                            mainTitle: title
                        }));
                        self.currentIndex = 0;
                        self.processedCount = 0;
                        this.totalItems = self.queue.length;
                        this.isCustomList = true;
                        this.showEditDialog = false;
                        self.showNextItem();
                    },
                    closeEditDialog() {
                        this.showEditDialog = false;
                    },
                    async rate(rating) {
                        if (!this.currentItem) return;

                        this.buttonsDisabled = true;
                        await this.saveQueue.add({
                            item: this.currentItem,
                            rating: rating
                        });

                        await this.showNextItem();
                    },
                },
                mounted() {
                },
                unmounted() {
                }
            })
                .component('cdx-button', Codex.CdxButton)
                .component('cdx-dialog', Codex.CdxDialog)
                .component('cdx-text-area', Codex.CdxTextArea)
                .mount(mountPoint);

            // 添加样式
            this.addDialogStyles();

            // 绑定键盘事件
            this.bindKeyboardEvents();
        }

        /**
         * 添加对话框样式
         */
        addDialogStyles() {
            const style = document.createElement('style');
            style.textContent = `
                .cdx-dialog {
                    max-width: 64rem;
                    height: 100%;
                }
                
                .rating-dialog .cdx-dialog__body {
                    overflow-y: auto;
                }
                
                .rating-content {
                    padding: 16px;
                }
                
                .status-section {
                    margin-bottom: 16px;
                    padding-bottom: 16px;
                    border-bottom: 1px solid #a2a9b1;
                }
                
                .status-text {
                    font-weight: bold;
                    margin-bottom: 8px;
                }
                
                .save-status-text {
                    font-size: 0.9em;
                    color: #666;
                    margin-bottom: 8px;
                }
                
                .progress-container {
                    width: 100%;
                    height: 8px;
                    background-color: #eaecf0;
                    border-radius: 4px;
                    overflow: hidden;
                    margin-bottom: 8px;
                }
                
                .progress-bar {
                    height: 100%;
                    background-color: #36c;
                    transition: width 0.3s ease;
                }
                
                .item-info {
                    font-size: 0.95em;
                }
                
                .preview-section {
                    margin-bottom: 16px;
                }
                
                .preview-content {
                    height: calc(100vh - 300px);
                    min-height: 300px;
                    max-height: 600px;
                    border: 1px solid #a2a9b1;
                    background: #fff;
                    padding: 16px;
                    overflow-y: auto;
                    border-radius: 2px;
                }
                
                .rating-buttons {
                    display: flex;
                    gap: 8px;
                    flex-wrap: wrap;
                    justify-content: center;
                    margin-bottom: 16px;
                }
                
                .rating-btn {
                    min-width: 80px;
                }
                
                .skip-btn {
                    margin-left: 16px;
                }
                
                .shortcut-hint {
                    text-align: center;
                    color: #666;
                }
            `;
            document.head.appendChild(style);
        }

        /**
         * 绑定键盘事件
         */
        bindKeyboardEvents() {
            const self = this;
            this.keyboardHandler = function (e) {
                // 检查是否在输入框中
                if ($(e.target).is('input, textarea')) {
                    return;
                }

                const keyMap = {
                    '0': 'Stub',
                    '1': 'Start',
                    '2': 'D',
                    '3': 'C',
                    '4': 'B',
                    '5': 'SList',
                    '6': 'List',
                    '7': 'CList',
                    '8': 'BList',
                    's': 'skip',
                    'S': 'skip'
                };

                if (keyMap[e.key]) {
                    e.preventDefault();
                    if (keyMap[e.key] === 'skip') {
                        self.handleSkip();
                    } else {
                        self.handleRating(keyMap[e.key]);
                    }
                }
            };

            document.addEventListener('keydown', this.keyboardHandler);
        }

        /**
         * 解绑键盘事件
         */
        unbindKeyboardEvents() {
            if (this.keyboardHandler) {
                document.removeEventListener('keydown', this.keyboardHandler);
                this.keyboardHandler = null;
            }
        }

        /**
         * 更新保存状态显示
         */
        updateSaveStatus(status) {
            if (this.vueApp) {
                const text = `保存队列:待处理 ${status.pending} | 处理中 ${status.processing} | 已完成 ${status.completed} | 失败 ${status.failed}`;
                this.vueApp.saveStatusText = text;
            }
        }

        async showNextItem() {
            if (this.currentIndex >= this.queue.length) {
                if (this.vueApp) {
                    this.vueApp.statusText = '所有条目已处理完毕!';
                    this.vueApp.buttonsDisabled = true;
                }

                const saveStatus = this.saveQueue?.getStatus();
                if (saveStatus && (saveStatus.pending > 0 || saveStatus.processing > 0)) {
                    this.vueApp.statusText = '等待所有保存完成...';
                } else {
                    mw.notify('所有条目已处理完毕!', { type: 'success' });
                }

                return false;
            }

            const item = this.queue[this.currentIndex];
            this.currentItem = item;
            this.currentIndex++;

            if (!this.vueApp) {
                return false;
            }

            const progress = ((this.currentIndex - 1) / this.queue.length) * 100;
            this.vueApp.progress = progress;

            this.vueApp.statusText = `进度:${this.currentIndex} / ${this.queue.length}`;
            this.vueApp.currentItem = item;
            this.vueApp.currentItemInfo = { loading: true };

            this.vueApp.previewContent = '<div style="padding: 20px; text-align: center;">加载中...</div>';
            this.vueApp.buttonsDisabled = true;

            this.getXToolsInfo(item.mainTitle).then(info => {
                if (this.vueApp && this.vueApp.currentItem && this.vueApp.currentItem.mainTitle === item.mainTitle) {
                    this.vueApp.currentItemInfo = info;
                    this.vueApp.$forceUpdate();
                }
            }).catch(error => {
                if (this.vueApp && this.vueApp.currentItem && this.vueApp.currentItem.mainTitle === item.mainTitle) {
                    this.vueApp.currentItemInfo = {
                        error: true,
                        errorMessage: error.message || '获取统计信息失败'
                    };
                }
            });

            try {
                const preview = await this.getArticlePreview(item.mainTitle);
                this.vueApp.previewContent = preview;
                this.vueApp.buttonsDisabled = false;

            } catch (error) {
                this.vueApp.previewContent =
                    '<div style="color: red; padding: 20px;">加载条目失败: ' +
                    mw.html.escape(error.message) + '</div>';

                this.vueApp.buttonsDisabled = false;
            }

            return true;
        }

        async handleRating(rating) {
            if (!this.currentItem) {
                return;
            }

            await this.saveQueue.add({
                item: this.currentItem,
                rating: rating
            });

            await this.showNextItem();
        }

        async handleSkip() {
            await this.showNextItem();
        }

        async getXToolsInfo(pageName) {
            function safeToLocaleString(num) {
                if (typeof num === 'number' && !isNaN(num)) {
                    return num.toLocaleString();
                }
                return '0';
            }

            try {
                const serverName = mw.config.get('wgServerName');
                const encodedPageName = encodeURIComponent(pageName);
                const apiUrl = `https://xtools.wmcloud.org/api/page/pageinfo/${serverName}/${encodedPageName}`;

                const pageInfo = await $.get(apiUrl);

                const project = pageInfo.project;
                const pageEnc = encodeURIComponent(pageInfo.page);
                const pageinfoUrl = `https://xtools.wmcloud.org/pageinfo/${project}/${pageEnc}`;
                const createdDate = new Date(pageInfo.created_at).toISOString().split('T')[0];
                const revisionsText = safeToLocaleString(pageInfo.revisions);
                const editorsText = safeToLocaleString(pageInfo.editors);
                const pageviewsText = safeToLocaleString(pageInfo.pageviews);
                const days = Math.round(pageInfo.secs_since_last_edit / 86400);

                return {
                    creator: pageInfo.creator,
                    createdDate: createdDate,
                    revisions: revisionsText,
                    editors: editorsText,
                    pageviews: pageviewsText,
                    daysSinceLastEdit: days,
                    pageviewsOffset: pageInfo.pageviews_offset,
                    pageinfoUrl: pageinfoUrl
                };
            } catch (error) {
                return null;
            }
        }











        cleanup() {
            this.unbindKeyboardEvents();

            if (this.vueApp) {
                this.vueApp.unmount();
                this.vueApp = null;
            }

            const mountPoint = document.getElementById('rating-dialog-mount');
            if (mountPoint) {
                mountPoint.remove();
            }
        }

        async getArticlePreview(title) {
            if (this.prefetchCache[title]) {
                const cached = this.prefetchCache[title];
                delete this.prefetchCache[title];
                return cached;
            }

            try {
                const response = await this.api.get({
                    action: 'parse',
                    page: title,
                    prop: 'text',
                    disablelimitreport: true,
                    format: 'json'
                });

                if (response.parse && response.parse.text) {
                    return response.parse.text['*'];
                } else {
                    throw new Error('无法获取页面内容');
                }
            } catch (error) {
                throw error;
            }
        }

        async startPrefetching() {
            const prefetchNext = async () => {
                const startIdx = this.currentIndex;
                const endIdx = Math.min(startIdx + CONFIG.prefetchCount, this.queue.length);

                for (let i = startIdx; i < endIdx; i++) {
                    const item = this.queue[i];
                    if (item && !this.prefetchCache[item.mainTitle]) {
                        try {
                            const response = await this.api.get({
                                action: 'parse',
                                page: item.mainTitle,
                                prop: 'text',
                                disablelimitreport: true,
                                format: 'json'
                            });

                            if (response.parse && response.parse.text) {
                                this.prefetchCache[item.mainTitle] = response.parse.text['*'];
                            }
                        } catch (error) {
                        }
                    }
                }
            };

            this.prefetchTimer = setInterval(prefetchNext, 2000);
            prefetchNext();
        }
    }

    class WPBSProcessor {
        process(text, options) {
            const result = {
                text: text,
                addedProvince: null,
                importanceFilled: 0
            };

            // 查找WPBS模板
            const wpbsMatch = this.findWPBS(text);

            if (wpbsMatch) {
                // 更新现有WPBS
                result.text = this.updateWPBS(text, wpbsMatch, options, result);
            } else if (CONFIG.enableAutoWPBS) {
                // 创建新WPBS
                result.text = this.createWPBS(text, options, result);
            }

            return result;
        }

        findWPBS(text) {
            const patterns = [
                /\{\{\s*WikiProject[\s_]+banner[\s_]+shell/i,
                /\{\{\s*WPBS/i,
                /\{\{\s*PJBS/i,
                /\{\{\s*Wikiproject[\s_]+banner[\s_]+shell/i,
                /\{\{\s*wikiproject[\s_]+banner[\s_]+shell/i
            ];

            for (const pattern of patterns) {
                const match = text.match(pattern);
                if (match) {
                    const start = match.index;
                    const end = this.findTemplateEnd(text, start);

                    if (end > start) {
                        return {
                            start: start,
                            end: end,
                            content: text.substring(start, end + 2)
                        };
                    }
                }
            }

            return null;
        }

        findTemplateEnd(text, start) {
            let depth = 0;
            let inTemplate = false;

            try {
                for (let i = start; i < text.length - 1; i++) {
                    if (text[i] === '{' && text[i + 1] === '{') {
                        depth++;
                        inTemplate = true;
                        i++;
                    } else if (text[i] === '}' && text[i + 1] === '}') {
                        depth--;
                        if (depth === 0 && inTemplate) {
                            return i + 1;
                        }
                        i++;
                    }
                }

                return text.length - 1;

            } catch (error) {
                return text.length - 1;
            }
        }

        updateWPBS(text, wpbsMatch, options, result) {
            const wpbsContent = wpbsMatch.content;

            const parseResult = this.parseWPBSTemplate(wpbsContent);
            if (!parseResult) {
                return this.createWPBS(text, options, result);
            }

            const { templateName, templateBody } = parseResult;

            const params = this.parseTemplateParams(templateBody);

            params.class = options.classRating;

            let projectsContent = '';

            if (params['1'] !== undefined) {
                projectsContent = params['1'];
            } else {
                const anonymousContent = this.extractAnonymousContent(templateBody);
                if (anonymousContent) {
                    projectsContent = anonymousContent;
                }
            }

            if (options.province) {
                const updatedProjects = this.updateProvinceProject(projectsContent, options.province);
                if (updatedProjects.added) {
                    result.addedProvince = options.province;
                }
                projectsContent = updatedProjects.content;
            }

            if (options.enableImportanceFill && projectsContent) {
                const filled = this.fillImportance(projectsContent);
                projectsContent = filled.text;
                result.importanceFilled = filled.count;
            }

            let newWPBS = `{{${templateName}`;

            if (params.class) {
                newWPBS += ` |class=${params.class}`;
            }

            for (const [key, value] of Object.entries(params)) {
                if (key !== 'class' && key !== '1' && key !== '_anonymous') {
                    newWPBS += ` |${key}=${value}`;
                }
            }

            if (projectsContent) {
                newWPBS += ` |1=\n${projectsContent.trim()}\n`;
            }

            newWPBS += '}}';

            const beforeWPBS = text.substring(0, wpbsMatch.start);
            const afterWPBS = text.substring(wpbsMatch.end + 1);

            let preservedSpacing = '';
            let cleanAfterWPBS = afterWPBS;

            const newlineMatch = afterWPBS.match(/^\n+/);
            if (newlineMatch) {
                preservedSpacing = newlineMatch[0];
                cleanAfterWPBS = afterWPBS.substring(newlineMatch[0].length);
            }

            return beforeWPBS + newWPBS + preservedSpacing + cleanAfterWPBS;
        }





        parseWPBSTemplate(wpbsContent) {
            let content = wpbsContent.trim();
            if (!content.startsWith('{{') || !content.endsWith('}}')) {
                return null;
            }

            content = content.slice(2, -2).trim();

            let templateName = '';
            let templateBody = '';

            let depth = 0;
            let nameEnd = -1;

            for (let i = 0; i < content.length; i++) {
                const char = content[i];
                const next = content[i + 1];

                if (char === '{' && next === '{') {
                    depth++;
                    i++;
                } else if (char === '}' && next === '}') {
                    depth--;
                    i++;
                } else if (char === '|' && depth === 0) {
                    nameEnd = i;
                    break;
                }
            }

            if (nameEnd === -1) {
                templateName = content.trim();
                templateBody = '';
            } else {
                templateName = content.substring(0, nameEnd).trim();
                templateBody = content.substring(nameEnd).trim();
            }

            if (!templateName) {
                return null;
            }

            return {
                templateName: templateName,
                templateBody: templateBody
            };
        }

        extractAnonymousContent(templateBody) {
            if (!templateBody || !templateBody.trim()) return '';

            if (templateBody.trim().startsWith('|')) {
                const parts = this.splitByTopLevelPipe(templateBody.substring(1));

                for (let i = 0; i < parts.length; i++) {
                    const part = parts[i].trim();
                    if (part && !part.includes('=')) {
                        return part;
                    } else if (part && part.includes('=')) {
                        const eqIndex = part.indexOf('=');
                        const paramName = part.substring(0, eqIndex).trim();

                        if (paramName.includes('{{') || paramName.includes('}}')) {
                            return part;
                        }
                    }
                }
            } else {
                if (!templateBody.includes('=') || this.isTemplateContent(templateBody)) {
                    return templateBody.trim();
                }
            }

            return '';
        }

        isTemplateContent(text) {
            return text.includes('{{') && text.includes('}}');
        }

        updateProvinceProject(projectsContent, province) {
            const result = {
                content: projectsContent,
                added: false
            };

            const provinceRegex = new RegExp(`\\{\\{\\s*${province}(?:专题|專題)\\s*(?:\\||\\}\\})`, 'i');

            if (provinceRegex.test(projectsContent)) {
                result.content = this.ensureProjectImportance(projectsContent, province);
            } else {
                const newProject = `{{${province}专题|importance=Low}}`;
                result.content = projectsContent ?
                    projectsContent.trim() + '\n' + newProject :
                    newProject;
                result.added = true;
            }

            return result;
        }

        ensureProjectImportance(content, provinceName) {
            const templates = this.findTopLevelTemplates(content);
            let result = content;

            templates.forEach(template => {
                const isTargetProject = new RegExp(`\\{\\{\\s*${provinceName}(?:专题|專題)`, 'i')
                    .test(template.content);

                if (isTargetProject && !/\|\s*importance\s*=/i.test(template.content)) {
                    const newContent = template.content.replace(/\}\}$/, '|importance=Low}}');
                    result = result.replace(template.content, newContent);
                }
            });

            return result;
        }

        parseTemplateParams(templateBody) {
            const params = {};

            if (!templateBody || !templateBody.trim()) {
                return params;
            }

            try {
                // 移除开头的 | 如果有
                const content = templateBody.trim().startsWith('|') ?
                    templateBody.trim().substring(1) :
                    templateBody.trim();

                // 按顶层 | 分割
                const parts = this.splitByTopLevelPipe(content);

                let hasNamedParams = false;
                let anonymousContent = '';

                parts.forEach((part) => {
                    const trimmedPart = part.trim();
                    if (!trimmedPart) return;

                    const eqIndex = trimmedPart.indexOf('=');

                    // 检查是否是有效的参数(参数名不应包含模板标记)
                    if (eqIndex > 0) {
                        const paramName = trimmedPart.substring(0, eqIndex).trim();

                        // 检查参数名是否有效(不包含模板标记或特殊字符)
                        if (!paramName.includes('{{') && !paramName.includes('}}') &&
                            !paramName.includes('|') && !paramName.includes('\n')) {
                            // 这是一个具名参数
                            const paramValue = trimmedPart.substring(eqIndex + 1).trim();
                            params[paramName] = paramValue;
                            hasNamedParams = true;
                        } else {
                            // 这看起来像是内容,不是参数
                            if (anonymousContent) {
                                anonymousContent += '\n' + trimmedPart;
                            } else {
                                anonymousContent = trimmedPart;
                            }
                        }
                    } else {
                        // 没有等号,这是匿名内容
                        if (anonymousContent) {
                            anonymousContent += '\n' + trimmedPart;
                        } else {
                            anonymousContent = trimmedPart;
                        }
                    }
                });

                // 如果有匿名内容,将其作为参数1
                if (anonymousContent) {
                    params['1'] = anonymousContent;
                }

                return params;

            } catch (error) {
                return params;
            }
        }

        splitByTopLevelPipe(text) {
            const parts = [];
            let current = '';
            let depth = 0;
            let inLink = false;

            try {
                for (let i = 0; i < text.length; i++) {
                    const char = text[i];
                    const next = text[i + 1];

                    // 处理模板
                    if (char === '{' && next === '{') {
                        depth++;
                        current += char + next;
                        i++;
                    } else if (char === '}' && next === '}') {
                        depth--;
                        current += char + next;
                        i++;
                    }
                    // 处理链接
                    else if (char === '[' && next === '[') {
                        inLink = true;
                        current += char + next;
                        i++;
                    } else if (char === ']' && next === ']') {
                        inLink = false;
                        current += char + next;
                        i++;
                    }
                    // 顶层管道符
                    else if (char === '|' && depth === 0 && !inLink) {
                        parts.push(current);
                        current = '';
                    } else {
                        current += char;
                    }
                }

                if (current) {
                    parts.push(current);
                }

                return parts;

            } catch (error) {
                return [text];
            }
        }

        fillImportance(content) {
            let result = content;
            let count = 0;

            // 查找所有顶层模板
            const templates = this.findTopLevelTemplates(content);

            templates.forEach(template => {
                // 跳过已有importance参数的模板
                if (!/\|\s*importance\s*=/i.test(template.content)) {
                    // 在模板末尾添加importance参数
                    const newContent = template.content.replace(/\}\}$/, '|importance=Low}}');
                    result = result.replace(template.content, newContent);
                    count++;
                }
            });

            return { text: result, count: count };
        }

        findTopLevelTemplates(text) {
            const templates = [];
            let depth = 0;
            let start = -1;

            for (let i = 0; i < text.length - 1; i++) {
                if (text[i] === '{' && text[i + 1] === '{') {
                    if (depth === 0) {
                        start = i;
                    }
                    depth++;
                    i++;
                } else if (text[i] === '}' && text[i + 1] === '}') {
                    depth--;
                    if (depth === 0 && start >= 0) {
                        templates.push({
                            start: start,
                            end: i + 1,
                            content: text.substring(start, i + 2)
                        });
                        start = -1;
                    }
                    i++;
                }
            }

            return templates;
        }

        createWPBS(text, options, result) {
            const params = {
                'class': options.classRating
            };

            if (options.province) {
                params['1'] = `{{${options.province}专题|importance=Low}}`;
                result.addedProvince = options.province;
            }

            const wpbs = this.buildTemplate('WikiProject banner shell', params);

            // 插入到页面顶部
            return wpbs + '\n' + text;
        }

        buildTemplate(name, params) {
            let template = `{{${name}`;

            // 添加class参数
            if (params['class']) {
                template += ` |class=${params['class']}`;
            }

            // 添加其他参数
            for (const [key, value] of Object.entries(params)) {
                if (key !== 'class') {
                    if (key === '1') {
                        template += ` |1=\n${value}\n`;
                    } else {
                        template += ` |${key}=${value}`;
                    }
                }
            }

            template += '}}';
            return template;
        }
    }

    $(document).ready(function () {
        const tool = new CategoryRatingTool();

        if (tool.canRun()) {
            mw.loader.using(['mediawiki.util'], function () {
                tool.addPortletLink();
            });
        }
    });

})();