NQ脚本浏览器油猴版,可自定义查询任意ip
SeaMod
2026-09-06 00:48
1
在油猴上安装完插件后,必须点击右上角输入对应数据库的key,大部分都是可以免费注册赠送免费key的
浏览器输入 https://example.com/#ip-quality 就可以自动打开脚本页面了
随机输入了个搬瓦工家的cn2线路ip,这是结果:

// ==UserScript==
// @name IP 质量体检 · 任意 IP · NQ 风格
// @namespace local.ip-quality.manual
// @version 2.10.1
// @description 安装后无需保留本地文件夹;打开 https://example.com/#ip-quality 查询任意 IP。含基础信息、类型、风险评分、风险因子。
// @match https://example.com/*
// @run-at document-end
// @noframes
// @sandbox DOM
// @grant GM_xmlhttpRequest
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_deleteValue
// @connect ipinfo.check.place
// @connect geoip.maxmind.com
// @connect geolite.info
// @connect ipinfo.io
// @connect api.ipapi.is
// @connect api.abuseipdb.com
// @connect api.ip2location.io
// @connect www.ipqualityscore.com
// @connect api.ipregistry.co
// @connect api.db-ip.com
// @connect api.ipdata.co
// @connect scamalytics.com
// @license AGPL-3.0-only
// ==/UserScript==
/*
INSTALL / 安装:
1. 安装官方 Tampermonkey:https://www.tampermonkey.net/
2. 在油猴管理面板选择“添加新脚本”,清空模板,粘贴本文件全部内容,Ctrl+S 保存。
Chrome 如提示,打开 Tampermonkey 详情中的“允许用户脚本”开关。
3. 打开并收藏 https://example.com/#ip-quality
4. 确认页面能显示后,本 .user.js 下载文件和旧版解压目录均可删除。
必须保留 Tampermonkey 扩展及其中已安装的本脚本;旧 Chrome 扩展可移除。
迁移:旧扩展中保存的 API Key 不会自动导入,需要在油猴版“接口设置”重新填写。
限制:不含他人的 API Key;公共接口可能返回 403。免费套餐可能缺少风险字段。
一次查询一个 IPv4/IPv6,保留基础信息、IP 类型、风险评分和风险因子。
源码与样式全部内置;没有 @require、@resource、本地文件读取或后台自动更新。
查询仍需联网。首次使用跨域查询时,按油猴提示允许列出的服务域名。
基础信息字段与评分显示参考 xykt/IPQuality,非原作者发布。
This derivative work is licensed under AGPL-3.0-only, without warranty.
Original project: https://github.com/xykt/IPQuality
原扩展 v1.1 的查询及排版已迁移为 v2.10.1 单文件,修改日期 2026-09-05。
*/
(async function(){
'use strict';
if(location.hostname!=='example.com'||location.pathname!=='/'||location.hash!=='#ip-quality')return;
if(document.readyState==='loading')await new Promise(resolve=>document.addEventListener('DOMContentLoaded',resolve,{once:true}));
const markup="<!doctype html>\n<html lang=\"zh-CN\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"><title>IP 质量查询</title><style>:root{color-scheme:dark;--bg:#1b1e25;--panel:#242832;--terminal:#101216;--ink:#ebedf2;--muted:#929aaa;--line:#363d49;--cyan:#55dfe3;--green:#66e579;--red:#ff6969;--yellow:#ffda6a;--mono:Consolas,'Cascadia Mono','Liberation Mono','Microsoft YaHei',monospace}*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--ink);font-family:system-ui,'Segoe UI','Microsoft YaHei',sans-serif;font-size:13px;line-height:1.5}main{max-width:1120px;margin:0 auto;padding:24px 24px 16px}.toolbar{display:flex;align-items:center;justify-content:space-between;gap:20px;margin-bottom:18px}.brand{display:flex;gap:11px;align-items:center}.brand-symbol{display:grid;place-items:center;width:34px;height:34px;border:1px solid #55dfe35c;border-radius:6px;color:var(--cyan);font:700 17px var(--mono)}h1{font-size:17px;margin:0;font-weight:650;letter-spacing:1px}.brand .muted{font-size:11px}.toolbar-actions{display:flex;gap:8px}button{font:inherit;font-size:12px;font-weight:600;border:1px solid var(--cyan);background:var(--cyan);color:#10272a;padding:8px 14px;border-radius:5px;cursor:pointer;white-space:nowrap}button.secondary{border-color:#46505f;background:transparent;color:#dce3ed}button:hover:enabled{filter:brightness(1.13)}button:disabled{opacity:.4;cursor:default}input,select{font:inherit;border:1px solid #495364;border-radius:4px;background:#1b1f27;color:var(--ink);padding:8px 10px}input[type=checkbox]{accent-color:var(--cyan)}input:focus-visible,select:focus-visible,button:focus-visible,a:focus-visible,summary:focus-visible{outline:2px solid var(--cyan);outline-offset:3px}.query{border:1px solid var(--line);border-radius:7px;background:var(--panel);padding:13px 16px;margin-bottom:20px}.inputrow{display:flex;align-items:center;gap:10px}.inputrow label{font-weight:600;color:#cbd2dc;white-space:nowrap}#ip{flex:1;min-width:100px;font:15px var(--mono);height:36px}.options{display:flex;align-items:center;gap:22px;flex-wrap:wrap;font-size:12px;color:#b5bfce;margin-top:10px}.options select,.filter select{padding:3px 6px;font-size:12px}.run-status{display:flex;align-items:center;gap:20px;margin-top:11px}#status{font-size:11px;color:var(--muted);flex:1}progress{height:3px;width:120px;border:0;accent-color:var(--cyan)}progress::-webkit-progress-bar{background:#3c4652}progress::-webkit-progress-value{background:var(--cyan)}.terminal{background:var(--terminal);padding:22px 28px 12px;border:1px solid #3d4651;border-radius:5px;box-shadow:0 10px 30px #0002;font-family:var(--mono);font-size:13px;line-height:1.55}.report-heading{text-align:center;border-top:2px solid #7a8693;border-bottom:2px solid #7a8693;padding:10px 0 9px;margin-bottom:13px}.report-heading h2{font-size:18px;margin:0 0 3px;font-weight:700;overflow-wrap:anywhere}.report-heading #reportTitle{color:var(--cyan)}.report-heading p{margin:0;font-size:11px;color:#aab2bf}.report-heading a{color:inherit;text-decoration:none}.report-heading a:hover{text-decoration:underline}.report-meta{display:flex;justify-content:center;gap:28px;font-size:11px;color:#b7bec9;margin-top:5px}.report-section{margin:0;padding:0 0 10px}.report-section+.report-section{border-top:1px dashed #505966;padding-top:8px}.sectionhead{display:flex;align-items:baseline;gap:9px;margin-bottom:5px;flex-wrap:wrap}.sectionhead h2{margin:0;font-size:14px;font-weight:700;letter-spacing:1px}.sectionhead .muted,.section-note{font-size:10px;color:var(--muted)}.section-note{margin-left:auto}.facts{display:flex;flex-direction:column;gap:1px}.fact{display:grid;grid-template-columns:110px 1fr;min-height:20px}.fact-label,.matrix tbody th,.matrix thead th:first-child,.score-name,.scope-line>span:first-child,.risk-legend>span:first-child{color:var(--cyan);font-weight:400}.fact-value{color:var(--green);overflow-wrap:anywhere}.unknown{color:#697484}.scroll{overflow-x:auto;scrollbar-width:thin;scrollbar-color:#4a5565 transparent}table{border-collapse:collapse;width:100%;text-align:left;font-size:12px}th,td{padding:7px 8px;border-bottom:1px solid #2a3039;vertical-align:top}th{color:var(--muted);font-weight:400}.matrix{table-layout:fixed;font-family:var(--mono);min-width:680px}.matrix th,.matrix td{padding:2px 4px;text-align:center;vertical-align:middle;border:0;height:23px;white-space:nowrap}.matrix th:first-child{width:95px;text-align:left;padding-left:0}.matrix thead th{color:#c0c7d0;font-size:11px;font-style:italic}.matrix thead th:first-child{font-size:12px;font-style:normal}.type-matrix{max-width:790px}.type-matrix th:first-child{width:95px}.factor-matrix{min-width:900px}.factor-matrix th:first-child{width:78px}.factor-matrix thead th{font-size:10px}.factor-matrix td{font-size:12px}.badge{display:inline-block;color:var(--muted);font-weight:600;font-size:12px;line-height:1.5}.badge.good{color:var(--green)}.badge.bad{color:var(--red)}.badge.warn{color:var(--yellow)}.type-badge{min-width:50px;padding:0 7px;color:#cad1dc;background:#333c49;line-height:19px;border-radius:1px;font-size:12px}.type-badge.good{background:#2e8f44;color:#fff}.type-badge.bad{background:#b43c45;color:#fff}.type-badge.warn{background:#a48422;color:#fff}.country-code{color:#dce1e9}.risk-legend,.score-row{display:grid;grid-template-columns:110px minmax(150px,1fr) 170px;gap:10px;align-items:center}.risk-legend{font-size:11px;margin-bottom:3px}.risk-legend>div{display:grid;grid-template-columns:repeat(5,1fr);text-align:center;color:#bfc7d1}.risk-legend>div span:nth-child(-n+2){color:var(--green)}.risk-legend>div span:nth-child(3){color:var(--yellow)}.risk-legend>div span:nth-child(n+4){color:var(--red)}.scores{display:flex;flex-direction:column;gap:3px}.score-row{min-height:23px}.score-name{font-size:12px}.score-track{height:21px;position:relative}.score-bands{display:flex;position:absolute;inset:3px 0;background:#25313a;overflow:hidden}.band{width:20%}.band-0{background:#276438}.band-1{background:#3d7943}.band-2{background:#8a731f}.band-3{background:#8f353a}.band-4{background:#70272e}.score-marker{position:absolute;top:0;z-index:1;transform:translateX(-50%);background:#e9f2f3;color:#14272b;min-width:34px;padding:0 5px;line-height:21px;height:21px;text-align:center;font-size:12px;font-weight:700;box-shadow:0 0 0 1px #101216}.marker-0{left:10%}.marker-1{left:30%}.marker-2{left:50%}.marker-3{left:70%}.marker-4{left:90%}.no-score{border-bottom:1px solid #252b32}.score-unavailable{font-size:11px;color:#778291;line-height:21px}.score-row>.badge{white-space:nowrap;font-size:11px}.scope-line{font-size:11px;margin-top:6px;display:flex;gap:12px;color:var(--green);overflow-wrap:anywhere}.chips{display:flex;gap:6px 19px;flex-wrap:wrap;margin:6px 0}.chips .badge{font-size:12px}.inline-details{margin-top:8px}.inline-details summary{color:#b1bac6;font-size:11px;cursor:pointer}.inline-details summary::marker{color:#6d7c8c}.inline-details .small{margin:8px 0}.filter{display:block;font-size:11px;margin:8px 0}.report-end{display:flex;justify-content:space-between;gap:15px;color:#808d9d;border-top:2px solid #7a8693;padding-top:8px;margin-top:1px;font-size:10px}.report-end span:last-child{letter-spacing:1px;color:#5c6979}.notice{font-size:11px;color:#a0adba;padding:9px 1px;line-height:1.65}.card{border:1px solid var(--line);border-radius:5px;background:#20252e;padding:11px 15px;margin:8px 0}.card summary{font-size:12px;cursor:pointer;color:#cbd5e2}.card summary .muted{font-size:11px;margin-left:8px}.card p{font-size:12px}.source-row{display:grid;grid-template-columns:130px 80px 1fr;gap:12px;padding:7px 0;border-bottom:1px solid #333b46;font-size:11px}.source-row a{font-family:var(--mono)}.muted{color:var(--muted)}.small{font-size:11px}a{color:var(--cyan)}footer{text-align:center;font-size:10px;color:#7f8a98;margin-top:18px}dialog{width:min(680px,95vw);max-height:90vh;border:1px solid #536071;background:#20252f;border-radius:8px;padding:24px;color:var(--ink)}dialog::backdrop{background:#0009}dialog h2{font-size:18px;margin:0}dialog p{font-size:12px;color:var(--muted)}.keyfield{display:block;margin:15px 0}.keyfield span{display:block;margin-bottom:5px;font-size:12px}.keyfield input{width:100%;font-family:var(--mono);font-size:12px}.keyfield a{font-size:11px;margin-left:8px}.check{display:block;margin:14px 0;font-size:12px}.dialogbuttons{display:flex;justify-content:flex-end;gap:8px;flex-wrap:wrap;margin-top:22px}@media(max-width:850px){main{padding:14px}.terminal{padding:16px 16px 12px}.toolbar{align-items:flex-start}.toolbar-actions{gap:5px;flex-wrap:wrap;justify-content:flex-end}button{padding:7px 10px}.risk-legend,.score-row{grid-template-columns:95px minmax(110px,1fr) 155px;gap:6px}.matrix{min-width:650px}.section-note{margin-left:0}.report-heading h2{font-size:16px}}@media(max-width:550px){.toolbar{flex-direction:column;gap:12px}.inputrow{flex-wrap:wrap}.inputrow label{width:100%;font-size:11px}#ip{width:100%;flex-basis:calc(100% - 140px)}.options{gap:10px}.report-meta{gap:4px;flex-direction:column}.risk-legend,.score-row{grid-template-columns:85px minmax(85px,1fr) 70px}.score-row>.badge{white-space:normal;line-height:1.2;font-size:10px}.score-marker{font-size:10px;padding:0 2px;min-width:24px}.fact{grid-template-columns:80px 1fr}.terminal{padding:14px 11px}.report-end span:last-child{display:none}.source-row{grid-template-columns:85px 55px 1fr}progress{width:60px}.run-status{gap:8px}.scope-line{font-size:10px}}@media print{body{background:#101216;print-color-adjust:exact;-webkit-print-color-adjust:exact}main{max-width:none;padding:0}.toolbar,.query,.notice,.card,footer{display:none}.terminal{border:0;box-shadow:none}.inline-details{display:none}}\n\n/* Score bars: transparent track with value and level after the actual fill. */\n.score-track{background:transparent;overflow:visible;border:0}.score-fill{height:100%;min-width:0;border-radius:2px}.score-end-label{position:absolute;top:0;margin-left:8px;display:inline-flex;align-items:center;gap:8px;height:21px;white-space:nowrap}.score-value{position:static;font-weight:700;font-size:12px;color:var(--ink);line-height:21px}.score-end-label .badge{font-size:11px}.score-track.qualitative{display:flex;align-items:center;gap:8px}.score-track.no-score{border:0}.factor-matrix{min-width:820px}\n.risk-legend,.score-row{grid-template-columns:110px minmax(100px,1fr) 110px}.risk-legend>div{display:grid;grid-template-columns:repeat(5,1fr);text-align:center}\n@media(max-width:850px){.risk-legend,.score-row{grid-template-columns:95px minmax(85px,1fr) 100px}}\n@media(max-width:550px){.risk-legend,.score-row{grid-template-columns:85px minmax(65px,1fr) 95px}.score-value{font-size:11px}.score-end-label{gap:5px;margin-left:6px}.risk-legend{font-size:10px}}\n\n/* Three equal reference segments; labels are white inside each segment. */\n.risk-legend{margin-bottom:7px}.risk-legend>.risk-reference{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));height:23px;overflow:hidden;border-radius:2px}.risk-reference>div{display:flex;align-items:center;justify-content:space-between;padding:0 5px;white-space:nowrap;min-width:0}.risk-reference>.reference-yellow{justify-content:center}.reference-green{background:#28b85b}.reference-yellow{background:#e5b82e}.reference-red{background:#ef5350}.risk-reference span{color:#fff!important;font-weight:600;text-shadow:0 1px 2px #000b,0 0 2px #0008}.score-fill.band-0,.score-fill.band-1{background:#28b85b}.score-fill.band-2{background:#e5b82e}.score-fill.band-3,.score-fill.band-4{background:#ef5350}\n\n/* NQ uses ANSI 41/42/43. Browser mapping: Windows Terminal Campbell.\n NQ does not define fixed RGB values; terminal themes may render differently. */\n:root{--nq-red:#c50f1f;--nq-green:#13a10e;--nq-yellow:#c19c00}\n.type-badge.good,.reference-green,.score-fill.band-0,.score-fill.band-1{background:var(--nq-green)}\n.type-badge.warn,.reference-yellow,.score-fill.band-2{background:var(--nq-yellow)}\n.type-badge.bad,.reference-red,.score-fill.band-3,.score-fill.band-4{background:var(--nq-red)}\n.risk-reference span{text-shadow:none}\n</style></head>\n<body><main>\n <header class=\"toolbar\"><div class=\"brand\"><span class=\"brand-symbol\">IP</span><div><h1>IP 质量体检</h1><span class=\"muted\">任意 IP 查询 · 终端报告版</span></div></div><div class=\"toolbar-actions\"><button id=\"screenshot\" class=\"secondary\" title=\"下载完整报告 PNG 图片\">截图</button><button id=\"exportJson\" class=\"secondary\" disabled>导出 JSON</button><button id=\"exportCsv\" class=\"secondary\" disabled>导出 CSV</button><button id=\"settingsButton\" class=\"secondary\">接口设置</button></div></header>\n <section class=\"query\"><form id=\"queryForm\"><div class=\"inputrow\"><label for=\"ip\">目标 IP</label><input id=\"ip\" placeholder=\"输入 IPv4 / IPv6 地址\" autocomplete=\"off\" spellcheck=\"false\" required><button id=\"start\" type=\"submit\">开始查询</button><button id=\"stop\" type=\"button\" class=\"secondary\" disabled>停止</button></div></form><div class=\"run-status\"><span id=\"status\" role=\"status\" aria-live=\"polite\">输入公网 IP 后开始查询</span><progress id=\"progress\" max=\"1\" value=\"0\"></progress></div></section>\n <article class=\"terminal\" aria-label=\"IP 质量体检报告\">\n <div class=\"report-heading\"><h2>IP 质量体检报告:<span id=\"reportTitle\">等待查询</span></h2><p><a href=\"https://github.com/xykt/IPQuality\" target=\"_blank\" rel=\"noreferrer\">参考 xykt / IPQuality</a><span class=\"heading-divider\"> · </span>任意 IP 查询版</p><div class=\"report-meta\"><span id=\"reportTime\">检测时间:—</span><span>版本:v2.10.1</span></div></div>\n <section class=\"report-section\"><div class=\"sectionhead\"><h2>一、基础信息</h2><span id=\"basicSource\" class=\"muted\"></span></div><div id=\"basic\" class=\"facts\"></div></section>\n <section class=\"report-section\"><div class=\"sectionhead\"><h2>二、IP 类型</h2></div><div class=\"scroll\"><table class=\"matrix type-matrix\"><thead id=\"typesHead\"></thead><tbody id=\"types\"></tbody></table></div></section>\n <section class=\"report-section\"><div class=\"sectionhead\"><h2>三、风险评分</h2><span class=\"section-note\" title=\"条形按风险分界值分段线性换算;低与中等、中等与高的换算节点为 33%、66% 长度,临界值归入较高等级。\">按风险区间换算 · 悬停查看分界值</span></div><div class=\"risk-legend\"><span>风险等级:</span><div class=\"risk-reference\" aria-label=\"风险等级对照:绿色极低至低,黄色中等,红色高至极高\"><div class=\"reference-green\"><span>极低</span><span>低</span></div><div class=\"reference-yellow\"><span>中等</span></div><div class=\"reference-red\"><span>高</span><span>极高</span></div></div><span></span></div><div id=\"scores\" class=\"scores\"></div><div class=\"scope-line\"><span>ipapi 网段:</span><span id=\"scoreNetwork\">—</span></div></section>\n <section class=\"report-section\"><div class=\"sectionhead\"><h2>四、风险因子</h2><span class=\"section-note\">-:数据库未返回 · 无:该字段未提供</span></div><div class=\"scroll\"><table class=\"matrix factor-matrix\"><thead id=\"factorsHead\"></thead><tbody id=\"factors\"></tbody></table></div></section>\n <div class=\"report-end\"><span id=\"reportFoot\">等待目标 IP · 未查询不代表低风险</span><span>IP QUALITY CHECK</span></div>\n </article>\n <aside id=\"notice\" class=\"notice\">查询会将目标 IP 发送给数据库。报告仅保留在当前页面。</aside>\n <details class=\"card\" id=\"sourceDetails\"><summary>数据源状态 <span id=\"sourceCount\" class=\"muted\">等待查询</span></summary><p class=\"muted small\">公共接口受限时可配置自己的 API。免费套餐不一定包含全部风控字段。</p><div id=\"sources\"></div></details>\n <details class=\"card\"><summary>使用说明与来源</summary><p>油猴单文件版:安装后可删除下载文件。收藏 https://example.com/#ip-quality 打开查询界面;无需保留本地文件夹。代码和已保存的 API 配置由油猴管理,删除此脚本会影响它们。入口网页不接收表单查询;目标 IP 只通过查询接口发给所选数据库。</p><p>这是基于 xykt/IPQuality 的独立查询脚本,不是原作者发布的工具。保留模块 1~4;不做流媒体、端口连接测试,不上传报告。</p><p>原脚本的 MaxMind 与多库公共接口由 Check.Place 提供。本工具不保证它们永久可用。Ipregistry 使用你的 API Key;DB-IP 未填 Key 时只能获得免费基础字段。</p><p>MaxMind 不可用时,基础信息依次回退到 IPinfo、ipapi、DB-IP,界面标明实际来源。地理位置是数据库记录,不是物理位置实测。</p><p>API Key 默认仅用于本次页面会话。勾选保存后存于本机 油猴脚本存储,不同步;不要把配置或含密钥的 API 地址分享给别人。</p><a href=\"https://github.com/xykt/IPQuality\" target=\"_blank\" rel=\"noreferrer\">原项目与源码</a> · <a href=\"https://www.gnu.org/licenses/agpl-3.0.html\" target=\"_blank\" rel=\"noreferrer\">AGPL-3.0 许可证</a></details>\n <footer>IP 质量查询 2.10 · 油猴版 · 按目标 IP 查询 · 开源 AGPL-3.0</footer>\n</main>\n<dialog id=\"settings\"><form id=\"settingsForm\"><h2>接口设置</h2><p>优先使用你填写的 API;留空时尝试原脚本公共接口。Ipregistry 需要自有 Key;DB-IP 免费接口不含完整风控。</p><label class=\"check\"><input type=\"checkbox\" id=\"relay\" checked> 启用原脚本 Check.Place 公共接口</label><div id=\"keyFields\"></div><label class=\"check\"><input type=\"checkbox\" id=\"remember\"> 将 API 配置保存在本机浏览器</label><p class=\"small muted\">关闭页面后,未保存的密钥会消失。导出报告不包含配置和密钥。</p><div class=\"dialogbuttons\"><button id=\"clearKeys\" type=\"button\" class=\"secondary\">清除已保存密钥</button><button id=\"closeSettings\" type=\"button\" class=\"secondary\">取消</button><button type=\"submit\">应用</button></div></form></dialog>\n</body></html>\n";
const parsed=new DOMParser().parseFromString(markup,'text/html');
const root=document.importNode(parsed.documentElement,true);
if(document.documentElement)document.replaceChild(root,document.documentElement);else document.appendChild(root);
// SPDX-License-Identifier: AGPL-3.0-only
// Adapted field mappings / risk thresholds from xykt/IPQuality (AGPL-3.0).
function parseIP(input) {
const text = String(input).trim();
if (/^(0|[1-9]\d{0,2})(\.(0|[1-9]\d{0,2})){3}$/.test(text)) {
const parts = text.split('.').map(Number);
if (parts.some(n => n > 255)) throw new Error('IPv4 每段必须在 0~255 之间');
const [a,b] = parts;
return {ip: parts.join('.'), version: 4, private: a===10 || a===127 || a===0 || a>=224 || (a===172 && b>=16 && b<=31) || (a===192 && b===168) || (a===169 && b===254) || (a===100 && b>=64 && b<=127)};
}
if (text.includes(':') && /^[\da-fA-F:.]+$/.test(text)) {
try {
const ip = new URL('http://['+text+']/').hostname.slice(1,-1);
return {ip, version: 6, private: ip==='::' || ip==='::1' || /^(f[cd]|fe[89ab]|ff)/i.test(ip)};
} catch {}
}
throw new Error('请输入单个有效 IPv4 或 IPv6 地址,不含端口、网址或 /24');
}
function get(o, path) { return path.split('.').reduce((v,k)=>v?.[k],o) ?? null; }
function bool(v) { return v===true || v==='true' ? true : v===false || v==='false' ? false : null; }
function any(...vs) { const bs=vs.map(bool); return bs.includes(true) ? true : bs.every(v=>v===false) ? false : null; }
function numeric(v) { return v===null || v===undefined || (typeof v==='string' && v.trim()==='') || !['string','number'].includes(typeof v) ? null : Number.isFinite(Number(v)) ? Number(v) : null; }
// Risk levels follow the user's display thresholds; scores remain provider values.
function risk(id, value) {
if (id==='ipapi') {
const m=typeof value==='string' && value.match(/^([\d.]+)\s*\(([^)]+)\)$/);
const n=numeric(m?m[1]:value);
if(n===null || n<0 || n>1)return null;
return {value:n,text:(n*100).toFixed(2)+'%',level:n<.01?'低':n<.03?'中等':'高',scope:'company.network 网段滥用比例'};
}
if(id==='dbip')return typeof value==='string'?{value:null,text:value,level:({low:'低',medium:'中等',high:'高'})[value.toLowerCase()]??value,scope:'数据库威胁等级(无数值评分)'}:null;
const n=numeric(value);if(n===null || n<0 || n>100)return null;
const level=id==='ipqs'?(n<75?'低':n<90?'中等':'高'):['scamalytics','abuseipdb','ip2location'].includes(id)?(n<10?'极低':n<20?'低':n<40?'中等':n<60?'高':'极高'):null;
return level?{value:n,text:String(n),level,scope:id==='abuseipdb'?'Abuse Confidence Score':'数据库评分'}:null;
}
// Piecewise interpolation onto the shared risk-level axis.
function scoreWidth(id,value){
if(value===null || value===undefined)return null;
const n=id==='ipapi'?value*100:value;
if(!Number.isFinite(n)||n<0||n>100)return null;
const points=id==='ipapi'?[[0,0],[1,33],[3,66],[100,100]]:id==='ipqs'?[[0,0],[75,33],[90,66],[100,100]]:[[0,0],[20,33],[40,66],[100,100]];
for(let i=1;i<points.length;i++){
const [x,y]=points[i], [prevX,prevY]=points[i-1];
if(n<=x)return prevY+(n-prevX)/(x-prevX)*(y-prevY);
}
return 100;
}
const MAXMIND_SERVICES={
'geolite-city':{name:'GeoLite City(免费账户)',url:'https://geolite.info/geoip/v2.1/city/'},
'geolite-country':{name:'GeoLite Country(免费账户)',url:'https://geolite.info/geoip/v2.1/country/'},
'geoip-city':{name:'GeoIP City Plus(付费/试用)',url:'https://geoip.maxmind.com/geoip/v2.1/city/'},
'geoip-country':{name:'GeoIP Country(付费/试用)',url:'https://geoip.maxmind.com/geoip/v2.1/country/'},
'geoip-insights':{name:'GeoIP Insights(付费/试用)',url:'https://geoip.maxmind.com/geoip/v2.1/insights/'}
};
let regionNames;
function regionLabel(code,name){
const text=String(code??'').trim(), match=text.match(/^\[?([a-z]{2})\]?$/i);
if(!match)return text||name||null;
const iso=match[1].toUpperCase();let zh;
try{regionNames??=new Intl.DisplayNames(['zh-Hans'],{type:'region',fallback:'none'});zh=regionNames.of(iso);}catch{}
return '['+iso+']'+(zh?' '+zh:'')+(name&&name!==zh&&name!==iso?' · '+name:'');
}
const SOURCES=[
{id:'maxmind',name:'MaxMind',web:'https://www.maxmind.com/'},
{id:'ipinfo',name:'IPinfo',web:'https://ipinfo.io/'},
{id:'scamalytics',name:'Scamalytics',web:'https://scamalytics.com/ip'},
{id:'ipregistry',name:'Ipregistry',web:'https://ipregistry.co/'},
{id:'ipapi',name:'ipapi.is',web:'https://ipapi.is/'},
{id:'abuseipdb',name:'AbuseIPDB',web:'https://www.abuseipdb.com/'},
{id:'ip2location',name:'IP2Location',web:'https://www.ip2location.io/'},
{id:'dbip',name:'DB-IP',web:'https://db-ip.com/'},
{id:'ipdata',name:'ipdata',web:'https://ipdata.co/'},
{id:'ipqs',name:'IPQS',web:'https://www.ipqualityscore.com/free-ip-lookup-proxy-vpn-test'}
];
function normalize(id, raw) {
const d=id==='ipinfo' ? raw.data??raw : id==='abuseipdb' ? raw.data??raw : raw;
const p=path=>get(d,path), f={proxy:null,tor:null,vpn:null,server:null,abuser:null,robot:null};
const o={id,basic:{},usage:null,company:null,country:null,factors:f,score:null,network:null};
const flags=map=>Object.entries(map).forEach(([key,path])=>f[key]=bool(p(path)));
const basic=map=>Object.entries(map).forEach(([key,path])=>o.basic[key]=p(path));
switch(id) {
case 'maxmind':
if(d.country||d.registered_country||d.traits||d.location){
basic({asn:'traits.autonomous_system_number',org:'traits.autonomous_system_organization',city:'city.names.en',region:'subdivisions.0.names.en',country:'country.names.en',countryCode:'country.iso_code',registered:'registered_country.iso_code',continent:'continent.names.en',timezone:'location.time_zone',latitude:'location.latitude',longitude:'location.longitude',accuracy:'location.accuracy_radius',postal:'postal.code'});
o.basic.city??=p('city.names.zh-CN');o.basic.region??=p('subdivisions.0.names.zh-CN');o.basic.country??=p('country.names.zh-CN');o.country=o.basic.countryCode;break;
}
basic({asn:'ASN.AutonomousSystemNumber',org:'ASN.AutonomousSystemOrganization',city:'City.Name',region:'City.Subdivisions.0.Name',country:'City.Country.Name',countryCode:'City.Country.IsoCode',registered:'Country.RegisteredCountry.IsoCode',continent:'City.Continent.Name',timezone:'City.Location.TimeZone',latitude:'City.Latitude',longitude:'City.Longitude',accuracy:'City.AccuracyRadius',postal:'City.PostalCode'});
o.basic.registered??=p('City.RegisteredCountry.IsoCode');o.basic.country??=p('Country.Name');o.basic.countryCode??=p('Country.IsoCode'); break;
case 'ipinfo':
basic({asn:'asn.asn',org:'asn.name',city:'city',region:'region',countryCode:'country',registered:'abuse.country',timezone:'timezone',postal:'postal',coordinates:'loc'});
o.basic.org??=p('org');o.usage=p('asn.type');o.company=p('company.type');o.country=p('country');o.network=p('asn.route');
flags({proxy:'privacy.proxy',tor:'privacy.tor',vpn:'privacy.vpn',server:'privacy.hosting'});break;
case 'ipapi':
basic({asn:'asn.asn',org:'asn.org',city:'location.city',region:'location.state',country:'location.country',countryCode:'location.country_code',timezone:'location.timezone',latitude:'location.latitude',longitude:'location.longitude',postal:'location.zip'});
o.usage=p('asn.type');o.company=p('company.type');o.country=p('location.country_code');o.network=p('company.network');o.score=risk(id,p('company.abuser_score'));
flags({proxy:'is_proxy',tor:'is_tor',vpn:'is_vpn',server:'is_datacenter',abuser:'is_abuser',robot:'is_crawler'});break;
case 'scamalytics':
o.score=risk(id,p('scamalytics.scamalytics_score')??p('score'));o.country=p('external_datasources.maxmind_geolite2.ip_country_code');
flags({proxy:'external_datasources.firehol.is_proxy',tor:'external_datasources.x4bnet.is_tor',vpn:'scamalytics.scamalytics_proxy.is_vpn',server:'scamalytics.scamalytics_proxy.is_datacenter',abuser:'scamalytics.is_blacklisted_external'});
f.robot=any(p('external_datasources.x4bnet.is_blacklisted_spambot'),p('external_datasources.x4bnet.is_bot_operamini'),p('external_datasources.x4bnet.is_bot_semrush'));break;
case 'ipregistry':
o.usage=p('connection.type');o.company=p('company.type');o.country=p('location.country.code');
flags({proxy:'security.is_proxy',vpn:'security.is_vpn',server:'security.is_cloud_provider',abuser:'security.is_abuser'});f.tor=any(p('security.is_tor'),p('security.is_tor_exit'));break;
case 'abuseipdb':o.usage=p('usageType');o.country=p('countryCode');o.score=risk(id,p('abuseConfidenceScore'));break;
case 'ip2location':
o.usage=p('usage_type');o.company=p('as_info.as_usage_type');o.country=p('country_code');o.score=risk(id,p('fraud_score'));
f.proxy=any(p('is_proxy'),p('proxy.is_public_proxy'),p('proxy.is_web_proxy'));
flags({tor:'proxy.is_tor',vpn:'proxy.is_vpn',server:'proxy.is_data_center',abuser:'proxy.is_spammer'});
f.robot=any(p('proxy.is_web_crawler'),p('proxy.is_scanner'),p('proxy.is_botnet'));break;
case 'dbip':o.country=p('countryCode');o.score=risk(id,p('threatLevel'));flags({proxy:'isProxy',robot:'isCrawler'});basic({city:'city',region:'stateProv',country:'countryName',countryCode:'countryCode',continent:'continentName'});break;
case 'ipdata':o.country=p('country_code');flags({proxy:'threat.is_proxy',tor:'threat.is_tor',server:'threat.is_datacenter'});f.abuser=any(p('threat.is_threat'),p('threat.is_known_abuser'),p('threat.is_known_attacker'));break;
case 'ipqs':o.country=p('country_code');o.score=risk(id,p('fraud_score'));flags({proxy:'proxy',tor:'tor',vpn:'vpn',abuser:'recent_abuse',robot:'bot_status'});break;
}
return o;
}
function validateData(id, data, ip) {
if (!data || typeof data!=='object' || Array.isArray(data)) throw new Error('接口未返回数据对象');
if (data.error || data.errors || data.errorCode || data.success===false) throw new Error('接口返回错误;请检查权限、配额或 API Key');
const returned=id==='scamalytics'?(data.scamalytics?.ip??data.ip):id==='maxmind'?data.traits?.ip_address:id==='abuseipdb'?data.data?.ipAddress:id==='dbip'?data.ipAddress:id==='ipinfo'?(data.data?.ip??data.ip):data.ip;
if(returned) {
let same=false;try{same=parseIP(returned).ip===parseIP(ip).ip;}catch{}
if(!same) throw new Error('接口返回的 IP 与目标不一致,已丢弃结果');
}
if(id==='scamalytics'&&data.scamalytics?.status&&data.scamalytics.status!=='ok')throw new Error('Scamalytics 返回错误,请检查 User、API Key 和接口权限');
const n=normalize(id,data);
if(!n.score && !n.usage && !n.company && !n.country && !Object.values(n.basic).some(v=>v!==null) && !Object.values(n.factors).some(v=>v!==null)) throw new Error('未返回可用字段,可能受限或接口结构已改变');
return n;
}
function migrateScamalytics(settings){
const key=settings.keys?.scamalytics??'';
if(!/^https:\/\//i.test(key))return settings;
try{
const url=new URL(key),parts=url.pathname.split('/').filter(Boolean);
if(!/^api[0-9]*\.scamalytics\.com$/.test(url.hostname)||url.username||url.password||!url.searchParams.get('key'))return settings;
if(!(parts.length===1||parts.length===2&&parts[0]==='v3'))return settings;
settings.scamalytics={user:decodeURIComponent(parts.at(-1)),endpoint:url.origin+(parts.length===2?'/v3/':'/')};
settings.keys.scamalytics=url.searchParams.get('key');
}catch{}
return settings;
}
function requestSpec(id, ip, settings={}) {
const key=(settings.keys?.[id]??'').trim(), q=encodeURIComponent(ip), k=encodeURIComponent(key);
const direct=(url,options={})=>({url,options,via:'官方接口'});
if(id==='maxmind'&&(key||settings.maxmind?.accountId)){
const account=String(settings.maxmind?.accountId??'').trim();
if(!account||!key)return {skip:'MaxMind 需同时填写 Account ID 和 License Key'};
if(!/^\d+$/.test(account)||!/^[\x21-\x7e]+$/.test(key))throw new Error('MaxMind Account ID 应为数字,请检查 License Key 格式');
const service=MAXMIND_SERVICES[settings.maxmind?.service??'geolite-city'];
if(!service)throw new Error('请选择有效的 MaxMind 服务');
return direct(service.url+q,{headers:{Authorization:'Basic '+btoa(account+':'+key),Accept:'application/json'}});
}
if(id==='scamalytics'&&(key||settings.scamalytics?.user)){
const user=String(settings.scamalytics?.user??'').trim();
if(/^https?:\/\//i.test(key))throw new Error('请分别填写 Scamalytics User 和 API Key,不再填写完整网址');
if(!key||!user)return {skip:'Scamalytics 需同时填写 User 和 API Key'};
if(user==='.'||user==='..'||/[\/\\?#\s]/.test(user))throw new Error('Scamalytics User 格式无效,请输入服务商分配的用户名');
const endpoint=settings.scamalytics?.endpoint??'https://api11.scamalytics.com/v3/';
if(!/^https:\/\/api[0-9]*\.scamalytics\.com\/(?:v3\/)?$/.test(endpoint))throw new Error('Scamalytics 服务地址无效');
const url=new URL(endpoint+encodeURIComponent(user));url.searchParams.set('key',key);url.searchParams.set('ip',ip);
return direct(url.href);
}
if(id==='ipregistry') {
if(!key) return {skip:'需要填写自己的 Ipregistry API Key'};
return direct('https://api.ipregistry.co/'+q,{headers:{Authorization:'ApiKey '+key}});
}
if(id==='dbip') return direct('https://api.db-ip.com/v2/'+(k||'free')+'/'+q);
if(id==='ipinfo') return direct(key?'https://ipinfo.io/'+q+'/json?token='+k:'https://ipinfo.io/widget/demo/'+q);
if(key) {
if(id==='ipapi')return direct('https://api.ipapi.is/',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({q:ip,key})});
if(id==='abuseipdb')return direct('https://api.abuseipdb.com/api/v2/check?ipAddress='+q+'&maxAgeInDays=90',{headers:{Key:key,Accept:'application/json'}});
if(id==='ip2location')return direct('https://api.ip2location.io/?key='+k+'&ip='+q);
if(id==='ipqs')return direct('https://www.ipqualityscore.com/api/json/ip/'+k+'/'+q);
if(id==='ipdata')return direct('https://api.ipdata.co/'+q+'?api-key='+k);
}
if(settings.relay===false)return {skip:'未启用原脚本接口,且未配置此数据源的自有 API'};
const db=id==='ipqs'?'ipqualityscore':id;
return {url:'https://ipinfo.check.place/'+q+(id==='maxmind'?'?lang=en':'?db='+db),via:'原脚本公共接口'};
}
function csvCell(v) {
let s=String(v??'');if(/^[=+\-@\t\r]/.test(s))s="'"+s;
return '"'+s.replaceAll('"','""')+'"';
}
// SPDX-License-Identifier: AGPL-3.0-only
function jsonRequest(url, options={}, parentSignal, timeout=15000) {
return new Promise((resolve,reject)=>{
let request, settled=false;
const finish=(err,data)=>{if(settled)return;settled=true;parentSignal?.removeEventListener('abort',abort);err?reject(err):resolve(data);};
const abort=()=>{finish(new Error('已停止'));request?.abort();};
if(parentSignal?.aborted){abort();return;}
parentSignal?.addEventListener('abort',abort,{once:true});
try {
request=GM_xmlhttpRequest({url,method:options.method??'GET',headers:options.headers??{},data:options.body,
anonymous:true,timeout,responseType:'text',
onload:r=>{
if(r.status<200||r.status>=300){finish(new Error(r.status===429?'HTTP 429:查询配额或速率受限':r.status===401||r.status===403?'HTTP '+r.status+':接口拒绝访问,可能需要自有 API Key':'HTTP '+r.status));return;}
try{finish(null,JSON.parse(r.responseText));}catch{finish(new Error('接口未返回 JSON,可能受限或已变更'));}
},
onerror:()=>finish(new Error('无法连接接口,请检查网络及油猴的域名访问权限')),
ontimeout:()=>finish(new Error('请求超时')),
onabort:()=>finish(new Error('已停止'))
});
}catch{finish(new Error('请求未能启动,请检查油猴权限'));}
});
}
async function lookupSource(source,ip,settings,signal) {
const start=Date.now();let via='';
try {
const spec=requestSpec(source.id,ip,settings);via=spec.via??'';
if(spec.skip)return {id:source.id,status:'skipped',error:spec.skip};
const raw=await jsonRequest(spec.url,spec.options,signal);
const normalized=validateData(source.id,raw,ip);
return {id:source.id,status:'ok',via,normalized,raw,ms:Date.now()-start};
} catch(e) {return {id:source.id,status:signal.aborted?'cancelled':'error',via,error:e.message,ms:Date.now()-start};}
}
async function pool(items,concurrency,worker,signal) {
let index=0;
await Promise.all(Array.from({length:Math.min(items.length,concurrency)},async()=>{
while(index<items.length && !signal?.aborted){const i=index++;await worker(items[i],i);}
}));
}
// SPDX-License-Identifier: AGPL-3.0-only
const $=id=>document.getElementById(id);
const el=(tag,text,className)=>{const n=document.createElement(tag);if(text!==undefined)n.textContent=text;if(className)n.className=className;return n;};
let settings={keys:{},relay:true},run=null,report=null;
const states={pending:'等待',ok:'已返回',error:'失败',skipped:'未查询',cancelled:'已停止'};
const sourceLink=(s,ip)=> {
if(s.id==='ipinfo')return 'https://ipinfo.io/'+ip;
if(s.id==='scamalytics')return 'https://scamalytics.com/ip/'+ip;
if(s.id==='abuseipdb')return 'https://www.abuseipdb.com/check/'+ip;
if(s.id==='ipqs')return 'https://www.ipqualityscore.com/free-ip-lookup-proxy-vpn-test/lookup/'+ip;
return s.web;
};
const riskClass=v=>/高|风险|DoS/.test(v)?'bad':/中等|可疑/.test(v)?'warn':/低/.test(v)?'good':'';
const badge=(text,cls='')=>el('span',text,'badge '+cls);
const cell=(row,value)=>{const c=el('td');c.append(value instanceof Node?value:String(value??'—'));row.append(c);};
function setBusy(busy){
for(const id of ['ip','start','settingsButton'])$(id).disabled=busy;
$('stop').disabled=!busy;
}
function updateProgress(){
if(!report)return;
const db=Object.values(report.sources).filter(x=>x.status!=='pending').length;
$('progress').max=SOURCES.length;
$('progress').value=db;
$('status').textContent=(run?'查询中':'本轮结束')+' · 数据库 '+db+'/'+SOURCES.length;
}
const displayNames={ipapi:'ipapi',ipregistry:'ipregistry',ip2location:'IP2Location',abuseipdb:'AbuseIPDB',ipinfo:'IPinfo',scamalytics:'Scamalytics',ipqs:'IPQS',dbip:'DB-IP',ipdata:'ipdata'};
function typeBadge(value){
if(value==null)return el('span','—','unknown');
const text=String(value),key=text.toLowerCase();
const map={hosting:['机房','bad'],dch:['机房','bad'],'data center/web hosting/transit':['机房','bad'],isp:['ISP','good'],business:['商业','warn'],com:['商业','warn'],education:['教育','warn'],edu:['教育','warn'],government:['政府','warn'],gov:['政府','warn'],banking:['银行','warn'],cdn:['CDN','bad'],mob:['移动','good'],mobile:['移动','good']};
const match=map[key]??[text,''];const node=badge(match[0],match[1]+' type-badge');node.title=text;return node;
}
function matrixHead(id,columns){const row=el('tr');row.append(el('th','数据库:'));for(const key of columns)row.append(el('th',displayNames[key]));$(id).replaceChildren(row);}
function renderDB(){
const rows=SOURCES.map(s=>({...s,...(report?.sources[s.id]??{status:'pending'})}));
const byId=Object.fromEntries(rows.map(r=>[r.id,r]));
const basicRow=['maxmind','ipinfo','ipapi','dbip'].map(id=>byId[id]).find(r=>r.status==='ok'&&Object.values(r.normalized.basic).some(v=>v!==null));
const b=basicRow?.normalized.basic??{};
$('basic').replaceChildren();$('basicSource').textContent=basicRow?'('+basicRow.name+' 数据库)':'(等待数据)';
const country=regionLabel(b.countryCode,b.country);
const city=[b.region,b.city,b.postal].filter(Boolean).join(', ');
const asn=b.asn==null?null:String(b.asn).startsWith('AS')?b.asn:'AS'+b.asn;
const facts=[['ASN',asn],['组织',b.org],['坐标',b.coordinates??(b.latitude!=null&&b.longitude!=null?b.latitude+', '+b.longitude:null)],['城市',city||null],['地理地区',country],['注册地区',regionLabel(b.registered)],['时区',b.timezone]];
for(const [label,value]of facts){const box=el('div',undefined,'fact');box.append(el('span',label+':','fact-label'),el('span',value??'—',value==null?'unknown':'fact-value'));$('basic').append(box);}
const typeColumns=['ipinfo','ipregistry','ipapi','ip2location','abuseipdb'];
matrixHead('typesHead',typeColumns);$('types').replaceChildren();
for(const [key,label]of [['usage','用途:'],['company','公司:']]){const row=el('tr');row.append(el('th',label));for(const id of typeColumns)cell(row,typeBadge(byId[id].normalized?.[key]));$('types').append(row);}
const factorColumns=['ip2location','ipapi','ipregistry','ipqs','scamalytics','ipdata','ipinfo','dbip'];
matrixHead('factorsHead',factorColumns);$('factors').replaceChildren();
for(const [key,label]of [['country','地区:'],['proxy','代理:'],['tor','Tor:'],['vpn','VPN:'],['server','服务器:'],['abuser','滥用:'],['robot','机器人:']]){
const row=el('tr');row.append(el('th',label));
for(const id of factorColumns){
const source=byId[id],n=source.normalized;
if(source.status!=='ok'||!n){cell(row,Object.assign(el('span','-','unknown'),{title:'数据库未返回可用结果'}));continue;}
const v=key==='country'?n.country:n.factors[key];
cell(row,key==='country'?(v?el('span',v,'country-code'):Object.assign(badge('无','good'),{title:'未提供此项结果'})):v===true?badge('是','bad'):v===false?badge('否','good'):Object.assign(badge('无','good'),{title:'未提供此项结果'}));
}$('factors').append(row);
}
$('scores').replaceChildren();
for(const id of ['ip2location','scamalytics','ipapi','abuseipdb','ipqs','dbip']){
const r=byId[id],score=r.normalized?.score;
const row=el('div',undefined,'score-row');row.append(el('span',displayNames[id]+':','score-name'));
const track=el('div',undefined,'score-track'+(score?'':' no-score'));
if(score){
const index=/极低/.test(score.level)?0:score.level==='低'?1:/中等|可疑/.test(score.level)?2:/极高|高风险/.test(score.level)?4:3;
if(score.value!==null){
const percent=Math.max(1,scoreWidth(id,score.value));
const fill=el('div',undefined,'score-fill band-'+index);fill.style.width=percent+'%';
const label=el('span',undefined,'score-end-label');label.style.left=percent+'%';
label.append(el('span',score.text,'score-value'),badge(score.level,riskClass(score.level)));track.append(fill,label);
track.setAttribute('role','meter');track.setAttribute('aria-valuemin','0');track.setAttribute('aria-valuemax','100');track.setAttribute('aria-valuenow',String(percent));track.setAttribute('aria-valuetext',score.text+','+score.level);
}else{track.classList.add('qualitative');track.append(el('span',score.text,'score-unavailable'),badge(score.level,riskClass(score.level)));}
track.setAttribute('aria-label',displayNames[id]+' '+score.text+','+score.level);
const rule=id==='ipapi'?'低 <1%;中等 1%–<3%;高 ≥3%':id==='ipqs'?'低 <75;中等 75–<90;高 ≥90':id==='dbip'?'仅显示数据库返回的等级':'极低 <10;低 10–<20;中等 20–<40;高 40–<60;极高 ≥60';
track.title=score.scope+';'+rule+';条形按风险分界值分段线性换算,视觉最短为 1%;完整数值见条末标签';
}else track.append(el('span',r.status==='pending'?'—':r.status==='ok'?'未提供评分':r.status==='error'?'查询失败':r.status==='skipped'?'未查询':'已停止','score-unavailable'));
row.append(track);
$('scores').append(row);
}
$('scoreNetwork').textContent=byId.ipapi.normalized?.network??'—';
$('sources').replaceChildren();
for(const r of rows){const n=r.normalized;const row=el('div',undefined,'source-row'),a=el('a',r.name);a.href=report?sourceLink(r,report.ip):r.web;a.target='_blank';a.rel='noreferrer';
row.append(a,badge(states[r.status],r.status==='ok'?'good':r.status==='error'?'warn':''),el('span',r.error??(r.via?r.via+' · '+r.ms+' ms'+(r.id==='dbip'&&!n?.score?' · 无风控字段':''):'等待查询')));$('sources').append(row);
}
const count=rows.filter(r=>r.status==='ok').length;$('sourceCount').textContent=report?count+'/'+SOURCES.length+' 已返回':'等待查询';
updateProgress();
}
async function start(event){
event.preventDefault();if(run)return;
let parsed;try{parsed=parseIP($('ip').value);}catch(e){$('status').textContent=e.message;return;}
if(parsed.private){$('status').textContent='这是私有、共享、回环或保留地址,公网风控数据库不适用。请输入公网 IP。';return;}
$('ip').value=parsed.ip;run=new AbortController();const signal=run.signal;setBusy(true);
$('exportJson').disabled=true;$('exportCsv').disabled=true;
report={tool:'IP 质量查询',version:'2.10.1',ip:parsed.ip,ipVersion:parsed.version,startedAt:new Date().toISOString(),completed:false,sources:Object.fromEntries(SOURCES.map(s=>[s.id,{status:'pending'}]))};
$('reportTitle').textContent=parsed.ip;
$('reportTime').textContent='检测时间:'+report.startedAt.slice(0,19).replace('T',' ')+' UTC';
$('reportFoot').textContent='正在查询 IPv'+parsed.version+' · '+parsed.ip;
renderDB();
const taskSettings=structuredClone(settings);
try {
await pool(SOURCES,3,async source=>{report.sources[source.id]=await lookupSource(source,parsed.ip,taskSettings,signal);renderDB();},signal);
report.completed=!signal.aborted;
}catch(e){report.error=e.message;}
finally {
for(const r of Object.values(report.sources))if(r.status==='pending'){r.status='cancelled';r.error=signal.aborted?'已停止':'未执行';}
report.finishedAt=new Date().toISOString();report.cancelled=signal.aborted;run=null;setBusy(false);renderDB();
const ok=Object.values(report.sources).filter(r=>r.status==='ok').length;
$('status').textContent=(report.error?'查询中断:'+report.error:report.cancelled?'已停止,保留已返回结果':'查询结束')+' · 数据源成功 '+ok+'/'+SOURCES.length;
$('notice').textContent=ok<SOURCES.length?'部分数据源未返回。查看下方状态;需要权限的接口可在设置中填写自有 API。缺失评分不代表低风险。':'报告仅反映各数据库当前记录;不同库的评分不能直接平均或互相替代。';
$('reportFoot').textContent=(report.cancelled?'查询已停止':report.error?'查询中断':'查询完成')+' · 数据源 '+ok+'/'+SOURCES.length+' · IPv'+parsed.version;
$('exportJson').disabled=false;$('exportCsv').disabled=false;
}
}
function publicReport(){
// Sanitize source responses as well as metadata; settings are never exported.
const secrets=Object.values(settings.keys).filter(v=>v&&v.length>=4);
if(settings.keys.scamalytics){
try{
const u=new URL(settings.keys.scamalytics.replaceAll('{ip}',encodeURIComponent(report.ip)));
secrets.push(u.href,...[...u.searchParams.values()].filter(v=>v.length>=4 && v!==report.ip),...u.pathname.split('/').filter(v=>v.length>=8 && v!==report.ip));
}catch{}
}
return JSON.parse(JSON.stringify(report,(key,value)=>{
if(/^(key|api[-_]?key|token|authorization|password|secret)$/i.test(key))return '[redacted]';
if(typeof value==='string')for(const secret of secrets)value=value.replaceAll(secret,'[redacted]').replaceAll(encodeURIComponent(secret),'[redacted]');
return value;
}));
}
function download(text,mime,extension){
const blob=new Blob([text],{type:mime});const url=URL.createObjectURL(blob),a=el('a');
a.href=url;a.download='ip-quality-'+report.ip.replaceAll(':','_')+'.'+extension;a.click();setTimeout(()=>URL.revokeObjectURL(url),3000);
}
async function screenshotReport(){
const button=$('screenshot');button.disabled=true;button.textContent='截图中…';
let frame;
try{
// Create the capture document only after a click, with no third-party runtime.
const css=[...document.querySelectorAll('style')].map(n=>n.textContent).join('\n');
frame=el('iframe');frame.setAttribute('aria-hidden','true');frame.tabIndex=-1;
frame.style.cssText='position:fixed;left:-20000px;top:0;width:1280px;height:1000px;border:0;pointer-events:none';
document.body.append(frame);
const doc=frame.contentDocument;
const style=doc.createElement('style');style.textContent=css;doc.head.append(style);
const target=doc.importNode(document.querySelector('.terminal'),true);
target.style.cssText='width:1072px;max-width:none;box-sizing:border-box;box-shadow:none;margin:0';
doc.body.style.cssText='margin:0;padding:0';doc.body.append(target);
await doc.fonts.ready;
const width=1072,height=Math.ceil(target.getBoundingClientRect().height);
if(height<1||height>12000)throw new Error('报告尺寸异常');
const wrapper=doc.createElement('div');wrapper.setAttribute('xmlns','http://www.w3.org/1999/xhtml');
wrapper.style.cssText='color:#ebedf2;background:#101216;font:13px/1.5 system-ui,Segoe UI,Microsoft YaHei,sans-serif';
wrapper.append(style.cloneNode(true),target.cloneNode(true));
const xml=new XMLSerializer().serializeToString(wrapper);
const svg='<svg xmlns="http://www.w3.org/2000/svg" width="'+width+'" height="'+height+'"><foreignObject width="100%" height="100%">'+xml+'</foreignObject></svg>';
const img=new Image();
await new Promise((resolve,reject)=>{
const timer=setTimeout(()=>reject(new Error('图片生成超时,请重试')),15000);
img.onload=()=>{clearTimeout(timer);resolve();};img.onerror=()=>{clearTimeout(timer);reject(new Error('浏览器无法绘制报告'));};
img.src='data:image/svg+xml;charset=utf-8,'+encodeURIComponent(svg);
});
const canvas=document.createElement('canvas');canvas.width=width*2;canvas.height=height*2;
const ctx=canvas.getContext('2d');ctx.fillStyle='#101216';ctx.fillRect(0,0,canvas.width,canvas.height);ctx.drawImage(img,0,0,canvas.width,canvas.height);
const blob=await new Promise((resolve,reject)=>canvas.toBlob(b=>b?resolve(b):reject(new Error('图片生成失败')),'image/png'));
const url=URL.createObjectURL(blob),a=el('a');a.href=url;a.download='ip-quality-'+(report?.ip??'report').replaceAll(':','_')+'-'+new Date().toISOString().replace(/[:.]/g,'-')+'.png';a.click();setTimeout(()=>URL.revokeObjectURL(url),60000);
button.title='图片已生成并提交浏览器下载';
}catch(e){$('status').textContent='截图失败:'+e.message;}
finally{frame?.remove();button.disabled=false;button.textContent='截图';}
}
$('screenshot').onclick=screenshotReport;
function exportCSV(){
const r=publicReport(),rows=[['模块','数据源','字段','结果','状态/说明']];
for(const [id,source]of Object.entries(r.sources)){
rows.push(['数据源',id,'状态',source.status,source.error??source.via??'']);const n=source.normalized;if(!n)continue;
for(const [field,value]of Object.entries(n.basic))rows.push(['1 基础信息',id,field,value,'']);
for(const field of ['usage','company','country','network'])rows.push(['2 IP类型',id,field,n[field],'']);
rows.push(['3 风险评分',id,'score',n.score?.text,n.score?.level]);
for(const [field,value]of Object.entries(n.factors))rows.push(['4 风险因子',id,field,value===null?'未知':value?'是':'否','']);
}
rows.unshift(['目标 IP',r.ip,'查询时间',r.startedAt,r.completed?'完成':'部分结果']);
download('\uFEFF'+rows.map(row=>row.map(csvCell).join(',')).join('\r\n'),'text/csv;charset=utf-8','csv');
}
const keySources=SOURCES;
for(const source of keySources){
if(source.id==='scamalytics'){
const label=el('label',undefined,'keyfield'),input=el('input');input.id='scamalyticsUser';input.autocomplete='off';input.spellcheck=false;input.placeholder='Scamalytics 分配的 User';label.append(el('span','Scamalytics User'),input);$('keyFields').append(label);
}
if(source.id==='maxmind'){
const accountLabel=el('label',undefined,'keyfield'),account=el('input');account.id='maxmindAccount';account.type='text';account.inputMode='numeric';account.pattern='[0-9]*';account.autocomplete='off';account.placeholder='MaxMind 账户中的数字 Account ID';accountLabel.append(el('span','MaxMind Account ID'),account);$('keyFields').append(accountLabel);
const serviceLabel=el('label',undefined,'keyfield'),select=el('select');select.id='maxmindService';
for(const [id,item]of Object.entries(MAXMIND_SERVICES)){const option=el('option',item.name);option.value=id;select.append(option);}
serviceLabel.append(el('span','MaxMind 服务'),select);$('keyFields').append(serviceLabel);
}
const label=el('label',undefined,'keyfield'),title=el('span',source.name+(source.id==='maxmind'?' License Key':source.id==='scamalytics'?' API Key':' API Key / Token'));
const link=el('a','官网');link.href=source.web;link.target='_blank';link.rel='noreferrer';title.append(link);
const input=el('input');input.type='password';input.id='key-'+source.id;input.autocomplete='off';input.spellcheck=false;input.placeholder='可选,填写你自己的密钥';label.append(title,input);$('keyFields').append(label);
}
$('settingsButton').onclick=()=>{$('scamalyticsUser').value=settings.scamalytics?.user??'';$('maxmindAccount').value=settings.maxmind?.accountId??'';$('maxmindService').value=settings.maxmind?.service??'geolite-city';for(const s of keySources)$('key-'+s.id).value=settings.keys[s.id]??'';$('relay').checked=settings.relay;$('settings').showModal();};
$('closeSettings').onclick=()=>$('settings').close();
$('settingsForm').onsubmit=async e=>{
e.preventDefault();settings={scamalytics:{user:$('scamalyticsUser').value.trim(),endpoint:settings.scamalytics?.endpoint??'https://api11.scamalytics.com/v3/'},maxmind:{accountId:$('maxmindAccount').value.trim(),service:$('maxmindService').value},relay:$('relay').checked,keys:Object.fromEntries(keySources.map(s=>[s.id,$('key-'+s.id).value.trim()]))};
try{if($('remember').checked)await GM_setValue('settings',settings);else await GM_deleteValue('settings');$('settings').close();}catch{$('status').textContent='配置已应用于当前页面,但本机保存失败';$('settings').close();}
};
$('clearKeys').onclick=async()=>{await GM_deleteValue('settings');settings={relay:true,keys:{}};$('scamalyticsUser').value='';$('maxmindAccount').value='';$('maxmindService').value='geolite-city';for(const s of keySources)$('key-'+s.id).value='';$('relay').checked=true;$('remember').checked=false;};
$('queryForm').addEventListener('submit',start);$('stop').onclick=()=>run?.abort();
$('exportJson').onclick=()=>download(JSON.stringify(publicReport(),null,2),'application/json','json');$('exportCsv').onclick=exportCSV;
try{const saved={settings:await GM_getValue('settings',null)};if(saved.settings){settings={scamalytics:saved.settings.scamalytics,maxmind:saved.settings.maxmind,relay:saved.settings.relay!==false,keys:saved.settings.keys??{}};$('remember').checked=true;}}catch{}
migrateScamalytics(settings);
renderDB();
})().catch(error=>{console.error('IP 质量体检初始化失败',error);});
最新回复 (9)
-
SeaMod 楼主 09-06 00:571楼^-^
-
-可鸽可弃- 09-06 00:572楼这个怎么用 有点没看明白
-
hiroqu 09-06 00:583楼这个功能有点鸡肋,直接去在线网站查也一样吧。
-
AngeloSakura 09-06 00:594楼bd
-
SeaMod 楼主 09-06 01:415楼^-^
-
PassionAries 09-06 01:446楼有点意思。。
-
xpro 09-06 07:247楼比网页操作麻烦了
-
waterflow 09-06 07:378楼费那老劲注册拿key,打开ipsuper直接查就是了……
-
SeaMod 楼主 09-06 17:499楼
* 帖子来源NodeSeek
附近帖子
- ↑gpt6不上架webchat吗
- ↑Googledrive 为啥 winodws 闪退 打不开
- ↑求助:购买的US家宽被ip2location识别为DCH
- ↑zouter老款JP是2T流量吗???
- ↑汇丰提款卡(红狮子)不支持绑定Apple Pay了
- 📍 NQ脚本浏览器油猴版,可自定义查询任意ip
- ↓大佬们华为老sg求估价
- ↓ccswitch反代astra请求不到上游,你们遇到这个问题了吗
- ↓Giffgaff APP能看到余额,这个是嘎了还是没嘎
- ↓OKX虚假关闭账户后续:发邮件给OKX要求删除数据,OKX无视邮件暂无回复
- ↓由于不了解,看到了补货通知 nobrand JP tyo2 ,以为是 cn2 ,浪费了 25 元,🥲
飞读
SeaMod
|
主题数 1 |
帖子数 1 |
注册排名 3 |