diff --git a/chrome-extension/FIX_SUMMARY.md b/chrome-extension/FIX_SUMMARY.md new file mode 100644 index 0000000..7fbdf71 --- /dev/null +++ b/chrome-extension/FIX_SUMMARY.md @@ -0,0 +1,161 @@ +# Chrome扩展修复总结 + +## 🐛 问题描述 + +Chrome扩展在尝试检查`chrome://` URL时发生错误: +``` +Error: Cannot access a chrome:// URL +``` + +## 🔧 修复方案 + +### 1. 限制Content Script运行范围 + +**修改前**: +```json +"matches": [""] +``` + +**修改后**: +```json +"matches": ["http://*/*", "https://*/*", "file://*/*"] +``` + +### 2. 添加受限页面检查 + +在`content.js`和`background.js`中添加了`isRestrictedUrl()`和`isRestrictedPage()`方法: + +```javascript +isRestrictedUrl(url) { + const restrictedProtocols = ['chrome:', 'chrome-extension:', 'moz-extension:', 'edge:', 'opera:']; + const restrictedDomains = ['extensions', 'extensions-internals']; + + try { + const urlObj = new URL(url); + + // 检查协议 + if (restrictedProtocols.includes(urlObj.protocol)) { + return true; + } + + // 检查域名 + if (restrictedDomains.includes(urlObj.hostname)) { + return true; + } + + // 检查特殊页面 + if (url.includes('chrome://') || url.includes('about:')) { + return true; + } + + return false; + } catch (e) { + // 如果URL解析失败,认为是受限页面 + return true; + } +} +``` + +### 3. 初始化时检查页面类型 + +在Content Script初始化时添加检查: + +```javascript +initialize() { + if (this.isInjected) return; + + // 检查是否在受限页面中 + if (this.isRestrictedPage()) { + console.log('Content Script 跳过受限页面:', window.location.href); + return; + } + + // ... 继续初始化 +} +``` + +### 4. 消息处理时添加安全检查 + +在处理来自background的消息时添加检查: + +```javascript +handleMessage(request, sender, sendResponse) { + // 如果是受限页面,直接返回 + if (this.isRestrictedPage()) { + sendResponse({ success: false, message: '受限页面,无法操作' }); + return; + } + + // ... 继续处理消息 +} +``` + +## 📁 修改的文件 + +1. **chrome-extension/manifest.json** + - 限制content_scripts的matches范围 + +2. **chrome-extension/content.js** + - 添加isRestrictedPage()方法 + - 在initialize()中添加页面检查 + - 在handleMessage()中添加安全检查 + +3. **chrome-extension/background.js** + - 添加isRestrictedUrl()方法 + - 在handleTabUpdate()中添加页面检查 + - 在handleMessage()中添加安全检查 + +4. **chrome-extension/dist/** (构建输出) + - 更新了所有对应的构建文件 + +## ✅ 修复效果 + +### 修复前的问题 +- ❌ 扩展尝试访问`chrome://`页面导致错误 +- ❌ Content Script在所有页面运行,包括受限页面 +- ❌ 没有对受限URL的检查机制 + +### 修复后的改进 +- ✅ Content Script只在HTTP/HTTPS/File协议页面运行 +- ✅ 自动跳过受限页面,避免权限错误 +- ✅ 添加了多层安全检查机制 +- ✅ 提供了清晰的错误提示信息 + +## 🧪 测试验证 + +### 受限页面测试用例 +- `chrome://extensions/` → 跳过 ✅ +- `chrome-extension://abc123/popup.html` → 跳过 ✅ +- `about:blank` → 跳过 ✅ +- `invalid-url` → 跳过 ✅ + +### 正常页面测试用例 +- `https://example.com` → 正常运行 ✅ +- `http://localhost:3000` → 正常运行 ✅ +- `file:///Users/test/data.json` → 正常运行 ✅ + +## 🚀 使用说明 + +1. **重新加载扩展** + - 在`chrome://extensions/`页面点击扩展的刷新按钮 + - 或移除后重新加载扩展 + +2. **测试功能** + - 访问正常网页测试JSON检测 + - 访问`chrome://`页面确认不会报错 + - 使用扩展弹窗的手动检查功能 + +3. **查看日志** + - Background Script: `chrome://extensions/` → 检查视图 + - Content Script: 目标页面开发者工具 + +## 📝 注意事项 + +1. **权限最小化**: 修改后的扩展遵循最小权限原则 +2. **错误处理**: 受限页面会优雅跳过,不会产生错误 +3. **向后兼容**: 修复不影响正常页面的功能 +4. **安全性**: 添加了多层检查确保不会访问受限内容 + +--- + +**修复完成!扩展现在可以安全地在所有页面环境中运行,不会再出现`chrome://`访问错误。** \ No newline at end of file diff --git a/chrome-extension/INSTALL.md b/chrome-extension/INSTALL.md new file mode 100644 index 0000000..ad57215 --- /dev/null +++ b/chrome-extension/INSTALL.md @@ -0,0 +1,160 @@ +# Chrome扩展安装和使用指南 + +## 快速开始 + +### 1. 构建扩展 + +在项目根目录执行以下命令: + +```bash +# 安装依赖(如果还没有安装) +npm install + +# 构建Chrome扩展 +npm run build:extension +``` + +### 2. 安装到Chrome + +1. 打开Chrome浏览器 +2. 在地址栏输入 `chrome://extensions/` 并回车 +3. 打开右上角的"开发者模式"开关 +4. 点击"加载已解压的扩展程序"按钮 +5. 选择项目中的 `chrome-extension/dist` 目录 +6. 扩展安装完成,会在扩展列表中看到"JSON Tools" + +### 3. 测试扩展 + +#### 方法一:使用测试页面 + +1. 在浏览器中打开 `chrome-extension/test-pages/test.html` +2. 点击页面上的"加载JSON数据"按钮 +3. 扩展应该会自动检测到JSON并打开JSON Tools + +#### 方法二:访问真实的JSON API + +1. 访问任何返回JSON数据的API端点,例如: + - `https://jsonplaceholder.typicode.com/posts/1` + - `https://api.github.com/users/octocat` +2. 扩展应该会自动检测并打开JSON Tools + +#### 方法三:手动触发 + +1. 在任何页面上点击扩展图标 +2. 在弹窗中点击"检查当前页面" +3. 如果页面包含JSON数据,会自动在JSON Tools中打开 + +## 功能说明 + +### 自动检测模式 + +扩展会自动检测以下情况: +- 页面内容是有效的JSON格式 +- URL包含 `.json`、`api` 或 `json` 关键词 +- 页面标题包含"json"关键词 +- Content-Type为 `application/json` + +### 浮动按钮 + +当检测到页面包含JSON数据时,会在页面右上角显示蓝色的"JSON Tools"浮动按钮,点击即可快速打开。 + +### 多标签页管理 + +- 如果JSON Tools已经打开,会在现有标签页中更新数据 +- 如果没有打开,会创建新的标签页 +- 支持同时处理多个JSON数据源 + +## 故障排除 + +### 扩展无法加载 + +1. 确保已正确构建扩展:`npm run build:extension` +2. 检查 `chrome-extension/dist` 目录是否存在 +3. 在Chrome扩展页面查看错误信息 + +### 无法检测JSON + +1. 确保页面内容是有效的JSON格式 +2. 检查浏览器控制台是否有错误 +3. 尝试手动点击扩展图标中的"检查当前页面" + +### JSON Tools页面无法打开 + +1. 确保主应用已正确构建 +2. 检查manifest.json中的路径配置 +3. 查看background script的错误日志 + +## 开发调试 + +### 查看日志 + +1. **Background Script日志**: + - 进入 `chrome://extensions/` + - 找到JSON Tools扩展 + - 点击"检查视图" -> "Service Worker" + +2. **Content Script日志**: + - 在目标页面按F12打开开发者工具 + - 查看Console面板 + +3. **JSON Tools页面日志**: + - 在JSON Tools页面按F12 + - 查看Console面板 + +### 重新加载扩展 + +修改代码后: +1. 在 `chrome://extensions/` 页面点击扩展的刷新按钮 +2. 或者重新运行构建命令:`npm run build:extension` + +### 测试不同场景 + +使用提供的测试页面 `chrome-extension/test-pages/test.html` 来测试: +- 简单JSON数据 +- 复杂嵌套JSON +- API响应模拟 +- 无效JSON数据 + +## 文件结构说明 + +``` +chrome-extension/ +├── manifest.json # 扩展配置文件 +├── background.js # 后台脚本,处理标签页监听 +├── content.js # 内容脚本,注入到网页 +├── injected.js # 注入脚本,在页面上下文中运行 +├── popup.html # 扩展弹窗界面 +├── popup.js # 弹窗交互逻辑 +├── build.js # 构建脚本 +├── test-extension.js # 测试脚本 +├── create-icons.js # 图标生成脚本 +├── icons/ # 扩展图标 +├── test-pages/ # 测试页面 +├── dist/ # 构建输出目录 +├── README.md # 详细说明文档 +└── INSTALL.md # 本安装指南 +``` + +## 权限说明 + +扩展请求的权限: + +- **activeTab**: 访问当前活动标签页内容 +- **storage**: 存储扩展设置和状态 +- **scripting**: 在页面中注入脚本 +- ****: 访问所有网站以检测JSON内容 + +所有权限仅用于JSON检测和展示功能,不会收集或传输用户数据。 + +## 联系支持 + +如果遇到问题: + +1. 查看GitHub Issues +2. 检查浏览器控制台错误信息 +3. 确保使用最新版本的扩展 +4. 提供详细的错误信息和复现步骤 + +--- + +**注意**: 这是一个开发版本的扩展,建议在生产环境使用前进行充分测试。 \ No newline at end of file diff --git a/chrome-extension/QUICK_START.md b/chrome-extension/QUICK_START.md new file mode 100644 index 0000000..1096db3 --- /dev/null +++ b/chrome-extension/QUICK_START.md @@ -0,0 +1,85 @@ +# Chrome扩展快速开始指南 + +## 🚀 快速安装和测试 + +### 1. 构建扩展 +```bash +npm run build:extension +``` + +### 2. 安装到Chrome +1. 打开Chrome浏览器 +2. 地址栏输入:`chrome://extensions/` +3. 开启右上角的"开发者模式" +4. 点击"加载已解压的扩展程序" +5. 选择项目中的 `chrome-extension/dist` 目录 +6. 扩展安装成功! + +### 3. 验证安装 +```bash +npm run test:extension +``` +应该看到:`所有测试通过!扩展已准备就绪。` + +### 4. 测试功能 + +#### 方法一:使用测试页面 +1. 在浏览器中打开:`chrome-extension/test-pages/test.html` +2. 点击"加载JSON数据"按钮 +3. 扩展应该自动检测并打开JSON Tools + +#### 方法二:访问真实API +1. 访问:`https://jsonplaceholder.typicode.com/posts/1` +2. 扩展应该自动检测JSON并打开JSON Tools + +#### 方法三:手动触发 +1. 在任何页面点击扩展图标 +2. 点击"检查当前页面" + +## ✅ 预期行为 + +- **自动检测**:页面包含JSON时自动打开JSON Tools +- **浮动按钮**:检测到JSON时显示蓝色"JSON Tools"按钮 +- **智能管理**:重用已打开的JSON Tools标签页 +- **错误处理**:无效JSON被自动忽略 + +## 🛠️ 故障排除 + +### 扩展无法加载 +```bash +# 重新构建 +npm run build:extension + +# 检查测试 +npm run test:extension +``` + +### 无法检测JSON +1. 检查页面内容是否为有效JSON +2. 打开开发者工具查看控制台错误 +3. 尝试手动点击扩展图标 + +### 构建失败 +```bash +# 清理并重新构建 +rm -rf dist +npm run build:extension +``` + +## 📁 重要文件 + +- `chrome-extension/dist/` - 构建完成的扩展文件 +- `chrome-extension/test-pages/test.html` - 功能测试页面 +- `chrome-extension/manifest.json` - 扩展配置文件 +- `src/services/chromeExtensionListener.ts` - 核心集成逻辑 + +## 🎯 下一步 + +1. 在Chrome中测试各种JSON数据源 +2. 体验自动检测功能 +3. 尝试不同的JSON格式和大小 +4. 根据需要自定义扩展配置 + +--- + +🎉 **恭喜!Chrome扩展已成功构建并准备使用!** \ No newline at end of file diff --git a/chrome-extension/README.md b/chrome-extension/README.md new file mode 100644 index 0000000..45be6bc --- /dev/null +++ b/chrome-extension/README.md @@ -0,0 +1,160 @@ +# JSON Tools Chrome 扩展 + +这是一个Chrome扩展,用于自动检测浏览器中的JSON数据并使用JSON Tools进行可视化展示。 + +## 功能特性 + +- 🔍 **自动检测**: 自动检测页面中的JSON数据 +- 📊 **可视化展示**: 使用JSON Tools的强大功能展示JSON数据 +- 🚀 **快速访问**: 通过浮动按钮或扩展弹窗快速打开JSON Tools +- 🔄 **实时同步**: 支持SPA应用的动态JSON内容检测 +- 📱 **响应式**: 适配各种屏幕尺寸 + +## 安装方法 + +### 方法一:开发模式安装 + +1. 克隆项目到本地 +2. 构建扩展: + ```bash + npm run build:extension + ``` +3. 打开Chrome浏览器,进入 `chrome://extensions/` +4. 开启"开发者模式" +5. 点击"加载已解压的扩展程序" +6. 选择项目中的 `chrome-extension/dist` 目录 + +### 方法二:使用预构建版本 + +如果你有预构建的扩展包,可以直接在Chrome中加载。 + +## 使用方法 + +### 自动检测模式 + +1. 浏览包含JSON数据的网页 +2. 扩展会自动检测JSON内容 +3. 如果检测到JSON,会自动打开JSON Tools页面进行可视化 + +### 手动触发模式 + +1. 在任何页面上点击扩展图标 +2. 点击"检查当前页面"按钮 +3. 如果页面包含JSON数据,会自动在JSON Tools中打开 + +### 浮动按钮 + +当检测到页面包含JSON数据时,页面右上角会显示一个蓝色的"JSON Tools"浮动按钮,点击即可快速打开。 + +## 技术架构 + +### 文件结构 + +``` +chrome-extension/ +├── manifest.json # 扩展清单文件 +├── background.js # 后台脚本 +├── content.js # 内容脚本 +├── injected.js # 注入脚本 +├── popup.html # 弹窗HTML +├── popup.js # 弹窗脚本 +├── build.js # 构建脚本 +├── icons/ # 图标目录 +├── dist/ # 构建输出目录 +└── README.md # 说明文档 +``` + +### 核心组件 + +#### Background Script (background.js) +- 监听标签页更新事件 +- 检测页面JSON内容 +- 管理JSON Tools标签页 + +#### Content Script (content.js) +- 在页面中注入脚本 +- 添加浮动按钮 +- 与background script通信 + +#### Injected Script (injected.js) +- 在页面上下文中运行 +- 检测JSON数据变化 +- 监听SPA应用的路由变化 + +#### Chrome Extension Listener (chromeExtensionListener.ts) +- 复用utoolsListener.ts的逻辑 +- 处理来自扩展的消息 +- 更新JSON Tools中的数据 + +## 开发说明 + +### 构建扩展 + +```bash +# 创建图标文件(可选) +npm run create:extension-icons + +# 构建主应用和扩展 +npm run build:extension + +# 测试扩展功能 +npm run test:extension +``` + +### 调试扩展 + +1. 在Chrome中加载扩展后,可以在以下位置查看日志: + - Background Script: `chrome://extensions/` -> 扩展详情 -> "检查视图" + - Content Script: 页面开发者工具的Console + - JSON Tools页面: F12开发者工具 + +### 修改扩展代码 + +扩展的主要逻辑文件: +- `chrome-extension/background.js`: 后台逻辑 +- `chrome-extension/content.js`: 页面交互 +- `src/services/chromeExtensionListener.ts`: 与主应用的集成 + +修改后需要重新构建: +```bash +npm run build:extension +``` + +然后在Chrome扩展页面点击刷新按钮。 + +## 权限说明 + +扩展需要以下权限: + +- `activeTab`: 访问当前活动标签页 +- `storage`: 存储扩展设置 +- `scripting`: 在页面中注入脚本 +- ``: 访问所有网站以检测JSON内容 + +## 故障排除 + +### 扩展无法检测JSON + +1. 确保页面内容确实是有效的JSON格式 +2. 检查浏览器控制台是否有错误信息 +3. 尝试手动点击"检查当前页面"按钮 + +### JSON Tools页面无法打开 + +1. 确保已正确构建扩展 +2. 检查manifest.json中的路径是否正确 +3. 查看background script的错误日志 + +### 浮动按钮不显示 + +1. 确保页面被识别为JSON页面 +2. 检查content script是否正确注入 +3. 查看页面控制台是否有JavaScript错误 + +## 贡献 + +欢迎提交Issue和Pull Request来改进这个扩展。 + +## 许可证 + +与主项目保持一致的许可证。 \ No newline at end of file diff --git a/chrome-extension/background.js b/chrome-extension/background.js new file mode 100644 index 0000000..fabf6b4 --- /dev/null +++ b/chrome-extension/background.js @@ -0,0 +1,302 @@ +/** + * Chrome Extension Background Script + * 负责监听标签页更新和检测JSON内容 + */ + +class ChromeExtensionListener { + constructor() { + this.isInitialized = false; + this.jsonToolsUrl = null; + } + + /** + * 初始化后台脚本 + */ + async initialize() { + if (this.isInitialized) return; + + // 获取插件的JSON Tools页面URL + this.jsonToolsUrl = chrome.runtime.getURL('index.html'); + + // 监听标签页更新事件 + chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { + this.handleTabUpdate(tabId, changeInfo, tab); + }); + + // 监听来自content script的消息 + chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { + this.handleMessage(request, sender, sendResponse); + return true; // 保持消息通道开放以支持异步响应 + }); + + // 监听插件图标点击事件 + chrome.action.onClicked.addListener((tab) => { + this.handleActionClick(tab); + }); + + this.isInitialized = true; + console.log('Chrome Extension Listener 已初始化'); + } + + /** + * 处理标签页更新事件 + */ + async handleTabUpdate(tabId, changeInfo, tab) { + // 只在页面加载完成时处理 + if (changeInfo.status !== 'complete') return; + + try { + // 检查是否为受限页面 + if (this.isRestrictedUrl(tab.url)) { + console.log(`跳过受限页面: ${tab.url}`); + return; + } + + // 检查页面是否包含JSON数据 + const hasJson = await this.checkPageForJson(tabId); + + if (hasJson) { + console.log(`检测到JSON数据: ${tab.url}`); + + // 获取页面JSON数据 + const jsonData = await this.extractJsonFromPage(tabId); + + if (jsonData) { + // 在当前标签页中打开JSON Tools页面并传递数据 + await this.openJsonToolsWithJson(jsonData, tab.url, tabId); + } + } + } catch (error) { + console.error('处理标签页更新时发生错误:', error); + } + } + + /** + * 检查页面是否包含JSON数据 + */ + async checkPageForJson(tabId) { + try { + const results = await chrome.scripting.executeScript({ + target: { tabId }, + func: () => { + // 检查页面内容是否为JSON + const content = document.body.innerText || document.body.textContent; + + if (!content || content.trim().length === 0) { + return false; + } + + // 尝试解析JSON + try { + const trimmedContent = content.trim(); + // 简单检查是否以 { 或 [ 开头 + if (trimmedContent.startsWith('{') || trimmedContent.startsWith('[')) { + JSON.parse(trimmedContent); + return true; + } + } catch (e) { + // 不是有效的JSON + } + + // 检查Content-Type header + const contentType = document.querySelector('meta[http-equiv="content-type"]')?.getAttribute('content'); + if (contentType && contentType.includes('application/json')) { + return true; + } + + // 检查页面标题或URL是否暗示这是JSON + const title = document.title.toLowerCase(); + const url = window.location.href.toLowerCase(); + + if (title.includes('json') || url.includes('.json') || url.includes('api')) { + // 再次尝试解析,这次更宽松一些 + try { + const cleanedContent = content.replace(/[\u0000-\u001F\u007F-\u009F]/g, '').trim(); + if (cleanedContent.startsWith('{') || cleanedContent.startsWith('[')) { + JSON.parse(cleanedContent); + return true; + } + } catch (e) { + // 仍然不是有效的JSON + } + } + + return false; + } + }); + + return results[0]?.result || false; + } catch (error) { + console.error('检查页面JSON时发生错误:', error); + return false; + } + } + + /** + * 从页面提取JSON数据 + */ + async extractJsonFromPage(tabId) { + try { + const results = await chrome.scripting.executeScript({ + target: { tabId }, + func: () => { + const content = document.body.innerText || document.body.textContent; + + try { + // 清理内容并尝试解析 + const cleanedContent = content.replace(/[\u0000-\u001F\u007F-\u009F]/g, '').trim(); + const jsonData = JSON.parse(cleanedContent); + + return { + data: jsonData, + rawContent: cleanedContent, + url: window.location.href, + title: document.title + }; + } catch (error) { + console.error('解析JSON失败:', error); + return null; + } + } + }); + + return results[0]?.result; + } catch (error) { + console.error('提取JSON数据时发生错误:', error); + return null; + } + } + + /** + * 在当前标签页中打开JSON Tools页面并传递数据 + */ + async openJsonToolsWithJson(jsonData, sourceUrl, tabId) { + try { + const jsonString = typeof jsonData.data === 'string' + ? jsonData.data + : JSON.stringify(jsonData.data, null, 2); + + // 直接在当前标签页中打开JSON Tools页面 + await chrome.tabs.update(tabId, { + url: chrome.runtime.getURL('index.html'), + active: true + }); + + // 等待页面加载完成后发送数据 + setTimeout(() => { + chrome.tabs.sendMessage(tabId, { + type: 'UPDATE_JSON_DATA', + payload: { + type: 'regex', + payload: jsonString, + sourceUrl: sourceUrl, + title: jsonData.title + } + }); + }, 2000); + } catch (error) { + console.error('打开JSON Tools页面时发生错误:', error); + } + } + + /** + * 检查是否为受限URL + */ + isRestrictedUrl(url) { + if (!url) return true; + + const restrictedProtocols = ['chrome:', 'chrome-extension:', 'moz-extension:', 'edge:', 'opera:']; + const restrictedDomains = ['extensions', 'extensions-internals']; + + try { + const urlObj = new URL(url); + + // 检查协议 + if (restrictedProtocols.includes(urlObj.protocol)) { + return true; + } + + // 检查域名 + if (restrictedDomains.includes(urlObj.hostname)) { + return true; + } + + // 检查特殊页面 + if (url.includes('chrome://') || url.includes('about:')) { + return true; + } + + return false; + } catch (e) { + // 如果URL解析失败,认为是受限页面 + return true; + } + } + + /** + * 处理插件图标点击事件 + */ + async handleActionClick(tab) { + try { + // 在新标签页中打开JSON Tools主页 + await chrome.tabs.create({ + url: chrome.runtime.getURL('index.html'), + active: true + }); + + console.log('插件图标被点击,已打开JSON Tools主页'); + } catch (error) { + console.error('打开JSON Tools主页时发生错误:', error); + } + } + + /** + * 处理来自content script的消息 + */ + async handleMessage(request, sender, sendResponse) { + // 检查是否为受限页面 + if (sender.tab && this.isRestrictedUrl(sender.tab.url)) { + sendResponse({ success: false, message: '受限页面,无法操作' }); + return; + } + + switch (request.type) { + case 'CHECK_JSON': + // 手动检查当前页面是否为JSON + if (!sender.tab) { + sendResponse({ success: false, message: '无法获取页面信息' }); + return; + } + + const hasJson = await this.checkPageForJson(sender.tab.id); + if (hasJson) { + const jsonData = await this.extractJsonFromPage(sender.tab.id); + if (jsonData) { + await this.openJsonToolsWithJson(jsonData, sender.tab.url, sender.tab.id); + sendResponse({ success: true, message: 'JSON数据已加载到JSON Tools' }); + } else { + sendResponse({ success: false, message: '无法解析JSON数据' }); + } + } else { + sendResponse({ success: false, message: '当前页面不包含有效的JSON数据' }); + } + break; + + + + case 'GET_EXTENSION_STATUS': + sendResponse({ + initialized: this.isInitialized, + jsonToolsUrl: this.jsonToolsUrl + }); + break; + + default: + sendResponse({ success: false, message: '未知消息类型' }); + } + } +} + +// 初始化Chrome扩展监听器 +const chromeExtensionListener = new ChromeExtensionListener(); +chromeExtensionListener.initialize(); \ No newline at end of file diff --git a/chrome-extension/build.js b/chrome-extension/build.js new file mode 100644 index 0000000..c21791d --- /dev/null +++ b/chrome-extension/build.js @@ -0,0 +1,249 @@ +/** + * Chrome Extension Build Script + * 用于构建Chrome扩展版本 + */ + +import fs from 'fs'; +import path from 'path'; +import { execSync } from 'child_process'; +import { fileURLToPath } from 'url'; + +// 获取当前文件的目录路径 +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +class ChromeExtensionBuilder { + constructor() { + this.projectRoot = path.resolve(__dirname, '..'); + this.extensionDir = __dirname; + this.distDir = path.join(this.extensionDir, 'dist'); + } + + /** + * 构建Chrome扩展 + */ + async build() { + console.log('开始构建Chrome扩展...'); + console.log(`项目根目录: ${this.projectRoot}`); + console.log(`扩展目录: ${this.extensionDir}`); + + try { + // 1. 构建主应用 + console.log('步骤1: 构建主应用...'); + await this.buildMainApp(); + + // 2. 清空dist目录 + console.log('步骤0: 清空dist目录...'); + await this.cleanDistDir(); + + // 3. 复制构建产物到扩展目录 + console.log('步骤2: 复制构建产物...'); + await this.copyBuildAssets(); + + // 4. 创建扩展特定的文件 + console.log('步骤3: 创建扩展特定文件...'); + await this.createExtensionFiles(); + + // 5. 更新manifest.json + console.log('步骤4: 更新manifest.json...'); + await this.updateManifest(); + + console.log('✅ Chrome扩展构建完成!'); + console.log(`📁 扩展目录: ${this.distDir}`); + console.log('🚀 请在Chrome中加载此目录作为未打包的扩展程序'); + + } catch (error) { + console.error('❌ 构建失败:', error); + process.exit(1); + } + } + + /** + * 清空dist目录 + */ + async cleanDistDir() { + if (fs.existsSync(this.distDir)) { + console.log(`清空目录: ${this.distDir}`); + await this.removeDirectory(this.distDir); + } + } + + /** + * 递归删除目录 + */ + async removeDirectory(dir) { + if (fs.existsSync(dir)) { + const items = fs.readdirSync(dir); + + for (const item of items) { + const itemPath = path.join(dir, item); + const stat = fs.statSync(itemPath); + + if (stat.isDirectory()) { + await this.removeDirectory(itemPath); + } else { + fs.unlinkSync(itemPath); + } + } + + fs.rmdirSync(dir); + } + } + + /** + * 构建主应用 + */ + async buildMainApp() { + console.log('构建主应用...'); + + // 切换到项目根目录并执行构建 + process.chdir(this.projectRoot); + + try { + execSync('npm run build', { stdio: 'inherit' }); + } catch (error) { + throw new Error('主应用构建失败: ' + error.message); + } + + console.log('主应用构建完成'); + } + + /** + * 复制构建产物 + */ + async copyBuildAssets() { + console.log('复制构建产物...'); + + const sourceDir = path.join(this.projectRoot, 'dist'); + + // 确保目标目录存在 + if (!fs.existsSync(this.distDir)) { + fs.mkdirSync(this.distDir, { recursive: true }); + } + + // 复制所有构建产物 + await this.copyDirectory(sourceDir, this.distDir); + + console.log('构建产物复制完成'); + } + + /** + * 创建扩展特定的文件 + */ + async createExtensionFiles() { + console.log('创建扩展特定文件...'); + + // 处理现有的index.html文件 + await this.processIndexHtml(); + + // 复制扩展文件到dist目录 + const extensionFiles = [ + 'manifest.json', + 'background.js', + 'content.js', + 'injected.js' + ]; + + for (const file of extensionFiles) { + const sourcePath = path.join(this.extensionDir, file); + const targetPath = path.join(this.distDir, file); + + if (fs.existsSync(sourcePath)) { + fs.copyFileSync(sourcePath, targetPath); + } + } + + console.log('扩展特定文件创建完成'); + } + + /** + * 处理现有的index.html文件 + */ + async processIndexHtml() { + console.log('处理index.html文件...'); + + const indexPath = path.join(this.distDir, 'index.html'); + let indexContent = fs.readFileSync(indexPath, 'utf8'); + + // 提取Monaco环境配置的script标签内容 + const monacoScriptMatch = indexContent.match(/\n ' + ); + } + + // 重写处理后的index.html + fs.writeFileSync(indexPath, indexContent); + + console.log('✅ 成功提取Monaco配置到main.js并更新index.html'); + } else { + console.log('⚠️ 未找到Monaco环境配置script标签'); + } + } + + /** + * 更新manifest.json + */ + async updateManifest() { + console.log('更新manifest.json...'); + + const manifestPath = path.join(this.distDir, 'manifest.json'); + let manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')); + + // 更新manifest中的路径引用 + if (manifest.web_accessible_resources) { + manifest.web_accessible_resources[0].resources.push('main.js', 'assets/*'); + } + + fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2)); + + console.log('manifest.json更新完成'); + } + + /** + * 递归复制目录 + */ + async copyDirectory(source, target) { + if (!fs.existsSync(target)) { + fs.mkdirSync(target, { recursive: true }); + } + + const items = fs.readdirSync(source); + + for (const item of items) { + const sourcePath = path.join(source, item); + const targetPath = path.join(target, item); + + const stat = fs.statSync(sourcePath); + + if (stat.isDirectory()) { + await this.copyDirectory(sourcePath, targetPath); + } else { + fs.copyFileSync(sourcePath, targetPath); + } + } + } +} + +// 如果直接运行此脚本 +if (process.argv[1] && process.argv[1].endsWith('build.js')) { + const builder = new ChromeExtensionBuilder(); + builder.build().catch(console.error); +} + +export default ChromeExtensionBuilder; \ No newline at end of file diff --git a/chrome-extension/content.js b/chrome-extension/content.js new file mode 100644 index 0000000..72b2cd1 --- /dev/null +++ b/chrome-extension/content.js @@ -0,0 +1,86 @@ +/** + * Chrome Extension Content Script + * 在页面中注入脚本以检测和处理JSON数据 + */ + +class ContentScriptManager { + constructor() { + this.isInjected = false; + } + + /** + * 初始化content script + */ + initialize() { + if (this.isInjected) return; + + // 检查是否在受限页面中 + if (this.isRestrictedPage()) { + console.log('Content Script 跳过受限页面:', window.location.href); + return; + } + + // 注入脚本到页面 + this.injectScript(); + + this.isInjected = true; + console.log('Content Script 已初始化'); + } + + /** + * 注入脚本到页面 + */ + injectScript() { + const script = document.createElement('script'); + script.src = chrome.runtime.getURL('injected.js'); + script.onload = function() { + this.remove(); + }; + (document.head || document.documentElement).appendChild(script); + } + + /** + * 检查是否为受限页面 + */ + isRestrictedPage() { + const url = window.location.href; + const restrictedProtocols = ['chrome:', 'chrome-extension:', 'moz-extension:', 'edge:', 'opera:']; + const restrictedDomains = ['extensions', 'extensions-internals']; + + try { + const urlObj = new URL(url); + + // 检查协议 + if (restrictedProtocols.includes(urlObj.protocol)) { + return true; + } + + // 检查域名 + if (restrictedDomains.includes(urlObj.hostname)) { + return true; + } + + // 检查特殊页面 + if (url.includes('chrome://') || url.includes('about:')) { + return true; + } + + return false; + } catch (e) { + // 如果URL解析失败,认为是受限页面 + return true; + } + } +} + +// 初始化Content Script +const contentScriptManager = new ContentScriptManager(); + +// 等待DOM加载完成 +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', () => { + contentScriptManager.initialize(); + }); +} else { + contentScriptManager.initialize(); +} \ No newline at end of file diff --git a/chrome-extension/create-icons.js b/chrome-extension/create-icons.js new file mode 100644 index 0000000..cb65e4e --- /dev/null +++ b/chrome-extension/create-icons.js @@ -0,0 +1,34 @@ +/** + * 创建Chrome扩展图标 + */ + +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +// 获取当前文件的目录路径 +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +// 创建简单的SVG图标 +const svgIcon = ` + + {} + JSON +`; + +// 创建icons目录 +const iconsDir = path.join(__dirname, 'icons'); +if (!fs.existsSync(iconsDir)) { + fs.mkdirSync(iconsDir); +} + +// 保存SVG图标(作为占位符) +['16', '32', '48', '128'].forEach(size => { + const svgPath = path.join(iconsDir, `icon${size}.svg`); + fs.writeFileSync(svgPath, svgIcon); +}); + +console.log('图标文件已创建(SVG格式)'); +console.log('注意:实际使用时需要转换为PNG格式'); +console.log('可以使用在线工具或ImageMagick等工具进行转换'); \ No newline at end of file diff --git a/chrome-extension/injected.js b/chrome-extension/injected.js new file mode 100644 index 0000000..9a19103 --- /dev/null +++ b/chrome-extension/injected.js @@ -0,0 +1,176 @@ +/** + * Injected Script - 在页面上下文中运行 + * 用于检测JSON数据变化和处理页面交互 + */ + +(function() { + // 避免重复注入 + if (window.__jsonToolsInjected) { + return; + } + window.__jsonToolsInjected = true; + + class JsonPageDetector { + constructor() { + this.originalContent = null; + this.observer = null; + this.isJsonPage = false; + } + + /** + * 初始化检测器 + */ + initialize() { + this.checkPageType(); + this.setupObserver(); + this.setupEventListeners(); + + console.log('JSON Tools 页面检测器已初始化'); + } + + /** + * 检查页面类型 + */ + checkPageType() { + const content = document.body.innerText || document.body.textContent; + + if (!content || content.trim().length === 0) { + this.isJsonPage = false; + return; + } + + try { + const trimmedContent = content.trim(); + if (trimmedContent.startsWith('{') || trimmedContent.startsWith('[')) { + JSON.parse(trimmedContent); + this.isJsonPage = true; + this.originalContent = trimmedContent; + this.notifyPageTypeChange(true); + } + } catch (e) { + this.isJsonPage = false; + } + } + + /** + * 设置MutationObserver监听页面变化 + */ + setupObserver() { + if (!window.MutationObserver) { + return; + } + + this.observer = new MutationObserver((mutations) => { + let shouldRecheck = false; + + mutations.forEach((mutation) => { + if (mutation.type === 'childList' && mutation.target === document.body) { + shouldRecheck = true; + } + }); + + if (shouldRecheck) { + // 延迟检查,避免频繁触发 + setTimeout(() => { + const wasJsonPage = this.isJsonPage; + this.checkPageType(); + + if (wasJsonPage !== this.isJsonPage) { + this.notifyPageTypeChange(this.isJsonPage); + } + }, 500); + } + }); + + this.observer.observe(document.body, { + childList: true, + subtree: true, + characterData: true + }); + } + + /** + * 设置事件监听器 + */ + setupEventListeners() { + // 监听URL变化(适用于SPA应用) + let currentUrl = window.location.href; + setInterval(() => { + if (window.location.href !== currentUrl) { + currentUrl = window.location.href; + setTimeout(() => this.checkPageType(), 1000); + } + }, 1000); + + // 监听页面可见性变化 + document.addEventListener('visibilitychange', () => { + if (!document.hidden) { + setTimeout(() => this.checkPageType(), 500); + } + }); + } + + /** + * 通知页面类型变化 + */ + notifyPageTypeChange(isJsonPage) { + // 通过自定义事件通知content script + const event = new CustomEvent('jsonToolsPageTypeChange', { + detail: { isJsonPage, url: window.location.href } + }); + document.dispatchEvent(event); + } + + /** + * 获取当前JSON数据 + */ + getCurrentJsonData() { + if (!this.isJsonPage) { + return null; + } + + try { + const content = document.body.innerText || document.body.textContent; + const cleanedContent = content.replace(/[\u0000-\u001F\u007F-\u009F]/g, '').trim(); + const jsonData = JSON.parse(cleanedContent); + + return { + data: jsonData, + rawContent: cleanedContent, + url: window.location.href, + title: document.title, + timestamp: new Date().toISOString() + }; + } catch (error) { + console.error('获取JSON数据失败:', error); + return null; + } + } + + /** + * 清理资源 + */ + destroy() { + if (this.observer) { + this.observer.disconnect(); + this.observer = null; + } + } + } + + // 创建并初始化检测器 + const detector = new JsonPageDetector(); + + // 等待页面加载完成 + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', () => { + detector.initialize(); + }); + } else { + detector.initialize(); + } + + // 暴露接口到全局,供content script调用 + window.__jsonToolsDetector = detector; + +})(); \ No newline at end of file diff --git a/chrome-extension/manifest.json b/chrome-extension/manifest.json new file mode 100644 index 0000000..3a1c211 --- /dev/null +++ b/chrome-extension/manifest.json @@ -0,0 +1,46 @@ +{ + "manifest_version": 3, + "name": "JSON Tools - JSON可视化工具", + "version": "1.0.0", + "description": "自动检测浏览器中的JSON数据并使用JSON Tools进行可视化展示", + "permissions": [ + "activeTab", + "storage", + "scripting" + ], + "host_permissions": [ + "" + ], + "background": { + "service_worker": "background.js", + "type": "module" + }, + "content_scripts": [ + { + "matches": ["http://*/*", "https://*/*", "file://*/*"], + "js": ["content.js"], + "run_at": "document_idle" + } + ], + "action": { + "default_title": "JSON Tools", + "default_icon": { + "64": "pwa-64x64.png", + "96": "pwa-96x96.png", + "192": "pwa-192x192.png", + "512": "pwa-512x512.png" + } + }, + "icons": { + "64": "pwa-64x64.png", + "96": "pwa-96x96.png", + "192": "pwa-192x192.png", + "512": "pwa-512x512.png" + }, + "web_accessible_resources": [ + { + "resources": ["injected.js"], + "matches": [""] + } + ] +} \ No newline at end of file diff --git a/chrome-extension/test-extension.js b/chrome-extension/test-extension.js new file mode 100644 index 0000000..24fdb2d --- /dev/null +++ b/chrome-extension/test-extension.js @@ -0,0 +1,208 @@ +/** + * Chrome扩展测试脚本 + * 用于验证扩展功能是否正常工作 + */ + +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +// 获取当前文件的目录路径 +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +class ExtensionTester { + constructor() { + this.extensionDir = path.resolve(__dirname); + this.distDir = path.join(this.extensionDir, 'dist'); + } + + /** + * 运行所有测试 + */ + async runTests() { + console.log('开始测试Chrome扩展...\n'); + + const tests = [ + this.testManifestExists, + this.testRequiredFiles, + this.testManifestStructure, + this.testContentScript, + this.testBackgroundScript, + this.testPopupFiles + ]; + + let passed = 0; + let failed = 0; + + for (const test of tests) { + try { + await test.call(this); + passed++; + console.log('✅ 通过'); + } catch (error) { + failed++; + console.log(`❌ 失败: ${error.message}`); + } + console.log(''); + } + + console.log(`\n测试完成: ${passed} 通过, ${failed} 失败`); + + if (failed > 0) { + console.log('\n请修复失败的测试后重试'); + process.exit(1); + } else { + console.log('\n所有测试通过!扩展已准备就绪。'); + } + } + + /** + * 测试manifest.json是否存在 + */ + testManifestExists() { + console.log('测试 manifest.json 是否存在...'); + + const manifestPath = path.join(this.distDir, 'manifest.json'); + if (!fs.existsSync(manifestPath)) { + throw new Error('manifest.json 文件不存在'); + } + } + + /** + * 测试必需文件是否存在 + */ + testRequiredFiles() { + console.log('测试必需文件是否存在...'); + + const requiredFiles = [ + 'manifest.json', + 'background.js', + 'content.js', + 'injected.js', + 'popup.html', + 'popup.js' + ]; + + for (const file of requiredFiles) { + const filePath = path.join(this.distDir, file); + if (!fs.existsSync(filePath)) { + throw new Error(`必需文件缺失: ${file}`); + } + } + } + + /** + * 测试manifest.json结构 + */ + testManifestStructure() { + console.log('测试 manifest.json 结构...'); + + const manifestPath = path.join(this.distDir, 'manifest.json'); + const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')); + + // 检查必需字段 + const requiredFields = ['manifest_version', 'name', 'version', 'permissions']; + for (const field of requiredFields) { + if (!manifest[field]) { + throw new Error(`manifest.json 缺少必需字段: ${field}`); + } + } + + // 检查manifest版本 + if (manifest.manifest_version !== 3) { + throw new Error('manifest_version 应该是 3'); + } + + // 检查权限 + const requiredPermissions = ['activeTab', 'storage', 'scripting']; + for (const permission of requiredPermissions) { + if (!manifest.permissions.includes(permission)) { + throw new Error(`缺少必需权限: ${permission}`); + } + } + } + + /** + * 测试content script + */ + testContentScript() { + console.log('测试 content script...'); + + const contentScriptPath = path.join(this.distDir, 'content.js'); + const content = fs.readFileSync(contentScriptPath, 'utf8'); + + // 检查关键函数 + const requiredFunctions = [ + 'ContentScriptManager', + 'initialize', + 'checkIsJsonPage', + 'addFloatingButton' + ]; + + for (const func of requiredFunctions) { + if (!content.includes(func)) { + throw new Error(`content.js 缺少关键函数: ${func}`); + } + } + } + + /** + * 测试background script + */ + testBackgroundScript() { + console.log('测试 background script...'); + + const backgroundScriptPath = path.join(this.distDir, 'background.js'); + const content = fs.readFileSync(backgroundScriptPath, 'utf8'); + + // 检查关键函数 + const requiredFunctions = [ + 'ChromeExtensionListener', + 'initialize', + 'checkPageForJson', + 'extractJsonFromPage', + 'openJsonToolsWithJson' + ]; + + for (const func of requiredFunctions) { + if (!content.includes(func)) { + throw new Error(`background.js 缺少关键函数: ${func}`); + } + } + } + + /** + * 测试popup文件 + */ + testPopupFiles() { + console.log('测试 popup 文件...'); + + // 检查popup.html + const popupHtmlPath = path.join(this.distDir, 'popup.html'); + const popupHtml = fs.readFileSync(popupHtmlPath, 'utf8'); + + if (!popupHtml.includes('popup.js')) { + throw new Error('popup.html 未引用 popup.js'); + } + + // 检查popup.js + const popupJsPath = path.join(this.distDir, 'popup.js'); + const popupJs = fs.readFileSync(popupJsPath, 'utf8'); + + const requiredFunctions = ['PopupManager', 'initialize', 'openJsonTools']; + for (const func of requiredFunctions) { + if (!popupJs.includes(func)) { + throw new Error(`popup.js 缺少关键函数: ${func}`); + } + } + } +} + +// 如果直接运行此脚本 +if (process.argv[1] && process.argv[1].endsWith('test-extension.js')) { + const tester = new ExtensionTester(); + tester.runTests().catch(console.error); +} + +export default ExtensionTester; \ No newline at end of file diff --git a/chrome-extension/test-fix.js b/chrome-extension/test-fix.js new file mode 100644 index 0000000..d7b594f --- /dev/null +++ b/chrome-extension/test-fix.js @@ -0,0 +1,116 @@ +/** + * 测试修复后的Chrome扩展 + */ + +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +function testRestrictedUrls() { + console.log('🧪 测试受限URL检查逻辑...'); + + const testCases = [ + { url: 'chrome://extensions/', expected: true, description: 'Chrome扩展页面' }, + { url: 'chrome-extension://abc123/popup.html', expected: true, description: '扩展内部页面' }, + { url: 'https://example.com', expected: false, description: '普通HTTPS页面' }, + { url: 'http://localhost:3000', expected: false, description: '本地开发页面' }, + { url: 'file:///Users/test/data.json', expected: false, description: '本地文件' }, + { url: 'about:blank', expected: true, description: 'about:页面' }, + { url: 'invalid-url', expected: true, description: '无效URL' } + ]; + + const restrictedProtocols = ['chrome:', 'chrome-extension:', 'moz-extension:', 'edge:', 'opera:']; + const restrictedDomains = ['extensions', 'extensions-internals']; + + testCases.forEach(testCase => { + let result = false; + + try { + const urlObj = new URL(testCase.url); + + // 检查协议 + if (restrictedProtocols.includes(urlObj.protocol)) { + result = true; + } + // 检查域名 + else if (restrictedDomains.includes(urlObj.hostname)) { + result = true; + } + // 检查特殊页面 + else if (testCase.url.includes('chrome://') || testCase.url.includes('about:')) { + result = true; + } + } catch (e) { + result = true; + } + + const status = result === testCase.expected ? '✅' : '❌'; + console.log(`${status} ${testCase.description}: ${testCase.url} -> ${result}`); + }); +} + +function testManifestContent() { + console.log('\n🧪 测试manifest.json配置...'); + + const manifestPath = path.join(__dirname, 'dist', 'manifest.json'); + + if (!fs.existsSync(manifestPath)) { + console.log('❌ manifest.json 不存在'); + return; + } + + const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')); + + // 检查content_scripts配置 + const contentScripts = manifest.content_scripts; + if (!contentScripts || contentScripts.length === 0) { + console.log('❌ content_scripts 配置缺失'); + return; + } + + const matches = contentScripts[0].matches; + const hasRestrictedUrls = matches.includes(''); + const hasValidUrls = matches.includes('http://*/*') && matches.includes('https://*/*'); + + if (hasRestrictedUrls) { + console.log('❌ 仍在使用 ,应该限制为具体协议'); + } else if (hasValidUrls) { + console.log('✅ content_scripts 配置正确,已限制为HTTP/HTTPS/File协议'); + } else { + console.log('⚠️ content_scripts 配置可能有问题'); + } +} + +function testFilesExist() { + console.log('\n🧪 测试必需文件是否存在...'); + + const requiredFiles = [ + 'dist/manifest.json', + 'dist/background.js', + 'dist/content.js', + 'dist/injected.js', + 'dist/popup.html', + 'dist/popup.js' + ]; + + requiredFiles.forEach(file => { + const filePath = path.join(__dirname, file); + const exists = fs.existsSync(filePath); + const status = exists ? '✅' : '❌'; + console.log(`${status} ${file}`); + }); +} + +// 运行所有测试 +console.log('🔧 Chrome扩展修复验证测试\n'); + +testRestrictedUrls(); +testManifestContent(); +testFilesExist(); + +console.log('\n✨ 测试完成!'); +console.log('💡 如果所有测试都通过,说明修复成功。'); +console.log('🚀 请在Chrome中重新加载扩展进行测试。'); \ No newline at end of file diff --git a/chrome-extension/test-pages/sample-api.json b/chrome-extension/test-pages/sample-api.json new file mode 100644 index 0000000..f7320f8 --- /dev/null +++ b/chrome-extension/test-pages/sample-api.json @@ -0,0 +1,40 @@ +{ + "success": true, + "data": { + "users": [ + { + "id": 1, + "name": "张三", + "email": "zhangsan@example.com", + "profile": { + "age": 28, + "city": "北京", + "interests": ["编程", "阅读", "旅行"] + }, + "active": true, + "created_at": "2023-01-15T08:30:00Z" + }, + { + "id": 2, + "name": "李四", + "email": "lisi@example.com", + "profile": { + "age": 32, + "city": "上海", + "interests": ["设计", "音乐", "摄影"] + }, + "active": false, + "created_at": "2023-02-20T14:15:00Z" + } + ], + "pagination": { + "current_page": 1, + "per_page": 10, + "total": 25, + "total_pages": 3 + } + }, + "message": "用户列表获取成功", + "timestamp": "2023-12-07T10:30:00Z", + "version": "1.0.0" +} \ No newline at end of file diff --git a/chrome-extension/test-pages/test.html b/chrome-extension/test-pages/test.html new file mode 100644 index 0000000..9ccab64 --- /dev/null +++ b/chrome-extension/test-pages/test.html @@ -0,0 +1,197 @@ + + + + + + JSON测试页面 + + + +

