// ==UserScript==
// @name IYUU 全站辅种检测
// @namespace iyuu-crossseed
// @version 1.3.5-icon-tune
// @description 实现在种子详情页显示该种在其他站点存在情况
// @match https://*/details.php*
// @match http://*/details.php*
// @run-at document-idle
// @grant GM_xmlhttpRequest
// @grant GM_addStyle
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_deleteValue
// @license GPL-3.0 License
// @connect 2025.iyuu.cn
// @connect *
// ==/UserScript==
(function () {
'use strict';
/* ========== 0) 可选:默认 Token(留空即可) ========== */
const IYUU_TOKEN_DEFAULT = '';
/* ========== 0.1) 站点图标映射(在此添加/维护图标 URL) ========== *
* 用法说明:
* 1)优先按 sid 匹配:ICON_MAP.sid[<数字sid>] = 'https://.../logo.png'
* 2)其次按名称匹配(nickname 或 site,不区分大小写、会做去空格处理):
* ICON_MAP.name['mteam'] = 'https://.../mteam.svg'
* ICON_MAP.name['ourbits'] = 'https://.../ourbits.png'
* 3)未配置图标的站点将不显示图标,只显示名称(名称字号会更大些)。
*/
const ICON_MAP = {
sid: {
1: 'https://icon.xiaoge.org/images/pt/FRDS.png',
2: 'https://icon.xiaoge.org/images/pt/PTHOME.png',
3: 'https://icon.xiaoge.org/images/pt/M-Team.png',
4: 'https://icon.xiaoge.org/images/pt/HDsky.png',
8: 'https://icon.xiaoge.org/images/pt/btschool.png',
6: 'https://icon.xiaoge.org/images/pt/Pter.png',
7: 'https://icon.xiaoge.org/images/pt/HDHome.png',
23: 'https://icon.xiaoge.org/images/pt/Nvme.png',
25: 'https://icon.xiaoge.org/images/pt/CHDbits.png',
33: 'https://icon.xiaoge.org/images/pt/OpenCD.png',
68: 'https://icon.xiaoge.org/images/pt/Audiences.png',
72: 'https://icon.xiaoge.org/images/pt/HHCLUB.png',
9: 'https://icon.xiaoge.org/images/pt/OurBits.png',
14: 'https://icon.xiaoge.org/images/pt/TTG.png',
86: 'https://icon.xiaoge.org/images/pt/UBits.png',
93: 'https://icon.xiaoge.org/images/pt/agsv.png',
89: 'https://icon.xiaoge.org/images/pt/carpt.png',
84: 'https://icon.xiaoge.org/images/pt/cyanbug.png',
90: 'https://icon.xiaoge.org/images/pt/dajiao.png',
51: 'https://icon.xiaoge.org/images/pt/dicmusic.png',
40: 'https://icon.xiaoge.org/images/pt/discfan.png',
64: 'https://icon.xiaoge.org/images/pt/gpw.png',
56: 'https://icon.xiaoge.org/images/pt/haidan.png',
29: 'https://icon.xiaoge.org/images/pt/hdarea.png',
105: 'https://icon.xiaoge.org/images/pt/hddolby.png',
57: 'https://icon.xiaoge.org/images/pt/hdfans.png',
97: 'https://icon.xiaoge.org/images/pt/hdkyl.png',
18: 'https://icon.xiaoge.org/images/pt/nicept.png',
88: 'https://icon.xiaoge.org/images/pt/panda.png',
94: 'https://icon.xiaoge.org/images/pt/ptvicomo.png',
95: 'https://icon.xiaoge.org/images/pt/qingwapt.png',
82: 'https://icon.xiaoge.org/images/pt/rousi.png',
24: 'https://icon.xiaoge.org/images/pt/soulvoice.png',
5: 'https://icon.xiaoge.org/images/pt/tjupt.png',
96: 'https://icon.xiaoge.org/images/pt/xingtan.png',
80: 'https://icon.xiaoge.org/images/pt/zhuque.png',
81: 'https://icon.xiaoge.org/images/pt/zmpt.png',
},
name: {
// 'mteam': 'https://example.com/icons/mteam.svg',
// 'hdchina': 'https://example.com/icons/hdchina.png',
// 'ourbits': 'https://example.com/icons/ourbits.svg',
}
};
function lookupIconURL({ sid, nickname, site }) {
if (sid != null && ICON_MAP.sid[sid]) return ICON_MAP.sid[sid];
const toKey = (s) => (s || '').toString().trim().toLowerCase();
const n1 = toKey(nickname);
const n2 = toKey(site);
if (n1 && ICON_MAP.name[n1]) return ICON_MAP.name[n1];
if (n2 && ICON_MAP.name[n2]) return ICON_MAP.name[n2];
return null; // 未配置则不显示图标
}
/* ========== 工具:安全样式与插入 ========== */
function addStyle(css) {
try { if (typeof GM_addStyle === 'function') return GM_addStyle(css); } catch {}
const st = document.createElement('style'); st.textContent = css;
(document.head || document.documentElement).appendChild(st);
}
function safePrepend(parent, child) {
try {
if (!parent) parent = document.body || document.documentElement;
if (parent.firstChild) parent.insertBefore(child, parent.firstChild);
else parent.appendChild(child);
} catch {
(document.body || document.documentElement).appendChild(child);
}
}
function findTopContainer() {
const selectors = ['#outer', '#wrapper', '#maincontent', '#content', '.main', 'body'];
for (const sel of selectors) { const el = document.querySelector(sel); if (el) return el; }
return document.body || document.documentElement;
}
/* ========== 顶部横幅 UI(样式微调版本) ========== */
addStyle(`
.iyuu-topbar{position:sticky;top:0;z-index:999999;background:rgba(9,14,28,.92);color:#fff;border-bottom:1px solid #ffffff1a;backdrop-filter:blur(6px)}
.iyuu-topbar-inner{display:flex;align-items:center;gap:12px;padding:10px 14px;flex-wrap:wrap;font:12px/1.45 system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial}
.iyuu-title{font-weight:700;margin-right:8px;white-space:nowrap}
.iyuu-hash{opacity:.75}
.iyuu-chips{display:flex;gap:10px;flex-wrap:wrap;align-items:stretch}
/* —— 气泡卡 —— */
.iyuu-chip{
display:inline-flex;flex-direction:column;align-items:center;justify-content:center;
gap:4px; /* 原 6px → 4px,减少元素间留白 */
padding:6px 8px; /* 原 8x10 → 6x8,收紧内边距 */
border-radius:12px;background:#0f172a;border:1px solid #243045;
text-decoration:none;color:#dbeafe;
min-width:92px;max-width:140px;min-height:66px; /* 保持原有框架大小不变 */
box-sizing:border-box;text-align:center
}
.iyuu-chip.ok{border-color:#22c55e;color:#dcfce7}
.iyuu-chip:hover{filter:brightness(1.05)}
/* 图标放大(原 22 → 28) */
.iyuu-icon{width:28px;height:28px;display:block;object-fit:contain}
/* 名称稍大(原 12px 左右 → 13.5px) */
.iyuu-label{display:block;line-height:1.22;font-size:13.5px}
/* 计数保持小一点 */
.iyuu-count{opacity:.85;font-size:10px}
/* 无图标场景:字号再大一点,更饱满 */
.iyuu-chip.noicon .iyuu-label{font-size:14.5px}
.iyuu-badge{font-size:10px;padding:2px 6px;border-radius:999px;background:#f59e0b;color:#221400}
.iyuu-badge.ok{background:#22c55e;color:#05290f}
.iyuu-badge.no{background:#ef4444;color:#360202}
.iyuu-badge.err{background:#f97316;color:#2c1302}
.iyuu-divider{height:18px;width:1px;background:#ffffff1a;margin:0 6px}
.iyuu-empty{opacity:.8;align-self:center}
.iyuu-right{display:flex;align-items:center;gap:8px;margin-left:auto}
.iyuu-input{display:flex;align-items:center;gap:6px;background:#0f172a;border:1px solid #243045;border-radius:8px;padding:4px 6px}
.iyuu-input input{width:200px;background:transparent;border:none;outline:none;color:#cde3ff}
.iyuu-btn{padding:4px 8px;border-radius:6px;border:none;cursor:pointer;background:#1e293b;color:#fff;font-size:12px}
.iyuu-btn:hover{filter:brightness(1.05)}
.iyuu-token-mask{opacity:.85}
.iyuu-eye{cursor:pointer;user-select:none;opacity:.9}
`);
const bar = document.createElement('div');
bar.className = 'iyuu-topbar';
bar.innerHTML = `
<div class="iyuu-topbar-inner">
<span class="iyuu-title">IYUU 全站检测</span>
<span class="iyuu-hash" id="iyuu-hash">hash: ——</span>
<span class="iyuu-divider"></span>
<span class="iyuu-badge" id="iyuu-badge">待检测</span>
<div class="iyuu-chips" id="iyuu-chips"></div>
<div class="iyuu-right">
<span>Token:<span class="iyuu-token-mask" id="iyuu-token-mask"></span></span>
<div class="iyuu-input">
<input id="iyuu-token-input" type="password" placeholder="在此粘贴 IYUU Token"/>
<span class="iyuu-eye" id="iyuu-eye" title="显示/隐藏">👁️</span>
</div>
<button class="iyuu-btn" id="iyuu-save">保存Token</button>
</div>
</div>
`;
safePrepend(findTopContainer(), bar);
const chipsEl = bar.querySelector('#iyuu-chips');
const badgeEl = bar.querySelector('#iyuu-badge');
const tokenMaskEl = bar.querySelector('#iyuu-token-mask');
const tokenInput = bar.querySelector('#iyuu-token-input');
const eyeBtn = bar.querySelector('#iyuu-eye');
const saveBtn = bar.querySelector('#iyuu-save');
const hashEl = bar.querySelector('#iyuu-hash');
const setBadge = (cls, text) => { badgeEl.className = `iyuu-badge ${cls||''}`.trim(); badgeEl.textContent = text; };
/**
* 新版 addChip:当无 iconURL 时会加上 .noicon 类,从而让名称字号更大。
* 保持卡片 min-width / min-height 不变,仅通过 gap/padding/字号/图标尺寸调整观感。
*/
const addChip = ({ label, href, ok=true, count=1, iconURL=null }) => {
const a = document.createElement(href ? 'a' : 'div');
a.className = `iyuu-chip ${ok ? 'ok' : ''} ${iconURL ? '' : 'noicon'}`.trim();
if (href) { a.href = href; a.target = '_blank'; a.rel = 'noopener noreferrer'; }
// 图标(可选)
if (iconURL) {
const img = document.createElement('img');
img.className = 'iyuu-icon';
img.src = iconURL;
img.alt = '';
a.appendChild(img);
}
// 名称
const nameEl = document.createElement('span');
nameEl.className = 'iyuu-label';
nameEl.textContent = label;
a.appendChild(nameEl);
// 数量(仅在 ok 且 count>1 时显示)
if (ok && count > 1) {
const cnt = document.createElement('span');
cnt.className = 'iyuu-count';
cnt.textContent = `(${count})`;
a.appendChild(cnt);
}
chipsEl.appendChild(a);
};
const showEmpty = (msg='未发现可辅种站点') => {
const span = document.createElement('span'); span.className='iyuu-empty'; span.textContent=msg; chipsEl.appendChild(span);
};
/* ========== 2) Token 存取 ========== */
const TOKEN_KEY = 'iyuu_crossseed_token_v1';
const SID_SHA1_CACHE_KEY = 'iyuu_sid_sha1_cache_v1';
function getStoredToken(){ try { return GM_getValue(TOKEN_KEY, '') || ''; } catch{} return ''; }
function setStoredToken(v){ try { GM_setValue(TOKEN_KEY, v || ''); } catch{} }
function getToken(){ const t = getStoredToken(); if (t) return t; if (IYUU_TOKEN_DEFAULT) return IYUU_TOKEN_DEFAULT; return ''; }
function clearSidSha1Cache(){ try { GM_deleteValue && GM_deleteValue(SID_SHA1_CACHE_KEY); } catch{} try { localStorage.removeItem(SID_SHA1_CACHE_KEY); } catch{} }
function maskToken(t){ if(!t) return '(未设置)'; if(t.length<=8) return t; return `${t.slice(0,4)}…${t.slice(-4)}`; }
function updateTokenMask(){ tokenMaskEl.textContent = maskToken(getToken()); }
updateTokenMask();
eyeBtn.addEventListener('click', () => { tokenInput.type = tokenInput.type === 'password' ? 'text' : 'password'; });
saveBtn.addEventListener('click', () => {
const v = (tokenInput.value || '').trim();
if (!v) { tokenInput.focus(); return; }
setStoredToken(v); clearSidSha1Cache(); updateTokenMask(); tokenInput.value = '';
runDetection();
});
/* ========== 3) Base32 → Hex(BTIH) ========== */
const B32MAP = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
function base32ToHex(b32){
b32 = (b32 || '').replace(/=+$/,'').toUpperCase();
let bits = '', hex='';
for (const ch of b32){ const v = B32MAP.indexOf(ch); if(v<0) return ''; bits += v.toString(2).padStart(5,'0'); }
for (let i=0;i+8<=bits.length;i+=8) hex += parseInt(bits.slice(i,i+8),2).toString(16).padStart(2,'0');
return hex;
}
/* ========== 4) 提取 infohash(页面/脚本/磁链/属性) ========== */
function extractInfoHashEnhanced() {
try {
for (const code of Array.from(document.scripts).map(s => s.textContent || '')) {
const m = code.match(/['"]([a-fA-F0-9]{40})['"]/); if (m) return m[1].toLowerCase();
}
const m2 = (document.body.innerText || '').match(/\b([a-fA-F0-9]{40})\b/); if (m2) return m2[1].toLowerCase();
const usp = new URL(location.href).searchParams;
const urlHash = usp.get('infohash') || usp.get('hash'); if (urlHash && /^[a-fA-F0-9]{40}$/.test(urlHash)) return urlHash.toLowerCase();
for (const a of Array.from(document.querySelectorAll('a[href^="magnet:"]'))) {
const u = new URL(a.getAttribute('href')); const xt = (u.searchParams.get('xt') || '').split(':').pop();
if (!xt) continue;
if (/^[a-fA-F0-9]{40}$/.test(xt)) return xt.toLowerCase();
if (/^[A-Z2-7]{32}$/i.test(xt)) { const hex = base32ToHex(xt); if (hex && hex.length >= 40) return hex.slice(0,40).toLowerCase(); }
}
const attrHex = document.querySelector('[data-infohash], [data-hash], [title*="infohash"], [title*="Info Hash"]');
if (attrHex){
const cands = [attrHex.getAttribute('data-infohash'), attrHex.getAttribute('data-hash'), attrHex.getAttribute('title')].filter(Boolean).join(' ');
const m = cands.match(/\b([a-fA-F0-9]{40})\b/); if (m) return m[1].toLowerCase();
}
} catch {}
return '';
}
/* ========== 5) 从页面定位 .torrent 下载地址(更全面) ========== */
function findTorrentDownloadURL() {
const passkeyA = Array.from(document.querySelectorAll('a[href*="download.php?id="]'))
.find(a => /passkey=/.test(a.getAttribute('href') || ''));
if (passkeyA) return new URL(passkeyA.getAttribute('href'), location.href).href;
let a = document.querySelector('a[href*="download.php?id="], a[href*="/download.php?id="]');
if (a) return new URL(a.getAttribute('href'), location.href).href;
const byText = Array.from(document.querySelectorAll('a')).find(x => /下载种子|下载地址|\.torrent/i.test(x.textContent||''));
if (byText) return new URL(byText.getAttribute('href'), location.href).href;
const onclickA = Array.from(document.querySelectorAll('a[onclick]')).find(x => /download\.php\?id=\d+/.test(x.getAttribute('onclick')||''));
if (onclickA) {
const m = (onclickA.getAttribute('onclick')||'').match(/download\.php\?id=\d+/i);
if (m) return new URL(m[0], location.href).href;
}
return '';
}
/* ========== 6) 兜底:下载 .torrent 并计算 infohash ========== */
async function fetchInfohashFromTorrent() {
const href = findTorrentDownloadURL();
if (!href) return '';
return new Promise((resolve) => {
GM_xmlhttpRequest({
method: 'GET',
url: href,
responseType: 'arraybuffer',
timeout: 30000,
anonymous: false,
headers: { Referer: location.href },
onload: async (r) => {
try {
const headers = (r.responseHeaders || '').toLowerCase();
if (headers.includes('content-type: text/html') && !headers.includes('application/x-bittorrent')) return resolve('');
const buf = r.response;
if (!buf) return resolve('');
const ih = await computeInfohashFromTorrentBytes(buf);
resolve(ih || '');
} catch { resolve(''); }
},
onerror: () => resolve(''),
ontimeout: () => resolve('')
});
});
}
/* ========== 7) 可靠 bencode 解析:取 info 原始字节做 SHA-1(BT v1) ========== */
async function computeInfohashFromTorrentBytes(buf) {
const b = new Uint8Array(buf);
function readLen(pos) {
let i = pos, len = 0;
if (i >= b.length || b[i] < 0x30 || b[i] > 0x39) throw new Error('len: expect digit');
while (i < b.length && b[i] >= 0x30 && b[i] <= 0x39) { len = len * 10 + (b[i] - 0x30); i++; }
if (b[i] !== 0x3A) throw new Error('len: missing colon');
return { len, next: i + 1 };
}
function readValueEnd(pos) {
const c = b[pos];
if (c === 0x69) {
let i = pos + 1;
if (b[i] === 0x2D) i++;
if (i >= b.length || b[i] < 0x30 || b[i] > 0x39) throw new Error('int: expect digit');
while (i < b.length && b[i] >= 0x30 && b[i] <= 0x39) i++;
if (b[i] !== 0x65) throw new Error('int: missing e');
return i + 1;
}
if (c === 0x6C) {
let i = pos + 1;
while (b[i] !== 0x65) { i = readValueEnd(i); }
return i + 1;
}
if (c === 0x64) {
let i = pos + 1;
while (b[i] !== 0x65) {
const { len, next } = readLen(i);
const keyStart = next, keyEnd = next + len;
const key = new TextDecoder().decode(b.slice(keyStart, keyEnd));
i = keyEnd;
if (key === 'info') {
const valStart = i;
const valEnd = readValueEnd(i);
const endPos = (typeof valEnd === 'number') ? valEnd : valEnd.end;
const infoSlice = b.slice(valStart, endPos);
return crypto.subtle.digest('SHA-1', infoSlice).then(d => {
const hex = Array.from(new Uint8Array(d)).map(x => x.toString(16).padStart(2,'0')).join('');
return { end: endPos, infohash: hex };
});
} else {
i = readValueEnd(i);
}
}
return i + 1;
}
if (c >= 0x30 && c <= 0x39) {
const { len, next } = readLen(pos);
return next + len;
}
throw new Error('value: bad prefix ' + c);
}
if (b[0] !== 0x64) throw new Error('torrent root not dict');
let i = 1;
while (b[i] !== 0x65) {
const { len, next } = readLen(i);
const keyStart = next, keyEnd = next + len;
const key = new TextDecoder().decode(b.slice(keyStart, keyEnd));
i = keyEnd;
if (key === 'info') {
const valStart = i;
const out = await readValueEnd(i);
if (typeof out === 'object' && out.infohash) return out.infohash;
const infoSlice = b.slice(valStart, out);
const d = await crypto.subtle.digest('SHA-1', infoSlice);
return Array.from(new Uint8Array(d)).map(x => x.toString(16).padStart(2,'0')).join('');
} else {
i = await readValueEnd(i);
}
}
return '';
}
/* ========== 8) IYUU API 封装 ========== */
const API_BASE = 'https://2025.iyuu.cn';
const httpGet = (url, headers={}) => new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method:'GET', url, headers: Object.assign({'Token': getToken()}, headers), timeout: 20000,
onload:r=> (r.status>=200 && r.status<300) ? resolve(r.responseText) : reject(new Error(`HTTP ${r.status}`)),
onerror:reject, ontimeout:()=>reject(new Error('timeout'))
});
});
const httpPost = (url, data, headers={}) => new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method:'POST', url, data, headers: Object.assign({'Token': getToken()}, headers), timeout: 20000,
onload:r=> (r.status>=200 && r.status<300) ? resolve(r.responseText) : reject(new Error(`HTTP ${r.status}`)),
onerror:reject, ontimeout:()=>reject(new Error('timeout'))
});
});
async function sha1Hex(str){ const enc=new TextEncoder().encode(str); const buf=await crypto.subtle.digest('SHA-1', enc); return Array.from(new Uint8Array(buf)).map(b=>b.toString(16).padStart(2,'0')).join(''); }
function loadSidSha1(){ try { const o=JSON.parse(localStorage.getItem('iyuu_sid_sha1_cache_v1')||'{}'); if(o.sid_sha1 && o.expire>Date.now()) return o.sid_sha1; } catch{} return null; }
function saveSidSha1(v){ try { const seven=7*24*3600*1000; const o={sid_sha1:v, expire:Date.now()+seven}; localStorage.setItem('iyuu_sid_sha1_cache_v1', JSON.stringify(o)); } catch{} }
/* ========== 9) 主流程 ========== */
async function runDetection(){
chipsEl.innerHTML = '';
let infohash = extractInfoHashEnhanced();
if (!infohash) {
try { infohash = await fetchInfohashFromTorrent(); } catch {}
}
if (!infohash) {
setBadge('err','缺少 hash');
hashEl.textContent='hash: 未识别';
showEmpty('当前页面未能识别到 infohash。已尝试 .torrent 解析仍失败(可能为 v2-only 或下载被替换为 HTML)。');
return;
} else {
hashEl.textContent = `hash: ${infohash.slice(0,8)}…`;
}
const token = getToken();
if (!token) { setBadge('err','未设置 Token'); showEmpty('请在右侧输入框粘贴 Token 并点击“保存Token”。'); return; }
try {
setBadge('', '检测中');
const sitesResp = JSON.parse(await httpGet(`${API_BASE}/reseed/sites/index`));
if (sitesResp.code !== 0) throw new Error(sitesResp.msg || 'sites/index 失败');
const sites = sitesResp.data?.sites || [];
const allSid = sites.map(s => s.id);
let sid_sha1 = loadSidSha1();
if (!sid_sha1) {
const reportResp = JSON.parse(await httpPost(`${API_BASE}/reseed/sites/reportExisting`, JSON.stringify({ sid_list: allSid }), { 'Content-Type':'application/json' }));
if (reportResp.code !== 0) throw new Error(reportResp.msg || 'reportExisting 失败');
sid_sha1 = reportResp.data?.sid_sha1; if (!sid_sha1) throw new Error('缺少 sid_sha1');
saveSidSha1(sid_sha1);
}
const hashes = [infohash].sort();
const jsonStr = JSON.stringify(hashes);
const sha1 = await sha1Hex(jsonStr);
const timestamp = Math.floor(Date.now()/1000).toString();
const version = '8.2.0';
const form = new URLSearchParams();
form.set('hash', jsonStr); form.set('sha1', sha1); form.set('sid_sha1', sid_sha1);
form.set('timestamp', timestamp); form.set('version', version);
const reseedResp = JSON.parse(await httpPost(`${API_BASE}/reseed/index/index`, form.toString(), { 'Content-Type': 'application/x-www-form-urlencoded' }));
if (reseedResp.code !== 0) throw new Error(reseedResp.msg || 'reseed/index 失败');
const data = reseedResp.data || {};
const firstKey = Object.keys(data)[0];
const items = (firstKey && data[firstKey]?.torrent) ? data[firstKey].torrent : [];
if (!items.length) { setBadge('no','未发现'); showEmpty(); return; }
setBadge('ok','已获取');
const bySid = new Map();
for (const t of items) { const sid = t.sid; if (!bySid.has(sid)) bySid.set(sid, []); bySid.get(sid).push(t); }
for (const [sid, arr] of bySid.entries()) {
const s = sites.find(x => x.id === sid); if (!s) continue;
const id = arr[0].torrent_id;
const scheme = (s.is_https === 0) ? 'http' : 'https';
const details = (s.details_page || 'details.php?id={}').replace('{}', id);
const href = `${scheme}://${s.base_url}/${details}`;
const iconURL = lookupIconURL({ sid, nickname: s.nickname, site: s.site });
const label = s.nickname || s.site || String(sid);
addChip({ label, href, ok: true, count: arr.length, iconURL });
}
} catch (e) {
setBadge('err','失败'); showEmpty(String(e && e.message || e));
try { console.error('[IYUU-crossseed]', e); } catch {}
}
}
/* ========== 10) 首次进入页面 ========== */
if (getToken()) runDetection();
else { setBadge('err','未设置 Token'); showEmpty('请在右侧输入框粘贴 Token 并点击“保存Token”。'); }
})();