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
This commit is contained in:
63
GEMINI.md
Normal file
63
GEMINI.md
Normal file
@@ -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` 作为项目入口点。
|
||||
@@ -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** - 取消选择模式
|
||||
|
||||
## 输出示例
|
||||
|
||||
|
||||
15
src/browser-extension/background.js
Normal file
15
src/browser-extension/background.js
Normal file
@@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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": ["<all_urls>"],
|
||||
|
||||
Reference in New Issue
Block a user