IYUU 全站辅种检测

实现在种子详情页显示该种在其他站点存在情况

// ==UserScript==
// @name         IYUU 全站辅种检测
// @namespace    iyuu-crossseed
// @version      1.0.3
// @description  实现在种子详情页显示该种在其他站点存在情况
// @author       YourName
// @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
// @connect      2025.iyuu.cn
// @connect      *
// ==/UserScript==

(function () {
  'use strict';

  /*** 基础配置(逻辑保持不变) ***/
  const IYUU_TOKEN_DEFAULT = '';
  const AUTO_KEY = 'iyuu_auto_query_v1';
  function getAutoQuery() {
    try { const v = GM_getValue(AUTO_KEY); if (typeof v === 'boolean') return v; } catch {}
    try { const v2 = localStorage.getItem(AUTO_KEY); if (v2 != null) return v2 === 'true'; } catch {}
    return true;
  }
  function setAutoQuery(v) {
    try { GM_setValue(AUTO_KEY, !!v); } catch {}
    try { localStorage.setItem(AUTO_KEY, (!!v).toString()); } catch {}
  }

  /*** 站点图标映射(逻辑保持不变) ***/
  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:{} };
  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 s=document.createElement('style'); s.textContent=css; (document.head||document.documentElement).appendChild(s); }
  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 sels=['#outer','#wrapper','#maincontent','#content','.main','body']; for(const sel of sels){ 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{position:relative;display:block;padding:10px 14px 66px 14px;font:12.5px/1.45 system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial}
    .iyuu-header{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
    .iyuu-title{font-weight:700;margin-right:2px;white-space:nowrap}
    .iyuu-hash{opacity:.9;max-width:46vw;text-overflow:ellipsis;overflow:hidden;white-space:nowrap}
    .iyuu-msg{opacity:.9;color:#e5e7eb;max-width:32vw;text-overflow:ellipsis;overflow:hidden;white-space:nowrap}
    .iyuu-divider{height:18px;width:1px;background:#ffffff1a;margin:0 4px}
    .iyuu-badge{font-size:12px;padding:2px 8px;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-top-right{position:absolute;right:14px;top:10px;display:flex;align-items:center;gap:8px;z-index:2}
    .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;font-size:12.5px}
    .iyuu-token-mask{opacity:.85}
    .iyuu-eye{cursor:pointer;user-select:none;opacity:.9}
    .iyuu-btn{padding:6px 10px;border-radius:8px;border:none;cursor:pointer;background:#1e293b;color:#fff;font-size:12.5px}
    .iyuu-btn:hover{filter:brightness(1.05)}
    .iyuu-top-spacer{height:44px}
    .iyuu-chips{display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:10px;align-items:stretch;width:100%}
    .iyuu-chip{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:4px;padding:8px 10px;border-radius:12px;background:#0f172a;border:1px solid #243045;text-decoration:none;color:#dbeafe;min-height:68px;box-sizing:border-box;text-align:center}
    .iyuu-chip.ok{border-color:#22c55e;color:#dcfce7}
    .iyuu-chip:hover{filter:brightness(1.05)}
    .iyuu-icon{width:28px;height:28px;display:block;object-fit:contain}
    .iyuu-label{display:block;line-height:1.22;font-size:13.5px}
    .iyuu-count{opacity:.85;font-size:10.5px}
    .iyuu-chip.noicon .iyuu-label{font-size:14.5px}
    .iyuu-empty{opacity:.85}
    .iyuu-foot-left,.iyuu-foot-right{position:absolute}
    .iyuu-foot-left{left:14px;bottom:12px;display:flex;align-items:center;gap:8px}
    .iyuu-mode-text{opacity:.95;font-size:12.5px}
    .iyuu-switch{position:relative;display:inline-block;width:44px;height:22px;vertical-align:middle}
    .iyuu-switch input{opacity:0;width:0;height:0}
    .iyuu-slider{position:absolute;cursor:pointer;top:0;left:0;right:0;bottom:0;background:#334155;border-radius:999px;transition:.2s}
    .iyuu-slider:before{position:absolute;content:"";height:18px;width:18px;left:2px;top:2px;background:white;border-radius:50%;transition:.2s}
    .iyuu-switch input:checked + .iyuu-slider{background:#22c55e}
    .iyuu-switch input:checked + .iyuu-slider:before{transform:translateX(22px)}
    #iyuu-manual-query{padding:9px 14px;font-size:12.5px;border-radius:10px;min-width:112px;box-shadow:0 2px 6px rgba(0,0,0,.22)}
    .iyuu-foot-right{right:14px;bottom:10px}
    @media (max-width:640px){.iyuu-input input{width:150px}.iyuu-hash{max-width:42vw}.iyuu-msg{max-width:28vw}}
  `);

  /*** DOM ***/
  const bar = document.createElement('div');
  bar.className = 'iyuu-topbar';
  bar.innerHTML = `
    <div class="iyuu-topbar-inner">
      <div class="iyuu-header">
        <span class="iyuu-title">IYUU 全站检测</span>
        <span class="iyuu-hash" id="iyuu-hash">hash: ——</span>
        <span class="iyuu-msg" id="iyuu-msg"></span>
        <span class="iyuu-divider"></span>
        <span class="iyuu-badge" id="iyuu-badge">待检测</span>
      </div>

      <div class="iyuu-top-right" id="iyuu-top-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 class="iyuu-top-spacer"></div>
      <div class="iyuu-chips" id="iyuu-chips"></div>

      <div class="iyuu-foot-left">
        <label class="iyuu-mode-text" id="iyuu-mode-label">自动查询</label>
        <label class="iyuu-switch" title="切换自动/手动查询">
          <input type="checkbox" id="iyuu-auto-toggle" />
          <span class="iyuu-slider"></span>
        </label>
      </div>

      <div class="iyuu-foot-right">
        <button class="iyuu-btn" id="iyuu-manual-query" style="display:none;">查询</button>
      </div>
    </div>
  `;
  safePrepend(findTopContainer(), bar);

  /*** 元素引用(去除多余空格以免 no-multi-spaces) ***/
  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 msgEl = bar.querySelector('#iyuu-msg'); // 新增:与 hash 同行的提示位
  const autoToggle = bar.querySelector('#iyuu-auto-toggle');
  const modeLabel = bar.querySelector('#iyuu-mode-label');
  const manualBtn = bar.querySelector('#iyuu-manual-query');

  const setBadge = (cls, text) => { badgeEl.className = `iyuu-badge ${cls || ''}`.trim(); badgeEl.textContent = text; };
  const setMessage = (text = '') => { msgEl.textContent = text || ''; };

  /*** 将技术错误“翻译成人话”,避免显示 HTTP 429/403 等码 ***/
  function humanizeError(err) {
    const raw = String((err && err.message) || err || '').toLowerCase();

    // 常见网络情形识别
    if (raw.includes('429') || raw.includes('too many') || raw.includes('频率') || raw.includes('limit')) {
      return '请求频繁,请稍后再试。';
    }
    if (raw.includes('timeout') || raw.includes('time out') || raw.includes('timed out')) {
      return '网络超时,请稍后再试。';
    }
    if (raw.includes('403') || raw.includes('forbidden') || raw.includes('unauthorized') || raw.includes('401')) {
      return '访问被拒绝,可能是 Token 无效。';
    }
    if (raw.includes('network') || raw.includes('failed to fetch') || raw.includes('error') || raw.includes('http')) {
      return '网络出现问题,稍后重试或检查网络环境。';
    }
    // 默认兜底:不给出代码,只给通用说明
    return '请求失败,请稍后再试。';
  }

  /*** 站点卡片 ***/
  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);
    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);
  };

  /*** 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{} try { return localStorage.getItem(TOKEN_KEY) || ''; } catch{} return ''; }
  function setStoredToken(v){ try { GM_setValue(TOKEN_KEY, v || ''); } catch{} try { localStorage.setItem(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(){ const t = getToken(); tokenMaskEl.textContent = maskToken(t); }
  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 = '';
    if (getAutoQuery()) runDetection(); else parseHashOnly();
  });

  /*** Hash 提取/.torrent 解析(逻辑不变) ***/
  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;
  }

  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 '';
  }

  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;
    const 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 '';
  }

  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('')
      });
    });
  }

  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) { // int
        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) { // list
        let i = pos + 1;
        while (b[i] !== 0x65) { i = readValueEnd(i); }
        return i + 1;
      }
      if (c === 0x64) { // dict
        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) { // str
        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 '';
  }

  /*** 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{} }

  /*** 模式 UI 联动(逻辑不变) ***/
  function updateAutoQueryUI(){
    const isAuto = getAutoQuery();
    autoToggle.checked = isAuto;
    modeLabel.textContent = isAuto ? '自动查询' : '手动查询';
    manualBtn.style.display = isAuto ? 'none' : '';
  }

  /*** 手动模式:仅解析 hash(无“点击右下角查询”提示) ***/
  async function parseHashOnly() {
    chipsEl.innerHTML = '';
    setMessage('');
    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;
    }
    hashEl.textContent = `hash: ${infohash.slice(0,8)}…`;
    setBadge('', '待检测');
  }

  /*** 主流程(逻辑不变;错误提示人类化并放在hash同行) ***/
  async function runDetection(forceApi = false){
    const isAuto = getAutoQuery();
    if (!isAuto && !forceApi) { await parseHashOnly(); return; }

    chipsEl.innerHTML = '';
    setMessage('');

    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','失败');
      setMessage(humanizeError(e));
      // 不再把原始技术码暴露给用户
      try { console.error('[IYUU-crossseed]', e); } catch {}
    }
  }

  /*** 绑定与初始化(逻辑不变) ***/
  function initAutoToggle(){
    autoToggle.checked = getAutoQuery();
    updateAutoQueryUI();
    autoToggle.addEventListener('change', async () => {
      const willAuto = autoToggle.checked;
      setAutoQuery(willAuto);
      updateAutoQueryUI();
      if (willAuto) runDetection(); else parseHashOnly();
    });
  }

  manualBtn.addEventListener('click', () => { runDetection(true); });

  safePrepend(findTopContainer(), bar);
  initAutoToggle();

  if (getToken()) { if (getAutoQuery()) runDetection(); else parseHashOnly(); }
  else { setBadge('err','未设置 Token'); showEmpty('请在右上角输入框粘贴 Token 并点击“保存Token”。'); }
})();