您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
一个可通过AI判断页面内容类型、可拖动、能对选中和悬停文字做出即时响应,并具备智能学习提醒(全屏警句)功能的AI看板娘。超时会受到递增惩罚!
// ==UserScript== // @name AI场景感知助手 // @namespace http://tampermonkey.net/ // @version 2.8 // @description 一个可通过AI判断页面内容类型、可拖动、能对选中和悬停文字做出即时响应,并具备智能学习提醒(全屏警句)功能的AI看板娘。超时会受到递增惩罚! // @author REI // @match *://*/* // @grant GM_xmlhttpRequest // @grant GM_getValue // @grant GM_setValue // @grant GM_deleteValue // @grant GM_registerMenuCommand // @connect * // @license MIT // ==/UserScript== (function() { 'use strict'; // --- 配置 (默认值) --- let config = { apiEndpoint: 'http://127.0.0.1:52112/v1/chat/completions', model: 'DeepSeek-V3-Fast', bubbleDuration: 6000, triggerCooldown: 2000, apiKey: '', isEnabled: true, // --- 学习提醒配置 --- learningReminderEnabled: true, forcedNonLearningSites: ['weibo.com', 'douyin.com'], maxStayDuration: 2, // 单次摸鱼时间(分钟) maxHourlyDuration: 10, // 每小时摸鱼总时长(分钟) }; // --- 全局状态 --- let state = { isThinking: false, isBubbleVisible: false, lastTriggerTime: 0, isDragging: false, didMove: false, dragOffsetX: 0, dragOffsetY: 0, lastHoveredElement: null, isTimerRunning: false, isLearningSite: false, // 【核心修复】新增计时器ID,用于管理和取消异步任务 typingTimer: null, hideBubbleTimer: null, }; // --- UI元素 --- const ui = { container: null, character: null, characterStatus: null, bubbleWrapper: null, bubble: null, bubbleContent: null, fullScreenOverlay: null, }; /** * 加载存储的配置 */ function loadConfig() { config.apiEndpoint = GM_getValue('ai_assistant_endpoint', config.apiEndpoint); config.model = GM_getValue('ai_assistant_model', config.model); config.apiKey = GM_getValue('ai_assistant_apikey', config.apiKey); config.isEnabled = GM_getValue('ai_assistant_enabled', config.isEnabled); config.learningReminderEnabled = GM_getValue('learning_reminder_enabled', config.learningReminderEnabled); try { config.forcedNonLearningSites = JSON.parse(GM_getValue('learning_forced_sites', JSON.stringify(config.forcedNonLearningSites))); } catch (e) { config.forcedNonLearningSites = []; } config.maxStayDuration = GM_getValue('learning_max_stay_duration', config.maxStayDuration); config.maxHourlyDuration = GM_getValue('learning_max_hourly_duration', config.maxHourlyDuration); } /** * 注册油猴菜单命令 */ function registerMenuCommands() { GM_registerMenuCommand('设置 API Key', () => { const newKey = prompt('请输入新的 API Key:', config.apiKey); if (newKey !== null) { config.apiKey = newKey; GM_setValue('ai_assistant_apikey', newKey); if(ui.container) showMessage('API Key 已更新!', true); } }); GM_registerMenuCommand('设置 API Endpoint', () => { const newEndpoint = prompt('请输入新的 API Endpoint:', config.apiEndpoint); if (newEndpoint !== null) { config.apiEndpoint = newEndpoint; GM_setValue('ai_assistant_endpoint', newEndpoint); if(ui.container) showMessage('API Endpoint 已更新!', true); } }); GM_registerMenuCommand('设置模型名称', () => { const newModel = prompt('请输入新的模型名称:', config.model); if (newModel !== null) { config.model = newModel; GM_setValue('ai_assistant_model', newModel); if(ui.container) showMessage('模型已更新!', true); } }); const toggleText = `看板娘: ${config.isEnabled ? '✅ 已启用' : '❌ 已禁用'} (点击切换)`; GM_registerMenuCommand(toggleText, () => { GM_setValue('ai_assistant_enabled', !config.isEnabled); location.reload(); }); GM_registerMenuCommand('--- 学习提醒设置 ---', () => {}); const reminderToggleText = `学习提醒: ${config.learningReminderEnabled ? '✅ 已启用' : '❌ 已禁用'} (点击切换)`; GM_registerMenuCommand(reminderToggleText, () => { GM_setValue('learning_reminder_enabled', !config.learningReminderEnabled); location.reload(); }); GM_registerMenuCommand('设置强制摸鱼网站 (跳过AI)', () => { const sites = prompt('以下网站将始终被视为摸鱼网站(用英文逗号,隔开):', config.forcedNonLearningSites.join(',')); if (sites !== null) { const siteArray = sites.split(',').map(s => s.trim().toLowerCase()).filter(Boolean); GM_setValue('learning_forced_sites', JSON.stringify(siteArray)); config.forcedNonLearningSites = siteArray; if(ui.container) showMessage('强制摸鱼网站列表已更新!', true); } }); GM_registerMenuCommand('设置单次摸鱼时长(分钟)', () => { const duration = parseInt(prompt('请输入在单个网站上允许停留的最长时间(分钟):', config.maxStayDuration), 10); if (!isNaN(duration) && duration > 0) { GM_setValue('learning_max_stay_duration', duration); config.maxStayDuration = duration; if(ui.container) showMessage('单次摸鱼时长已更新!', true); } }); GM_registerMenuCommand('设置每小时摸鱼总时长(分钟)', () => { const duration = parseInt(prompt('请输入每小时允许的总摸鱼时间(分钟):', config.maxHourlyDuration), 10); if (!isNaN(duration) && duration > 0) { GM_setValue('learning_max_hourly_duration', duration); config.maxHourlyDuration = duration; if(ui.container) showMessage('每小时摸鱼总时长已更新!', true); } }); GM_registerMenuCommand('清除AI页面分类缓存', () => { GM_deleteValue('ai_page_classification_cache'); if(ui.container) showMessage('AI页面分类缓存已清除!', true); location.reload(); }); } /** * 初始化函数 */ function init() { loadConfig(); registerMenuCommands(); if (!config.isEnabled) { console.log('AI看板娘已禁用。'); return; } createUI(); applyStyles(); loadPosition(); bindEvents(); initLearningReminder(); console.log('AI看板娘已启动 ✨'); } /** * 创建DOM元素 */ function createUI() { ui.container = document.createElement('div'); ui.container.id = 'ai-kanban-container'; document.body.appendChild(ui.container); ui.container.innerHTML = ` <div id="ai-bubble-wrapper"> <div id="ai-bubble"> <div class="bubble-content"></div> </div> </div> <div id="ai-character"> <div id="ai-character-status"></div> </div> `; ui.character = document.getElementById('ai-character'); ui.characterStatus = document.getElementById('ai-character-status'); ui.bubbleWrapper = document.getElementById('ai-bubble-wrapper'); ui.bubble = document.getElementById('ai-bubble'); ui.bubbleContent = ui.bubble.querySelector('.bubble-content'); } /** * 应用CSS样式 */ function applyStyles() { const styles = ` #ai-kanban-container { position: fixed; z-index: 99999; } #ai-character { width: 70px; height: 70px; background: linear-gradient(145deg, #89f7fe 0%, #66a6ff 100%); border-radius: 50%; cursor: grab; transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275), box-shadow 0.3s ease; box-shadow: 0 5px 15px rgba(0, 123, 255, 0.3); border: 3px solid rgba(255, 255, 255, 0.5); position: relative; display: flex; justify-content: center; align-items: center; overflow: hidden; } #ai-character-status { color: white; font-size: 13px; font-weight: bold; text-shadow: 0 0 5px rgba(0, 0, 0, 0.5); line-height: 1.2; text-align: center; padding: 2px; user-select: none; } #ai-character:active { cursor: grabbing; transform: scale(1.1); } #ai-character.thinking { animation: thinking-sway 1.2s ease-in-out infinite; } @keyframes thinking-sway { 0%, 100% { transform: translateX(0) rotate(0); } 25% { transform: translateX(-3px) rotate(-2deg); } 75% { transform: translateX(3px) rotate(2deg); } } #ai-bubble-wrapper { position: absolute; width: 300px; opacity: 0; transform: translateY(20px) scale(0.9); pointer-events: none; transition: opacity 0.4s ease, transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1); } #ai-bubble-wrapper.on-right { bottom: 50%; left: 100%; transform: translate(15px, 50%); } #ai-bubble-wrapper.on-left { bottom: 50%; right: 100%; transform: translate(-15px, 50%); } #ai-bubble-wrapper.visible { opacity: 1; transform: translate(var(--tx, 15px), 50%) scale(1); animation: pop-in 0.5s cubic-bezier(0.68, -0.55, 0.27, 1.55) forwards; } #ai-bubble-wrapper.on-left.visible { --tx: -15px; } #ai-bubble-wrapper.on-right.visible { --tx: 15px; } @keyframes pop-in { 0% { transform: scale(0.8); opacity: 0; } 70% { transform: scale(1.05); opacity: 1; } 100% { transform: scale(1); opacity: 1; } } #ai-bubble { background: rgba(255, 255, 255, 0.85); backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px); border-radius: 20px; padding: 15px 20px; box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.15); border: 1px solid rgba(255, 255, 255, 0.2); position: relative; } #ai-bubble::after { content: ''; position: absolute; top: 50%; width: 0; height: 0; border-top: 10px solid transparent; border-bottom: 10px solid transparent; } .on-right #ai-bubble::after { right: 100%; transform: translateY(-50%); border-right: 10px solid rgba(255, 255, 255, 0.85); } .on-left #ai-bubble::after { left: 100%; transform: translateY(-50%); border-left: 10px solid rgba(255, 255, 255, 0.85); } .bubble-content { font-size: 15px; line-height: 1.5; color: #333; word-wrap: break-word; } .typing-cursor { display: inline-block; width: 2px; height: 1em; background-color: #007bff; animation: blink 0.7s infinite; vertical-align: middle; } @keyframes blink { 0%, 100% { opacity: 1 } 50% { opacity: 0 } } /* 全屏提醒样式 */ #ai-fullscreen-overlay { position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background-color: rgba(0, 0, 0, 0.85); backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px); z-index: 999999; display: flex; justify-content: center; align-items: center; opacity: 0; animation: fadeIn 0.5s forwards; } @keyframes fadeIn { to { opacity: 1; } } .overlay-content { max-width: 600px; text-align: center; color: white; padding: 40px; background: rgba(255, 255, 255, 0.1); border-radius: 20px; border: 1px solid rgba(255, 255, 255, 0.2); transform: scale(0.9); animation: pop-in-overlay 0.5s 0.3s forwards cubic-bezier(0.175, 0.885, 0.32, 1.275); } @keyframes pop-in-overlay { to { transform: scale(1); } } .overlay-reason { font-size: 18px; margin-bottom: 20px; color: #ffc107; } .overlay-quote { font-family: 'KaiTi', 'STKaiti', serif; font-size: 28px; line-height: 1.6; margin-bottom: 30px; min-height: 70px; } .overlay-close-button { background: #ffc107; color: #333; border: none; padding: 10px 25px; border-radius: 50px; cursor: pointer; font-size: 16px; transition: background-color 0.3s, transform 0.2s, opacity 0.3s; } .overlay-close-button:hover:not(:disabled) { background-color: #ffca2c; transform: scale(1.05); } .overlay-close-button:disabled { background-color: #6c757d; cursor: not-allowed; opacity: 0.7; } `; const styleSheet = document.createElement('style'); styleSheet.textContent = styles; document.head.appendChild(styleSheet); } /** * 绑定事件 */ function bindEvents() { ui.character.addEventListener('mousedown', (e) => { e.preventDefault(); state.isDragging = true; state.didMove = false; state.dragOffsetX = e.clientX - ui.container.offsetLeft; state.dragOffsetY = e.clientY - ui.container.offsetTop; document.addEventListener('mousemove', onMouseMove); document.addEventListener('mouseup', onMouseUp); }); function onMouseMove(e) { if (!state.isDragging) return; state.didMove = true; let newX = e.clientX - state.dragOffsetX; let newY = e.clientY - state.dragOffsetY; const containerRect = ui.container.getBoundingClientRect(); newX = Math.max(0, Math.min(newX, window.innerWidth - containerRect.width)); newY = Math.max(0, Math.min(newY, window.innerHeight - containerRect.height)); ui.container.style.left = `${newX}px`; ui.container.style.top = `${newY}px`; } function onMouseUp() { if (!state.isDragging) return; state.isDragging = false; document.removeEventListener('mousemove', onMouseMove); document.removeEventListener('mouseup', onMouseUp); savePosition(ui.container.offsetLeft, ui.container.offsetTop); if (!state.didMove) { triggerAnalysis('manual_click'); } } document.addEventListener('click', (e) => { if (ui.container.contains(e.target)) return; if (Math.random() < 0.2) triggerAnalysis('random_click'); }); let scrollTimeout; document.addEventListener('scroll', () => { clearTimeout(scrollTimeout); scrollTimeout = setTimeout(() => { if (Math.random() < 0.15) triggerAnalysis('scroll'); }, 500); }); document.addEventListener('mouseover', (e) => { state.lastHoveredElement = e.target; }); let mouseMoveTimeout; document.addEventListener('mousemove', () => { if (state.isDragging) return; clearTimeout(mouseMoveTimeout); mouseMoveTimeout = setTimeout(() => { if (Math.random() < 0.25) { triggerAnalysis('mouse_stop'); } }, 2000); }); document.addEventListener('mouseup', (e) => { if (ui.container.contains(e.target) || state.isDragging) return; setTimeout(() => { const selectedText = window.getSelection().toString().trim(); if (selectedText.length > 0) { triggerAnalysis('selection'); } }, 600); }); } function savePosition(x, y) { GM_setValue('ai_assistant_pos', JSON.stringify({ x, y })); } function loadPosition() { const pos = JSON.parse(GM_getValue('ai_assistant_pos', 'null')); if (pos) { ui.container.style.left = `${pos.x}px`; ui.container.style.top = `${pos.y}px`; } else { ui.container.style.right = '30px'; ui.container.style.bottom = '30px'; ui.container.style.left = 'auto'; ui.container.style.top = 'auto'; } } function updateCharacterStatus(text) { if (ui.characterStatus) { ui.characterStatus.innerHTML = text; } } function adjustBubblePosition() { const characterRect = ui.container.getBoundingClientRect(); const screenCenter = window.innerWidth / 2; ui.bubbleWrapper.classList.remove('on-left', 'on-right'); if (characterRect.left < screenCenter) { ui.bubbleWrapper.classList.add('on-right'); } else { ui.bubbleWrapper.classList.add('on-left'); } } /** * 【核心修复】重构了 showMessage 函数,以正确处理中断和连续调用 * @param {string} message - 要显示的消息 * @param {boolean} isForce - 是否为强制消息 (例如,系统通知) */ function showMessage(message, isForce = false) { // 1. 清理旧状态:在显示新消息之前,立即停止任何正在进行的打字动画和计划中的隐藏任务。 clearTimeout(state.typingTimer); clearTimeout(state.hideBubbleTimer); // 2. 准备新消息 ui.bubbleContent.innerHTML = ''; // 清空内容 adjustBubblePosition(); ui.bubbleWrapper.classList.add('visible'); state.isBubbleVisible = true; // 3. 开始新的打字动画 typeWriter(message, ui.bubbleContent, () => { // 打字结束后,计划一个新的隐藏任务,并将计时器ID保存起来 const duration = isForce ? config.bubbleDuration * 1.5 : config.bubbleDuration; state.hideBubbleTimer = setTimeout(hideBubble, duration); }); } /** * 【核心修复】修改了 hideBubble 函数,使其在隐藏时也清理计时器 */ function hideBubble() { if (!state.isBubbleVisible) return; ui.bubbleWrapper.classList.remove('visible'); state.isBubbleVisible = false; // 清理工作,确保没有残留的计时器 clearTimeout(state.typingTimer); clearTimeout(state.hideBubbleTimer); } /** * 【核心修复】修改了 typeWriter 函数,使其将计时器ID存入全局状态 * @param {string} text - 要打字的文本 * @param {HTMLElement} element - 目标元素 * @param {function} callback - 完成后的回调函数 */ function typeWriter(text, element, callback) { let i = 0; const cursor = document.createElement('span'); cursor.className = 'typing-cursor'; element.appendChild(cursor); function type() { if (i < text.length) { element.insertBefore(document.createTextNode(text.charAt(i)), cursor); i++; // 将计时器ID保存到state中,以便可以从外部取消它 state.typingTimer = setTimeout(type, Math.random() * 80 + 50); } else { cursor.remove(); state.typingTimer = null; // 清空计时器ID if (callback) callback(); } } type(); } async function triggerAnalysis(triggerType) { if (state.isLearningSite) { return; } const now = Date.now(); const isSelectionTrigger = triggerType === 'selection'; if (state.isThinking || state.isDragging) return; if (!isSelectionTrigger && (now - state.lastTriggerTime < config.triggerCooldown)) return; if (!config.apiKey) { showMessage("请先在油猴菜单中设置API Key", true); return; } state.isThinking = true; state.lastTriggerTime = now; ui.character.classList.add('thinking'); updateCharacterStatus('思考中🤔'); try { const pageContext = extractPageContext(); const prompt = createWittyPrompt(pageContext, triggerType); const wittyRemark = await callAIAPI(prompt); if (wittyRemark) showMessage(wittyRemark); } catch (error) { console.error('AI看板娘出错了:', error); showMessage("呀,智慧消失了..."); } finally { state.isThinking = false; ui.character.classList.remove('thinking'); if (!config.learningReminderEnabled) { updateCharacterStatus('😊'); } else { if (state.isLearningSite) { updateCharacterStatus('学习中📚'); } else { updateCharacterStatus('摸鱼中'); } } } } function extractPageContext() { let hoveredText = ''; if (state.lastHoveredElement && state.lastHoveredElement.innerText) { const validTags = ['P', 'H1', 'H2', 'H3', 'H4', 'A', 'SPAN', 'DIV', 'LI', 'BUTTON', 'TD', 'BLOCKQUOTE', 'STRONG', 'EM']; if (validTags.includes(state.lastHoveredElement.tagName.toUpperCase())) { hoveredText = state.lastHoveredElement.innerText.trim().substring(0, 300); } } return { title: document.title, url: window.location.href, selection: window.getSelection().toString().trim(), hoveredText: hoveredText, pageText: document.body.innerText.substring(0, 4000) }; } function createWittyPrompt(context, trigger) { let actionDescription = "正在浏览页面"; let contextDetails = `- 页面标题: "${context.title}"`; if (trigger === 'selection' && context.selection) { actionDescription = `刚刚选择了一段文字`; contextDetails += `\n- 选择的文本: "${context.selection.substring(0, 500)}"`; } else if (trigger === 'mouse_stop' && context.hoveredText) { actionDescription = `的鼠标停留在了这段内容上`; contextDetails += `\n- 悬停处的文本: "${context.hoveredText}"`; } if (trigger !== 'selection' && trigger !== 'mouse_stop' && context.pageText) { contextDetails += `\n\n# 供参考的页面内容概览:\n${context.pageText.substring(0, 1000)}`; } return `你是一个网站的AI看板娘,性格俏皮可爱,有点小毒舌。你的任务是观察用户的行为和当前页面内容,然后用一句简短、有趣的俏皮话来吐槽或评论,***尽量避免问句***,要正能量,。\n\n# 规则:\n- 必须只回复一句话。\n- 语言风格要活泼、自然,像朋友一样。\n- 长度严格控制在30个字以内。\n- 直接返回俏皮话,不要包含任何其他说明或符号**尽量避免问句***。\n\n# 当前情景:\n- 用户行为: 用户${actionDescription}\n${contextDetails}\n\n请根据以上情景,表达你的观点(能锐评的地方锐评一下):`; } function callAIAPI(prompt, temperature = 0.8, max_tokens = 100) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'POST', url: config.apiEndpoint, headers: { 'Authorization': `Bearer ${config.apiKey}`, 'Content-Type': 'application/json' }, data: JSON.stringify({ model: config.model, messages: [{ role: 'user', content: prompt }], temperature: temperature, max_tokens: max_tokens, }), onload: (response) => { if (response.status >= 200 && response.status < 300) { try { const data = JSON.parse(response.responseText); const content = data.choices[0]?.message?.content?.trim(); if (content) resolve(content); else reject(new Error('AI返回了无效的内容')); } catch (e) { reject(new Error('解析AI响应失败')); } } else { reject(new Error(`API请求失败: ${response.status} - ${response.responseText}`)); } }, onerror: (e) => reject(new Error('网络请求错误')), ontimeout: () => reject(new Error('请求超时')), }); }); } // --- 学习提醒功能 (AI内容感知 + 全屏提醒版) --- const TRACKING_INTERVAL = 5 * 1000; function createClassificationPrompt(context) { return `你是一个效率助手,任务是判断一个网页的主要用途。请优先根据【页面内容】来判断,【标题】和【网址】仅作辅助参考。 判断该网页是否主要用于“学习”或“工作”。 - **学习/工作网站**: 在线课程、学术搜索等。 - **摸鱼网站**: 视频娱乐、社交媒体、新闻八卦、在线购物、游戏、小说漫画等。 - **提示**: 只要不相关数学、英语、政治、电路的大概率都算摸鱼。 - **附加规则**: 一些技术类论坛、科技讨论、工具、代码编程等也强制算作摸鱼!!!。 - **常驻页面**:www.sophnet.com不是摸鱼网站!!! **重要规则:** 1. 如果判断为**学习/工作**网站,**只回答 "NO"**。 2. 如果判断为**摸鱼**网站,**只回答 "YES"**。 3. 不要有任何其他解释或文字。 **示例:** - 网址是 bilibili.com, 标题是“Python入门教程”,内容是关于“变量、循环、函数”,你应该回答 "NO"。 - 网址是 bilibili.com, 标题是“年度搞笑视频集锦”,内容是“哈哈哈哈”,你应该回答 "YES"。 --- # 待判断的页面信息: - **网址**: ${context.url} - **标题**: ${context.title} - **页面主要内容节选**: "${(context.pageText || '无内容').substring(0, 2000)}" --- 你的回答 (YES/NO):`; } async function getAIClassification() { if (state.isThinking) return null; state.isThinking = true; ui.character.classList.add('thinking'); updateCharacterStatus('分析中...'); showMessage("正在分析页面类型...", true); try { const context = extractPageContext(); const prompt = createClassificationPrompt(context); const response = await callAIAPI(prompt, 0.1, 5); // hideBubble() 会在 showMessage 中自动处理,这里可以省略 if (response && response.toUpperCase().includes('YES')) { return true; } if (response && response.toUpperCase().includes('NO')) { return false; } return null; } catch (error) { console.error("AI页面分类失败:", error); showMessage("AI分析失败,暂时不计时啦。", true); return null; } finally { state.isThinking = false; ui.character.classList.remove('thinking'); } } function startTrackingTimer() { if (state.isTimerRunning) return; state.isTimerRunning = true; console.log('AI看板娘: 已启动摸鱼计时器。'); checkAndUpdateTime(); setInterval(checkAndUpdateTime, TRACKING_INTERVAL); } async function initLearningReminder() { if (!config.learningReminderEnabled) { updateCharacterStatus('😊'); return; } if (!config.apiKey) { console.warn("学习提醒功能需要API Key才能进行AI页面分类。"); updateCharacterStatus('发呆中'); return; } const currentHost = window.location.hostname; let isNonLearning; if (config.forcedNonLearningSites.some(site => currentHost.includes(site))) { console.log(`AI看板娘: ${currentHost} 在强制摸鱼列表中。`); isNonLearning = true; } else { let classificationCache = JSON.parse(GM_getValue('ai_page_classification_cache', '{}')); const cacheKey = window.location.href; if (classificationCache.hasOwnProperty(cacheKey)) { isNonLearning = classificationCache[cacheKey]; console.log(`AI看板娘: 根据缓存, 此页面是 ${isNonLearning ? '摸鱼' : '学习'} 网站。`); } else { isNonLearning = await getAIClassification(); if (isNonLearning !== null) { classificationCache[cacheKey] = isNonLearning; const keys = Object.keys(classificationCache); if (keys.length > 50) { delete classificationCache[keys[0]]; } GM_setValue('ai_page_classification_cache', JSON.stringify(classificationCache)); } } } if (isNonLearning === true) { state.isLearningSite = false; updateCharacterStatus('摸鱼中'); showMessage(`我认为你在摸鱼,计时开始!`, true); startTrackingTimer(); } else if (isNonLearning === false) { state.isLearningSite = true; updateCharacterStatus('学习中📚'); showMessage(`加油!`, true); GM_deleteValue('learning_current_site_stay'); } else { // isNonLearning is null (AI failed) state.isLearningSite = false; updateCharacterStatus('发呆中'); // 此处的 showMessage 已经在 getAIClassification 的 catch 块中处理了 } } function checkAndUpdateTime() { const now = Date.now(); const oneHourAgo = now - 60 * 60 * 1000; let hourlyLog = JSON.parse(GM_getValue('learning_hourly_log', '[]')); hourlyLog = hourlyLog.filter(timestamp => timestamp > oneHourAgo); const lastLogTime = hourlyLog.length > 0 ? hourlyLog[hourlyLog.length - 1] : 0; if (now - lastLogTime < (TRACKING_INTERVAL - 1000)) { const totalMinutesWithoutUpdate = (hourlyLog.length * TRACKING_INTERVAL) / (1000 * 60); const timerText = `${Math.floor(totalMinutesWithoutUpdate)} / ${config.maxHourlyDuration}m`; updateCharacterStatus(timerText); return; } hourlyLog.push(now); GM_setValue('learning_hourly_log', JSON.stringify(hourlyLog)); const totalMinutes = (hourlyLog.length * TRACKING_INTERVAL) / (1000 * 60); const timerText = `${Math.floor(totalMinutes)} / ${config.maxHourlyDuration}m`; updateCharacterStatus(timerText); if (totalMinutes > config.maxHourlyDuration) { const overtimeMinutes = totalMinutes - config.maxHourlyDuration; triggerLearningReminder('hourly', overtimeMinutes); return; } let siteStayInfo = JSON.parse(GM_getValue('learning_current_site_stay', '{}')); const currentHost = window.location.hostname; if (siteStayInfo.host !== currentHost) { siteStayInfo = { host: currentHost, startTime: now }; } GM_setValue('learning_current_site_stay', JSON.stringify(siteStayInfo)); const stayMinutes = (now - siteStayInfo.startTime) / (1000 * 60); if (stayMinutes > config.maxStayDuration) { const overtimeMinutes = stayMinutes - config.maxStayDuration; triggerLearningReminder('stay', overtimeMinutes); } } function createInspirationalQuotePrompt(reason) { const reasonText = reason === 'stay' ? '在同一个娱乐网站停留太久' : '一小时内娱乐总时间超标'; return `你是一位智慧的导师,请针对以下场景,生成一句简短、有力、引人深思的名言警句,风格可以是哲学性的、现代的或略带幽默的,但核心是激励人专注和自律。 要求: 1. 直接返回名言本身,不要包含任何诸如 “好的,这是一句...” 之类的多余解释。 2. 语言为中文。 3. 长度在15到30字之间。 4. 用户在备战考研,请激励他 场景:用户因为【${reasonText}】而分心了。 名言警句:`; } async function showFullScreenReminder(reason, overtimeMinutes = 0) { if (ui.fullScreenOverlay) return; const buttonDelaySeconds = Math.min(60, 5 + Math.floor(overtimeMinutes)); ui.fullScreenOverlay = document.createElement('div'); ui.fullScreenOverlay.id = 'ai-fullscreen-overlay'; const reasonText = reason === 'stay' ? `你在当前网站摸鱼已超过 ${config.maxStayDuration} 分钟` : `近一小时摸鱼已超过 ${config.maxHourlyDuration} 分钟`; ui.fullScreenOverlay.innerHTML = ` <div class="overlay-content"> <div class="overlay-reason">${reasonText} (已超时 ${Math.round(overtimeMinutes)} 分钟)</div> <div class="overlay-quote">正在生成警句...</div> <button class="overlay-close-button" disabled>不如不等 (${buttonDelaySeconds}s)</button> </div> `; document.body.appendChild(ui.fullScreenOverlay); const closeButton = ui.fullScreenOverlay.querySelector('.overlay-close-button'); const quoteElement = ui.fullScreenOverlay.querySelector('.overlay-quote'); let countdown = buttonDelaySeconds; const countdownInterval = setInterval(() => { countdown--; if (countdown > 0) { closeButton.textContent = `不如不等 (${countdown}s)`; } else { clearInterval(countdownInterval); closeButton.disabled = false; closeButton.textContent = '不如不等'; } }, 1000); const closeOverlay = () => { if (ui.fullScreenOverlay) { clearInterval(countdownInterval); ui.fullScreenOverlay.remove(); ui.fullScreenOverlay = null; document.removeEventListener('keydown', onKeydown); } }; const onKeydown = (e) => { if (e.key === 'Escape' && !closeButton.disabled) { closeOverlay(); } }; closeButton.addEventListener('click', closeOverlay); document.addEventListener('keydown', onKeydown); try { const prompt = createInspirationalQuotePrompt(reason); const quote = await callAIAPI(prompt, 0.9, 60); quoteElement.textContent = `“ ${quote} ”`; } catch (error) { console.error('生成警句失败:', error); quoteElement.textContent = "“ 别让短暂的愉悦,偷走你未来的成就。”"; } } function triggerLearningReminder(reason, overtimeMinutes = 0) { const now = Date.now(); const lastReminderTime = GM_getValue('learning_last_reminder_time', 0); const reminderCooldown = Math.max(30 * 1000, (300 - overtimeMinutes * 10) * 1000); if (now - lastReminderTime < reminderCooldown) return; GM_setValue('learning_last_reminder_time', now); showFullScreenReminder(reason, overtimeMinutes); } // --- 启动脚本 --- if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();