MediaWiki:BatchUpload.js:修订间差异

来自3D坦克-WIKI

 
无编辑摘要
第13行: 第13行:
         pageTitle: '批量上传',
         pageTitle: '批量上传',
         containerId: 'mw-batch-upload',
         containerId: 'mw-batch-upload',
         maxFiles: 200,      // 本地队列上限,不是服务器限制
         maxFiles: 250,      // 本地队列上限,不是服务器限制
         intervalMs: 500    // 两个文件之间的间隔;上传始终串行
         intervalMs: 500    // 两个文件之间的间隔;上传始终串行
     };
     };

2026年9月11日 (五) 08:57的版本

/*
 * MediaWiki 批量上传页面 v1.0
 * 将本文件全文保存到 MediaWiki:BatchUpload.js(不要加 <script> 标签)。
 * 入口:Project:批量上传;加载片段见 Common.js.snippet.js。
 * 依赖:MediaWiki 自带 mediawiki.api / mediawiki.util / jQuery;现代浏览器。
 * 不使用外部服务、不保存密码、不请求忽略上传警告。
 */
(function () {
    'use strict';

    var CONFIG = {
        namespace: 4,
        pageTitle: '批量上传',
        containerId: 'mw-batch-upload',
        maxFiles: 250,       // 本地队列上限,不是服务器限制
        intervalMs: 500     // 两个文件之间的间隔;上传始终串行
    };

    if (mw.config.get('wgNamespaceNumber') !== CONFIG.namespace ||
        mw.config.get('wgTitle') !== CONFIG.pageTitle ||
        mw.config.get('wgAction') !== 'view' ||
        mw.config.get('wgDiffNewId')) {
        return;
    }

    mw.loader.using(['mediawiki.api', 'mediawiki.util']).then(function () {
        jQuery(function () {
            var root = document.getElementById(CONFIG.containerId);
            if (!root || root.dataset.batchUploadReady) {
                return;
            }
            root.dataset.batchUploadReady = '1';
            mount(root);
        });
    }, function () {
        jQuery(function () {
            var root = document.getElementById(CONFIG.containerId);
            if (root) {
                root.textContent = '无法加载 MediaWiki API 模块。请强制刷新,或联系界面管理员。';
            }
        });
    });

    function mount(root) {
        mw.util.addCSS([
            '#mw-batch-upload{--bu-border:var(--border-color-subtle,#c8ccd1);color:var(--color-base,#202122);font-size:14px;line-height:1.6;margin:1em 0}',
            '#mw-batch-upload *{box-sizing:border-box}',
            '#mw-batch-upload .bu-panel{border:1px solid var(--bu-border);border-radius:8px;padding:20px;background:var(--background-color-base,#fff)}',
            '#mw-batch-upload .bu-heading{font-size:21px;font-weight:700;margin:0 0 6px}',
            '#mw-batch-upload .bu-note{color:var(--color-subtle,#54595d);margin:6px 0 12px;font-size:13px}',
            '#mw-batch-upload .bu-drop{padding:24px 16px;text-align:center;border:2px dashed var(--bu-border);border-radius:6px;margin:16px 0;background:var(--background-color-interactive-subtle,#f8f9fa)}',
            '#mw-batch-upload .bu-drop.bu-over{border-color:#36c;background:#eaf3ff;color:#202122}',
            '#mw-batch-upload .bu-fields{display:grid;grid-template-columns:2fr 1fr;gap:16px;margin-bottom:14px}',
            '#mw-batch-upload label{display:block;font-weight:600}',
            '#mw-batch-upload input[type=text],#mw-batch-upload textarea{width:100%;padding:8px;border:1px solid var(--bu-border);border-radius:4px;color:inherit;background:var(--background-color-base,#fff);font:inherit;font-weight:400}',
            '#mw-batch-upload textarea{min-height:100px;resize:vertical}',
            '#mw-batch-upload .bu-inline{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin:8px 0 16px}',
            '#mw-batch-upload .bu-inline input{flex:1;min-width:140px}',
            '#mw-batch-upload button{padding:7px 13px;border:1px solid var(--bu-border);border-radius:4px;font:inherit;font-weight:600;cursor:pointer;background:var(--background-color-interactive,#eaecf0);color:inherit}',
            '#mw-batch-upload button.bu-primary{background:#36c;border-color:#36c;color:#fff}',
            '#mw-batch-upload button:disabled{opacity:.5;cursor:not-allowed}',
            '#mw-batch-upload button:focus-visible,#mw-batch-upload input:focus-visible,#mw-batch-upload textarea:focus-visible{outline:2px solid #36c;outline-offset:2px}',
            '#mw-batch-upload .bu-toolbar{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}',
            '#mw-batch-upload .bu-message{padding:10px 12px;margin:10px 0;border-left:3px solid #36c;background:var(--background-color-interactive-subtle,#f8f9fa);white-space:pre-wrap;overflow-wrap:anywhere}',
            '#mw-batch-upload .bu-scroll{overflow-x:auto}',
            '#mw-batch-upload table{width:100%;min-width:630px;border-collapse:collapse;margin-top:12px}',
            '#mw-batch-upload th,#mw-batch-upload td{padding:10px 8px;border-bottom:1px solid var(--bu-border);text-align:left;vertical-align:top}',
            '#mw-batch-upload th{font-weight:600;background:var(--background-color-interactive-subtle,#f8f9fa)}',
            '#mw-batch-upload .bu-original{width:23%;overflow-wrap:anywhere}',
            '#mw-batch-upload .bu-target{width:31%}',
            '#mw-batch-upload .bu-result{width:36%;overflow-wrap:anywhere}',
            '#mw-batch-upload .bu-size{font-size:12px;color:var(--color-subtle,#54595d)}',
            '#mw-batch-upload .bu-status{white-space:pre-wrap}',
            '#mw-batch-upload tr[data-state=success] .bu-status{color:var(--color-success,#14866d)}',
            '#mw-batch-upload tr[data-state=warning] .bu-status{color:var(--color-warning,#8b5a00)}',
            '#mw-batch-upload tr[data-state=failed] .bu-status{color:var(--color-error,#b32424)}',
            '#mw-batch-upload progress{display:block;width:100%;height:8px;accent-color:#36c;margin:7px 0}',
            '#mw-batch-upload .bu-empty{text-align:center;color:var(--color-subtle,#54595d);padding:24px}',
            '#mw-batch-upload [hidden]{display:none!important}',
            '@media(max-width:650px){#mw-batch-upload .bu-fields{grid-template-columns:1fr}#mw-batch-upload .bu-panel{padding:14px}}'
        ].join('\n'));

        // HTML 全部为静态字符串。文件名、API 错误、用户输入只以 textContent/value 写入。
        root.innerHTML = `
            <section class="bu-panel" aria-label="批量上传文件">
                <div class="bu-heading">批量上传文件</div>
                <p class="bu-note">上传到当前 Wiki,使用当前账号的权限。遇到警告不自动确认,不主动覆盖已有文件。</p>
                <div class="bu-message" data-el="message" role="status" aria-live="polite">正在检查登录状态和上传权限……</div>
                <div class="bu-drop" data-el="drop">
                    <button type="button" data-el="choose" disabled>选择多个文件</button>
                    <span> 或将文件拖到这里</span>
                    <input type="file" multiple hidden data-el="files" aria-label="选择待上传文件" disabled>
                    <div class="bu-note">可以分多次添加。类型和大小由本站设置决定;本工具不支持目录或分片上传。</div>
                </div>
                <div class="bu-fields">
                    <label>统一文件说明(维基语法)
                        <textarea data-el="description" placeholder="填写来源、作者和授权说明。若使用许可模板,请填写本站已有、且确实适用的模板。" disabled></textarea>
                    </label>
                    <label>分类(可选,每行一个名称)
                        <textarea data-el="categories" placeholder="活动照片&#10;2026年资料" disabled></textarea>
                    </label>
                </div>
                <label for="bu-prefix">文件名前缀(可选)</label>
                <div class="bu-inline">
                    <input id="bu-prefix" data-el="prefix" type="text" placeholder="例如:项目A-" disabled>
                    <button type="button" data-el="applyPrefix" disabled>应用到未成功项</button>
                </div>
                <p class="bu-note">应用前缀会按“前缀 + 原始文件名”重建未成功项的名称,替换手工改名;不会重复叠加前缀。请保留正确的扩展名。</p>
                <div class="bu-toolbar">
                    <button type="button" class="bu-primary" data-el="start" disabled>开始上传</button>
                    <button type="button" data-el="pause" disabled>本项完成后暂停</button>
                    <button type="button" data-el="retry" disabled>重试失败 / 警告项</button>
                    <button type="button" data-el="clear" disabled>清空列表</button>
                </div>
                <div data-el="summary" role="status" aria-live="polite">尚未选择文件</div>
                <progress data-el="overall" max="1" value="0" aria-label="处理进度(按文件数量)"></progress>
                <div class="bu-scroll">
                    <table>
                        <thead><tr><th scope="col">本地文件</th><th scope="col">目标文件名</th><th scope="col">状态 / 传输进度</th><th scope="col">操作</th></tr></thead>
                        <tbody data-el="rows"></tbody>
                    </table>
                    <div class="bu-empty" data-el="empty">选择文件后,会在这里显示上传队列。</div>
                </div>
                <p class="bu-note">进度条表示传输或已处理数量,不代表上传成功;以“成功”状态为准。暂停不会中断正在处理的文件。清空列表不会删除站内文件。</p>
            </section>`;

        var ui = {};
        root.querySelectorAll('[data-el]').forEach(function (el) { ui[el.dataset.el] = el; });
        var items = [];
        var busy = false;
        var pauseRequested = false;
        var ready = false;
        var userName = mw.config.get('wgUserName');
        var api;
        var fileNamespace = (mw.config.get('wgFormattedNamespaces') || {})[6] || 'File';
        var categoryNamespace = (mw.config.get('wgFormattedNamespaces') || {})[14] || 'Category';

        function message(text) { ui.message.textContent = text; }
        function fileUrl(name) { return mw.util.getUrl(fileNamespace + ':' + name); }
        function sleep(ms) { return new Promise(function (resolve) { window.setTimeout(resolve, ms); }); }
        function sizeText(bytes) {
            if (bytes < 1024) { return bytes + ' B'; }
            if (bytes < 1048576) { return (bytes / 1024).toFixed(1) + ' KiB'; }
            return (bytes / 1048576).toFixed(1) + ' MiB';
        }
        function counts() {
            var result = { queued: 0, uploading: 0, success: 0, warning: 0, failed: 0 };
            items.forEach(function (item) { result[item.state]++; });
            return result;
        }
        function refresh() {
            var c = counts();
            var locked = busy || !ready;
            ['choose', 'files', 'description', 'categories', 'prefix'].forEach(function (key) {
                ui[key].disabled = locked;
            });
            ui.applyPrefix.disabled = locked || !items.some(function (item) { return item.state !== 'success'; });
            ui.start.disabled = locked || !c.queued;
            ui.pause.disabled = !busy || pauseRequested;
            ui.retry.disabled = locked || !(c.failed + c.warning);
            ui.clear.disabled = busy || !items.length;
            ui.empty.hidden = items.length > 0;
            ui.overall.max = Math.max(items.length, 1);
            ui.overall.value = c.success + c.warning + c.failed;
            ui.summary.textContent = items.length ?
                '共 ' + items.length + ' 项 · 成功 ' + c.success + ' · 警告 ' + c.warning +
                ' · 失败 ' + c.failed + ' · 待处理 ' + c.queued + (busy ? ' · 正在处理' : '') : '尚未选择文件';
            items.forEach(function (item) {
                item.input.disabled = locked || item.state === 'success';
                item.remove.disabled = busy;
            });
        }
        function setState(item, state, text) {
            item.state = state;
            item.row.dataset.state = state;
            item.status.textContent = text;
            item.progress.hidden = state !== 'uploading' && state !== 'success';
            if (state === 'queued') { item.progress.value = 0; }
            if (state === 'success') { item.progress.value = 100; }
            item.link.hidden = state !== 'success' && state !== 'warning' && state !== 'failed';
            item.link.href = fileUrl(item.input.value.trim());
            item.link.textContent = state === 'success' ? '查看文件' : '检查目标页面';
            refresh();
        }
        function resetItem(item) {
            if (item.state !== 'success') { setState(item, 'queued', '等待上传'); }
        }
        function addFiles(files) {
            if (busy || !ready) { return; }
            var added = 0;
            var excess = 0;
            Array.from(files).forEach(function (file) {
                if (items.length >= CONFIG.maxFiles) { excess++; return; }
                var tr = document.createElement('tr');
                tr.dataset.state = 'queued';
                tr.innerHTML = '<td class="bu-original"><div class="bu-name"></div><div class="bu-size"></div></td>' +
                    '<td class="bu-target"><input type="text"></td>' +
                    '<td class="bu-result"><div class="bu-status"></div><progress max="100" value="0" hidden></progress><a target="_blank" rel="noopener noreferrer" hidden></a></td>' +
                    '<td><button type="button" title="只移出队列,不删除站内文件">移除</button></td>';
                tr.querySelector('.bu-name').textContent = file.name;
                tr.querySelector('.bu-size').textContent = sizeText(file.size);
                var item = {
                    file: file, state: 'queued', row: tr,
                    input: tr.querySelector('input'), status: tr.querySelector('.bu-status'),
                    progress: tr.querySelector('progress'), link: tr.querySelector('a'),
                    remove: tr.querySelector('button')
                };
                item.input.value = ui.prefix.value + file.name;
                item.input.setAttribute('aria-label', file.name + ' 的目标文件名');
                item.progress.setAttribute('aria-label', file.name + ' 的传输进度');
                item.status.textContent = '等待上传';
                item.input.addEventListener('input', function () { resetItem(item); });
                item.remove.addEventListener('click', function () {
                    if (busy) { return; }
                    items = items.filter(function (other) { return other !== item; });
                    tr.remove();
                    refresh();
                });
                items.push(item);
                ui.rows.appendChild(tr);
                added++;
            });
            message('已添加 ' + added + ' 个文件。请检查文件名、来源和授权说明后点击“开始上传”。' +
                (excess ? '\n另有 ' + excess + ' 个文件未添加:本地队列上限为 ' + CONFIG.maxFiles + ' 个。' : ''));
            refresh();
        }

        // 将 mw.Api 的多参数拒绝转换为 Error;不丢失服务器的错误信息。
        function apiError(code, data) {
            var raw = data && (data.error || (data.errors && data.errors[0]));
            var text = raw && (raw.info || raw.text || raw['*']);
            var status = data && data.xhr && data.xhr.status;
            if (code === 'http') {
                text = '网络 / HTTP 异常' + (status ? '(HTTP ' + status + ')' : '') +
                    '。若上传请求已发出,结果可能未确认;请先检查目标页面,再决定是否重试。';
            }
            var error = new Error(text || String(code || '未知 API 错误'));
            error.code = String(code || 'unknown');
            error.stopQueue = code === 'http' || /assert|badtoken|notloggedin|read.?only|upload.*disabled|ratelimited|maxlag|permissiondenied|blocked/i.test(error.code);
            return error;
        }
        function request(jqPromise) {
            return new Promise(function (resolve, reject) {
                jqPromise.done(resolve).fail(function (code, data) { reject(apiError(code, data)); });
            });
        }
        function warningError(text) {
            var error = new Error(text);
            error.isWarning = true;
            return error;
        }
        function warningText(warnings) {
            var labels = {
                exists: '同名文件已存在',
                'exists-normalized': '规范化后的文件名已存在',
                'page-exists': '同名文件说明页已存在',
                duplicate: '相同内容的文件已存在',
                'no-change': '内容与现有文件一致',
                duplicateversions: '内容与已有历史版本一致',
                'was-deleted': '这个文件名曾被删除',
                'duplicate-archive': '相同内容的文件曾被删除',
                badfilename: '文件名需要调整',
                'bad-prefix': '文件名前缀不符合站点要求',
                'large-file': '文件大小触发站点警告'
            };
            return Object.keys(warnings || {}).map(function (key) {
                var value = warnings[key];
                var detail = typeof value === 'string' ? value : JSON.stringify(value);
                return (labels[key] || '站点警告') + ' [' + key + ']' + (detail ? ':' + detail.slice(0, 500) : '');
            }).join('\n') || '服务器返回上传警告。';
        }
        function pageText() {
            var categories = [];
            ui.categories.value.split(/\r?\n/).forEach(function (line) {
                var name = line.trim();
                if (!name) { return; }
                [categoryNamespace, 'Category', '分类', '分類'].forEach(function (ns) {
                    if (name.slice(0, ns.length + 1).toLowerCase() === (ns + ':').toLowerCase()) {
                        name = name.slice(ns.length + 1).trim();
                    }
                });
                if (!name || /[\[\]{}|<>#\u0000-\u001f\u007f]/.test(name)) {
                    throw new Error('分类请每行只填一个名称,不要填写 [[Category:…]] 等维基代码。');
                }
                if (categories.indexOf(name) === -1) { categories.push(name); }
            });
            var text = ui.description.value.trim();
            if (categories.length) {
                text += (text ? '\n\n' : '') + categories.map(function (name) {
                    return '[[' + categoryNamespace + ':' + name + ']]';
                }).join('\n');
            }
            return text;
        }
        async function uploadOne(item, text) {
            var name = item.input.value.trim();
            if (!name || /[\\/:#<>\[\]|{}\u0000-\u001f\u007f]/.test(name) || !/\.[^.\s]+$/.test(name)) {
                throw new Error('目标名称需包含扩展名,且不能包含路径、命名空间或非法字符(如 / : # [ ])。');
            }
            if (!item.file.size) {
                throw new Error('文件为空,或浏览器无法读取。请使用文件选择器选择普通文件,不要拖入文件夹。');
            }
            item.input.value = name;
            setState(item, 'uploading', '正在检查目标文件名……');
            item.progress.removeAttribute('value');

            // 同时检查本地文件说明页及共享仓库文件;避免无意遮蔽共享文件。
            var check = await request(api.get({
                action: 'query', titles: fileNamespace + ':' + name,
                prop: 'imageinfo', iiprop: 'timestamp'
            }));
            var page = check.query && check.query.pages && check.query.pages[0];
            if (!page || page.invalid || page.ns !== 6) {
                throw new Error('服务器不接受这个文件名,或未能返回有效的文件页面信息。');
            }
            if (!page.missing || (page.imageinfo && page.imageinfo.length) ||
                (page.imagerepository && page.imagerepository !== '')) {
                throw warningError('目标文件或文件说明页已存在(可能来自共享仓库)。未发送文件;请改名或使用已有文件。');
            }
            // 使用服务器规范化的首字母、空格与名称。
            name = page.title.slice(page.title.indexOf(':') + 1);
            item.input.value = name;
            item.progress.value = 0;
            item.status.textContent = '正在获取令牌并上传……';

            var result = await request(api.postWithToken('csrf', {
                action: 'upload', filename: name, file: item.file,
                text: text, comment: '使用批量上传页面上传', watchlist: 'nochange'
                // 刻意不传 ignorewarnings,也不根据 filekey 自动确认警告。
            }, {
                contentType: 'multipart/form-data', // mw.Api 会构造 FormData 并设置 boundary
                timeout: 0,
                xhr: function () {
                    var xhr = jQuery.ajaxSettings.xhr();
                    if (xhr.upload) {
                        xhr.upload.addEventListener('progress', function (event) {
                            if (event.lengthComputable) {
                                var percent = Math.min(100, Math.round(event.loaded / event.total * 100));
                                item.progress.value = percent;
                                item.status.textContent = percent < 100 ? '正在传输 ' + percent + '%' : '传输完成,等待服务器确认……';
                            }
                        });
                    }
                    return xhr;
                }
            }));
            var upload = result && result.upload;
            // 只以 API 的 Success 为成功;HTTP 200 或传输到 100% 都不是成功确认。
            if (upload && upload.result === 'Success') {
                item.input.value = upload.filename || name;
                setState(item, 'success', '成功');
                return;
            }
            if (upload && (upload.result === 'Warning' || upload.warnings)) {
                throw warningError(warningText(upload.warnings) + '\n本工具没有确认警告,未完成正式发布。');
            }
            var unknown = new Error('服务器返回了未识别的上传结果。请先检查目标文件页面,不要直接重复上传。');
            unknown.stopQueue = true;
            throw unknown;
        }
        async function run() {
            if (busy || !ready || !items.some(function (item) { return item.state === 'queued'; })) { return; }
            var text;
            try { text = pageText(); }
            catch (error) { message(error.message); return; }
            busy = true;
            pauseRequested = false;
            refresh();
            message('正在逐个上传。请保持页面打开;“本项完成后暂停”不会中断已发出的请求。');
            try {
                for (var i = 0; i < items.length; i++) {
                    if (pauseRequested) { break; }
                    var item = items[i];
                    if (item.state !== 'queued') { continue; }
                    try {
                        await uploadOne(item, text);
                    } catch (error) {
                        setState(item, error.isWarning ? 'warning' : 'failed',
                            (error.message || String(error)) + (error.code ? ' [' + error.code + ']' : ''));
                        if (error.stopQueue) { pauseRequested = true; }
                    }
                    if (!pauseRequested && i < items.length - 1) { await sleep(CONFIG.intervalMs); }
                }
            } finally {
                busy = false;
                refresh();
                message(pauseRequested ?
                    '队列已暂停。当前请求已返回,未处理项仍在队列中。请检查错误或账号状态,再点击“开始上传”继续;失败项需手动重试。' :
                    '本轮处理结束。请检查各项状态;成功项不会被本工具再次上传。警告项未自动确认,失败项可在排查后重试。');
            }
        }

        ui.choose.addEventListener('click', function () { ui.files.click(); });
        ui.files.addEventListener('change', function () { addFiles(ui.files.files); ui.files.value = ''; });
        ['dragenter', 'dragover'].forEach(function (type) {
            ui.drop.addEventListener(type, function (event) {
                event.preventDefault();
                if (ready && !busy) { ui.drop.classList.add('bu-over'); }
            });
        });
        ui.drop.addEventListener('dragleave', function (event) {
            if (!ui.drop.contains(event.relatedTarget)) { ui.drop.classList.remove('bu-over'); }
        });
        ui.drop.addEventListener('drop', function (event) {
            event.preventDefault();
            ui.drop.classList.remove('bu-over');
            if (event.dataTransfer) { addFiles(event.dataTransfer.files); }
        });
        ui.applyPrefix.addEventListener('click', function () {
            if (busy || !ready) { return; }
            items.forEach(function (item) {
                if (item.state !== 'success') { item.input.value = ui.prefix.value + item.file.name; resetItem(item); }
            });
            message('已按“前缀 + 原始文件名”更新未成功项。请检查后再开始上传。');
        });
        ui.start.addEventListener('click', function () { run(); });
        ui.pause.addEventListener('click', function () {
            pauseRequested = true;
            message('将在当前文件处理完成后暂停;当前请求不会中止。');
            refresh();
        });
        ui.retry.addEventListener('click', function () {
            if (busy || !ready) { return; }
            items.forEach(function (item) {
                if (item.state === 'failed' || item.state === 'warning') { resetItem(item); }
            });
            run();
        });
        ui.clear.addEventListener('click', function () {
            if (busy) { return; }
            items = [];
            ui.rows.textContent = '';
            message('已清空本地列表。此前上传到本站的文件不会被删除。');
            refresh();
        });
        window.addEventListener('beforeunload', function (event) {
            if (busy || items.some(function (item) { return item.state !== 'success'; })) {
                event.preventDefault();
                event.returnValue = ''; // 浏览器显示自己的离开页面提示文字
            }
        });

        if (!window.File || !window.FormData || !window.Promise) {
            message('浏览器不支持所需的文件上传功能,请使用较新的 Chrome、Firefox、Edge 或 Safari。');
            return;
        }
        if (!userName) {
            message('请先登录,再打开此页面。本工具不提供匿名上传。');
            var login = document.createElement('a');
            login.href = mw.util.getUrl('Special:UserLogin', { returnto: mw.config.get('wgPageName').replace(/_/g, ' ') });
            login.textContent = ' 前往登录';
            ui.message.appendChild(login);
            return;
        }
        api = new mw.Api({ parameters: {
            formatversion: 2, errorformat: 'plaintext',
            assert: 'user', assertuser: userName, maxlag: 5
        } });
        request(api.get({ action: 'query', meta: 'userinfo', uiprop: 'rights' })).then(function (data) {
            var info = data.query && data.query.userinfo;
            if (!info || !Array.isArray(info.rights)) {
                throw new Error('未能确认账号的上传权限,请刷新或联系管理员。');
            }
            if (info.rights.indexOf('upload') === -1) {
                message('当前账号没有 upload 权限。界面脚本不能授予上传权限,请联系站点管理员。');
                return;
            }
            ready = true;
            message('当前账号:' + userName + '。请选择文件。建议先用 2~3 个小文件测试。');
            refresh();
        }).catch(function (error) {
            message('初始化失败:' + error.message + '。请检查登录状态或 API 是否可用,再刷新页面。');
        });
    }
}());
报告 Wiki 中的错误
请不要使用此表单申诉封号或封锁处罚。此表单仅用于反馈与 Wiki 相关的错误或改进建议。
您的游戏昵称
页面链接
我们已为您自动填写此项,如有误,请从浏览器地址栏复制所需页面的链接并粘贴至此处。
留言内容
请告诉我们您希望修改该页面的哪些内容,或指出其中的错误/不准确之处。可附带截图链接。