您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
一键将网页正文转换为 Markdown,支持 AI 优化、编辑预览,可保存自定义提示词。
// ==UserScript== // @name 网页转 Markdown 插件 // @namespace http://tampermonkey.net/ // @version 1.0 // @description 一键将网页正文转换为 Markdown,支持 AI 优化、编辑预览,可保存自定义提示词。 // @author An // @match *://*/* // @grant GM_addStyle // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @grant GM_registerMenuCommand // @require https://cdnjs.cloudflare.com/ajax/libs/readability/0.5.0/Readability.min.js // @require https://cdnjs.cloudflare.com/ajax/libs/turndown/7.1.3/turndown.min.js // @require https://cdnjs.cloudflare.com/ajax/libs/marked/4.3.0/marked.min.js // @license MIT // ==/UserScript== (function() { 'use strict'; // ================================================================================= // 模块一:核心逻辑 (非 UI) // ================================================================================= // 默认的 AI 提示词 const DEFAULT_AI_PROMPT = '请对以下 Markdown 内容进行格式和排版优化,以提升其可读性。请注意:此次优化的目标是改善展现形式,而不是修改内容本身。所有文字、数据和核心信息必须保持原样,不得增删或改写。你可以调整的方面包括但不限于:规范化标题层级(例如,`#`, `##`)。整理列表格式(有序和无序)。确保代码块和行内代码使用正确的 Markdown 语法。调整段落间距和换行。修正纯粹的 Markdown 语法错误。'; /** * 使用 Readability 提取文章内容 * @param {Document} doc 文档对象 * @return {Object|null} 提取的文章对象或 null */ function extractArticle(doc) { try { const reader = new Readability(doc.cloneNode(true)); const article = reader.parse(); if (!article || !article.content) { // 尝试从常见内容容器中提取内容 const contentSelectors = ['main', 'article', '#main', '#content', '.main', '.content']; for (const selector of contentSelectors) { const main = doc.querySelector(selector); if (main) { return { title: doc.title, content: main.innerHTML }; } } return null; } return article; } catch (e) { console.error('Error extracting article with Readability:', e); return null; } } // 配置 TurndownService 实例 const turndownService = new TurndownService({ headingStyle: 'atx', codeBlockStyle: 'fenced', hr: '---', bulletListMarker: '-' }); // 添加图片处理规则 turndownService.addRule('images', { filter: 'img', replacement: function (content, node) { let src = node.getAttribute('src') || ''; // 将相对 URL 转换为绝对 URL if (src && !src.startsWith('http') && !src.startsWith('data:')) { src = new URL(src, window.location.href).href; } const alt = node.getAttribute('alt') || ''; return ``; } }); // 添加表格处理改进 turndownService.addRule('tableCell', { filter: ['th', 'td'], replacement: function (content, node) { return ` ${content.trim()} |`; } }); /** * 将 HTML 转换为 Markdown * @param {string} htmlString HTML 字符串 * @return {string} 转换后的 Markdown 字符串 */ function htmlToMarkdown(htmlString) { const sanitizedHtml = new DOMParser().parseFromString(htmlString, 'text/html').body; return turndownService.turndown(sanitizedHtml); } /** * 使用 DeepSeek API 优化 Markdown 内容 * @param {string} prompt 指令提示词 * @param {string} originalMarkdown 原始 Markdown 文本 * @param {string} apiKey DeepSeek API 密钥 * @return {Promise<string>} 优化后的 Markdown 文本 */ async function getEnhancedMarkdown(prompt, originalMarkdown, apiKey) { if (!apiKey || apiKey.trim() === '') { throw new Error('请先输入有效的DeepSeek API Key'); } return new Promise((resolve, reject) => { const requestTimeout = 60000; // 60秒超时时间 GM_xmlhttpRequest({ method: 'POST', url: 'https://api.deepseek.com/chat/completions', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` }, data: JSON.stringify({ model: 'deepseek-chat', messages: [ { role: "system", content: prompt }, { role: "user", content: originalMarkdown } ] }), timeout: requestTimeout, onload: res => { try { if (res.status >= 200 && res.status < 300) { const response = JSON.parse(res.responseText); if (response.choices && response.choices[0] && response.choices[0].message) { resolve(response.choices[0].message.content); } else { reject('API 返回结果格式异常'); } } else { reject(`API错误: ${res.status} - ${res.responseText}`); } } catch (error) { reject(`解析 API 响应失败: ${error.message}`); } }, onerror: err => reject(`网络错误: ${err}`), ontimeout: () => reject('请求超时,请稍后再试') }); }); } /** * 下载文件 * @param {string} filename 文件名 * @param {string} content 文件内容 * @param {string} mimeType 文件 MIME 类型 */ function downloadFile(filename, content, mimeType = 'text/markdown;charset=utf-8') { const blob = new Blob([content], { type: mimeType }); const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = sanitizeFilename(filename); document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(a.href); } /** * 净化文件名,移除不合法字符 * @param {string} filename 文件名 * @return {string} 净化后的文件名 */ function sanitizeFilename(filename) { return filename.replace(/[/?%*:|"<>\\]/g, '-'); } // ================================================================================= // 模块二:浏览器插件 UI // ================================================================================= class MarkdownConverterUI { constructor() { this.container = null; this.elements = {}; this.currentArticleTitle = 'untitled'; this.init(); } init() { this.injectCSS(); this.createUI(); this.attachEventListeners(); GM_registerMenuCommand("启动 Markdown 转换器", () => this.toggle()); } toggle() { if (!this.container) return; this.container.style.display = (this.container.style.display === 'none') ? 'block' : 'none'; } createUI() { this.container = document.createElement('div'); this.container.id = 'mk-converter-container'; this.container.style.display = 'none'; this.container.innerHTML = ` <div id="mk-converter-header"><span>网页转 Markdown</span><button id="mk-converter-close" class="mk-btn-close">×</button></div> <div id="mk-converter-body"> <div id="mk-single-page-initial" style="display: flex; justify-content: center; align-items: center; height: 100%;"> <button id="mk-convert-current-page-btn" class="mk-btn mk-btn-primary">转换当前页面</button> </div> <div id="mk-preview-view" style="display: none;"> <div class="mk-control-group"> <div class="mk-ai-input-group"> <input type="text" id="mk-ai-api-key" placeholder="请输入DeepSeek API key"> <div class="mk-tooltip" id="mk-api-tooltip"> <span>API 密钥已保存</span> </div> </div> <div class="mk-prompt-container"> <textarea id="mk-ai-prompt" rows="3" placeholder="AI 指令 Prompt..."></textarea> <div class="mk-prompt-buttons"> <button id="mk-save-prompt-btn" class="mk-btn mk-btn-small" title="保存当前提示词"> 保存提示词 </button> <button id="mk-reset-prompt-btn" class="mk-btn mk-btn-small" title="重置为默认提示词"> 重置提示词 </button> <div class="mk-tooltip" id="mk-prompt-tooltip"> <span>提示词已保存</span> </div> </div> </div> </div> <div class="mk-control-group mk-export-controls"> <button id="mk-copy-btn" class="mk-btn">复制</button> <button id="mk-download-btn" class="mk-btn">下载</button> <button id="mk-ai-optimize-btn" class="mk-btn mk-btn-primary" style="margin-left: auto;">AI 优化并下载</button> </div> <div id="mk-dual-pane-editor" class="mk-control-group"> <textarea id="mk-md-editor" placeholder="Markdown 源码..."></textarea> <div id="mk-md-preview" class="mk-preview-pane"></div> </div> </div> </div>`; document.body.appendChild(this.container); this.cacheElements(); } cacheElements() { this.elements = { closeBtn: this.container.querySelector('#mk-converter-close'), convertCurrentPageBtn: this.container.querySelector('#mk-convert-current-page-btn'), previewView: this.container.querySelector('#mk-preview-view'), aiApiKey: this.container.querySelector('#mk-ai-api-key'), apiTooltip: this.container.querySelector('#mk-api-tooltip'), aiPrompt: this.container.querySelector('#mk-ai-prompt'), savePromptBtn: this.container.querySelector('#mk-save-prompt-btn'), resetPromptBtn: this.container.querySelector('#mk-reset-prompt-btn'), promptTooltip: this.container.querySelector('#mk-prompt-tooltip'), aiOptimizeBtn: this.container.querySelector('#mk-ai-optimize-btn'), mdEditor: this.container.querySelector('#mk-md-editor'), mdPreview: this.container.querySelector('#mk-md-preview'), copyBtn: this.container.querySelector('#mk-copy-btn'), downloadBtn: this.container.querySelector('#mk-download-btn'), }; } attachEventListeners() { // 拖拽功能 this.setupDraggable(); // 关闭按钮 this.elements.closeBtn.addEventListener('click', () => this.toggle()); // 加载保存的API密钥 this.elements.aiApiKey.value = GM_getValue('deepseek_api_key', ''); // 加载保存的AI提示词 this.elements.aiPrompt.value = GM_getValue('custom_ai_prompt', DEFAULT_AI_PROMPT); // 保存API密钥并提供反馈 this.elements.aiApiKey.addEventListener('input', (e) => this.handleApiKeyInput(e)); // 保存和重置提示词功能 this.elements.savePromptBtn.addEventListener('click', () => this.handleSavePrompt()); this.elements.resetPromptBtn.addEventListener('click', () => this.handleResetPrompt()); // 其他事件监听 this.elements.convertCurrentPageBtn.addEventListener('click', () => this.handleConvertCurrentPage()); this.elements.mdEditor.addEventListener('input', () => this.updatePreview()); this.elements.aiOptimizeBtn.addEventListener('click', () => this.handleAiOptimize()); this.elements.copyBtn.addEventListener('click', () => this.handleCopy()); this.elements.downloadBtn.addEventListener('click', () => this.handleDownload()); } handleSavePrompt() { const promptText = this.elements.aiPrompt.value.trim(); if (promptText) { GM_setValue('custom_ai_prompt', promptText); // 显示可视反馈 this.elements.promptTooltip.classList.add('show'); setTimeout(() => { this.elements.promptTooltip.classList.remove('show'); }, 2000); } else { alert('提示词不能为空'); } } handleResetPrompt() { this.elements.aiPrompt.value = DEFAULT_AI_PROMPT; GM_setValue('custom_ai_prompt', DEFAULT_AI_PROMPT); this.showNotification('已重置为默认提示词'); } setupDraggable() { const header = this.container.querySelector('#mk-converter-header'); let isDragging = false, offset = { x: 0, y: 0 }; header.addEventListener('mousedown', (e) => { if (e.target.matches('.mk-btn-close')) return; isDragging = true; offset = { x: e.clientX - this.container.offsetLeft, y: e.clientY - this.container.offsetTop }; header.style.cursor = 'grabbing'; }); document.addEventListener('mousemove', (e) => { if (isDragging) { const left = Math.max(0, Math.min(window.innerWidth - this.container.offsetWidth, e.clientX - offset.x)); const top = Math.max(0, Math.min(window.innerHeight - this.container.offsetHeight, e.clientY - offset.y)); this.container.style.left = `${left}px`; this.container.style.top = `${top}px`; } }); document.addEventListener('mouseup', () => { isDragging = false; header.style.cursor = 'grab'; }); } handleApiKeyInput(e) { const key = e.target.value.trim(); GM_setValue('deepseek_api_key', key); // 显示可视反馈 if (key) { this.elements.apiTooltip.classList.add('show'); setTimeout(() => { this.elements.apiTooltip.classList.remove('show'); }, 2000); } } async handleConvertCurrentPage() { const btn = this.elements.convertCurrentPageBtn; btn.textContent = '转换中...'; btn.disabled = true; try { const article = extractArticle(document); if (!article) { alert('无法提取当前页面的正文。'); return; } this.currentArticleTitle = article.title || document.title; const markdown = `# ${this.currentArticleTitle}\n\n` + htmlToMarkdown(article.content); this.elements.mdEditor.value = markdown; this.updatePreview(); this.container.querySelector('#mk-single-page-initial').style.display = 'none'; this.elements.previewView.style.display = 'flex'; // 使用保存的自定义提示词或默认提示词 this.elements.aiPrompt.value = GM_getValue('custom_ai_prompt', DEFAULT_AI_PROMPT); } catch (error) { console.error('转换页面失败:', error); alert(`转换页面失败: ${error.message || '未知错误'}`); } finally { btn.textContent = '转换当前页面'; btn.disabled = false; } } updatePreview() { try { this.elements.mdPreview.innerHTML = marked.parse(this.elements.mdEditor.value); } catch (error) { console.error('预览渲染失败:', error); this.elements.mdPreview.innerHTML = '<div class="mk-error">渲染预览时出错</div>'; } } async handleAiOptimize() { const btn = this.elements.aiOptimizeBtn; btn.textContent = '处理中...'; btn.disabled = true; try { const apiKey = this.elements.aiApiKey.value.trim(); if (!apiKey) { alert('请先输入有效的DeepSeek API Key'); throw new Error('API Key未提供'); } const prompt = this.elements.aiPrompt.value || DEFAULT_AI_PROMPT; // 显示加载状态 this.elements.mdPreview.innerHTML = '<div class="mk-loading">AI 优化中,请稍候...</div>'; const enhancedMarkdown = await getEnhancedMarkdown(prompt, this.elements.mdEditor.value, apiKey); this.elements.mdEditor.value = enhancedMarkdown; this.updatePreview(); // 提取文章标题 const titleMatch = enhancedMarkdown.match(/^#\s(.+)/m); const title = titleMatch ? titleMatch[1] : this.currentArticleTitle; downloadFile(`${title}_(AI优化).md`, enhancedMarkdown); alert('AI 优化完成并已自动下载!'); } catch (error) { alert(`AI 优化失败: ${error.message || error}`); console.error('AI 优化失败:', error); this.updatePreview(); // 恢复原有预览 } finally { btn.textContent = 'AI 优化并下载'; btn.disabled = false; } } handleCopy() { try { navigator.clipboard.writeText(this.elements.mdEditor.value) .then(() => { this.showNotification('已复制到剪贴板!'); }) .catch(() => { alert('无法访问剪贴板,请检查浏览器权限。'); }); } catch (error) { console.error('复制失败:', error); alert('复制失败!'); } } handleDownload() { try { const markdownText = this.elements.mdEditor.value; const titleMatch = markdownText.match(/^#\s(.+)/m); const title = titleMatch ? titleMatch[1] : this.currentArticleTitle; downloadFile(`${title}.md`, markdownText); this.showNotification('下载成功!'); } catch (error) { console.error('下载失败:', error); alert('下载失败!'); } } showNotification(message) { const notification = document.createElement('div'); notification.className = 'mk-notification'; notification.textContent = message; document.body.appendChild(notification); setTimeout(() => { notification.classList.add('show'); setTimeout(() => { notification.classList.remove('show'); setTimeout(() => { document.body.removeChild(notification); }, 300); }, 2000); }, 10); } injectCSS() { GM_addStyle(` #mk-converter-container { position: fixed; top: 50px; right: 20px; width: 800px; max-width: 90vw; height: 85vh; background-color: #fff; border: 1px solid #e0e0e0; border-radius: 8px; box-shadow: 0 5px 15px rgba(0,0,0,0.1); z-index: 99999; display: flex; flex-direction: column; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; color: #222; resize: both; overflow: hidden; min-width: 600px; min-height: 500px; } #mk-converter-header { padding: 10px 15px; background-color: #f8f9fa; border-bottom: 1px solid #e9ecef; cursor: grab; display: flex; justify-content: space-between; align-items: center; flex-shrink: 0; } #mk-converter-header span { font-weight: 600; font-size: 16px; } .mk-btn-close { background: none; border: none; font-size: 24px; cursor: pointer; color: #888; } .mk-btn-close:hover { color: #333; } #mk-converter-body { padding: 15px; flex-grow: 1; display: flex; flex-direction: column; overflow: hidden; } .mk-btn { padding: 8px 16px; font-size: 14px; border-radius: 5px; border: 1px solid #ccc; background-color: #f0f0f0; cursor: pointer; transition: all 0.2s ease; } .mk-btn:hover { background-color: #e6e6e6; } .mk-btn:active { transform: translateY(1px); } .mk-btn:disabled { opacity: 0.6; cursor: not-allowed; } .mk-btn-primary { background-color: #007bff; color: white; border-color: #007bff; } .mk-btn-primary:hover { background-color: #0056b3; } .mk-btn-small { padding: 4px 8px; font-size: 12px; } .mk-control-group { margin-bottom: 15px; flex-shrink: 0; } #mk-preview-view { flex-direction: column; height: 100%; display: flex; } .mk-ai-input-group { display: flex; gap: 10px; margin-bottom: 10px; position: relative; } #mk-ai-api-key, #mk-ai-prompt { padding: 10px; border: 1px solid #ced4da; border-radius: 4px; font-size: 13px; transition: border-color 0.3s; } #mk-ai-api-key { flex-grow: 1; } .mk-prompt-container { position: relative; width: 100%; } #mk-ai-prompt { width: 100%; box-sizing: border-box; resize: vertical; margin-bottom: 5px; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; } .mk-prompt-buttons { display: flex; gap: 8px; margin-bottom: 10px; } .mk-icon { margin-right: 4px; } #mk-dual-pane-editor { display: flex; flex-grow: 1; gap: 10px; height: 50vh; min-height: 300px; } #mk-md-editor, #mk-md-preview { width: 50%; height: 100%; padding: 12px; border: 1px solid #ced4da; border-radius: 4px; overflow-y: auto; font-size: 14px; } #mk-md-editor { resize: none; font-family: 'SFMono-Regular', Consolas, Menlo, monospace; line-height: 1.5; } .mk-preview-pane { line-height: 1.6; } .mk-export-controls { display: flex; gap: 10px; align-items: center; } .mk-tooltip { position: absolute; top: -30px; right: 0; background-color: #333; color: white; padding: 5px 10px; border-radius: 4px; font-size: 12px; opacity: 0; transition: opacity 0.3s; pointer-events: none; } .mk-tooltip.show { opacity: 1; } #mk-prompt-tooltip { top: -30px; right: 0; } .mk-notification { position: fixed; bottom: 20px; right: 20px; background-color: #333; color: white; padding: 10px 20px; border-radius: 4px; z-index: 100000; transform: translateY(20px); opacity: 0; transition: all 0.3s ease; } .mk-notification.show { transform: translateY(0); opacity: 0.9; } .mk-loading { display: flex; justify-content: center; align-items: center; height: 100%; color: #666; font-style: italic; } .mk-error { color: #d9534f; padding: 10px; border: 1px solid #d9534f; border-radius: 4px; } #mk-md-preview h1 { border-bottom: 1px solid #eaecef; padding-bottom: 0.3em; } #mk-md-preview h2 { border-bottom: 1px solid #eaecef; padding-bottom: 0.3em; } #mk-md-preview pre { background-color: #f6f8fa; padding: 16px; border-radius: 6px; overflow: auto; } #mk-md-preview code { background-color: rgba(27,31,35,.05); padding: 0.2em 0.4em; border-radius: 3px; font-size: 85%; } #mk-md-preview pre code { background-color: transparent; padding: 0; } #mk-md-preview blockquote { padding: 0 1em; color: #6a737d; border-left: 0.25em solid #dfe2e5; } #mk-md-preview img { max-width: 100%; } #mk-md-preview table { border-collapse: collapse; width: 100%; overflow: auto; display: block; } #mk-md-preview table th, #mk-md-preview table td { padding: 6px 13px; border: 1px solid #dfe2e5; } #mk-md-preview table tr { background-color: #fff; border-top: 1px solid #c6cbd1; } #mk-md-preview table tr:nth-child(2n) { background-color: #f6f8fa; } `); } } new MarkdownConverterUI(); })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址