// ==UserScript== // @name 全能网页悬浮助手 (支持另存为/导出) // @namespace https://scriptcat.org/ // @version 1.3 // @description 新增笔记导出与截图另存为功能,支持手动选择保存路径 // @author You // @match *://*/* // @grant GM_setValue // @grant GM_getValue // @grant GM_xmlhttpRequest // @connect * // @run-at document-idle // ==/UserScript== (function () { 'use strict'; if (window.top !== window.self) return; if (document.getElementById('cyber-assistant-host')) return; // ==================== 创建 Shadow DOM ==================== const host = document.createElement('div'); host.id = 'cyber-assistant-host'; host.style.cssText = 'position:fixed; top:0; left:0; width:0; height:0; z-index: 2147483647;'; document.documentElement.appendChild(host); const shadow = host.attachShadow({ mode: 'open' }); // ==================== 注入 CSS 样式 ==================== const style = document.createElement('style'); style.textContent = ` :host { all: initial; } * { box-sizing: border-box; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; user-select: none; } .hidden { opacity: 0 !important; pointer-events: none !important; } /* 悬浮球区域 */ #ball-wrapper { position: fixed; bottom: 50px; right: 30px; display: flex; flex-direction: column; align-items: center; gap: 6px; z-index: 10000; } #ball-time { font-size: 12px; font-weight: bold; color: #00d2ff; font-family: monospace; background: rgba(16, 23, 38, 0.85); padding: 2px 8px; border-radius: 10px; border: 1px solid rgba(0, 210, 255, 0.3); pointer-events: none; box-shadow: 0 2px 10px rgba(0,0,0,0.3); } #float-ball { width: 50px; height: 50px; background: rgba(16, 23, 38, 0.85); backdrop-filter: blur(10px); border: 1px solid rgba(0, 210, 255, 0.4); border-radius: 50%; color: #00d2ff; font-size: 24px; display: flex; justify-content: center; align-items: center; cursor: grab; box-shadow: 0 4px 15px rgba(0,0,0,0.5); transition: all 0.2s; } #float-ball:hover { transform: scale(1.05); box-shadow: 0 0 15px rgba(0, 210, 255, 0.6); } #float-ball:active { cursor: grabbing; } /* 主面板 */ #panel { position: fixed; width: 340px; height: 520px; background: rgba(7, 11, 20, 0.85); backdrop-filter: blur(12px); border: 1px solid rgba(0, 210, 255, 0.3); border-radius: 16px; box-shadow: 0 10px 30px rgba(0,0,0,0.7); display: none; flex-direction: column; overflow: hidden; z-index: 9999; color: #e0e6ed; transition: opacity 0.3s; } #panel.open { display: flex; } /* 导航栏 */ .nav-tabs { display: flex; border-bottom: 1px solid rgba(0, 210, 255, 0.15); background: rgba(16, 23, 38, 0.5); } .nav-item { flex: 1; padding: 10px 0; text-align: center; font-size: 12px; cursor: pointer; color: #8a9bb8; transition: all 0.2s; } .nav-item.active { color: #00d2ff; border-bottom: 2px solid #00d2ff; background: rgba(0, 210, 255, 0.05); } .nav-item:hover { color: #00d2ff; } /* 内容区 */ .content-area { flex: 1; padding: 15px; overflow-y: auto; position: relative; } .tab-pane { display: none; height: 100%; flex-direction: column; } .tab-pane.active { display: flex; } /* 通用输入框和按钮 */ input { width: 100%; background: rgba(16, 23, 38, 0.8); border: 1px solid rgba(0, 210, 255, 0.2); color: #e0e6ed; border-radius: 8px; padding: 8px 12px; outline: none; font-size: 13px; transition: border 0.2s; user-select: text; } input:focus { border-color: #00d2ff; box-shadow: 0 0 8px rgba(0, 210, 255, 0.3); } button { background: linear-gradient(135deg, #0055ff, #00d2ff); color: #fff; border: none; border-radius: 8px; padding: 8px 15px; cursor: pointer; font-weight: bold; font-size: 13px; box-shadow: 0 0 10px rgba(0, 210, 255, 0.4); transition: all 0.2s; } button:hover { box-shadow: 0 0 15px rgba(0, 210, 255, 0.7); transform: translateY(-1px); } /* 1. 时间区域 */ .clock-display { text-align: center; font-size: 36px; font-weight: bold; color: #00d2ff; margin: 0 0 5px; text-shadow: 0 0 15px rgba(0,210,255,0.4); font-family: monospace; } .date-display { text-align: center; font-size: 12px; color: #8a9bb8; margin-bottom: 15px; } .stopwatch-box { background: rgba(0,0,0,0.3); padding: 10px; border-radius: 12px; border: 1px solid rgba(0,255,204,0.2); margin-bottom: 15px; text-align: center; } .stopwatch-title { font-size: 12px; color: #00ffcc; margin-bottom: 5px; font-weight: bold; } .stopwatch-display { font-size: 28px; font-weight: bold; color: #00ffcc; font-family: monospace; text-shadow: 0 0 10px rgba(0,255,204,0.3); margin-bottom: 10px; } .sw-controls { display: flex; justify-content: center; gap: 10px; } .sw-controls button { background: transparent; color: #00ffcc; border: 1px solid #00ffcc; box-shadow: none; padding: 4px 12px; } .sw-controls button:hover { background: rgba(0,255,204,0.1); color: #fff; box-shadow: 0 0 10px rgba(0,255,204,0.5); } /* 闹钟 */ .alarm-picker { display: flex; justify-content: center; align-items: center; gap: 10px; margin-bottom: 10px; background: rgba(16,23,38,0.6); padding: 8px; border-radius: 12px; border: 1px solid rgba(0,210,255,0.1); } .picker-col { display: flex; flex-direction: column; align-items: center; } .picker-colon { font-size: 20px; font-weight: bold; color: #00d2ff; padding-bottom: 15px; } .time-num { font-size: 22px; font-weight: bold; color: #e0e6ed; font-family: monospace; margin: 4px 0; background: rgba(0,0,0,0.3); padding: 2px 8px; border-radius: 6px; width: 50px; text-align: center; } .btn-arrow { background: rgba(0, 210, 255, 0.1); color: #00d2ff; border: 1px solid rgba(0, 210, 255, 0.2); border-radius: 4px; padding: 0 10px; cursor: pointer; font-size: 12px; transition: all 0.1s; box-shadow: none; } .btn-arrow:hover { background: #00d2ff; color: #000; transform: none; box-shadow: 0 0 10px rgba(0,210,255,0.5); } .btn-add-alarm { width: 100%; margin-bottom: 10px; padding: 8px; } .alarm-list { flex: 1; overflow-y: auto; } .alarm-item { display: flex; justify-content: space-between; background: rgba(16,23,38,0.6); padding: 6px 12px; border-radius: 6px; margin-bottom: 6px; border: 1px solid rgba(0,210,255,0.1); color: #e0e6ed; font-family: monospace; font-size: 14px;} .alarm-item button { padding: 2px 8px; font-size: 12px; background: #ff3366; box-shadow: none; } /* 2. 笔记 */ #note-textarea { flex: 1; resize: none; margin-bottom: 10px; background: rgba(16, 23, 38, 0.8); border: 1px solid rgba(0, 210, 255, 0.2); color: #e0e6ed; border-radius: 8px; padding: 10px; outline: none; font-size: 13px; user-select: text; } #note-textarea:focus { border-color: #00d2ff; } .note-actions { display: flex; justify-content: space-between; align-items: center; } .note-status { font-size: 11px; color: #00ffcc; transition: opacity 0.3s; opacity: 0; } #btn-export-note { padding: 6px 12px; font-size: 12px; background: transparent; border: 1px solid #00d2ff; color: #00d2ff; box-shadow: none; } #btn-export-note:hover { background: rgba(0,210,255,0.1); } /* 3. AI 对话 */ #ai-chat-box { flex: 1; overflow-y: auto; background: rgba(0,0,0,0.3); border-radius: 8px; padding: 10px; margin-bottom: 10px; display: flex; flex-direction: column; gap: 10px; } .msg { max-width: 85%; padding: 8px 12px; border-radius: 8px; font-size: 13px; line-height: 1.4; word-break: break-all; user-select: text; } .msg.user { align-self: flex-end; background: rgba(0, 210, 255, 0.2); border: 1px solid rgba(0, 210, 255, 0.3); } .msg.ai { align-self: flex-start; background: rgba(255, 255, 255, 0.05); border: 1px solid rgba(255,255,255,0.1); } .ai-input-group { display: flex; gap: 8px; } .ai-config { display: flex; flex-direction: column; gap: 8px; } .ai-config input { font-size: 12px; } /* 4. 截图 */ .screenshot-btn-area { display: flex; flex-direction: column; gap: 15px; align-items: center; justify-content: center; height: 100%; } .screenshot-btn-area p { color: #8a9bb8; text-align: center; font-size: 12px; line-height: 1.5; } .screenshot-btn-area button { width: 80%; padding: 12px; font-size: 14px; } #btn-screenshot-save-as { background: transparent; border: 1px solid #00d2ff; color: #00d2ff; box-shadow: none; } #btn-screenshot-save-as:hover { background: rgba(0,210,255,0.1); } /* 滚动条 */ ::-webkit-scrollbar { width: 4px; } ::-webkit-scrollbar-thumb { background: #00d2ff; border-radius: 2px; } `; shadow.appendChild(style); // ==================== 注入 HTML ==================== const ui = document.createElement('div'); ui.innerHTML = `
⚡
--:--
00:00:00
2023年01月01日 星期一
⏱️ 正计时
00:00:00.00
08
:
00
已自动保存
你好,我是你的 AI 助手。请先在下方配置 API 信息。

