锁定Gemini:UI已等比例缩小。针对手机端优化。后台运行增强版。位置记忆同步优化。可配置隐藏投票框。
目前為
// ==UserScript==
// @name Gemini Hunter (Mobile Fix)
// @namespace http://tampermonkey.net/
// @version 2.14.0
// @author Mozi & Google Gemini
// @match https://lmarena.ai/*
// @match https://beta.lmarena.ai/*
// @match https://chat.lmsys.org/*
// @icon https://lmarena.ai/favicon.ico
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_deleteValue
// @grant GM_addStyle
// @grant GM_notification
// @license MIT
// @description 锁定Gemini:UI已等比例缩小。针对手机端优化。后台运行增强版。位置记忆同步优化。可配置隐藏投票框。
// ==/UserScript==
(function() {
'use strict';
// ============================================================
// --- 【后台防休眠核心】 Web Worker 代理定时器 ---
// ============================================================
const workerBlob = new Blob([`
var timers = {};
self.onmessage = function(e) {
var data = e.data;
switch (data.type) {
case 'SET_INTERVAL':
timers[data.id] = setInterval(function() {
self.postMessage({ type: 'TICK', id: data.id });
}, data.delay);
break;
case 'CLEAR_INTERVAL':
clearInterval(timers[data.id]);
delete timers[data.id];
break;
case 'SET_TIMEOUT':
timers[data.id] = setTimeout(function() {
self.postMessage({ type: 'TICK', id: data.id });
delete timers[data.id];
}, data.delay);
break;
case 'CLEAR_TIMEOUT':
clearTimeout(timers[data.id]);
delete timers[data.id];
break;
}
};
`], { type: 'application/javascript' });
const workerUrl = URL.createObjectURL(workerBlob);
const bgWorker = new Worker(workerUrl);
const workerCallbacks = {};
let workerTimerIdCounter = 0;
bgWorker.onmessage = function(e) {
const callback = workerCallbacks[e.data.id];
if (callback && typeof callback === 'function') {
callback();
}
};
const _setInterval = function(callback, delay) {
const id = ++workerTimerIdCounter;
workerCallbacks[id] = callback;
bgWorker.postMessage({ type: 'SET_INTERVAL', id: id, delay: delay });
return id;
};
const _clearInterval = function(id) {
if (workerCallbacks[id]) {
bgWorker.postMessage({ type: 'CLEAR_INTERVAL', id: id });
delete workerCallbacks[id];
}
};
const _setTimeout = function(callback, delay) {
const id = ++workerTimerIdCounter;
workerCallbacks[id] = callback;
bgWorker.postMessage({ type: 'SET_TIMEOUT', id: id, delay: delay });
return id;
};
const _clearTimeout = function(id) {
if (workerCallbacks[id]) {
bgWorker.postMessage({ type: 'CLEAR_TIMEOUT', id: id });
delete workerCallbacks[id];
}
};
const setInterval = _setInterval;
const clearInterval = _clearInterval;
const setTimeout = _setTimeout;
const clearTimeout = _clearTimeout;
// ============================================================
// --- 核心配置 ---
const CONFIG = {
prompt1: "你是谁",
keyword1: "Gemini",
placeholderText: "正在查找,请稍后...",
resetUrl: window.location.origin + "/c/new",
defaultIcon: "🧬",
titlePrefix: "Gemini Hunter"
};
// --- 状态管理 ---
const sessionActive = sessionStorage.getItem('gh_session_active');
let storedIsRunning = GM_getValue('isRunning', false);
// 【修复1】手机端 Session 容错:如果 GM 显示正在运行但 session 丢了,补回 session 而不是停止
if (storedIsRunning && !sessionActive) {
sessionStorage.setItem('gh_session_active', 'true');
// 不再强制停止,信任 GM_getValue
}
let isRunning = storedIsRunning;
let savedLockedSide = GM_getValue('gh_locked_side', null);
let lockedSide = savedLockedSide;
let isMinimized = GM_getValue('gh_minimized_state_v1', false);
let isAutoExpand = GM_getValue('gh_auto_expand', true);
let isSoundEnabled = GM_getValue('gh_sound_enabled', true);
let isAutoHideOther = GM_getValue('gh_auto_hide_other', false);
let isRemoveVoteUI = GM_getValue('gh_remove_vote_ui', true);
let chatMonitorInterval = null;
let timerInterval = null;
let highlightInterval = null;
let checkLoopInterval = null;
let timerActive = false;
let originalTitle = document.title;
// --- UI 构建 ---
const uiHtml = `
<div id="gh-panel" style="${isMinimized ? 'display:none;' : ''}">
<div class="gh-blur-bg"></div>
<div class="gh-header" id="gh-header">
<div class="gh-title">
<span class="gh-icon">🧬</span> Gemini Hunter
</div>
<div class="gh-win-ctrl">
<div id="gh-btn-min" title="收起"></div>
</div>
</div>
<div class="gh-body">
<div class="gh-timer-box">
<span class="gh-timer-label">TIME</span>
<span id="gh-timer-display">0.00 s</span>
</div>
<div class="gh-status-bar">
<div class="gh-dot"></div>
<span id="gh-msg">就绪</span>
</div>
<div class="gh-settings">
<label class="gh-toggle-row">
<span class="gh-lbl">自动全屏</span>
<div class="gh-switch">
<input type="checkbox" id="gh-chk-expand" ${isAutoExpand ? 'checked' : ''}>
<span class="gh-slider"></span>
</div>
</label>
<label class="gh-toggle-row">
<span class="gh-lbl">隐藏非目标</span>
<div class="gh-switch">
<input type="checkbox" id="gh-chk-hide" ${isAutoHideOther ? 'checked' : ''}>
<span class="gh-slider"></span>
</div>
</label>
<label class="gh-toggle-row">
<span class="gh-lbl">隐藏投票框</span>
<div class="gh-switch">
<input type="checkbox" id="gh-chk-remove-vote" ${isRemoveVoteUI ? 'checked' : ''}>
<span class="gh-slider"></span>
</div>
</label>
<label class="gh-toggle-row">
<span class="gh-lbl">提示音</span>
<div class="gh-switch">
<input type="checkbox" id="gh-chk-sound" ${isSoundEnabled ? 'checked' : ''}>
<span class="gh-slider"></span>
</div>
</label>
</div>
<div class="gh-controls">
<button id="gh-btn-start">
<span class="btn-icon">▶</span> 开始
</button>
<button id="gh-btn-stop" style="display:none;">
<span class="btn-icon">⏹</span> 停止
</button>
</div>
</div>
</div>
<div id="gh-ball" style="${!isMinimized ? 'display:none;' : ''}">
<div class="gh-ball-ripple"></div>
<div class="gh-ball-inner">
<span class="gh-ball-text">${CONFIG.defaultIcon}</span>
</div>
</div>
`;
const container = document.createElement('div');
container.innerHTML = uiHtml;
document.body.appendChild(container);
// --- CSS ---
GM_addStyle(`
:root { --gh-primary: #6366f1; --gh-primary-dark: #4f46e5; --gh-success: #10b981; --gh-danger: #ef4444; --gh-bg: rgba(255, 255, 255, 0.92); --gh-shadow: 0 8px 20px -5px rgba(0, 0, 0, 0.15); }
#gh-panel { position: fixed; top: 12%; left: 5%; width: 90%; max-width: 270px; background: var(--gh-bg); backdrop-filter: blur(15px); -webkit-backdrop-filter: blur(15px); border: 1px solid rgba(255,255,255,0.6); border-radius: 18px; box-shadow: var(--gh-shadow); z-index: 100000; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; overflow: hidden; transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1), opacity 0.2s; touch-action: none; }
@media (min-width: 768px) { #gh-panel { width: 220px; top: 80px; right: 40px; left: auto; } }
.gh-header { padding: 12px 15px 8px; display: flex; justify-content: space-between; align-items: center; cursor: move; user-select: none; }
.gh-title { font-weight: 700; font-size: 13px; color: #1f2937; display: flex; align-items: center; gap: 6px; }
.gh-ver { font-size: 9px; font-weight: 600; color: #9ca3af; background: rgba(0,0,0,0.05); padding: 2px 6px; border-radius: 8px; text-transform: uppercase;}
.gh-win-ctrl { display: flex; padding: 4px; margin: -4px; }
#gh-btn-min { width: 22px; height: 22px; border-radius: 50%; background: rgba(0,0,0,0.06); cursor: pointer; position: relative; }
#gh-btn-min::before { content: ""; position: absolute; top: 10px; left: 6px; width: 10px; height: 2px; background: #6b7280; border-radius: 2px; }
.gh-body { padding: 0 15px 18px; display: flex; flex-direction: column; gap: 12px; }
.gh-timer-box { background: #f9fafb; border-radius: 12px; padding: 8px 12px; display: flex; justify-content: space-between; align-items: center; border: 1px solid #e5e7eb; }
.gh-timer-label { font-size: 9px; font-weight: 800; color: #9ca3af; letter-spacing: 1px; }
#gh-timer-display { font-family: monospace; font-size: 17px; font-weight: 700; color: #374151; }
.gh-status-bar { background: rgba(255,255,255,0.6); border: 1px solid rgba(0,0,0,0.05); padding: 8px 10px; border-radius: 10px; display: flex; align-items: center; gap: 8px; font-size: 12px; color: #4b5563; font-weight: 500; }
.gh-dot { width: 8px; height: 8px; border-radius: 50%; background: #d1d5db; flex-shrink: 0;}
.gh-dot.active { background: var(--gh-primary); box-shadow: 0 0 6px var(--gh-primary); animation: gh-pulse-dot 1.5s infinite; }
.gh-dot.success { background: var(--gh-success); }
.gh-dot.error { background: var(--gh-danger); }
.gh-settings { padding: 0 2px; display: flex; flex-direction: column; gap: 8px; }
.gh-toggle-row { display: flex; justify-content: space-between; align-items: center; cursor: pointer; user-select: none; -webkit-tap-highlight-color: transparent; }
.gh-lbl { font-size: 12px; font-weight: 600; color: #4b5563; }
.gh-switch { position: relative; display: inline-block; width: 34px; height: 20px; flex-shrink: 0;}
.gh-switch input { opacity: 0; width: 0; height: 0; }
.gh-slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #e5e7eb; transition: .3s; border-radius: 34px; }
.gh-slider:before { position: absolute; content: ""; height: 16px; width: 16px; left: 2px; bottom: 2px; background-color: white; transition: .3s; border-radius: 50%; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
input:checked + .gh-slider { background-color: var(--gh-primary); }
input:checked + .gh-slider:before { transform: translateX(14px); }
.gh-controls button { width: 100%; padding: 10px; border: none; border-radius: 12px; font-weight: 600; cursor: pointer; font-size: 13px; display: flex; align-items: center; justify-content: center; gap: 6px; color: white; touch-action: manipulation; }
#gh-btn-start { background: linear-gradient(135deg, #4f46e5, #6366f1); box-shadow: 0 3px 10px rgba(99, 102, 241, 0.3); }
#gh-btn-stop { background: linear-gradient(135deg, #dc2626, #ef4444); box-shadow: 0 3px 10px rgba(239, 68, 68, 0.3); }
.gh-winner-glow { box-shadow: inset 0 0 0 3px #10b981, 0 0 25px rgba(16, 185, 129, 0.25) !important; border-radius: 12px !important; }
.gh-hidden-bubble { display: none !important; }
#gh-ball { position: fixed; top: 70%; right: 15px; width: 48px; height: 48px; z-index: 100001; cursor: pointer; user-select: none; display: flex; align-items: center; justify-content: center; touch-action: none; }
.gh-ball-inner { width: 40px; height: 40px; background: #ffffff; border-radius: 50%; color: #4f46e5; display: flex; align-items: center; justify-content: center; position: relative; z-index: 2; box-shadow: 0 6px 16px rgba(0, 0, 0, 0.15); }
.gh-ball-text { font-weight: 800; font-size: 20px; transition: all 0.3s; }
.gh-google-letter {
background: linear-gradient(135deg, #4285F4 20%, #EA4335 40%, #FBBC05 60%, #34A853 80%);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
color: transparent;
font-family: "Google Sans", "Product Sans", Roboto, Arial, sans-serif;
font-weight: 900 !important;
font-size: 25px !important;
letter-spacing: -1px;
filter: drop-shadow(0 1px 2px rgba(0,0,0,0.1));
}
.gh-ball-ripple { position: absolute; width: 52px; height: 52px; border-radius: 50%; border: 2px solid transparent; z-index: 1; transition: opacity 0.3s; pointer-events: none; opacity: 0; }
#gh-ball.running .gh-ball-ripple { opacity: 1; border-top-color: #3b82f6; border-left-color: #a855f7; animation: gh-spin-smooth 1s linear infinite; }
@keyframes gh-spin-smooth { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
@keyframes gh-pulse-dot { 0% { opacity: 1; } 50% { opacity: 0.4; } 100% { opacity: 1; } }
`);
// --- DOM 引用 ---
const panel = document.getElementById('gh-panel');
const ball = document.getElementById('gh-ball');
const ballInner = ball.querySelector('.gh-ball-text');
const btnMin = document.getElementById('gh-btn-min');
const header = document.getElementById('gh-header');
const btnStart = document.getElementById('gh-btn-start');
const btnStop = document.getElementById('gh-btn-stop');
const txtMsg = document.getElementById('gh-msg');
const dot = document.querySelector('.gh-dot');
const chkExpand = document.getElementById('gh-chk-expand');
const chkHide = document.getElementById('gh-chk-hide');
const chkSound = document.getElementById('gh-chk-sound');
const chkRemoveVote = document.getElementById('gh-chk-remove-vote');
const timerDisplay = document.getElementById('gh-timer-display');
function updateBallText(side) {
if (side === 'A' || side === 'B' || side === 'BOTH') {
ballInner.innerText = (side === 'BOTH') ? '双' : side;
ballInner.classList.add('gh-google-letter');
ballInner.classList.remove('gh-ball-text');
ballInner.className = 'gh-ball-text gh-google-letter';
} else {
ballInner.innerText = CONFIG.defaultIcon;
ballInner.className = 'gh-ball-text';
}
}
// --- 交互逻辑 ---
panel.addEventListener('click', (e) => { e.stopPropagation(); });
if (window.innerWidth < 768 && !isMinimized && !panel.style.left) {
panel.style.left = '5%'; panel.style.top = '20%';
}
chkExpand.addEventListener('change', (e) => {
isAutoExpand = e.target.checked;
GM_setValue('gh_auto_expand', isAutoExpand);
if (lockedSide && isAutoExpand) triggerExpand(lockedSide);
});
chkHide.addEventListener('change', (e) => {
isAutoHideOther = e.target.checked;
GM_setValue('gh_auto_hide_other', isAutoHideOther);
});
chkSound.addEventListener('change', (e) => {
isSoundEnabled = e.target.checked;
GM_setValue('gh_sound_enabled', isSoundEnabled);
});
chkRemoveVote.addEventListener('change', (e) => {
isRemoveVoteUI = e.target.checked;
GM_setValue('gh_remove_vote_ui', isRemoveVoteUI);
removeVotingBar();
});
// --- 音频管理 ---
function keepAliveAudio() {
const AudioContext = window.AudioContext || window.webkitAudioContext;
if (!AudioContext) return;
const ctx = new AudioContext();
const osc = ctx.createOscillator();
const gain = ctx.createGain();
gain.gain.value = 0.01;
osc.connect(gain);
gain.connect(ctx.destination);
osc.start();
osc.stop(ctx.currentTime + 0.1);
}
function playVictoryTheme() {
if (!isSoundEnabled) return;
const AudioContext = window.AudioContext || window.webkitAudioContext;
if (!AudioContext) return;
const ctx = new AudioContext();
const now = ctx.currentTime;
const melody = [[523.25,0,0.1,'square'],[659.25,0.1,0.1,'square'],[783.99,0.2,0.1,'square'],[1046.50,0.3,0.4,'square'],[523.25,0.4,0.05,'sawtooth'],[783.99,0.45,0.05,'sawtooth'],[1046.50,0.5,0.05,'sawtooth'],[523.25,0.6,0.6,'triangle']];
melody.forEach(n => {
const o = ctx.createOscillator(); const g = ctx.createGain();
o.type = n[3]; o.frequency.value = n[0]; o.connect(g); g.connect(ctx.destination);
o.start(now+n[1]); g.gain.setValueAtTime(0.05, now+n[1]);
g.gain.exponentialRampToValueAtTime(0.001, now+n[1]+n[2]); o.stop(now+n[1]+n[2]);
});
}
// --- 计时器逻辑 ---
function startTimer() {
if (timerInterval) clearInterval(timerInterval);
timerActive = true;
const startTime = Date.now();
timerDisplay.innerText = "0.00 s";
timerDisplay.style.color = "#374151";
timerInterval = setInterval(() => {
const elapsed = (Date.now() - startTime) / 1000;
const timeStr = elapsed.toFixed(2);
timerDisplay.innerText = timeStr + " s";
document.title = `[${Math.floor(elapsed)}s] 查找中...`;
}, 50);
}
function stopTimer() {
if (timerInterval) {
clearInterval(timerInterval);
timerInterval = null;
}
timerActive = false;
timerDisplay.style.color = "#10b981";
timerDisplay.style.transform = "scale(1.2)";
setTimeout(() => { timerDisplay.style.transform = "scale(1)"; }, 200);
document.title = originalTitle;
}
function isLoadingIndicatorVisible() {
const el = document.querySelector('canvas[data-sentry-component="Loading"]');
return !!el;
}
let uiTimerSeenLoading = false;
setInterval(() => {
if (timerActive) {
const isLoading = isLoadingIndicatorVisible();
if (isLoading) {
uiTimerSeenLoading = true;
}
if (uiTimerSeenLoading && !isLoading) {
stopTimer();
uiTimerSeenLoading = false;
if (isSoundEnabled && !isRunning) {
playVictoryTheme();
}
}
} else {
uiTimerSeenLoading = false;
}
}, 100);
function setupGlobalListeners() {
document.addEventListener('keydown', (e) => {
if (e.target.tagName === 'TEXTAREA' && e.key === 'Enter' && !e.shiftKey) {
setTimeout(() => { startTimer(); }, 50);
}
}, true);
document.addEventListener('click', (e) => {
const btn = e.target.closest('button');
if (btn) {
const label = btn.getAttribute('aria-label') || "";
const testid = btn.getAttribute('data-testid') || "";
const type = btn.getAttribute('type') || "";
if (label.includes("Send") || testid.includes("send") || type === "submit") {
startTimer();
}
}
}, true);
}
setupGlobalListeners();
// --- 窗口管理 ---
function toggleMinimizeUI() {
isMinimized = !isMinimized;
GM_setValue('gh_minimized_state_v1', isMinimized);
if (isMinimized) {
const pRect = panel.getBoundingClientRect();
const screenMid = window.innerWidth / 2;
const isRightSide = (pRect.left + pRect.width / 2) > screenMid;
let newTop = pRect.top;
let newLeft;
if (isRightSide) newLeft = pRect.right - 48;
else newLeft = pRect.left;
if (newTop < 0) newTop = 10;
if (newTop > window.innerHeight - 60) newTop = window.innerHeight - 60;
ball.style.top = newTop + 'px';
ball.style.left = newLeft + 'px';
panel.style.display = 'none';
ball.style.display = 'flex';
} else {
const bRect = ball.getBoundingClientRect();
const screenMid = window.innerWidth / 2;
const isRightSide = (bRect.left + bRect.width / 2) > screenMid;
panel.style.opacity = '0';
panel.style.display = 'block';
const pWidth = panel.offsetWidth;
const pHeight = panel.offsetHeight;
let newTop = bRect.top;
let newLeft;
if (isRightSide) newLeft = bRect.right - pWidth;
else newLeft = bRect.left;
if (newLeft < 5) newLeft = 5;
if (newLeft + pWidth > window.innerWidth) newLeft = window.innerWidth - pWidth - 5;
if (newTop < 5) newTop = 5;
if (newTop + pHeight > window.innerHeight) newTop = window.innerHeight - pHeight - 5;
panel.style.left = newLeft + 'px';
panel.style.top = newTop + 'px';
requestAnimationFrame(() => {
panel.style.opacity = '1';
panel.style.transform = 'scale(1)';
});
ball.style.display = 'none';
}
}
// 【修复2】彻底重写拖拽逻辑,解决手机端点击不灵敏问题
function makeDraggable(element, handle = element, clickCallback = null) {
let startX, startY, initialLeft, initialTop;
let hasMoved = false; // 替换原有的 isDragging 逻辑,仅用于判断是否移动
const onStart = (clientX, clientY) => {
hasMoved = false; // 重置移动标记
startX = clientX;
startY = clientY;
const rect = element.getBoundingClientRect();
initialLeft = rect.left;
initialTop = rect.top;
// 准备移动
element.style.position = 'fixed';
element.style.right = 'auto';
element.style.bottom = 'auto';
element.style.left = initialLeft + 'px';
element.style.top = initialTop + 'px';
element.style.transition = 'none';
if(element === ball) element.style.animation = 'none';
};
const onMove = (clientX, clientY) => {
const dist = Math.sqrt(Math.pow(clientX - startX, 2) + Math.pow(clientY - startY, 2));
// 【关键】增加防抖阈值到 10px,小于这个距离视作点击
if (dist > 10) {
hasMoved = true;
let newLeft = initialLeft + (clientX - startX);
let newTop = initialTop + (clientY - startY);
const maxW = window.innerWidth - element.offsetWidth;
const maxH = window.innerHeight - element.offsetHeight;
// 边界检查
if (newLeft < 0) newLeft = 0;
if (newLeft > maxW) newLeft = maxW;
if (newTop < 0) newTop = 0;
if (newTop > maxH) newTop = maxH;
element.style.left = newLeft + 'px';
element.style.top = newTop + 'px';
}
};
const onEnd = (e) => {
document.body.style.userSelect = '';
handle.style.cursor = 'move';
element.style.transition = 'transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1), opacity 0.2s';
// 如果几乎没移动,且传入了点击回调,则触发点击逻辑
if (!hasMoved && clickCallback) {
// 阻止默认事件,防止某些设备触发两次
if(e.cancelable) e.preventDefault();
clickCallback();
}
};
// 鼠标事件
handle.addEventListener('mousedown', (e) => {
if(e.target === btnMin || e.target.closest('.gh-win-ctrl')) return;
e.stopPropagation();
document.body.style.userSelect = 'none';
handle.style.cursor = 'grabbing';
onStart(e.clientX, e.clientY);
const moveHandler = (ev) => onMove(ev.clientX, ev.clientY);
const upHandler = (ev) => {
document.removeEventListener('mousemove', moveHandler);
document.removeEventListener('mouseup', upHandler);
onEnd(ev);
};
document.addEventListener('mousemove', moveHandler);
document.addEventListener('mouseup', upHandler);
});
// 触摸事件
handle.addEventListener('touchstart', (e) => {
if(e.target === btnMin || e.target.closest('.gh-win-ctrl')) return;
const touch = e.touches[0];
onStart(touch.clientX, touch.clientY);
}, { passive: false });
handle.addEventListener('touchmove', (e) => {
const touch = e.touches[0];
// 如果正在移动,阻止默认滚动
if (hasMoved) e.preventDefault();
onMove(touch.clientX, touch.clientY);
}, { passive: false });
handle.addEventListener('touchend', (e) => {
onEnd(e);
}, { passive: false });
// 只有PC端的click事件保留,移动端全靠 touchend 里的逻辑
if (clickCallback && element === ball) {
element.addEventListener('click', (e) => {
// PC端点击
if (!hasMoved) {
e.stopPropagation();
clickCallback();
}
});
}
}
// 重新绑定拖拽和点击
btnMin.addEventListener('click', (e) => { e.stopPropagation(); toggleMinimizeUI(); });
btnMin.addEventListener('touchend', (e) => { e.stopPropagation(); e.preventDefault(); toggleMinimizeUI(); });
makeDraggable(panel, header);
// 将 toggleMinimizeUI 作为回调传给 makeDraggable,由内部统一处理点击判定
makeDraggable(ball, ball, toggleMinimizeUI);
// --- 辅助工具 ---
function updateStatus(msg, type = 'normal') {
txtMsg.innerText = msg; dot.className = 'gh-dot';
if (type === 'active') dot.classList.add('active');
else if (type === 'success') dot.classList.add('success');
else if (type === 'error') dot.classList.add('error');
}
function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
function generateWhitePngBlob() {
return new Promise((resolve) => {
const canvas = document.createElement('canvas'); canvas.width = 100; canvas.height = 100;
const ctx = canvas.getContext('2d'); ctx.fillStyle = '#FFFFFF'; ctx.fillRect(0, 0, 100, 100);
canvas.toBlob((blob) => { resolve(blob); }, 'image/png');
});
}
async function pasteGeneratedImage(element) {
try {
const blob = await generateWhitePngBlob();
const file = new File([blob], "gen.png", { type: "image/png" });
const dataTransfer = new DataTransfer(); dataTransfer.items.add(file);
const pasteEvent = new ClipboardEvent('paste', { bubbles: true, cancelable: true, clipboardData: dataTransfer });
element.focus(); element.dispatchEvent(pasteEvent); return true;
} catch (e) { return false; }
}
async function clickImageButton() {
const allButtons = document.querySelectorAll('button');
for (let btn of allButtons) { if (btn.innerText && btn.innerText.trim() === "Image") { btn.click(); return true; } }
return false;
}
function getSendButton() {
return document.querySelector('button[data-testid="send-button"]') || document.querySelector('button[aria-label="Send message"]') || document.querySelector('button[type="submit"]');
}
async function fillTextOnly(element, text) {
element.focus();
const valueSetter = Object.getOwnPropertyDescriptor(element, 'value')?.set;
const prototype = Object.getPrototypeOf(element);
const prototypeValueSetter = Object.getOwnPropertyDescriptor(prototype, 'value')?.set;
if (valueSetter && prototypeValueSetter && valueSetter !== prototypeValueSetter) prototypeValueSetter.call(element, text);
else if (valueSetter) valueSetter.call(element, text);
else element.value = text;
element.dispatchEvent(new Event('input', { bubbles: true }));
}
async function clickSend() {
startTimer();
const sendBtn = getSendButton();
if (sendBtn && !sendBtn.disabled) { sendBtn.click(); return true; }
else {
const textarea = document.querySelector('textarea');
if(textarea) textarea.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
return true;
}
}
function removeVotingBar() {
const targetDivs = document.querySelectorAll('.md\\:absolute.md\\:top-0.md\\:-translate-y-full');
targetDivs.forEach(div => {
const text = div.innerText;
if (text.includes('Left is Better') || text.includes('A is better') || text.includes('tie') || text.includes('Both are bad')) {
if (isRemoveVoteUI) {
if (div.style.display !== 'none') div.style.display = 'none';
} else {
if (div.style.display === 'none') div.style.display = '';
}
}
});
}
setInterval(removeVotingBar, 1000);
// --- 核心等待逻辑 ---
function waitForResponse() {
return new Promise(resolve => {
if (checkLoopInterval) clearInterval(checkLoopInterval);
const startTime = Date.now();
let seenLoading = false;
checkLoopInterval = setInterval(() => {
if (!isRunning) {
clearInterval(checkLoopInterval);
resolve("STOPPED");
return;
}
if (document.body.innerText.includes("Something went wrong") || document.body.innerText.includes("Failed to load")) {
clearInterval(checkLoopInterval); resolve("ERROR");
}
const hasLoading = isLoadingIndicatorVisible();
if (hasLoading) seenLoading = true;
if (seenLoading && !hasLoading) {
clearInterval(checkLoopInterval); stopTimer(); resolve("FOUND");
return;
}
const timePassed = Date.now() - startTime;
if (timePassed > 10000 && !hasLoading) {
const { A, B } = getModelResponses();
if (A.length > 10 && B.length > 10) {
clearInterval(checkLoopInterval); stopTimer(); resolve("FOUND");
return;
}
}
}, 100);
setTimeout(() => { clearInterval(checkLoopInterval); resolve("TIMEOUT"); }, 120000);
});
}
// --- 定位逻辑 ---
function findHeaderElement(text) {
const candidates = Array.from(document.querySelectorAll('span, h2, div, h3'));
return candidates.find(el => el.innerText && el.innerText.trim() === text && el.offsetParent !== null);
}
function getModelContainers() {
const headerA = findHeaderElement("Assistant A");
const headerB = findHeaderElement("Assistant B");
if (!headerA || !headerB) return { containerA: null, containerB: null };
function findColumnWrapper(selfHeader, otherHeader) {
let curr = selfHeader.parentElement;
let bestContainer = null;
for (let i = 0; i < 10 && curr && curr !== document.body; i++) {
if (curr.contains(otherHeader)) break;
bestContainer = curr;
curr = curr.parentElement;
}
return bestContainer;
}
return { containerA: findColumnWrapper(headerA, headerB), containerB: findColumnWrapper(headerB, headerA) };
}
function getModelResponses() {
const { containerA, containerB } = getModelContainers();
const bubbles = Array.from(document.querySelectorAll('.prose'));
let lastBubbleA = "";
let lastBubbleB = "";
if (!containerA || !containerB) return { A: "", B: "" };
bubbles.forEach(b => {
const text = b.innerText;
if (text.includes(CONFIG.prompt1) && text.length < 50) return;
if (text.includes("正在查找")) return;
if (containerA.contains(b)) lastBubbleA = text;
else if (containerB.contains(b)) lastBubbleB = text;
});
return { A: lastBubbleA || "", B: lastBubbleB || "" };
}
async function triggerExpand(side) {
if (!side) return;
if (side === 'BOTH') side = 'A';
const targetText = side === 'A' ? "Assistant A" : "Assistant B";
const headerEl = findHeaderElement(targetText);
if (headerEl) {
let parent = headerEl.parentElement;
for(let i=0; i<5 && parent; i++) {
const buttons = parent.querySelectorAll('button');
if (buttons.length >= 1) { buttons[buttons.length - 1].click(); return; }
parent = parent.parentElement;
}
}
}
function startPersistentHighlight() {
if (!lockedSide) return;
updateStatus("锁定: " + (lockedSide==='BOTH'?'双侧':lockedSide), 'success');
updateBallText(lockedSide);
document.title = `[${lockedSide}] 锁定成功 - GH`;
if (highlightInterval) clearInterval(highlightInterval);
highlightInterval = setInterval(() => {
const { containerA, containerB } = getModelContainers();
if (!containerA || !containerB) return;
const bubbles = Array.from(document.querySelectorAll('.prose'));
bubbles.forEach(b => {
const text = b.innerText;
if (text.includes(CONFIG.prompt1) && text.length < 50) return;
const isSideA = containerA.contains(b);
const isSideB = containerB.contains(b);
if (!isSideA && !isSideB) return;
let isTarget = false;
if (lockedSide === 'BOTH') isTarget = true;
else if (lockedSide === 'A' && isSideA) isTarget = true;
else if (lockedSide === 'B' && isSideB) isTarget = true;
let container = b.closest('.bg-surface-primary') ||
b.closest('.border.rounded-xl') ||
b.closest('.border.rounded-lg') ||
b.closest('[data-testid="model-answer"]');
if (!container && b.parentElement && b.parentElement.parentElement) {
container = b.parentElement.parentElement.parentElement;
}
let targetEl = container || b;
if (isTarget) {
if (!targetEl.classList.contains('gh-winner-glow')) targetEl.classList.add('gh-winner-glow');
} else {
if (targetEl.classList.contains('gh-winner-glow')) targetEl.classList.remove('gh-winner-glow');
}
if (isAutoHideOther && lockedSide !== 'BOTH') {
if ((lockedSide === 'A' && isSideB) || (lockedSide === 'B' && isSideA)) targetEl.classList.add('gh-hidden-bubble');
else targetEl.classList.remove('gh-hidden-bubble');
} else targetEl.classList.remove('gh-hidden-bubble');
});
}, 200);
if (chatMonitorInterval) clearInterval(chatMonitorInterval);
let lastBubbleCount = document.querySelectorAll('.prose').length;
chatMonitorInterval = setInterval(async () => {
if (!isAutoExpand) return;
const currentCount = document.querySelectorAll('.prose').length;
if (currentCount > lastBubbleCount) {
lastBubbleCount = currentCount;
await sleep(800);
triggerExpand(lockedSide);
}
}, 1000);
}
async function runSequence() {
if (!GM_getValue('isRunning', false)) return;
isRunning = true;
GM_deleteValue('gh_locked_side');
lockedSide = null;
if (chatMonitorInterval) clearInterval(chatMonitorInterval);
if (highlightInterval) clearInterval(highlightInterval);
updateStatus("准备中...", 'normal');
document.querySelectorAll('.gh-winner-glow').forEach(el => el.classList.remove('gh-winner-glow'));
document.querySelectorAll('.gh-hidden-bubble').forEach(el => el.classList.remove('gh-hidden-bubble'));
await sleep(1000);
if (!isRunning) return;
const textarea = document.querySelector('textarea, [contenteditable="true"]');
if (!textarea) { updateStatus("输入框未就绪", 'error'); return; }
updateStatus("验证 (Gemini)...", 'active');
await pasteGeneratedImage(textarea);
await sleep(2000);
if (!isRunning) return;
await clickImageButton();
await sleep(500);
await fillTextOnly(textarea, CONFIG.prompt1);
await sleep(300);
await clickSend();
updateStatus("等待回复...", 'active');
const voteResult1 = await waitForResponse();
if (voteResult1 === "STOPPED") return;
if (voteResult1 !== "FOUND") return retry();
updateStatus("判别中...", 'active');
await sleep(500);
const resp1 = getModelResponses();
const regex = new RegExp(CONFIG.keyword1, 'i');
const isA = regex.test(resp1.A);
const isB = regex.test(resp1.B);
let finalFound = false;
if (isA || isB) {
finalFound = true;
if (isA && isB) lockedSide = 'BOTH';
else if (isA) lockedSide = 'A';
else lockedSide = 'B';
}
if (finalFound) {
GM_setValue('gh_locked_side', lockedSide);
updateBallText(lockedSide);
const textareaDone = document.querySelector('textarea, [contenteditable="true"]');
if (textareaDone) await fillTextOnly(textareaDone, "");
startPersistentHighlight();
if(!isMinimized) toggleMinimizeUI();
if (isSoundEnabled) {
try { const u = new SpeechSynthesisUtterance(`锁定成功`); u.lang = 'zh-CN'; window.speechSynthesis.speak(u); } catch(e){}
playVictoryTheme();
}
GM_notification({ text: `成功锁定目标!位置:${lockedSide === 'BOTH' ? '双侧' : lockedSide + '侧'}`, title: 'Gemini Hunter', timeout: 5000 });
GM_setValue('isRunning', false);
sessionStorage.removeItem('gh_session_active');
isRunning = false;
updateUIState(false);
} else {
updateStatus("未发现,重试...", 'error');
await sleep(1000);
return retry();
}
}
// 【修复3】点击开始时增加延时,确保数据写入手机存储后再跳转
function startHunt() {
GM_deleteValue('gh_locked_side');
lockedSide = null;
keepAliveAudio();
sessionStorage.setItem('gh_session_active', 'true');
GM_setValue('isRunning', true);
isRunning = true;
updateBallText(null);
updateUIState(true);
updateStatus("初始化...", 'active');
// 增加500ms延时,防止脚本状态未保存就被浏览器杀掉
setTimeout(() => {
window.location.href = CONFIG.resetUrl;
}, 500);
}
function stopHunt() {
GM_deleteValue('gh_locked_side');
lockedSide = null;
GM_setValue('isRunning', false);
sessionStorage.removeItem('gh_session_active');
isRunning = false;
if (timerInterval) clearInterval(timerInterval);
if (chatMonitorInterval) clearInterval(chatMonitorInterval);
if (highlightInterval) clearInterval(highlightInterval);
if (checkLoopInterval) clearInterval(checkLoopInterval);
timerActive = false;
document.title = originalTitle;
updateUIState(false);
updateStatus("已停止", 'normal');
document.querySelectorAll('.gh-winner-glow').forEach(el => el.classList.remove('gh-winner-glow'));
document.querySelectorAll('.gh-hidden-bubble').forEach(el => el.classList.remove('gh-hidden-bubble'));
updateBallText(null);
}
function retry() {
if (!GM_getValue('isRunning', false) || !isRunning) {
updateStatus("已停止", 'normal');
return;
}
window.location.href = CONFIG.resetUrl;
}
function updateUIState(running) {
if (running) {
btnStart.style.display = 'none'; btnStop.style.display = 'flex'; ball.classList.add('running');
} else {
btnStart.style.display = 'flex'; btnStop.style.display = 'none'; ball.classList.remove('running');
}
}
btnStart.addEventListener('click', startHunt);
btnStart.addEventListener('touchend', (e) => { e.preventDefault(); startHunt(); });
btnStop.addEventListener('click', stopHunt);
btnStop.addEventListener('touchend', (e) => { e.preventDefault(); stopHunt(); });
// --- 启动逻辑 ---
if (lockedSide) {
if (!isMinimized) toggleMinimizeUI();
updateBallText(lockedSide);
startPersistentHighlight();
}
else {
updateBallText(null);
if (isMinimized) {
panel.style.display = 'none'; ball.style.display = 'flex';
}
else {
panel.style.opacity = '0'; panel.style.transform = 'scale(0.9)';
setTimeout(() => { panel.style.opacity = '1'; panel.style.transform = 'scale(1)'; }, 100);
}
}
if (isRunning && !lockedSide && sessionActive) {
updateUIState(true);
keepAliveAudio();
setTimeout(runSequence, 2000);
} else if (!sessionActive && isRunning) {
updateUIState(false);
GM_setValue('isRunning', false);
}
})();