Greasy Fork

Greasy Fork is available in English.

网页内容超级捕手

快速捕获网页正文内容,完整保留原文格式(包括各级标题、加粗斜体、图片表格、超链接等)。支持自由编辑、AI总结,支持一键复制和另存为Word文档两个选项。修复了部分网页图片捕获失败、另存为word图片保存失败的问题,通过多线程并发将图片下载到内存后再另存,另存后自动将图片从内存中清除。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         网页内容超级捕手
// @namespace    http://tampermonkey.net/
// @version      1.1.1
// @description  快速捕获网页正文内容,完整保留原文格式(包括各级标题、加粗斜体、图片表格、超链接等)。支持自由编辑、AI总结,支持一键复制和另存为Word文档两个选项。修复了部分网页图片捕获失败、另存为word图片保存失败的问题,通过多线程并发将图片下载到内存后再另存,另存后自动将图片从内存中清除。
// @author       Mrchen
// @match        *://*/*
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_xmlhttpRequest
// @grant        GM_registerMenuCommand
// @connect      *
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/purify.min.js
// @require      https://cdn.jsdelivr.net/npm/@mozilla/[email protected]/Readability.js
// @require      https://cdn.jsdelivr.net/npm/[email protected]/marked.min.js
// @run-at       document-idle
// ==/UserScript==