由于浏览器限制,脚本无法直接指定系统文件夹。
你可以选择直接下载(走浏览器默认路径),或者“另存为”手动选择文件夹。

`; shadow.appendChild(ui); // ==================== 元素引用 ==================== const ballWrapper = shadow.getElementById('ball-wrapper'); const ball = shadow.getElementById('float-ball'); const ballTime = shadow.getElementById('ball-time'); const panel = shadow.getElementById('panel'); const navItems = shadow.querySelectorAll('.nav-item'); const panes = shadow.querySelectorAll('.tab-pane'); // ==================== 1. 拖拽与 Tab ==================== let isDragging = false, hasDragged = false, startX, startY, initialX, initialY; ball.addEventListener('mousedown', (e) => { isDragging = true; hasDragged = false; startX = e.clientX; startY = e.clientY; const rect = ballWrapper.getBoundingClientRect(); initialX = rect.left; initialY = rect.top; ballWrapper.style.transition = 'none'; document.addEventListener('mousemove', onMouseMove); document.addEventListener('mouseup', onMouseUp); }); function onMouseMove(e) { if (!isDragging) return; const dx = e.clientX - startX, dy = e.clientY - startY; if (Math.abs(dx) > 3 || Math.abs(dy) > 3) hasDragged = true; let newX = Math.max(0, Math.min(window.innerWidth - 50, initialX + dx)); let newY = Math.max(0, Math.min(window.innerHeight - 80, initialY + dy)); ballWrapper.style.left = `${newX}px`; ballWrapper.style.top = `${newY}px`; ballWrapper.style.right = 'auto'; ballWrapper.style.bottom = 'auto'; if (panel.classList.contains('open')) { panel.style.left = `${Math.max(10, newX - 290)}px`; panel.style.top = `${Math.max(10, newY - 460)}px`; panel.style.right = 'auto'; panel.style.bottom = 'auto'; } } function onMouseUp() { isDragging = false; ballWrapper.style.transition = 'opacity 0.3s'; document.removeEventListener('mousemove', onMouseMove); document.removeEventListener('mouseup', onMouseUp); } ball.addEventListener('click', () => { if (hasDragged) return; const rect = ballWrapper.getBoundingClientRect(); if (panel.classList.contains('open')) panel.classList.remove('open'); else { panel.style.left = `${Math.max(10, rect.left - 290)}px`; panel.style.top = `${Math.max(10, rect.top - 460)}px`; panel.style.right = 'auto'; panel.style.bottom = 'auto'; panel.classList.add('open'); } }); navItems.forEach(item => { item.addEventListener('click', () => { navItems.forEach(i => i.classList.remove('active')); panes.forEach(p => p.classList.remove('active')); item.classList.add('active'); shadow.getElementById(`pane-${item.dataset.tab}`).classList.add('active'); }); }); // ==================== 2. 时钟与秒表 ==================== const clockTime = shadow.getElementById('clock-time'); const clockDate = shadow.getElementById('clock-date'); setInterval(() => { const now = new Date(); const timeStr = now.toTimeString().split(' ')[0]; ballTime.textContent = timeStr.substring(0, 5); clockTime.textContent = timeStr; clockDate.textContent = now.toLocaleDateString('zh-CN', { year: 'numeric', month: 'long', day: 'numeric', weekday: 'long' }); }, 1000); const swDisplay = shadow.getElementById('sw-display'); const swBtnStart = shadow.getElementById('sw-btn-start'); const swBtnReset = shadow.getElementById('sw-btn-reset'); let swStartTime = 0, swElapsed = 0, swTimer = null, isSwRunning = false; function updateSwDisplay() { if (isSwRunning) { swElapsed = Date.now() - swStartTime; const totalSecs = Math.floor(swElapsed / 1000); const h = String(Math.floor(totalSecs / 3600)).padStart(2, '0'); const m = String(Math.floor((totalSecs % 3600) / 60)).padStart(2, '0'); const s = String(totalSecs % 60).padStart(2, '0'); const ms = String(Math.floor((swElapsed % 1000) / 10)).padStart(2, '0'); swDisplay.textContent = `${h}:${m}:${s}.${ms}`; } } swBtnStart.addEventListener('click', () => { if (isSwRunning) { isSwRunning = false; clearInterval(swTimer); swBtnStart.textContent = '继续'; swBtnStart.style.color = '#00d2ff'; swBtnStart.style.borderColor = '#00d2ff'; } else { isSwRunning = true; swStartTime = Date.now() - swElapsed; swTimer = setInterval(updateSwDisplay, 31); swBtnStart.textContent = '暂停'; swBtnStart.style.color = '#ff3366'; swBtnStart.style.borderColor = '#ff3366'; } }); swBtnReset.addEventListener('click', () => { isSwRunning = false; clearInterval(swTimer); swElapsed = 0; swDisplay.textContent = '00:00:00.00'; swBtnStart.textContent = '开始'; swBtnStart.style.color = '#00ffcc'; swBtnStart.style.borderColor = '#00ffcc'; }); // ==================== 3. 闹钟 ==================== const alarmList = shadow.getElementById('alarm-list'); let alHour = 8, alMin = 0; const elHour = shadow.getElementById('alarm-hour'), elMin = shadow.getElementById('alarm-min'); function updateAlarmPicker() { elHour.textContent = String(alHour).padStart(2, '0'); elMin.textContent = String(alMin).padStart(2, '0'); } shadow.getElementById('hour-up').onclick = () => { alHour = (alHour + 1) % 24; updateAlarmPicker(); }; shadow.getElementById('hour-down').onclick = () => { alHour = (alHour - 1 + 24) % 24; updateAlarmPicker(); }; shadow.getElementById('min-up').onclick = () => { alMin = (alMin + 1) % 60; updateAlarmPicker(); }; shadow.getElementById('min-down').onclick = () => { alMin = (alMin - 1 + 60) % 60; updateAlarmPicker(); }; let alarms = GM_getValue('cyber_alarms', []); setInterval(() => { const currentHM = new Date().toTimeString().substring(0, 5); if (alarms.includes(currentHM) && new Date().getSeconds() === 0) triggerAlarm(currentHM); }, 1000); function renderAlarms() { alarmList.innerHTML = ''; alarms.sort().forEach(time => { const div = document.createElement('div'); div.className = 'alarm-item'; div.innerHTML = `⏰ ${time}`; alarmList.appendChild(div); }); alarmList.querySelectorAll('button').forEach(btn => { btn.onclick = () => { alarms = alarms.filter(t => t !== btn.dataset.time); GM_setValue('cyber_alarms', alarms); renderAlarms(); }; }); } shadow.getElementById('btn-add-alarm').addEventListener('click', () => { const val = `${String(alHour).padStart(2, '0')}:${String(alMin).padStart(2, '0')}`; if (!alarms.includes(val)) { alarms.push(val); GM_setValue('cyber_alarms', alarms); renderAlarms(); } }); function triggerAlarm(time) { const audioCtx = new (window.AudioContext || window.webkitAudioContext)(); const oscillator = audioCtx.createOscillator(); const gainNode = audioCtx.createGain(); oscillator.connect(gainNode); gainNode.connect(audioCtx.destination); oscillator.type = 'sine'; oscillator.frequency.setValueAtTime(880, audioCtx.currentTime); gainNode.gain.setValueAtTime(1, audioCtx.currentTime); gainNode.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 1); oscillator.start(); oscillator.stop(audioCtx.currentTime + 1); const notify = document.createElement('div'); notify.style.cssText = 'position:fixed; top:20px; left:50%; transform:translateX(-50%); background:#00d2ff; color:#000; padding:15px 30px; border-radius:8px; font-weight:bold; font-size:18px; z-index:10001; box-shadow:0 0 20px rgba(0,210,255,0.8);'; notify.textContent = `⏰ 闹钟时间到!(${time})`; shadow.appendChild(notify); setTimeout(() => notify.remove(), 5000); } renderAlarms(); // ==================== 4. 笔记 (增加导出) ==================== const noteTextarea = shadow.getElementById('note-textarea'); const noteStatus = shadow.getElementById('note-status'); const btnExportNote = shadow.getElementById('btn-export-note'); noteTextarea.value = GM_getValue('cyber_notes', ''); let noteTimeout; noteTextarea.addEventListener('input', () => { noteStatus.style.opacity = '0'; clearTimeout(noteTimeout); noteTimeout = setTimeout(() => { GM_setValue('cyber_notes', noteTextarea.value); noteStatus.style.opacity = '1'; setTimeout(() => noteStatus.style.opacity = '0', 2000); }, 800); }); btnExportNote.addEventListener('click', () => { const content = noteTextarea.value; if (!content) return alert("笔记为空!"); const blob = new Blob([content], { type: 'text/plain;charset=utf-8' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `网页笔记_${new Date().toLocaleDateString().replace(/\//g, '-')}.txt`; a.click(); URL.revokeObjectURL(url); }); // ==================== 5. AI 对话 ==================== const aiKeyInput = shadow.getElementById('ai-key'), aiUrlInput = shadow.getElementById('ai-url'); const btnSaveAi = shadow.getElementById('btn-save-ai'), aiConfigPanel = shadow.getElementById('ai-config-panel'); const aiInputPanel = shadow.getElementById('ai-input-panel'), aiInput = shadow.getElementById('ai-input'); const btnSendAi = shadow.getElementById('btn-send-ai'), aiChatBox = shadow.getElementById('ai-chat-box'); let aiKey = GM_getValue('cyber_ai_key', ''), aiUrl = GM_getValue('cyber_ai_url', 'https://api.deepseek.com/chat/completions'); if (aiKey) { aiConfigPanel.style.display = 'none'; aiInputPanel.style.display = 'flex'; aiUrlInput.value = aiUrl; } btnSaveAi.addEventListener('click', () => { const key = aiKeyInput.value.trim(), url = aiUrlInput.value.trim(); if (key) { aiKey = key; aiUrl = url || 'https://api.deepseek.com/chat/completions'; GM_setValue('cyber_ai_key', aiKey); GM_setValue('cyber_ai_url', aiUrl); aiConfigPanel.style.display = 'none'; aiInputPanel.style.display = 'flex'; addMessage('配置已保存,可以开始提问了。', 'ai'); } }); function addMessage(text, type) { const div = document.createElement('div'); div.className = `msg ${type}`; div.textContent = text; aiChatBox.appendChild(div); aiChatBox.scrollTop = aiChatBox.scrollHeight; } btnSendAi.addEventListener('click', () => { const text = aiInput.value.trim(); if (!text) return; addMessage(text, 'user'); aiInput.value = ''; addMessage('思考中...', 'ai'); const lastMsg = aiChatBox.lastChild; GM_xmlhttpRequest({ method: "POST", url: aiUrl, headers: { "Content-Type": "application/json", "Authorization": `Bearer ${aiKey}` }, data: JSON.stringify({ model: "deepseek-chat", messages: [{ role: "user", content: text }], stream: false }), onload: function(res) { if (res.status === 200) { try { lastMsg.textContent = JSON.parse(res.responseText).choices[0].message.content; } catch (e) { lastMsg.textContent = "解析错误。"; } } else { lastMsg.textContent = `请求失败 (${res.status})。`; } aiChatBox.scrollTop = aiChatBox.scrollHeight; }, onerror: function() { lastMsg.textContent = "网络请求失败。"; } }); }); // ==================== 6. 截图 (支持另存为) ==================== const btnScreenshot = shadow.getElementById('btn-screenshot'); const btnScreenshotSaveAs = shadow.getElementById('btn-screenshot-save-as'); function loadAndShot(isSaveAs) { const scriptId = 'html2canvas-script'; if (!document.getElementById(scriptId)) { const script = document.createElement('script'); script.id = scriptId; script.src = 'https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js'; script.onload = () => takeScreenshot(isSaveAs); script.onerror = () => alert("截图组件加载失败!"); document.body.appendChild(script); } else { takeScreenshot(isSaveAs); } } btnScreenshot.addEventListener('click', () => loadAndShot(false)); btnScreenshotSaveAs.addEventListener('click', () => loadAndShot(true)); async function takeScreenshot(isSaveAs) { const oldText1 = btnScreenshot.textContent, oldText2 = btnScreenshotSaveAs.textContent; btnScreenshot.disabled = true; btnScreenshotSaveAs.disabled = true; btnScreenshot.textContent = '截图中...'; btnScreenshotSaveAs.textContent = '截图中...'; ballWrapper.classList.add('hidden'); panel.classList.add('hidden'); const forceRestoreTimeout = setTimeout(() => restoreUI(), 8000); function restoreUI() { clearTimeout(forceRestoreTimeout); ballWrapper.classList.remove('hidden'); panel.classList.remove('hidden'); btnScreenshot.textContent = oldText1; btnScreenshotSaveAs.textContent = oldText2; btnScreenshot.disabled = false; btnScreenshotSaveAs.disabled = false; } setTimeout(async () => { try { if (!window.html2canvas) throw new Error("组件未加载"); const canvas = await window.html2canvas(document.body, { useCORS: true, ignoreElements: (el) => el.id === 'cyber-assistant-host' }); const filename = `screenshot_${Date.now()}.png`; if (isSaveAs && window.showSaveFilePicker) { // 尝试使用高级 API 另存为 try { const handle = await window.showSaveFilePicker({ suggestedName: filename, types: [{ description: 'PNG Image', accept: {'image/png': ['.png']} }] }); const writable = await handle.createWritable(); const blob = await new Promise(resolve => canvas.toBlob(resolve, 'image/png')); await writable.write(blob); await writable.close(); } catch (err) { // 如果用户取消了,或者浏览器不支持,回退到普通下载 if (err.name !== 'AbortError') { console.warn("另存为失败,回退到默认下载", err); downloadCanvas(canvas, filename); } } } else { // 普通下载 downloadCanvas(canvas, filename); } } catch (err) { console.error(err); } finally { restoreUI(); } }, 300); } function downloadCanvas(canvas, filename) { const a = document.createElement('a'); a.download = filename; a.href = canvas.toDataURL('image/png'); a.click(); } })();