跳转到内容

User:魔琴/gadgets/quickredirectplus/index.js

维基百科,自由的百科全书
注意:保存之后,你必须清除浏览器缓存才能看到做出的更改。Google ChromeFirefoxMicrosoft EdgeSafari:按住⇧ Shift键并单击工具栏的“刷新”按钮。参阅Help:绕过浏览器缓存以获取更多帮助。
/* <nowiki>
 * QR+ 快速创建重定向
 * 此脚本由 ChatGPT 生成
 * License: CC0 1.0 Universal
 * <https://creativecommons.org/publicdomain/zero/1.0/deed.en>
 */

(function () {
  var api = new mw.Api();

  function RedirectDialog(config) {
    RedirectDialog.parent.call(this, config);
  }
  OO.inheritClass(RedirectDialog, OO.ui.ProcessDialog);

  RedirectDialog.static.name = 'redirectDialog';
  RedirectDialog.static.title = 'QR+快速创建重定向页面';
  RedirectDialog.static.actions = [
    { action: 'cancel', label: '取消', flags: 'safe' },
    { action: 'create', label: '创建', flags: ['primary', 'progressive'] }
  ];
  RedirectDialog.static.size = 'large';

  RedirectDialog.prototype.initialize = function () {
    RedirectDialog.parent.prototype.initialize.call(this);
    var panel = new OO.ui.PanelLayout({ padded: true, expanded: false });

    this.targetInput = new OO.ui.MultilineTextInputWidget({ placeholder: '每行一个目标页面名(回车分隔)', rows: 5 });
    this.targetInput.$input.attr('rows', 5).css('min-height', '7.5em');

    this.sectionInput = new OO.ui.TextInputWidget();
    this.templateInput = new OO.ui.TextInputWidget({ placeholder: '不需「重定向」三字。每类用空格分开,如错字 繁简 模板' });
    this.summaryInput = new OO.ui.TextInputWidget();

    this.templateHint = $('<div>').addClass('qrp-template-hints').css({ 'margin-top': '0.5em', 'font-size': '90%', 'white-space': 'normal' });
    this.targetHint = $('<div>').addClass('qrp-target-hints').css({ 'margin-top': '0.5em', 'font-size': '90%', 'white-space': 'normal' });

    var apiCheck = new mw.Api();

    var templateField = new OO.ui.FieldLayout(this.templateInput, { label: '重定向分类(可选)', align: 'top' });
    var targetField = new OO.ui.FieldLayout(this.targetInput, { label: '将哪个页面重定向到此?(每行一个)', align: 'top' });

    var fieldset = new OO.ui.FieldsetLayout({ label: '重定向设置' });
    fieldset.addItems([
      targetField,
      new OO.ui.FieldLayout(this.sectionInput, { label: '到哪个章节?(可选)', align: 'top' }),
      templateField,
      new OO.ui.FieldLayout(this.summaryInput, { label: '编辑摘要(可选)', align: 'top' })
    ]);

    templateField.$element.append(this.templateHint);
    targetField.$element.append(this.targetHint);

    // 状态与顺序
    this._qrp_templateStatusMap = this._qrp_templateStatusMap || {};
    this._qrp_templateItems = this._qrp_templateItems || [];
    this._qrp_targetStatusMap = this._qrp_targetStatusMap || {};
    this._qrp_targetItems = this._qrp_targetItems || [];

    // IME flags
    this._qrp_templateComposing = false;
    this._qrp_targetComposing = false;
    var makeBox = function (name, state, options) {
      options = options || {};
      var kind = options.kind || 'template';
      var pageTitle = options.pageTitle || name;

      var colors = {
        template: {
          exists: { border: 'green', color: 'green' },
          missing: { border: 'red', color: 'red' },
          fail: { border: 'red', color: 'red' },
          pending: { border: '#999', color: '#666', opacity: 0.9 }
        },
        target: {
          exists: { border: 'red', color: 'red' },
          missing: { border: 'green', color: 'green' },
          fail: { border: 'red', color: 'red' },
          pending: { border: '#999', color: '#666', opacity: 0.9 }
        }
      };
      var style = colors[kind] && colors[kind][state] ? colors[kind][state] : { border: '#ccc', color: '#333' };

      var href = mw.util.getUrl(pageTitle);
      var $a = $('<a>').attr({ href: href, target: '_blank', rel: 'noopener noreferrer' }).css({ 'text-decoration': 'none' });

      var $s = $('<span>').text(name).css({ 'display': 'inline-block', 'border': '1px solid ' + style.border, 'border-radius': '4px', 'padding': '2px 6px', 'margin': '2px', 'font-size': '90%', 'background-color': '#fff', 'color': style.color, 'cursor': 'pointer' });
      if (style.opacity) $s.css('opacity', style.opacity);
      if (state === 'pending') $s.append($('<span>').text(' …').css({ 'font-weight': 'bold', 'color': style.color }));
      $a.append($s);
      return $a;
    };

    var renderTemplates = function () {
      if (this._qrp_templateComposing) return; // 在 IME 输入时跳过渲染
      var existList = [], missingList = [], invalidList = [], failList = []; 
      for (var i = 0; i < this._qrp_templateItems.length; i++) {
        var it = this._qrp_templateItems[i];
        var st = this._qrp_templateStatusMap[it];
        if (st === 'exists') existList.push(it);
        else if (st === 'missing') missingList.push(it);
        else if (st === 'invalid') invalidList.push(it);
        else if (st === 'fail') failList.push(it);
      }
      this.templateHint.empty();
      if (existList.length) {
        var $row = $('<div>').css({ 'margin-bottom': '0.25em' }).append($('<strong>').text('可用:'));
        for (var j = 0; j < existList.length; j++) $row.append(makeBox(existList[j], 'exists', { kind: 'template', pageTitle: 'Template:' + existList[j] + '重定向' }));
        this.templateHint.append($row);
      }
      if (missingList.length) {
        var $row2 = $('<div>').css({ 'margin-bottom': '0.25em' }).append($('<strong>').text('错误:模板不存在!'));
        for (var k = 0; k < missingList.length; k++) $row2.append(makeBox(missingList[k], 'missing', { kind: 'template', pageTitle: 'Template:' + missingList[k] + '重定向' }));
        this.templateHint.append($row2);
      }
      if (invalidList.length) {
        var $rowInvalid = $('<div>').css({ 'margin-bottom': '0.25em' }).append($('<strong>').text('标题无效:'));
        for (var m = 0; m < invalidList.length; m++) $rowInvalid.append(makeBox(invalidList[m], 'fail', { kind: 'template', pageTitle: 'Template:' + invalidList[m] + '重定向' }));
        this.templateHint.append($rowInvalid);
      }
      if (failList.length) {
        var $row3 = $('<div>').css({ 'margin-bottom': '0.25em' }).append($('<strong>').text('查询失败: '));
        for (var l = 0; l < failList.length; l++) $row3.append(makeBox(failList[l], 'fail', { kind: 'template', pageTitle: 'Template:' + failList[l] + '重定向' }));
        this.templateHint.append($row3);
      }
      var pendingList = [];
      for (var p = 0; p < this._qrp_templateItems.length; p++) { var itp = this._qrp_templateItems[p]; if (this._qrp_templateStatusMap[itp] === 'pending') pendingList.push(itp); }
      if (pendingList.length) { var $row4 = $('<div>').css({ 'margin-bottom': '0.25em' }).append($('<strong>').text('查询中:')); pendingList.forEach(function (itm) { $row4.append(makeBox(itm, 'pending', { kind: 'template', pageTitle: 'Template:' + itm + '重定向' })); }); this.templateHint.append($row4); }
      try { if (typeof this.updateSize === 'function') this.updateSize(); } catch (e) { }
      try { this.$body.css({ 'overflow': 'auto' }); } catch (e) { }
    }.bind(this);

    var renderTargets = function () {
      if (this._qrp_targetComposing) return;
      var existList = [], missingList = [], invalidList = [], failList = []; 
      for (var i = 0; i < this._qrp_targetItems.length; i++) {
        var it = this._qrp_targetItems[i];
        var st = this._qrp_targetStatusMap[it];
        if (st === 'exists') existList.push(it);
        else if (st === 'missing') missingList.push(it);
        else if (st === 'invalid') invalidList.push(it);
        else if (st === 'fail') failList.push(it);
      }
      this.targetHint.empty();
      if (missingList.length) {
        var $row = $('<div>').css({ 'margin-bottom': '0.25em' }).append($('<strong>').text('可创建:'));
        for (var j = 0; j < missingList.length; j++) $row.append(makeBox(missingList[j], 'missing', { kind: 'target', pageTitle: missingList[j] }));
        this.targetHint.append($row);
      }
      if (invalidList.length) {
        var $rowInvalid = $('<div>').css({ 'margin-bottom': '0.25em' }).append($('<strong>').text('标题无效:'));
        for (var m2 = 0; m2 < invalidList.length; m2++) $rowInvalid.append(makeBox(invalidList[m2], 'fail', { kind: 'target', pageTitle: invalidList[m2] }));
        this.targetHint.append($rowInvalid);
      }
      if (existList.length) { var $row2 = $('<div>').css({ 'margin-bottom': '0.25em' }).append($('<strong>').text('错误:目标页面已存在!')); for (var k = 0; k < existList.length; k++) $row2.append(makeBox(existList[k], 'exists', { kind: 'target', pageTitle: existList[k] })); this.targetHint.append($row2); }
      if (failList.length) { var $row3 = $('<div>').css({ 'margin-bottom': '0.25em' }).append($('<strong>').text('查询失败:')); for (var l = 0; l < failList.length; l++) $row3.append(makeBox(failList[l], 'fail', { kind: 'target', pageTitle: failList[l] })); this.targetHint.append($row3); }
      var pendingList = [];
      for (var p = 0; p < this._qrp_targetItems.length; p++) { var itp = this._qrp_targetItems[p]; if (this._qrp_targetStatusMap[itp] === 'pending') pendingList.push(itp); }
      if (pendingList.length) { var $row4 = $('<div>').css({ 'margin-bottom': '0.25em' }).append($('<strong>').text('查询中:')); pendingList.forEach(function (itm) { $row4.append(makeBox(itm, 'pending', { kind: 'target', pageTitle: itm })); }); this.targetHint.append($row4); }
      try { if (typeof this.updateSize === 'function') this.updateSize(); } catch (e) { }
      try { this.$body.css({ 'overflow': 'auto' }); } catch (e) { }
    }.bind(this);

    // composition handlers:在 IME 输入时跳过渲染与查询
    this.templateInput.$input.on('compositionstart.qrp', function () { this._qrp_templateComposing = true; }.bind(this));
    this.templateInput.$input.on('compositionend.qrp', function () { this._qrp_templateComposing = false; this.templateInput.$input.trigger('input.qrp'); }.bind(this));
    this.targetInput.$input.on('compositionstart.qrp', function () { this._qrp_targetComposing = true; }.bind(this));
    this.targetInput.$input.on('compositionend.qrp', function () { this._qrp_targetComposing = false; this.targetInput.$input.trigger('input.qrp'); }.bind(this));

    // helper: 使用 API 返回的 converted / normalized 信息将原始请求 title 映射到返回的页面
    function handleQueryResponse(requestTitles, data, mapResultFn) {
      requestTitles = requestTitles || [];
      var pages = (data && data.query && data.query.pages) ? data.query.pages : {};
      var converted = (data && data.query && data.query.converted) ? data.query.converted : [];
      var normalized = (data && data.query && data.query.normalized) ? data.query.normalized : [];
      var interwiki = (data && data.query && data.query.interwiki) ? data.query.interwiki : [];

      // 构造转换 map: fromOriginal -> toConverted
      var convMap = {};
      converted.forEach(function (c) { convMap[c.from] = c.to; });
      // normalized map: from -> to
      var normMap = {};
      normalized.forEach(function (n) { normMap[n.from] = n.to; });

      // interwiki 集合(以 API 返回的 title 为键)
      var interwikiSet = {};
      interwiki.forEach(function (iw) { if (iw && iw.title) interwikiSet[iw.title] = true; });

      // 将 pages 转成 title -> page 对象映射(方便查找)
      var titleToPage = {};
      Object.keys(pages).forEach(function (pid) { var pg = pages[pid]; if (pg && pg.title) titleToPage[pg.title] = pg; });

      // 对每个请求 title,决定它对应的 page(如果有)或是否为 invalid/interwiki
      requestTitles.forEach(function (req) {
        var looked = convMap[req] || normMap[req] || req; // 先用 converted 再用 normalized
        var pg = titleToPage[looked];
        if (!pg) {
          // 作为兜底,尝试把下划线/空格标准化
          var alt = looked.replace(/_/g, ' ');
          pg = titleToPage[alt] || titleToPage[alt.replace(/ /g, '_')];
        }
        if (pg) {
          // API 明确返回了 page 对象
          if (pg.invalid) {
            // 标为 invalid
            mapResultFn(req, { invalid: true });
          } else {
            // 返回原始 page 对象,调用方会根据 pg.missing 判断 exists/missing
            mapResultFn(req, pg);
          }
        } else {
          // 没有返回对应 page,检查 interwiki
          if (interwikiSet[req] || interwikiSet[convMap[req]] || interwikiSet[normMap[req]] || interwikiSet[looked]) {
            // 把 interwiki 当作 invalid(按需求显示为“标题无效”)
            mapResultFn(req, { invalid: true });
          } else {
            // 没有匹配,视为 missing
            mapResultFn(req, undefined);
          }
        }
      });

    }

    // template input handler
    this.templateInput.$input.off('input.qrp').on('input.qrp', (function () {
      var raw = this.templateInput.getValue().trim();
      var newItems = raw ? raw.split(/\s+/).filter(Boolean) : [];
      var removed = this._qrp_templateItems.filter(function (it) { return newItems.indexOf(it) === -1; });
      removed.forEach(function (it) { delete this._qrp_templateStatusMap[it]; }, this);
      this._qrp_templateItems = newItems.slice();
      var toQuery = [];
      newItems.forEach(function (item) { if (!this._qrp_templateStatusMap.hasOwnProperty(item) || this._qrp_templateStatusMap[item] === 'pending') { this._qrp_templateStatusMap[item] = 'pending'; toQuery.push(item); } }, this);
      if (this._qrp_templateComposing) return; // 正在 IME 输入,延后处理
      renderTemplates();
      if (toQuery.length > 0) {
        // 为每个 item 构造请求标题(Template:NAME重定向)并保存映射
        var requestTitles = toQuery.map(function (it) { return 'Template:' + it + '重定向'; });
        var requestMap = {};
        toQuery.forEach(function (it, idx) { requestMap[requestTitles[idx]] = it; });

        apiCheck.get({ action: 'query', titles: requestTitles, converttitles: 1 })
          .done(function (data) {
            // 使用 helper 将响应映射回原始请求
            handleQueryResponse(requestTitles, data, function (reqTitle, pg) {
              var itemName = requestMap[reqTitle];
              if (pg && pg.invalid) {
                // invalid 或 interwiki
                this._qrp_templateStatusMap[itemName] = 'invalid';
                return;
              }
              if (pg === undefined) {
                // 未返回对应 page,则视为 missing
                this._qrp_templateStatusMap[itemName] = 'missing';
                return;
              }
              var exists = (pg.missing === undefined);
              this._qrp_templateStatusMap[itemName] = exists ? 'exists' : 'missing';
            }.bind(this));
            // 对仍处于 pending 的条目标为 missing
            toQuery.forEach(function (it) { if (this._qrp_templateStatusMap[it] === 'pending') this._qrp_templateStatusMap[it] = 'missing'; }, this);
            renderTemplates();
          }.bind(this)).fail(function () { toQuery.forEach(function (it) { this._qrp_templateStatusMap[it] = 'fail'; }, this); renderTemplates(); }.bind(this));
      }
    }).bind(this));

    // target input handler
    this.targetInput.$input.off('input.qrp').on('input.qrp', (function () {
      var raw = this.targetInput.getValue();
      var lines = raw.split(/\r?\n/).map(function (s) { return s.trim(); }).filter(Boolean);
      var newItems = lines;
      var removed = this._qrp_targetItems.filter(function (it) { return newItems.indexOf(it) === -1; });
      removed.forEach(function (it) { delete this._qrp_targetStatusMap[it]; }, this);
      this._qrp_targetItems = newItems.slice();
      var toQuery = [];
      newItems.forEach(function (item) { if (!this._qrp_targetStatusMap.hasOwnProperty(item) || this._qrp_targetStatusMap[item] === 'pending') { this._qrp_targetStatusMap[item] = 'pending'; toQuery.push(item); } }, this);
      if (this._qrp_targetComposing) return;
      renderTargets();
      if (toQuery.length > 0) {
        var requestTitles = toQuery.slice(); // 页面名原样请求
        var requestMap = {};
        toQuery.forEach(function (it, idx) { requestMap[requestTitles[idx]] = it; });

        apiCheck.get({ action: 'query', titles: requestTitles, converttitles: 1 })
          .done(function (data) {
            handleQueryResponse(requestTitles, data, function (reqTitle, pg) {
              var itemName = requestMap[reqTitle];
              if (pg === undefined) {
                // 未返回对应 page,则视为 missing
                this._qrp_targetStatusMap[itemName] = 'missing';
                return;
              }
              // 如果被标为 invalid(bad title / interwiki),视为不可创建(失败)
              if (pg && pg.invalid) { this._qrp_targetStatusMap[itemName] = 'invalid'; return; }

              var exists = (pg.missing === undefined);
              this._qrp_targetStatusMap[itemName] = exists ? 'exists' : 'missing';
            }.bind(this));
            toQuery.forEach(function (it) { if (this._qrp_targetStatusMap[it] === 'pending') this._qrp_targetStatusMap[it] = 'missing'; }, this);
            renderTargets();
          }.bind(this)).fail(function () { toQuery.forEach(function (it) { this._qrp_targetStatusMap[it] = 'fail'; }, this); renderTargets(); }.bind(this));
      }
    }).bind(this));

    panel.$element.append(fieldset.$element);
    this.$body.append(panel.$element);

    try { this.$body.css({ 'max-height': '60vh', 'overflow': 'auto' }); } catch (e) { }
  };

  RedirectDialog.prototype.getActionProcess = function (action) {
    var dialog = this;
    if (action === 'cancel') { return new OO.ui.Process(function () { dialog.close(); }); }
    if (action === 'create') {
      return new OO.ui.Process().next(function () {
        var rawTitles = dialog.targetInput.getValue();
        var titles = rawTitles.split(/\r?\n/).map(function (s) { return s.trim(); }).filter(Boolean);
        if (!titles.length) { mw.notify('请填写至少一个目标页面名称(每行一个)。', { type: 'error' }); return; }
        var current = mw.config.get('wgPageName').replace(/_/g, ' ');
        var section = dialog.sectionInput.getValue().trim(); current = section ? current + '#' + section : current;
        var raw = dialog.templateInput.getValue().trim();
        // 过滤模板:如果模板被检测为 invalid(在 statusMap 中为 'invalid'),则跳过该模板并提示
        var rawForUse = '';
        if (raw) {
          var tplItemsTmp = raw.split(/\s+/).filter(Boolean);
          var usedTplsTmp = [];
          tplItemsTmp.forEach(function (it) {
            if (dialog._qrp_templateStatusMap[it] === 'fail' || dialog._qrp_templateStatusMap[it] === 'invalid') {
              mw.notify('跳过不可用的模板:' + it, { type: 'error' });
            } else {
              usedTplsTmp.push(it);
            }
          });
          rawForUse = usedTplsTmp.join(' ');
        }
        raw = rawForUse; // 接下来脚本内部关于 raw 的处理会使用过滤后的值,避免把不可用模板写入页面
        var summary = dialog.summaryInput.getValue().trim(); summary = summary ? summary + ' // ' : summary;
        var allowedTitles = [];
        var skipped = [];
        titles.forEach(function (t) {
          var st = dialog._qrp_targetStatusMap[t];
          if (st === 'fail' || st === 'invalid') { skipped.push({title: t, reason: 'invalid_or_interwiki'}); }
          else if (st === 'exists') { skipped.push({title: t, reason: 'exists'}); }
          else { allowedTitles.push(t); }
        });
        if (skipped.length) {
          skipped.forEach(function (it) {
            if (it.reason === 'exists') mw.notify('该页面已经存在:' + it.title, { type: 'error' });
            else mw.notify('跳过无效或跨站点链接(interwiki)页面:' + it.title, { type: 'error' });
          });
        }
        if (!allowedTitles.length) { mw.notify('没有可创建的目标页面(已全部跳过)。', { type: 'error' }); return; }
        var chain = $.Deferred().resolve().promise();
        allowedTitles.forEach(function (title) {
          chain = chain.then(function () {
            var lines = ['#REDIRECT [[' + current + ']]']; if (raw) { raw.split(/\s+/).filter(Boolean).forEach(function (item) { if (lines.length === 1) lines.push('', '{{Redirect category shell|'); lines.push('{{' + item + '重定向}}'); }); if (lines.length > 2) lines.push('}}'); }
            var d = $.Deferred(); api.post({ action: 'edit', title: title, text: lines.join('\n'), summary: summary + '[[User:魔琴/gadgets/quickredirectplus|QR+快速重定向]]到[[' + current + ']]', createonly: true, token: mw.user.tokens.get('csrfToken') })
              .done(function () { mw.notify('重定向创建成功:' + title, { type: 'success' }); d.resolve(); })
              .fail(function (jqXHR, textStatus, errorThrown) { var code = jqXHR && jqXHR.responseJSON && jqXHR.responseJSON.error && jqXHR.responseJSON.error.code; if (code === 'articleexists') { mw.notify('该页面已经存在:' + title, { type: 'error' }); } else { mw.notify('错误(' + title + '):' + (code || errorThrown.error.code + ': ' + errorThrown.error.info || textStatus || '未知错误'), { type: 'error' }); } d.resolve(); });
            return d.promise();
          });
        });
        return chain.then(function () { dialog.close(); });
      });
    }
    return RedirectDialog.parent.prototype.getActionProcess.apply(this, arguments);
  };

  // 将 WM 与 Dialog 设为单例,避免多次创建并支持复用
  var qrPlusWindowManager = null;
  var qrPlusDialog = null;

  function openRedirectDialog() {
    if (!qrPlusWindowManager) {
      qrPlusWindowManager = new OO.ui.WindowManager();
      $('body').append(qrPlusWindowManager.$element);
    }
    if (!qrPlusDialog) {
      qrPlusDialog = new RedirectDialog();
      qrPlusWindowManager.addWindows([qrPlusDialog]);
      // 初始化时暴露并绑定 close 清理 current reference
      try {
        window.QRPlus_currentDialog = qrPlusDialog;
        qrPlusDialog.on('close', function () {
          try { if (window.QRPlus_currentDialog === qrPlusDialog) window.QRPlus_currentDialog = null; } catch (e) {}
        });
      } catch (e) { /* ignore */ }
    }
    // 如果 dialog 已存在,无论是否曾经关闭过,都直接打开(复用实例)
    qrPlusWindowManager.openWindow(qrPlusDialog);

    // 再次确保 current reference(在 dialog 再次打开时设置)
    try {
      window.QRPlus_currentDialog = qrPlusDialog;
    } catch (e) { /* ignore */ }
  }

  function showRedirectButton() { var button = new OO.ui.ButtonWidget({ label: 'QR+快速创建重定向', icon: 'articleRedirect' }); button.on('click', openRedirectDialog); $('#siteSub').append($('<div>').append(button.$element)); }

  //
  // === 新增整合:注入 ToolsRedirect 按钮并处理事件传递(可选增强)
  //
  (function () {
    // 辅助:从 ToolsRedirect 的对话框容器收集已勾选的页面标题(优先 data('page-title'))
    function collectCheckedFromToolsRedirectContainer($container) {
      var titles = [];
      $container.find('input[type=checkbox]:checked').each(function () {
        var dt = $(this).data && $(this).data('page-title');
        if (!dt) {
          var $row = $(this).closest('p,dd,li,div');
          dt = $row.find('a[title]').first().attr('title') || $row.text();
        }
        if (dt && typeof dt === 'string') {
          titles.push(dt.replace(/_/g, ' ').trim());
        }
      });
      // fallback: 若容器中没有 checkbox,则尝试查找带 .new class 的新建链接
      if (!titles.length) {
        $container.find('a.new[title]').each(function () {
          var t = $(this).attr('title') || $(this).text();
          if (t) titles.push(t.replace(/_/g, ' ').trim());
        });
      }
      // 去重并保留顺序
      var seen = {};
      return titles.filter(function (t) { if (seen[t]) return false; seen[t] = true; return true; });
    }

    // 把 titles 合并到 dialog 的 targetInput
    function importIntoDialog(dialog, titles) {
      if (!dialog || !titles || !titles.length) return 0;
      try {
        var targetInput = dialog.targetInput;
        if (!targetInput || typeof targetInput.getValue !== 'function' || typeof targetInput.setValue !== 'function') return 0;
        var cur = (targetInput.getValue() || '').split(/\r?\n/).map(function (s) { return s.trim(); }).filter(Boolean);
        var added = 0;
        titles.forEach(function (t) { if (cur.indexOf(t) === -1) { cur.push(t); added++; } });
        if (added) {
          targetInput.setValue(cur.join('\n'));
          // 触发输入事件以触发内部处理/渲染
          try { targetInput.$input.trigger('input.qrp'); } catch (e) {}
          // 更新对话框尺寸(如果有该方法)
          try { if (typeof dialog.updateSize === 'function') dialog.updateSize(); } catch (e) {}
          // 把光标聚焦到输入框,便于用户立即看到并编辑
          try { targetInput.$input.focus(); } catch (e) {}
        }
        return added;
      } catch (e) {
        return 0;
      }
    }

    // 处理来自 ToolsRedirect 的自定义事件(或按钮直接 dispatch)
    function onImportEvent(ev) {
      var titles = (ev && ev.detail && Array.isArray(ev.detail)) ? ev.detail : [];
      // 若当前 dialog 已打开并可见,直接导入
      if (window.QRPlus_currentDialog && window.QRPlus_currentDialog.targetInput && window.QRPlus_currentDialog.$element && window.QRPlus_currentDialog.$element.is(':visible')) {
        var addedNow = importIntoDialog(window.QRPlus_currentDialog, titles);
        if (addedNow) mw.notify('已自动导入 ' + addedNow + ' 条候选到 QR+。', { type: 'success' });
        else mw.notify('未导入新候选(可能已存在或无候选)。', { type: 'info' });
        return;
      }
      // 否则保存为 pending 并打开 QR+;openRedirectDialog 会创建或复用 dialog
      window.QRPlus_pendingImport = titles || [];
      openRedirectDialog();

      // 更稳健的等待逻辑:
      // - 等待 QR+ 实例存在并且内部 targetInput 已准备好且对话框可见
      // - 最多等待 5 秒(5000ms),检测间隔 80ms
      var waited = 0, interval = 80, timeout = 5000;
      var poll = setInterval(function () {
        var dialog = window.QRPlus_currentDialog || qrPlusDialog;
        var canImport = false;
        try {
          if (dialog && dialog.targetInput && typeof dialog.targetInput.setValue === 'function') {
            // 要求对话框元素在 DOM 中且可见(避免在未 attach 或不可见时设置值)
            if (dialog.$element && dialog.$element.is(':visible')) {
              canImport = true;
            } else {
              // 额外判断:如果 WindowManager 有 currentWindow 属性并且等于 dialog
              try {
                if (qrPlusWindowManager && qrPlusWindowManager.currentWindow && qrPlusWindowManager.currentWindow === dialog) {
                  canImport = true;
                }
              } catch (e) {}
            }
          }
        } catch (e) { canImport = false; }

        if (canImport) {
          clearInterval(poll);
          var added = importIntoDialog(dialog, window.QRPlus_pendingImport || []);
          if (added) mw.notify('已自动导入 ' + added + ' 条候选到 QR+。', { type: 'success' });
          else mw.notify('未导入新候选(可能已存在或无候选)。', { type: 'info' });
          window.QRPlus_pendingImport = null;
          return;
        }

        waited += interval;
        if (waited >= timeout) {
          clearInterval(poll);
          // 超时仍未就绪:保留 pendingImport(用户手动打开 QR+ 时会触发导入逻辑)
          mw.notify('等待 QR+ 就绪超时,请手动打开 QR+(或稍后重试导入)。', { type: 'warn' });
        }
      }, interval);
    }

    // 监听自定义事件(从 ToolsRedirect 按钮或其它脚本派发)
    window.addEventListener('QRPlus.ImportFromToolsRedirect', onImportEvent);

    // MutationObserver 注入按钮到 ToolsRedirect 对话框
    var INJECTED_FLAG = 'qrplus-button-injected';
    function injectButtonIntoDialog($dlg) {
      if ($dlg.data(INJECTED_FLAG)) return;
      var $tabArea = $dlg.find('.tab-redirect').first();
      if (!$tabArea.length) $tabArea = $dlg; // 回退

      var $btn = $('<button type="button" class="mw-ui-button mw-ui-progressive">')
        .text('在 QR+ 打开并导入(勾选项)')
        .css({ 'margin-left': '8px', 'margin-bottom': '6px' });

      $btn.on('click', function (e) {
        e.preventDefault();
        var selected = collectCheckedFromToolsRedirectContainer($dlg);

        // 先关闭 ToolsRedirect 的对话框,避免同时存在两个模态窗口导致界面完全被锁定
        try {
          if ($dlg && typeof $dlg.dialog === 'function') {
            $dlg.dialog('close');
          }
        } catch (err) {
          // ignore
        }

        // 等待 dialog 完全关闭,再打开 QR+ 并派发事件(短延迟确保 UI 状态恢复)
        setTimeout(function () {
          try {
            window.dispatchEvent(new CustomEvent('QRPlus.ImportFromToolsRedirect', { detail: selected }));
          } catch (err) {
            // IE fallback for CustomEvent
            try {
              var ev = document.createEvent('CustomEvent');
              ev.initCustomEvent('QRPlus.ImportFromToolsRedirect', true, true, selected);
              window.dispatchEvent(ev);
            } catch (e2) {
              // 最后退回:直接调用 openRedirectDialog 并把 titles 放到 pending
              window.QRPlus_pendingImport = selected;
              openRedirectDialog();
            }
          }
          mw.notify('已向 QR+ 发送 ' + (selected.length || 0) + ' 条已勾选候选(若 QR+ 支持自动导入,将自动填入)。', { type: 'info' });
        }, 120);
      });

      $tabArea.prepend($btn);
      $dlg.data(INJECTED_FLAG, true);
    }

    // 观察 DOM,注入到新出现的 dialog-redirect
    var mo = new MutationObserver(function (mutations) {
      mutations.forEach(function (m) {
        (m.addedNodes || []).forEach(function (node) {
          if (node.nodeType !== 1) return;
          var $n = $(node);
          if ($n.hasClass('dialog-redirect')) {
            injectButtonIntoDialog($n);
          } else {
            var $found = $n.find('.dialog-redirect');
            if ($found.length) $found.each(function () { injectButtonIntoDialog($(this)); });
          }
        });
      });
    });
    mo.observe(document.body, { childList: true, subtree: true });

    // 如果 dialog 已存在(脚本稍后加载),立即注入
    $(function () {
      $('.dialog-redirect').each(function () { injectButtonIntoDialog($(this)); });
    });

  })();
  //
  // === 结束新增整合段落
  //

  $(function () {
    mw.loader.using(['mediawiki.util', 'mediawiki.api', 'ext.gadget.site-lib'/* ,'ext.gadget.HanAssist'*/, 'oojs-ui-windows'], function () {
      var count = 0;
      var alwaysShow = mw.config.get('alwaysShowQuickRedirectPlus') || (window.alwaysShowQuickRedirectPlus === true);
      if (mw.config.get('wgAction') === 'view' && !mw.config.get('wgIsRedirect')) {
        mw.util.addPortletLink('p-cactions', '#', 'QR+快速创建重定向', 'ca-quickredirectplus', '使用快速对话框创建重定向', null, null);
        $('#ca-quickredirectplus').click(function (e) { if (count === 0) { showRedirectButton(); count++; } openRedirectDialog(); });
        // 如果全域标志为真,页面加载时立即显示大按钮
        if (alwaysShow) { showRedirectButton(); count++; }
      }
    });
  });
})();
  
// </nowiki>