Cloudflare Helper

Cloudflare 增强工具:Pages清理 + 环境导出 + DNS管理 + R2文件管理 + WAF/Cache

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

You will need to install an extension such as Tampermonkey to install this script.

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name         Cloudflare Helper
// @namespace    http://tampermonkey.net/
// @version      1.0
// @license      GPL
// @description  Cloudflare 增强工具:Pages清理 + 环境导出 + DNS管理 + R2文件管理 + WAF/Cache
// @author       Atom
// @match        https://dash.cloudflare.com/*
// @connect      api.cloudflare.com
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_setClipboard
// @grant        GM_xmlhttpRequest
// ==/UserScript==

(function () {
    'use strict';

    // ================= 样式定义 (暗黑高科技风) =================
    const STYLES = `
        #cf-tools-btn {
            position: fixed; bottom: 20px; right: 20px; z-index: 10000;
            background: linear-gradient(135deg, #00C9FF 0%, #92FE9D 100%);
            color: #000; border: none; border-radius: 50%; width: 56px; height: 56px;
            box-shadow: 0 4px 15px rgba(0,0,0,0.4); cursor: pointer;
            font-size: 26px; display: flex; align-items: center; justify-content: center;
            transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
        }
        #cf-tools-btn:hover { transform: scale(1.1) rotate(180deg); }
        #cf-tools-panel {
            position: fixed; bottom: 90px; right: 20px; width: 600px;
            background: #1a1a1a; color: #e0e0e0; border: 1px solid #333; border-radius: 12px;
            z-index: 10000; padding: 0; box-shadow: 0 20px 50px rgba(0,0,0,0.6);
            display: none; font-family: 'Inter', system-ui, sans-serif; overflow: hidden;
            backdrop-filter: blur(10px);
        }
        .cf-header {
            background: #252525; padding: 12px 20px; border-bottom: 1px solid #333;
            display: flex; justify-content: space-between; align-items: center; font-weight: 600;
        }
        .cf-tabs { display: flex; background: #202020; border-bottom: 1px solid #333; overflow-x: auto; }
        .cf-tab {
            flex: 1; text-align: center; padding: 12px 0; cursor: pointer; color: #888; font-size: 13px;
            transition: 0.2s; border-right: 1px solid #2a2a2a; white-space: nowrap; min-width: 80px;
        }
        .cf-tab:hover { background: #2a2a2a; color: #ddd; }
        .cf-tab.active { background: #2a2a2a; color: #00C9FF; border-bottom: 2px solid #00C9FF; font-weight: bold; }
        .cf-content { padding: 20px; min-height: 350px; max-height: 600px; display: flex; flex-direction: column; }
        .cf-section { display: none; flex: 1; overflow-y: hidden; flex-direction: column; }
        .cf-section.active { display: flex; animation: fadeIn 0.3s; }

        .cf-input-group { margin-bottom: 15px; }
        .cf-input-group label { display: block; font-size: 12px; color: #888; margin-bottom: 6px; font-weight: 500; }
        .cf-input-group input, .cf-input-group textarea, .cf-input-group select {
            width: 100%; box-sizing: border-box; padding: 10px;
            background: #2a2a2a; border: 1px solid #444; color: white; border-radius: 6px;
            font-family: monospace; font-size: 13px; outline: none; transition: border 0.2s;
        }
        .cf-input-group input:focus { border-color: #00C9FF; }

        .cf-log-area {
            min-height: 80px; max-height: 150px; overflow-y: auto; background: #111; border: 1px solid #333;
            padding: 10px; font-size: 12px; margin-top: 15px; white-space: pre-wrap; color: #bbb; border-radius: 6px;
            flex-shrink: 0;
        }
        .cf-btn {
            padding: 8px 16px; border: none; border-radius: 6px; cursor: pointer; font-weight: 600; font-size: 13px;
            transition: 0.2s; width: auto; display: inline-flex; justify-content: center; align-items: center;
        }
        .cf-btn-primary { background: #00C9FF; color: #000; }
        .cf-btn-primary:hover { background: #0099CC; }
        .cf-btn-stop { background: #ef4444; color: white; animation: pulse 2s infinite; }
        .cf-btn-success { background: #10b981; color: white; }

        /* R2 列表样式 */
        .cf-r2-browser { flex: 1; display: flex; flex-direction: column; overflow: hidden; border: 1px solid #333; border-radius: 6px; margin-top: 10px; }
        .cf-r2-toolbar { padding: 8px; background: #252525; border-bottom: 1px solid #333; display: flex; gap: 10px; align-items: center; }
        .cf-r2-list { flex: 1; overflow-y: auto; background: #1a1a1a; }
        .cf-r2-item {
            display: flex; align-items: center; padding: 8px 10px; border-bottom: 1px solid #333; font-size: 12px; cursor: pointer;
            transition: background 0.1s;
        }
        .cf-r2-item:hover { background: #2a2a2a; }
        .cf-r2-item input { margin-right: 10px; }
        .cf-r2-name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: #ddd; }
        .cf-r2-meta { color: #666; width: 120px; text-align: right; margin-left: 10px; font-size: 11px; }
        .cf-copy-btn {
            background: none; border: none; cursor: pointer; font-size: 14px; opacity: 0.5; transition: 0.2s; padding: 0 5px;
        }
        .cf-copy-btn:hover { opacity: 1; transform: scale(1.2); }

        @keyframes fadeIn { from { opacity: 0; transform: translateY(5px); } to { opacity: 1; transform: translateY(0); } }
        @keyframes pulse { 0% { opacity: 1; } 50% { opacity: 0.7; } 100% { opacity: 1; } }
        @keyframes slideIn { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } }

        /* Toast */
        .cf-toast {
            position: fixed; top: 20px; right: 20px; z-index: 20000;
            padding: 12px 20px; border-radius: 8px; font-size: 13px; color: white;
            box-shadow: 0 4px 15px rgba(0,0,0,0.4); animation: slideIn 0.3s;
            display: flex; align-items: center; gap: 10px; min-width: 250px;
            backdrop-filter: blur(10px); border: 1px solid rgba(255,255,255,0.1);
        }
        .cf-toast-success { background: rgba(16, 185, 129, 0.9); }
        .cf-toast-error { background: rgba(239, 68, 68, 0.9); }
        .cf-toast-info { background: rgba(59, 130, 246, 0.9); }
        .cf-toast-warn { background: rgba(245, 158, 11, 0.9); }

        /* Confirm Modal */
        .cf-modal-overlay {
            position: fixed; top: 0; left: 0; width: 100%; height: 100%;
            background: rgba(0,0,0,0.6); z-index: 20000;
            display: flex; align-items: center; justify-content: center;
            backdrop-filter: blur(3px); animation: fadeIn 0.2s;
        }
        .cf-modal {
            background: #1a1a1a; border: 1px solid #333; width: 320px;
            border-radius: 12px; padding: 20px; box-shadow: 0 20px 50px rgba(0,0,0,0.7);
            text-align: center; color: #e0e0e0; font-family: 'Inter', system-ui, sans-serif;
        }
        .cf-modal h3 { margin: 0 0 10px 0; font-size: 16px; color: white; }
        .cf-modal p { margin: 0 0 20px 0; font-size: 13px; color: #aaa; line-height: 1.5; }
        .cf-modal-actions { display: flex; gap: 10px; justify-content: center; }
        .cf-modal-btn {
            flex: 1; padding: 10px; border-radius: 6px; border: none; cursor: pointer;
            font-weight: 600; font-size: 13px; transition: 0.2s;
        }
        .cf-modal-confirm { background: #ef4444; color: white; }
        .cf-modal-confirm:hover { background: #dc2626; }
        .cf-modal-cancel { background: #333; color: #ccc; }
        .cf-modal-cancel:hover { background: #444; }
    `;

    const style = document.createElement('style');
    style.innerHTML = STYLES;
    document.head.appendChild(style);

    // ================= 核心工具函数 =================
    function showToast(msg, type = 'info') {
        const toast = document.createElement('div');
        toast.className = `cf-toast cf-toast-${type}`;
        toast.innerHTML = `<span>${type === 'success' ? '✅' : type === 'error' ? '❌' : type === 'warn' ? '⚠️' : 'ℹ️'}</span><span>${msg}</span>`;
        document.body.appendChild(toast);
        setTimeout(() => {
            toast.style.opacity = '0';
            toast.style.transform = 'translateX(100%)';
            setTimeout(() => toast.remove(), 300);
        }, 3000);
    }

    function showConfirm(title, msg, onConfirm) {
        const overlay = document.createElement('div');
        overlay.className = 'cf-modal-overlay';
        overlay.innerHTML = `
            <div class="cf-modal">
                <h3>${title}</h3>
                <p>${msg}</p>
                <div class="cf-modal-actions">
                    <button class="cf-modal-btn cf-modal-cancel">取消</button>
                    <button class="cf-modal-btn cf-modal-confirm">确定</button>
                </div>
            </div>
        `;
        document.body.appendChild(overlay);

        const close = () => { overlay.style.opacity = '0'; setTimeout(() => overlay.remove(), 200); };
        overlay.querySelector('.cf-modal-cancel').onclick = close;
        overlay.querySelector('.cf-modal-confirm').onclick = () => {
            close();
            onConfirm();
        };
        overlay.onclick = (e) => { if (e.target === overlay) close(); };
    }

    function gmFetch(method, url, headers, body = null) {
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: method, url: url, headers: headers,
                data: body ? JSON.stringify(body) : null,
                onload: (res) => {
                    let data = null;
                    try { data = JSON.parse(res.responseText); } catch (e) { }
                    resolve({ ok: res.status >= 200 && res.status < 300, status: res.status, data: data });
                },
                onerror: () => reject(new Error("Network Error"))
            });
        });
    }

    function formatBytes(bytes, decimals = 2) {
        if (!+bytes) return '0 B';
        const k = 1024;
        const dm = decimals < 0 ? 0 : decimals;
        const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
        const i = Math.floor(Math.log(bytes) / Math.log(k));
        return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`;
    }

    function log(el, msg, type = 'info') {
        const colors = { error: '#ef4444', success: '#10b981', warn: '#f59e0b', info: '#888' };
        const div = document.createElement('div');
        div.style.color = colors[type];
        div.textContent = `[${new Date().toLocaleTimeString()}] ${msg}`;
        el.appendChild(div);
        el.scrollTop = el.scrollHeight;
    }

    // ================= UI构建 =================
    const btn = document.createElement('div');
    btn.id = 'cf-tools-btn';
    btn.innerHTML = '🪣';
    document.body.appendChild(btn);

    const panel = document.createElement('div');
    panel.id = 'cf-tools-panel';
    panel.innerHTML = `
        <div class="cf-header">
            <span>🚀 Cloudflare Helper 1.0</span>
            <span id="cf-close" style="cursor:pointer; opacity:0.7;">✕</span>
        </div>
        <div class="cf-tabs">
            <div class="cf-tab active" data-tab="r2">R2 管理</div>
            <div class="cf-tab" data-tab="pages">Pages清理</div>
            <div class="cf-tab" data-tab="env">环境导出</div>
        </div>

        <div class="cf-content">
            <div class="cf-input-group" style="margin-bottom:15px;">
                <input type="password" id="cf-api-token" placeholder="API Token (需 R2:Edit, Pages:Edit 权限)">
            </div>

            <!-- R2 模块 -->
            <div id="tab-r2" class="cf-section active">
                <div class="cf-input-group" style="display:flex; gap:10px; margin-bottom:10px;">
                    <button id="btn-r2-load-buckets" class="cf-btn cf-btn-primary" style="width:auto;">刷新 Bucket</button>
                    <select id="cf-r2-buckets" style="flex:1;"><option>请加载...</option></select>
                </div>
                <div class="cf-input-group" style="display:flex; gap:5px;">
                    <input type="text" id="cf-r2-domain" placeholder="R2 访问域名 (自动获取或手动输入)" style="font-size:12px; padding:6px; flex:1;">
                    <button id="btn-r2-detect-domain" class="cf-btn" style="padding:4px 8px; font-size:11px; background:#333;">🔍 检测</button>
                </div>
                <div class="cf-r2-browser">
                    <div class="cf-r2-toolbar">
                        <label style="display:flex; align-items:center; color:#ccc; font-size:12px; cursor:pointer; margin-right:10px;">
                            <input type="checkbox" id="cf-r2-select-all" style="margin-right:5px;"> 全选
                        </label>
                        <button id="btn-r2-load-files" class="cf-btn cf-btn-success" style="padding:4px 10px;">📂 加载</button>
                        <button id="btn-r2-delete" class="cf-btn cf-btn-stop" style="display:none; padding:4px 10px;">🗑️ 删除</button>
                        <span id="cf-r2-count" style="margin-left:auto; color:#666; font-size:11px;">0 items</span>
                    </div>
                    <div id="cf-r2-list" class="cf-r2-list">
                        <div style="padding:20px; text-align:center; color:#666;">请选择 Bucket 并加载</div>
                    </div>
                    <div class="cf-r2-toolbar" style="border-top:1px solid #333; border-bottom:none;">
                       <button id="btn-r2-more" class="cf-btn" style="width:100%; background:#2a2a2a; color:#ccc; display:none;">加载更多...</button>
                    </div>
                </div>
                <div id="log-r2" class="cf-log-area" style="height:80px; margin-top:5px;">R2 就绪</div>
            </div>

            <!-- Pages 模块 -->
            <div id="tab-pages" class="cf-section">
                <div class="cf-input-group" style="display:flex; gap:10px; margin-bottom:10px;">
                    <input type="text" id="cf-pages-info" readonly style="color:#888; flex:1;" value="自动识别项目 (需在项目详情页)">
                    <button id="btn-pages-refresh" class="cf-btn cf-btn-primary" style="padding:4px 12px;">🔄 刷新</button>
                </div>
                <button id="btn-pages-run" class="cf-btn cf-btn-primary">开始清理历史部署</button>
                <div id="log-pages" class="cf-log-area">等待开始...</div>
            </div>

            <!-- Env 模块 -->
            <div id="tab-env" class="cf-section">
                <div class="cf-input-group" style="display:flex; gap:10px; margin-bottom:10px;">
                    <input type="text" id="cf-env-info" readonly style="color:#888; flex:1;" value="自动识别项目 (需在项目详情页)">
                    <button id="btn-env-refresh" class="cf-btn cf-btn-primary" style="padding:4px 12px;">🔄 刷新</button>
                </div>
                <div class="cf-input-group">
                    <select id="cf-env-type" style="margin-bottom:10px;">
                        <option value="production">Production (生产环境)</option>
                        <option value="preview">Preview (预览环境)</option>
                    </select>
                    <button id="btn-env-get" class="cf-btn cf-btn-success" style="width:100%">获取 .env 并复制</button>
                </div>
                <textarea id="cf-env-output" style="flex:1; background:#111; color:#0f0; border:1px solid #333; padding:10px; font-family:monospace; font-size:11px;" placeholder="结果显示区域..."></textarea>
            </div>
        </div>
    `;
    document.body.appendChild(panel);

    // ================= 逻辑 =================
    // Tab
    panel.querySelectorAll('.cf-tab').forEach(t => {
        t.onclick = function () {
            panel.querySelectorAll('.cf-tab').forEach(x => x.classList.remove('active'));
            panel.querySelectorAll('.cf-section').forEach(x => x.classList.remove('active'));
            this.classList.add('active');
            document.getElementById(`tab-${this.dataset.tab}`).classList.add('active');
        };
    });

    const savedToken = GM_getValue('cf_api_token', '');
    if (savedToken) document.getElementById('cf-api-token').value = savedToken;
    document.getElementById('cf-api-token').onchange = function () { GM_setValue('cf_api_token', this.value.trim()); };

    btn.onclick = () => { panel.style.display = panel.style.display === 'block' ? 'none' : 'block'; detectContext(); };
    document.getElementById('cf-close').onclick = () => panel.style.display = 'none';

    function getHeaders() {
        const t = document.getElementById('cf-api-token').value;
        if (!t) { showToast("请先填写 API Token", "error"); return null; }
        return { "Authorization": "Bearer " + t, "Content-Type": "application/json" };
    }

    // 上下文感知
    let currentAccountId = '';
    function detectContext() {
        const url = window.location.href;
        const parts = url.split('/');

        // Account ID
        const dashIdx = parts.indexOf('dash.cloudflare.com');
        if (dashIdx > -1) {
            currentAccountId = parts[dashIdx + 1];
        }

        // Pages Info
        if (url.includes('pages')) {
            const viewIdx = parts.indexOf('view');
            if (viewIdx > -1 && parts[viewIdx + 1]) {
                const projName = parts[viewIdx + 1];
                // 排除一些关键字
                if (projName !== 'settings' && projName !== 'deployments' && currentAccountId) {
                    const infoText = `${currentAccountId} / ${projName}`;
                    document.getElementById('cf-pages-info').value = infoText;
                    document.getElementById('cf-env-info').value = infoText;
                    window.cfCurrentPage = { accId: currentAccountId, projName };
                }
            }
        }
    }

    // 绑定刷新按钮事件
    document.getElementById('btn-pages-refresh').onclick = () => {
        detectContext();
        const info = window.cfCurrentPage;
        if (info) {
            showToast(`已更新项目信息: ${info.projName}`, 'success');
        } else {
            showToast('未检测到项目信息,请确保在 Pages 详情页', 'warn');
        }
    };
    document.getElementById('btn-env-refresh').onclick = document.getElementById('btn-pages-refresh').onclick;

    // 监听 URL 变化自动刷新 (适用于 SPA 页面切换)
    let lastUrl = window.location.href;
    new MutationObserver(() => {
        const url = window.location.href;
        if (url !== lastUrl) {
            lastUrl = url;
            detectContext();
        }
    }).observe(document, {subtree: true, childList: true});

    // ================= R2 功能区 =================
    let r2Cursor = null;

    // 自动检测 R2 域名 (r2.dev 或 custom)
    async function detectR2Domain(bucketName) {
        const h = getHeaders();
        if (!h) return;

        try {
            // 1. 尝试获取 Custom Domains
            const res = await gmFetch('GET', `https://api.cloudflare.com/client/v4/accounts/${currentAccountId}/r2/buckets/${bucketName}/domains`, h);
            if (res.ok && res.data.result && res.data.result.domains && res.data.result.domains.length > 0) {
                const d = res.data.result.domains[0];
                return `https://${d.domain}`;
            }

            // 2. 尝试推测 r2.dev (API 通常不直接返回这个,但如果有开启 public access,格式通常固定)
            // 注:Cloudflare API 实际上很难直接获取 r2.dev 的子域名 hash,除非列出 usage 或其他 hack。
            // 这里我们尝试从 Bucket 属性中找,或者提示用户。
            // 暂时只支持 Custom Domain 的自动获取。
            return null;
        } catch (e) {
            console.error(e);
            return null;
        }
    }

    // 绑定检测按钮
    document.getElementById('btn-r2-detect-domain').onclick = async () => {
        const bucket = document.getElementById('cf-r2-buckets').value;
        if (!bucket || bucket === '请加载...') return showToast("请先选择 Bucket", "warn");

        const btn = document.getElementById('btn-r2-detect-domain');
        btn.textContent = "检测中...";

        const domain = await detectR2Domain(bucket);
        if (domain) {
            document.getElementById('cf-r2-domain').value = domain;
            GM_setValue('cf_r2_domain', domain); // 保存
            showToast(`已获取域名: ${domain}`, "success");
        } else {
            showToast("未检测到绑定域名,请手动输入", "info");
        }
        btn.textContent = "🔍 检测";
    };

    // 1. 加载 Buckets
    document.getElementById('btn-r2-load-buckets').onclick = async function () {
        const h = getHeaders(); if (!h) return;
        if (!currentAccountId) {
            showConfirm('输入 Account ID', '请输入您的 Account ID (可在 URL 中找到)', () => { });
            return;
        }

        const logEl = document.getElementById('log-r2');
        log(logEl, "加载 Buckets...", "info");

        try {
            const res = await gmFetch('GET', `https://api.cloudflare.com/client/v4/accounts/${currentAccountId}/r2/buckets`, h);
            if (!res.ok) throw new Error("加载失败: " + res.status);

            const buckets = res.data.result.buckets || res.data.result;
            const sel = document.getElementById('cf-r2-buckets');
            sel.innerHTML = "";
            buckets.forEach(b => {
                const opt = document.createElement('option');
                opt.value = b.name;
                opt.textContent = `${b.name}`;
                sel.appendChild(opt);
            });
            log(logEl, `发现 ${buckets.length} 个 Bucket`, "success");

            // 自动选中第一个并尝试检测域名
            if(buckets.length > 0) {
               // 延时一下体验更好
               setTimeout(() => document.getElementById('btn-r2-detect-domain').click(), 500);
            }

        } catch (e) { log(logEl, e.message, "error"); }
    };

    // 2. 加载文件列表
    document.getElementById('btn-r2-load-files').onclick = () => loadR2Files(false);
    document.getElementById('btn-r2-more').onclick = () => loadR2Files(true);

    async function loadR2Files(append) {
        const h = getHeaders(); if (!h) return;
        const bucket = document.getElementById('cf-r2-buckets').value;
        if (!bucket || bucket === '请加载...') return showToast("请先加载并选择 Bucket", "warn");

        const logEl = document.getElementById('log-r2');
        const listEl = document.getElementById('cf-r2-list');
        const btnMore = document.getElementById('btn-r2-more');

        if (!append) {
            listEl.innerHTML = "加载中...";
            r2Cursor = null;
            document.getElementById('cf-r2-select-all').checked = false;
        } else {
            btnMore.textContent = "加载中...";
        }

        try {
            let url = `https://api.cloudflare.com/client/v4/accounts/${currentAccountId}/r2/buckets/${bucket}/objects?per_page=50`;
            if (r2Cursor) url += `&cursor=${r2Cursor}`;

            const res = await gmFetch('GET', url, h);
            if (!res.ok) throw new Error("API Error");

            const objects = res.data.result.objects || res.data.result;
            r2Cursor = res.data.result_info?.cursor;

            if (!append) listEl.innerHTML = "";

            if (!objects.length && !append) {
                listEl.innerHTML = `<div style="padding:20px;text-align:center;">空 Bucket</div>`;
                return;
            }

            const savedDomain = GM_getValue('cf_r2_domain', '');
            if (savedDomain) document.getElementById('cf-r2-domain').value = savedDomain;
            document.getElementById('cf-r2-domain').onchange = function() { GM_setValue('cf_r2_domain', this.value.trim()); };

            objects.forEach(obj => {
                const item = document.createElement('div');
                item.className = "cf-r2-item";
                item.innerHTML = `
                    <input type="checkbox" class="cf-r2-chk" value="${obj.key}">
                    <div class="cf-r2-name" title="${obj.key}">${obj.key}</div>
                    <button class="cf-copy-btn" title="复制链接">🔗</button>
                    <div class="cf-r2-meta">${formatBytes(obj.size)}<br>${new Date(obj.uploaded).toLocaleDateString()}</div>
                `;

                // 绑定复制按钮事件
                item.querySelector('.cf-copy-btn').onclick = (e) => {
                    e.stopPropagation();
                    const domain = document.getElementById('cf-r2-domain').value;
                    if (!domain) {
                        showToast('请先输入或检测 R2 访问域名', 'warn');
                        return;
                    }
                    // 智能拼接 URL: 确保域名和路径之间只有一个斜杠
                    const cleanDomain = domain.replace(/\/+$/, '');
                    const cleanKey = obj.key.startsWith('/') ? obj.key : '/' + obj.key;
                    // 对 key 进行 encodeURI 处理,以支持中文和特殊字符
                    const url = cleanDomain + encodeURI(cleanKey);

                    GM_setClipboard(url);
                    showToast('已复制: ' + url, 'success');
                };

                // 图片预览事件
                const ext = obj.key.split('.').pop().toLowerCase();
                if (['jpg', 'png', 'jpeg', 'gif', 'webp', 'svg', 'ico'].includes(ext)) {
                    item.onmouseenter = (e) => {
                        const domain = document.getElementById('cf-r2-domain').value;
                        if (!domain) return;

                        let src = domain.endsWith('/') ? domain + obj.key : domain + '/' + obj.key;
                        const box = document.getElementById('cf-r2-preview');

                        // 预加载图片
                        const img = new Image();
                        img.src = src;
                        img.onload = () => {
                            box.innerHTML = '';
                            img.style.maxWidth = '200px';
                            img.style.maxHeight = '200px';
                            img.style.display = 'block';
                            box.appendChild(img);
                            box.style.display = 'block';

                            // 智能定位:避免超出屏幕右侧和底部
                            const rect = box.getBoundingClientRect();
                            let left = e.clientX + 20;
                            let top = e.clientY + 10;

                            if (left + rect.width > window.innerWidth) {
                                left = e.clientX - rect.width - 20;
                            }
                            if (top + rect.height > window.innerHeight) {
                                top = window.innerHeight - rect.height - 10;
                            }

                            box.style.left = left + 'px';
                            box.style.top = top + 'px';
                        };
                        img.onerror = () => {
                            // 加载失败时不显示预览框,或者显示错误占位
                            // box.style.display = 'none';
                        };
                    };
                    item.onmousemove = (e) => {
                        const box = document.getElementById('cf-r2-preview');
                        if (box.style.display === 'block') {
                             const rect = box.getBoundingClientRect();
                             let left = e.clientX + 20;
                             let top = e.clientY + 10;

                             if (left + 220 > window.innerWidth) left = e.clientX - 230; // 简单防溢出
                             if (top + 220 > window.innerHeight) top = window.innerHeight - 230;

                             box.style.left = left + 'px';
                             box.style.top = top + 'px';
                        }
                    };
                    item.onmouseleave = () => {
                        const box = document.getElementById('cf-r2-preview');
                        box.style.display = 'none';
                        box.innerHTML = ''; // 清空内容
                    };
                }
                listEl.appendChild(item);
            });

            bindCheckboxEvents();

            if (r2Cursor) {
                btnMore.style.display = "block";
                btnMore.textContent = "加载更多...";
            } else {
                btnMore.style.display = "none";
            }

            document.getElementById('cf-r2-count').textContent = `${document.querySelectorAll('.cf-r2-chk').length} items`;
            log(logEl, `加载成功 (+${objects.length})`, "success");

        } catch (e) { log(logEl, e.message, "error"); }
    }

    // 全选逻辑
    document.getElementById('cf-r2-select-all').onchange = function () {
        const checked = this.checked;
        document.querySelectorAll('.cf-r2-chk').forEach(chk => {
            chk.checked = checked;
        });
        updateDeleteBtn();
    };

    function bindCheckboxEvents() {
        document.querySelectorAll('.cf-r2-chk').forEach(c => {
            c.onchange = updateDeleteBtn;
        });
    }

    function updateDeleteBtn() {
        const count = document.querySelectorAll('.cf-r2-chk:checked').length;
        const delBtn = document.getElementById('btn-r2-delete');
        if (count > 0) {
            delBtn.style.display = "block";
            delBtn.textContent = `🗑️ 删除 (${count})`;
        } else {
            delBtn.style.display = "none";
        }
        const all = document.querySelectorAll('.cf-r2-chk');
        if (all.length > 0 && count === all.length) {
            document.getElementById('cf-r2-select-all').checked = true;
        } else {
            document.getElementById('cf-r2-select-all').checked = false;
        }
    }

    // 3. 删除文件
    document.getElementById('btn-r2-delete').onclick = async function () {
        const bucket = document.getElementById('cf-r2-buckets').value;
        const checks = document.querySelectorAll('.cf-r2-chk:checked');
        if (!checks.length) return;

        showConfirm('确认删除', `⚠️ 确定永久删除选中的 ${checks.length} 个文件?\n此操作不可恢复!`, async () => {
            const h = getHeaders();
            const logEl = document.getElementById('log-r2');

            log(logEl, "开始删除...");
            for (let chk of checks) {
                const key = chk.value;
                const url = `https://api.cloudflare.com/client/v4/accounts/${currentAccountId}/r2/buckets/${bucket}/objects/${encodeURIComponent(key)}`;
                const res = await gmFetch('DELETE', url, h);
                if (res.ok) {
                    chk.parentElement.remove();
                    log(logEl, `已删除 ${key}`, "success");
                } else {
                    log(logEl, `删除失败 ${key}`, "error");
                }
                await new Promise(r => setTimeout(r, 100));
            }
            updateDeleteBtn();
            document.getElementById('cf-r2-count').textContent = `${document.querySelectorAll('.cf-r2-chk').length} items`;
            showToast("删除操作完成", "success");
        });
    };

    // ================= Pages 清理逻辑 =================
    let pagesRunning = false;
    let pagesShouldStop = false;

    document.getElementById('btn-pages-run').onclick = async function () {
        const logEl = document.getElementById('log-pages');
        const btn = this;

        if (pagesRunning) {
            pagesShouldStop = true;
            log(logEl, '🛑 正在停止中...', 'warn');
            btn.textContent = '正在停止...';
            return;
        }

        const h = getHeaders();
        const info = window.cfCurrentPage;
        if (!h || !info) return log(logEl, '❌ 未检测到 Pages 项目信息', 'error');

        pagesRunning = true;
        pagesShouldStop = false;
        btn.textContent = '⏹ 停止清理';
        btn.className = 'cf-btn cf-btn-stop';

        const url = `https://api.cloudflare.com/client/v4/accounts/${info.accId}/pages/projects/${info.projName}/deployments`;

        try {
            log(logEl, '🚀 任务开始', 'info');
            while (!pagesShouldStop) {
                log(logEl, '正在获取部署列表...');
                const res = await gmFetch('GET', `${url}?per_page=25&sort_by=created_on&sort_order=asc`, h);

                if (!res.ok) throw new Error('API Error: ' + res.status);

                const list = res.data.result;
                if (!list || list.length === 0) {
                    log(logEl, '✅ 全部清理完成!', 'success');
                    showToast('Pages 部署清理完成', 'success');
                    break;
                }

                log(logEl, `发现 ${list.length} 个旧部署,开始删除...`);
                for (let dep of list) {
                    if (pagesShouldStop) { log(logEl, '⚠️ 已终止。', 'warn'); break; }

                    const del = await gmFetch('DELETE', `${url}/${dep.id}`, h);
                    if (del.ok) {
                        log(logEl, `已删除 ${dep.id.substring(0, 8)}`, 'success');
                    } else if (del.status === 429) {
                        log(logEl, '⏳ API限速,暂停 5 秒...', 'warn');
                        await new Promise(r => setTimeout(r, 5000));
                    } else {
                        log(logEl, `删除失败 ${del.status}`, 'error');
                    }
                    await new Promise(r => setTimeout(r, 200));
                }
            }
        } catch (e) {
            log(logEl, e.message, 'error');
        }

        pagesRunning = false;
        btn.textContent = '开始清理部署历史';
        btn.className = 'cf-btn cf-btn-primary';
    };

    // ================= Env 导出逻辑 =================
    document.getElementById('btn-env-get').onclick = async function () {
        const h = getHeaders();
        const info = window.cfCurrentPage;
        const envType = document.getElementById('cf-env-type').value;
        const out = document.getElementById('cf-env-output');

        if (!h || !info) { out.value = '❌ 错误: 请先进入 Pages 项目详情页'; return; }

        out.value = '正在获取...';
        const url = `https://api.cloudflare.com/client/v4/accounts/${info.accId}/pages/projects/${info.projName}`;

        try {
            const res = await gmFetch('GET', url, h);
            if (!res.ok) throw new Error('API Error');

            const vars = res.data.result.deployment_configs?.[envType]?.env_vars || {};
            let text = `# Exported from Cloudflare Pages [${envType}]\n`;
            for (let [key, val] of Object.entries(vars)) {
                text += `${key}=${val.value}\n`;
            }
            out.value = text;
            GM_setClipboard(text);
            showToast('环境变量已复制到剪贴板', 'success');
        } catch (e) { out.value = '获取失败: ' + e.message; }
    };

})();