From 2ab8ba9c3c959a069f325e4e6265e14dabb19414 Mon Sep 17 00:00:00 2001 From: empty Date: Wed, 3 Dec 2025 17:20:26 +0800 Subject: [PATCH] feat: add smart element selection mode with shortcut support - Added background script for handling global shortcuts (Alt+Shift+X) - Implemented element highlighting and click-to-extract in content script - Updated manifest to include background worker and commands - Updated documentation with new feature usage - Added GEMINI.md project context --- GEMINI.md | 63 +++++++++++++++++++++ docs/EXTENSION.md | 9 ++- src/browser-extension/background.js | 15 +++++ src/browser-extension/content.css | 12 +++- src/browser-extension/content.js | 85 +++++++++++++++++++++++++++++ src/browser-extension/manifest.json | 12 ++++ 6 files changed, 194 insertions(+), 2 deletions(-) create mode 100644 GEMINI.md create mode 100644 src/browser-extension/background.js diff --git a/GEMINI.md b/GEMINI.md new file mode 100644 index 0000000..fce2d65 --- /dev/null +++ b/GEMINI.md @@ -0,0 +1,63 @@ +# Web2MCP 项目上下文 + +## 项目概览 + +Web2MCP 是一个工具集,旨在将网页内容转换为大语言模型(LLM)友好的格式(Markdown、JSON、XML)。它由两个主要组件组成: +1. **浏览器扩展:** 一个 Chrome 扩展(Manifest V3),允许用户直接从浏览器中框选并提取内容。 +2. **Python 脚本:** 用于程序化网页抓取和内容结构化的独立脚本。 + +## 目录结构 + +* `src/browser-extension/`:Chrome 扩展的源代码。 + * `manifest.json`:扩展配置(Manifest V3)。 + * `content.js`:DOM 遍历和内容提取的核心逻辑。 + * `popup.html` & `popup.js`:扩展弹出窗口的用户界面。 +* `scripts/`:实用脚本。 + * `mcp.py`:使用 `requests` 和 `BeautifulSoup` 获取并结构化网页内容的 Python 脚本。 + * `generate_icons.py`:用于生成扩展图标的辅助工具。 +* `docs/`:文档。 + * `EXTENSION.md`:扩展的详细功能列表和使用说明。 + * `INSTALL.md`:安装指南。 + +## 构建与运行 + +### 浏览器扩展 + +该扩展不需要构建步骤(无 webpack/bundler)。它作为原始源码扩展运行。 + +1. **在 Chrome 中加载:** + * 访问 `chrome://extensions/`。 + * 启用“开发者模式”。 + * 点击“加载已解压的扩展程序”并选择 `src/browser-extension` 目录。 + +### Python 脚本 + +**先决条件:** +* Python 3.x +* 依赖项:`requests`,`beautifulsoup4` + +**安装:** +```bash +pip install requests beautifulsoup4 +``` + +**用法:** +```bash +# 运行抓取脚本(当前目标为硬编码的 URL) +python scripts/mcp.py +``` + +## 主要功能 + +* **内容提取:** 支持标题、段落、代码块、列表、表格、图片和链接。 +* **格式:** 导出为 Markdown、JSON 和 XML。 +* **交互:** + * **扩展:** 支持“区域框选”(拖动选择)和“整页”提取。自动复制到剪贴板。 + * **脚本:** 获取 HTML 并将其解析为结构化的 JSON 文件(默认为 `wechat_dev_seo_structured.json`)。 + +## 开发规范 + +* **扩展架构:** 使用标准 Web 技术(HTML/CSS/JS),无需框架。 +* **Manifest 版本:** 严格遵循 V3。 +* **Python 风格:** 遵循标准 Python 脚本实践。 +* **文档:** 在 `docs/` 文件夹中维护文档;`README.md` 作为项目入口点。 \ No newline at end of file diff --git a/docs/EXTENSION.md b/docs/EXTENSION.md index 82d8d32..bd66347 100644 --- a/docs/EXTENSION.md +++ b/docs/EXTENSION.md @@ -5,6 +5,7 @@ ## 功能特性 - 🎯 **区域框选提取** - 拖拽鼠标框选想要提取的区域 +- 🔍 **元素智能识别** - 快捷键激活,自动识别并高亮网页元素,点击即可提取 - 📄 **整页提取** - 一键提取整个页面内容 - 📝 **多种输出格式** - 支持 Markdown、JSON、XML - 📋 **自动复制** - 提取后自动复制到剪贴板 @@ -35,9 +36,15 @@ 4. 如果是框选模式,拖拽鼠标选择区域 5. 提取完成后内容自动复制到剪贴板 +**智能元素提取模式:** +1. 按下快捷键(默认 `Alt+Shift+X` / Mac: `Option+Shift+X`) +2. 移动鼠标,扩展会自动高亮当前的 HTML 元素 +3. 点击高亮的元素即可提取该区域内容 + ## 快捷操作 -- **ESC** - 取消框选模式 +- **Alt+Shift+X** (Mac: **Option+Shift+X**) - 开启/关闭元素智能识别模式 +- **ESC** - 取消选择模式 ## 输出示例 diff --git a/src/browser-extension/background.js b/src/browser-extension/background.js new file mode 100644 index 0000000..9fe0db0 --- /dev/null +++ b/src/browser-extension/background.js @@ -0,0 +1,15 @@ +chrome.commands.onCommand.addListener((command) => { + if (command === 'toggle-element-selection') { + chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { + if (tabs.length === 0) return; + const tabId = tabs[0].id; + + // 尝试发送消息,如果失败可能是页面未加载content script + chrome.tabs.sendMessage(tabId, { action: 'toggleElementSelection' }) + .catch(() => { + // 可以选择注入脚本或忽略 + console.log('Cannot send message to tab', tabId); + }); + }); + } +}); diff --git a/src/browser-extension/content.css b/src/browser-extension/content.css index a68a4b0..628b491 100644 --- a/src/browser-extension/content.css +++ b/src/browser-extension/content.css @@ -17,6 +17,16 @@ pointer-events: none; } +.llm-extractor-element-highlight { + position: fixed; + border: 2px solid #ff4757; + background: rgba(255, 71, 87, 0.1); + z-index: 999999; + pointer-events: none; + transition: all 0.1s ease-out; + box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.5); +} + .llm-extractor-hint { position: fixed; top: 20px; @@ -73,4 +83,4 @@ transform: translateY(0); opacity: 1; } -} +} \ No newline at end of file diff --git a/src/browser-extension/content.js b/src/browser-extension/content.js index 3668886..03d4df2 100644 --- a/src/browser-extension/content.js +++ b/src/browser-extension/content.js @@ -18,10 +18,85 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { copyToClipboard(formatted); saveToStorage(formatted); sendResponse({ success: true }); + } else if (request.action === 'toggleElementSelection') { + // 如果已经在选择模式,则退出 + if (isSelecting) { + cleanup(); + } else { + // 默认格式或者读取存储的格式? 这里暂时用默认markdown + // 理想情况应该从 storage 读取,这里简化处理 + chrome.storage.local.get('format', (res) => { + currentFormat = res.format || 'markdown'; + startElementSelectionMode(); + }); + } + sendResponse({ success: true }); } return true; }); +// === 元素选择模式 === +let highlightBox = null; +let lastTarget = null; + +function startElementSelectionMode() { + isSelecting = true; + + // 创建提示 + const hint = document.createElement('div'); + hint.className = 'llm-extractor-hint'; + hint.textContent = '移动鼠标选择元素,点击提取,ESC 取消'; + document.body.appendChild(hint); + + // 创建高亮框 + highlightBox = document.createElement('div'); + highlightBox.className = 'llm-extractor-element-highlight'; + highlightBox.style.display = 'none'; + document.body.appendChild(highlightBox); + + // 绑定事件 + document.addEventListener('mousemove', onElementMouseMove, true); + document.addEventListener('click', onElementClick, true); + document.addEventListener('keydown', onKeyDown); +} + +function onElementMouseMove(e) { + if (!isSelecting) return; + + const target = document.elementFromPoint(e.clientX, e.clientY); + + if (target && target !== lastTarget && target !== highlightBox && !target.classList.contains('llm-extractor-hint')) { + lastTarget = target; + + const rect = target.getBoundingClientRect(); + highlightBox.style.display = 'block'; + highlightBox.style.left = rect.left + 'px'; + highlightBox.style.top = rect.top + 'px'; + highlightBox.style.width = rect.width + 'px'; + highlightBox.style.height = rect.height + 'px'; + } +} + +function onElementClick(e) { + if (!isSelecting) return; + + e.preventDefault(); + e.stopPropagation(); + + if (lastTarget) { + // 提取内容 + const content = extractContent(lastTarget); + const formatted = formatContent(content, currentFormat); + + copyToClipboard(formatted); + saveToStorage(formatted); + showNotification('✅ 元素内容已提取'); + } + + cleanup(); +} + +// === 框选模式 === // 开始框选模式 function startSelectionMode() { isSelecting = true; @@ -111,12 +186,18 @@ function onKeyDown(e) { function cleanup() { isSelecting = false; + lastTarget = null; if (selectionBox) { selectionBox.remove(); selectionBox = null; } + if (highlightBox) { + highlightBox.remove(); + highlightBox = null; + } + if (overlay) { overlay.remove(); overlay = null; @@ -128,6 +209,10 @@ function cleanup() { document.removeEventListener('mousedown', onMouseDown); document.removeEventListener('mousemove', onMouseMove); document.removeEventListener('mouseup', onMouseUp); + + document.removeEventListener('mousemove', onElementMouseMove, true); + document.removeEventListener('click', onElementClick, true); + document.removeEventListener('keydown', onKeyDown); } diff --git a/src/browser-extension/manifest.json b/src/browser-extension/manifest.json index 6bb7e3b..a4a4d25 100644 --- a/src/browser-extension/manifest.json +++ b/src/browser-extension/manifest.json @@ -17,6 +17,18 @@ "128": "icons/icon128.png" } }, + "background": { + "service_worker": "background.js" + }, + "commands": { + "toggle-element-selection": { + "suggested_key": { + "default": "Alt+Shift+X", + "mac": "Alt+Shift+X" + }, + "description": "Toggle Element Selection Mode" + } + }, "content_scripts": [ { "matches": [""],