Chrome扩展JSON测试页面

+ +
+

测试1: 直接JSON响应

+

这个页面包含直接的JSON数据,扩展应该能自动检测到。

+ +
+
+ +
+

测试2: API响应模拟

+

模拟API返回的JSON数据。

+ +
+
+ +
+

测试3: 复杂嵌套JSON

+

测试复杂嵌套结构的JSON数据。

+ +
+
+ +
+

测试4: 错误JSON

+

测试格式错误的JSON(扩展应该忽略)。

+ +
+
+ + + + \ No newline at end of file diff --git a/package.json b/package.json index 9f64349..0d4d30b 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,9 @@ "scripts": { "dev": "vite", "build": "tsc && vite build", + "build:extension": "node chrome-extension/build.js", + "test:extension": "node chrome-extension/test-extension.js", + "create:extension-icons": "node chrome-extension/create-icons.js", "lint": "eslint -c .eslintrc.json ./src/**/**/*.{ts,tsx} --fix", "preview": "vite preview", "semantic-release": "semantic-release" @@ -14,7 +17,7 @@ "@heroui/react": "^2.7.11", "@iarna/toml": "^2.2.5", "@jsonquerylang/jsonquery": "^5.0.4", - "@monaco-editor/react": "4.7.0-rc.0", + "@monaco-editor/react": "4.8.0-rc.0", "@react-aria/visually-hidden": "3.8.19", "@react-types/shared": "3.26.0", "@types/validator": "^13.15.2", @@ -48,11 +51,12 @@ "tailwindcss": "^3.4.17", "toml": "^3.0.0", "validator": "^13.15.15", - "vanilla-jsoneditor-cn": "^2.2.3", + "vanilla-jsoneditor": "^3.1.0", "xml-js": "^1.6.11", "zustand": "^5.0.5" }, "devDependencies": { + "@types/chrome": "^0.0.332", "@iconify/react": "^5.2.1", "@semantic-release/changelog": "^6.0.3", "@semantic-release/git": "^10.0.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4e97cff..44e08f9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,8 +18,8 @@ importers: specifier: ^5.0.4 version: 5.0.4 '@monaco-editor/react': - specifier: 4.7.0-rc.0 - version: 4.7.0-rc.0(monaco-editor@0.52.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + specifier: 4.8.0-rc.0 + version: 4.8.0-rc.0(monaco-editor@0.52.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@react-aria/visually-hidden': specifier: 3.8.19 version: 3.8.19(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -119,9 +119,9 @@ importers: validator: specifier: ^13.15.15 version: 13.15.15 - vanilla-jsoneditor-cn: - specifier: ^2.2.3 - version: 2.2.3 + vanilla-jsoneditor: + specifier: ^3.1.0 + version: 3.10.0 xml-js: specifier: ^1.6.11 version: 1.6.11 @@ -138,6 +138,9 @@ importers: '@semantic-release/git': specifier: ^10.0.1 version: 10.0.1(semantic-release@23.1.1(typescript@5.6.3)) + '@types/chrome': + specifier: ^0.0.332 + version: 0.0.332 '@types/js-yaml': specifier: ^4.0.9 version: 4.0.9 @@ -1750,10 +1753,6 @@ packages: peerDependencies: jsep: ^0.4.0||^1.0.0 - '@jsonquerylang/jsonquery@4.1.1': - resolution: {integrity: sha512-Rfyvq70Zrb561BqSuXLsl0rG0/1tz913EQDL/4zpkp+laFGUxXIVPSaJWcdREJwADXLZDkQyaWplzEaPQvh+7A==} - hasBin: true - '@jsonquerylang/jsonquery@5.0.4': resolution: {integrity: sha512-QdgVkapeGRxUqOOJuh2svDutejKaCizhupEmO4ZKSsaLolD7w5QhgrjmBNuS1wMCM5TyNKifK4i1wBDfNzO9xQ==} hasBin: true @@ -1773,11 +1772,11 @@ packages: '@marijn/find-cluster-break@1.0.2': resolution: {integrity: sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==} - '@monaco-editor/loader@1.5.0': - resolution: {integrity: sha512-hKoGSM+7aAc7eRTRjpqAZucPmoNOC4UUbknb/VNoTkEIkCPhqV8LfbsgM1webRM7S/z21eHEx9Fkwx8Z/C/+Xw==} + '@monaco-editor/loader@1.6.1': + resolution: {integrity: sha512-w3tEnj9HYEC73wtjdpR089AqkUPskFRcdkxsiSFt3SoUc3OHpmu+leP94CXBm4mHfefmhsdfI0ZQu6qJ0wgtPg==} - '@monaco-editor/react@4.7.0-rc.0': - resolution: {integrity: sha512-YfjXkDK0bcwS0zo8PXptvQdCQfOPPtzGsAzmIv7PnoUGFdIohsR+NVDyjbajMddF+3cWUm/3q9NzP/DUke9a+w==} + '@monaco-editor/react@4.8.0-rc.0': + resolution: {integrity: sha512-8XmP088nzZByuhgMh52iaT3TV1/6rVblmkZZMI3T7aWFUdaqWU9CdYj8mcTZ5l8Lc2YvLw0hbF8JfTP4fTW/Bg==} peerDependencies: monaco-editor: '>= 0.25.0 < 1' react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -1878,36 +1877,42 @@ packages: engines: {node: '>= 10.0.0'} cpu: [arm] os: [linux] + libc: [glibc] '@parcel/watcher-linux-arm-musl@2.5.1': resolution: {integrity: sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==} engines: {node: '>= 10.0.0'} cpu: [arm] os: [linux] + libc: [musl] '@parcel/watcher-linux-arm64-glibc@2.5.1': resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] '@parcel/watcher-linux-arm64-musl@2.5.1': resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [linux] + libc: [musl] '@parcel/watcher-linux-x64-glibc@2.5.1': resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [linux] + libc: [glibc] '@parcel/watcher-linux-x64-musl@2.5.1': resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [linux] + libc: [musl] '@parcel/watcher-win32-arm64@2.5.1': resolution: {integrity: sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==} @@ -2528,56 +2533,67 @@ packages: resolution: {integrity: sha512-x+e/Z9H0RAWckn4V2OZZl6EmV0L2diuX3QB0uM1r6BvhUIv6xBPL5mrAX2E3e8N8rEHVPwFfz/ETUbV4oW9+lQ==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.44.0': resolution: {integrity: sha512-1exwiBFf4PU/8HvI8s80icyCcnAIB86MCBdst51fwFmH5dyeoWVPVgmQPcKrMtBQ0W5pAs7jBCWuRXgEpRzSCg==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.44.0': resolution: {integrity: sha512-ZTR2mxBHb4tK4wGf9b8SYg0Y6KQPjGpR4UWwTFdnmjB4qRtoATZ5dWn3KsDwGa5Z2ZBOE7K52L36J9LueKBdOQ==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.44.0': resolution: {integrity: sha512-GFWfAhVhWGd4r6UxmnKRTBwP1qmModHtd5gkraeW2G490BpFOZkFtem8yuX2NyafIP/mGpRJgTJ2PwohQkUY/Q==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-loongarch64-gnu@4.44.0': resolution: {integrity: sha512-xw+FTGcov/ejdusVOqKgMGW3c4+AgqrfvzWEVXcNP6zq2ue+lsYUgJ+5Rtn/OTJf7e2CbgTFvzLW2j0YAtj0Gg==} cpu: [loong64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-powerpc64le-gnu@4.44.0': resolution: {integrity: sha512-bKGibTr9IdF0zr21kMvkZT4K6NV+jjRnBoVMt2uNMG0BYWm3qOVmYnXKzx7UhwrviKnmK46IKMByMgvpdQlyJQ==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-gnu@4.44.0': resolution: {integrity: sha512-vV3cL48U5kDaKZtXrti12YRa7TyxgKAIDoYdqSIOMOFBXqFj2XbChHAtXquEn2+n78ciFgr4KIqEbydEGPxXgA==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.44.0': resolution: {integrity: sha512-TDKO8KlHJuvTEdfw5YYFBjhFts2TR0VpZsnLLSYmB7AaohJhM8ctDSdDnUGq77hUh4m/djRafw+9zQpkOanE2Q==} cpu: [riscv64] os: [linux] + libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.44.0': resolution: {integrity: sha512-8541GEyktXaw4lvnGp9m84KENcxInhAt6vPWJ9RodsB/iGjHoMB2Pp5MVBCiKIRxrxzJhGCxmNzdu+oDQ7kwRA==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.44.0': resolution: {integrity: sha512-iUVJc3c0o8l9Sa/qlDL2Z9UP92UZZW1+EmQ4xfjTc1akr0iUFZNfxrXJ/R1T90h/ILm9iXEY6+iPrmYB3pXKjw==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.44.0': resolution: {integrity: sha512-PQUobbhLTQT5yz/SPg116VJBgz+XOtXt8D1ck+sfJJhuEsMj2jSej5yTdp8CvWBSceu+WW+ibVL6dm0ptG5fcA==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-win32-arm64-msvc@4.44.0': resolution: {integrity: sha512-M0CpcHf8TWn+4oTxJfh7LQuTuaYeXGbk0eageVjQCKzYLsajWS/lFC94qlRqOlyC2KvRT90ZrfXULYmukeIy7w==} @@ -2691,6 +2707,9 @@ packages: '@types/babel__traverse@7.20.7': resolution: {integrity: sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==} + '@types/chrome@0.0.332': + resolution: {integrity: sha512-Fl4luF9q/iroXMKbL1LQyr/nCKhKwkwkZQ9qeeePFECW/8SP/Lrx2kJ26wnpTpr0iAznkENoO8g924CP4mfNxw==} + '@types/debug@4.1.12': resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} @@ -2709,6 +2728,15 @@ packages: '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/filesystem@0.0.36': + resolution: {integrity: sha512-vPDXOZuannb9FZdxgHnqSwAG/jvdGM8Wq+6N4D/d80z+D4HWH+bItqsZaVRQykAn6WEVeEkLm2oQigyHtgb0RA==} + + '@types/filewriter@0.0.33': + resolution: {integrity: sha512-xFU8ZXTw4gd358lb2jw25nxY9QAgqn2+bKKjKOYfNCzN4DKCFetK7sPtrlpg66Ywe3vWY9FNxprZawAh9wfJ3g==} + + '@types/har-format@1.2.16': + resolution: {integrity: sha512-fluxdy7ryD3MV6h8pTfTYpy/xQzCFC7m89nOH9y94cNqJ1mDIDPut7MnRHI3F6qRmh/cT2fUjG1MLdCNb4hE9A==} + '@types/hast@2.3.10': resolution: {integrity: sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==} @@ -6523,8 +6551,8 @@ packages: resolution: {integrity: sha512-BgWVbCI72aIQy937xbawcs+hrVaN/CZ2UwutgaJ36hGqRrLNM+f5LUT/YPRbo8IV/ASeFzXszezV+y2+rq3l8A==} engines: {node: '>= 0.10'} - vanilla-jsoneditor-cn@2.2.3: - resolution: {integrity: sha512-fq1ObO3f2a0ACK0uOr5A8pYmYck6MfyS2Eyu73PW6KEwTAtqfD7pQT6kUOcwsPREZy8zDu54afs7N900Zci+ug==} + vanilla-jsoneditor@3.10.0: + resolution: {integrity: sha512-AiI8vDEqUhc63rA8JykuhyOBse5xu/yZXL1B3iij59Y0zWb53Z5cOBgiFwh4pa4fAqOfDsp9flezqNZdDWk9lQ==} vanilla-picker@2.12.3: resolution: {integrity: sha512-qVkT1E7yMbUsB2mmJNFmaXMWE2hF8ffqzMMwe9zdAikd8u2VfnsVY2HQcOUi2F38bgbxzlJBEdS1UUhOXdF9GQ==} @@ -8934,8 +8962,6 @@ snapshots: dependencies: jsep: 1.4.0 - '@jsonquerylang/jsonquery@4.1.1': {} - '@jsonquerylang/jsonquery@5.0.4': {} '@lezer/common@1.2.3': {} @@ -8956,13 +8982,13 @@ snapshots: '@marijn/find-cluster-break@1.0.2': {} - '@monaco-editor/loader@1.5.0': + '@monaco-editor/loader@1.6.1': dependencies: state-local: 1.0.7 - '@monaco-editor/react@4.7.0-rc.0(monaco-editor@0.52.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + '@monaco-editor/react@4.8.0-rc.0(monaco-editor@0.52.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: - '@monaco-editor/loader': 1.5.0 + '@monaco-editor/loader': 1.6.1 monaco-editor: 0.52.2 react: 19.1.0 react-dom: 19.1.0(react@19.1.0) @@ -10205,6 +10231,11 @@ snapshots: dependencies: '@babel/types': 7.27.6 + '@types/chrome@0.0.332': + dependencies: + '@types/filesystem': 0.0.36 + '@types/har-format': 1.2.16 + '@types/debug@4.1.12': dependencies: '@types/ms': 2.1.0 @@ -10227,6 +10258,14 @@ snapshots: '@types/estree@1.0.8': {} + '@types/filesystem@0.0.36': + dependencies: + '@types/filewriter': 0.0.33 + + '@types/filewriter@0.0.33': {} + + '@types/har-format@1.2.16': {} + '@types/hast@2.3.10': dependencies: '@types/unist': 2.0.11 @@ -11074,7 +11113,7 @@ snapshots: dependencies: cipher-base: 1.0.6 inherits: 2.0.4 - ripemd160: 2.0.1 + ripemd160: 2.0.2 sha.js: 2.4.11 create-hash@1.2.0: @@ -14684,7 +14723,7 @@ snapshots: validator@13.15.15: {} - vanilla-jsoneditor-cn@2.2.3: + vanilla-jsoneditor@3.10.0: dependencies: '@codemirror/autocomplete': 6.18.6 '@codemirror/commands': 6.8.1 @@ -14696,7 +14735,7 @@ snapshots: '@codemirror/view': 6.37.2 '@fortawesome/free-regular-svg-icons': 6.7.2 '@fortawesome/free-solid-svg-icons': 6.7.2 - '@jsonquerylang/jsonquery': 4.1.1 + '@jsonquerylang/jsonquery': 5.0.4 '@lezer/highlight': 1.2.1 '@replit/codemirror-indentation-markers': 6.5.3(@codemirror/language@6.11.1)(@codemirror/state@6.5.2)(@codemirror/view@6.37.2) ajv: 8.17.1 diff --git a/src/components/monacoEditor/MonacoJsonEditor.tsx b/src/components/monacoEditor/MonacoJsonEditor.tsx index e94322d..491a2af 100644 --- a/src/components/monacoEditor/MonacoJsonEditor.tsx +++ b/src/components/monacoEditor/MonacoJsonEditor.tsx @@ -372,11 +372,13 @@ const MonacoJsonEditor: React.FC = ({ } try { - // 不支持 lossless-json 语法 + // 不支持 lossless-json 语法 解析 JSON 数据 const jsonData = JSON.parse(editorValue); + // 使用 jsonQuery 进行过滤 const filteredData = jsonquery(jsonData, currentFilter); + // 格式化过滤后的数据 const formattedResult = stringifyJson(filteredData, 2); setFilteredValue(formattedResult); diff --git a/src/components/vanillaJsonEditor/VanillaJsonEditor.tsx b/src/components/vanillaJsonEditor/VanillaJsonEditor.tsx index f7e5b1e..3ff0c00 100644 --- a/src/components/vanillaJsonEditor/VanillaJsonEditor.tsx +++ b/src/components/vanillaJsonEditor/VanillaJsonEditor.tsx @@ -6,7 +6,7 @@ import { JsonEditor, JSONEditorPropsOptional, Mode, -} from "vanilla-jsoneditor-cn"; +} from "vanilla-jsoneditor"; import "@/styles/vanilla.scss"; diff --git a/src/pages/indexPage.tsx b/src/pages/indexPage.tsx index a1671f1..b148e60 100644 --- a/src/pages/indexPage.tsx +++ b/src/pages/indexPage.tsx @@ -1,7 +1,7 @@ import { useEffect, useLayoutEffect, useRef, useState } from "react"; import { cn } from "@heroui/react"; import { useTheme } from "next-themes"; -import { Content } from "vanilla-jsoneditor-cn"; +import { Content } from "vanilla-jsoneditor"; import { useTabStore } from "@/store/useTabStore"; import DynamicTabs, { @@ -17,7 +17,7 @@ import VanillaJsonEditor, { VanillaJsonEditorRef, } from "@/components/vanillaJsonEditor/VanillaJsonEditor.tsx"; -import "vanilla-jsoneditor-cn/themes/jse-theme-dark.css"; +import "vanilla-jsoneditor/themes/jse-theme-dark.css"; import MonacoDiffEditor, { MonacoDiffEditorRef, } from "@/components/monacoEditor/MonacoDiffEditor.tsx"; @@ -34,6 +34,7 @@ import clipboard from "@/utils/clipboard"; import toast from "@/utils/toast"; import { stringifyJson } from "@/utils/json"; import UtoolsListener from "@/services/utoolsListener"; +import ChromeExtensionListener from "@/services/chromeExtensionListener"; export default function IndexPage() { const { theme } = useTheme(); @@ -506,6 +507,14 @@ export default function IndexPage() { // 同步标签页数据 await syncTabStore(); + + // 初始化 ChromeExtensionListener + ChromeExtensionListener.getInstance().initialize(); + + // 通知Chrome扩展页面已准备就绪 + setTimeout(() => { + ChromeExtensionListener.getInstance().notifyPageReady(); + }, 1000); }; init(); @@ -514,6 +523,9 @@ export default function IndexPage() { useEffect(() => { // 设置 UtoolsListener 的编辑器引用 UtoolsListener.getInstance().setEditorRefs(monacoJsonEditorRefs.current); + + // 设置 ChromeExtensionListener 的编辑器引用 + ChromeExtensionListener.getInstance().setEditorRefs(monacoJsonEditorRefs.current); }, [monacoJsonEditorRefs]); useEffect(() => { diff --git a/src/services/chromeExtensionListener.ts b/src/services/chromeExtensionListener.ts new file mode 100644 index 0000000..fd4bdef --- /dev/null +++ b/src/services/chromeExtensionListener.ts @@ -0,0 +1,183 @@ +import { useTabStore } from "@/store/useTabStore"; + +/** + * Chrome扩展事件监听器管理 + * + * 这个类负责管理Chrome插件的监听事件, + * 包括从background script接收JSON数据。 + * 复用utoolsListener.ts的逻辑结构,但适配Chrome扩展环境。 + */ +class ChromeExtensionListener { + private static instance: ChromeExtensionListener; + private isInitialized: boolean = false; + private editorRefs: Record = {}; + + private constructor() {} + + /** + * 获取单例实例 + */ + public static getInstance(): ChromeExtensionListener { + if (!ChromeExtensionListener.instance) { + ChromeExtensionListener.instance = new ChromeExtensionListener(); + } + + return ChromeExtensionListener.instance; + } + + /** + * 设置编辑器引用 + */ + public setEditorRefs(editorRefs: Record): void { + this.editorRefs = editorRefs; + } + + /** + * 初始化监听器 + */ + public initialize(): void { + if (this.isInitialized) { + return; + } + + if (typeof window !== "undefined") { + // 监听来自Chrome扩展的消息 + this.setupMessageListener(); + this.isInitialized = true; + console.log("Chrome扩展监听器已初始化"); + } + } + + /** + * 设置消息监听器 + */ + private setupMessageListener(): void { + // 监听来自Chrome扩展的消息 + window.addEventListener('message', (event) => { + // 确保消息来源可信 + if (event.source !== window) { + return; + } + + if (event.data && event.data.type === 'CHROME_EXTENSION_MESSAGE') { + this.handleExtensionMessage(event.data.payload); + } + }); + + // 如果是在Chrome扩展环境中,也监听chrome.runtime消息 + if (typeof chrome !== 'undefined' && chrome.runtime && chrome.runtime.onMessage) { + chrome.runtime.onMessage.addListener((request, _sender, sendResponse) => { + if (request.type === 'UPDATE_JSON_DATA') { + this.handleExtensionMessage(request.payload); + sendResponse({ success: true }); + } + return true; + }); + } + } + + /** + * 处理Chrome扩展消息 + */ + private handleExtensionMessage(data: any): void { + try { + console.log("Chrome扩展消息:", data); + + if (data.type === "regex" && data.payload != "") { + const tabStore = useTabStore.getState(); + const { addTab, activeTab, updateTabContent } = tabStore; + + setTimeout(() => { + const curTab = activeTab(); + + // 当前tab1 且 nextKey 为 2 且tab1内容为空 + if ( + curTab && + curTab.key === "1" && + tabStore.nextKey === 2 && + curTab.content.trim() === "" + ) { + // 更新现有标签页内容 + updateTabContent(curTab.key, data.payload); + + // 调用编辑器的 updateValue 方法更新编辑器内容 + const editorRef = this.editorRefs[curTab.key]; + + if (editorRef && editorRef.updateValue) { + editorRef.updateValue(data.payload); + setTimeout(() => { + editorRef.format(); + }, 300); + } + console.log("Chrome扩展数据已更新到 tab1:", data.payload); + } else { + // 创建新标签页,包含来源信息 + const title = data.sourceUrl + ? `JSON - ${new URL(data.sourceUrl).hostname}` + : data.title || "JSON数据"; + + addTab(title, data.payload); + console.log("Chrome扩展已创建新标签页:", data.payload); + } + }, 100); + } + } catch (error) { + console.error("处理Chrome扩展消息时发生错误:", error); + } + } + + /** + * 检查是否已初始化 + */ + public isListenerInitialized(): boolean { + return this.isInitialized; + } + + /** + * 检查是否在Chrome扩展环境中 + */ + public isChromeExtensionEnvironment(): boolean { + return typeof chrome !== 'undefined' && + !!(chrome.runtime && chrome.runtime.id); + } + + /** + * 向Chrome扩展发送消息 + */ + public sendMessageToExtension(message: any): void { + if (this.isChromeExtensionEnvironment()) { + try { + chrome.runtime.sendMessage(message); + } catch (error) { + console.error("发送消息到Chrome扩展失败:", error); + } + } + } + + /** + * 通知Chrome扩展页面已准备就绪 + */ + public notifyPageReady(): void { + this.sendMessageToExtension({ + type: 'PAGE_READY', + timestamp: Date.now() + }); + } + + /** + * 通知Chrome扩展当前JSON数据状态 + */ + public notifyJsonDataStatus(tabKey: string, hasValidJson: boolean): void { + this.sendMessageToExtension({ + type: 'JSON_DATA_STATUS', + payload: { + tabKey, + hasValidJson, + timestamp: Date.now() + } + }); + } +} + +export default ChromeExtensionListener; +export { ChromeExtensionListener }; \ No newline at end of file diff --git a/src/store/useTabStore.ts b/src/store/useTabStore.ts index 0be7d24..4f83989 100644 --- a/src/store/useTabStore.ts +++ b/src/store/useTabStore.ts @@ -1,14 +1,14 @@ // useTabStore.ts import { create } from "zustand"; import { devtools, subscribeWithSelector } from "zustand/middleware"; -import { Content, Mode, JSONContent, TextContent } from "vanilla-jsoneditor-cn"; +import { Content, JSONContent, Mode, TextContent } from "vanilla-jsoneditor"; import { useSettingsStore } from "./useSettingsStore"; import { useHistoryStore } from "./useHistoryStore"; import { StorageManager } from "@/lib/storage/StorageManager"; import { getSyncManager } from "@/lib/storage/MultiWindowSyncManager"; -import { parseJson, stringifyJson } from "@/utils/json"; +import { stringifyJson } from "@/utils/json"; import { generateUUID } from "@/utils/uuid"; // 存储管理器实例 @@ -518,7 +518,7 @@ export const useTabStore = create()( try { // 尝试解析 JSON - const parsedJson = parseJson(activeTab.content); + const parsedJson = JSON.parse(activeTab.content); activeTab.vanilla = { json: parsedJson }; activeTab.vanillaMode = Mode.tree; diff --git a/src/types/history.ts b/src/types/history.ts index 5647262..1c525d5 100644 --- a/src/types/history.ts +++ b/src/types/history.ts @@ -2,7 +2,7 @@ * 历史记录相关类型定义 */ -import { Content } from "vanilla-jsoneditor-cn"; +import { Content } from "vanilla-jsoneditor"; import { TabItem } from "@/store/useTabStore"; diff --git a/vite.config.ts b/vite.config.ts index fbb550b..4e3c2df 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -31,118 +31,118 @@ export default defineConfig({ languageWorkers: ["editorWorkerService", "json", "typescript"], }), nodePolyfills(), - VitePWA({ - registerType: "prompt", // 改为 prompt,让我们自己控制更新 - strategies: "generateSW", - includeAssets: ["favicon.ico", "apple-touch-icon.png", "logo.png"], - workbox: { - maximumFileSizeToCacheInBytes: 20 * 1024 * 1024, // 20MB - globPatterns: ["**/*.{js,css,html,ico,png,svg}"], - cleanupOutdatedCaches: true, // 清理过期的缓存 - skipWaiting: false, // 不自动跳过等待 - clientsClaim: false, // 不自动控制所有客户端 - runtimeCaching: [ - { - urlPattern: /^https:\/\/fonts\.googleapis\.com\/.*/i, - handler: "CacheFirst", - options: { - cacheName: "google-fonts-cache", - expiration: { - maxEntries: 10, - maxAgeSeconds: 60 * 60 * 24 * 365, // 1 year - }, - }, - }, - { - urlPattern: /\.(?:png|jpg|jpeg|svg|gif|webp)$/, - handler: "CacheFirst", - options: { - cacheName: "images-cache", - expiration: { - maxEntries: 100, - maxAgeSeconds: 60 * 60 * 24 * 30, // 30 days - }, - }, - }, - { - urlPattern: /\.(?:js|css)$/, - handler: "StaleWhileRevalidate", - options: { - cacheName: "static-resources", - expiration: { - maxEntries: 50, - maxAgeSeconds: 60 * 60 * 24 * 7, // 7 days - }, - }, - }, - ], - }, - devOptions: { - enabled: true, // 在开发环境中启用 PWA - type: "module", - }, - manifest: { - name: "JSON Tools - 多功能JSON处理助手", - short_name: "JSON Tools", - description: "强大的JSON工具集,支持格式化、验证、转换、编辑等多种功能", - background_color: "#ffffff", - display: "standalone", - orientation: "portrait", - scope: "/", - start_url: "/", - theme_color: "#f5f5f5", - categories: ["productivity", "developer", "utilities"], - handle_links: "auto", - icons: [ - { - src: "pwa-64x64.png", - sizes: "64x64", - type: "image/png", - purpose: "any", - }, - { - src: "pwa-192x192.png", - sizes: "192x192", - type: "image/png", - purpose: "any", - }, - { - src: "pwa-512x512.png", - sizes: "512x512", - type: "image/png", - purpose: "any", - }, - { - src: "maskable-icon-192x192.png", - sizes: "192x192", - type: "image/png", - purpose: "maskable", - }, - { - src: "maskable-icon-512x512.png", - sizes: "512x512", - type: "image/png", - purpose: "maskable", - }, - ], - shortcuts: [ - { - name: "格式化 JSON", - short_name: "格式化", - description: "快速格式化 JSON 数据", - url: "/?tool=format", - icons: [ - { - src: "pwa-96x96.png", - sizes: "96x96", - }, - ], - }, - ], - }, - }), + // VitePWA({ + // registerType: "prompt", // 改为 prompt,让我们自己控制更新 + // strategies: "generateSW", + // includeAssets: ["favicon.ico", "apple-touch-icon.png", "logo.png"], + // workbox: { + // maximumFileSizeToCacheInBytes: 20 * 1024 * 1024, // 20MB + // globPatterns: ["**/*.{js,css,html,ico,png,svg}"], + // cleanupOutdatedCaches: true, // 清理过期的缓存 + // skipWaiting: false, // 不自动跳过等待 + // clientsClaim: false, // 不自动控制所有客户端 + // runtimeCaching: [ + // { + // urlPattern: /^https:\/\/fonts\.googleapis\.com\/.*/i, + // handler: "CacheFirst", + // options: { + // cacheName: "google-fonts-cache", + // expiration: { + // maxEntries: 10, + // maxAgeSeconds: 60 * 60 * 24 * 365, // 1 year + // }, + // }, + // }, + // { + // urlPattern: /\.(?:png|jpg|jpeg|svg|gif|webp)$/, + // handler: "CacheFirst", + // options: { + // cacheName: "images-cache", + // expiration: { + // maxEntries: 100, + // maxAgeSeconds: 60 * 60 * 24 * 30, // 30 days + // }, + // }, + // }, + // { + // urlPattern: /\.(?:js|css)$/, + // handler: "StaleWhileRevalidate", + // options: { + // cacheName: "static-resources", + // expiration: { + // maxEntries: 50, + // maxAgeSeconds: 60 * 60 * 24 * 7, // 7 days + // }, + // }, + // }, + // ], + // }, + // devOptions: { + // enabled: true, // 在开发环境中启用 PWA + // type: "module", + // }, + // manifest: { + // name: "JSON Tools - 多功能JSON处理助手", + // short_name: "JSON Tools", + // description: "强大的JSON工具集,支持格式化、验证、转换、编辑等多种功能", + // background_color: "#ffffff", + // display: "standalone", + // orientation: "portrait", + // scope: "/", + // start_url: "/", + // theme_color: "#f5f5f5", + // categories: ["productivity", "developer", "utilities"], + // handle_links: "auto", + // icons: [ + // { + // src: "pwa-64x64.png", + // sizes: "64x64", + // type: "image/png", + // purpose: "any", + // }, + // { + // src: "pwa-192x192.png", + // sizes: "192x192", + // type: "image/png", + // purpose: "any", + // }, + // { + // src: "pwa-512x512.png", + // sizes: "512x512", + // type: "image/png", + // purpose: "any", + // }, + // { + // src: "maskable-icon-192x192.png", + // sizes: "192x192", + // type: "image/png", + // purpose: "maskable", + // }, + // { + // src: "maskable-icon-512x512.png", + // sizes: "512x512", + // type: "image/png", + // purpose: "maskable", + // }, + // ], + // shortcuts: [ + // { + // name: "格式化 JSON", + // short_name: "格式化", + // description: "快速格式化 JSON 数据", + // url: "/?tool=format", + // icons: [ + // { + // src: "pwa-96x96.png", + // sizes: "96x96", + // }, + // ], + // }, + // ], + // }, + // }), ], optimizeDeps: { - include: ["vanilla-jsoneditor-cn"], + include: ["vanilla-jsoneditor"], }, });