Cloudflare 增强工具:Pages清理 + 环境导出 + DNS管理 + R2文件管理 + WAF/Cache
// ==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; }
};
})();