Cloudflare Detector

Cloudflare 节点检测(PC + 移动端优化 / IP / Colo / 状态 / 可拖拽 / 长按拖动 / 复制)

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Cloudflare Detector
// @namespace    https://github.com/coderyjf/CloudflareDetector
// @version      1.4.4
// @description  Cloudflare 节点检测(PC + 移动端优化 / IP / Colo / 状态 / 可拖拽 / 长按拖动 / 复制)
// @author       coderyjf
// @match        *://*/*
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @connect      *
// @run-at       document-idle
// @icon         https://www.google.com/s2/favicons?sz=64&domain=cloudflare.com
// @license      MIT
// ==/UserScript==

(function () {
  "use strict";

  /* =========================
           ⚙️ 配置
  ========================= */

  const CACHE_KEY = "cf_lite_pp";
  const CACHE_TTL = 30000;
  const POS_KEY = "cf_detector_pos_v5";
  const PANEL_GAP = 12;

  const IS_MOBILE =
    /Android|iPhone|iPad|iPod|HarmonyOS|Mobile/i.test(navigator.userAgent) ||
    window.innerWidth <= 768;

  const ICON_SIZE = IS_MOBILE ? 36 : 40;

  const LONG_PRESS_TIME = 220;

  /* =========================
         🌍 机场三字码
  ========================= */

  const coloMap = {
    // ==================== 亚洲 ====================

    // 中国 (China)
    KHN: "南昌",
    CAN: "广州",
    SZX: "深圳",
    SHA: "上海虹桥",
    PVG: "上海浦东",
    PEK: "北京首都",
    BJS: "北京",
    CKG: "重庆",
    CTU: "成都",
    KMG: "昆明",
    XIY: "西安",
    HGH: "杭州",
    WUH: "武汉",
    CSX: "长沙",
    SHE: "沈阳",
    DLC: "大连",
    TAO: "青岛",
    XMN: "厦门",
    NGB: "宁波",
    NKG: "南京",
    HRB: "哈尔滨",
    TSN: "天津",
    URC: "乌鲁木齐",
    LXA: "拉萨",
    KWE: "贵阳",
    FOC: "福州",
    CZX: "常州",
    CGD: "常德",
    ACX: "兴义",
    BHY: "北海",
    CGO: "郑州",
    FUO: "佛山",
    HAK: "海口",
    HFE: "合肥",
    HYN: "台州",
    JXG: "嘉兴",
    LHW: "兰州",
    LYA: "洛阳",
    NNG: "南宁",
    PKX: "北京大兴",
    SJW: "石家庄",
    TEN: "铜仁",
    TNA: "济南",
    TYN: "太原",
    WHU: "芜湖",
    XFN: "襄阳",
    XNN: "西宁",
    ZGN: "中山",
    TPE: "台北",
    KHH: "高雄",
    HKG: "香港",
    MFM: "澳门",

    // 日本 (Japan)
    NRT: "东京成田",
    HND: "东京羽田",
    KIX: "大阪",
    NGO: "名古屋",
    FUK: "福冈",
    OKA: "冲绳",

    // 韩国 (South Korea)
    ICN: "首尔",
    PUS: "釜山",

    // 蒙古 (Mongolia)
    ULN: "乌兰巴托",

    // 新加坡 (Singapore)
    SIN: "新加坡",

    // 马来西亚 (Malaysia)
    KUL: "吉隆坡",
    KCH: "古晋",
    JHB: "新山",

    // 印度尼西亚 (Indonesia)
    CGK: "雅加达",
    DPS: "巴厘岛",
    JOG: "日惹",
    MLG: "玛琅",

    // 菲律宾 (Philippines)
    MNL: "马尼拉",
    CEB: "宿务",
    CRK: "安吉利斯",
    CGY: "卡加延德奥罗",

    // 泰国 (Thailand)
    BKK: "曼谷",
    CNX: "清迈",
    URT: "素叻他尼",

    // 越南 (Vietnam)
    SGN: "胡志明市",
    HAN: "河内",
    DAD: "岘港",

    // 老挝 (Laos)
    VTE: "万象",

    // 柬埔寨 (Cambodia)
    PNH: "金边",

    // 缅甸 (Myanmar)
    RGN: "仰光",

    // 文莱 (Brunei)
    BWN: "斯里巴加湾",

    // 印度 (India)
    DEL: "新德里",
    BOM: "孟买",
    MAA: "金奈",
    BLR: "班加罗尔",
    HYD: "海得拉巴",
    CCU: "加尔各答",
    AMD: "艾哈迈达巴德",
    AGR: "阿格拉",
    BBI: "布巴内斯瓦尔",
    CJB: "哥印拜陀",
    COK: "科钦",
    CNN: "坎努尔",
    IXC: "昌迪加尔",
    KNU: "坎普尔",
    NAG: "那格浦尔",
    PAT: "巴特那",
    PNQ: "浦那",

    // 巴基斯坦 (Pakistan)
    KHI: "卡拉奇",
    LHE: "拉合尔",
    ISB: "伊斯兰堡",

    // 孟加拉国 (Bangladesh)
    DAC: "达卡",
    CGP: "吉大港",
    JSR: "杰索尔",

    // 斯里兰卡 (Sri Lanka)
    CMB: "科伦坡",

    // 尼泊尔 (Nepal)
    KTM: "加德满都",

    // 不丹 (Bhutan)
    PBH: "廷布",

    // 马尔代夫 (Maldives)
    MLE: "马累",

    // 哈萨克斯坦 (Kazakhstan)
    NQZ: "阿斯塔纳",
    ALA: "阿拉木图",
    AKX: "阿克托别",

    // 乌兹别克斯坦 (Uzbekistan)
    TAS: "塔什干",

    // 吉尔吉斯斯坦 (Kyrgyzstan)
    FRU: "比什凯克",

    // 塔吉克斯坦 (Tajikistan) - 暂无数据

    // 土库曼斯坦 (Turkmenistan) - 暂无数据

    // 阿富汗 (Afghanistan) - 暂无数据

    // 伊朗 (Iran) - 暂无数据

    // 伊拉克 (Iraq)
    BGW: "巴格达",
    BSR: "巴士拉",
    EBL: "埃尔比勒",
    ISU: "苏莱曼尼亚",
    NJF: "纳杰夫",
    XNH: "纳西里耶",

    // 叙利亚 (Syria) - 暂无数据

    // 黎巴嫩 (Lebanon)
    BEY: "贝鲁特",

    // 约旦 (Jordan)
    AMM: "安曼",

    // 以色列 (Israel)
    TLV: "特拉维夫",
    HFA: "海法",

    // 巴勒斯坦 (Palestine)
    ZDM: "拉马拉",

    // 沙特阿拉伯 (Saudi Arabia)
    JED: "吉达",
    RUH: "利雅得",
    DMM: "达曼",

    // 也门 (Yemen) - 暂无数据

    // 阿曼 (Oman)
    MCT: "马斯喀特",

    // 阿联酋 (UAE)
    DXB: "迪拜",
    AUH: "阿布扎比",

    // 卡塔尔 (Qatar)
    DOH: "多哈",

    // 巴林 (Bahrain)
    BAH: "麦纳麦",

    // 科威特 (Kuwait)
    KWI: "科威特城",

    // 土耳其 (Turkey)
    IST: "伊斯坦布尔",
    ADB: "伊兹密尔",
    ANK: "安卡拉",

    // 塞浦路斯 (Cyprus)
    LCA: "尼科西亚",

    // 高加索地区
    EVN: "埃里温", // 亚美尼亚
    GYD: "巴库", // 阿塞拜疆
    TBS: "第比利斯", // 格鲁吉亚
    LLK: "阿斯塔拉", // 阿塞拜疆

    // ==================== 欧洲 ====================

    // 俄罗斯 (Russia)
    DME: "莫斯科",
    LED: "圣彼得堡",
    KJA: "克拉斯诺亚尔斯克",
    SVX: "叶卡捷琳堡",

    // 乌克兰 (Ukraine)
    KBP: "基辅",

    // 白俄罗斯 (Belarus)
    MSQ: "明斯克",

    // 摩尔多瓦 (Moldova)
    KIV: "基希讷乌",

    // 立陶宛 (Lithuania)
    VNO: "维尔纽斯",

    // 拉脱维亚 (Latvia)
    RIX: "里加",

    // 爱沙尼亚 (Estonia)
    TLL: "塔林",

    // 芬兰 (Finland)
    HEL: "赫尔辛基",

    // 瑞典 (Sweden)
    ARN: "斯德哥尔摩",
    GOT: "哥德堡",

    // 挪威 (Norway)
    OSL: "奥斯陆",

    // 丹麦 (Denmark)
    CPH: "哥本哈根",

    // 冰岛 (Iceland)
    KEF: "雷克雅未克",

    // 爱尔兰 (Ireland)
    DUB: "都柏林",
    ORK: "科克",

    // 英国 (United Kingdom)
    LHR: "伦敦希思罗",
    LGW: "伦敦盖特威克",
    MAN: "曼彻斯特",
    EDI: "爱丁堡",
    BHX: "伯明翰",
    GLA: "格拉斯哥",

    // 荷兰 (Netherlands)
    AMS: "阿姆斯特丹",
    RTM: "鹿特丹",

    // 比利时 (Belgium)
    BRU: "布鲁塞尔",

    // 卢森堡 (Luxembourg)
    LUX: "卢森堡",

    // 法国 (France)
    CDG: "巴黎戴高乐",
    ORY: "巴黎奥利",
    MRS: "马赛",
    LYS: "里昂",
    NCE: "尼斯",
    BOD: "波尔多",
    RUN: "留尼汪",

    // 德国 (Germany)
    FRA: "法兰克福",
    MUC: "慕尼黑",
    DUS: "杜塞尔多夫",
    TXL: "柏林泰格尔",
    BER: "柏林勃兰登堡",
    HAM: "汉堡",
    STR: "斯图加特",
    HAJ: "汉诺威",

    // 瑞士 (Switzerland)
    ZRH: "苏黎世",
    GVA: "日内瓦",

    // 奥地利 (Austria)
    VIE: "维也纳",

    // 波兰 (Poland)
    WAW: "华沙",
    WRO: "弗罗茨瓦夫",
    KRK: "克拉科夫",

    // 捷克 (Czech Republic)
    PRG: "布拉格",

    // 斯洛伐克 (Slovakia)
    BTS: "布拉迪斯拉发",

    // 匈牙利 (Hungary)
    BUD: "布达佩斯",

    // 斯洛文尼亚 (Slovenia)
    LJU: "卢布尔雅那",

    // 克罗地亚 (Croatia)
    ZAG: "萨格勒布",

    // 塞尔维亚 (Serbia)
    BEG: "贝尔格莱德",

    // 波斯尼亚和黑塞哥维那 - 暂无数据

    // 黑山 - 暂无数据

    // 阿尔巴尼亚 (Albania)
    TIA: "地拉那",

    // 北马其顿 (North Macedonia)
    SKP: "斯科普里",

    // 保加利亚 (Bulgaria)
    SOF: "索菲亚",

    // 罗马尼亚 (Romania)
    OTP: "布加勒斯特",
    CLJ: "克卢日",

    // 希腊 (Greece)
    ATH: "雅典",
    SKG: "塞萨洛尼基",
    HER: "伊拉克利翁",

    // 马耳他 (Malta)
    MLA: "瓦莱塔",

    // 意大利 (Italy)
    MXP: "米兰马尔彭萨",
    LIN: "米兰利纳特",
    BGY: "贝加莫",
    FCO: "罗马菲乌米奇诺",
    NAP: "那不勒斯",
    VCE: "威尼斯",
    PMO: "巴勒莫",

    // 西班牙 (Spain)
    MAD: "马德里",
    BCN: "巴塞罗那",
    VLC: "瓦伦西亚",
    AGP: "马拉加",
    SVQ: "塞维利亚",

    // 葡萄牙 (Portugal)
    LIS: "里斯本",
    OPO: "波尔图",

    // 直布罗陀 (Gibraltar) - 暂无

    // ==================== 非洲 ====================

    // 摩洛哥 (Morocco)
    CMN: "卡萨布兰卡",
    RAK: "马拉喀什",

    // 阿尔及利亚 (Algeria)
    ALG: "阿尔及尔",
    ORN: "奥兰",
    CZL: "康斯坦丁",
    AAE: "安纳巴",

    // 突尼斯 (Tunisia)
    TUN: "突尼斯",

    // 利比亚 (Libya) - 暂无

    // 埃及 (Egypt)
    CAI: "开罗",
    ALY: "亚历山大",

    // 苏丹 (Sudan) - 暂无

    // 毛里塔尼亚 (Mauritania) - 暂无

    // 塞内加尔 (Senegal)
    DKR: "达喀尔",
    DSS: "达喀尔新",

    // 冈比亚 (Gambia) - 暂无

    // 马里 (Mali)
    BKO: "巴马科",

    // 布基纳法索 (Burkina Faso)
    OUA: "瓦加杜古",

    // 科特迪瓦 (Ivory Coast)
    ABJ: "阿比让",
    ASK: "亚穆苏克罗",

    // 加纳 (Ghana)
    ACC: "阿克拉",

    // 多哥 (Togo)
    LFW: "洛美",

    // 贝宁 (Benin)
    COO: "科托努",

    // 尼日尔 (Niger) - 暂无

    // 尼日利亚 (Nigeria)
    LOS: "拉各斯",
    ABV: "阿布贾",

    // 喀麦隆 (Cameroon) - 暂无

    // 赤道几内亚 - 暂无

    // 加蓬 (Gabon) - 暂无

    // 刚果布 (Congo) - 暂无

    // 刚果金 (DRC)
    FIH: "金沙萨",

    // 中非 - 暂无

    // 卢旺达 (Rwanda)
    KGL: "基加利",

    // 布隆迪 (Burundi) - 暂无

    // 乌干达 (Uganda)
    EBB: "坎帕拉",

    // 肯尼亚 (Kenya)
    NBO: "内罗毕",
    MBA: "蒙巴萨",

    // 坦桑尼亚 (Tanzania)
    DAR: "达累斯萨拉姆",

    // 莫桑比克 (Mozambique)
    MPM: "马普托",

    // 马拉维 (Malawi)
    LLW: "利隆圭",

    // 赞比亚 (Zambia)
    LUN: "卢萨卡",

    // 津巴布韦 (Zimbabwe)
    HRE: "哈拉雷",

    // 博茨瓦纳 (Botswana)
    GBE: "哈博罗内",

    // 纳米比亚 (Namibia)
    WDH: "温得和克",

    // 南非 (South Africa)
    JNB: "约翰内斯堡",
    CPT: "开普敦",
    DUR: "德班",

    // 马达加斯加 (Madagascar)
    TNR: "塔那那利佛",

    // 毛里求斯 (Mauritius)
    MRU: "路易港",

    // 吉布提 (Djibouti)
    JIB: "吉布提市",

    // 索马里 (Somalia) - 暂无

    // 埃塞俄比亚 (Ethiopia)
    ADD: "亚的斯亚贝巴",

    // 厄立特里亚 (Eritrea) - 暂无

    // 南苏丹 (South Sudan) - 暂无

    // ==================== 北美洲 ====================

    // 加拿大 (Canada)
    YVR: "温哥华",
    YYC: "卡尔加里",
    YEG: "埃德蒙顿",
    YXE: "萨斯卡通",
    YWG: "温尼伯",
    YYZ: "多伦多",
    YOW: "渥太华",
    YUL: "蒙特利尔",
    YHZ: "哈利法克斯",

    // 美国 (USA)
    SEA: "西雅图",
    PDX: "波特兰",
    SFO: "旧金山",
    OAK: "奥克兰",
    SJC: "圣何塞",
    SMF: "萨克拉门托",
    LAX: "洛杉矶",
    SAN: "圣迭戈",
    LAS: "拉斯维加斯",
    PHX: "菲尼克斯",
    SLC: "盐湖城",
    DEN: "丹佛",
    ABQ: "阿尔伯克基",
    TUS: "图森",
    OKC: "俄克拉荷马城",
    DFW: "达拉斯",
    IAH: "休斯顿",
    AUS: "奥斯汀",
    SAT: "圣安东尼奥",
    MCI: "堪萨斯城",
    STL: "圣路易斯",
    MEM: "孟菲斯",
    MSY: "新奥尔良",
    BNA: "纳什维尔",
    ATL: "亚特兰大",
    JAX: "杰克逊维尔",
    MCO: "奥兰多",
    TPA: "坦帕",
    MIA: "迈阿密",
    CLT: "夏洛特",
    RDU: "罗利",
    IAD: "华盛顿杜勒斯",
    DCA: "华盛顿里根",
    BWI: "巴尔的摩",
    RIC: "里士满",
    ORF: "诺福克",
    PHL: "费城",
    JFK: "纽约肯尼迪",
    EWR: "纽瓦克",
    LGA: "纽约拉瓜迪亚",
    BOS: "波士顿",
    BUF: "布法罗",
    PIT: "匹兹堡",
    CLE: "克利夫兰",
    CVG: "辛辛那提",
    CMH: "哥伦布",
    IND: "印第安纳波利斯",
    DTW: "底特律",
    MKE: "密尔沃基",
    ORD: "芝加哥",
    MSP: "明尼阿波利斯",
    OMA: "奥马哈",
    FSD: "苏福尔斯",
    ANC: "安克雷奇",
    HNL: "檀香山",

    // 墨西哥 (Mexico)
    MEX: "墨西哥城",
    GDL: "瓜达拉哈拉",
    MTY: "蒙特雷",
    QRO: "克雷塔罗",

    // 危地马拉 (Guatemala)
    GUA: "危地马拉城",

    // 伯利兹 (Belize) - 暂无

    // 萨尔瓦多 (El Salvador) - 暂无

    // 洪都拉斯 (Honduras)
    SAP: "圣佩德罗苏拉",
    TGU: "特古西加尔巴",

    // 尼加拉瓜 (Nicaragua) - 暂无

    // 哥斯达黎加 (Costa Rica)
    SJO: "圣何塞",

    // 巴拿马 (Panama)
    PTY: "巴拿马城",

    // 加勒比地区
    GCM: "开曼群岛", // 开曼
    NAS: "拿骚", // 巴哈马
    KIN: "金斯敦", // 牙买加
    PAP: "太子港", // 海地
    SDQ: "圣多明各", // 多米尼加
    STI: "圣地亚哥", // 多米尼加
    SJU: "圣胡安", // 波多黎各
    BGI: "布里奇敦", // 巴巴多斯
    GND: "圣乔治", // 格林纳达
    MBJ: "蒙特哥贝", // 牙买加
    SXM: "圣马丁", // 荷属圣马丁
    UVF: "卡斯特里", // 圣卢西亚

    // ==================== 南美洲 ====================

    // 哥伦比亚 (Colombia)
    BOG: "波哥大",
    MDE: "麦德林",
    CLO: "卡利",
    BAQ: "巴兰基亚",

    // 委内瑞拉 (Venezuela)
    CCS: "加拉加斯",

    // 圭亚那 (Guyana)
    GEO: "乔治敦",

    // 苏里南 (Suriname)
    PBM: "帕拉马里博",

    // 厄瓜多尔 (Ecuador)
    UIO: "基多",
    GYE: "瓜亚基尔",

    // 秘鲁 (Peru)
    LIM: "利马",
    CUZ: "库斯科",

    // 玻利维亚 (Bolivia)
    LPB: "拉巴斯",
    VVI: "圣克鲁斯",

    // 巴西 (Brazil)
    GRU: "圣保罗",
    CGH: "圣保罗孔戈尼亚斯",
    VCP: "坎皮纳斯",
    GIG: "里约热内卢",
    BSB: "巴西利亚",
    CNF: "贝洛奥里藏特",
    CWB: "库里提巴",
    POA: "阿雷格里港",
    FLN: "弗洛里亚诺波利斯",
    SSA: "萨尔瓦多",
    REC: "累西腓",
    FOR: "福塔莱萨",
    BEL: "贝伦",
    MAO: "马瑙斯",
    GOI: "戈亚尼亚",
    UDI: "乌贝兰迪亚",
    CGB: "库亚巴",
    PMW: "帕尔马斯",
    JDO: "北茹阿泽鲁",
    RAO: "里贝朗普雷图",
    SJK: "圣若泽杜斯坎普斯",
    SJP: "圣若泽杜里奥普雷图",
    SOD: "索罗卡巴",
    ITJ: "伊塔雅伊",
    JOI: "若因维利",
    NVT: "廷博",
    CAW: "坎波斯",
    ARU: "阿拉萨图巴",
    CFC: "卡萨多尔",
    BNU: "布鲁梅瑙",
    XAP: "沙佩科",
    QWJ: "阿美利卡纳",

    // 巴拉圭 (Paraguay)
    ASU: "亚松森",

    // 乌拉圭 (Uruguay)
    MVD: "蒙得维的亚",

    // 阿根廷 (Argentina)
    EZE: "布宜诺斯艾利斯",
    AEP: "布宜诺斯艾利斯国内",
    COR: "科尔多瓦",
    NQN: "内乌肯",

    // 智利 (Chile)
    SCL: "圣地亚哥",
    ARI: "阿里卡",
    CCP: "康塞普西翁",

    // 福克兰群岛 - 暂无

    // ==================== 大洋洲 ====================

    // 澳大利亚 (Australia)
    PER: "珀斯",
    ADL: "阿德莱德",
    MEL: "墨尔本",
    SYD: "悉尼",
    CBR: "堪培拉",
    BNE: "布里斯班",
    HBA: "霍巴特",

    // 新西兰 (New Zealand)
    AKL: "奥克兰",
    WLG: "惠灵顿",
    CHC: "基督城",

    // 巴布亚新几内亚 (Papua New Guinea) - 暂无

    // 斐济 (Fiji)
    SUV: "苏瓦",
    NAN: "纳迪",

    // 新喀里多尼亚 (New Caledonia)
    NOU: "努美阿",

    // 法属波利尼西亚 (French Polynesia)
    PPT: "塔希提",

    // 关岛 (Guam)
    GUM: "哈加特纳",

    // 其他太平洋岛屿 - 暂无

    // ==================== 其他/备用 ====================
    LOCAL: "本地网络",
    "N/A": "未知",
  };

  /* =========================
           🎨 UI风格
  ========================= */

  GM_addStyle(`
    #cfpp{
      position:fixed;
      left:18px;
      bottom:${IS_MOBILE ? 92 : 130}px;

      width:${ICON_SIZE}px;
      height:${ICON_SIZE}px;

      border-radius:50%;

      background:
        linear-gradient(
          135deg,
          #ff9a3c,
          #f38020
        );

      display:flex;
      align-items:center;
      justify-content:center;

      z-index:999999;

      cursor:grab;
      user-select:none;
      -webkit-user-select:none;

      box-shadow:
        0 8px 22px rgba(0,0,0,.28);

      opacity: .3;
      transition:
        transform .18s ease,
        box-shadow .18s ease,
        opacity .18s ease;

      animation:cfpop .42s ease;

      touch-action:none;
      -webkit-tap-highlight-color: transparent;
    }

    @media (hover: hover) {
      #cfpp:hover{
        transform:scale(1.08);
        opacity: 1;
        box-shadow:
          0 14px 30px rgba(0,0,0,.32);
      }
    }

    #cfpp.cfpp-panel-open{
      opacity: 1;
    }

    #cfpp.dragging{
      cursor:grabbing;
      transition:none;
      opacity:.92;
      transform:scale(1.06);
    }

    #cfpp svg{
      width:${IS_MOBILE ? 24 : 22}px;
      height:${IS_MOBILE ? 24 : 22}px;

      fill:#fff;
      pointer-events:none;
    }

    #cfpanel{
      position:fixed;

      display:none;

      min-width:${IS_MOBILE ? 230 : 210}px;
      max-width:min(${IS_MOBILE ? 92 : 340}px,calc(100vw - 16px));

      padding:${IS_MOBILE ? 16 : 14}px ${IS_MOBILE ? 16 : 15}px;

      border-radius:${IS_MOBILE ? 18 : 16}px;

      background:
        rgba(255,255,255,.84);

      backdrop-filter:blur(18px);
      -webkit-backdrop-filter:blur(18px);

      border:
        1px solid rgba(255,255,255,.45);

      box-shadow:
        0 12px 38px rgba(0,0,0,.22);

      font-size:${IS_MOBILE ? 14 : 13}px;
      line-height:1.75;

      font-family:
        Inter,
        ui-monospace,
        SFMono-Regular,
        Menlo,
        Monaco,
        Consolas;

      color:#111827;

      z-index:999998;

      user-select:none;
      -webkit-user-select:none;

      overflow-wrap:break-word;
      word-break:break-word;

      box-sizing:border-box;

      opacity:0;
      transform:translateY(4px) scale(.98);

      transition:
        opacity .18s ease,
        transform .18s ease;

      -webkit-tap-highlight-color: transparent;
    }

    #cfpanel.show{
      display:block;
      opacity:1;
      transform:translateY(0) scale(1);
    }

    .cf-title{
      display:flex;
      align-items:center;
      justify-content:center;
      gap:7px;

      margin-bottom:12px;

      color:#f38020;

      font-weight:700;
      font-size:${IS_MOBILE ? 15 : 14}px;

      white-space:nowrap;
      text-align:center;
    }

    .cf-row{
      display:flex;
      align-items:center;
      justify-content:center;
      gap:4px;

      margin:7px 0;

      flex-wrap:wrap;

      text-align:center;
    }

    .cf-ok{
      color:#18a058;
      font-weight:700;
    }

    .cf-no{
      color:#ef4444;
      font-weight:700;
    }

    .cf-warn{
      color:#f59e0b;
      font-weight:700;
    }

    .cf-ip{
      cursor:pointer;

      display:inline;
      padding:0;
      margin:0;

      background:none;
      border:none;
      border-radius:0;

      transition: font-weight .12s ease;
      user-select: none;
      -webkit-user-select: none;
    }

    .cf-ip:hover{
      font-weight:600;
    }

    .cf-copy-ok{
      color:#18a058;
      font-weight:700;
    }

    .cf-loading{
      display:inline-flex;
      align-items:center;
      gap:8px;
    }

    .cf-loading::after{
      content:"";

      width:12px;
      height:12px;

      border-radius:50%;

      border:
        2px solid rgba(243,128,32,.22);

      border-top-color:#f38020;

      animation:
        cfspin .75s linear infinite;
    }

    @keyframes cfspin{
      to{
        transform:rotate(360deg);
      }
    }

    @keyframes cfpop{
      0%{
        transform:scale(.5);
        opacity:0;
      }

      100%{
        transform:scale(1);
        opacity:1;
      }
    }
  `);

  /* =========================
            辅助函数
  ========================= */

  function el(tag) {
    return document.createElement(tag);
  }

  function cacheGet() {
    try {
      let d = sessionStorage.getItem(CACHE_KEY);

      if (!d) return null;

      d = JSON.parse(d);

      if (Date.now() - d.t > CACHE_TTL) {
        return null;
      }

      return d.v;
    } catch {
      return null;
    }
  }

  function cacheSet(v) {
    sessionStorage.setItem(
      CACHE_KEY,
      JSON.stringify({
        t: Date.now(),
        v,
      }),
    );
  }

  async function copy(text) {
    try {
      await navigator.clipboard.writeText(text);
      return true;
    } catch {
      try {
        const input = document.createElement("textarea");

        input.value = text;

        document.body.appendChild(input);

        input.select();

        document.execCommand("copy");

        input.remove();

        return true;
      } catch {
        return false;
      }
    }
  }

  async function fetchTrace() {
    const timeout = 3000;
    const url = location.origin + "/cdn-cgi/trace";

    if (IS_MOBILE) {
      const controller = new AbortController();
      const timer = setTimeout(() => {
        controller.abort();
      }, timeout);
      try {
        const r = await fetch(url, {
          method: "GET",
          cache: "no-store",
          signal: controller.signal,
        });

        if (!r.ok) {
          return "";
        }
        return await r.text();
      } catch {
        return "";
      } finally {
        clearTimeout(timer);
      }
    }
    return new Promise((resolve) => {
      GM_xmlhttpRequest({
        method: "GET",
        url,
        timeout,

        headers: {
          "cache-control": "no-cache",
        },

        onload: (r) => resolve(r.responseText || ""),
        ontimeout: () => resolve(""),
        onerror: () => resolve(""),
      });
    });
  }

  /* =========================
             嗅探
  ========================= */

  async function detect() {
    const cached = cacheGet();

    if (cached) return cached;

    const data = {
      ip: "未知",
      colo: "未知",
      cf: false,
      trace: false,
    };

    try {
      const r = await fetch(location.href, {
        method: "HEAD",
        cache: "no-store",
      });

      const server = r.headers.get("server") || "";
      const cfRay = r.headers.get("cf-ray") || "";

      if (server.toLowerCase().includes("cloudflare") || cfRay) {
        data.cf = true;
      }
    } catch {}

    try {
      const text = await fetchTrace();

      if (text.includes("colo=")) {
        data.trace = true;

        const co = (text.match(/colo=([A-Z]+)/) || [])[1];

        data.colo = coloMap[co] ? `${coloMap[co]} (${co})` : co || "未知";

        const ip = (text.match(/ip=([^\n]+)/) || [])[1];

        if (ip) {
          data.ip = ip;
        }
      }
    } catch {}

    cacheSet(data);

    return data;
  }

  /* =========================
             状态
  ========================= */

  function getStatus(d) {
    if (!d.cf) {
      return {
        text: "无 Cloudflare",
        dot: "🔴",
        color: "cf-no",
      };
    }

    if (d.cf && d.trace) {
      return {
        text: "Cloudflare 正常",
        dot: "🟢",
        color: "cf-ok",
      };
    }

    return {
      text: "Cloudflare 已启用",
      dot: "🟡",
      color: "cf-warn",
    };
  }

  /* =========================
            面板位置
  ========================= */

  function updatePanelPosition(icon, panel) {
    requestAnimationFrame(() => {
      const iconRect = icon.getBoundingClientRect();

      const panelRect = panel.getBoundingClientRect();

      const winW = window.innerWidth;
      const winH = window.innerHeight;

      const centerX = iconRect.left + iconRect.width / 2;
      const centerY = iconRect.top + iconRect.height / 2;

      const isLeft = centerX < winW / 2;
      const isTop = centerY < winH / 2;

      let panelX;
      let panelY;

      if (IS_MOBILE) {
        panelX = Math.max(
          8,
          Math.min(centerX - panelRect.width / 2, winW - panelRect.width - 8),
        );

        if (isTop) {
          panelY = iconRect.bottom + PANEL_GAP;
        } else {
          panelY = iconRect.top - panelRect.height - PANEL_GAP;
        }
      } else {
        if (isLeft) {
          panelX = iconRect.left + ICON_SIZE + PANEL_GAP;
        } else {
          panelX = iconRect.left - panelRect.width - PANEL_GAP;
        }

        if (isTop) {
          panelY = iconRect.top;
        } else {
          panelY = iconRect.top + ICON_SIZE - panelRect.height;
        }
      }

      panelX = Math.max(8, Math.min(panelX, winW - panelRect.width - 8));

      panelY = Math.max(8, Math.min(panelY, winH - panelRect.height - 8));

      panel.style.left = panelX + "px";
      panel.style.top = panelY + "px";
      panel.style.bottom = "auto";
    });
  }

  /* =========================
             拖拽
  ========================= */

  function enableDrag(panel, icon) {
    let saved = null;

    try {
      saved = JSON.parse(localStorage.getItem(POS_KEY) || "null");
    } catch {}

    let currentX = 18;
    let currentY = window.innerHeight - 180;

    if (saved) {
      currentX = saved.x;
      currentY = saved.y;
    }

    function updateIconPosition(x, y) {
      const maxX = window.innerWidth - ICON_SIZE;
      const maxY = window.innerHeight - ICON_SIZE;

      x = Math.max(0, Math.min(x, maxX));
      y = Math.max(0, Math.min(y, maxY));

      currentX = x;
      currentY = y;

      icon.style.left = x + "px";
      icon.style.top = y + "px";
      icon.style.bottom = "auto";

      updatePanelPosition(icon, panel);

      localStorage.setItem(POS_KEY, JSON.stringify({ x, y }));
    }

    updateIconPosition(currentX, currentY);

    let dragging = false;
    let moved = false;

    let offsetX = 0;
    let offsetY = 0;

    let longPressTimer = null;

    function start(x, y) {
      dragging = true;
      moved = false;

      icon.classList.add("dragging");

      offsetX = x - currentX;
      offsetY = y - currentY;
    }

    function move(x, y) {
      if (!dragging) return;

      moved = true;

      updateIconPosition(x - offsetX, y - offsetY);
    }

    function end() {
      dragging = false;

      icon.classList.remove("dragging");

      clearTimeout(longPressTimer);
    }

    /* =========================
               电脑端
    ========================= */

    icon.addEventListener("mousedown", (e) => {
      start(e.clientX, e.clientY);

      e.preventDefault();
    });

    document.addEventListener("mousemove", (e) => {
      move(e.clientX, e.clientY);
    });

    document.addEventListener("mouseup", end);

    /* =========================
               移动端
    ========================= */

    icon.addEventListener(
      "touchstart",
      (e) => {
        const touch = e.touches[0];

        moved = false;

        longPressTimer = setTimeout(() => {
          start(touch.clientX, touch.clientY);
        }, LONG_PRESS_TIME);
      },
      { passive: true },
    );

    document.addEventListener(
      "touchmove",
      (e) => {
        const touch = e.touches[0];

        if (!dragging) {
          if (longPressTimer) {
            clearTimeout(longPressTimer);
          }

          return;
        }

        move(touch.clientX, touch.clientY);

        e.preventDefault();
      },
      { passive: false },
    );

    document.addEventListener("touchend", end);

    window.addEventListener("resize", () => {
      updateIconPosition(currentX, currentY);
    });

    return () => moved;
  }

  /* =========================
            初始化
  ========================= */

  function init() {
    const icon = el("div");
    icon.id = "cfpp";

    icon.innerHTML = `
      <svg viewBox="0 0 24 24">
        <path d="M17 10a5 5 0 0 0-10 1 4 4 0 0 0 0 8h10a3 3 0 0 0 0-6z"/>
      </svg>
    `;

    const panel = el("div");
    panel.id = "cfpanel";

    document.body.appendChild(icon);
    document.body.appendChild(panel);

    const hasMoved = enableDrag(panel, icon);

    let opened = false;
    let loading = false;

    async function togglePanel() {
      if (loading) return;

      opened = !opened;

      if (!opened) {
        icon.classList.remove("cfpp-panel-open");
        icon.blur();
        panel.classList.remove("show");

        setTimeout(() => {
          if (!opened) {
            panel.style.display = "none";
          }
        }, 180);

        return;
      }

      icon.classList.add("cfpp-panel-open");

      panel.style.display = "block";

      requestAnimationFrame(() => {
        panel.classList.add("show");
      });

      panel.innerHTML = `
        <div class="cf-title">
          ⏳ Cloudflare Detector
        </div>

        <div class="cf-row">
          <span class="cf-loading cf-warn">
            正在检测节点
          </span>
        </div>
      `;

      updatePanelPosition(icon, panel);

      loading = true;

      const d = await detect();

      loading = false;

      const st = getStatus(d);

      let info = "";

      if (d.cf) {
        if (d.trace) {
          info = `
            <div class="cf-row">
              节点: ${d.colo}
            </div>

            <div class="cf-row">
              <span
                class="cf-ip"
                id="cf-copy-ip"
              >
                IP: ${d.ip}
              </span>
            </div>
          `;
        } else {
          info = `
            <div class="cf-row">
              Trace 被禁用
            </div>
          `;
        }
      }

      panel.innerHTML = `
        <div class="cf-title">
          ${st.dot} Cloudflare Detector
        </div>

        <div class="cf-row">
          状态:
          <span class="${st.color}">
            ${st.text}
          </span>
        </div>

        ${info}
      `;

      updatePanelPosition(icon, panel);

      if (d.cf && d.trace) {
        const ipEl = document.getElementById("cf-copy-ip");

        ipEl?.addEventListener("click", async () => {
          const ok = await copy(d.ip);

          if (!ok) return;

          ipEl.innerHTML = '<span class="cf-copy-ok">IP: 已复制 ✓</span>';

          setTimeout(() => {
            ipEl.innerText = `IP: ${d.ip}`;
          }, 1200);
        });
      }
    }

    icon.addEventListener("click", () => {
      if (hasMoved()) return;

      togglePanel();
    });

    document.addEventListener("click", (e) => {
      if (opened && !panel.contains(e.target) && !icon.contains(e.target)) {
        opened = false;

        icon.classList.remove("cfpp-panel-open");

        panel.classList.remove("show");

        setTimeout(() => {
          if (!opened) {
            panel.style.display = "none";
          }
        }, 180);
      }
    });
  }

  init();
})();