MediaWiki:Common.js:修订间差异

来自虾仁维基
跳转到导航 跳转到搜索
虾仁留言 | 贡献
添加虾仁AI浮窗助手组件
MediaWiki default留言 | 贡献
增加作品审核标签(ai audit mode)
第321行: 第321行:
     var API = '/ai_proxy.php';
     var API = '/ai_proxy.php';
     var TITLE = '虾仁AI';
     var TITLE = '虾仁AI';
    var AUDIT_TEMPLATE = '作品名:\n1. 视觉识别(主角是否为草帽熊猫头或明显变体):\n2. 听觉识别(是否使用虾仁系列标志性AI配音):\n3. 主角身份(是否虾仁人格内核的投射,名字可不同):\n4. 完整性(有无完整结局):\n5. 深度(有无情感内核/名场面):\n6. 原创性(虾仁原生或深度改编):\n7. 自述与作品链接:\n';


     var css = document.createElement('style');
     var css = document.createElement('style');
第358行: 第359行:
       '<div class="xr-ai-tabs">' +
       '<div class="xr-ai-tabs">' +
       '<button class="xr-ai-tab xr-active" data-mode="general">闲聊</button>' +
       '<button class="xr-ai-tab xr-active" data-mode="general">闲聊</button>' +
       '<button class="xr-ai-tab" data-mode="wiki">问虾仁宇宙</button></div>' +
       '<button class="xr-ai-tab" data-mode="wiki">问虾仁宇宙</button>' +
      '<button class="xr-ai-tab" data-mode="audit">作品审核</button></div>' +
       '<div class="xr-ai-hint">内容由 AI 生成,仅供参考</div>' +
       '<div class="xr-ai-hint">内容由 AI 生成,仅供参考</div>' +
       '<div class="xr-ai-msgs"></div>' +
       '<div class="xr-ai-msgs"></div>' +
第388行: 第390行:
         t.classList.add('xr-active');
         t.classList.add('xr-active');
         mode = t.getAttribute('data-mode');
         mode = t.getAttribute('data-mode');
        if (mode === 'audit' && !inputEl.value.trim()) { inputEl.value = AUDIT_TEMPLATE; inputEl.focus(); }
       });
       });
     });
     });

2026年7月30日 (四) 02:10的版本

/* 虾仁维基 - 自定义侧边栏导航 v2.1 */ /* 优化:古风主题统一、健壮初始化、精致样式 */ /* 最后更新: 2026-07-30 */

