// ==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 // @downloadURL https://update.greasyfork.icu/scripts/565216/Cloudflare%20Helper.user.js // @updateURL https://update.greasyfork.icu/scripts/565216/Cloudflare%20Helper.meta.js // ==/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 = `${type === 'success' ? '✅' : type === 'error' ? '❌' : type === 'warn' ? '⚠️' : 'ℹ️'}${msg}`; 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 = `

${title}

${msg}

`; 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 = `
🚀 Cloudflare Helper 1.0
R2 管理
Pages清理
环境导出
0 items
请选择 Bucket 并加载
R2 就绪
等待开始...
`; 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 = `
空 Bucket
`; 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 = `
${obj.key}
${formatBytes(obj.size)}
${new Date(obj.uploaded).toLocaleDateString()}
`; // 绑定复制按钮事件 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; } }; })();