Auto-click any button on any page. Supports interval, time limit, click limit, and persistent run across page navigation.
// ==UserScript==
// @name iClick
// @namespace https://github.com/
// @version 2.1
// @description Auto-click any button on any page. Supports interval, time limit, click limit, and persistent run across page navigation.
// @match *://*/*
// @grant GM_setValue
// @grant GM_getValue
// ==/UserScript==
(function () {
'use strict';
// ── 暗色模式 ──
const dark = window.matchMedia('(prefers-color-scheme: dark)').matches;
const C = dark
? { bg: '#1f1f1f', border: '#333', text: '#ccc', sub: '#888', badge: '#2a2a2a', sep: '#333' }
: { bg: '#fff', border: '#ddd', text: '#333', sub: '#555', badge: '#f5f5f5', sep: '#eee' };
// ── 共享顶栏 ──
function getTopbar() {
let bar = document.getElementById('kiro-topbar');
if (!bar) {
bar = document.createElement('div');
bar.id = 'kiro-topbar';
bar.style.cssText = `position:fixed;top:0;left:0;right:0;z-index:99999;background:${C.bg};border-bottom:1px solid ${C.border};display:flex;align-items:stretch;min-height:36px;box-shadow:0 2px 6px rgba(0,0,0,.15);font-size:13px;font-family:sans-serif;`;
document.body.appendChild(bar);
document.body.style.paddingTop = '36px';
}
return bar;
}
function addSection(id) {
const bar = getTopbar();
if (bar.querySelector(`#${id}`)) return bar.querySelector(`#${id}`);
if (bar.children.length > 0) {
const sep = document.createElement('div');
sep.style.cssText = `width:1px;background:${C.sep};margin:6px 0;flex-shrink:0;`;
bar.appendChild(sep);
}
const spacer = document.createElement('div');
spacer.style.cssText = 'flex:1;';
bar.appendChild(spacer);
const sec = document.createElement('div');
sec.id = id;
sec.style.cssText = 'display:flex;align-items:center;gap:8px;padding:0 14px;flex-shrink:0;overflow:hidden;';
bar.appendChild(sec);
return sec;
}
// ── 跨页持久运行:页面加载时检查是否应自动恢复 ──
const PERSIST_KEY = 'ac_persist';
const savedState = JSON.parse(GM_getValue(PERSIST_KEY, 'null'));
const shouldResume = savedState && savedState.running && savedState.target;
const savedInterval = GM_getValue('ac_interval', 1500);
let running = false, timer = null, clockTimer = null;
let count = 0, elapsed = 0;
// ── UI ──
const sec = addSection('kiro-clicker');
const label = el('span', `font-weight:700;white-space:nowrap;color:${C.text};font-size:13px;letter-spacing:.5px;`, 'iClick');
const targetSelect = document.createElement('select');
targetSelect.style.cssText = `padding:2px 8px;border:1px solid ${C.border};border-radius:12px;font-size:12px;background:${C.badge};color:${C.text};max-width:120px;flex-shrink:0;outline:none;cursor:pointer;`;
const intervalInput = document.createElement('input');
intervalInput.type = 'number'; intervalInput.value = savedInterval; intervalInput.min = 100;
intervalInput.title = '间隔(ms)';
intervalInput.style.cssText = `width:64px;padding:2px 8px;border:1px solid ${C.border};border-radius:12px;font-size:12px;background:${C.badge};color:${C.text};outline:none;text-align:center;`;
const limitInput = document.createElement('input');
limitInput.type = 'number'; limitInput.value = GM_getValue('ac_limit', 0); limitInput.min = 0;
limitInput.title = '时限(分钟,0=不限)';
limitInput.style.cssText = `width:54px;padding:2px 8px;border:1px solid ${C.border};border-radius:12px;font-size:12px;background:${C.badge};color:${C.text};outline:none;text-align:center;`;
const maxInput = document.createElement('input');
maxInput.type = 'number'; maxInput.value = GM_getValue('ac_max', 0); maxInput.min = 0;
maxInput.title = '点击上限(0=不限)';
maxInput.style.cssText = `width:54px;padding:2px 8px;border:1px solid ${C.border};border-radius:12px;font-size:12px;background:${C.badge};color:${C.text};outline:none;text-align:center;`;
const status = el('span', `color:${C.sub};white-space:nowrap;font-size:12px;`, '就绪');
const toggleBtn = btn('▶ 开始', '#1677ff');
const lbInterval = el('span', `color:${C.sub};font-size:11px;white-space:nowrap;`, 'ms');
const lbLimit = el('span', `color:${C.sub};font-size:11px;white-space:nowrap;`, 'min');
const lbMax = el('span', `color:${C.sub};font-size:11px;white-space:nowrap;`, 'max');
sec.append(label, targetSelect, lbInterval, intervalInput, lbLimit, limitInput, lbMax, maxInput, status, toggleBtn);
// 扫描按钮
const refreshButtons = () => {
const cur = targetSelect.value || (savedState && savedState.target) || '';
targetSelect.innerHTML = '';
[...new Set(
[...document.querySelectorAll('button,input[type=button],input[type=submit],[role=button]')]
.filter(b => b.offsetParent && !document.getElementById('kiro-clicker')?.contains(b))
.map(b => (b.textContent || b.value || '').trim())
.filter(Boolean)
)].forEach(text => {
const o = document.createElement('option');
o.value = o.textContent = text;
if (text === cur) o.selected = true;
targetSelect.appendChild(o);
});
};
refreshButtons();
targetSelect.onfocus = refreshButtons;
const findTarget = () =>
[...document.querySelectorAll('button,input[type=button],input[type=submit],[role=button]')]
.find(b => (b.textContent || b.value || '').trim() === targetSelect.value
&& !b.disabled && b.offsetParent
&& !document.getElementById('kiro-clicker')?.contains(b));
const stop = (reason) => {
running = false;
clearInterval(timer); clearInterval(clockTimer);
toggleBtn.textContent = '▶ 开始'; toggleBtn.style.background = '#1677ff';
GM_setValue(PERSIST_KEY, JSON.stringify({ running: false, target: targetSelect.value }));
if (reason) status.textContent = reason;
};
const start = () => {
const interval = Math.max(100, parseInt(intervalInput.value) || 1500);
const limitSec = (parseFloat(limitInput.value) || 0) * 60;
const maxClicks = parseInt(maxInput.value) || 0;
GM_setValue('ac_interval', interval);
GM_setValue('ac_limit', limitInput.value);
GM_setValue('ac_max', maxInput.value);
GM_setValue(PERSIST_KEY, JSON.stringify({ running: true, target: targetSelect.value }));
count = 0; elapsed = 0; running = true;
toggleBtn.textContent = '⏹ 停止'; toggleBtn.style.background = '#ff4d4f';
clockTimer = setInterval(() => {
elapsed++;
const h = Math.floor(elapsed / 3600), m = Math.floor((elapsed % 3600) / 60), s = elapsed % 60;
const timeStr = `${h ? h + 'h ' : ''}${m ? m + 'm ' : ''}${s}s`;
status.textContent = `已点击 ${count} 次 · ${timeStr}`;
if (limitSec > 0 && elapsed >= limitSec) stop('⏱ 已达时限,自动停止');
}, 1000);
timer = setInterval(() => {
const el = findTarget();
if (el) {
el.click(); count++;
if (maxClicks > 0 && count >= maxClicks) stop(`✅ 已达上限 ${maxClicks} 次,自动停止`);
} else {
status.textContent = `⚠️ 未找到按钮「${targetSelect.value}」`;
}
}, interval);
};
toggleBtn.onclick = () => running ? stop() : start();
// 自动恢复
if (shouldResume) {
// 等 DOM 稳定后恢复
setTimeout(() => { refreshButtons(); start(); status.textContent = '已自动恢复运行'; }, 1500);
}
function btn(text, color) {
const b = document.createElement('button');
b.textContent = text;
b.style.cssText = `padding:3px 14px;background:${color};color:#fff;border:none;border-radius:12px;cursor:pointer;font-size:12px;white-space:nowrap;flex-shrink:0;font-weight:500;letter-spacing:.3px;transition:opacity .15s;`;
b.onmouseover = () => b.style.opacity = '0.8';
b.onmouseout = () => b.style.opacity = '1';
return b;
}
function el(tag, css, text) {
const e = document.createElement(tag);
e.style.cssText = css;
if (text !== undefined) e.textContent = text;
return e;
}
})();