(function() {
    'use strict';

    if (window.top !== window.self) return;
    if (window.__WEB_EXTRACTOR_INJECTED__) return;
    window.__WEB_EXTRACTOR_INJECTED__ = true;

    console.log('[Web Content Extractor] 插件已成功注入当前页面');

    if (typeof Readability === 'undefined') {
        console.error('[Web Content Extractor] 致命错误: Readability.js 核心库未加载成功,请检查网络。');
    }
    if (typeof marked === 'undefined') {
        console.warn('[Web Content Extractor] 警告: marked.js (Markdown解析库) 未加载成功,AI 总结将降级为普通文本显示。');
    }

    // ==========================================
    // 1. 配置管理
    // ==========================================
    const DEFAULT_CONFIG = {
        apiUrl: 'https://api.deepseek.com/chat/completions',
        apiKey: '',
        apiModel: 'deepseek-chat',
        apiPrompt: '请帮我精简总结以下网页内容,提取核心观点和关键信息,使用Markdown格式进行排版:\n\n',
        uiTheme: 'dark',
        // Word 导出专属设置
        wordFontCn: '楷体',
        wordFontEn: 'Times New Roman',
        wordSizeBody: '12pt', // 小四
        wordSizeH1: '18pt',   // 小二
        wordSizeH2: '16pt',   // 三号
        wordSizeH3: '14pt'    // 四号
    };

    function getConfig() {
        return {
            apiUrl: GM_getValue('apiUrl', DEFAULT_CONFIG.apiUrl),
            apiKey: GM_getValue('apiKey', DEFAULT_CONFIG.apiKey),
            apiModel: GM_getValue('apiModel', DEFAULT_CONFIG.apiModel),
            apiPrompt: GM_getValue('apiPrompt', DEFAULT_CONFIG.apiPrompt),
            uiTheme: GM_getValue('uiTheme', DEFAULT_CONFIG.uiTheme),
            wordFontCn: GM_getValue('wordFontCn', DEFAULT_CONFIG.wordFontCn),
            wordFontEn: GM_getValue('wordFontEn', DEFAULT_CONFIG.wordFontEn),
            wordSizeBody: GM_getValue('wordSizeBody', DEFAULT_CONFIG.wordSizeBody),
            wordSizeH1: GM_getValue('wordSizeH1', DEFAULT_CONFIG.wordSizeH1),
            wordSizeH2: GM_getValue('wordSizeH2', DEFAULT_CONFIG.wordSizeH2),
            wordSizeH3: GM_getValue('wordSizeH3', DEFAULT_CONFIG.wordSizeH3),
        };
    }

    function setConfig(key, value) {
        GM_setValue(key, value);
    }

    if (typeof GM_registerMenuCommand !== 'undefined') {
        GM_registerMenuCommand('⚙️ 系统设置', openSettingsModal);
    }

    // ==========================================
    // 2. 初始化 Shadow DOM 与核心 UI
    // ==========================================
    if (!document.documentElement) return;

    const host = document.createElement('div');
    host.id = 'web-extractor-root';
    host.style.cssText = 'position: fixed; top: 0; left: 0; width: 0; height: 0; z-index: 2147483647; pointer-events: none; overflow: visible; display: block;';
    document.documentElement.appendChild(host);

    const shadow = host.attachShadow({ mode: 'closed' });

    const style = document.createElement('style');
    style.textContent = `
        :host {
            font-family: "SF Pro Display", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
            --transition-speed: 0.3s;
        }

        /* ----- 主题变量定义 ----- */
        .theme-dark {
            --bg: rgba(15, 23, 42, 0.85);
            --bg-solid: #0f172a;
            --text: #f8fafc;
            --text-muted: #94a3b8;
            --border: rgba(51, 65, 85, 0.8);
            --primary: #00f0ff;
            --primary-hover: #00c3cc;
            --primary-bg: rgba(0, 240, 255, 0.1);
            --shadow: 0 10px 40px rgba(0, 0, 0, 0.5), 0 0 20px rgba(0, 240, 255, 0.15);
            --toast-bg: rgba(0, 0, 0, 0.9);
            --modal-bg: rgba(0, 0, 0, 0.6);
            --glass-blur: blur(12px);
            --btn-md-bg: rgba(255,255,255,0.1);
            --btn-md-hover: rgba(255,255,255,0.2);
        }

        .theme-light {
            --bg: rgba(255, 255, 255, 0.9);
            --bg-solid: #ffffff;
            --text: #1e293b;
            --text-muted: #64748b;
            --border: rgba(226, 232, 240, 0.9);
            --primary: #3b82f6;
            --primary-hover: #2563eb;
            --primary-bg: rgba(59, 130, 246, 0.1);
            --shadow: 0 10px 30px rgba(0, 0, 0, 0.08), 0 4px 6px rgba(0, 0, 0, 0.04);
            --toast-bg: rgba(30, 41, 59, 0.9);
            --modal-bg: rgba(0, 0, 0, 0.3);
            --glass-blur: blur(16px);
            --btn-md-bg: rgba(0,0,0,0.05);
            --btn-md-hover: rgba(0,0,0,0.1);
        }

        .theme-yellow {
            --bg: rgba(250, 246, 233, 0.95);
            --bg-solid: #faf6e9;
            --text: #3f3f3b;
            --text-muted: #78716c;
            --border: rgba(214, 211, 201, 0.8);
            --primary: #65a30d;
            --primary-hover: #4d7c0f;
            --primary-bg: rgba(101, 163, 13, 0.1);
            --shadow: 0 10px 30px rgba(0, 0, 0, 0.06);
            --toast-bg: rgba(63, 63, 59, 0.9);
            --modal-bg: rgba(0, 0, 0, 0.3);
            --glass-blur: blur(10px);
            --btn-md-bg: rgba(0,0,0,0.05);
            --btn-md-hover: rgba(0,0,0,0.1);
        }

        /* 触发条 */
        #trigger-bar {
            position: fixed;
            right: 0;
            top: 40vh;
            width: 6px;
            height: 60px;
            background: var(--primary);
            border-radius: 8px 0 0 8px;
            cursor: pointer;
            pointer-events: auto;
            transition: width var(--transition-speed), box-shadow var(--transition-speed), transform 0.3s cubic-bezier(0.2, 0.8, 0.2, 1);
            box-shadow: 0 0 10px var(--primary-bg);
            z-index: 2147483647;
            user-select: none;
        }
        #trigger-bar:hover {
            width: 14px;
            box-shadow: 0 0 15px var(--primary);
        }

        /* 主面板 */
        #main-panel {
            position: fixed;
            right: 20px;
            top: 5vh;
            width: 440px;
            height: 90vh;
            min-width: 320px;
            min-height: 400px;
            max-width: 95vw;
            max-height: 95vh;
            background: var(--bg);
            backdrop-filter: var(--glass-blur);
            -webkit-backdrop-filter: var(--glass-blur);
            border-radius: 16px;
            box-shadow: var(--shadow);
            border: 1px solid var(--border);
            display: flex;
            flex-direction: column;
            pointer-events: auto;
            transform: translateX(120%);
            transition: transform 0.4s cubic-bezier(0.2, 0.8, 0.2, 1);
            overflow: hidden;
            z-index: 2147483647;
            resize: both; /* 允许自由拉伸大小 */
        }
        #main-panel.open { transform: translateX(0); }

        /* 顶部区域 */
        #header {
            padding: 10px 16px;
            background: rgba(0,0,0,0.04);
            border-bottom: 1px solid var(--border);
            display: flex;
            gap: 10px;
            cursor: move;
            align-items: center;
            user-select: none;
        }

        .tabs { display: flex; gap: 4px; flex: 1; }

        .tab-btn {
            background: transparent;
            border: none;
            color: var(--text-muted);
            padding: 8px 12px;
            font-size: 14px;
            font-weight: 600;
            border-radius: 6px;
            cursor: pointer;
            transition: all 0.2s ease;
        }
        .tab-btn:hover { color: var(--text); background: rgba(128,128,128,0.1); }
        .tab-btn.active {
            background: var(--primary-bg);
            color: var(--primary);
            box-shadow: 0 0 8px var(--primary-bg);
        }

        .icon-btn {
            background: transparent;
            border: 1px solid transparent;
            border-radius: 8px;
            color: var(--text-muted);
            cursor: pointer;
            width: 28px;
            height: 28px;
            display: flex;
            align-items: center;
            justify-content: center;
            font-size: 16px;
            transition: all 0.2s;
        }
        .icon-btn:hover {
            color: var(--text);
            background: rgba(128,128,128,0.1);
        }
        #btn-clear:hover { color: #ef4444; background: rgba(239, 68, 68, 0.1); }

        #content-area {
            flex: 1;
            display: flex;
            flex-direction: column;
            overflow: hidden;
            position: relative;
        }

        /* 独立视图区 */
        .view-content {
            padding: 20px 15px 20px 20px;
            overflow-y: auto;
            color: var(--text);
            font-size: 15px;
            line-height: 1.7;
            word-wrap: break-word;
            display: none;
        }
        .view-content.active {
            display: block;
            flex: 1;
            height: 100%;
            box-sizing: border-box;
        }
        .view-content[contenteditable="true"]:focus {
            outline: none;
            box-shadow: inset 0 0 0 2px var(--primary-bg);
            border-radius: 8px;
        }

        /* 美化并强化滚动条以便于拖动 */
        .view-content::-webkit-scrollbar { width: 8px; }
        .view-content::-webkit-scrollbar-track { background: rgba(128,128,128,0.1); border-radius: 4px; }
        .view-content::-webkit-scrollbar-thumb { background: rgba(128,128,128,0.4); border-radius: 4px; }
        .view-content::-webkit-scrollbar-thumb:hover { background: var(--primary); }

        /* 排版格式 */
        .view-content h1 { font-size: 1.6em; font-weight: 700; margin: 0.5em 0; border-bottom: 1px solid var(--border); padding-bottom: 0.3em; }
        .view-content h2 { font-size: 1.4em; font-weight: 600; margin: 0.8em 0 0.5em; color: var(--primary); }
        .view-content h3 { font-size: 1.2em; font-weight: 600; margin: 0.6em 0; }
        .view-content p { margin-bottom: 1em; }
        .view-content img { max-width: 100%; height: auto; border-radius: 8px; display: block; margin: 15px 0; box-shadow: 0 4px 12px rgba(0,0,0,0.1); }
        .view-content a { color: var(--primary); text-decoration: none; border-bottom: 1px dashed var(--primary); padding-bottom: 1px; }
        .view-content a:hover { text-decoration: solid; }
        .view-content pre { background: var(--bg-solid); padding: 12px; border-radius: 8px; border: 1px solid var(--border); overflow-x: auto; margin-bottom: 1em; }
        .view-content code { background: var(--bg-solid); color: #ec4899; padding: 2px 6px; border-radius: 4px; font-family: 'Fira Code', monospace; font-size: 0.9em; border: 1px solid var(--border); }
        .view-content blockquote { border-left: 4px solid var(--primary); margin: 0 0 1em 0; padding: 8px 12px; background: var(--primary-bg); border-radius: 0 8px 8px 0; color: var(--text-muted); }
        .view-content ul, .view-content ol { margin-bottom: 1em; padding-left: 20px; }

        /* 底部 */
        #footer { padding: 14px 16px; border-top: 1px solid var(--border); background: rgba(0,0,0,0.02); display:flex; gap: 10px; align-items: stretch;}

        .copy-action-btn {
            flex: 1; padding: 10px 4px; color: #fff;
            border: none; border-radius: 8px; font-size: 13px; font-weight: 600; letter-spacing: 0.5px;
            cursor: pointer; transition: all 0.2s;
            display: flex; justify-content: center; align-items: center; gap: 4px;
        }
        #copy-btn {
            background: var(--btn-md-bg); color: var(--text); border: 1px solid var(--border);
        }
        #copy-btn:hover { background: var(--btn-md-hover); transform: translateY(-1px); }

        #save-md-btn {
            background: var(--btn-md-bg); color: var(--text); border: 1px solid var(--border);
        }
        #save-md-btn:hover { background: var(--btn-md-hover); transform: translateY(-1px); }

        #save-word-btn {
            background: var(--primary); box-shadow: 0 4px 12px var(--primary-bg);
        }
        #save-word-btn:hover { filter: brightness(1.1); transform: translateY(-1px); }
        #save-word-btn:active { transform: translateY(1px); }

        .loader {
            display: inline-block; width: 14px; height: 14px;
            border: 2px solid transparent; border-radius: 50%;
            border-top-color: currentColor; border-left-color: currentColor;
            animation: spin 0.8s linear infinite; vertical-align: middle; margin-right: 6px;
        }
        @keyframes spin { to { transform: rotate(360deg); } }
        .loading-text { text-align: center; color: var(--text-muted); margin-top: 60px; font-size: 14px; letter-spacing: 0.5px;}

        #toast-container {
            position: absolute; bottom: 80px; left: 50%; transform: translateX(-50%) translateY(10px);
            background: var(--toast-bg); color: #fff; padding: 10px 20px;
            border-radius: 30px; font-size: 13px; opacity: 0; pointer-events: none;
            transition: all 0.3s cubic-bezier(0.2, 0.8, 0.2, 1); z-index: 10; white-space: nowrap;
            box-shadow: 0 4px 15px rgba(0,0,0,0.2); font-weight: 500;
        }
        #toast-container.show { opacity: 1; transform: translateX(-50%) translateY(0); }

        /* 设置弹窗 */
        #settings-modal {
            position: fixed; top: 0; left: 0; width: 100vw; height: 100vh;
            background: var(--modal-bg); display: none; justify-content: center; align-items: center;
            z-index: 9999; pointer-events: auto; backdrop-filter: blur(4px);
        }
        #settings-modal.show { display: flex; }
        .modal-content {
            background: var(--bg-solid); padding: 30px; border-radius: 16px; width: 90%; max-width: 480px; max-height: 85vh; overflow-y: auto;
            box-shadow: 0 20px 50px rgba(0,0,0,0.3); color: var(--text); border: 1px solid var(--border);
        }
        .modal-content::-webkit-scrollbar { width: 6px; }
        .modal-content::-webkit-scrollbar-thumb { background: var(--border); border-radius: 10px; }

        .modal-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 25px; }
        .modal-header h3 { margin: 0; font-size: 18px; font-weight: 600; color: var(--text); }
        .form-group { margin-bottom: 18px; }
        .form-group label { display: block; font-weight: 500; margin-bottom: 6px; font-size: 13px; color: var(--text-muted);}
        .form-group input, .form-group textarea, .form-group select {
            width: 100%; padding: 10px 12px; border: 1px solid var(--border); border-radius: 8px;
            background: transparent; color: var(--text); box-sizing: border-box; font-family: inherit; font-size: 14px; outline: none;
        }
        .form-group input:focus, .form-group textarea:focus, .form-group select:focus { border-color: var(--primary); }
        .form-group select { appearance: none; background-color: var(--bg); cursor: pointer;}
        .form-group textarea { resize: vertical; min-height: 80px; }

        .section-title { font-size: 14px; font-weight: 600; color: var(--primary); margin: 25px 0 15px 0; border-bottom: 1px dashed var(--border); padding-bottom: 8px;}
        .grid-2col { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }

        .save-btn { width: 100%; padding: 12px; background: var(--primary); color: #fff; border: none; border-radius: 8px; font-size: 15px; font-weight: 600; cursor: pointer; margin-top: 10px; transition: 0.2s; }
        .save-btn:hover { filter: brightness(1.1); }

        .ai-cursor {
            display: inline-block; width: 8px; height: 16px; background-color: var(--primary);
            animation: blink 1s step-end infinite; vertical-align: middle; margin-left: 4px; border-radius: 2px;
        }
        @keyframes blink { 0%, 100% { opacity: 1; box-shadow: 0 0 5px var(--primary); } 50% { opacity: 0; box-shadow: none; } }
        .ai-block { padding: 16px; background: var(--primary-bg); border-left: 4px solid var(--primary); border-radius: 0 8px 8px 0; margin-top: 15px; line-height: 1.8; }
        .stop-btn { background: transparent; border: 1px solid #ef4444; color: #ef4444; border-radius: 6px; padding: 4px 10px; cursor: pointer; font-size: 12px; transition: 0.2s; }
        .stop-btn:hover { background: rgba(239, 68, 68, 0.1); }

        #btn-toggle-key {
            position: absolute; right: 10px; top: 50%; transform: translateY(-50%);
            background: transparent; border: none; cursor: pointer; font-size: 16px;
            padding: 0; opacity: 0.6; transition: 0.2s; color: var(--text);
        }
        #btn-toggle-key:hover { opacity: 1; }
    `;
    shadow.appendChild(style);

    const wrapper = document.createElement('div');
    wrapper.id = 'app-wrapper';
    wrapper.innerHTML = `
        <div id="trigger-bar"></div>
        <div id="main-panel">
            <div id="header">
                <div class="tabs">
                    <button class="tab-btn active" id="tab-extract">📄 一键提取</button>
                    <button class="tab-btn" id="tab-ai">✨ AI 总结</button>
                </div>
                <div style="display:flex; gap:6px;">
                    <button class="tab-btn" id="btn-refresh" title="强制重新执行当前标签任务" style="padding: 6px 10px; font-size: 12px;">🔄 重新执行</button>
                    <button class="tab-btn" id="btn-clear" title="清空当前页内容" style="padding: 6px 10px; font-size: 12px;">🗑️ 清空</button>
                    <button class="icon-btn" id="close-btn" title="收起面板">&times;</button>
                </div>
            </div>

            <div id="content-area">
                <div id="view-extract" class="view-content active" contenteditable="false">
                    <div class="loading-text">点击上方「📄 一键提取」开始提取,<br>提取后的内容可<b>自由编辑,或点击🗑️清空</b>。</div>
                </div>
                <div id="view-ai" class="view-content" contenteditable="false">
                    <div class="loading-text">请先在「📄 一键提取」标签下提取网页内容。<br>切换到本标签即可自动生成总结。</div>
                </div>
            </div>

            <div id="toast-container"></div>
            <div id="footer">
                <button class="icon-btn" id="settings-btn" title="系统设置" style="width:40px; height:40px; border:1px solid var(--border);">⚙️</button>
                <button id="copy-btn" class="copy-action-btn">📋 复制</button>
                <button id="save-md-btn" class="copy-action-btn">📝 存为 MD</button>
                <button id="save-word-btn" class="copy-action-btn">💾 存为 Word</button>
            </div>
        </div>

        <!-- 设置弹窗 -->
        <div id="settings-modal">
            <div class="modal-content">
                <div class="modal-header">
                    <h3>设置面板</h3>
                    <button class="icon-btn close-modal" id="close-modal-btn" style="font-size:24px;">&times;</button>
                </div>
                <div class="form-group">
                    <label>UI 主题风格</label>
                    <select id="cfg-theme">
                        <option value="dark">🌌 科技黑 (Dark)</option>
                        <option value="light">☁️ 极简白 (Light) </option>
                        <option value="yellow">📜 护眼黄 (Yellow) </option>
                    </select>
                </div>
                <div class="form-group">
                    <label>AI 请求地址 (URL)</label>
                    <input type="text" id="cfg-url" placeholder="https://api.deepseek.com/chat/completions">
                </div>
                <div class="form-group">
                    <label>API Key (仅在您的本地保存)</label>
                    <div style="position: relative;">
                        <input type="password" id="cfg-key" placeholder="sk-..." style="width: 100%; box-sizing: border-box; padding-right: 36px;">
                        <button type="button" id="btn-toggle-key" title="显示/隐藏">👁️</button>
                    </div>
                </div>
                <div class="form-group">
                    <label>AI 模型名称</label>
                    <input type="text" id="cfg-model" placeholder="deepseek-chat">
                </div>
                <div class="form-group">
                    <label>系统提示词 (Prompt)</label>
                    <textarea id="cfg-prompt" placeholder="请将以下内容进行精简总结..."></textarea>
                </div>

                <div class="section-title">📄 Word 导出格式设置</div>
                <div class="grid-2col">
                    <div class="form-group">
                        <label>中文字体</label>
                        <select id="cfg-word-cn">
                            <option value="宋体">宋体</option>
                            <option value="黑体">黑体</option>
                            <option value="楷体">楷体</option>
                            <option value="仿宋">仿宋</option>
                            <option value="微软雅黑">微软雅黑</option>
                        </select>
                    </div>
                    <div class="form-group">
                        <label>英文字体</label>
                        <select id="cfg-word-en">
                            <option value="Times New Roman">Times New Roman</option>
                            <option value="Arial">Arial</option>
                            <option value="Calibri">Calibri</option>
                            <option value="Helvetica">Helvetica</option>
                            <option value="Tahoma">Tahoma</option>
                        </select>
                    </div>
                </div>
                <div class="grid-2col">
                    <div class="form-group">
                        <label>正文及三级以下标题字号</label>
                        <select id="cfg-word-sz-body">
                            <option value="26pt">一号</option>
                            <option value="24pt">小一</option>
                            <option value="22pt">二号</option>
                            <option value="18pt">小二</option>
                            <option value="16pt">三号</option>
                            <option value="15pt">小三</option>
                            <option value="14pt">四号</option>
                            <option value="12pt">小四</option>
                            <option value="10.5pt">五号</option>
                            <option value="9pt">小五</option>
                        </select>
                    </div>
                    <div class="form-group">
                        <label>一级标题字号</label>
                        <select id="cfg-word-sz-h1">
                            <option value="26pt">一号</option>
                            <option value="24pt">小一</option>
                            <option value="22pt">二号</option>
                            <option value="18pt">小二</option>
                            <option value="16pt">三号</option>
                            <option value="15pt">小三</option>
                            <option value="14pt">四号</option>
                            <option value="12pt">小四</option>
                            <option value="10.5pt">五号</option>
                            <option value="9pt">小五</option>
                        </select>
                    </div>
                </div>
                <div class="grid-2col">
                    <div class="form-group">
                        <label>二级标题字号</label>
                        <select id="cfg-word-sz-h2">
                            <option value="26pt">一号</option>
                            <option value="24pt">小一</option>
                            <option value="22pt">二号</option>
                            <option value="18pt">小二</option>
                            <option value="16pt">三号</option>
                            <option value="15pt">小三</option>
                            <option value="14pt">四号</option>
                            <option value="12pt">小四</option>
                            <option value="10.5pt">五号</option>
                            <option value="9pt">小五</option>
                        </select>
                    </div>
                    <div class="form-group">
                        <label>三级标题字号</label>
                        <select id="cfg-word-sz-h3">
                            <option value="26pt">一号</option>
                            <option value="24pt">小一</option>
                            <option value="22pt">二号</option>
                            <option value="18pt">小二</option>
                            <option value="16pt">三号</option>
                            <option value="15pt">小三</option>
                            <option value="14pt">四号</option>
                            <option value="12pt">小四</option>
                            <option value="10.5pt">五号</option>
                            <option value="9pt">小五</option>
                        </select>
                    </div>
                </div>

                <button class="save-btn" id="save-cfg-btn">保存并应用设置</button>
            </div>
        </div>
    `;
    shadow.appendChild(wrapper);

    // ==========================================
    // 3. UI 交互逻辑及状态机
    // ==========================================
    const appWrapper = shadow.querySelector('#app-wrapper');
    const triggerBar = shadow.querySelector('#trigger-bar');
    const mainPanel = shadow.querySelector('#main-panel');
    const header = shadow.querySelector('#header');
    const tabExtract = shadow.querySelector('#tab-extract');
    const tabAi = shadow.querySelector('#tab-ai');
    const viewExtract = shadow.querySelector('#view-extract');
    const viewAi = shadow.querySelector('#view-ai');
    const btnRefresh = shadow.querySelector('#btn-refresh');
    const btnClear = shadow.querySelector('#btn-clear');
    const btnClose = shadow.querySelector('#close-btn');
    const btnCopy = shadow.querySelector('#copy-btn');
    const btnSaveMd = shadow.querySelector('#save-md-btn');
    const btnSaveWord = shadow.querySelector('#save-word-btn');
    const btnSettings = shadow.querySelector('#settings-btn');
    const toastContainer = shadow.querySelector('#toast-container');
    const modal = shadow.querySelector('#settings-modal');

    const btnToggleKey = shadow.querySelector('#btn-toggle-key');
    const inputKey = shadow.querySelector('#cfg-key');

    let isPanelOpen = false;
    let currentTab = 'extract';
    let extractHasData = false;
    let aiNeedsUpdate = true;
    let currentAbortController = null;

    btnToggleKey.addEventListener('click', () => {
        if (inputKey.type === 'password') {
            inputKey.type = 'text';
            btnToggleKey.textContent = '🙈';
        } else {
            inputKey.type = 'password';
            btnToggleKey.textContent = '👁️';
        }
    });

    function applyTheme(themeName) { appWrapper.className = `theme-${themeName}`; }
    applyTheme(getConfig().uiTheme);

    const togglePanel = (forceOpen = null) => {
        isPanelOpen = forceOpen !== null ? forceOpen : !isPanelOpen;
        if (isPanelOpen) {
            mainPanel.classList.add('open');
            triggerBar.style.transform = 'translateX(100%)';
        } else {
            mainPanel.classList.remove('open');
            triggerBar.style.transform = 'translateX(0)';
        }
    };

    let isTriggerDragging = false;
    let triggerDragStartY;
    let triggerInitialTop;

    triggerBar.addEventListener('mousedown', (e) => {
        isTriggerDragging = false;
        triggerDragStartY = e.clientY;
        triggerInitialTop = triggerBar.getBoundingClientRect().top;

        const onMouseMove = (moveEvent) => {
            const dy = moveEvent.clientY - triggerDragStartY;
            if (Math.abs(dy) > 3) {
                isTriggerDragging = true;
                let newTop = triggerInitialTop + dy;
                newTop = Math.max(0, Math.min(newTop, window.innerHeight - triggerBar.offsetHeight));
                triggerBar.style.top = `${newTop}px`;
                triggerBar.style.transition = 'none';
            }
        };

        const onMouseUp = () => {
            document.removeEventListener('mousemove', onMouseMove);
            document.removeEventListener('mouseup', onMouseUp);
            triggerBar.style.transition = 'width var(--transition-speed), box-shadow var(--transition-speed), transform 0.3s cubic-bezier(0.2, 0.8, 0.2, 1)';
            if (!isTriggerDragging) { togglePanel(true); }
        };

        document.addEventListener('mousemove', onMouseMove);
        document.addEventListener('mouseup', onMouseUp);
        e.preventDefault();
    });

    btnClose.addEventListener('click', () => togglePanel(false));
    document.addEventListener('mousedown', (e) => {
        if (isPanelOpen && !host.contains(e.target) && modal.style.display !== 'flex') togglePanel(false);
    });

    const showToast = (msg, duration = 2500) => {
        toastContainer.textContent = msg;
        toastContainer.classList.add('show');
        setTimeout(() => toastContainer.classList.remove('show'), duration);
    };

    let isDragging = false, dragStartX, dragStartY, initialRight, initialTop;
    header.addEventListener('mousedown', (e) => {
        if (e.target.tagName.toLowerCase() === 'button') return;
        isDragging = true;
        dragStartX = e.clientX; dragStartY = e.clientY;
        const rect = mainPanel.getBoundingClientRect();
        initialRight = window.innerWidth - rect.right;
        initialTop = rect.top;
        mainPanel.style.transition = 'none';
    });
    document.addEventListener('mousemove', (e) => {
        if (!isDragging) return;
        const panelWidth = mainPanel.offsetWidth;
        let newRight = Math.max(0, Math.min(initialRight + (dragStartX - e.clientX), window.innerWidth - panelWidth));
        let newTop = Math.max(0, Math.min(initialTop + (e.clientY - dragStartY), window.innerHeight - header.offsetHeight));
        mainPanel.style.right = `${newRight}px`;
        mainPanel.style.top = `${newTop}px`;
    });
    document.addEventListener('mouseup', () => {
        if (isDragging) {
            isDragging = false;
            mainPanel.style.transition = 'transform 0.4s cubic-bezier(0.2, 0.8, 0.2, 1)';
        }
    });

    // ==========================================
    // 4. 多标签页核心路由与逻辑
    // ==========================================
    function switchTab(target) {
        currentTab = target;
        if (target === 'extract') {
            tabExtract.classList.add('active');
            tabAi.classList.remove('active');
            viewExtract.classList.add('active');
            viewAi.classList.remove('active');
            if (!extractHasData) runExtract();
        } else if (target === 'ai') {
            tabAi.classList.add('active');
            tabExtract.classList.remove('active');
            viewAi.classList.add('active');
            viewExtract.classList.remove('active');
            if (!extractHasData) {
                showToast('⚠️ 暂无内容,请先提取网页内容');
                switchTab('extract');
                return;
            }
            if (aiNeedsUpdate) { runAI(); }
        }
    }

    tabExtract.addEventListener('click', () => switchTab('extract'));
    tabAi.addEventListener('click', () => switchTab('ai'));

    btnRefresh.addEventListener('click', () => {
        if (currentTab === 'extract') runExtract();
        else if (currentTab === 'ai') runAI(true);
    });

    btnClear.addEventListener('click', () => {
        if (currentAbortController) {
            currentAbortController.abort();
            currentAbortController = null;
        }

        if (currentTab === 'extract') {
            extractHasData = false;
            aiNeedsUpdate = true;
            viewExtract.removeAttribute('contenteditable');
            viewExtract.innerHTML = '<div class="loading-text" style="margin-top: 100px;">🗑️ 网页内容已清空<br><br>请点击上方「🔄 重新执行」</div>';
            viewAi.removeAttribute('contenteditable');
            viewAi.innerHTML = '<div class="loading-text">请先在「一键提取」标签下提取文章。<br>切换到本标签即可自动生成总结。</div>';
            showToast('🗑️ 正文内容已清空');
        } else if (currentTab === 'ai') {
            viewAi.removeAttribute('contenteditable');
            viewAi.innerHTML = '<div class="loading-text" style="margin-top: 100px;">🗑️ 总结内容已清空<br><br>点击上方「🔄 重新执行」可再次生成</div>';
            aiNeedsUpdate = false;
            viewAi.removeAttribute('data-raw-markdown'); // 清空备份的纯文本 markdown
            showToast('🗑️ 总结内容已清空');
        }
    });

    // 监听用户输入事件
    viewExtract.addEventListener('input', () => { aiNeedsUpdate = true; });
    viewAi.addEventListener('input', () => { viewAi.removeAttribute('data-raw-markdown'); });

    // ==========================================
    // 5. 模块:正文提取
    // ==========================================
    function runExtract() {
        if (typeof Readability === 'undefined') {
            viewExtract.innerHTML = '<div class="loading-text" style="color:#ef4444;">❌ Readability引擎未能加载,请检查网络。</div>';
            return;
        }
        viewExtract.removeAttribute('contenteditable');
        viewExtract.innerHTML = '<div class="loading-text"><span class="loader"></span>正在分析网页深层结构...</div>';

        setTimeout(() => {
            try {
                let article;
                try {
                    const documentClone = document.cloneNode(true);
                    const reader = new Readability(documentClone, { keepClasses: false, debug: false });
                    article = reader.parse();
                } catch (cloneErr) {
                    console.warn("[Web Content Extractor] 启动 DOM 净化降级方案...");
                    const cleanDoc = document.implementation.createHTMLDocument(document.title);
                    cleanDoc.body.innerHTML = document.body.innerHTML;
                    const reader2 = new Readability(cleanDoc, { keepClasses: false, debug: false });
                    article = reader2.parse();
                }

                if (article && article.content) {
                    const cleanHTML = DOMPurify.sanitize(article.content, {
                        ALLOWED_TAGS: ['h1','h2','h3','h4','h5','h6','p','a','ul','ol','li','b','i','strong','em','strike','code','hr','br','div','table','thead','tbody','tr','th','td','pre','blockquote','img'],
                        ALLOWED_ATTR: ['href', 'src', 'alt', 'title', 'data-src', 'data-original', 'srcset']
                    });
                    viewExtract.innerHTML = `
                        <h1>${article.title || '无标题文章'}</h1>
                        ${article.byline ? `<p style="color:var(--text-muted); font-size: 0.9em; margin-top:-5px;">作者/来源: ${article.byline}</p>` : ''}
                        <hr style="border:0; border-top: 1px solid var(--border); margin: 15px 0;">
                        ${cleanHTML}
                    `;
                    extractHasData = true;
                    aiNeedsUpdate = true;
                    viewExtract.setAttribute('contenteditable', 'true');
                    showToast('✅ 提取成功,内容支持直接编辑');
                } else throw new Error("未能识别到网页主体。");
            } catch (err) {
                viewExtract.innerHTML = `<div class="loading-text" style="color:#ef4444;">❌ 提取失败<br><br><small>${err.message}</small></div>`;
            }
        }, 300);
    }

    // ==========================================
    // 6. 模块:流式 AI 总结
    // ==========================================
    function runAI(force = false) {
        if (currentAbortController) {
            if (force) {
                currentAbortController.abort();
            } else return;
        }

        const currentText = viewExtract.innerText.trim();
        if (!currentText || !extractHasData) return;

        const config = getConfig();
        if (!config.apiKey) {
            openSettingsModal();
            showToast('⚠️ 请先配置 API Key');
            switchTab('extract');
            return;
        }

        aiNeedsUpdate = false;
        viewAi.removeAttribute('contenteditable');
        viewAi.removeAttribute('data-raw-markdown'); // 清空旧的 Markdown
        viewAi.innerHTML = `
            <h3 style="color: var(--primary); margin-top:0; display:flex; justify-content:space-between; align-items:center;">
                <span>🤖 AI 智能总结</span>
                <button id="btn-stop-ai" class="stop-btn">⏹️ 停止生成</button>
            </h3>
            <div id="ai-output" class="ai-block">
                <span style="color:var(--text-muted); font-style:italic;" id="ai-status">正在连接 AI 服务...</span><span class="ai-cursor"></span>
            </div>
        `;

        const btnStopAi = shadow.querySelector('#btn-stop-ai');
        const statusSpan = shadow.querySelector('#ai-status');
        const outputDiv = shadow.querySelector('#ai-output');

        const payload = {
            model: config.apiModel,
            messages: [
                { role: "system", content: "你是一个专业的阅读助手,擅长提取文章核心内容。请直接输出精简总结,使用markdown排版。" },
                { role: "user", content: config.apiPrompt + "\n\n" + currentText.substring(0, 12000) }
            ],
            temperature: 0.5,
            stream: true
        };

        const thisAbortController = new AbortController();
        currentAbortController = thisAbortController;

        let fullText = "";
        let streamBuffer = "";
        let isDone = false;
        let lastIndex = 0;
        let isStream = true;

        let streamHandled = false;
        let streamReader = null;
        let hasFinished = false;

        // 统一提取渲染函数
        const renderOutput = (text) => {
            if (typeof marked !== 'undefined') {
                try {
                    let parsedHTML = marked.parse(text);
                    outputDiv.innerHTML = DOMPurify.sanitize(parsedHTML) + '<span class="ai-cursor"></span>';
                } catch (e) {
                    let fallbackHTML = text.replace(/\n/g, '<br>').replace(/\*\*(.*?)\*\*/g, '<strong style="color:var(--text);">$1</strong>').replace(/\*(.*?)\*/g, '<em>$1</em>');
                    outputDiv.innerHTML = fallbackHTML + '<span class="ai-cursor"></span>';
                }
            } else {
                let fallbackHTML = text.replace(/\n/g, '<br>').replace(/\*\*(.*?)\*\*/g, '<strong style="color:var(--text);">$1</strong>').replace(/\*(.*?)\*/g, '<em>$1</em>');
                outputDiv.innerHTML = fallbackHTML + '<span class="ai-cursor"></span>';
            }
            if (currentTab === 'ai') viewAi.scrollTop = viewAi.scrollHeight;
        };

        const processChunk = (chunk) => {
            // 检测是否非流式同步返回(如配置报错或模型不支持流式)
            if (fullText.length === 0 && streamBuffer.length === 0 && chunk.trim().startsWith('{') && !chunk.includes('data: ')) {
                isStream = false;
            }

            if (!isStream) {
                streamBuffer += chunk;
                return;
            }

            streamBuffer += chunk;
            const lines = streamBuffer.split('\n');
            streamBuffer = lines.pop() || "";

            for (let line of lines) {
                const trimmedLine = line.trim();
                if (!trimmedLine) continue;
                if (trimmedLine === 'data: [DONE]') { isDone = true; break; }

                if (trimmedLine.startsWith('data: ')) {
                    const jsonStr = trimmedLine.substring(6).trim();
                    if (!jsonStr || jsonStr === '[DONE]') continue;

                    try {
                        const data = JSON.parse(jsonStr);
                        const content = data.choices?.[0]?.delta?.content || "";
                        if (content) {
                            fullText += content;
                            renderOutput(fullText);
                        }
                    } catch (e) {
                        // 忽略单个数据块的 JSON 解析错误,防止断流
                    }
                }
            }
        };

        const cleanUp = () => {
            if (currentAbortController === thisAbortController) {
                if (btnStopAi && btnStopAi.parentNode) btnStopAi.remove();
                currentAbortController = null;
                viewAi.setAttribute('contenteditable', 'true');
            }
        };

        const finishGeneration = () => {
            if (hasFinished) return;
            hasFinished = true;

            if (!isStream) {
                try {
                    const data = JSON.parse(streamBuffer);
                    fullText = data.choices?.[0]?.message?.content || "";
                    renderOutput(fullText);
                    showToast('💡 当前模型不支持流式,已为您同步生成');
                } catch (e) {
                    outputDiv.innerHTML = `<span style="color:#ef4444;">❌ 解析响应数据失败或模型未响应正确内容</span>`;
                }
            } else if (streamBuffer.trim() && !isDone) {
                processChunk('\n');
            }

            const cursor = shadow.querySelector('.ai-cursor');
            if (cursor) cursor.remove();
            viewAi.dataset.rawMarkdown = fullText;
            cleanUp();
        };

        // 绑定手动停止事件
        btnStopAi.addEventListener('click', () => {
             if (currentAbortController === thisAbortController) {
                 thisAbortController.abort();
                 showToast('⏹️ 用户手动终止生成');
             }
        });

        const requestObj = GM_xmlhttpRequest({
            method: 'POST',
            url: config.apiUrl,
            headers: {
                'Content-Type': 'application/json',
                'Authorization': `Bearer ${config.apiKey}`,
                'Accept': 'application/json, text/event-stream'
            },
            data: JSON.stringify(payload),
            responseType: 'stream',
            onreadystatechange: async function(response) {
                if (thisAbortController.signal.aborted) return;

                // 不能在 onload 中读取流。必须在 readyState === 2 或 3 时尽早拦截
                if (!streamHandled && response.response instanceof ReadableStream) {
                    streamHandled = true;
                    if (statusSpan && statusSpan.parentNode) statusSpan.remove();

                    try {
                        streamReader = response.response.getReader();
                        const decoder = new TextDecoder('utf-8');
                        while (!isDone) {
                            const { value, done } = await streamReader.read();
                            if (done) { isDone = true; break; }
                            if (thisAbortController.signal.aborted) { streamReader.cancel(); break; }

                            const chunkStr = decoder.decode(value, { stream: true });
                            processChunk(chunkStr);
                        }
                        finishGeneration();
                    } catch (e) {
                        outputDiv.innerHTML = `<span style="color:#ef4444;">❌ 生成失败: 流读取异常 (${e.message})</span>`;
                        cleanUp();
                    }
                    return;
                }

                // 兜底方案:老旧环境若不支持 ReadableStream 返回,采用传统的 responseText 切片渐进读取
                if (!streamHandled && (response.readyState === 3 || response.readyState === 4)) {
                    if (statusSpan && statusSpan.parentNode) statusSpan.remove();

                    const currentResponseText = response.responseText || "";
                    if (currentResponseText.length > lastIndex) {
                        const newText = currentResponseText.substring(lastIndex);
                        lastIndex = currentResponseText.length;
                        processChunk(newText);
                    }

                    if (response.readyState === 4) {
                        finishGeneration();
                    }
                }
            },
            onload: function(response) {
                if (thisAbortController.signal.aborted) return;

                // HTTP 状态非 200 的报错处理
                if (response.status !== 200) {
                    let errorMsg = `HTTP 错误状态: ${response.status}`;
                    try {
                        const errData = JSON.parse(response.responseText || "{}");
                        errorMsg = errData.error?.message || errorMsg;
                    } catch(e){}
                    outputDiv.innerHTML = `<span style="color:#ef4444;">❌ 生成失败: ${errorMsg}</span>`;
                    cleanUp();
                    return;
                }

                // 仅作为防丢帧措施:如果既不支持 ReadableStream 也没有触发 readyState = 3
                if (!streamHandled && lastIndex === 0) {
                   if (statusSpan && statusSpan.parentNode) statusSpan.remove();
                   const text = response.responseText || "";
                   if (text) processChunk(text);
                   finishGeneration();
                }
            },
            onerror: function(error) {
                if (thisAbortController.signal.aborted) return;
                outputDiv.innerHTML = `<span style="color:#ef4444;">❌ 生成失败: 网络请求异常 (请检查API地址、网络或代理环境)</span>`;
                cleanUp();
            }
        });

        // 监听 AbortController 发出的终止信号切断底层网络请求
        thisAbortController.signal.addEventListener('abort', () => {
            requestObj.abort();
            if (statusSpan) {
                statusSpan.textContent = '⏹️ 已手动终止生成';
                statusSpan.style.color = '#ef4444';
            }
            const cursor = shadow.querySelector('.ai-cursor');
            if (cursor) cursor.remove();
            cleanUp();
        });
    }

    // ==========================================
    // 7. 设置面板模块
    // ==========================================
    function openSettingsModal() {
        const config = getConfig();
        shadow.querySelector('#cfg-theme').value = config.uiTheme;
        shadow.querySelector('#cfg-url').value = config.apiUrl;
        shadow.querySelector('#cfg-key').value = config.apiKey;
        inputKey.type = 'password';
        btnToggleKey.textContent = '👁️';
        shadow.querySelector('#cfg-model').value = config.apiModel;
        shadow.querySelector('#cfg-prompt').value = config.apiPrompt;

        // Word 导出设置反填
        shadow.querySelector('#cfg-word-cn').value = config.wordFontCn;
        shadow.querySelector('#cfg-word-en').value = config.wordFontEn;
        shadow.querySelector('#cfg-word-sz-body').value = config.wordSizeBody;
        shadow.querySelector('#cfg-word-sz-h1').value = config.wordSizeH1;
        shadow.querySelector('#cfg-word-sz-h2').value = config.wordSizeH2;
        shadow.querySelector('#cfg-word-sz-h3').value = config.wordSizeH3;

        modal.classList.add('show');
        togglePanel(true);
    }

    btnSettings.addEventListener('click', openSettingsModal);
    shadow.querySelector('#close-modal-btn').addEventListener('click', () => modal.classList.remove('show'));

    shadow.querySelector('#save-cfg-btn').addEventListener('click', () => {
        const selectedTheme = shadow.querySelector('#cfg-theme').value;
        setConfig('uiTheme', selectedTheme);
        setConfig('apiUrl', shadow.querySelector('#cfg-url').value.trim());
        setConfig('apiKey', inputKey.value.trim());
        setConfig('apiModel', shadow.querySelector('#cfg-model').value.trim());
        setConfig('apiPrompt', shadow.querySelector('#cfg-prompt').value.trim());

        // 保存 Word 导出设置,若用户清空则回落为默认值
        setConfig('wordFontCn', shadow.querySelector('#cfg-word-cn').value.trim() || '楷体');
        setConfig('wordFontEn', shadow.querySelector('#cfg-word-en').value.trim() || 'Times New Roman');
        setConfig('wordSizeBody', shadow.querySelector('#cfg-word-sz-body').value.trim() || '12pt');
        setConfig('wordSizeH1', shadow.querySelector('#cfg-word-sz-h1').value.trim() || '18pt');
        setConfig('wordSizeH2', shadow.querySelector('#cfg-word-sz-h2').value.trim() || '16pt');
        setConfig('wordSizeH3', shadow.querySelector('#cfg-word-sz-h3').value.trim() || '14pt');

        applyTheme(selectedTheme);
        modal.classList.remove('show');
        showToast('✅ 设置已应用');
    });

    // ==========================================
    // 8. 复制模块 (Markdown 与 Word 智能路由)
    // ==========================================
    function getActiveView() {
        return currentTab === 'extract' ? viewExtract : viewAi;
    }

    // --- 提取内容的公共函数 (确保复制和导出MD的格式100%一致) ---
    function extractTextAndHtml() {
        const targetView = getActiveView();
        let cleanNode = targetView.cloneNode(true);
        const stopBtn = cleanNode.querySelector('#btn-stop-ai');
        if (stopBtn) stopBtn.remove();

        // 移除 GitHub 中因为洗掉类名变为纯粹空 a 标签的锚点
        cleanNode.querySelectorAll('a').forEach(a => {
            if (!a.textContent.trim() && !a.querySelector('img')) {
                a.remove();
            }
        });

        // 移除所有的空块级元素,防止富文本粘贴时Markdown解析器产生幽灵空行
        cleanNode.querySelectorAll('p, div, h1, h2, h3, h4, h5, h6, li, span').forEach(el => {
            if (!el.textContent.trim() && !el.querySelector('img') && !el.querySelector('canvas')) {
                el.remove();
            }
        });

        // 在复制前把图片的相对路径修正为绝对路径
        const images = cleanNode.querySelectorAll('img');
        images.forEach(img => {
            let src = img.getAttribute('src') || img.getAttribute('data-src') || img.getAttribute('data-original');
            if (src && !src.startsWith('data:')) {
                try {
                    if (src.startsWith('//')) src = window.location.protocol + src;
                    else if (src.startsWith('/')) src = window.location.origin + src;
                    else if (!src.startsWith('http')) src = new URL(src, window.location.href).href;
                    img.src = src; // 写回标准的绝对路径
                } catch (e) {}
            }
        });

        // 提取富文本内容
        const cleanHTML = cleanNode.innerHTML;

        // 提取纯文本内容
        let cleanText = "";

        if (currentTab === 'ai' && viewAi.dataset.rawMarkdown) {
            // 如果在 AI 界面并且存在原始 Markdown,则优先采用原始 Markdown,防止 # 丢失
            cleanText = viewAi.dataset.rawMarkdown;
        } else {
            // 针对 GitHub / README 等纯文本编辑器环境,手动构建简易 Markdown 结构以保留语义
            const mdNode = cleanNode.cloneNode(true);

            // 必须先处理图片,否则后续 a 标签的处理会抹除 img 元素
            mdNode.querySelectorAll('img').forEach(img => {
                const alt = img.getAttribute('alt') || 'image';
                let src = img.src;

                if (src && src.includes('github.com') && src.includes('/blob/')) {
                    src = src.replace(/\/blob\//, '/raw/');
                }

                const span = document.createElement('span');
                span.textContent = src ? `![${alt}](${src})` : '';
                img.replaceWith(span);
            });

            mdNode.querySelectorAll('h1').forEach(h => h.prepend('# '));
            mdNode.querySelectorAll('h2').forEach(h => h.prepend('## '));
            mdNode.querySelectorAll('h3').forEach(h => h.prepend('### '));
            mdNode.querySelectorAll('h4,h5,h6').forEach(h => h.prepend('#### '));
            mdNode.querySelectorAll('strong, b').forEach(s => { s.prepend('**'); s.append('**'); });
            mdNode.querySelectorAll('em, i').forEach(e => { e.prepend('*'); e.append('*'); });
            mdNode.querySelectorAll('li').forEach(li => li.prepend('* '));
            mdNode.querySelectorAll('blockquote').forEach(bq => bq.prepend('> '));
            mdNode.querySelectorAll('code').forEach(c => { if(c.parentElement.tagName !== 'PRE') { c.prepend('`'); c.append('`'); } });
            mdNode.querySelectorAll('pre').forEach(p => { p.prepend('```\n'); p.append('\n```'); });

            // 处理链接:此时 innerText 已包含图片的 Markdown 标记
            mdNode.querySelectorAll('a').forEach(a => {
                let href = a.getAttribute('href');
                if(href && !href.startsWith('javascript:')) {
                    // 链接地址也同步修复 GitHub 路径
                    if (href.includes('github.com') && href.includes('/blob/')) {
                        href = href.replace(/\/blob\//, '/raw/');
                    }
                    const linkText = a.textContent.trim() || 'link';
                    a.textContent = `[${linkText}](${href})`;
                }
            });

            // 挂载并提取 innerText 以获得正确的换行排版
            mdNode.style.position = 'absolute';
            mdNode.style.left = '-9999px';
            mdNode.style.whiteSpace = 'pre-wrap';
            document.body.appendChild(mdNode);
            cleanText = mdNode.innerText;
            document.body.removeChild(mdNode);

            // 彻底消除由于 DOM 结构转换产生的多余空白行
            cleanText = cleanText.replace(/(\r?\n[\t ]*){3,}/g, '\n\n');
        }

        return { cleanHTML, cleanText };
    }

    // --- 一键复制 (富文本/纯文本双轨复制) ---
    btnCopy.addEventListener('click', async () => {
        if (currentTab === 'ai' && currentAbortController) {
             showToast('⚠️ AI 生成未完成,无法完整复制'); return;
        }
        if (!extractHasData) { showToast('⚠️ 暂无内容可复制'); return; }

        const originalText = btnCopy.textContent;
        btnCopy.textContent = '🔄 复制中...';

        try {
            const { cleanHTML, cleanText } = extractTextAndHtml();

            if (typeof ClipboardItem !== 'undefined') {
                await navigator.clipboard.write([new ClipboardItem({
                    'text/html': new Blob([cleanHTML], { type: 'text/html' }),
                    'text/plain': new Blob([cleanText], { type: 'text/plain' })
                })]);
            } else {
                await navigator.clipboard.writeText(cleanText);
            }
            showToast('✅ 已复制到剪贴板');
            btnCopy.textContent = '✅ 复制成功';
        } catch (err) {
            showToast('❌ 剪贴板受限,请手动框选复制');
        }
        setTimeout(() => { btnCopy.textContent = originalText; }, 2000);
    });

    // --- 另存为 Markdown (下载为 .md) ---
    btnSaveMd.addEventListener('click', async () => {
        if (currentTab === 'ai' && currentAbortController) {
             showToast('⚠️ AI 生成未完成,无法完整处理'); return;
        }
        if (!extractHasData) { showToast('⚠️ 暂无内容可保存'); return; }

        const originalText = btnSaveMd.textContent;
        btnSaveMd.textContent = '🔄 准备处理...';

        try {
            const { cleanText } = extractTextAndHtml();

            const targetView = getActiveView();
            const h1Element = targetView.querySelector('h1');
            const documentTitle = h1Element ? h1Element.innerText.trim().replace(/[\\/:*?"<>|]/g, '') : (currentTab === 'ai' ? 'AI生成总结' : '网页提取内容');
            const filename = documentTitle + '.md';

            // 加入 BOM (\ufeff) 有助于兼容部分编辑器对 utf-8 编码的识别
            const blob = new Blob(['\ufeff', cleanText], { type: 'text/markdown;charset=utf-8' });
            const url = URL.createObjectURL(blob);
            const a = document.createElement('a');
            a.href = url;
            a.download = filename;

            document.body.appendChild(a);
            a.click();
            document.body.removeChild(a);

            setTimeout(() => URL.revokeObjectURL(url), 3000);

            showToast('✅ 已成功保存为 Markdown 文档');
            btnSaveMd.textContent = '✅ 保存成功';
        } catch (err) {
            console.error(err);
            showToast('❌ 保存失败,请重试');
        } finally {
            setTimeout(() => { btnSaveMd.textContent = originalText; }, 2000);
        }
    });

    // --- 图片转换为 Base64 助手函数 ---
    function fetchImageAsBase64(url, maxWidth) {
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: 'GET',
                url: url,
                responseType: 'blob',
                timeout: 10000,
                ontimeout: function() {
                    reject(new Error('Image download timeout: ' + url));
                },
                onload: function(response) {
                    if (response.status !== 200) {
                        reject(new Error('Network response was not ok, status: ' + response.status));
                        return;
                    }
                    const blob = response.response;
                    const objectUrl = URL.createObjectURL(blob);
                    const img = new Image();
                    img.onload = function() {
                        let width = img.width;
                        let height = img.height;

                        if (width > maxWidth) {
                            height = Math.round((maxWidth / width) * height);
                            width = maxWidth;
                        }

                        const canvas = document.createElement('canvas');
                        canvas.width = width;
                        canvas.height = height;
                        const ctx = canvas.getContext('2d');

                        ctx.fillStyle = '#FFFFFF';
                        ctx.fillRect(0, 0, width, height);
                        ctx.drawImage(img, 0, 0, width, height);

                        const dataURL = canvas.toDataURL('image/jpeg', 0.85);
                        URL.revokeObjectURL(objectUrl);
                        resolve(dataURL);
                    };
                    img.onerror = function(e) {
                        URL.revokeObjectURL(objectUrl);
                        reject(e);
                    };
                    img.src = objectUrl;
                },
                onerror: function(err) {
                    reject(err);
                }
            });
        });
    }

    // --- 另存为 Word (下载为 .doc) ---
    btnSaveWord.addEventListener('click', async () => {
        if (btnSaveWord.dataset.processing === 'true') {
            showToast('⏳ 正在处理中,请勿重复点击');
            return;
        }

        if (currentTab === 'ai' && currentAbortController) {
             showToast('⚠️ AI 生成未完成,无法完整处理'); return;
        }
        if (!extractHasData) { showToast('⚠️ 暂无内容可保存'); return; }

        btnSaveWord.dataset.processing = 'true'; // 加锁
        const targetView = getActiveView();
        const originalText = btnSaveWord.textContent;
        btnSaveWord.textContent = '🔄 准备处理...';

        await new Promise(resolve => setTimeout(resolve, 30));

        try {
            const config = getConfig();
            let cloneNode = targetView.cloneNode(true);
            const stopBtn = cloneNode.querySelector('#btn-stop-ai');
            if (stopBtn) stopBtn.remove();

            const wordCSS = `
                body, p, div, span, li, a, h1, h2, h3, h4, h5, h6 {
                    font-family: "${config.wordFontEn}", "${config.wordFontCn}", serif;
                    mso-ascii-font-family: "${config.wordFontEn}";
                    mso-fareast-font-family: "${config.wordFontCn}";
                    mso-hansi-font-family: "${config.wordFontEn}";
                }
                code, pre {
                    font-family: 'Courier New', Courier, monospace;
                    mso-ascii-font-family: 'Courier New';
                    mso-fareast-font-family: "${config.wordFontCn}";
                    background-color: #f3f4f6;
                    padding: 4pt;
                    font-size: ${config.wordSizeBody};
                }
                img {
                    max-width: 600px;
                    height: auto;
                }
            `;

            const elements = cloneNode.querySelectorAll('*');
            elements.forEach(el => {
                const tag = el.tagName.toLowerCase();

                if (['p', 'ul', 'ol', 'li', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tag)) {
                    el.style.marginTop = '6pt';
                    el.style.marginBottom = '6pt';
                    el.style.lineHeight = '1.5';
                }

                if (['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tag)) {
                    el.style.fontWeight = 'bold';
                    el.style.marginTop = '12pt';
                }

                if (tag === 'h1') {
                    el.style.fontSize = config.wordSizeH1;
                } else if (tag === 'h2') {
                    el.style.fontSize = config.wordSizeH2;
                } else if (tag === 'h3') {
                    el.style.fontSize = config.wordSizeH3;
                } else if (['h4', 'h5', 'h6', 'p', 'span', 'div', 'li'].includes(tag)) {
                    el.style.fontSize = config.wordSizeBody;
                }
            });

            const images = cloneNode.querySelectorAll('img');
            const tasks = [];

            images.forEach(img => {
                let src = img.getAttribute('src') || img.getAttribute('data-src') || img.getAttribute('data-original');
                if (!src || src.startsWith('data:')) return;

                if (src.startsWith('//')) src = window.location.protocol + src;
                else if (src.startsWith('/')) src = window.location.origin + src;
                else if (!src.startsWith('http')) src = new URL(src, window.location.href).href;

                tasks.push({ img, src });
            });

            const totalTasks = tasks.length;
            let completedTasks = 0;

            if (totalTasks > 0) {
                btnSaveWord.textContent = `🔄 处理图片 (0/${totalTasks})...`;

                const MAX_CONCURRENT = 5;
                let currentIndex = 0;

                const worker = async () => {
                    while (currentIndex < totalTasks) {
                        const taskIndex = currentIndex++;
                        const { img, src } = tasks[taskIndex];

                        try {
                            const base64Data = await fetchImageAsBase64(src, 600);
                            img.src = base64Data;
                            img.removeAttribute('srcset');
                            img.removeAttribute('data-src');
                            img.removeAttribute('data-original');
                            img.removeAttribute('loading');
                        } catch (e) {
                            console.warn('[Web Content Extractor] 图片转Base64失败,保留原图链接:', src, e);
                        } finally {
                            completedTasks++;
                            btnSaveWord.textContent = `🔄 处理图片 (${completedTasks}/${totalTasks})...`;
                        }
                    }
                };

                const workers = Array.from({ length: Math.min(MAX_CONCURRENT, totalTasks) }).map(() => worker());
                await Promise.all(workers);
            }

            btnSaveWord.textContent = '🔄 正在生成文档...';

            const h1Element = cloneNode.querySelector('h1');
            const documentTitle = h1Element ? h1Element.innerText.trim().replace(/[\\/:*?"<>|]/g, '') : (currentTab === 'ai' ? 'AI生成总结' : '网页提取内容');
            const filename = documentTitle + '.doc';

            const htmlHeader = `<html xmlns:o='urn:schemas-microsoft-com:office:office' xmlns:w='urn:schemas-microsoft-com:office:word' xmlns='http://www.w3.org/TR/REC-html40'>
            <head>
                <meta charset='utf-8'>
                <title>${documentTitle}</title>
                <style>${wordCSS}</style>
            </head><body>`;
            const htmlFooter = `</body></html>`;

            let fullHTML = htmlHeader + cloneNode.innerHTML + htmlFooter;
            fullHTML = fullHTML.replace(/&quot;/g, "'");

            const blob = new Blob(['\ufeff', fullHTML], { type: 'application/msword' });
            const url = URL.createObjectURL(blob);
            const a = document.createElement('a');
            a.href = url;
            a.download = filename;

            document.body.appendChild(a);
            a.click();
            document.body.removeChild(a);

            setTimeout(() => URL.revokeObjectURL(url), 3000);

            showToast('✅ 已成功保存为 Word 文档');
            btnSaveWord.textContent = '✅ 保存成功';
            btnSaveWord.style.background = '#10b981';
        } catch (err) {
            console.error(err);
            showToast('❌ 保存失败,请重试');
        } finally {
            delete btnSaveWord.dataset.processing;
            setTimeout(() => {
                btnSaveWord.textContent = originalText;
                btnSaveWord.style.background = '';
            }, 2000);
        }
    });

})();