// ==UserScript== // @name Mobile AI Summary (MD3) // @namespace http://tampermonkey.net/ // @version 1.3 // @description 为移动端设计的AI页面总结工具,采用Material Design 3风格 // @author Justin Ye // @match *://*/* // @grant GM_setValue // @grant GM_getValue // @grant GM_xmlhttpRequest // @connect api.openai.com // @connect * // @downloadURL none // ==/UserScript== (function() { 'use strict'; // 封装 GM_xmlhttpRequest 为 Promise 形式,解决跨域问题 function gmFetch(url, options) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: options.method || 'GET', url: url, headers: options.headers, data: options.body, onload: (response) => { if (response.status >= 200 && response.status < 300) { resolve({ json: () => Promise.resolve(JSON.parse(response.responseText)), text: () => Promise.resolve(response.responseText), status: response.status }); } else { reject(new Error(`HTTP error! status: ${response.status} ${response.statusText || ''}\nResponse: ${response.responseText.substring(0, 100)}...`)); } }, onerror: (error) => { console.error('GM_xmlhttpRequest error:', error); reject(new Error('Network error: Failed to fetch')); }, ontimeout: () => { reject(new Error('Request timeout')); } }); }); } // MD3 风格定义 const md3Colors = { light: { primary: '#006492', onPrimary: '#ffffff', primaryContainer: '#cae6ff', onPrimaryContainer: '#001e30', surface: '#f8f9ff', onSurface: '#191c20', surfaceContainer: '#f0f4f8', outline: '#72777f', shadow: 'rgba(0, 0, 0, 0.2)', surfaceVariant: '#dde3ea', onSurfaceVariant: '#41484d' }, dark: { primary: '#8ccceb', onPrimary: '#00344e', primaryContainer: '#004b6f', onPrimaryContainer: '#cae6ff', surface: '#111418', onSurface: '#e1e2e8', surfaceContainer: '#1d2024', outline: '#8c9199', shadow: 'rgba(0, 0, 0, 0.4)', surfaceVariant: '#41484d', onSurfaceVariant: '#c1c7ce' } }; // 新的提示词 const newPrompt = `你是一个专业的中文内容总结器。你的任务是分析提供的网页内容,**识别内容类型(例如:新闻报道、研究报告、普通文章、市场分析等)**,并在此基础上创建一个清晰、简洁、结构良好的中文总结。**总结必须严格依据原文内容,不得进行任何推测、假设或添加原文中未包含的信息。** 请严格遵守以下指南: 1. **内容类型识别与结构确定:** * 首先识别原文的内容类型。 * 根据内容类型,确定最合适的分段和总结重点。总结的结构应该逻辑清晰,反映原文的核心信息。 * 你可以使用 **\`##\`** 作为主要分段的标题,例如:对于新闻可以使用“事件概述”、“关键进展”等,对于报告可以使用“主要发现”、“数据支持”等。不必拘泥于原文的固定分段,但要确保覆盖核心要点。 2. **输出格式:** * 使用 **\`##\`** 表示主要段落标题。 * 使用 **\`•\`** 表示段落内的关键点和细节。 * 使用 **粗体** 突出重要术语、概念或关键信息。 * 使用 **\`>\`** 表示引人注目的原文引述(如果适用)。 3. **内容要求:** * 总结必须**严格忠于原文**,不允许加入任何个人观点、推测或假设。 * **识别并保留原文中的重要数据、数字、统计信息或关键事实。** * 根据识别的内容类型,调整总结的侧重点,但**所有信息必须来源于原文**。 4. **写作风格:** * 语言清晰简洁。 * 专业且客观的语调。 * 逻辑流畅。 * 易于理解。 * 聚焦于原文的核心信息和重要细节。 5. **重要规则:** * **DO NOT show your reasoning process.** (不要显示你的思考过程或内部步骤。)`; // 默认配置 const defaultConfig = { apiUrl: 'https://api.openai.com/v1/chat/completions', apiKey: '', prompt: newPrompt, model: 'gpt-3.5-turbo', theme: 'auto' // auto, light, dark }; // GM 函数兼容层 const GM = { setValue: (key, value) => { try { if (typeof GM_setValue !== 'undefined') { GM_setValue(key, value); } else { localStorage.setItem(`mobile_ai_summary_${key}`, JSON.stringify(value)); } } catch (error) { console.error('保存配置失败:', error); } }, getValue: (key, defaultValue) => { try { if (typeof GM_getValue !== 'undefined') { return GM_getValue(key, defaultValue); } const value = localStorage.getItem(`mobile_ai_summary_${key}`); return value ? JSON.parse(value) : defaultValue; } catch (error) { console.error('获取配置失败:', error); return defaultValue; } } }; // 获取配置 let config = { apiUrl: GM.getValue('apiUrl', defaultConfig.apiUrl), apiKey: GM.getValue('apiKey', defaultConfig.apiKey), prompt: GM.getValue('prompt', defaultConfig.prompt), model: GM.getValue('model', defaultConfig.model), theme: GM.getValue('theme', defaultConfig.theme) }; // 加载 marked.js let markedLoaded = false; const markedScript = document.createElement('script'); markedScript.src = 'https://cdn.jsdelivr.net/npm/marked/marked.min.js'; markedScript.onload = () => { markedLoaded = true; marked.setOptions({ breaks: true, gfm: true }); }; document.head.appendChild(markedScript); // 创建样式 const style = document.createElement('style'); style.textContent = ` /* Material Design 3 风格样式 */ :root { --mas-primary: ${md3Colors.light.primary}; --mas-on-primary: ${md3Colors.light.onPrimary}; --mas-primary-container: ${md3Colors.light.primaryContainer}; --mas-on-primary-container: ${md3Colors.light.onPrimaryContainer}; --mas-surface: ${md3Colors.light.surface}; --mas-on-surface: ${md3Colors.light.onSurface}; --mas-surface-container: ${md3Colors.light.surfaceContainer}; --mas-outline: ${md3Colors.light.outline}; --mas-shadow: ${md3Colors.light.shadow}; --mas-surface-variant: ${md3Colors.light.surfaceVariant}; --mas-on-surface-variant: ${md3Colors.light.onSurfaceVariant}; } [data-theme="dark"] { --mas-primary: ${md3Colors.dark.primary}; --mas-on-primary: ${md3Colors.dark.onPrimary}; --mas-primary-container: ${md3Colors.dark.primaryContainer}; --mas-on-primary-container: ${md3Colors.dark.onPrimaryContainer}; --mas-surface: ${md3Colors.dark.surface}; --mas-on-surface: ${md3Colors.dark.onSurface}; --mas-surface-container: ${md3Colors.dark.surfaceContainer}; --mas-outline: ${md3Colors.dark.outline}; --mas-shadow: ${md3Colors.dark.shadow}; --mas-surface-variant: ${md3Colors.dark.surfaceVariant}; --mas-on-surface-variant: ${md3Colors.dark.onSurfaceVariant}; } /* Pixel 风格优化 */ .mas-fab-container { position: fixed; left: 16px; /* 移到左侧 */ bottom: 80px; /* 避开底部导航栏 */ z-index: 999999; display: flex; flex-direction: column; gap: 16px; pointer-events: none; /* 允许点击穿透 */ } .mas-fab { width: 56px; height: 56px; border-radius: 20px; /* Pixel 风格更圆润 */ background-color: var(--mas-primary-container); color: var(--mas-on-primary-container); border: none; box-shadow: 0 4px 12px rgba(0,0,0,0.15); display: flex; align-items: center; justify-content: center; font-size: 24px; cursor: pointer; pointer-events: auto; transition: all 0.2s cubic-bezier(0.2, 0, 0, 1); -webkit-tap-highlight-color: transparent; } .mas-fab:active { transform: scale(0.92); box-shadow: 0 2px 6px rgba(0,0,0,0.1); } .mas-fab svg { width: 24px; height: 24px; fill: currentColor; } /* Bottom Sheet 样式 */ .mas-bottom-sheet { position: fixed; left: 0; right: 0; bottom: 0; background-color: var(--mas-surface-container); border-top-left-radius: 28px; border-top-right-radius: 28px; box-shadow: 0 -4px 20px rgba(0,0,0,0.15); z-index: 1000000; transform: translateY(100%); transition: transform 0.35s cubic-bezier(0.2, 0, 0, 1); max-height: 85vh; display: flex; flex-direction: column; color: var(--mas-on-surface); font-family: 'Google Sans', system-ui, -apple-system, sans-serif; /* 尝试使用 Google Sans */ } .mas-bottom-sheet.show { transform: translateY(0); } .mas-drag-handle { width: 32px; height: 4px; background-color: var(--mas-outline); opacity: 0.4; border-radius: 2px; margin: 16px auto 0; flex-shrink: 0; } .mas-sheet-content { padding: 24px; overflow-y: auto; -webkit-overflow-scrolling: touch; } .mas-sheet-header { font-size: 22px; font-weight: 400; margin-bottom: 24px; color: var(--mas-on-surface); font-weight: 500; } .mas-icon-btn { color: var(--mas-on-surface-variant); background: transparent; border: none; padding: 8px; border-radius: 50%; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: background-color 0.2s; } .mas-icon-btn:hover { background-color: rgba(0, 0, 0, 0.08); } .mas-icon-btn:active { background-color: rgba(0, 0, 0, 0.12); } [data-theme="dark"] .mas-icon-btn:hover { background-color: rgba(255, 255, 255, 0.08); } [data-theme="dark"] .mas-icon-btn:active { background-color: rgba(255, 255, 255, 0.12); } /* 表单样式 */ .mas-input-group { margin-bottom: 20px; } .mas-label { display: block; font-size: 12px; color: var(--mas-on-surface-variant); margin-bottom: 8px; font-weight: 500; } .mas-input, .mas-textarea, .mas-select { width: 100%; padding: 16px; border: 1px solid var(--mas-outline); border-radius: 12px; /* 更圆润的输入框 */ background: var(--mas-surface); font-size: 16px; color: var(--mas-on-surface); box-sizing: border-box; transition: all 0.2s; font-family: inherit; } .mas-input:focus, .mas-textarea:focus, .mas-select:focus { outline: none; border-color: var(--mas-primary); background: var(--mas-surface-container); box-shadow: 0 0 0 2px var(--mas-primary-container); } .mas-textarea { min-height: 100px; resize: vertical; font-family: inherit; } /* 按钮样式 */ .mas-btn { background-color: var(--mas-primary); color: var(--mas-on-primary); border: none; border-radius: 24px; /* 完全圆角 */ padding: 0 24px; font-size: 14px; font-weight: 500; cursor: pointer; width: 100%; height: 48px; /* 更高的点击区域 */ display: flex; align-items: center; justify-content: center; transition: all 0.2s; letter-spacing: 0.5px; } .mas-btn:active { opacity: 0.9; transform: scale(0.98); } .mas-btn-text { background: transparent; color: var(--mas-primary); margin-top: 8px; } /* 结果内容样式 */ .mas-result-content { font-size: 16px; line-height: 1.6; } .mas-result-content h1, .mas-result-content h2, .mas-result-content h3 { margin-top: 1em; margin-bottom: 0.5em; color: var(--mas-on-surface); } .mas-result-content p { margin-bottom: 1em; color: var(--mas-on-surface-variant); } .mas-result-content ul { padding-left: 20px; margin-bottom: 1em; color: var(--mas-on-surface-variant); } .mas-result-content li { margin-bottom: 0.5em; } .mas-result-content strong { font-weight: 700; color: var(--mas-on-surface); } .mas-result-content blockquote { border-left: 4px solid var(--mas-primary); background-color: var(--mas-surface-variant); margin: 1em 0; padding: 12px 16px; border-radius: 0 12px 12px 0; color: var(--mas-on-surface-variant); } /* 遮罩层 */ .mas-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.32); z-index: 999999; display: none; opacity: 0; transition: opacity 0.3s; } .mas-overlay.show { display: block; opacity: 1; } .mas-actions { display: flex; gap: 12px; margin-top: 24px; } `; document.head.appendChild(style); // 创建 UI 元素 const fabContainer = document.createElement('div'); fabContainer.className = 'mas-fab-container'; // 主 FAB (合并了总结和设置入口) const mainFab = document.createElement('button'); mainFab.className = 'mas-fab'; mainFab.innerHTML = ` `; fabContainer.appendChild(mainFab); document.body.appendChild(fabContainer); // 遮罩层 const overlay = document.createElement('div'); overlay.className = 'mas-overlay'; document.body.appendChild(overlay); // 主面板 Bottom Sheet (包含首页、设置、结果) const mainSheet = document.createElement('div'); mainSheet.className = 'mas-bottom-sheet'; // 应用主题 function applyTheme() { const theme = config.theme; if (theme === 'dark') { mainSheet.setAttribute('data-theme', 'dark'); } else if (theme === 'light') { mainSheet.setAttribute('data-theme', 'light'); } else { // Auto if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) { mainSheet.setAttribute('data-theme', 'dark'); } else { mainSheet.setAttribute('data-theme', 'light'); } } } // 渲染主面板内容 function renderSheetContent(view = 'home', data = null) { let content = ''; applyTheme(); if (view === 'home') { content = `
当前页面:
${document.title}
正在生成...
'}正在分析页面内容...
'); const pageContent = document.body.innerText.substring(0, 5000); try { // 使用封装的 gmFetch 替代原生 fetch const response = await gmFetch(config.apiUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${config.apiKey}` }, body: JSON.stringify({ model: config.model, messages: [ { role: 'system', content: config.prompt }, { role: 'user', content: pageContent } ] }) }); const data = await response.json(); if (data.choices && data.choices[0]) { const rawContent = data.choices[0].message.content; // 等待 marked 加载 if (!markedLoaded) { await new Promise(resolve => setTimeout(resolve, 500)); } let htmlContent = rawContent; try { htmlContent = marked.parse(rawContent); } catch (e) { console.error('Markdown parse error', e); } // 更新结果视图内容 const contentDiv = document.getElementById('masResultContent'); if (contentDiv) { contentDiv.innerHTML = htmlContent; } } else { throw new Error('API 返回数据异常'); } } catch (error) { const contentDiv = document.getElementById('masResultContent'); if (contentDiv) { contentDiv.innerHTML = `出错啦: ${error.message}
`; } } } // 简单的拖拽支持 (FAB) let isDragging = false; let startY = 0; let startBottom = 100; fabContainer.addEventListener('touchstart', (e) => { if (e.target.closest('.mas-fab')) { isDragging = true; startY = e.touches[0].clientY; const style = window.getComputedStyle(fabContainer); startBottom = parseInt(style.bottom); } }); document.addEventListener('touchmove', (e) => { if (!isDragging) return; e.preventDefault(); // 防止滚动 const deltaY = startY - e.touches[0].clientY; let newBottom = startBottom + deltaY; newBottom = Math.max(20, Math.min(newBottom, window.innerHeight - 100)); fabContainer.style.bottom = `${newBottom}px`; }, { passive: false }); document.addEventListener('touchend', () => { isDragging = false; }); })();