(function ($, mw) {

   'use strict';
   var COLLAPSE_KEY = 'xiarenwiki-nav-v2';
   var WORKS = ['我在大明那些年', '彼岸轮回', '一品布衣'];
   var WORK_DIRECT = ['宇宙层', '人物', '世界观', '设定'];
   function init() {
       var wgIsArticle = mw.config.get('wgIsArticle');
       var wgCanonicalSpecialPageName = mw.config.get('wgCanonicalSpecialPageName');
       if (!wgIsArticle && wgCanonicalSpecialPageName) {
           return;
       }
       mw.loader.using('mediawiki.api').then(function () {
           waitForPanel(function ($panel) {
               buildNav($panel);
           });
       }).catch(function (e) {
           console.error('[虾仁导航] 依赖加载失败,降级构建', e);
           waitForPanel(function ($panel) {
               buildNav($panel);
           });
       });
   }
   function waitForPanel(cb) {
       var $panel = $('#mw-panel');
       if ($panel.length) return cb($panel);
       var tries = 0;
       var timer = setInterval(function () {
           $panel = $('#mw-panel');
           if ($panel.length || ++tries > 50) {
               clearInterval(timer);
               if ($panel.length) cb($panel);
           }
       }, 100);
   }
   function buildNav($panel) {
       var html = ;

html += '

';

       html += section('Wiki', [
           ['首页', '首页'],
           ['Wiki简介', 'Wiki简介'],
           ['管理员告示', '管理员告示'],
           ['随机页面', 'Special:Random'],
           ['快速搜索', 'Special:Search'],
           ['相关平台', '相关平台'],
           ['Wiki源站点', 'Wiki源站点']
       ]);
       html += section('虾仁简介', [
           ['虾仁?', '虾仁?'],
           ['虾仁共创收录规则', '虾仁共创收录规则'],
           ['虾仁宇宙结构介绍', '虾仁宇宙结构介绍']
       ]);

html += '

'; html += '
虾仁宇宙
'; html += '
卷帙加载中…
'; html += '

'; html += '

'; html += '
虾仁文学
'; html += '
';
       html += link('创始宣言', '创始宣言');
       html += link('虾仁六章', '虾仁六章');
       html += link('虾仁异志', '虾仁异志');
       html += link('虾仁诗作', '虾仁诗作');
       html += link('虾仁音乐', '虾仁音乐');
html += '
'; html += '
虾仁论文 ' + plus('虾仁论文') + '
'; html += '
';
       html += link('去中心化创作模式的研究', '去中心化创作模式的研究');
       html += link('数字时代的孙悟空', '数字时代的孙悟空');
html += '

';

       html += section('虾仁历史', [
           ['起源', '虾仁历史/起源'],
           ['传播', '虾仁历史/传播'],
           ['转折', '虾仁历史/转折'],
           ['爆发', '虾仁历史/爆发'],
           ['危机', '虾仁历史/危机'],
           ['转型', '虾仁历史/转型']
       ]);

html += '

<a href="' + url('共创入口') + '">共创入口</a>

';

       $panel.html(html);
       restoreCollapse();
       bindEvents();
       highlightCurrent();
       loadUniverse();
   }
   function section(title, items) {
       var sid = title.replace(/[^a-z0-9\u4e00-\u9fff]/gi, ).toLowerCase();

var h = '

'; h += '
' + title + '
'; h += '
';
       for (var i = 0; i < items.length; i++) {
           h += link(items[i][0], items[i][1]);
       }
h += '

';

       return h;
   }
   function link(label, page, cls) {

return '

<a href="' + url(page) + '">' + label + '</a>

';

   }
   function url(page) { return mw.util.getUrl(page); }
   function plus(page) {
       return '<a class="xn-plus" href="' + url(page) + '?action=edit" title="编辑/新增">+</a>';
   }
   async function loadUniverse() {
       var $body = $('#xn-universe');
       if (!$body.length) return;
       try {
           var api = new mw.Api();
           var html = ;
           for (var i = 0; i < WORKS.length; i++) {
               var work = WORKS[i];
               var workSid = 'work' + i;
               var subpages = [];
               try {
                   var result = await api.get({
                       action: 'query',
                       list: 'allpages',
                       apprefix: work + '/',
                       aplimit: 100
                   });
                   subpages = result.query.allpages || [];
               } catch (e) {
                   console.error('[虾仁导航] API error for ' + work, e);
               }

html += '

'; html += '
' + work + '
'; html += '
';
               var directSet = {};
               for (var j = 0; j < subpages.length; j++) {
                   var rel = subpages[j].title.substring(work.length + 1);
                   if (rel.indexOf('/') === -1) directSet[rel] = true;
               }
               for (var k = 0; k < WORK_DIRECT.length; k++) {
                   var item = WORK_DIRECT[k];
                   var exists = directSet[item];
                   html += link(item, work + '/' + item, exists ?  : 'xn-missing');
               }
               var plotSegs = [];
               for (j = 0; j < subpages.length; j++) {
                   rel = subpages[j].title.substring(work.length + 1);
                   var parts = rel.split('/');
                   if (parts.length === 2 && parts[0] === '剧情层') {
                       plotSegs.push(parts[1]);
                   }
               }
               var plotSid = workSid + 'plot';
html += '
'; html += '
剧情层 ' + plus(work + '/剧情层') + '
'; html += '
';
               for (k = 0; k < plotSegs.length; k++) {
                   var seg = plotSegs[k];
                   var segSid = workSid + 'seg' + k;
                   var segPage = work + '/剧情层/' + seg;
                   var jinji = [];
                   try {
                       var segResult = await api.get({
                           action: 'query',
                           list: 'allpages',
                           apprefix: segPage + '/',
                           aplimit: 50
                       });
                       var segPages = segResult.query.allpages || [];
                       for (var m = 0; m < segPages.length; m++) {
                           jinji.push(segPages[m].title.substring(segPage.length + 1));
                       }
                   } catch (e2) {}
html += '
'; html += '
' + seg + ' ' + plus(segPage) + '
'; html += '
';
                   for (m = 0; m < jinji.length; m++) {
                       html += link(jinji[m], segPage + '/' + jinji[m]);
                   }
html += '
';
               }
html += '
'; html += '

';

           }

html += '

';

           $body.html(html);
           restoreCollapse();
           bindEvents();
           highlightCurrent();
       } catch (e) {
           console.error('[虾仁导航] 虾仁宇宙加载失败', e);

$body.html('

');

       }
   }
   function highlightCurrent() {
       var currentTitle = mw.config.get('wgPageName');
       $('#mw-panel .xn-link a').each(function () {
           var href = $(this).attr('href');
           if (!href) return;
           var match = false;
           if (href.indexOf('title=') > 0) {
               var m = href.match(/title=([^&]+)/);
               if (m) match = decodeURIComponent(m[1]) === currentTitle;
           } else {
               var parts = href.split('/');
               var last = parts[parts.length - 1];
               match = decodeURIComponent(last) === currentTitle;
           }
           if (match) {
               $(this).addClass('xn-active');
               $(this).closest('.xn-link').addClass('xn-active-link');
               $(this).parents('.xn-body, .xn-subbody, .xn-subsubbody').each(function () {
                   $(this).show();
                   var sid = $(this).attr('id');
                   if (sid && sid.indexOf('xn-') === 0) {
                       sid = sid.substring(3);
                       $('[data-sid="' + sid + '"]').removeClass('xn-collapsed');
                   }
               });
           }
       });
   }
   function bindEvents() {
       $(document).off('click.xn').on('click.xn', '.xn-expandable', function (e) {
           if ($(e.target).closest('.xn-plus').length) return;
           var sid = $(this).data('sid');
           var $body = $('#xn-' + sid);
           if (!$body.length) $body = $(this).next('.xn-body,.xn-subbody,.xn-subsubbody');
           if (!$body.length) return;
           $body.slideToggle(150);
           $(this).toggleClass('xn-collapsed', !$body.is(':visible'));
           var state = loadState();
           state[sid] = $body.is(':visible');
           saveState(state);
       });
   }
   function restoreCollapse() {
       var state = loadState();
       var defaults = { 'universe': true };
       for (var key in defaults) {
           if (!(key in state)) state[key] = defaults[key];
       }
       Object.keys(state).forEach(function (sid) {
           var $header = $('[data-sid="' + sid + '"]');
           var $body = $('#xn-' + sid);
           if (!$body.length) $body = $header.next('.xn-body,.xn-subbody,.xn-subsubbody');
           if (!$body.length) return;
           if (state[sid]) {
               $body.show();
               $header.removeClass('xn-collapsed');
           } else {
               $body.hide();
               $header.addClass('xn-collapsed');
           }
       });
   }
   function loadState() {
       try { return JSON.parse(localStorage.getItem(COLLAPSE_KEY) || '{}'); }
       catch (e) { return {}; }
   }
   function saveState(s) {
       try { localStorage.setItem(COLLAPSE_KEY, JSON.stringify(s)); }
       catch (e) {}
   }
   $(init);

})(jQuery, mediaWiki);

/* ===== 虾仁AI 浮窗助手(2026-07-30 v2) ===== */ (function () {

 if (window.__xrAiInited) return;
 window.__xrAiInited = true;
 function xrAvatar(size) {
   return '<svg viewBox="0 0 48 48" width="' + size + '" height="' + size + '" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><ellipse cx="24" cy="14.5" rx="18" ry="5.5" fill="#caa53d"/><ellipse cx="24" cy="14.5" rx="18" ry="5.5" fill="none" stroke="#a8842a" stroke-width="0.8"/><path d="M13 14.5 Q24 2.5 35 14.5 Z" fill="#d9b84a"/><path d="M13 14.5 Q24 2.5 35 14.5 Z" fill="none" stroke="#a8842a" stroke-width="0.8"/><circle cx="12.5" cy="25" r="6.5" fill="#2c2c2c"/><circle cx="35.5" cy="25" r="6.5" fill="#2c2c2c"/><circle cx="24" cy="29" r="15" fill="#fdfdfd" stroke="#e9e2d2" stroke-width="0.8"/><ellipse cx="18" cy="28" rx="4.5" ry="5.6" fill="#2c2c2c"/><ellipse cx="30" cy="28" rx="4.5" ry="5.6" fill="#2c2c2c"/><circle cx="18" cy="28" r="1.9" fill="#fff"/><circle cx="30" cy="28" r="1.9" fill="#fff"/><ellipse cx="24" cy="33" rx="2.1" ry="1.5" fill="#2c2c2c"/><path d="M20.5 36 Q24 38.5 27.5 36" stroke="#2c2c2c" stroke-width="1.3" fill="none" stroke-linecap="round"/><circle cx="15" cy="33" r="2" fill="#c41e3a" opacity="0.18"/><circle cx="33" cy="33" r="2" fill="#c41e3a" opacity="0.18"/></svg>';
 }
 function init() {
   var API = '/ai_proxy.php';
   var TITLE = '虾仁AI';
   var AUDIT_TEMPLATE = '作品名:\n1. 视觉识别(主角是否为草帽熊猫头或明显变体):\n2. 听觉识别(是否使用虾仁系列标志性AI配音):\n3. 主角身份(是否虾仁人格内核的投射,名字可不同):\n4. 完整性(有无完整结局):\n5. 深度(有无情感内核/名场面):\n6. 原创性(虾仁原生或深度改编):\n7. 自述与作品链接:\n';
   var css = document.createElement('style');
   css.textContent = [
     '#xr-ai-fab{position:fixed;right:22px;bottom:22px;z-index:99999;width:56px;height:56px;border-radius:50%;border:none;display:flex;align-items:center;justify-content:center;cursor:pointer;background:linear-gradient(135deg,#c41e3a,#9b182d);color:#fff;font-size:26px;box-shadow:0 6px 20px rgba(155,24,45,.4);transition:transform .2s ease,box-shadow .2s ease;font-family:"Noto Serif SC","Songti SC",serif}',
     '#xr-ai-fab:hover{transform:scale(1.08);box-shadow:0 8px 26px rgba(155,24,45,.55)}',
     '#xr-ai-panel{position:fixed;right:22px;bottom:90px;z-index:99999;width:360px;max-width:calc(100vw - 32px);height:520px;max-height:calc(100vh - 120px);display:none;flex-direction:column;overflow:hidden;background:#fbf6ec;border:1px solid #e7d8bf;border-radius:14px;box-shadow:0 12px 40px rgba(80,40,30,.25);font-family:"Noto Serif SC","Source Han Serif SC","Songti SC","STSong","SimSun",serif;color:#3a2418}',
     '#xr-ai-panel.xr-show{display:flex}',
     '.xr-ai-head{display:flex;align-items:center;justify-content:space-between;padding:12px 14px;background:linear-gradient(135deg,#c41e3a,#9b182d);color:#fff}',
     '.xr-ai-title{font-weight:700;font-size:16px;letter-spacing:.05em;display:flex;align-items:center;gap:6px}',
     '.xr-ai-close{background:none;border:none;color:#fff;font-size:20px;cursor:pointer;line-height:1}',
     '.xr-ai-tabs{display:flex;border-bottom:1px solid #ecdfc8;background:#f3e9d6}',
     '.xr-ai-tab{flex:1;padding:9px 0;text-align:center;cursor:pointer;font-size:13px;color:#7a5a3a;border:none;background:none;transition:color .2s}',
     '.xr-ai-tab.xr-active{color:#c41e3a;font-weight:700;box-shadow:inset 0 -2px 0 #c41e3a}',
     '.xr-ai-hint{text-align:center;font-size:11px;color:#a98c6b;padding:0 0 6px}',
     '.xr-ai-msgs{flex:1;overflow-y:auto;padding:14px;display:flex;flex-direction:column;gap:10px}',
     '.xr-ai-msg{max-width:84%;padding:9px 12px;border-radius:12px;line-height:1.6;font-size:14px;white-space:pre-wrap;word-break:break-word}',
     '.xr-ai-msg.xr-user{align-self:flex-end;background:#c41e3a;color:#fff;border-bottom-right-radius:3px}',
     '.xr-ai-msg.xr-ai{align-self:flex-start;background:#fff;color:#3a2418;border:1px solid #ecdfc8;border-left:3px solid #c41e3a;border-bottom-left-radius:3px}',
     '.xr-ai-inputbar{display:flex;gap:8px;padding:10px;border-top:1px solid #ecdfc8;background:#f6eddb}',
     '.xr-ai-input{flex:1;resize:none;border:1px solid #e0cda8;border-radius:9px;padding:8px 10px;font-family:inherit;font-size:14px;background:#fff;outline:none;max-height:80px}',
     '.xr-ai-input:focus{border-color:#c41e3a}',
     '.xr-ai-send{border:none;border-radius:9px;padding:0 16px;background:#9b182d;color:#fff;cursor:pointer;font-family:inherit;font-size:14px;transition:background .2s}',
     '.xr-ai-send:hover{background:#c41e3a}',
     '.xr-ai-send:disabled{opacity:.5;cursor:default}'
   ].join('\n');
   document.head.appendChild(css);
   var fab = document.createElement('button');
   fab.id = 'xr-ai-fab'; fab.innerHTML = xrAvatar(38); fab.title = TITLE; fab.setAttribute('aria-label', TITLE);
   var panel = document.createElement('div');
   panel.id = 'xr-ai-panel';
   panel.innerHTML =

'

' + xrAvatar(22) + ' ' + TITLE + '' + '<button class="xr-ai-close" title="收起">×</button>

' + '

' +
     '<button class="xr-ai-tab xr-active" data-mode="general">闲聊</button>' +
     '<button class="xr-ai-tab" data-mode="wiki">问虾仁宇宙</button>' +
'<button class="xr-ai-tab" data-mode="audit">作品审核</button>

' + '

内容由 AI 生成,仅供参考

' + '

' + '

' +
     '<textarea class="xr-ai-input" rows="1" placeholder="和虾仁AI说点什么…"></textarea>' +
'<button class="xr-ai-send">发送</button>

' +

     '<input type="text" name="company" tabindex="-1" autocomplete="off" ' +
     'style="position:absolute;left:-9999px;width:1px;height:1px;opacity:0;" aria-hidden="true">';
   document.body.appendChild(fab);
   document.body.appendChild(panel);
   var msgsEl = panel.querySelector('.xr-ai-msgs');
   var inputEl = panel.querySelector('.xr-ai-input');
   var sendEl = panel.querySelector('.xr-ai-send');
   var closeEl = panel.querySelector('.xr-ai-close');
   var tabs = panel.querySelectorAll('.xr-ai-tab');
   var honey = panel.querySelector('input[name=company]');
   var mode = 'general';
   var history = [];
   var busy = false;
   fab.addEventListener('click', function () { panel.classList.toggle('xr-show'); });
   closeEl.addEventListener('click', function () { panel.classList.remove('xr-show'); });
   tabs.forEach(function (t) {
     t.addEventListener('click', function () {
       tabs.forEach(function (x) { x.classList.remove('xr-active'); });
       t.classList.add('xr-active');
       mode = t.getAttribute('data-mode');
       if (mode === 'audit' && !inputEl.value.trim()) { inputEl.value = AUDIT_TEMPLATE; inputEl.focus(); }
     });
   });
   function scrollBottom() { msgsEl.scrollTop = msgsEl.scrollHeight; }
   function addBubble(cls, text) {
     var d = document.createElement('div');
     d.className = 'xr-ai-msg ' + cls;
     d.textContent = text;
     msgsEl.appendChild(d);
     scrollBottom();
     return d;
   }
   function callAI(msgs, m, onChunk) {
     return fetch(API, {
       method: 'POST',
       headers: { 'Content-Type': 'application/json' },
       body: JSON.stringify({ mode: m, messages: msgs, company: honey.value })
     }).then(function (res) {
       if (!res.ok) throw new Error('HTTP ' + res.status);
       var reader = res.body.getReader();
       var decoder = new TextDecoder();
       var buf = ;
       function pump() {
         return reader.read().then(function (result) {
           if (result.done) return;
           buf += decoder.decode(result.value, { stream: true });
           var idx;
           while ((idx = buf.indexOf('\n')) >= 0) {
             var line = buf.slice(0, idx).trim();
             buf = buf.slice(idx + 1);
             if (!line) continue;
             try {
               var obj = JSON.parse(line);
               if (obj.content) onChunk(obj.content);
               if (obj.error) onChunk('(' + obj.error + ')');
             } catch (e) {}
           }
           return pump();
         });
       }
       return pump();
     });
   }
   function send() {
     var text = inputEl.value.trim();
     if (!text || busy) return;
     busy = true; sendEl.disabled = true;
     addBubble('xr-user', text);
     history.push({ role: 'user', content: text });
     inputEl.value = ;
     var aiBubble = addBubble('xr-ai', '…');
     var acc = ;
     callAI(history.slice(-12), mode, function (chunk) {
       acc += chunk;
       aiBubble.textContent = acc + '▌';
       scrollBottom();
     }).then(function () {
       aiBubble.textContent = acc || '(暂无回复)';
       history.push({ role: 'assistant', content: acc });
       busy = false; sendEl.disabled = false; scrollBottom();
     }).catch(function (e) {
       aiBubble.textContent = '出错了:' + (e && e.message ? e.message : e);
       busy = false; sendEl.disabled = false;
     });
   }
   sendEl.addEventListener('click', send);
   inputEl.addEventListener('keydown', function (e) {
     if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); send(); }
   });
   addBubble('xr-ai', '你好,我是虾仁AI,可以陪你闲聊,也能帮你查「虾仁宇宙」的资料。选个模式开始吧~');
 }
 if (document.readyState === 'loading') {
   document.addEventListener('DOMContentLoaded', init);
 } else {
   init();
 }

})();