<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Linserin Tools</title>
    <link>/</link>
    <description>Recent content on Linserin Tools</description>
    <generator>Hugo</generator>
    <language>zh-cn</language>
    <lastBuildDate>Mon, 22 Jun 2026 10:52:18 +0800</lastBuildDate>
    <atom:link href="/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>代码高亮</title>
      <link>/list/code-highlight/</link>
      <pubDate>Mon, 22 Jun 2026 10:52:18 +0800</pubDate>
      <guid>/list/code-highlight/</guid>
      <description>&lt;h2 id=&#34;使用方式&#34;&gt;使用方式&lt;/h2&gt;&#xA;&lt;p&gt;Past your code.&lt;/p&gt;&#xA;&lt;p&gt;Powered by &lt;a href=&#34;http://highlightjs.org&#34;&gt;highlight.js&lt;/a&gt;&lt;/p&gt;&#xA;&lt;!DOCTYPE html&gt;&#xD;&#xA;&lt;html lang=&#34;zh-CN&#34;&gt;&#xD;&#xA;&lt;head&gt;&#xD;&#xA;&lt;meta charset=&#34;UTF-8&#34;&gt;&#xD;&#xA;&lt;meta name=&#34;viewport&#34; content=&#34;width=device-width, initial-scale=1&#34;&gt;&#xD;&#xA;&lt;link rel=&#34;stylesheet&#34; id=&#34;themeCss&#34; href=&#34;https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.10.0/styles/default.min.css&#34;&gt;&#xD;&#xA;&lt;script src=&#34;https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.10.0/highlight.min.js&#34;&gt;&lt;/script&gt;&#xD;&#xA;&lt;script src=&#34;https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js&#34;&gt;&lt;/script&gt;&#xD;&#xA;&lt;/head&gt;&#xD;&#xA;&lt;body style=&#34;margin:0;padding:20px;font-family:sans-serif;display:flex;flex-direction:column;align-items:center;min-height:100vh;box-sizing:border-box&#34;&gt;&#xD;&#xA;&#xD;&#xA;&lt;div style=&#34;max-width:800px;width:100%&#34;&gt;&#xD;&#xA;  &lt;h2 style=&#34;margin:0 0 12px;font-size:18px;font-weight:600;color:#333&#34;&gt;&lt;/h2&gt;&#xD;&#xA;&#xD;&#xA;  &lt;div style=&#34;display:flex;gap:8px;margin-bottom:12px;flex-wrap:wrap&#34;&gt;&#xD;&#xA;    &lt;select id=&#34;lang&#34; style=&#34;padding:6px 10px;font-size:13px;border:1px solid #ccc;border-radius:4px;background:#fff&#34;&gt;&#xD;&#xA;      &lt;option value=&#34;&#34;&gt;自动检测&lt;/option&gt;&#xD;&#xA;      &lt;option value=&#34;html&#34;&gt;HTML&lt;/option&gt;&#xD;&#xA;      &lt;option value=&#34;css&#34;&gt;CSS&lt;/option&gt;&#xD;&#xA;      &lt;option value=&#34;javascript&#34;&gt;JavaScript&lt;/option&gt;&#xD;&#xA;      &lt;option value=&#34;typescript&#34;&gt;TypeScript&lt;/option&gt;&#xD;&#xA;      &lt;option value=&#34;python&#34;&gt;Python&lt;/option&gt;&#xD;&#xA;      &lt;option value=&#34;go&#34;&gt;Go&lt;/option&gt;&#xD;&#xA;      &lt;option value=&#34;java&#34;&gt;Java&lt;/option&gt;&#xD;&#xA;      &lt;option value=&#34;cpp&#34;&gt;C++&lt;/option&gt;&#xD;&#xA;      &lt;option value=&#34;c&#34;&gt;C&lt;/option&gt;&#xD;&#xA;      &lt;option value=&#34;rust&#34;&gt;Rust&lt;/option&gt;&#xD;&#xA;      &lt;option value=&#34;sql&#34;&gt;SQL&lt;/option&gt;&#xD;&#xA;      &lt;option value=&#34;bash&#34;&gt;Bash&lt;/option&gt;&#xD;&#xA;      &lt;option value=&#34;json&#34;&gt;JSON&lt;/option&gt;&#xD;&#xA;      &lt;option value=&#34;xml&#34;&gt;XML&lt;/option&gt;&#xD;&#xA;      &lt;option value=&#34;markdown&#34;&gt;Markdown&lt;/option&gt;&#xD;&#xA;    &lt;/select&gt;&#xD;&#xA;    &lt;select id=&#34;theme&#34; style=&#34;padding:6px 10px;font-size:13px;border:1px solid #ccc;border-radius:4px;background:#fff&#34;&gt;&#xD;&#xA;      &lt;option value=&#34;default&#34;&gt;Default&lt;/option&gt;&#xD;&#xA;      &lt;option value=&#34;github&#34;&gt;GitHub&lt;/option&gt;&#xD;&#xA;      &lt;option value=&#34;github-dark&#34;&gt;GitHub Dark&lt;/option&gt;&#xD;&#xA;      &lt;option value=&#34;vs&#34;&gt;VS&lt;/option&gt;&#xD;&#xA;      &lt;option value=&#34;vs2015&#34;&gt;VS2015&lt;/option&gt;&#xD;&#xA;      &lt;option value=&#34;atom-one-dark&#34;&gt;Atom One Dark&lt;/option&gt;&#xD;&#xA;      &lt;option value=&#34;atom-one-light&#34;&gt;Atom One Light&lt;/option&gt;&#xD;&#xA;      &lt;option value=&#34;monokai&#34;&gt;Monokai&lt;/option&gt;&#xD;&#xA;      &lt;option value=&#34;monokai-sublime&#34;&gt;Monokai Sublime&lt;/option&gt;&#xD;&#xA;      &lt;option value=&#34;dracula&#34;&gt;Dracula&lt;/option&gt;&#xD;&#xA;      &lt;option value=&#34;nord&#34;&gt;Nord&lt;/option&gt;&#xD;&#xA;      &lt;option value=&#34;solarized-light&#34;&gt;Solarized Light&lt;/option&gt;&#xD;&#xA;      &lt;option value=&#34;solarized-dark&#34;&gt;Solarized Dark&lt;/option&gt;&#xD;&#xA;      &lt;option value=&#34;tomorrow&#34;&gt;Tomorrow&lt;/option&gt;&#xD;&#xA;      &lt;option value=&#34;tomorrow-night&#34;&gt;Tomorrow Night&lt;/option&gt;&#xD;&#xA;      &lt;option value=&#34;stackoverflow-light&#34;&gt;StackOverflow Light&lt;/option&gt;&#xD;&#xA;      &lt;option value=&#34;stackoverflow-dark&#34;&gt;StackOverflow Dark&lt;/option&gt;&#xD;&#xA;      &lt;option value=&#34;androidstudio&#34;&gt;Android Studio&lt;/option&gt;&#xD;&#xA;      &lt;option value=&#34;agate&#34;&gt;Agate&lt;/option&gt;&#xD;&#xA;      &lt;option value=&#34;shades-of-purple&#34;&gt;Shades of Purple&lt;/option&gt;&#xD;&#xA;      &lt;option value=&#34;obsidian&#34;&gt;Obsidian&lt;/option&gt;&#xD;&#xA;      &lt;option value=&#34;rainbow&#34;&gt;Rainbow&lt;/option&gt;&#xD;&#xA;    &lt;/select&gt;&#xD;&#xA;    &lt;button id=&#34;downloadBtn&#34; style=&#34;padding:6px 16px;font-size:13px;border:none;border-radius:4px;background:#333;color:#fff;cursor:pointer&#34;&gt;下载为 PNG&lt;/button&gt;&#xD;&#xA;    &lt;button id=&#34;copyBtn&#34; style=&#34;padding:6px 16px;font-size:13px;border:1px solid #ccc;border-radius:4px;background:#fff;color:#333;cursor:pointer&#34;&gt;复制 HTML&lt;/button&gt;&#xD;&#xA;    &lt;span id=&#34;status&#34; style=&#34;font-size:12px;color:#999;align-self:center;margin-left:auto&#34;&gt;&lt;/span&gt;&#xD;&#xA;  &lt;/div&gt;&#xD;&#xA;&#xD;&#xA;  &lt;textarea id=&#34;input&#34; placeholder=&#34;在此粘贴或输入代码…&#34; style=&#34;width:100%;height:200px;padding:12px;font-family:monospace;font-size:14px;border:1px solid #ddd;border-radius:4px;resize:vertical;box-sizing:border-box;tab-size:2;white-space:pre;overflow:auto&#34;&gt;&lt;/textarea&gt;&#xD;&#xA;&#xD;&#xA;  &lt;div id=&#34;preview&#34; style=&#34;margin-top:12px;border-radius:4px;overflow:hidden;background:#fff;box-shadow:0 1px 4px rgba(0,0,0,.1)&#34;&gt;&#xD;&#xA;    &lt;pre style=&#34;margin:0;padding:16px;overflow:auto;font-size:14px;line-height:1.5&#34;&gt;&lt;code id=&#34;output&#34; class=&#34;hljs&#34;&gt;&lt;/code&gt;&lt;/pre&gt;&#xD;&#xA;  &lt;/div&gt;&#xD;&#xA;&#xD;&#xA;  &lt;p style=&#34;margin:8px 0 0;font-size:12px;color:#aaa;text-align:center&#34;&gt;输入代码后自动高亮，点击「下载为 PNG」保存为图片&lt;/p&gt;&#xD;&#xA;&lt;/div&gt;&#xD;&#xA;&#xD;&#xA;&lt;script&gt;&#xD;&#xA;const input = document.getElementById(&#39;input&#39;);&#xD;&#xA;const output = document.getElementById(&#39;output&#39;);&#xD;&#xA;const langSelect = document.getElementById(&#39;lang&#39;);&#xD;&#xA;const themeSelect = document.getElementById(&#39;theme&#39;);&#xD;&#xA;const themeCss = document.getElementById(&#39;themeCss&#39;);&#xD;&#xA;const status = document.getElementById(&#39;status&#39;);&#xD;&#xA;const preview = document.getElementById(&#39;preview&#39;);&#xD;&#xA;const previewBox = preview.querySelector(&#39;pre&#39;);&#xD;&#xA;let timer = null;&#xD;&#xA;&#xD;&#xA;&#xD;&#xA;const darkThemes = new Set([&#xD;&#xA;  &#39;github-dark&#39;,&#39;vs2015&#39;,&#39;atom-one-dark&#39;,&#39;monokai&#39;,&#39;monokai-sublime&#39;,&#xD;&#xA;  &#39;dracula&#39;,&#39;nord&#39;,&#39;solarized-dark&#39;,&#39;tomorrow-night&#39;,&#xD;&#xA;  &#39;stackoverflow-dark&#39;,&#39;androidstudio&#39;,&#39;agate&#39;,&#39;shades-of-purple&#39;,&#39;obsidian&#39;&#xD;&#xA;]);&#xD;&#xA;&#xD;&#xA;function switchTheme(name) {&#xD;&#xA;  themeCss.href = &#39;https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.10.0/styles/&#39; + name + &#39;.min.css&#39;;&#xD;&#xA;  const isDark = darkThemes.has(name);&#xD;&#xA;  preview.style.background = isDark ? &#39;#1e1e1e&#39; : &#39;#fff&#39;;&#xD;&#xA;  previewBox.style.background = isDark ? &#39;#1e1e1e&#39; : &#39;#fff&#39;;&#xD;&#xA;}&#xD;&#xA;&#xD;&#xA;themeSelect.addEventListener(&#39;change&#39;, () =&gt; switchTheme(themeSelect.value));&#xD;&#xA;&#xD;&#xA;function highlight() {&#xD;&#xA;  const code = input.value;&#xD;&#xA;  const lang = langSelect.value;&#xD;&#xA;  const el = document.createElement(&#39;code&#39;);&#xD;&#xA;  el.className = &#39;hljs&#39;;&#xD;&#xA;  if (lang) el.className += &#39; language-&#39; + lang;&#xD;&#xA;  el.textContent = code || &#39; &#39;;&#xD;&#xA;  if (code) {&#xD;&#xA;    try {&#xD;&#xA;      if (lang) {&#xD;&#xA;        el.innerHTML = hljs.highlight(code, { language: lang }).value;&#xD;&#xA;      } else {&#xD;&#xA;        el.innerHTML = hljs.highlightAuto(code).value;&#xD;&#xA;      }&#xD;&#xA;    } catch (e) {&#xD;&#xA;      el.textContent = code;&#xD;&#xA;    }&#xD;&#xA;  }&#xD;&#xA;  output.innerHTML = el.innerHTML;&#xD;&#xA;  output.className = el.className;&#xD;&#xA;}&#xD;&#xA;&#xD;&#xA;input.addEventListener(&#39;input&#39;, () =&gt; {&#xD;&#xA;  clearTimeout(timer);&#xD;&#xA;  timer = setTimeout(highlight, 200);&#xD;&#xA;});&#xD;&#xA;&#xD;&#xA;langSelect.addEventListener(&#39;change&#39;, highlight);&#xD;&#xA;&#xD;&#xA;document.getElementById(&#39;downloadBtn&#39;).addEventListener(&#39;click&#39;, async () =&gt; {&#xD;&#xA;  if (!input.value.trim()) { status.textContent = &#39;⚠️ 请先输入代码&#39;; return; }&#xD;&#xA;  status.textContent = &#39;⏳ 生成中…&#39;;&#xD;&#xA;  try {&#xD;&#xA;    const pre = preview.querySelector(&#39;pre&#39;);&#xD;&#xA;    const origOverflow = pre.style.overflow;&#xD;&#xA;    pre.style.overflow = &#39;visible&#39;;&#xD;&#xA;    const isDark = darkThemes.has(themeSelect.value);&#xD;&#xA;    const canvas = await html2canvas(preview, {&#xD;&#xA;      scale: 2,&#xD;&#xA;      backgroundColor: isDark ? &#39;#1e1e1e&#39; : &#39;#fff&#39;,&#xD;&#xA;      allowTaint: false,&#xD;&#xA;      useCORS: true,&#xD;&#xA;      logging: false,&#xD;&#xA;    });&#xD;&#xA;    pre.style.overflow = origOverflow;&#xD;&#xA;    const link = document.createElement(&#39;a&#39;);&#xD;&#xA;    link.download = &#39;code.png&#39;;&#xD;&#xA;    link.href = canvas.toDataURL(&#39;image/png&#39;);&#xD;&#xA;    link.click();&#xD;&#xA;    status.textContent = &#39;✅ 已下载&#39;;&#xD;&#xA;  } catch (e) {&#xD;&#xA;    status.textContent = &#39;❌ 生成失败&#39;;&#xD;&#xA;    console.error(e);&#xD;&#xA;  }&#xD;&#xA;});&#xD;&#xA;&#xD;&#xA;document.getElementById(&#39;copyBtn&#39;).addEventListener(&#39;click&#39;, async () =&gt; {&#xD;&#xA;  const html = output.innerHTML;&#xD;&#xA;  if (!html.trim() || html === &#39;&lt;span class=&#34;hljs&#34;&gt;&lt;/span&gt;&#39;) { status.textContent = &#39;⚠️ 请先输入代码&#39;; return; }&#xD;&#xA;  try {&#xD;&#xA;    await navigator.clipboard.writeText(&#xD;&#xA;      &#39;&lt;pre&gt;&lt;code class=&#34;hljs&#39; + (langSelect.value ? &#39; language-&#39; + langSelect.value : &#39;&#39;) + &#39;&#34;&gt;&#39; + html + &#39;&lt;/code&gt;&lt;/pre&gt;&#39;&#xD;&#xA;    );&#xD;&#xA;    status.textContent = &#39;✅ 已复制&#39;;&#xD;&#xA;  } catch {&#xD;&#xA;    status.textContent = &#39;❌ 复制失败&#39;;&#xD;&#xA;  }&#xD;&#xA;});&#xD;&#xA;&#xD;&#xA;highlight();&#xD;&#xA;&lt;/script&gt;&#xD;&#xA;&lt;/body&gt;&#xD;&#xA;&lt;/html&gt;</description>
    </item>
    <item>
      <title>2FA</title>
      <link>/list/2fa/</link>
      <pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate>
      <guid>/list/2fa/</guid>
      <description>&lt;h2 id=&#34;使用方式&#34;&gt;使用方式&lt;/h2&gt;&#xA;&lt;p&gt;在页面下方的输入框中粘贴你的 &lt;strong&gt;Base32 密钥&lt;/strong&gt;（即 2FA 密钥，通常由 Authenticator 应用提供），点击「保存」即可生成动态验证码。&lt;/p&gt;&#xA;&lt;p&gt;密钥保存在浏览器本地存储中，刷新页面不会丢失。点击「清除」可重新输入。&lt;/p&gt;&#xA;&lt;div id=&#34;2fa&#34;&gt;&#xD;&#xA;  &lt;div id=&#34;key-setup&#34; style=&#34;margin-bottom:16px;text-align:center&#34;&gt;&#xD;&#xA;    &lt;input type=&#34;text&#34; id=&#34;key-input&#34; placeholder=&#34;输入你的 Base32 密钥...&#34; style=&#34;padding:8px 12px;font-size:14px;border:1px solid #ccc;border-radius:4px;width:280px;max-width:100%;font-family:monospace&#34;&gt;&#xD;&#xA;    &lt;button id=&#34;key-save&#34; style=&#34;padding:8px 16px;font-size:14px;border:none;border-radius:4px;background:#e53935;color:#fff;cursor:pointer;margin-left:8px&#34;&gt;保存&lt;/button&gt;&#xD;&#xA;    &lt;button id=&#34;key-clear&#34; style=&#34;padding:8px 16px;font-size:14px;border:1px solid #ccc;border-radius:4px;background:transparent;color:#666;cursor:pointer;margin-left:4px&#34;&gt;清除&lt;/button&gt;&#xD;&#xA;  &lt;/div&gt;&#xD;&#xA;  &lt;div id=&#34;key-status&#34; style=&#34;text-align:center;font-size:13px;color:#999;margin-bottom:12px&#34;&gt;&lt;/div&gt;&#xD;&#xA;  &lt;div id=&#34;totp-display&#34; style=&#34;display:none&#34;&gt;&#xD;&#xA;    &lt;div id=&#34;code&#34;&gt;------&lt;/div&gt;&#xD;&#xA;    &lt;svg viewBox=&#34;0 0 60 60&#34; width=&#34;60&#34; height=&#34;60&#34;&gt;&#xD;&#xA;      &lt;circle cx=&#34;30&#34; cy=&#34;30&#34; r=&#34;26&#34; fill=&#34;none&#34; stroke=&#34;#ddd&#34; stroke-width=&#34;4&#34;/&gt;&#xD;&#xA;      &lt;circle cx=&#34;30&#34; cy=&#34;30&#34; r=&#34;26&#34; fill=&#34;none&#34; stroke=&#34;#e53935&#34; stroke-width=&#34;4&#34; stroke-dasharray=&#34;163.36&#34; stroke-dashoffset=&#34;0&#34; transform=&#34;rotate(-90 30 30)&#34; id=&#34;arc&#34;/&gt;&#xD;&#xA;    &lt;/svg&gt;&#xD;&#xA;    &lt;span id=&#34;sec&#34;&gt;30&lt;/span&gt;&#xD;&#xA;    &lt;div id=&#34;next&#34;&gt;▶ &lt;span id=&#34;nextcode&#34;&gt;------&lt;/span&gt;&lt;/div&gt;&#xD;&#xA;  &lt;/div&gt;&#xD;&#xA;&lt;/div&gt;&#xD;&#xA;&lt;script&gt;&#xD;&#xA;const step=30;&#xD;&#xA;const STORAGE_KEY=&#39;2fa_secret&#39;;&#xD;&#xA;&#xD;&#xA;&#xD;&#xA;function base32Decode(s){&#xD;&#xA;  const map=&#39;ABCDEFGHIJKLMNOPQRSTUVWXYZ234567&#39;;&#xD;&#xA;  let bits=0,bitCount=0,out=[];&#xD;&#xA;  for(let ch of s.toUpperCase()){&#xD;&#xA;    let val=map.indexOf(ch);&#xD;&#xA;    if(val&lt;0) continue; &#xD;&#xA;    bits=(bits&lt;&lt;5)|val;&#xD;&#xA;    bitCount+=5;&#xD;&#xA;    if(bitCount&gt;=8){&#xD;&#xA;      out.push((bits&gt;&gt;&gt;(bitCount-8))&amp;255);&#xD;&#xA;      bitCount-=8;&#xD;&#xA;    }&#xD;&#xA;  }&#xD;&#xA;  return Uint8Array.from(out);&#xD;&#xA;}&#xD;&#xA;&#xD;&#xA;async function getKey(){&#xD;&#xA;  let stored=localStorage.getItem(STORAGE_KEY);&#xD;&#xA;  if(!stored) return null;&#xD;&#xA;  try{ return base32Decode(stored); }catch(e){ return null; }&#xD;&#xA;}&#xD;&#xA;&#xD;&#xA;function truncate(h){let o=h[19]&amp;0xf;return((h[o]&amp;0x7f)&lt;&lt;24|(h[o+1]&amp;0xff)&lt;&lt;16|(h[o+2]&amp;0xff)&lt;&lt;8|(h[o+3]&amp;0xff))%1000000}&#xD;&#xA;function pad(n){return n.toString().padStart(6,&#39;0&#39;)}&#xD;&#xA;&#xD;&#xA;async function totp(t,keyBytes){&#xD;&#xA;  let msg=Uint8Array.from([0,0,0,0,t&gt;&gt;&gt;24,t&gt;&gt;&gt;16&amp;255,t&gt;&gt;&gt;8&amp;255,t&amp;255]);&#xD;&#xA;  let k=await crypto.subtle.importKey(&#39;raw&#39;,keyBytes,{name:&#39;HMAC&#39;,hash:&#39;SHA-1&#39;},!1,[&#39;sign&#39;]);&#xD;&#xA;  let s=await crypto.subtle.sign(&#39;HMAC&#39;,k,msg);&#xD;&#xA;  return new Uint8Array(s);&#xD;&#xA;}&#xD;&#xA;&#xD;&#xA;function updateDisplay(keyBytes){&#xD;&#xA;  let t=Math.floor(Date.now()/1e3),c=Math.floor(t/step);&#xD;&#xA;  (async()=&gt;{&#xD;&#xA;    let h=await totp(c,keyBytes),code=pad(truncate(h));&#xD;&#xA;    let h2=await totp(c+1,keyBytes),next=pad(truncate(h2));&#xD;&#xA;    document.getElementById(&#39;code&#39;).textContent=code;&#xD;&#xA;    document.getElementById(&#39;nextcode&#39;).textContent=next;&#xD;&#xA;  })();&#xD;&#xA;  let r=step-(t%step);&#xD;&#xA;  document.getElementById(&#39;sec&#39;).textContent=r;&#xD;&#xA;  let circ=document.getElementById(&#39;arc&#39;),d=163.36;&#xD;&#xA;  circ.style.strokeDashoffset=d*(1-r/step);&#xD;&#xA;}&#xD;&#xA;&#xD;&#xA;let _timer=null;&#xD;&#xA;function startTOTP(keyBytes){&#xD;&#xA;  document.getElementById(&#39;totp-display&#39;).style.display=&#39;&#39;;&#xD;&#xA;  document.getElementById(&#39;key-input&#39;).style.display=&#39;none&#39;;&#xD;&#xA;  document.getElementById(&#39;key-save&#39;).style.display=&#39;none&#39;;&#xD;&#xA;  document.getElementById(&#39;key-clear&#39;).textContent=&#39;清除密钥&#39;;&#xD;&#xA;  document.getElementById(&#39;key-status&#39;).textContent=&#39;✅ 密钥已配置&#39;;&#xD;&#xA;  if(_timer) clearInterval(_timer);&#xD;&#xA;  updateDisplay(keyBytes);&#xD;&#xA;  _timer=setInterval(()=&gt;updateDisplay(keyBytes),200);&#xD;&#xA;}&#xD;&#xA;&#xD;&#xA;function stopTOTP(){&#xD;&#xA;  if(_timer){ clearInterval(_timer);_timer=null; }&#xD;&#xA;  document.getElementById(&#39;totp-display&#39;).style.display=&#39;none&#39;;&#xD;&#xA;  document.getElementById(&#39;key-input&#39;).style.display=&#39;&#39;;&#xD;&#xA;  document.getElementById(&#39;key-save&#39;).style.display=&#39;&#39;;&#xD;&#xA;  document.getElementById(&#39;key-clear&#39;).textContent=&#39;清除&#39;;&#xD;&#xA;  let secret=localStorage.getItem(STORAGE_KEY);&#xD;&#xA;  document.getElementById(&#39;key-status&#39;).textContent=&#39;&#39;;&#xD;&#xA;}&#xD;&#xA;&#xD;&#xA;function init(){&#xD;&#xA;  let savedKey=localStorage.getItem(STORAGE_KEY);&#xD;&#xA;  if(savedKey){&#xD;&#xA;    let bytes=base32Decode(savedKey);&#xD;&#xA;    if(bytes&amp;&amp;bytes.length&gt;0){ startTOTP(bytes); return; }&#xD;&#xA;  }&#xD;&#xA;  stopTOTP();&#xD;&#xA;}&#xD;&#xA;&#xD;&#xA;document.getElementById(&#39;key-save&#39;).addEventListener(&#39;click&#39;,()=&gt;{&#xD;&#xA;  let input=document.getElementById(&#39;key-input&#39;).value.trim();&#xD;&#xA;  if(!input){ alert(&#39;请输入 Base32 密钥&#39;); return; }&#xD;&#xA;  let bytes=base32Decode(input);&#xD;&#xA;  if(bytes.length&lt;5){ alert(&#39;密钥太短，请检查输入是否正确（Base32 格式）&#39;); return; }&#xD;&#xA;  localStorage.setItem(STORAGE_KEY,input);&#xD;&#xA;  startTOTP(bytes);&#xD;&#xA;});&#xD;&#xA;&#xD;&#xA;document.getElementById(&#39;key-clear&#39;).addEventListener(&#39;click&#39;,()=&gt;{&#xD;&#xA;  localStorage.removeItem(STORAGE_KEY);&#xD;&#xA;  document.getElementById(&#39;key-input&#39;).value=&#39;&#39;;&#xD;&#xA;  stopTOTP();&#xD;&#xA;});&#xD;&#xA;&#xD;&#xA;document.getElementById(&#39;key-input&#39;).addEventListener(&#39;keydown&#39;,(e)=&gt;{&#xD;&#xA;  if(e.key===&#39;Enter&#39;) document.getElementById(&#39;key-save&#39;).click();&#xD;&#xA;});&#xD;&#xA;&#xD;&#xA;&#xD;&#xA;(function(){&#xD;&#xA;  let container=document.getElementById(&#39;2fa&#39;);&#xD;&#xA;  let shortcodeKey=container.getAttribute(&#39;data-key&#39;);&#xD;&#xA;  if(shortcodeKey){&#xD;&#xA;    document.getElementById(&#39;key-input&#39;).value=shortcodeKey;&#xD;&#xA;    document.getElementById(&#39;key-save&#39;).click();&#xD;&#xA;  }else{&#xD;&#xA;    init();&#xD;&#xA;  }&#xD;&#xA;})();&#xD;&#xA;&lt;/script&gt;</description>
    </item>
    <item>
      <title>IP 查询</title>
      <link>/list/ip/</link>
      <pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate>
      <guid>/list/ip/</guid>
      <description>&lt;!DOCTYPE html&gt;&#xD;&#xA;&lt;html lang=&#34;zh-CN&#34;&gt;&#xD;&#xA;&lt;head&gt;&#xD;&#xA;&lt;meta charset=&#34;UTF-8&#34;&gt;&#xD;&#xA;&lt;meta name=&#34;viewport&#34; content=&#34;width=device-width, initial-scale=1.0&#34;&gt;&#xD;&#xA;&lt;title&gt;IPInfo + DoH 查询工具&lt;/title&gt;&#xD;&#xA;&#xD;&#xA;&lt;style&gt;&#xD;&#xA;&#xD;&#xA;body{&#xD;&#xA;    font-family: Arial, sans-serif;&#xD;&#xA;    margin:0;&#xD;&#xA;    padding:20px;&#xD;&#xA;}&#xD;&#xA;&#xD;&#xA;.container{&#xD;&#xA;    max-width:1200px;&#xD;&#xA;    margin:auto;&#xD;&#xA;}&#xD;&#xA;&#xD;&#xA;.card{&#xD;&#xA;    border-radius:12px;&#xD;&#xA;    padding:20px;&#xD;&#xA;    margin-bottom:20px;&#xD;&#xA;    box-shadow:0 2px 10px rgba(0,0,0,.08);&#xD;&#xA;}&#xD;&#xA;&#xD;&#xA;h1{&#xD;&#xA;    margin-top:0;&#xD;&#xA;}&#xD;&#xA;&#xD;&#xA;input{&#xD;&#xA;    width:100%;&#xD;&#xA;    padding:12px;&#xD;&#xA;    font-size:16px;&#xD;&#xA;    border:1px solid #ccc;&#xD;&#xA;    border-radius:8px;&#xD;&#xA;    box-sizing:border-box;&#xD;&#xA;}&#xD;&#xA;&#xD;&#xA;button{&#xD;&#xA;    margin-top:12px;&#xD;&#xA;    padding:12px 18px;&#xD;&#xA;    border:none;&#xD;&#xA;    border-radius:8px;&#xD;&#xA;    cursor:pointer;&#xD;&#xA;    font-size:15px;&#xD;&#xA;}&#xD;&#xA;&#xD;&#xA;.primary{&#xD;&#xA;    background:#111;&#xD;&#xA;    color:white;&#xD;&#xA;}&#xD;&#xA;&#xD;&#xA;.secondary{&#xD;&#xA;    background:#ddd;&#xD;&#xA;}&#xD;&#xA;&#xD;&#xA;table{&#xD;&#xA;    width:100%;&#xD;&#xA;    border-collapse:collapse;&#xD;&#xA;    margin-top:15px;&#xD;&#xA;}&#xD;&#xA;&#xD;&#xA;th, td{&#xD;&#xA;    border:1px solid #ddd;&#xD;&#xA;    padding:10px;&#xD;&#xA;    text-align:left;&#xD;&#xA;    vertical-align:top;&#xD;&#xA;}&#xD;&#xA;&#xD;&#xA;th{&#xD;&#xA;    background:#eee;&#xD;&#xA;}&#xD;&#xA;&#xD;&#xA;.status{&#xD;&#xA;    margin-top:10px;&#xD;&#xA;    color:#666;&#xD;&#xA;    font-size:14px;&#xD;&#xA;}&#xD;&#xA;&#xD;&#xA;pre{&#xD;&#xA;    background:#111;&#xD;&#xA;    color:#0f0;&#xD;&#xA;    padding:15px;&#xD;&#xA;    overflow:auto;&#xD;&#xA;    border-radius:8px;&#xD;&#xA;    max-height:400px;&#xD;&#xA;}&#xD;&#xA;&#xD;&#xA;.badge{&#xD;&#xA;    display:inline-block;&#xD;&#xA;    padding:4px 8px;&#xD;&#xA;    border-radius:999px;&#xD;&#xA;    font-size:12px;&#xD;&#xA;    background:#eee;&#xD;&#xA;}&#xD;&#xA;&#xD;&#xA;.good{&#xD;&#xA;    background:#d4ffd4;&#xD;&#xA;}&#xD;&#xA;&#xD;&#xA;.bad{&#xD;&#xA;    background:#ffd4d4;&#xD;&#xA;}&#xD;&#xA;&#xD;&#xA;&lt;/style&gt;&#xD;&#xA;&lt;/head&gt;&#xD;&#xA;&lt;body&gt;&#xD;&#xA;&#xD;&#xA;&lt;div class=&#34;container&#34;&gt;&#xD;&#xA;&#xD;&#xA;    &lt;div class=&#34;card&#34;&gt;&#xD;&#xA;&#xD;&#xA;        &lt;h1&gt;IPInfo + DoH 查询工具&lt;/h1&gt;&#xD;&#xA;&#xD;&#xA;        &lt;input&#xD;&#xA;            type=&#34;text&#34;&#xD;&#xA;            id=&#34;input&#34;&#xD;&#xA;            placeholder=&#34;输入 IP 或域名，例如：8.8.8.8 或 example.com&#34;&#xD;&#xA;        &gt;&#xD;&#xA;&#xD;&#xA;        &lt;button class=&#34;primary&#34; id=&#34;queryBtn&#34;&gt;&#xD;&#xA;            开始查询&#xD;&#xA;        &lt;/button&gt;&#xD;&#xA;&#xD;&#xA;        &lt;button class=&#34;secondary&#34; id=&#34;myIpBtn&#34;&gt;&#xD;&#xA;            查询我的 IP&#xD;&#xA;        &lt;/button&gt;&#xD;&#xA;&#xD;&#xA;        &lt;button class=&#34;secondary&#34; id=&#34;copyBtn&#34; style=&#34;display:none;&#34;&gt;&#xD;&#xA;            复制 JSON&#xD;&#xA;        &lt;/button&gt;&#xD;&#xA;&#xD;&#xA;        &lt;div class=&#34;status&#34; id=&#34;status&#34;&gt;&lt;/div&gt;&#xD;&#xA;&#xD;&#xA;        &#xD;&#xA;        &lt;div id=&#34;progressContainer&#34; style=&#34;display:none; margin-top:10px;&#34;&gt;&#xD;&#xA;            &lt;progress id=&#34;progressBar&#34; style=&#34;width:100%;&#34; max=&#34;100&#34; value=&#34;0&#34;&gt;&lt;/progress&gt;&#xD;&#xA;        &lt;/div&gt;&#xD;&#xA;&#xD;&#xA;    &lt;/div&gt;&#xD;&#xA;&#xD;&#xA;    &lt;div class=&#34;card&#34;&gt;&#xD;&#xA;&#xD;&#xA;        &lt;h2&gt;查询结果&lt;/h2&gt;&#xD;&#xA;&#xD;&#xA;        &lt;table id=&#34;resultTable&#34;&gt;&#xD;&#xA;            &lt;thead&gt;&#xD;&#xA;                &lt;tr&gt;&#xD;&#xA;                    &lt;th&gt;DoH&lt;/th&gt;&#xD;&#xA;                    &lt;th&gt;IP&lt;/th&gt;&#xD;&#xA;                    &lt;th&gt;主机名&lt;/th&gt;&#xD;&#xA;                    &lt;th&gt;城市&lt;/th&gt;&#xD;&#xA;                    &lt;th&gt;区域&lt;/th&gt;&#xD;&#xA;                    &lt;th&gt;国家&lt;/th&gt;&#xD;&#xA;                    &lt;th&gt;ASN / 组织&lt;/th&gt;&#xD;&#xA;                    &lt;th&gt;坐标&lt;/th&gt;&#xD;&#xA;                    &lt;th&gt;邮编&lt;/th&gt;&#xD;&#xA;                    &lt;th&gt;时区&lt;/th&gt;&#xD;&#xA;                    &lt;th&gt;Anycast&lt;/th&gt;&#xD;&#xA;                &lt;/tr&gt;&#xD;&#xA;            &lt;/thead&gt;&#xD;&#xA;            &lt;tbody id=&#34;resultBody&#34;&gt;&lt;/tbody&gt;&#xD;&#xA;        &lt;/table&gt;&#xD;&#xA;&#xD;&#xA;    &lt;/div&gt;&#xD;&#xA;&#xD;&#xA;&lt;/div&gt;&#xD;&#xA;&#xD;&#xA;&lt;script&gt;&#xD;&#xA;&#xD;&#xA;const TOKEN = &#34;74577d84a8037c&#34;;&#xD;&#xA;&#xD;&#xA;const dohServers = [&#xD;&#xA;&#xD;&#xA;    {&#xD;&#xA;        name:&#34;Google&#34;,&#xD;&#xA;        url:&#34;https://dns.google/resolve&#34;&#xD;&#xA;    },&#xD;&#xA;&#xD;&#xA;    {&#xD;&#xA;        name:&#34;Cloudflare&#34;,&#xD;&#xA;        url:&#34;https://cloudflare-dns.com/dns-query&#34;&#xD;&#xA;    },&#xD;&#xA;&#xD;&#xA;    {&#xD;&#xA;        name:&#34;AliDNS&#34;,&#xD;&#xA;        url:&#34;https://dns.alidns.com/resolve&#34;&#xD;&#xA;    },&#xD;&#xA;    {&#xD;&#xA;        name:&#34;DNS.SB&#34;,&#xD;&#xA;        url:&#34;https://doh.dns.sb/dns-query&#34;&#xD;&#xA;    }&#xD;&#xA;&#xD;&#xA;];&#xD;&#xA;&#xD;&#xA;const input = document.getElementById(&#34;input&#34;);&#xD;&#xA;const queryBtn = document.getElementById(&#34;queryBtn&#34;);&#xD;&#xA;const myIpBtn = document.getElementById(&#34;myIpBtn&#34;);&#xD;&#xA;const copyBtn = document.getElementById(&#34;copyBtn&#34;);&#xD;&#xA;&#xD;&#xA;const resultTable = document.getElementById(&#34;resultTable&#34;);&#xD;&#xA;const resultBody = document.getElementById(&#34;resultBody&#34;);&#xD;&#xA;&#xD;&#xA;const statusBox = document.getElementById(&#34;status&#34;);&#xD;&#xA;const progressContainer = document.getElementById(&#34;progressContainer&#34;);&#xD;&#xA;const progressBar = document.getElementById(&#34;progressBar&#34;);&#xD;&#xA;&#xD;&#xA;let currentJSON = &#34;&#34;;&#xD;&#xA;&#xD;&#xA;&#xD;&#xA;&#xD;&#xA;function isIP(str){&#xD;&#xA;    const ipv4 = /^(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}$/;&#xD;&#xA;    const ipv6 = /:/;&#xD;&#xA;    return ipv4.test(str) || ipv6.test(str);&#xD;&#xA;}&#xD;&#xA;&#xD;&#xA;function isDomain(str){&#xD;&#xA;    return /^[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(str);&#xD;&#xA;}&#xD;&#xA;&#xD;&#xA;async function queryIP(ip){&#xD;&#xA;    const url = `https://ipinfo.io/${ip}?token=${TOKEN}`;&#xD;&#xA;    const response = await fetch(url);&#xD;&#xA;    return await response.json();&#xD;&#xA;}&#xD;&#xA;&#xD;&#xA;&#xD;&#xA;&#xD;&#xA;function updateProgress(percent, text){&#xD;&#xA;    progressBar.value = percent;&#xD;&#xA;    statusBox.innerText = text;&#xD;&#xA;    progressContainer.style.display = &#34;block&#34;;&#xD;&#xA;}&#xD;&#xA;&#xD;&#xA;&#xD;&#xA;function addRow(provider, data){&#xD;&#xA;    const tr = document.createElement(&#34;tr&#34;);&#xD;&#xA;    tr.innerHTML = `&#xD;&#xA;        &lt;td&gt;${provider}&lt;/td&gt;&#xD;&#xA;        &lt;td&gt;${data.ip || &#34;N/A&#34;}&lt;/td&gt;&#xD;&#xA;        &lt;td&gt;${data.hostname || &#34;N/A&#34;}&lt;/td&gt;&#xD;&#xA;        &lt;td&gt;${data.city || &#34;N/A&#34;}&lt;/td&gt;&#xD;&#xA;        &lt;td&gt;${data.region || &#34;N/A&#34;}&lt;/td&gt;&#xD;&#xA;        &lt;td&gt;${data.country || &#34;N/A&#34;}&lt;/td&gt;&#xD;&#xA;        &lt;td&gt;${data.org || &#34;N/A&#34;}&lt;/td&gt;&#xD;&#xA;        &lt;td&gt;${data.loc || &#34;N/A&#34;}&lt;/td&gt;&#xD;&#xA;        &lt;td&gt;${data.postal || &#34;N/A&#34;}&lt;/td&gt;&#xD;&#xA;        &lt;td&gt;${data.timezone || &#34;N/A&#34;}&lt;/td&gt;&#xD;&#xA;        &lt;td&gt;${data.anycast ? &#39;&lt;span class=&#34;badge good&#34;&gt;Yes&lt;/span&gt;&#39; : &#39;&lt;span class=&#34;badge bad&#34;&gt;No&lt;/span&gt;&#39;}&lt;/td&gt;&#xD;&#xA;    `;&#xD;&#xA;    resultBody.appendChild(tr);&#xD;&#xA;    return tr;&#xD;&#xA;}&#xD;&#xA;&#xD;&#xA;&#xD;&#xA;function updateRow(tr, data){&#xD;&#xA;    const cells = tr.children;&#xD;&#xA;    cells[1].textContent = data.ip || &#34;N/A&#34;;&#xD;&#xA;    cells[2].textContent = data.hostname || &#34;N/A&#34;;&#xD;&#xA;    cells[3].textContent = data.city || &#34;N/A&#34;;&#xD;&#xA;    cells[4].textContent = data.region || &#34;N/A&#34;;&#xD;&#xA;    cells[5].textContent = data.country || &#34;N/A&#34;;&#xD;&#xA;    cells[6].textContent = data.org || &#34;N/A&#34;;&#xD;&#xA;    cells[7].textContent = data.loc || &#34;N/A&#34;;&#xD;&#xA;    cells[8].textContent = data.postal || &#34;N/A&#34;;&#xD;&#xA;    cells[9].textContent = data.timezone || &#34;N/A&#34;;&#xD;&#xA;    cells[10].innerHTML = data.anycast&#xD;&#xA;        ? &#39;&lt;span class=&#34;badge good&#34;&gt;Yes&lt;/span&gt;&#39;&#xD;&#xA;        : &#39;&lt;span class=&#34;badge bad&#34;&gt;No&lt;/span&gt;&#39;;&#xD;&#xA;}&#xD;&#xA;&#xD;&#xA;&#xD;&#xA;function addPlaceholderRow(provider, ip){&#xD;&#xA;    const tr = document.createElement(&#34;tr&#34;);&#xD;&#xA;    tr.innerHTML = `&#xD;&#xA;        &lt;td&gt;${provider}&lt;/td&gt;&#xD;&#xA;        &lt;td&gt;${ip}&lt;/td&gt;&#xD;&#xA;        &lt;td&gt;...&lt;/td&gt;&#xD;&#xA;        &lt;td&gt;...&lt;/td&gt;&#xD;&#xA;        &lt;td&gt;...&lt;/td&gt;&#xD;&#xA;        &lt;td&gt;...&lt;/td&gt;&#xD;&#xA;        &lt;td&gt;...&lt;/td&gt;&#xD;&#xA;        &lt;td&gt;...&lt;/td&gt;&#xD;&#xA;        &lt;td&gt;...&lt;/td&gt;&#xD;&#xA;        &lt;td&gt;...&lt;/td&gt;&#xD;&#xA;        &lt;td&gt;...&lt;/td&gt;&#xD;&#xA;    `;&#xD;&#xA;    resultBody.appendChild(tr);&#xD;&#xA;    return tr;&#xD;&#xA;}&#xD;&#xA;&#xD;&#xA;&#xD;&#xA;function addErrorRow(provider, message){&#xD;&#xA;    const tr = document.createElement(&#34;tr&#34;);&#xD;&#xA;    tr.innerHTML = `&lt;td&gt;${provider}&lt;/td&gt;&lt;td colspan=&#34;10&#34;&gt;${message}&lt;/td&gt;`;&#xD;&#xA;    resultBody.appendChild(tr);&#xD;&#xA;    return tr;&#xD;&#xA;}&#xD;&#xA;&#xD;&#xA;&#xD;&#xA;&#xD;&#xA;async function startQuery(target){&#xD;&#xA;&#xD;&#xA;    &#xD;&#xA;    resultBody.innerHTML = &#34;&#34;;&#xD;&#xA;    currentJSON = &#34;&#34;;&#xD;&#xA;    copyBtn.style.display = &#34;none&#34;;&#xD;&#xA;    resultTable.style.display = &#34;table&#34;;&#xD;&#xA;    progressContainer.style.display = &#34;none&#34;;&#xD;&#xA;    progressBar.value = 0;&#xD;&#xA;    statusBox.innerText = &#34;&#34;;&#xD;&#xA;&#xD;&#xA;    try {&#xD;&#xA;&#xD;&#xA;        &#xD;&#xA;        if(target === &#34;json&#34; || target === &#34;myip&#34;){&#xD;&#xA;            updateProgress(10, &#34;正在查询本机 IP...&#34;);&#xD;&#xA;            const data = await queryIP(&#34;json&#34;); &#xD;&#xA;            addRow(&#34;本机 IP&#34;, data);&#xD;&#xA;            currentJSON = JSON.stringify(data, null, 2);&#xD;&#xA;            updateProgress(100, &#34;查询完成&#34;);&#xD;&#xA;            copyBtn.style.display = &#34;inline-block&#34;;&#xD;&#xA;            return;&#xD;&#xA;        }&#xD;&#xA;&#xD;&#xA;        &#xD;&#xA;        if(isIP(target)){&#xD;&#xA;            updateProgress(10, &#34;正在查询 IP 信息...&#34;);&#xD;&#xA;            const data = await queryIP(target);&#xD;&#xA;            addRow(&#34;Direct&#34;, data);&#xD;&#xA;            currentJSON = JSON.stringify(data, null, 2);&#xD;&#xA;            updateProgress(100, &#34;查询完成&#34;);&#xD;&#xA;            copyBtn.style.display = &#34;inline-block&#34;;&#xD;&#xA;            return;&#xD;&#xA;        }&#xD;&#xA;&#xD;&#xA;        &#xD;&#xA;        if(isDomain(target)){&#xD;&#xA;&#xD;&#xA;            const dnsPhase = 30;      &#xD;&#xA;            const ipPhase  = 70;      &#xD;&#xA;            const dnsCount = dohServers.length;&#xD;&#xA;            let dnsCompleted = 0;&#xD;&#xA;            let allIpTasks = [];       &#xD;&#xA;&#xD;&#xA;            &#xD;&#xA;            const dnsPromises = dohServers.map(async (doh) =&gt; {&#xD;&#xA;&#xD;&#xA;                &#xD;&#xA;                let result;&#xD;&#xA;                try {&#xD;&#xA;                    const start = performance.now();&#xD;&#xA;                    const response = await fetch(`${doh.url}?name=${target}&amp;type=A`, {&#xD;&#xA;                        headers: { &#34;accept&#34;: &#34;application/dns-json&#34; }&#xD;&#xA;                    });&#xD;&#xA;                    const ms = Math.round(performance.now() - start);&#xD;&#xA;                    const data = await response.json();&#xD;&#xA;                    const ips = data.Answer&#xD;&#xA;                        ? data.Answer.filter(x =&gt; x.type === 1).map(x =&gt; x.data)&#xD;&#xA;                        : [];&#xD;&#xA;                    result = { provider: doh.name, ips, latency: ms, error: null };&#xD;&#xA;                } catch(err) {&#xD;&#xA;                    result = { provider: doh.name, ips: [], latency: null, error: err.message };&#xD;&#xA;                }&#xD;&#xA;&#xD;&#xA;                &#xD;&#xA;                dnsCompleted++;&#xD;&#xA;                const pct = (dnsCompleted / dnsCount) * dnsPhase;&#xD;&#xA;                updateProgress(pct, `DNS 解析中 (${result.provider})`);&#xD;&#xA;&#xD;&#xA;                &#xD;&#xA;                if(result.error || result.ips.length === 0){&#xD;&#xA;                    addErrorRow(result.provider, result.error || &#34;无 IP 返回&#34;);&#xD;&#xA;                    return []; &#xD;&#xA;                }&#xD;&#xA;&#xD;&#xA;                &#xD;&#xA;                const tasks = [];&#xD;&#xA;                for(const ip of result.ips){&#xD;&#xA;                    const label = `${result.provider} (${result.latency}ms)`;&#xD;&#xA;                    const tr = addPlaceholderRow(label, ip);&#xD;&#xA;                    tasks.push({&#xD;&#xA;                        tr,&#xD;&#xA;                        provider: result.provider,&#xD;&#xA;                        latency: result.latency,&#xD;&#xA;                        ip,&#xD;&#xA;                        data: null,&#xD;&#xA;                        error: null&#xD;&#xA;                    });&#xD;&#xA;                }&#xD;&#xA;                return tasks;&#xD;&#xA;            });&#xD;&#xA;&#xD;&#xA;            &#xD;&#xA;            const nestedTasks = await Promise.all(dnsPromises);&#xD;&#xA;            allIpTasks = nestedTasks.flat();&#xD;&#xA;&#xD;&#xA;            &#xD;&#xA;            if(allIpTasks.length === 0){&#xD;&#xA;                updateProgress(100, &#34;DNS 解析未获得有效 IP&#34;);&#xD;&#xA;                copyBtn.style.display = &#34;inline-block&#34;;&#xD;&#xA;                return;&#xD;&#xA;            }&#xD;&#xA;&#xD;&#xA;            &#xD;&#xA;            const total = allIpTasks.length;&#xD;&#xA;            let completed = 0;&#xD;&#xA;&#xD;&#xA;            await Promise.allSettled(allIpTasks.map(async (task) =&gt; {&#xD;&#xA;                try {&#xD;&#xA;                    const info = await queryIP(task.ip);&#xD;&#xA;                    updateRow(task.tr, info);&#xD;&#xA;                    task.data = info;           &#xD;&#xA;                } catch(err){&#xD;&#xA;                    const cells = task.tr.children;&#xD;&#xA;                    for(let i=2; i&lt;=10; i++) cells[i].textContent = `错误: ${err.message}`;&#xD;&#xA;                    task.error = err.message;&#xD;&#xA;                } finally {&#xD;&#xA;                    completed++;&#xD;&#xA;                    const pct = dnsPhase + (completed / total) * ipPhase;&#xD;&#xA;                    updateProgress(pct, `IP  (${completed}/${total})`);&#xD;&#xA;                }&#xD;&#xA;            }));&#xD;&#xA;&#xD;&#xA;            &#xD;&#xA;            const allResults = allIpTasks.map(t =&gt; ({&#xD;&#xA;                provider: t.provider,&#xD;&#xA;                latency: t.latency,&#xD;&#xA;                ip: t.ip,&#xD;&#xA;                info: t.data,&#xD;&#xA;                error: t.error&#xD;&#xA;            }));&#xD;&#xA;            currentJSON = JSON.stringify(allResults, null, 2);&#xD;&#xA;&#xD;&#xA;            updateProgress(100, &#34;查询完成&#34;);&#xD;&#xA;            copyBtn.style.display = &#34;inline-block&#34;;&#xD;&#xA;            return;&#xD;&#xA;        }&#xD;&#xA;&#xD;&#xA;        &#xD;&#xA;        alert(&#34;请输入有效 IP 或域名&#34;);&#xD;&#xA;&#xD;&#xA;    } catch(err){&#xD;&#xA;        statusBox.innerText = &#34;查询失败：&#34; + err.message;&#xD;&#xA;        progressContainer.style.display = &#34;none&#34;;&#xD;&#xA;    }&#xD;&#xA;}&#xD;&#xA;&#xD;&#xA;&#xD;&#xA;&#xD;&#xA;queryBtn.addEventListener(&#34;click&#34;, () =&gt; {&#xD;&#xA;    const value = input.value.trim();&#xD;&#xA;    if(!value){&#xD;&#xA;        alert(&#34;请输入 IP 或域名&#34;);&#xD;&#xA;        return;&#xD;&#xA;    }&#xD;&#xA;    startQuery(value);&#xD;&#xA;});&#xD;&#xA;&#xD;&#xA;myIpBtn.addEventListener(&#34;click&#34;, () =&gt; {&#xD;&#xA;    startQuery(&#34;json&#34;);   &#xD;&#xA;});&#xD;&#xA;&#xD;&#xA;copyBtn.addEventListener(&#34;click&#34;, async () =&gt; {&#xD;&#xA;    try{&#xD;&#xA;        await navigator.clipboard.writeText(currentJSON);&#xD;&#xA;        alert(&#34;JSON 已复制&#34;);&#xD;&#xA;    } catch(err){&#xD;&#xA;        alert(&#34;复制失败&#34;);&#xD;&#xA;    }&#xD;&#xA;});&#xD;&#xA;&#xD;&#xA;&lt;/script&gt;&#xD;&#xA;&#xD;&#xA;&lt;/body&gt;&#xD;&#xA;&lt;/html&gt;</description>
    </item>
    <item>
      <title>UUID 生成</title>
      <link>/list/uuid/</link>
      <pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate>
      <guid>/list/uuid/</guid>
      <description>&lt;label&gt;数量：&lt;input type=&#34;number&#34; id=&#34;uuid-count&#34; value=&#34;10&#34; min=&#34;1&#34;&gt;&lt;/label&gt;&#xD;&#xA;&lt;button id=&#34;uuid-gen&#34;&gt;生成&lt;/button&gt;&#xD;&#xA;&lt;br&gt;&#xD;&#xA;&lt;textarea id=&#34;uuid-output&#34; rows=&#34;12&#34; readonly style=&#34;width:100%;font-family:monospace;box-sizing:border-box&#34;&gt;&lt;/textarea&gt;&#xD;&#xA;&lt;script&gt;&#xD;&#xA;(function(){&#xD;&#xA;  const ta = document.getElementById(&#39;uuid-output&#39;);&#xD;&#xA;  const countInput = document.getElementById(&#39;uuid-count&#39;);&#xD;&#xA;  const genBtn = document.getElementById(&#39;uuid-gen&#39;);&#xD;&#xA;  const urlNum = parseInt(new URLSearchParams(location.search).get(&#39;num&#39;));&#xD;&#xA;  if (urlNum &gt; 0) countInput.value = urlNum;&#xD;&#xA;&#xD;&#xA;  function generate() {&#xD;&#xA;    const n = parseInt(countInput.value) || 1;&#xD;&#xA;    const lines = [];&#xD;&#xA;    for (let i = 0; i &lt; n; i++) {&#xD;&#xA;      lines.push(crypto.randomUUID());&#xD;&#xA;    }&#xD;&#xA;    ta.value = lines.join(&#39;\n&#39;);&#xD;&#xA;  }&#xD;&#xA;&#xD;&#xA;  generate();&#xD;&#xA;  genBtn.addEventListener(&#39;click&#39;, generate);&#xD;&#xA;})();&#xD;&#xA;&lt;/script&gt;</description>
    </item>
  </channel>
</rss>
