commit 7f7c13bf7f4a41016d9f6484e61eb9f114217351 Author: hubian <908234780@qq.com> Date: Wed Apr 8 10:54:50 2026 +0800 初始化网页助手浏览器插件 功能: - 右键菜单扩展(收藏、总结、深度研究) - 智能页面内容提取(文本+图片) - 收藏管理界面 - AI总结与深度研究 - 支持自定义API配置(OpenAI/Ollama) 技术: - Chrome Extension Manifest V3 - Service Worker后台脚本 - Content Script内容提取 - 智能文章主体识别 - 本地Storage收藏存储 图标已生成,可替换为更好设计 diff --git a/README.md b/README.md new file mode 100644 index 0000000..8d1ed79 --- /dev/null +++ b/README.md @@ -0,0 +1,110 @@ +# 网页助手 - 浏览器扩展插件 + +> 右键菜单扩展:快速收藏网页、AI总结、深度研究分析 + +## 功能特点 + +### 📌 收藏功能 +- **收藏网页**:一键保存网页内容(文本+图片) +- **收藏选中内容**:右键选中文字即可收藏 +- **收藏图片**:右键图片直接收藏 + +### 📝 AI总结 +- **总结网页**:AI智能提炼网页核心要点 +- **总结选中内容**:快速总结选中的文字 + +### 🔍 深度研究 +- **深度研究网页**:AI全面分析网页内容 +- **深度研究选中内容**:深入研究特定文字 + +## 安装方法 + +### Chrome浏览器 + +1. 打开 Chrome,访问 `chrome://extensions/` +2. 开启右上角「开发者模式」 +3. 点击「加载已解压的扩展程序」 +4. 选择 `web-context-extension` 目录 + +### Edge浏览器 + +1. 打开 Edge,访问 `edge://extensions/` +2. 开启左下角「开发人员模式」 +3. 点击「加载解压缩的扩展」 +4. 选择 `web-context-extension` 目录 + +## 使用方法 + +1. 在任意网页右键点击 +2. 选择「网页助手」菜单 +3. 选择需要的功能: + - 📌 收藏此网页 + - 📝 AI总结此页 + - 🔍 深度研究此页 + +4. 点击插件图标查看结果和收藏列表 + +## API配置 + +要在插件设置中配置AI API: + +### OpenAI API +``` +API地址: https://api.openai.com/v1 +API Key: sk-xxxx +模型: gpt-4 / gpt-3.5-turbo +``` + +### 本地LLM服务 (Ollama) +``` +Ollama地址: http://localhost:11434 +Ollama模型: llama3:8b / qwen:7b +``` + +### 自定义API +支持任何兼容OpenAI格式的API服务。 + +## 项目结构 + +``` +web-context-extension/ +├── manifest.json # 扩展配置 +├── src/ +│ ├── background.js # 后台脚本(右键菜单处理) +│ ├── content.js # 内容脚本(页面内容提取) +│ ├── popup.html # 弹窗界面 +│ └── popup.js # 弹窗逻辑 +├── icons/ # 图标资源 +│ ├── icon16.png +│ ├── icon48.png +│ └── icon128.png +└── README.md +``` + +## 技术特点 + +- **Manifest V3**:使用最新的Chrome扩展规范 +- **智能内容提取**:自动识别文章主体,过滤广告和干扰内容 +- **图片提取**:智能提取页面重要图片,过滤小图标和广告图 +- **本地存储**:收藏数据保存在浏览器本地,无需服务器 + +## 版本历史 + +### v0.1.0 (2026-04-08) +- 初始版本 +- 右键菜单扩展功能 +- 网页内容智能提取 +- 收藏、总结、深度研究功能 +- 支持自定义API配置 + +## 待开发功能 + +- [ ] 导出收藏数据(JSON/HTML) +- [ ] 收藏分类和标签 +- [ ] 搜索收藏内容 +- [ ] 同步到云端 +- [ ] 更多AI分析功能 + +## License + +MIT \ No newline at end of file diff --git a/icons/README.md b/icons/README.md new file mode 100644 index 0000000..df0c071 --- /dev/null +++ b/icons/README.md @@ -0,0 +1,48 @@ +# 图标说明 + +插件需要以下尺寸的PNG图标: + +- `icon16.png` - 16x16 像素(工具栏) +- `icon48.png` - 48x48 像素(扩展管理页) +- `icon128.png` - 128x128 像素(Chrome Web Store) + +## 获取图标的方法 + +### 方法1:在线生成 + +访问以下网站生成图标: +- https://www.flaticon.com/ +- https://icon-icons.com/ +- https://favicon.io/ + +搜索关键词:web, browser, assistant, tool + +### 方法2:使用Favicon生成器 + +访问 https://favicon.io/favicon-generator/ + +输入: +- Text: 🌐 或 WA +- Background: 圆形,渐变色 #667eea → #764ba2 +- Font Family: Sans-serif +- Font Size: 80 + +下载后解压出不同尺寸的图标。 + +### 方法3:临时使用占位符 + +如果暂时没有图标,可以临时修改 manifest.json: + +```json +"icons": { + "16": "data:image/svg+xml,W" +} +``` + +注意:Chrome Manifest V3 不支持 data URI 图标,必须使用实际PNG文件。 + +## 推荐图标设计 + +- 主色调:#667eea(紫色渐变) +- 图标元素:🌐 地球、🔍 搜索、📌 书签 +- 风格:简洁、现代、圆角 \ No newline at end of file diff --git a/icons/generate-icons.js b/icons/generate-icons.js new file mode 100644 index 0000000..f2326eb --- /dev/null +++ b/icons/generate-icons.js @@ -0,0 +1,105 @@ +// 使用Node.js生成PNG图标 +// 运行: node generate-icons.js + +const fs = require('fs') +const path = require('path') + +// 简单的PNG生成器(使用PNG文件格式规范) +function createSimplePNG(width, height, bgColor) { + // PNG文件签名 + const signature = Buffer.from([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]) + + // IHDR chunk (图像头) + const ihdrData = Buffer.alloc(13) + ihdrData.writeUInt32BE(width, 0) // width + ihdrData.writeUInt32BE(height, 4) // height + ihdrData[8] = 8 // bit depth + ihdrData[9] = 2 // color type (RGB) + ihdrData[10] = 0 // compression method + ihdrData[11] = 0 // filter method + ihdrData[12] = 0 // interlace method + + const ihdrChunk = createChunk('IHDR', ihdrData) + + // IDAT chunk (图像数据) + // 创建简单的纯色图像 + const rawData = Buffer.alloc(height * (1 + width * 3)) + let offset = 0 + + const r = bgColor.r + const g = bgColor.g + const b = bgColor.b + + for (let y = 0; y < height; y++) { + rawData[offset++] = 0 // filter type: none + for (let x = 0; x < width; x++) { + rawData[offset++] = r + rawData[offset++] = g + rawData[offset++] = b + } + } + + // 使用zlib压缩 + const zlib = require('zlib') + const compressedData = zlib.deflateSync(rawData) + const idatChunk = createChunk('IDAT', compressedData) + + // IEND chunk (图像结束) + const iendChunk = createChunk('IEND', Buffer.alloc(0)) + + // 组合所有chunk + return Buffer.concat([signature, ihdrChunk, idatChunk, iendChunk]) +} + +function createChunk(type, data) { + const length = Buffer.alloc(4) + length.writeUInt32BE(data.length, 0) + + const typeBuffer = Buffer.from(type, 'ascii') + + // CRC32计算 + const crcData = Buffer.concat([typeBuffer, data]) + const crc = crc32(crcData) + const crcBuffer = Buffer.alloc(4) + crcBuffer.writeUInt32BE(crc >>> 0, 0) + + return Buffer.concat([length, typeBuffer, data, crcBuffer]) +} + +// CRC32计算 +function crc32(buffer) { + let crc = 0xFFFFFFFF + const table = getCrc32Table() + + for (let i = 0; i < buffer.length; i++) { + crc = (crc >>> 8) ^ table[(crc ^ buffer[i]) & 0xFF] + } + + return crc ^ 0xFFFFFFFF +} + +function getCrc32Table() { + const table = new Uint32Array(256) + for (let i = 0; i < 256; i++) { + let c = i + for (let j = 0; j < 8; j++) { + c = (c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1) + } + table[i] = c + } + return table +} + +// 生成图标 +const sizes = [16, 48, 128] +const color = { r: 102, g: 126, b: 234 } // #667eea 紫色 + +sizes.forEach(size => { + const png = createSimplePNG(size, size, color) + const filename = path.join(__dirname, `icon${size}.png`) + fs.writeFileSync(filename, png) + console.log(`生成: ${filename} (${size}x${size})`) +}) + +console.log('图标生成完成!') +console.log('请替换为更好的图标设计,推荐使用 https://favicon.io/') \ No newline at end of file diff --git a/icons/icon.svg b/icons/icon.svg new file mode 100644 index 0000000..f1b90cc --- /dev/null +++ b/icons/icon.svg @@ -0,0 +1,10 @@ + + + + + + + + + W + \ No newline at end of file diff --git a/icons/icon128.png b/icons/icon128.png new file mode 100644 index 0000000..189b3d2 Binary files /dev/null and b/icons/icon128.png differ diff --git a/icons/icon16.png b/icons/icon16.png new file mode 100644 index 0000000..3ced90c Binary files /dev/null and b/icons/icon16.png differ diff --git a/icons/icon48.png b/icons/icon48.png new file mode 100644 index 0000000..2c15d69 Binary files /dev/null and b/icons/icon48.png differ diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..cb754eb --- /dev/null +++ b/manifest.json @@ -0,0 +1,62 @@ +{ + "manifest_version": 3, + "name": "网页助手 - 收藏、总结、深度研究", + "name__zh_CN": "网页助手", + "version": "0.1.0", + "description": "右键菜单扩展:快速收藏网页、AI总结、深度研究分析", + "description__zh_CN": "右键菜单扩展:快速收藏网页、AI总结、深度研究分析", + + "permissions": [ + "contextMenus", + "activeTab", + "storage", + "tabs", + "scripting" + ], + + "host_permissions": [ + "" + ], + + "background": { + "service_worker": "src/background.js", + "type": "module" + }, + + "action": { + "default_popup": "src/popup.html", + "default_icon": { + "16": "icons/icon16.png", + "48": "icons/icon48.png", + "128": "icons/icon128.png" + }, + "default_title": "网页助手" + }, + + "icons": { + "16": "icons/icon16.png", + "48": "icons/icon48.png", + "128": "icons/icon128.png" + }, + + "content_scripts": [ + { + "matches": [""], + "js": ["src/content.js"], + "run_at": "document_end" + } + ], + + "_locales": { + "zh_CN": { + "messages": { + "extensionName": { + "message": "网页助手" + }, + "extensionDesc": { + "message": "右键菜单扩展:快速收藏网页、AI总结、深度研究分析" + } + } + } + } +} \ No newline at end of file diff --git a/scripts/generate-icons.sh b/scripts/generate-icons.sh new file mode 100644 index 0000000..a865257 --- /dev/null +++ b/scripts/generate-icons.sh @@ -0,0 +1,52 @@ +#!/bin/bash +# 生成图标脚本 + +# 创建SVG图标并转换为PNG +# 需要安装: npm install sharp + +echo "生成插件图标..." + +# 使用Node.js生成PNG图标 +node -e " +const fs = require('fs'); +const path = require('path'); + +// 简单的PNG生成(16x16, 48x48, 128x128) +// 使用纯色背景+文字 + +const sizes = [16, 48, 128]; + +sizes.forEach(size => { + // 创建简单的PNG文件头 + const filename = path.join('icons', \`icon\${size}.png\`); + + // 使用最简单的方式:生成一个小的有效PNG + // 这里用base64编码的简单图标 + + const iconData = Buffer.from([ + 0x89, 0x50, 0x4E, 0x47, // PNG signature + 0x0D, 0x0A, 0x1A, 0x0A, + // IHDR chunk + 0x00, 0x00, 0x00, size > 16 ? (size === 48 ? 0x30 : 0x80) : 0x10, // width + 0x00, 0x00, 0x00, size > 16 ? (size === 48 ? 0x30 : 0x80) : 0x10, // height + 0x08, // bit depth + 0x02, // color type (RGB) + 0x00, // compression + 0x00, // filter + 0x00, // interlace + ]); + + console.log(\`Created placeholder: \${filename}\`); +}); + +console.log('图标生成完成'); +" +" 2>/dev/null || echo "使用占位图标" + +# 创建占位PNG文件 +for size in 16 48 128; do + cp /dev/null icons/icon${size}.png 2>/dev/null || touch icons/icon${size}.png +done + +echo "请手动添加PNG图标到 icons/ 目录" +echo "推荐使用在线工具生成: https://www.flaticon.com/" \ No newline at end of file diff --git a/src/background.js b/src/background.js new file mode 100644 index 0000000..89fb751 --- /dev/null +++ b/src/background.js @@ -0,0 +1,537 @@ +// Background Service Worker - 处理右键菜单和消息 + +// API配置 - 可在popup中设置 +const DEFAULT_API_CONFIG = { + baseUrl: 'http://localhost:3000/api', // 默认本地API + apiKey: '', + model: 'gpt-4', +} + +// 存储的收藏列表 +let collections = [] + +// 初始化右键菜单 +chrome.runtime.onInstalled.addListener(() => { + // 创建父菜单 + chrome.contextMenus.create({ + id: 'web-assist', + title: '网页助手', + contexts: ['page', 'selection', 'image', 'link'] + }) + + // 收藏网页 + chrome.contextMenus.create({ + id: 'collect', + parentId: 'web-assist', + title: '📌 收藏此网页', + contexts: ['page'] + }) + + // 收藏选中内容 + chrome.contextMenus.create({ + id: 'collect-selection', + parentId: 'web-assist', + title: '📌 收藏选中内容', + contexts: ['selection'] + }) + + // 收藏图片 + chrome.contextMenus.create({ + id: 'collect-image', + parentId: 'web-assist', + title: '📌 收藏此图片', + contexts: ['image'] + }) + + // 分隔线 + chrome.contextMenus.create({ + id: 'separator1', + parentId: 'web-assist', + type: 'separator', + contexts: ['page'] + }) + + // AI总结 + chrome.contextMenus.create({ + id: 'summarize', + parentId: 'web-assist', + title: '📝 AI总结此页', + contexts: ['page'] + }) + + // 总结选中内容 + chrome.contextMenus.create({ + id: 'summarize-selection', + parentId: 'web-assist', + title: '📝 总结选中内容', + contexts: ['selection'] + }) + + // 分隔线 + chrome.contextMenus.create({ + id: 'separator2', + parentId: 'web-assist', + type: 'separator', + contexts: ['page'] + }) + + // 深度研究 + chrome.contextMenus.create({ + id: 'deep-research', + parentId: 'web-assist', + title: '🔍 深度研究此页', + contexts: ['page'] + }) + + // 研究选中内容 + chrome.contextMenus.create({ + id: 'research-selection', + parentId: 'web-assist', + title: '🔍 深度研究选中内容', + contexts: ['selection'] + }) + + console.log('网页助手:右键菜单已创建') + + // 加载已保存的收藏 + loadCollections() +}) + +// 加载收藏数据 +async function loadCollections() { + const result = await chrome.storage.local.get(['collections']) + collections = result.collections || [] +} + +// 保存收藏数据 +async function saveCollections() { + await chrome.storage.local.set({ collections }) +} + +// 获取API配置 +async function getApiConfig() { + const result = await chrome.storage.local.get(['apiConfig']) + return result.apiConfig || DEFAULT_API_CONFIG +} + +// 右键菜单点击处理 +chrome.contextMenus.onClicked.addListener(async (info, tab) => { + const menuId = info.menuItemId + + try { + switch (menuId) { + case 'collect': + await handleCollectPage(tab) + break + + case 'collect-selection': + await handleCollectSelection(tab, info.selectionText) + break + + case 'collect-image': + await handleCollectImage(info.srcUrl, tab) + break + + case 'summarize': + await handleSummarize(tab) + break + + case 'summarize-selection': + await handleSummarizeSelection(tab, info.selectionText) + break + + case 'deep-research': + await handleDeepResearch(tab) + break + + case 'research-selection': + await handleResearchSelection(tab, info.selectionText) + break + } + } catch (error) { + console.error('处理菜单点击失败:', error) + showNotification('操作失败', error.message) + } +}) + +// 收藏整个网页 +async function handleCollectPage(tab) { + // 执行内容脚本获取页面内容 + const results = await chrome.scripting.executeScript({ + target: { tabId: tab.id }, + func: getPageContent + }) + + const pageData = results[0].result + + const collection = { + id: Date.now().toString(), + type: 'page', + url: tab.url, + title: tab.title, + content: pageData.content, + images: pageData.images, + timestamp: new Date().toISOString(), + favicon: tab.favIconUrl + } + + collections.push(collection) + await saveCollections() + + showNotification('收藏成功', `已收藏: ${tab.title}`) +} + +// 收藏选中内容 +async function handleCollectSelection(tab, selectionText) { + const collection = { + id: Date.now().toString(), + type: 'selection', + url: tab.url, + title: tab.title, + content: selectionText, + timestamp: new Date().toISOString(), + favicon: tab.favIconUrl + } + + collections.push(collection) + await saveCollections() + + showNotification('收藏成功', '已收藏选中内容') +} + +// 收藏图片 +async function handleCollectImage(imageUrl, tab) { + const collection = { + id: Date.now().toString(), + type: 'image', + url: tab.url, + title: tab.title, + imageUrl: imageUrl, + timestamp: new Date().toISOString(), + favicon: tab.favIconUrl + } + + collections.push(collection) + await saveCollections() + + showNotification('收藏成功', '已收藏图片') +} + +// AI总结页面 +async function handleSummarize(tab) { + showNotification('正在总结...', '请稍候') + + // 获取页面内容 + const results = await chrome.scripting.executeScript({ + target: { tabId: tab.id }, + func: getPageContent + }) + + const pageData = results[0].result + const apiConfig = await getApiConfig() + + // 调用AI API进行总结 + const summary = await callAiApi(apiConfig, 'summarize', { + title: tab.title, + url: tab.url, + content: pageData.content, + images: pageData.images + }) + + // 保存总结结果并打开popup显示 + await chrome.storage.local.set({ + lastResult: { + type: 'summary', + title: tab.title, + url: tab.url, + result: summary, + timestamp: new Date().toISOString() + } + }) + + // 打开popup显示结果 + chrome.action.openPopup() +} + +// AI总结选中内容 +async function handleSummarizeSelection(tab, selectionText) { + showNotification('正在总结...', '请稍候') + + const apiConfig = await getApiConfig() + + const summary = await callAiApi(apiConfig, 'summarize', { + title: tab.title, + url: tab.url, + content: selectionText + }) + + await chrome.storage.local.set({ + lastResult: { + type: 'summary', + title: tab.title, + url: tab.url, + selection: selectionText, + result: summary, + timestamp: new Date().toISOString() + } + }) + + chrome.action.openPopup() +} + +// 深度研究页面 +async function handleDeepResearch(tab) { + showNotification('正在深度研究...', '这可能需要一些时间') + + const results = await chrome.scripting.executeScript({ + target: { tabId: tab.id }, + func: getPageContent + }) + + const pageData = results[0].result + const apiConfig = await getApiConfig() + + // 调用AI API进行深度研究 + const researchResult = await callAiApi(apiConfig, 'deep-research', { + title: tab.title, + url: tab.url, + content: pageData.content, + images: pageData.images + }) + + await chrome.storage.local.set({ + lastResult: { + type: 'research', + title: tab.title, + url: tab.url, + result: researchResult, + timestamp: new Date().toISOString() + } + }) + + chrome.action.openPopup() +} + +// 深度研究选中内容 +async function handleResearchSelection(tab, selectionText) { + showNotification('正在深度研究...', '请稍候') + + const apiConfig = await getApiConfig() + + const researchResult = await callAiApi(apiConfig, 'deep-research', { + title: tab.title, + url: tab.url, + content: selectionText + }) + + await chrome.storage.local.set({ + lastResult: { + type: 'research', + title: tab.title, + url: tab.url, + selection: selectionText, + result: researchResult, + timestamp: new Date().toISOString() + } + }) + + chrome.action.openPopup() +} + +// 获取页面内容的函数(注入到页面执行) +function getPageContent() { + // 获取主要文本内容 + const getMainContent = () => { + // 尝试获取article或main区域 + const article = document.querySelector('article') || + document.querySelector('main') || + document.querySelector('.content') || + document.querySelector('.post-content') || + document.querySelector('.article-content') + + if (article) { + return article.innerText + } + + // 否则获取body内容,但过滤掉脚本和样式 + const body = document.body.cloneNode(true) + const scripts = body.querySelectorAll('script, style, nav, header, footer, aside, .sidebar, .ads, .advertisement') + scripts.forEach(el => el.remove()) + + return body.innerText.substring(0, 50000) // 限制长度 + } + + // 获取页面图片 + const getImages = () => { + const images = [] + const imgElements = document.querySelectorAll('img') + + imgElements.forEach(img => { + const src = img.src || img.dataset.src + if (src && !src.startsWith('data:') && img.width > 100 && img.height > 100) { + images.push({ + src: src, + alt: img.alt || '', + width: img.width, + height: img.height + }) + } + }) + + return images.slice(0, 10) // 最多10张图片 + } + + return { + content: getMainContent(), + images: getImages(), + meta: { + description: document.querySelector('meta[name="description"]')?.content || '', + keywords: document.querySelector('meta[name="keywords"]')?.content || '', + author: document.querySelector('meta[name="author"]')?.content || '' + } + } +} + +// 调用AI API +async function callAiApi(config, actionType, data) { + const prompts = { + summarize: `请对以下网页内容进行总结,提炼关键要点,用简洁的中文回答: + +标题: ${data.title} +URL: ${data.url} +内容: ${data.content} + +请按以下格式输出总结: +1. 【主题概述】一句话概括主题 +2. 【核心要点】列出3-5个关键要点 +3. 【价值评估】这个内容的价值和应用场景`, + + deep-research: `请对以下内容进行深度研究和分析: + +标题: ${data.title} +URL: ${data.url} +内容: ${data.content} + +请进行以下分析: +1. 【内容真实性】分析内容的可信度和来源 +2. 【核心观点】提炼主要观点和论据 +3. 【相关背景】补充相关背景知识 +4. 【延伸阅读】推荐3-5个相关主题 +5. 【批判性思考】指出可能的偏见或局限性 +6. 【应用建议】如何应用这些信息` + } + + const prompt = prompts[actionType] + + // 如果配置了API,调用外部API + if (config.baseUrl && config.apiKey) { + try { + const response = await fetch(`${config.baseUrl}/chat`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${config.apiKey}` + }, + body: JSON.stringify({ + model: config.model, + messages: [{ role: 'user', content: prompt }], + stream: false + }) + }) + + const result = await response.json() + return result.choices?.[0]?.message?.content || result.response || '分析完成' + } catch (error) { + console.error('API调用失败:', error) + return `API调用失败: ${error.message}\n\n请检查API配置是否正确。` + } + } + + // 如果没有配置API,使用本地模拟 + return simulateAiResponse(actionType, data) +} + +// 模拟AI响应(当没有配置外部API时) +function simulateAiResponse(actionType, data) { + if (actionType === 'summarize') { + return `📌 **主题概述** +${data.title} + +📝 **核心要点** +• 内容长度: ${data.content.length} 字符 +• 页面包含: ${data.images?.length || 0} 张图片 +• URL来源: ${data.url} + +💡 **提示** +要获得AI智能总结,请在插件设置中配置API(如OpenAI、本地LLM服务等)` + } + + if (actionType === 'deep-research') { + return `🔍 **深度研究报告** + +📊 **基本信息** +• 标题: ${data.title} +• URL: ${data.url} +• 内容长度: ${data.content.length} 字符 +• 图片数量: ${data.images?.length || 0} + +📝 **内容摘要** +${data.content.substring(0, 500)}... + +💡 **提示** +要获得AI深度分析,请在插件设置中配置API(如OpenAI、本地LLM服务等) + +可配置的API服务: +• OpenAI API (gpt-4, gpt-3.5-turbo) +• 本地LLM服务 (Ollama, LMStudio) +• 其他兼容OpenAI格式的API` + } +} + +// 显示通知 +function showNotification(title, message) { + chrome.notifications?.create({ + type: 'basic', + iconUrl: 'icons/icon48.png', + title: title, + message: message + }) +} + +// 监听来自popup的消息 +chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { + if (message.action === 'getCollections') { + sendResponse({ collections }) + } + + if (message.action === 'deleteCollection') { + collections = collections.filter(c => c.id !== message.id) + saveCollections() + sendResponse({ success: true }) + } + + if (message.action === 'clearCollections') { + collections = [] + saveCollections() + sendResponse({ success: true }) + } + + if (message.action === 'getApiConfig') { + getApiConfig().then(config => sendResponse({ config })) + return true // 保持异步响应 + } + + if (message.action === 'saveApiConfig') { + chrome.storage.local.set({ apiConfig: message.config }) + sendResponse({ success: true }) + } + + if (message.action === 'getLastResult') { + chrome.storage.local.get(['lastResult']).then(result => { + sendResponse({ result: result.lastResult }) + }) + return true + } + + return true +}) \ No newline at end of file diff --git a/src/content.js b/src/content.js new file mode 100644 index 0000000..39db16b --- /dev/null +++ b/src/content.js @@ -0,0 +1,188 @@ +// Content Script - 注入到页面中 + +console.log('网页助手已加载') + +// 监听来自background的消息 +chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { + if (message.action === 'getPageContent') { + const content = extractPageContent() + sendResponse(content) + } + + if (message.action === 'getSelection') { + const selection = window.getSelection().toString() + sendResponse({ selection }) + } + + return true +}) + +// 提取页面内容 +function extractPageContent() { + // 获取主要文本内容 + const getMainContent = () => { + // 尝试多种常见的文章容器选择器 + const selectors = [ + 'article', + 'main', + '[role="main"]', + '.content', + '.article-content', + '.post-content', + '.entry-content', + '.post-body', + '.article-body', + '#content', + '#main-content' + ] + + for (const selector of selectors) { + const element = document.querySelector(selector) + if (element && element.innerText.length > 200) { + return cleanText(element.innerText) + } + } + + // 如果没找到,使用智能提取 + return extractSmartContent() + } + + // 智能提取主要内容 + function extractSmartContent() { + const candidates = [] + const elements = document.querySelectorAll('p, div, section') + + elements.forEach(el => { + const text = el.innerText + const length = text.length + const linkDensity = getLinkDensity(el) + + // 评分: 文本长度越长越好,链接密度越低越好 + if (length > 100) { + candidates.push({ + element: el, + score: length * (1 - linkDensity), + text: cleanText(text) + }) + } + }) + + // 选择得分最高的 + candidates.sort((a, b) => b.score - a.score) + + if (candidates.length > 0) { + return candidates[0].text.substring(0, 50000) + } + + // 最后备选: body内容 + return getBodyContent() + } + + // 计算链接密度 + function getLinkDensity(element) { + const links = element.querySelectorAll('a') + const linkText = Array.from(links).reduce((sum, a) => sum + a.innerText.length, 0) + const totalText = element.innerText.length + return totalText > 0 ? linkText / totalText : 0 + } + + // 获取body内容(过滤无用元素) + function getBodyContent() { + const body = document.body.cloneNode(true) + const removeSelectors = [ + 'script', 'style', 'noscript', 'iframe', + 'nav', 'header', 'footer', 'aside', + '.sidebar', '.ads', '.advertisement', '.ad', + '.comments', '.comment', '.social-share', + '.navigation', '.menu', '.breadcrumb', + '[role="navigation"]', '[role="banner"]', '[role="complementary"]' + ] + + removeSelectors.forEach(selector => { + body.querySelectorAll(selector).forEach(el => el.remove()) + }) + + return cleanText(body.innerText).substring(0, 50000) + } + + // 清理文本 + function cleanText(text) { + return text + .replace(/\s+/g, ' ') + .replace(/\n\s*\n/g, '\n\n') + .trim() + } + + // 获取图片 + function getImages() { + const images = [] + const seen = new Set() + + const imgElements = document.querySelectorAll('img') + + imgElements.forEach(img => { + const src = img.src || img.dataset.src || img.dataset.original + + // 过滤条件 + if (!src) return + if (seen.has(src)) return + if (src.startsWith('data:')) return // 过滤base64 + if (src.includes('avatar') && img.width < 100) return // 过滤小头像 + if (img.width < 50 || img.height < 50) return // 过滤太小图片 + if (img.width > img.height * 5 || img.height > img.width * 5) return // 过滤极端比例 + if (src.includes('emoji') || src.includes('icon') || src.includes('logo')) return + + seen.add(src) + + images.push({ + src, + alt: img.alt || '', + width: img.naturalWidth || img.width, + height: img.naturalHeight || img.height, + caption: getImageCaption(img) + }) + }) + + return images.slice(0, 20) + } + + // 获取图片caption + function getImageCaption(img) { + // 查找图片附近的caption + const parent = img.parentElement + const caption = parent.querySelector('figcaption, .caption, .image-caption') + if (caption) return caption.innerText + + // 查找alt或title + if (img.alt && img.alt.length > 10) return img.alt + if (img.title) return img.title + + return '' + } + + // 获取元数据 + function getMetadata() { + return { + title: document.title, + description: document.querySelector('meta[name="description"]')?.content || '', + keywords: document.querySelector('meta[name="keywords"]')?.content || '', + author: document.querySelector('meta[name="author"]')?.content || + document.querySelector('.author')?.innerText || '', + publishDate: document.querySelector('meta[property="article:published_time"]')?.content || + document.querySelector('time')?.getAttribute('datetime') || '', + siteName: document.querySelector('meta[property="og:site_name"]')?.content || '', + ogImage: document.querySelector('meta[property="og:image"]')?.content || '' + } + } + + return { + content: getMainContent(), + images: getImages(), + metadata: getMetadata(), + url: window.location.href, + timestamp: new Date().toISOString() + } +} + +// 导出函数供background调用 +window.webAssistExtract = extractPageContent \ No newline at end of file diff --git a/src/popup.html b/src/popup.html new file mode 100644 index 0000000..b811615 --- /dev/null +++ b/src/popup.html @@ -0,0 +1,456 @@ + + + + + + 网页助手 + + + + + + 🌐 网页助手 + + 🗑️ + + + + + + 📝 结果 + 📌 收藏 + ⚙️ 设置 + + + + + + + + + 📄 + 暂无结果右键点击网页使用助手功能 + + + + + + + + + + + + + + + + + API配置 + + API地址 + + + + API Key + + + + 模型 + + GPT-4 + GPT-3.5 Turbo + Claude 3 Opus + Llama 3 (本地) + Qwen (本地) + + + + + + 本地LLM服务 + + Ollama地址 + + + + Ollama模型 + + + + + 保存设置 + + + + + + + + + \ No newline at end of file diff --git a/src/popup.js b/src/popup.js new file mode 100644 index 0000000..d3f5ab9 --- /dev/null +++ b/src/popup.js @@ -0,0 +1,258 @@ +// Popup Script - 插件弹窗界面逻辑 + +// 初始化 +document.addEventListener('DOMContentLoaded', async () => { + initTabs() + loadLastResult() + loadCollections() + loadSettings() +}) + +// Tab切换 +function initTabs() { + const tabs = document.querySelectorAll('.tab') + const panels = document.querySelectorAll('.panel') + + tabs.forEach(tab => { + tab.addEventListener('click', () => { + const target = tab.dataset.tab + + tabs.forEach(t => t.classList.remove('active')) + panels.forEach(p => p.classList.remove('active')) + + tab.classList.add('active') + document.getElementById(`${target}Panel`).classList.add('active') + + // 刷新对应面板内容 + if (target === 'collections') { + loadCollections() + } + }) + }) +} + +// 加载最新结果 +async function loadLastResult() { + const response = await chrome.runtime.sendMessage({ action: 'getLastResult' }) + + if (response.result) { + renderResult(response.result) + } else { + renderEmpty() + } +} + +// 渲染结果 +function renderResult(result) { + const container = document.getElementById('resultContent') + + const typeIcon = result.type === 'summary' ? '📝' : '🔍' + const typeName = result.type === 'summary' ? 'AI总结' : '深度研究' + + container.innerHTML = ` + + ${typeIcon} ${typeName} + ${result.url} + ${result.result} + + 💾 保存收藏 + 📋 复制 + + + ` +} + +// 渲染空状态 +function renderEmpty() { + document.getElementById('resultContent').innerHTML = ` + + 📄 + 暂无结果右键点击网页使用助手功能 + + 📌 收藏网页 + 📝 AI总结 + 🔍 深度研究 + + + ` +} + +// 保存结果到收藏 +async function saveResult() { + const response = await chrome.runtime.sendMessage({ action: 'getLastResult' }) + const result = response.result + + if (result) { + const collection = { + id: Date.now().toString(), + type: result.type, + url: result.url, + title: result.title, + content: result.result, + selection: result.selection, + timestamp: new Date().toISOString() + } + + // 发送到background保存 + chrome.runtime.sendMessage({ + action: 'addCollection', + collection + }) + + showToast('已保存到收藏') + } +} + +// 复制结果 +function copyResult() { + const response = chrome.runtime.sendMessage({ action: 'getLastResult' }) + response.then(res => { + if (res.result) { + navigator.clipboard.writeText(res.result.result) + showToast('已复制到剪贴板') + } + }) +} + +// 加载收藏列表 +async function loadCollections() { + const response = await chrome.runtime.sendMessage({ action: 'getCollections' }) + const collections = response.collections || [] + + const container = document.getElementById('collectionsList') + + if (collections.length === 0) { + container.innerHTML = ` + + 📌 + 暂无收藏右键点击网页收藏内容 + + ` + return + } + + container.innerHTML = ` + + ${collections.map(item => renderCollectionItem(item)).join('')} + + ` +} + +// 渲染单个收藏项 +function renderCollectionItem(item) { + const typeLabels = { + page: '网页', + selection: '选文', + image: '图片', + summary: '总结', + research: '研究' + } + + const typeIcons = { + page: '📄', + selection: '📝', + image: '🖼️', + summary: '📊', + research: '🔍' + } + + const preview = item.content ? item.content.substring(0, 100) + '...' : '' + const time = new Date(item.timestamp).toLocaleString('zh-CN') + + return ` + + + ${item.title || '未命名'} + ${typeIcons[item.type]} ${typeLabels[item.type] || item.type} + + ${item.url} + ${preview ? `${preview}` : ''} + ${item.imageUrl ? `` : ''} + + + ` +} + +// 打开URL +function openUrl(url) { + chrome.tabs.create({ url }) +} + +// 复制收藏内容 +async function copyItem(id) { + const response = await chrome.runtime.sendMessage({ action: 'getCollections' }) + const item = response.collections.find(c => c.id === id) + + if (item) { + const text = `${item.title}\n${item.url}\n\n${item.content || ''}` + navigator.clipboard.writeText(text) + showToast('已复制到剪贴板') + } +} + +// 删除收藏 +async function deleteItem(id) { + await chrome.runtime.sendMessage({ action: 'deleteCollection', id }) + loadCollections() + showToast('已删除') +} + +// 清空所有收藏 +document.getElementById('clearBtn').addEventListener('click', async () => { + if (confirm('确定要清空所有收藏吗?')) { + await chrome.runtime.sendMessage({ action: 'clearCollections' }) + loadCollections() + showToast('已清空所有收藏') + } +}) + +// 加载设置 +async function loadSettings() { + const response = await chrome.runtime.sendMessage({ action: 'getApiConfig' }) + const config = response.config + + document.getElementById('apiUrl').value = config.baseUrl || '' + document.getElementById('apiKey').value = config.apiKey || '' + document.getElementById('modelSelect').value = config.model || 'gpt-4' + document.getElementById('ollamaUrl').value = config.ollamaUrl || 'http://localhost:11434' + document.getElementById('ollamaModel').value = config.ollamaModel || 'llama3:8b' +} + +// 保存设置 +document.getElementById('saveSettings').addEventListener('click', async () => { + const config = { + baseUrl: document.getElementById('apiUrl').value, + apiKey: document.getElementById('apiKey').value, + model: document.getElementById('modelSelect').value, + ollamaUrl: document.getElementById('ollamaUrl').value, + ollamaModel: document.getElementById('ollamaModel').value + } + + await chrome.runtime.sendMessage({ action: 'saveApiConfig', config }) + showToast('设置已保存') +}) + +// 显示Toast提示 +function showToast(message) { + const toast = document.getElementById('toast') + toast.textContent = message + toast.classList.add('show') + + setTimeout(() => { + toast.classList.remove('show') + }, 2000) +} + +// 将函数暴露到全局供onclick使用 +window.saveResult = saveResult +window.copyResult = copyResult +window.openUrl = openUrl +window.copyItem = copyItem +window.deleteItem = deleteItem \ No newline at end of file