diff --git a/.eslintignore b/.eslintignore index 842c4df1..5d6ea794 100644 --- a/.eslintignore +++ b/.eslintignore @@ -5,6 +5,7 @@ demo/ .ice/ scripts/ locale/ +dist/ # node 覆盖率文件 coverage/ diff --git a/.eslintrc.js b/.eslintrc.js index 9e97dc7f..73da16dc 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -31,12 +31,12 @@ module.exports = getESLintConfig('react-ts', { { 'pattern': '@/**', 'group': 'parent', - 'position': 'before' - } + 'position': 'before', + }, ], 'pathGroupsExcludedImportTypes': ['builtin'], - 'newlines-between': 'never' - } - ] + 'newlines-between': 'never', + }, + ], }, }); diff --git a/DEBUG_GUIDE.md b/DEBUG_GUIDE.md new file mode 100644 index 00000000..881f65c0 --- /dev/null +++ b/DEBUG_GUIDE.md @@ -0,0 +1,166 @@ +# Header Editor V3 调试指南 + +## 问题:请求网址时没有添加 x-tag header + +### 🔍 调试步骤 + +#### 1. 检查规则是否已创建 +首先确认您是否已经正确创建了添加 x-tag header 的规则: + +**在 Header Editor 选项页面:** +1. 打开 Chrome 扩展管理页面 (`chrome://extensions/`) +2. 找到 Header Editor,点击"详细信息" +3. 点击"扩展程序选项" +4. 检查是否有类似以下的规则: + - 规则类型:修改发送头 + - 头名称:x-tag (或 X-Tag) + - 头内容:您想要的值 + - 匹配类型:全部(或特定模式) + - 启用状态:✅ 已启用 + +#### 2. 使用调试工具检查规则状态 +打开浏览器开发者工具 (F12),在 Console 中执行: + +```javascript +// 检查 x-tag 规则的完整状态 +testXTagHeaderRule() +``` + +这个命令将: +- 检查当前的规则 +- 如果没有 x-tag 规则,自动创建一个测试规则 +- 验证规则转换是否正确 +- 检查 V3 规则是否正确应用 + +#### 3. 检查扩展是否被禁用 +```javascript +// 检查扩展是否被禁用 +chrome.runtime.sendMessage({method: 'getRuleStats'}) +``` + +#### 4. 启用调试日志 +```javascript +// 启用详细的调试日志 +chrome.runtime.sendMessage({method: 'enableDebugLogging'}) +``` + +启用后,刷新页面并查看控制台输出,查找相关的规则应用信息。 + +#### 5. 验证规则是否生效 +访问 https://httpbin.org/headers 来检查请求头: + +1. 打开 https://httpbin.org/headers +2. 查看返回的 JSON 中的 `headers` 字段 +3. 检查是否包含您的 x-tag header + +**或者在开发者工具中检查:** +1. 打开开发者工具 (F12) +2. 切换到 Network 标签 +3. 刷新页面 +4. 点击任意请求 +5. 在 Headers 标签中查看 Request Headers +6. 查找您的 x-tag header + +### 🚨 常见问题和解决方案 + +#### 问题 1:规则存在但没有生效 +**可能原因:** +- 规则转换失败 +- V3 规则没有正确应用 +- 匹配条件不正确 + +**解决方案:** +```javascript +// 强制刷新规则 +chrome.runtime.sendMessage({method: 'testRuleApplication'}) +``` + +#### 问题 2:规则转换失败 +**可能原因:** +- 规则格式不正确 +- 包含不支持的功能 + +**解决方案:** +确保规则满足以下条件: +- 规则类型为 `modifySendHeader` +- 不使用自定义函数 (`isFunction: false`) +- Header 名称和值都不为空 + +#### 问题 3:V3 规则限制 +**可能原因:** +- Chrome 的 declarativeNetRequest 有一些限制 + +**解决方案:** +- 确保 header 名称符合 HTTP 规范 +- 避免使用特殊字符 +- 检查是否超过规则数量限制 + +### 🔧 手动创建测试规则 + +如果自动创建不工作,可以手动创建: + +1. 打开 Header Editor 选项页面 +2. 点击"添加规则" +3. 填写以下信息: + - 名称:`测试 X-Tag Header` + - 规则类型:`修改发送头` + - 匹配类型:`全部` + - 执行类型:`普通` + - 头名称:`X-Tag` + - 头内容:`test-value` +4. 点击保存 + +### 📊 检查规则统计 + +```javascript +// 获取当前规则统计 +chrome.runtime.sendMessage({method: 'getRuleStats'}).then(response => { + console.log('规则统计:', response.stats); +}); + +// 获取当前应用的 V3 规则 +chrome.declarativeNetRequest.getDynamicRules().then(rules => { + console.log('当前 V3 规则:', rules); + const headerRules = rules.filter(rule => + rule.action.type === 'modifyHeaders' && + rule.action.requestHeaders + ); + console.log('修改请求头的规则:', headerRules); +}); +``` + +### 🔍 深度调试 + +如果上述步骤都没有解决问题,请: + +1. **收集调试信息:** + ```javascript + // 收集完整的调试信息 + const debugInfo = { + rules: await chrome.runtime.sendMessage({method: 'getRuleStats'}), + v3Rules: await chrome.declarativeNetRequest.getDynamicRules(), + permissions: await chrome.permissions.getAll() + }; + console.log('调试信息:', debugInfo); + ``` + +2. **检查权限:** + 确保扩展有以下权限: + - `declarativeNetRequest` + - `declarativeNetRequestWithHostAccess` + - 相关的主机权限 + +3. **检查浏览器版本:** + 确保使用 Chrome 88+ 或其他支持 Manifest V3 的浏览器 + +### 📝 报告问题 + +如果问题仍然存在,请提供以下信息: + +1. 浏览器版本 +2. Header Editor 版本 +3. 创建的规则详情 +4. 调试输出结果 +5. 期望的行为 vs 实际行为 + +这将帮助我们更好地诊断和解决问题。 \ No newline at end of file diff --git a/MIGRATION_REPORT.md b/MIGRATION_REPORT.md new file mode 100644 index 00000000..d6d2fe6c --- /dev/null +++ b/MIGRATION_REPORT.md @@ -0,0 +1,151 @@ +# Header Editor Manifest V3 迁移报告 + +## 项目概述 +Header Editor 是一个浏览器扩展,允许用户修改 HTTP 请求和响应头。本项目已完成从 Manifest V2 到 Manifest V3 的迁移。 + +## 最新修复 (2024-12-19) + +### 问题诊断 +经过代码审查发现以下关键问题: +1. **规则更新事件监听缺失** - 规则变化时没有自动重新应用 V3 规则 +2. **规则转换逻辑不完善** - V3RuleConverter 存在类型错误和转换不准确 +3. **初始化时序问题** - 数据库未完全准备好就开始应用规则 +4. **调试信息不足** - 缺少详细的调试日志来排查问题 + +### 修复内容 + +#### 1. 事件监听系统 (src/pages/background/index.ts) +- ✅ 添加规则更新事件监听 (`EVENTs.RULE_UPDATE`, `EVENTs.RULE_DELETE`) +- ✅ 添加偏好设置变化监听 (`disable-all` 设置) +- ✅ 改进初始化流程,等待数据库和规则缓存完全准备 +- ✅ 添加自动规则刷新机制 +- ✅ 添加测试接口和调试命令 + +#### 2. 规则转换器优化 (src/pages/background/v3-rule-converter.ts) +- ✅ 修复类型定义问题,使用正确的 declarativeNetRequest 类型 +- ✅ 改进规则验证逻辑,确保生成的 V3 规则有效 +- ✅ 优化资源类型设置,移除不支持的类型 +- ✅ 添加批量规则应用,避免一次性应用过多规则 +- ✅ 改进错误处理和日志记录 +- ✅ 添加规则转换详细日志记录 + +#### 3. API 处理器增强 (src/pages/background/api-handler.ts) +- ✅ 在所有规则操作后自动触发 V3 规则刷新 +- ✅ 添加详细的操作日志记录 +- ✅ 改进错误处理和状态反馈 +- ✅ 添加规则统计信息接口 + +#### 4. 日志系统升级 (src/share/core/logger.ts) +- ✅ 重构日志系统,支持不同日志级别 +- ✅ 添加专用的规则转换日志方法 +- ✅ 添加性能统计日志 +- ✅ 添加扩展状态日志 +- ✅ 改进日志格式和上下文信息 + +### 核心改进 + +#### 自动规则同步 +现在规则的任何变化都会自动触发 V3 规则的重新应用: +- 创建新规则 → 自动应用到 declarativeNetRequest +- 修改现有规则 → 自动更新 declarativeNetRequest +- 删除规则 → 自动从 declarativeNetRequest 移除 +- 偏好设置变化 → 自动调整规则应用状态 + +#### 规则转换改进 +- 更准确的规则类型识别和转换 +- 更严格的规则验证 +- 更好的错误处理和回退机制 +- 更详细的转换统计和日志 + +#### 调试和测试功能 +- 可通过 console 调用 `testRuleApplication()` 进行测试 +- 支持动态启用/禁用调试日志 +- 提供详细的规则转换和应用统计 +- 支持获取当前规则状态和转换结果 + +## 主要变化说明 + +### 1. 网络请求处理 +- **V2**: 使用 `chrome.webRequest` API 进行实时拦截和修改 +- **V3**: 使用 `chrome.declarativeNetRequest` API 进行声明式规则处理 + +### 2. 背景脚本 +- **V2**: 持久化背景页面 (`background.html`) +- **V3**: 服务工作者 (`background.js`) + +### 3. 权限系统 +- **V2**: `webRequestBlocking` 权限 +- **V3**: `declarativeNetRequest` 和 `declarativeNetRequestWithHostAccess` 权限 + +### 4. 规则应用方式 +- **V2**: 运行时动态处理每个请求 +- **V3**: 预配置规则集,由浏览器引擎处理 + +## 功能限制 + +### 不支持的功能 +1. **自定义 JavaScript 函数规则** - V3 不允许执行任意代码 +2. **响应体修改** - declarativeNetRequest 不支持响应体修改 +3. **复杂正则表达式** - V3 API 对正则表达式有限制 +4. **动态 IP 获取** - 无法在 V3 中获取客户端 IP + +### 功能替代方案 +- 简单的头部修改 → 使用 `modifyHeaders` 动作 +- URL 重定向 → 使用 `redirect` 动作 +- 请求阻止 → 使用 `block` 动作 +- 域名匹配 → 使用 `domains` 条件 + +## 测试验证 + +### 测试方法 +1. 打开浏览器开发者工具 +2. 切换到 Console 标签 +3. 执行 `testRuleApplication()` 进行功能测试 +4. 检查规则转换统计和应用结果 + +### 测试内容 +- 规则转换正确性 +- 规则应用成功率 +- 事件监听响应 +- 错误处理能力 +- 性能表现 + +## 部署建议 + +### 开发环境 +- 启用调试日志: `chrome.runtime.sendMessage({method: 'enableDebugLogging'})` +- 运行测试: `testRuleApplication()` +- 检查规则统计: `chrome.runtime.sendMessage({method: 'getRuleStats'})` + +### 生产环境 +- 默认使用 INFO 级别日志 +- 定期检查规则应用状态 +- 监控转换失败的规则 + +## 后续工作建议 + +1. **用户体验优化** + - 添加规则转换失败的用户提示 + - 提供规则迁移建议 + - 改进错误消息的用户友好性 + +2. **功能增强** + - 支持更多的 URL 匹配模式 + - 优化规则优先级管理 + - 添加规则冲突检测 + +3. **性能优化** + - 优化大量规则的处理性能 + - 减少规则应用的延迟 + - 改进内存使用 + +4. **稳定性改进** + - 增强错误恢复机制 + - 添加规则备份和恢复功能 + - 提高扩展的崩溃恢复能力 + +## 结论 + +本次修复解决了 Header Editor 在 Manifest V3 环境下的核心问题,确保了规则的正确转换和应用。通过完善的事件监听、详细的日志记录和自动化测试,显著提升了扩展的稳定性和可维护性。 + +虽然 V3 的限制导致部分高级功能无法使用,但对于大多数用户的基本需求(请求头修改、URL 重定向、请求阻止等),扩展已能够正常工作。 \ No newline at end of file diff --git a/package.json b/package.json index b2eefc74..59c8161b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "header-editor", - "version": "5.0.0", + "version": "6.0.0", "description": "Header Editor", "author": "ShuangYa", "license": "GPL-2.0", diff --git a/scripts/webpack/dev.js b/scripts/webpack/dev.js index 830d784f..b932ba35 100644 --- a/scripts/webpack/dev.js +++ b/scripts/webpack/dev.js @@ -4,7 +4,7 @@ module.exports = function (config, context) { // 调试模式下,开启自动重载和自动编译 if (config.get('mode') === 'development') { // config.plugin('reload').use(ChromeExtensionReloader); - config.devServer.hot(false); + config.devServer.hot(true); config.devServer.open(false); const devMiddleware = config.devServer.store.get('devMiddleware'); config.devServer.store.set('devMiddleware', { @@ -13,10 +13,10 @@ module.exports = function (config, context) { }); } - config.plugin('bundle-analyzer').use(new BundleAnalyzerPlugin({ - analyzerMode: 'static', - reportFilename: '../temp/bundle-analyze.html', - })) + // config.plugin('bundle-analyzer').use(new BundleAnalyzerPlugin({ + // analyzerMode: 'static', + // reportFilename: '../temp/bundle-analyze.html', + // })) return config; }; diff --git a/scripts/webpack/remove-html.js b/scripts/webpack/remove-html.js index cf61414c..3d061798 100644 --- a/scripts/webpack/remove-html.js +++ b/scripts/webpack/remove-html.js @@ -6,7 +6,7 @@ module.exports = function (config, context) { continue; } const pageName = item.name.substr(18); - if (pageName === 'background' || pageName.indexOf('inject-') === 0 || pageName.indexOf('worker-') === 0) { + if (pageName === 'content' || pageName === 'background' || pageName.indexOf('inject-') === 0 || pageName.indexOf('worker-') === 0) { config.plugins.delete(item.name); console.log('Remove html entry: ' + item.name); } diff --git a/src/enterprise/policy-handler.ts b/src/enterprise/policy-handler.ts new file mode 100644 index 00000000..d0bddded --- /dev/null +++ b/src/enterprise/policy-handler.ts @@ -0,0 +1,371 @@ +import browser from 'webextension-polyfill'; +import logger from '@/share/core/logger'; +import { prefs } from '@/share/core/prefs'; + +/** + * 企业策略支持处理器 + * 在企业环境中提供完整的功能支持 + */ +export class EnterpriseSupport { + private static instance: EnterpriseSupport; + private isEnterpriseEnvironment = false; + private managementInfo: any = null; + private policyInfo: any = null; + + constructor() { + this.detectEnterpriseEnvironment(); + } + + static getInstance(): EnterpriseSupport { + if (!EnterpriseSupport.instance) { + EnterpriseSupport.instance = new EnterpriseSupport(); + } + return EnterpriseSupport.instance; + } + + /** + * 检测是否在企业环境中 + */ + private async detectEnterpriseEnvironment(): Promise { + try { + // 检查扩展是否由企业策略安装 + const managementInfo = await browser.management.getSelf(); + this.managementInfo = managementInfo; + + // 检查安装类型 + const isEnterpriseInstalled = managementInfo.installType === 'admin' || + managementInfo.installType === 'development'; + + // 检查是否有企业策略 + const hasPolicySupport = await this.checkPolicySupport(); + + this.isEnterpriseEnvironment = isEnterpriseInstalled || hasPolicySupport; + + if (this.isEnterpriseEnvironment) { + logger.info('检测到企业环境,启用完整功能支持'); + await this.enableEnterpriseFeatures(); + } + } catch (error) { + logger.error('检测企业环境时发生错误:', error); + this.isEnterpriseEnvironment = false; + } + } + + /** + * 检查企业策略支持 + */ + private async checkPolicySupport(): Promise { + try { + // 检查是否有企业策略 API + if (!browser.enterprise || !browser.enterprise.platformKeys) { + return false; + } + + // 尝试检查企业策略设置 + // 这里可以添加更多的企业策略检测逻辑 + return true; + } catch (error) { + logger.debug('企业策略检测失败:', error); + return false; + } + } + + /** + * 启用企业功能 + */ + private async enableEnterpriseFeatures(): Promise { + try { + // 记录企业环境信息 + await prefs.set('enterprise_mode', { + enabled: true, + installType: this.managementInfo?.installType, + timestamp: Date.now(), + }); + + // 启用完整的 webRequest 功能 + await this.enableFullWebRequestSupport(); + + // 设置企业特定的配置 + await this.applyEnterpriseConfiguration(); + + logger.info('企业功能已启用'); + } catch (error) { + logger.error('启用企业功能时发生错误:', error); + } + } + + /** + * 启用完整的 webRequest 支持 + */ + private async enableFullWebRequestSupport(): Promise { + try { + // 在企业环境中,可以使用完整的 webRequest API + // 这里设置相关的配置标志 + await prefs.set('webRequest_enterprise_enabled', true); + + // 通知其他组件使用完整功能 + logger.info('企业环境中启用完整 webRequest 支持'); + } catch (error) { + logger.error('启用 webRequest 支持时发生错误:', error); + } + } + + /** + * 应用企业配置 + */ + private async applyEnterpriseConfiguration(): Promise { + try { + // 企业特定的配置 + const enterpriseConfig = { + // 允许更多的规则数量 + maxRules: 100000, + // 允许复杂的正则表达式 + allowComplexRegex: true, + // 允许用户自定义函数 + allowCustomFunctions: true, + // 允许响应体修改 + allowResponseBodyModification: true, + // 禁用一些限制 + disableV3Restrictions: true, + }; + + await prefs.set('enterprise_config', enterpriseConfig); + logger.info('企业配置已应用:', enterpriseConfig); + } catch (error) { + logger.error('应用企业配置时发生错误:', error); + } + } + + /** + * 检查当前是否为企业环境 + */ + isEnterpriseMode(): boolean { + return this.isEnterpriseEnvironment; + } + + /** + * 获取企业信息 + */ + getEnterpriseInfo(): { + isEnterprise: boolean; + installType?: string; + hasFullSupport: boolean; + supportedFeatures: string[]; + } { + return { + isEnterprise: this.isEnterpriseEnvironment, + installType: this.managementInfo?.installType, + hasFullSupport: this.isEnterpriseEnvironment, + supportedFeatures: this.isEnterpriseEnvironment ? [ + 'webRequest', + 'customFunctions', + 'responseBodyModification', + 'unlimitedRules', + 'complexRegex', + ] : [], + }; + } + + /** + * 请求企业策略权限 + */ + async requestEnterprisePermissions(): Promise { + try { + if (!this.isEnterpriseEnvironment) { + logger.warn('非企业环境,无法请求企业权限'); + return false; + } + + // 请求额外的权限 + const granted = await browser.permissions.request({ + permissions: ['webRequest', 'webRequestBlocking', 'management'], + origins: ['*://*/*'], + }); + + if (granted) { + logger.info('企业权限已授予'); + await this.enableEnterpriseFeatures(); + } + + return granted; + } catch (error) { + logger.error('请求企业权限时发生错误:', error); + return false; + } + } + + /** + * 获取企业策略配置 + */ + async getEnterpriseConfig(): Promise { + try { + return await prefs.get('enterprise_config') || {}; + } catch (error) { + logger.error('获取企业配置时发生错误:', error); + return {}; + } + } + + /** + * 设置企业策略配置 + */ + async setEnterpriseConfig(config: any): Promise { + try { + if (!this.isEnterpriseEnvironment) { + throw new Error('非企业环境,无法设置企业配置'); + } + + await prefs.set('enterprise_config', config); + logger.info('企业配置已更新:', config); + } catch (error) { + logger.error('设置企业配置时发生错误:', error); + throw error; + } + } + + /** + * 生成企业部署指南 + */ + generateDeploymentGuide(): { + policyTemplate: any; + installationSteps: string[]; + configurationOptions: any; + } { + return { + policyTemplate: { + '3rdparty': { + extensions: { + 'headereditor@addon.firefoxcn.net': { + enterprise_mode: true, + max_rules: 100000, + allow_custom_functions: true, + allow_response_body_modification: true, + disable_v3_restrictions: true, + }, + }, + }, + }, + installationSteps: [ + '1. 下载企业版 Header Editor 扩展包', + '2. 创建企业策略配置文件', + '3. 通过 Group Policy 或 MDM 部署策略', + '4. 在目标机器上安装扩展', + '5. 验证企业功能是否正常工作', + ], + configurationOptions: { + maxRules: { + description: '最大规则数量', + type: 'number', + default: 100000, + min: 1000, + max: 1000000, + }, + allowCustomFunctions: { + description: '允许自定义函数', + type: 'boolean', + default: true, + }, + allowResponseBodyModification: { + description: '允许响应体修改', + type: 'boolean', + default: true, + }, + disableV3Restrictions: { + description: '禁用 V3 限制', + type: 'boolean', + default: true, + }, + }, + }; + } + + /** + * 验证企业功能 + */ + async validateEnterpriseFeatures(): Promise<{ + isValid: boolean; + availableFeatures: string[]; + missingFeatures: string[]; + recommendations: string[]; + }> { + try { + const availableFeatures: string[] = []; + const missingFeatures: string[] = []; + const recommendations: string[] = []; + + // 检查 webRequest 权限 + const permissions = await browser.permissions.getAll(); + if (permissions.permissions?.includes('webRequest')) { + availableFeatures.push('webRequest'); + } else { + missingFeatures.push('webRequest'); + recommendations.push('需要通过企业策略授予 webRequest 权限'); + } + + // 检查管理权限 + if (permissions.permissions?.includes('management')) { + availableFeatures.push('management'); + } else { + missingFeatures.push('management'); + } + + // 检查企业配置 + const enterpriseConfig = await this.getEnterpriseConfig(); + if (Object.keys(enterpriseConfig).length > 0) { + availableFeatures.push('enterpriseConfig'); + } else { + missingFeatures.push('enterpriseConfig'); + recommendations.push('需要设置企业配置'); + } + + return { + isValid: missingFeatures.length === 0, + availableFeatures, + missingFeatures, + recommendations, + }; + } catch (error) { + logger.error('验证企业功能时发生错误:', error); + return { + isValid: false, + availableFeatures: [], + missingFeatures: ['unknown'], + recommendations: ['验证过程中发生错误,请检查日志'], + }; + } + } + + /** + * 获取企业支持状态 + */ + async getStatus(): Promise<{ + isSupported: boolean; + installType: string; + features: any; + config: any; + validation: any; + }> { + try { + const validation = await this.validateEnterpriseFeatures(); + + return { + isSupported: this.isEnterpriseEnvironment, + installType: this.managementInfo?.installType || 'unknown', + features: this.getEnterpriseInfo(), + config: await this.getEnterpriseConfig(), + validation, + }; + } catch (error) { + logger.error('获取企业支持状态时发生错误:', error); + return { + isSupported: false, + installType: 'unknown', + features: { isEnterprise: false, hasFullSupport: false, supportedFeatures: [] }, + config: {}, + validation: { isValid: false, availableFeatures: [], missingFeatures: [], recommendations: [] }, + }; + } + } +} + +export default EnterpriseSupport; diff --git a/src/manifest.json b/src/manifest.json index da03a902..5ce244f0 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -4,25 +4,45 @@ "version": null, "description": "__MSG_description__", "homepage_url": "https://he.firefoxcn.net", - "manifest_version": 2, + "manifest_version": 3, "icons": { "128": "assets/images/128.png" }, "permissions": [ "tabs", - "webRequest", - "webRequestBlocking", "storage", - "*://*/*", - "unlimitedStorage" + "notifications", + "declarativeNetRequest", + "declarativeNetRequestFeedback", + "declarativeNetRequestWithHostAccess" ], - "content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self';", + "host_permissions": [ + "*://*/*" + ], + "declarative_net_request": { + "rule_resources": [] + }, + "content_security_policy": { + "extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self';" + }, "background": { - "scripts": [ - "assets/js/background.js" - ] + "service_worker": "assets/js/background.js" }, - "browser_action": { + "content_scripts": [ + { + "matches": [""], + "css": [ + "assets/css/content.css" + ], + "js": [ + "external/react.min.js", + "external/react-dom.min.js", + "assets/js/content.js" + ], + "run_at": "document_end" + } + ], + "action": { "default_icon": { "128": "assets/images/128.png" }, @@ -33,18 +53,5 @@ "options_ui": { "page": "options.html", "open_in_tab": true - }, - "__amo__browser_specific_settings": { - "gecko": { - "id": "headereditor-amo@addon.firefoxcn.net", - "strict_min_version": "77.0" - } - }, - "__xpi__browser_specific_settings": { - "gecko": { - "id": "headereditor@addon.firefoxcn.net", - "strict_min_version": "77.0", - "update_url": "https://ext.firefoxcn.net/header-editor/install/update.json" - } - } + } } \ No newline at end of file diff --git a/src/pages/background/api-handler.ts b/src/pages/background/api-handler.ts index cd488391..d5ae09f2 100644 --- a/src/pages/background/api-handler.ts +++ b/src/pages/background/api-handler.ts @@ -1,58 +1,234 @@ import browser from 'webextension-polyfill'; import logger from '@/share/core/logger'; -import { APIs, TABLE_NAMES_ARR } from '@/share/core/constant'; +import { APIs, TABLE_NAMES_ARR, TABLE_NAMES } from '@/share/core/constant'; import { prefs } from '@/share/core/prefs'; import rules from './core/rules'; import { openURL } from './utils'; import { getDatabase } from './core/db'; +// 获取全局规则处理器 +function getRuleHandler() { + return (globalThis as any).headerEditorRuleHandler; +} + function execute(request: any) { + logger.debug('执行 API 请求:', request); + if (request.method === 'notifyBackground') { request.method = request.reason; delete request.reason; } + switch (request.method) { case APIs.HEALTH_CHECK: return new Promise((resolve) => { getDatabase() - .then(() => resolve(true)) - .catch(() => resolve(false)); + .then(() => { + logger.debug('健康检查通过'); + resolve(true); + }) + .catch((error) => { + logger.error('健康检查失败:', error); + resolve(false); + }); }); + case APIs.OPEN_URL: + logger.debug('打开URL:', request.url); return openURL(request); - case APIs.GET_RULES: - return Promise.resolve(rules.get(request.type, request.options)); + + case APIs.GET_RULES: { + logger.debug('获取规则:', { type: request.type, options: request.options }); + const rulesResult = rules.get(request.type, request.options); + logger.debug('规则查询结果:', rulesResult?.length || 0, '条规则'); + return Promise.resolve(rulesResult); + } + case APIs.SAVE_RULE: - return rules.save(request.rule); + logger.debug('保存规则:', request.rule); + + // 通知活动标签页 + browser.tabs.query({ active: true, currentWindow: true }).then((tabs) => { + tabs.forEach((tab) => { + if (tab.id) { + logger.debug('向 content-script 发送保存规则消息', { tabId: tab.id, rule: request.rule }); + browser.tabs.sendMessage(tab.id, { method: APIs.SAVE_RULE, rule: request.rule }); + } + }); + }); + + // 保存规则并刷新 V3 规则 + return rules.save(request.rule).then((result) => { + logger.info('规则保存成功:', result); + + // 触发 V3 规则刷新 + const ruleHandler = getRuleHandler(); + if (ruleHandler) { + logger.debug('触发 V3 规则刷新(保存规则后)'); + ruleHandler.refresh().catch((error: any) => { + logger.error('V3 规则刷新失败:', error); + }); + } else { + logger.warn('规则处理器未找到,无法刷新 V3 规则'); + } + + return result; + }).catch((error) => { + logger.error('保存规则失败:', error); + throw error; + }); + case APIs.DELETE_RULE: - return rules.remove(request.type, request.id); + logger.debug('删除规则:', { type: request.type, id: request.id }); + + return rules.remove(request.type, request.id).then((result) => { + logger.info('规则删除成功:', { type: request.type, id: request.id }); + + // 触发 V3 规则刷新 + const ruleHandler = getRuleHandler(); + if (ruleHandler) { + logger.debug('触发 V3 规则刷新(删除规则后)'); + ruleHandler.refresh().catch((error: any) => { + logger.error('V3 规则刷新失败:', error); + }); + } else { + logger.warn('规则处理器未找到,无法刷新 V3 规则'); + } + + return result; + }).catch((error) => { + logger.error('删除规则失败:', error); + throw error; + }); + case APIs.SET_PREFS: - return prefs.set(request.key, request.value); + logger.debug('设置偏好:', { key: request.key, value: request.value }); + + // 通知活动标签页 + browser.tabs.query({ active: true, currentWindow: true }).then((tabs) => { + tabs.forEach((tab) => { + if (tab.id) { + logger.debug('向 content-script 发送偏好设置消息', { tabId: tab.id, key: request.key, value: request.value }); + browser.tabs.sendMessage(tab.id, { method: APIs.SET_PREFS, key: request.key, value: request.value }); + } + }); + }); + + return prefs.set(request.key, request.value).then((result) => { + logger.info('偏好设置成功:', { key: request.key, value: request.value }); + + // 如果是禁用/启用扩展,触发 V3 规则刷新 + if (request.key === 'disable-all') { + const ruleHandler = getRuleHandler(); + if (ruleHandler) { + logger.debug('触发 V3 规则刷新(偏好设置变化)'); + ruleHandler.refresh().catch((error: any) => { + logger.error('V3 规则刷新失败:', error); + }); + } + } + + return result; + }).catch((error) => { + logger.error('设置偏好失败:', error); + throw error; + }); + case APIs.UPDATE_CACHE: + logger.debug('更新缓存:', request.type); + if (request.type === 'all') { - return Promise.all(TABLE_NAMES_ARR.map((tableName) => rules.updateCache(tableName))); + return Promise.all(TABLE_NAMES_ARR.map((tableName) => { + logger.debug('更新表缓存:', tableName); + return rules.updateCache(tableName); + })).then((results) => { + logger.info('所有表缓存更新完成'); + + // 触发 V3 规则刷新 + const ruleHandler = getRuleHandler(); + if (ruleHandler) { + logger.debug('触发 V3 规则刷新(缓存更新后)'); + ruleHandler.refresh().catch((error: any) => { + logger.error('V3 规则刷新失败:', error); + }); + } + + return results; + }); } else { - return rules.updateCache(request.type); + return rules.updateCache(request.type).then((result) => { + logger.info('表缓存更新完成:', request.type); + + // 触发 V3 规则刷新 + const ruleHandler = getRuleHandler(); + if (ruleHandler) { + logger.debug('触发 V3 规则刷新(缓存更新后)'); + ruleHandler.refresh().catch((error: any) => { + logger.error('V3 规则刷新失败:', error); + }); + } + + return result; + }); } + default: + logger.warn('未知的 API 方法:', request.method); break; } // return false; } export default function createApiHandler() { - browser.runtime.onMessage.addListener((request) => { - logger.debug('Background Receive Message', request); + logger.info('创建 API 处理器'); + + browser.runtime.onMessage.addListener((request: any, sender, sendResponse) => { + if (request.method === 'GetData') { + logger.debug('收到来自 content-script 的 GetData 请求', { request, sender }); + + const response = { + rules: rules.get(TABLE_NAMES.sendHeader), + enableRules: rules.get(TABLE_NAMES.sendHeader, { enable: true }), + enable: !prefs.get('disable-all'), + currentIPList: [], // V3 中无法获取IP信息 + }; + + logger.debug('返回 content-script 的数据', { + rulesCount: response.rules?.length || 0, + enableRulesCount: response.enableRules?.length || 0, + enable: response.enable, + }); + + sendResponse(response); + return; + } + + logger.debug('Background 收到消息', request); + if (request.method === 'batchExecute') { - const queue = request.batch.map((item) => { + logger.debug('执行批量操作:', request.batch?.length || 0, '个操作'); + + const queue = request.batch.map((item: any) => { const res = execute(item); if (res) { return res; } return Promise.resolve(); }); - return Promise.allSettled(queue); + + return Promise.allSettled(queue).then((results) => { + logger.debug('批量操作完成:', results.length, '个结果'); + return results; + }); + } + + const result = execute(request); + if (result && typeof result.then === 'function') { + result.catch((error) => { + logger.error('API 执行失败:', error); + }); } - return execute(request); + + return result; }); } diff --git a/src/pages/background/index.ts b/src/pages/background/index.ts index a89f4be4..93028244 100644 --- a/src/pages/background/index.ts +++ b/src/pages/background/index.ts @@ -1,11 +1,500 @@ +// Header Editor Manifest V3 背景脚本 +// 导入必要的模块 +import { prefs } from '@/share/core/prefs'; +import { TABLE_NAMES_ARR, TABLE_NAMES, EVENTs, RULE_TYPE, RULE_MATCH_TYPE } from '@/share/core/constant'; +import logger from '@/share/core/logger'; +import notify from '@/share/core/notify'; import createApiHandler from './api-handler'; -import createRequestHandler from './request-handler'; -import './upgrade'; +import { V3RuleConverter } from './v3-rule-converter'; +import rules from './core/rules'; -if (typeof window !== 'undefined') { - window.IS_BACKGROUND = true; +console.log('Header Editor Service Worker 启动'); + +// 设置全局标识 +if (typeof globalThis !== 'undefined') { + globalThis.IS_BACKGROUND = true; +} + +// 规则处理器 +class V3RuleHandler { + private conversionStats: any = null; + private isInitialized = false; + + async initialize(): Promise { + console.log('初始化 V3 规则处理器...'); + + // 等待偏好设置准备完成 + await new Promise((resolve) => { + prefs.ready(() => { + console.log('偏好设置已准备完成'); + resolve(); + }); + }); + + // 等待数据库和规则缓存完全准备好 + await this.waitForRulesReady(); + + // 创建API处理器 + createApiHandler(); + console.log('API处理器已创建'); + + // 设置规则变化监听 + this.setupRuleEventListeners(); + + // 加载并应用规则 + await this.loadAndApplyRules(); + + this.isInitialized = true; + console.log('V3 规则处理器初始化完成'); + } + + private async waitForRulesReady(): Promise { + console.log('等待规则缓存准备完成...'); + + // 等待所有表的缓存更新完成 + const maxRetries = 30; // 最多等待30秒 + let retries = 0; + + while (retries < maxRetries) { + let allReady = true; + + for (const tableName of TABLE_NAMES_ARR) { + const tableRules = rules.get(tableName); + if (tableRules === null) { + allReady = false; + break; + } + } + + if (allReady) { + console.log('所有规则缓存已准备完成'); + return; + } + + await new Promise((resolve) => setTimeout(resolve, 1000)); + retries++; + } + + logger.warn('等待规则缓存超时,但继续初始化'); + } + + private setupRuleEventListeners(): void { + console.log('设置规则变化事件监听...'); + + // 监听规则更新事件 + notify.event.on(EVENTs.RULE_UPDATE, (event: any) => { + console.log('收到规则更新事件:', event); + this.handleRuleChange(); + }); + + // 监听规则删除事件 + notify.event.on(EVENTs.RULE_DELETE, (event: any) => { + console.log('收到规则删除事件:', event); + this.handleRuleChange(); + }); + } + + private async handleRuleChange(): Promise { + if (!this.isInitialized) { + console.log('规则处理器尚未初始化,跳过规则变化处理'); + return; + } + + try { + console.log('处理规则变化,重新应用规则...'); + await this.loadAndApplyRules(); + console.log('规则变化处理完成'); + } catch (error) { + logger.error('处理规则变化时发生错误:', error); + console.error('处理规则变化失败:', error); + } + } + + async loadAndApplyRules(): Promise { + try { + // 检查扩展是否被禁用 + if (prefs.get('disable-all')) { + console.log('扩展已被禁用,清除所有规则'); + await V3RuleConverter.applyV3Rules([]); + return; + } + + // 获取所有启用的规则 + const allRules: any[] = []; + for (const tableName of TABLE_NAMES_ARR) { + const tableRules = rules.get(tableName, { enable: true }) || []; + allRules.push(...tableRules); + } + + console.log(`加载了 ${allRules.length} 个启用的规则`); + + if (allRules.length === 0) { + // 清除所有动态规则 + await V3RuleConverter.applyV3Rules([]); + logger.info('没有启用的规则,已清除所有动态规则'); + return; + } + + // 转换规则为 V3 格式 + const conversionResult = V3RuleConverter.convertRulesToV3(allRules); + this.conversionStats = conversionResult; + + // 检查规则限制 + const limitCheck = V3RuleConverter.checkRuleLimits(conversionResult.convertedRules); + if (!limitCheck.isValid) { + logger.warn('规则超过 V3 限制:', limitCheck.errors); + console.warn('规则超过 V3 限制:', limitCheck.errors); + } + + // 应用 V3 规则 + await V3RuleConverter.applyV3Rules(conversionResult.convertedRules); + + const stats = { + total: allRules.length, + converted: conversionResult.convertedRules.length, + unconverted: conversionResult.unconvertedRules.length, + warnings: conversionResult.warnings.length, + }; + + logger.info('规则应用完成:', stats); + console.log('规则应用统计:', stats); + + if (conversionResult.unconvertedRules.length > 0) { + console.warn('无法转换的规则:', conversionResult.unconvertedRules.map((r) => r.name)); + } + + if (conversionResult.warnings.length > 0) { + console.warn('转换警告:', conversionResult.warnings); + } + } catch (error) { + logger.error('应用规则时发生错误:', error); + console.error('应用规则失败:', error); + throw error; + } + } + + async refresh(): Promise { + console.log('刷新规则...'); + await this.loadAndApplyRules(); + } + + getStats(): any { + return this.conversionStats; + } +} + +// 测试 x-tag header 规则 +async function testXTagHeaderRule() { + console.log('开始测试 x-tag header 规则...'); + + try { + // 1. 检查当前规则 + console.log('1. 检查现有的 sendHeader 规则...'); + const currentSendRules = rules.get(TABLE_NAMES.sendHeader, { enable: true }) || []; + console.log(`当前有 ${currentSendRules.length} 个启用的发送头规则`); + + const xTagRules = currentSendRules.filter((rule) => + rule.action && + typeof rule.action === 'object' && + rule.action.name && + rule.action.name.toLowerCase() === 'x-tag'); + + console.log(`其中有 ${xTagRules.length} 个 x-tag 规则:`, xTagRules); + + // 2. 创建测试规则(如果不存在) + if (xTagRules.length === 0) { + console.log('2. 创建测试 x-tag 规则...'); + const testXTagRule = { + id: -1, // 新规则 + name: '测试 X-Tag Header', + enable: true, + ruleType: RULE_TYPE.MODIFY_SEND_HEADER, + matchType: RULE_MATCH_TYPE.ALL, + pattern: '*', + isFunction: false, + code: '', + exclude: '', + group: 'test', + action: { + name: 'X-Tag', + value: 'HeaderEditor-Test', + }, + }; + + console.log('准备保存测试规则:', testXTagRule); + const savedRule = await rules.save(testXTagRule); + console.log('测试规则保存成功:', savedRule); + + // 等待一下让事件处理完成 + await new Promise((resolve) => setTimeout(resolve, 1000)); + } + + // 3. 重新检查规则 + console.log('3. 重新检查规则...'); + const updatedSendRules = rules.get(TABLE_NAMES.sendHeader, { enable: true }) || []; + const updatedXTagRules = updatedSendRules.filter((rule) => + rule.action && + typeof rule.action === 'object' && + rule.action.name && + rule.action.name.toLowerCase().includes('tag')); + console.log(`现在有 ${updatedXTagRules.length} 个 tag 相关规则:`, updatedXTagRules); + + // 4. 检查规则转换 + console.log('4. 测试规则转换...'); + if (updatedXTagRules.length > 0) { + const conversionResult = V3RuleConverter.convertRulesToV3(updatedXTagRules); + console.log('规则转换结果:', { + converted: conversionResult.convertedRules.length, + unconverted: conversionResult.unconvertedRules.length, + warnings: conversionResult.warnings, + }); + + if (conversionResult.convertedRules.length > 0) { + console.log('转换后的 V3 规则:', conversionResult.convertedRules); + } + + if (conversionResult.warnings.length > 0) { + console.warn('转换警告:', conversionResult.warnings); + } + } + + // 5. 检查当前应用的 V3 规则 + console.log('5. 检查当前应用的 V3 规则...'); + const currentV3Rules = await V3RuleConverter.getCurrentRules(); + console.log(`当前已应用 ${currentV3Rules.length} 个 V3 规则`); + + const v3HeaderRules = currentV3Rules.filter((rule) => + rule.action && + rule.action.type === 'modifyHeaders' && + rule.action.requestHeaders && + rule.action.requestHeaders.some((header) => + header.header && header.header.toLowerCase().includes('tag'))); + + console.log(`其中有 ${v3HeaderRules.length} 个修改 tag header 的 V3 规则:`, v3HeaderRules); + + // 6. 刷新规则应用 + console.log('6. 刷新规则应用...'); + if (ruleHandler) { + await ruleHandler.refresh(); + console.log('规则刷新完成'); + + // 再次检查 V3 规则 + const refreshedV3Rules = await V3RuleConverter.getCurrentRules(); + const refreshedHeaderRules = refreshedV3Rules.filter((rule) => + rule.action && + rule.action.type === 'modifyHeaders' && + rule.action.requestHeaders && + rule.action.requestHeaders.some((header) => + header.header && header.header.toLowerCase().includes('tag'))); + + console.log(`刷新后有 ${refreshedHeaderRules.length} 个修改 tag header 的 V3 规则:`, refreshedHeaderRules); + } + + // 7. 提供测试建议 + console.log('7. 测试建议:'); + console.log('请访问任意网站(如 https://httpbin.org/headers)查看请求头是否包含 X-Tag'); + console.log('您也可以在开发者工具的 Network 标签中查看请求头'); + + console.log('x-tag header 规则测试完成'); + } catch (error) { + console.error('测试 x-tag header 规则时发生错误:', error); + } +} + +// 导出测试函数到全局 +if (typeof globalThis !== 'undefined') { + globalThis.testXTagHeaderRule = testXTagHeaderRule; } -// 开始初始化 -createApiHandler(); -createRequestHandler(); +// 全局规则处理器实例 +let ruleHandler: V3RuleHandler | null = null; + +// 测试辅助函数 +async function testRuleApplication() { + if (!ruleHandler) { + console.error('规则处理器未初始化'); + return; + } + + console.log('开始测试规则应用功能...'); + + try { + // 1. 测试获取现有规则 + console.log('1. 获取现有规则...'); + const currentRules = await V3RuleConverter.getCurrentRules(); + console.log(`当前已应用 ${currentRules.length} 个 V3 规则`); + + // 2. 测试规则转换 + console.log('2. 测试规则转换...'); + const testRule = { + id: 999, + name: '测试规则', + enable: true, + ruleType: RULE_TYPE.MODIFY_SEND_HEADER, + matchType: RULE_MATCH_TYPE.ALL, + pattern: '*', + isFunction: false, + code: '', + exclude: '', + group: 'test', + action: { + name: 'User-Agent', + value: 'Test-Agent', + }, + }; + + const conversionResult = V3RuleConverter.convertRulesToV3([testRule]); + console.log('规则转换结果:', { + converted: conversionResult.convertedRules.length, + unconverted: conversionResult.unconvertedRules.length, + warnings: conversionResult.warnings.length, + }); + + if (conversionResult.convertedRules.length > 0) { + console.log('转换后的 V3 规则:', conversionResult.convertedRules[0]); + } + + // 3. 测试规则限制检查 + console.log('3. 测试规则限制检查...'); + const limits = V3RuleConverter.getRuleLimits(); + const limitCheck = V3RuleConverter.checkRuleLimits(conversionResult.convertedRules); + console.log('规则限制:', limits); + console.log('规则限制检查结果:', limitCheck); + + // 4. 测试规则刷新 + console.log('4. 测试规则刷新...'); + await ruleHandler.refresh(); + console.log('规则刷新完成'); + + // 5. 获取统计信息 + console.log('5. 获取统计信息...'); + const stats = ruleHandler.getStats(); + console.log('转换统计:', stats); + + // 6. 验证规则数量 + console.log('6. 验证规则数量...'); + const newRules = await V3RuleConverter.getCurrentRules(); + console.log(`刷新后已应用 ${newRules.length} 个 V3 规则`); + + console.log('规则应用功能测试完成'); + } catch (error) { + console.error('测试规则应用功能时发生错误:', error); + } +} + +// 导出测试函数到全局 +if (typeof globalThis !== 'undefined') { + globalThis.testRuleApplication = testRuleApplication; +} + +// 初始化函数 +async function initialize() { + try { + console.log('开始初始化 Header Editor...'); + + ruleHandler = new V3RuleHandler(); + await ruleHandler.initialize(); + + console.log('Header Editor 初始化完成'); + + // 在调试模式下运行测试 + if (logger.getLevel() === 'DEBUG') { + setTimeout(() => { + testRuleApplication(); + }, 2000); + } + } catch (error) { + console.error('Header Editor 初始化失败:', error); + + // 显示错误通知 + try { + await chrome.notifications.create({ + type: 'basic', + iconUrl: 'assets/images/128.png', + title: 'Header Editor 初始化失败', + message: '请检查控制台错误信息,或重新加载扩展。', + }); + } catch (notifyError) { + console.error('显示通知失败:', notifyError); + } + } +} + +// 监听规则更新事件 +if (typeof chrome !== 'undefined' && chrome.runtime) { + // 监听安装事件 + chrome.runtime.onInstalled.addListener((details) => { + console.log('Extension installed:', details.reason); + + if (details.reason === 'install') { + // 首次安装时打开选项页面 + try { + chrome.tabs.create({ + url: chrome.runtime.getURL('options.html'), + }); + } catch (error) { + console.log('无法创建选项页面:', error); + } + } + }); + + // 监听启动事件 + chrome.runtime.onStartup.addListener(() => { + console.log('Extension startup'); + }); + + // 监听偏好设置变化 + chrome.storage.onChanged.addListener((changes, namespace) => { + if (namespace === 'local' && changes['disable-all']) { + console.log('检测到 disable-all 偏好设置变化:', changes['disable-all']); + if (ruleHandler) { + ruleHandler.refresh(); + } + } + }); + + // 监听开发者工具命令 + chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { + if (request.method === 'testRuleApplication') { + testRuleApplication().then(() => { + sendResponse({ success: true }); + }).catch((error) => { + sendResponse({ success: false, error: error.message }); + }); + return true; // 保持消息通道开放 + } + + if (request.method === 'getRuleStats') { + const stats = ruleHandler?.getStats() || null; + sendResponse({ stats }); + return true; + } + + if (request.method === 'enableDebugLogging') { + logger.enableDebug(); + sendResponse({ success: true, level: logger.getLevel() }); + return true; + } + + if (request.method === 'disableDebugLogging') { + logger.disableDebug(); + sendResponse({ success: true, level: logger.getLevel() }); + return true; + } + }); +} + +// 简单的状态检查 +console.log('Service Worker 环境检查:', { + hasChrome: typeof chrome !== 'undefined', + hasRuntime: typeof chrome !== 'undefined' && !!chrome.runtime, + hasDeclarativeNetRequest: typeof chrome !== 'undefined' && !!(chrome as any).declarativeNetRequest, +}); + +// 启动初始化 +initialize().catch(console.error); + +// 导出全局访问 +if (typeof globalThis !== 'undefined') { + globalThis.headerEditorRuleHandler = ruleHandler; +} diff --git a/src/pages/background/request-handler.ts b/src/pages/background/request-handler.ts deleted file mode 100644 index 7c29742f..00000000 --- a/src/pages/background/request-handler.ts +++ /dev/null @@ -1,492 +0,0 @@ -/* eslint-disable @typescript-eslint/member-ordering */ -import { TextDecoder, TextEncoder } from 'text-encoding'; -import browser, { WebRequest } from 'webextension-polyfill'; -import { getGlobal, IS_CHROME, IS_SUPPORT_STREAM_FILTER } from '@/share/core/utils'; -import emitter from '@/share/core/emitter'; -import logger from '@/share/core/logger'; -import type { Rule } from '@/share/core/types'; -import { TABLE_NAMES } from '@/share/core/constant'; -import { prefs } from '@/share/core/prefs'; -import rules from './core/rules'; - -// 最大修改8MB的Body -const MAX_BODY_SIZE = 8 * 1024 * 1024; - -enum REQUEST_TYPE { - REQUEST, - RESPONSE, -} - -type HeaderRequestDetails = WebRequest.OnHeadersReceivedDetailsType | WebRequest.OnBeforeSendHeadersDetailsType; -type AnyRequestDetails = WebRequest.OnBeforeRequestDetailsType | HeaderRequestDetails; -interface CustomFunctionDetail { - id: string; - url: string; - tab: number; - method: string; - frame: number; - parentFrame: number; - // @ts-ignore - proxy: any; - type: WebRequest.ResourceType; - time: number; - originUrl: string; - documentUrl: string; - requestHeaders: WebRequest.HttpHeaders | null; - responseHeaders: WebRequest.HttpHeaders | null; - statusCode?: number; - statusLine?: string; -} -class RequestHandler { - private _disableAll = false; - private excludeHe = true; - private includeHeaders = false; - private modifyBody = false; - private savedRequestHeader = new Map(); - private deleteHeaderTimer: ReturnType | null = null; - private deleteHeaderQueue = new Map(); - private textDecoder: Map = new Map(); - private textEncoder: Map = new Map(); - - constructor() { - this.initHook(); - this.loadPrefs(); - } - get disableAll() { - return this._disableAll; - } - set disableAll(to) { - if (this._disableAll === to) { - return; - } - this._disableAll = to; - browser.browserAction.setIcon({ - path: `/assets/images/128${to ? 'w' : ''}.png`, - }); - } - - private createHeaderListener(type: string): any { - const result = ['blocking']; - result.push(type); - if ( - IS_CHROME && - // @ts-ignore - chrome.webRequest.OnBeforeSendHeadersOptions.hasOwnProperty('EXTRA_HEADERS') - ) { - result.push('extraHeaders'); - } - return result; - } - - private initHook() { - browser.webRequest.onBeforeRequest.addListener(this.handleBeforeRequest.bind(this), { urls: [''] }, [ - 'blocking', - ]); - browser.webRequest.onBeforeSendHeaders.addListener( - this.handleBeforeSend.bind(this), - { urls: [''] }, - this.createHeaderListener('requestHeaders'), - ); - browser.webRequest.onHeadersReceived.addListener( - this.handleReceived.bind(this), - { urls: [''] }, - this.createHeaderListener('responseHeaders'), - ); - } - - private loadPrefs() { - emitter.on(emitter.EVENT_PREFS_UPDATE, (key: string, val: any) => { - switch (key) { - case 'exclude-he': - this.excludeHe = val; - break; - case 'disable-all': - this.disableAll = val; - break; - case 'include-headers': - this.includeHeaders = val; - break; - case 'modify-body': - this.modifyBody = val; - break; - default: - break; - } - }); - - prefs.ready(() => { - this.excludeHe = prefs.get('exclude-he'); - this.disableAll = prefs.get('disable-all'); - this.includeHeaders = prefs.get('include-headers'); - this.modifyBody = prefs.get('modify-body'); - }); - } - - private beforeAll(e: AnyRequestDetails) { - if (this.disableAll) { - return false; - } - // 判断是否是HE自身 - if (this.excludeHe && e.url.indexOf(browser.runtime.getURL('')) === 0) { - return false; - } - return true; - } - - /** - * BeforeRequest事件,可撤销、重定向 - * @param any e - */ - handleBeforeRequest(e: WebRequest.OnBeforeRequestDetailsType) { - if (!this.beforeAll(e)) { - return; - } - logger.debug(`handle before request ${e.url}`, e); - // 可用:重定向,阻止加载 - const rule = rules.get(TABLE_NAMES.request, { url: e.url, enable: true }); - // Browser is starting up, pass all requests - if (rule === null) { - return; - } - let redirectTo = e.url; - const detail = this.makeDetails(e); - for (const item of rule) { - if (item.action === 'cancel' && !item.isFunction) { - return { cancel: true }; - } else if (item.isFunction) { - try { - const r = item._func(redirectTo, detail); - if (typeof r === 'string') { - logger.debug(`[rule: ${item.id}] redirect ${redirectTo} to ${r}`); - redirectTo = r; - } - if (r === '_header_editor_cancel_' || (item.action === 'cancel' && r === true)) { - logger.debug(`[rule: ${item.id}] cancel`); - return { cancel: true }; - } - } catch (err) { - console.error(err); - } - } else if (item.to) { - if (item.matchType === 'regexp') { - const to = redirectTo.replace(item._reg, item.to); - logger.debug(`[rule: ${item.id}] redirect ${redirectTo} to ${to}`); - redirectTo = to; - } else { - logger.debug(`[rule: ${item.id}] redirect ${redirectTo} to ${item.to}`); - redirectTo = item.to; - } - } - } - if (redirectTo && redirectTo !== e.url) { - if (/^([a-zA-Z0-9]+)%3A/.test(redirectTo)) { - redirectTo = decodeURIComponent(redirectTo); - } - return { redirectUrl: redirectTo }; - } - } - - /** - * beforeSend事件,可修改请求头 - * @param any e - */ - handleBeforeSend(e: WebRequest.OnBeforeSendHeadersDetailsType) { - if (!this.beforeAll(e)) { - return; - } - // 修改请求头 - if (!e.requestHeaders) { - return; - } - logger.debug(`handle before send ${e.url}`, e.requestHeaders); - const rule = rules.get(TABLE_NAMES.sendHeader, { url: e.url, enable: true }); - // Browser is starting up, pass all requests - if (rule === null) { - return; - } - this.modifyHeaders(e, REQUEST_TYPE.REQUEST, rule); - logger.debug(`handle before send:finish ${e.url}`, e.requestHeaders); - return { requestHeaders: e.requestHeaders }; - } - - handleReceived(e: WebRequest.OnHeadersReceivedDetailsType) { - if (!this.beforeAll(e)) { - return; - } - const detail = this.makeDetails(e); - // 删除暂存的headers - if (this.includeHeaders) { - detail.requestHeaders = this.savedRequestHeader.get(e.requestId) || null; - this.savedRequestHeader.delete(e.requestId); - this.deleteHeaderQueue.delete(e.requestId); - } - // 修改响应体 - if (this.modifyBody) { - let canModifyBody = true; - // 检查有没有Content-Length头,如有,则不能超过MAX_BODY_SIZE,否则不进行修改 - if (e.responseHeaders) { - for (const it of e.responseHeaders) { - if (it.name.toLowerCase() === 'content-length') { - if (it.value && parseInt(it.value, 10) >= MAX_BODY_SIZE) { - canModifyBody = false; - } - break; - } - } - } - if (canModifyBody) { - this.modifyReceivedBody(e, detail); - } - } - // 修改响应头 - if (!e.responseHeaders) { - return; - } - logger.debug(`handle received ${e.url}`, e.responseHeaders); - const rule = rules.get(TABLE_NAMES.receiveHeader, { url: e.url, enable: true }); - // Browser is starting up, pass all requests - if (rule) { - this.modifyHeaders(e, REQUEST_TYPE.RESPONSE, rule, detail); - } - logger.debug(`handle received:finish ${e.url}`, e.responseHeaders); - return { responseHeaders: e.responseHeaders }; - } - - private makeDetails(request: AnyRequestDetails): CustomFunctionDetail { - const details = { - id: request.requestId, - url: request.url, - tab: request.tabId, - method: request.method, - frame: request.frameId, - parentFrame: request.parentFrameId, - // @ts-ignore - proxy: request.proxyInfo || null, - type: request.type, - time: request.timeStamp, - originUrl: request.originUrl || '', - documentUrl: request.documentUrl || '', - requestHeaders: null, - responseHeaders: null, - }; - - ['statusCode', 'statusLine', 'requestHeaders', 'responseHeaders'].forEach((p) => { - if (p in request) { - // @ts-ignore - details[p] = request[p]; - } - }); - - return details; - } - - private textEncode(encoding: string, text: string) { - let encoder = this.textEncoder.get(encoding); - if (!encoder) { - // UTF-8使用原生API,性能更好 - if (encoding === 'UTF-8' && getGlobal().TextEncoder) { - encoder = new (getGlobal().TextEncoder)(); - } else { - encoder = new TextEncoder(encoding, { NONSTANDARD_allowLegacyEncoding: true }); - } - this.textEncoder.set(encoding, encoder); - } - // 防止解码失败导致整体错误 - try { - return encoder.encode(text); - } catch (e) { - console.error(e); - return new Uint8Array(0); - } - } - - private textDecode(encoding: string, buffer: Uint8Array) { - let encoder = this.textDecoder.get(encoding); - if (!encoder) { - // 如果原生支持的话,优先使用原生 - if (getGlobal().TextDecoder) { - try { - encoder = new (getGlobal().TextDecoder)(encoding); - } catch (e) { - encoder = new TextDecoder(encoding); - } - } else { - encoder = new TextDecoder(encoding); - } - this.textDecoder.set(encoding, encoder); - } - // 防止解码失败导致整体错误 - try { - return encoder.decode(buffer); - } catch (e) { - console.error(e); - return ''; - } - } - - private modifyHeaders( - request: HeaderRequestDetails, - type: REQUEST_TYPE, - rule: Rule[], - presetDetail?: CustomFunctionDetail, - ) { - // @ts-ignore - const headers = request[type === REQUEST_TYPE.REQUEST ? 'requestHeaders' : 'responseHeaders']; - if (!headers) { - return; - } - if (this.includeHeaders && type === REQUEST_TYPE.REQUEST) { - // 暂存headers - this.savedRequestHeader.set( - request.requestId, - (request as WebRequest.OnBeforeSendHeadersDetailsType).requestHeaders, - ); - this.autoDeleteSavedHeader(request.requestId); - } - const newHeaders: { [key: string]: string } = {}; - let hasFunction = false; - for (let i = 0; i < rule.length; i++) { - if (!rule[i].isFunction) { - // @ts-ignore - newHeaders[rule[i].action.name] = rule[i].action.value; - rule.splice(i, 1); - i--; - } else { - hasFunction = true; - } - } - for (let i = 0; i < headers.length; i++) { - const name = headers[i].name.toLowerCase(); - if (newHeaders[name] === undefined) { - continue; - } - if (newHeaders[name] === '_header_editor_remove_') { - headers.splice(i, 1); - i--; - } else { - headers[i].value = newHeaders[name]; - } - delete newHeaders[name]; - } - for (const k in newHeaders) { - if (newHeaders[k] === '_header_editor_remove_') { - continue; - } - headers.push({ - name: k, - value: newHeaders[k], - }); - } - if (hasFunction) { - const detail = presetDetail || this.makeDetails(request); - rule.forEach((item) => { - try { - item._func(headers, detail); - } catch (e) { - console.error(e); - } - }); - } - } - - private autoDeleteSavedHeader(id?: string) { - if (id) { - this.deleteHeaderQueue.set(id, new Date().getTime() / 100); - } - if (this.deleteHeaderTimer !== null) { - return; - } - this.deleteHeaderTimer = getGlobal().setTimeout(() => { - // clear timeout - if (this.deleteHeaderTimer) { - clearTimeout(this.deleteHeaderTimer); - } - this.deleteHeaderTimer = null; - // check time - const curTime = new Date().getTime() / 100; - // k: id, v: time - const iter = this.deleteHeaderQueue.entries(); - for (const [k, v] of iter) { - if (curTime - v >= 90) { - this.savedRequestHeader.delete(k); - this.deleteHeaderQueue.delete(k); - } - } - if (this.deleteHeaderQueue.size > 0) { - this.autoDeleteSavedHeader(); - } - }, 10000); - } - - private modifyReceivedBody(e: WebRequest.OnHeadersReceivedDetailsType, detail: CustomFunctionDetail) { - if (!IS_SUPPORT_STREAM_FILTER) { - return; - } - - let rule = rules.get(TABLE_NAMES.receiveBody, { url: e.url, enable: true }); - if (rule === null) { - return; - } - rule = rule.filter((item) => item.isFunction); - if (rule.length === 0) { - return; - } - - const filter = browser.webRequest.filterResponseData(e.requestId); - let buffers: Uint8Array | null = null; - // @ts-ignore - filter.ondata = (event: WebRequest.StreamFilterEventData) => { - const { data } = event; - if (buffers === null) { - buffers = new Uint8Array(data); - return; - } - const buffer = new Uint8Array(buffers.byteLength + data.byteLength); - // 将响应分段数据收集拼接起来,在完成加载后整体替换。 - // 这可能会改变浏览器接收数据分段渲染的行为。 - buffer.set(buffers); - buffer.set(new Uint8Array(data), buffers.buffer.byteLength); - buffers = buffer; - // 如果长度已经超长了,那就不要尝试修改了 - if (buffers.length > MAX_BODY_SIZE) { - buffers = null; - filter.close(); - } - }; - - // @ts-ignore - filter.onstop = () => { - if (buffers === null) { - filter.close(); - return; - } - - // 缓存实例,减少开销 - for (const item of rule!) { - const encoding = item.encoding || 'UTF-8'; - try { - const _text = this.textDecode(encoding, new Uint8Array(buffers!.buffer)); - const text = item._func(_text, detail); - if (typeof text === 'string' && text !== _text) { - buffers = this.textEncode(encoding, text); - } - } catch (err) { - console.error(err); - } - } - - filter.write(buffers.buffer); - buffers = null; - filter.close(); - }; - - // @ts-ignore - filter.onerror = () => { - buffers = null; - }; - } -} - -export default function createRequestHandler() { - return new RequestHandler(); -} diff --git a/src/pages/background/upgrade.ts b/src/pages/background/upgrade.ts index a4c71c37..92d6fca8 100644 --- a/src/pages/background/upgrade.ts +++ b/src/pages/background/upgrade.ts @@ -5,10 +5,13 @@ import notify from '@/share/core/notify'; import { getDatabase } from './core/db'; // Upgrade -const downloadHistory = localStorage.getItem('dl_history'); -if (downloadHistory) { - storage.getLocal().set({ dl_history: JSON.parse(downloadHistory) }); - localStorage.removeItem('dl_history'); +// Service Worker环境中没有localStorage,跳过升级 +if (typeof localStorage !== 'undefined' && typeof localStorage.getItem === 'function') { + const downloadHistory = localStorage.getItem('dl_history'); + if (downloadHistory) { + storage.getLocal().set({ dl_history: JSON.parse(downloadHistory) }); + localStorage.removeItem('dl_history'); + } } // Put a version mark @@ -62,24 +65,29 @@ storage }); }; - const groups = localStorage.getItem('groups'); - if (groups) { - const g = JSON.parse(groups); - localStorage.removeItem('groups'); - rebindRuleWithGroup(g); - } else { - storage - .getLocal() - .get('groups') - .then((r) => { - if (r.groups !== undefined) { - rebindRuleWithGroup(r.groups).then(() => storage.getLocal().remove('groups')); - } else { - const g = {}; - g[browser.i18n.getMessage('ungrouped')] = []; - rebindRuleWithGroup(g); - } - }); + // Service Worker环境中没有localStorage,跳过groups迁移 + if (typeof localStorage !== 'undefined' && typeof localStorage.getItem === 'function') { + const groups = localStorage.getItem('groups'); + if (groups) { + const g = JSON.parse(groups); + localStorage.removeItem('groups'); + rebindRuleWithGroup(g); + return; + } } + + // 如果没有localStorage或没有groups数据,使用storage.local + storage + .getLocal() + .get('groups') + .then((r) => { + if (r.groups !== undefined) { + rebindRuleWithGroup(r.groups).then(() => storage.getLocal().remove('groups')); + } else { + const g = {}; + g[browser.i18n.getMessage('ungrouped')] = []; + rebindRuleWithGroup(g); + } + }); } }); diff --git a/src/pages/background/v3-rule-converter.ts b/src/pages/background/v3-rule-converter.ts new file mode 100644 index 00000000..510cd898 --- /dev/null +++ b/src/pages/background/v3-rule-converter.ts @@ -0,0 +1,558 @@ +import browser from 'webextension-polyfill'; +import type { Rule } from '@/share/core/types'; +import logger from '@/share/core/logger'; + +// declarativeNetRequest 规则接口 +interface V3Rule { + id: number; + priority: number; + action: { + type: 'block' | 'redirect' | 'modifyHeaders' | 'upgradeScheme' | 'allow' | 'allowAllRequests'; + redirect?: { url: string; regexSubstitution?: string }; + requestHeaders?: Array<{ + header: string; + operation: 'set' | 'remove' | 'append'; + value?: string; + }>; + responseHeaders?: Array<{ + header: string; + operation: 'set' | 'remove' | 'append'; + value?: string; + }>; + }; + condition: { + urlFilter?: string; + regexFilter?: string; + domains?: string[]; + excludedDomains?: string[]; + resourceTypes?: browser.DeclarativeNetRequest.ResourceType[]; + excludedResourceTypes?: browser.DeclarativeNetRequest.ResourceType[]; + requestMethods?: string[]; + excludedRequestMethods?: string[]; + }; +} + +// 转换结果接口 +interface ConversionResult { + convertedRules: V3Rule[]; + unconvertedRules: Rule[]; + warnings: string[]; +} + +export class V3RuleConverter { + private static ruleIdCounter = 1000; // 从1000开始,避免与静态规则冲突 + + /** + * 将传统规则转换为 V3 规则 + */ + static convertRulesToV3(rules: Rule[]): ConversionResult { + const convertedRules: V3Rule[] = []; + const unconvertedRules: Rule[] = []; + const warnings: string[] = []; + + // 重置规则ID计数器 + this.ruleIdCounter = 1000; + + for (const rule of rules) { + try { + if (this.isConvertible(rule)) { + const v3Rule = this.convertSingleRule(rule); + if (v3Rule) { + convertedRules.push(v3Rule); + } + } else { + unconvertedRules.push(rule); + let reason = '未知原因'; + if (rule.isFunction) { + reason = '包含自定义函数'; + } else if (rule.ruleType === 'modifyReceiveBody') { + reason = '不支持响应体修改'; + } else if (rule.matchType === 'regexp' && this.isComplexRegex(rule.pattern)) { + reason = '包含复杂的正则表达式'; + } + warnings.push(`规则 "${rule.name}" 无法转换为 V3 格式: ${reason}`); + } + } catch (error) { + logger.error(`转换规则 "${rule.name}" 时发生错误:`, error); + unconvertedRules.push(rule); + warnings.push(`规则 "${rule.name}" 转换失败: ${error.message}`); + } + } + + return { convertedRules, unconvertedRules, warnings }; + } + + /** + * 检查规则是否可以转换为 V3 格式 + */ + private static isConvertible(rule: Rule): boolean { + // 不支持自定义函数 + if (rule.isFunction) { + return false; + } + + // 不支持响应体修改 + if (rule.ruleType === 'modifyReceiveBody') { + return false; + } + + // 检查是否为支持的规则类型 + const supportedTypes = ['cancel', 'redirect', 'modifySendHeader', 'modifyReceiveHeader']; + if (!supportedTypes.includes(rule.ruleType)) { + return false; + } + + // 检查正则表达式复杂度 + if (rule.matchType === 'regexp' && this.isComplexRegex(rule.pattern)) { + return false; + } + + return true; + } + + /** + * 检查是否为复杂正则表达式 + */ + private static isComplexRegex(pattern: string): boolean { + // 简单检查,如果包含复杂的正则特性,认为是复杂的 + const complexFeatures = [ + '\\d', '\\w', '\\s', '\\D', '\\W', '\\S', // 字符类 + '\\b', '\\B', // 边界 + '(?:', '(?=', '(?!', '(?<=', '(? pattern.includes(feature)); + } + + /** + * 转换单个规则 + */ + private static convertSingleRule(rule: Rule): V3Rule | null { + const startTime = Date.now(); + + try { + const v3Rule: V3Rule = { + id: this.ruleIdCounter++, + priority: Math.max(1, Math.min(100, rule.priority || 1)), // 确保优先级在有效范围内 + action: this.convertAction(rule), + condition: this.convertCondition(rule), + }; + + // 验证生成的规则 + if (!this.validateV3Rule(v3Rule)) { + logger.warn(`规则 "${rule.name}" 转换后验证失败,跳过应用`); + logger.logRuleConversion(rule, v3Rule, false); + return null; + } + + const duration = Date.now() - startTime; + logger.logPerformance(`规则转换 ${rule.name}`, duration); + logger.logRuleConversion(rule, v3Rule, true); + + return v3Rule; + } catch (error) { + const duration = Date.now() - startTime; + logger.error(`转换规则 "${rule.name}" 时发生错误:`, error); + logger.logPerformance(`规则转换失败 ${rule.name}`, duration); + logger.logRuleConversion(rule, null, false); + return null; + } + } + + /** + * 验证 V3 规则是否有效 + */ + private static validateV3Rule(rule: V3Rule): boolean { + // 检查 ID 是否有效 + if (!rule.id || rule.id < 1) { + return false; + } + + // 检查优先级是否有效 + if (rule.priority < 1 || rule.priority > 100) { + return false; + } + + // 检查动作是否有效 + if (!rule.action || !rule.action.type) { + return false; + } + + // 检查条件是否有效 + if (!rule.condition) { + return false; + } + + // 至少需要一个匹配条件 + const hasMatchCondition = + rule.condition.urlFilter || + rule.condition.regexFilter || + rule.condition.domains?.length || + rule.condition.excludedDomains?.length; + + if (!hasMatchCondition) { + return false; + } + + return true; + } + + /** + * 转换规则动作 + */ + private static convertAction(rule: Rule): V3Rule['action'] { + switch (rule.ruleType) { + case 'cancel': + return { type: 'block' }; + + case 'redirect': + if (rule.to) { + // 简单的 URL 重定向 + if (rule.matchType === 'regexp' && !this.isComplexRegex(rule.pattern)) { + return { + type: 'redirect', + redirect: { + url: rule.to, + regexSubstitution: rule.to, + }, + }; + } else { + return { + type: 'redirect', + redirect: { url: rule.to }, + }; + } + } + return { type: 'block' }; + + case 'modifySendHeader': + return { + type: 'modifyHeaders', + requestHeaders: this.convertHeaders(rule), + }; + + case 'modifyReceiveHeader': + return { + type: 'modifyHeaders', + responseHeaders: this.convertHeaders(rule), + }; + + default: + throw new Error(`不支持的规则类型: ${rule.ruleType}`); + } + } + + /** + * 转换请求头/响应头 + */ + private static convertHeaders(rule: Rule): Array<{ + header: string; + operation: 'set' | 'remove' | 'append'; + value?: string; + }> { + if (!rule.action || typeof rule.action !== 'object') { + return []; + } + + const headers: Array<{ + header: string; + operation: 'set' | 'remove' | 'append'; + value?: string; + }> = []; + + if (rule.action.name && rule.action.value !== undefined) { + const headerName = rule.action.name.trim(); + if (!headerName) { + return []; + } + + if (rule.action.value === '_header_editor_remove_') { + headers.push({ + header: headerName, + operation: 'remove', + }); + } else { + headers.push({ + header: headerName, + operation: 'set', + value: String(rule.action.value), + }); + } + } + + return headers; + } + + /** + * 转换匹配条件 + */ + private static convertCondition(rule: Rule): V3Rule['condition'] { + const condition: V3Rule['condition'] = {}; + + // 设置资源类型 + if (rule.ruleType === 'modifySendHeader') { + // 请求头修改适用于所有资源类型 + condition.resourceTypes = [ + 'main_frame', 'sub_frame', 'stylesheet', 'script', 'image', + 'font', 'object', 'xmlhttprequest', 'ping', 'csp_report', + 'media', 'websocket', 'other', + ]; + } else if (rule.ruleType === 'modifyReceiveHeader') { + // 响应头修改适用于所有资源类型 + condition.resourceTypes = [ + 'main_frame', 'sub_frame', 'stylesheet', 'script', 'image', + 'font', 'object', 'xmlhttprequest', 'ping', 'csp_report', + 'media', 'websocket', 'other', + ]; + } else { + // 其他规则类型使用默认资源类型 + condition.resourceTypes = [ + 'main_frame', 'sub_frame', 'stylesheet', 'script', 'image', + 'font', 'object', 'xmlhttprequest', 'ping', 'csp_report', + 'media', 'websocket', 'other', + ]; + } + + // 转换 URL 匹配 + switch (rule.matchType) { + case 'all': + // 匹配所有URL + condition.urlFilter = '*'; + break; + + case 'regexp': + if (!this.isComplexRegex(rule.pattern)) { + condition.regexFilter = rule.pattern; + } else { + // 尝试将复杂正则转换为简单过滤器 + const simpleFilter = this.convertComplexRegexToFilter(rule.pattern); + if (simpleFilter) { + condition.urlFilter = simpleFilter; + } else { + condition.urlFilter = '*'; + } + } + break; + + case 'prefix': + // URL前缀匹配 + condition.urlFilter = `${rule.pattern}*`; + break; + + case 'domain': { + // 域名匹配 + const domain = this.normalizeDomain(rule.pattern); + if (domain) { + condition.domains = [domain]; + } else { + condition.urlFilter = '*'; + } + break; + } + + case 'url': + // 完整URL匹配 + condition.urlFilter = rule.pattern; + break; + + default: + // 默认匹配所有 + condition.urlFilter = '*'; + } + + // 处理排除模式 + if (rule.exclude && rule.exclude.trim()) { + try { + const excludeDomain = this.normalizeDomain(rule.exclude); + if (excludeDomain) { + condition.excludedDomains = [excludeDomain]; + } + } catch (error) { + logger.warn(`无法处理排除模式 "${rule.exclude}":`, error); + } + } + + return condition; + } + + /** + * 标准化域名 + */ + private static normalizeDomain(domain: string): string { + if (!domain) return ''; + + // 移除协议前缀 + domain = domain.replace(/^https?:\/\//, ''); + + // 移除路径 + domain = domain.split('/')[0]; + + // 移除端口 + domain = domain.split(':')[0]; + + // 移除 www. 前缀(可选) + if (domain.startsWith('www.')) { + domain = domain.substring(4); + } + + return domain.toLowerCase(); + } + + /** + * 检查是否为域名模式 + */ + private static isDomainPattern(pattern: string): boolean { + // 简单检查是否像域名 + return /^[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(pattern); + } + + /** + * 将复杂正则表达式转换为简单过滤器 + */ + private static convertComplexRegexToFilter(regex: string): string { + try { + // 尝试提取简单的URL模式 + if (regex.includes('://')) { + // 如果包含协议,提取域名部分 + const match = regex.match(/https?:\/\/([^/\s?]+)/); + if (match) { + return `*://${match[1]}/*`; + } + } + + // 提取域名模式 + const domainMatch = regex.match(/([a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/); + if (domainMatch) { + return `*://${domainMatch[1]}/*`; + } + + // 如果无法转换,返回通配符 + return '*'; + } catch (error) { + logger.warn('转换复杂正则表达式失败:', error); + return '*'; + } + } + + /** + * 从模式中提取域名 + */ + private static extractDomainFromPattern(pattern: string): string | null { + try { + const match = pattern.match(/(?:https?:\/\/)?([^/\s?]+)/); + return match ? match[1] : null; + } catch (error) { + return null; + } + } + + /** + * 应用 V3 规则到浏览器 + */ + static async applyV3Rules(rules: V3Rule[]): Promise { + const startTime = Date.now(); + + try { + logger.info(`准备应用 ${rules.length} 个 V3 规则`); + + // 先移除现有的动态规则 + const existingRules = await browser.declarativeNetRequest.getDynamicRules(); + const existingRuleIds = existingRules.map((r) => r.id); + + if (existingRuleIds.length > 0) { + logger.info(`移除现有的 ${existingRuleIds.length} 个动态规则`); + await browser.declarativeNetRequest.updateDynamicRules({ + removeRuleIds: existingRuleIds, + }); + } + + // 应用新规则 + if (rules.length > 0) { + logger.info(`应用 ${rules.length} 个新规则`); + + // 分批应用规则,避免一次性应用太多规则 + const batchSize = 100; + for (let i = 0; i < rules.length; i += batchSize) { + const batch = rules.slice(i, i + batchSize); + logger.debug(`应用规则批次 ${Math.floor(i / batchSize) + 1}/${Math.ceil(rules.length / batchSize)}, 包含 ${batch.length} 个规则`); + + await browser.declarativeNetRequest.updateDynamicRules({ + addRules: batch, + }); + } + } + + const duration = Date.now() - startTime; + logger.logPerformance('V3 规则应用', duration); + logger.logV3RuleApplication(rules, true); + + // 验证规则是否成功应用 + const newRules = await browser.declarativeNetRequest.getDynamicRules(); + if (newRules.length !== rules.length) { + logger.warn(`规则应用不完整: 期望 ${rules.length} 个,实际 ${newRules.length} 个`); + } + } catch (error) { + const duration = Date.now() - startTime; + logger.logPerformance('V3 规则应用失败', duration); + logger.logV3RuleApplication(rules, false, error); + logger.error('应用 V3 规则时发生错误:', error); + throw error; + } + } + + /** + * 获取 V3 规则限制信息 + */ + static getRuleLimits(): { + MAX_NUMBER_OF_DYNAMIC_RULES: number; + MAX_NUMBER_OF_REGEX_RULES: number; + MAX_NUMBER_OF_STATIC_RULES: number; + } { + return { + MAX_NUMBER_OF_DYNAMIC_RULES: 30000, + MAX_NUMBER_OF_REGEX_RULES: 1000, + MAX_NUMBER_OF_STATIC_RULES: 30000, + }; + } + + /** + * 检查规则是否超过限制 + */ + static checkRuleLimits(rules: V3Rule[]): { + isValid: boolean; + errors: string[]; + } { + const limits = this.getRuleLimits(); + const errors: string[] = []; + + if (rules.length > limits.MAX_NUMBER_OF_DYNAMIC_RULES) { + errors.push(`规则数量 (${rules.length}) 超过限制 (${limits.MAX_NUMBER_OF_DYNAMIC_RULES})`); + } + + const regexRules = rules.filter((r) => r.condition.regexFilter); + if (regexRules.length > limits.MAX_NUMBER_OF_REGEX_RULES) { + errors.push(`正则表达式规则数量 (${regexRules.length}) 超过限制 (${limits.MAX_NUMBER_OF_REGEX_RULES})`); + } + + return { + isValid: errors.length === 0, + errors, + }; + } + + /** + * 获取当前应用的动态规则 + */ + static async getCurrentRules(): Promise { + try { + const rules = await browser.declarativeNetRequest.getDynamicRules(); + return rules; + } catch (error) { + logger.error('获取当前规则失败:', error); + return []; + } + } +} diff --git a/src/pages/content/index.tsx b/src/pages/content/index.tsx new file mode 100644 index 00000000..3ea8dcdc --- /dev/null +++ b/src/pages/content/index.tsx @@ -0,0 +1,398 @@ +import React, { useRef, useState, useEffect } from 'react'; +import ReactDOM from 'react-dom'; +import { cx, css } from '@emotion/css'; +import browser from 'webextension-polyfill'; +import { Card, Switch, Table, Popover, Banner, Space, Badge, Select, Toast, Typography } from '@douyinfe/semi-ui'; +import { IconSetting, IconMore, IconQuit, IconSafe } from '@douyinfe/semi-icons'; +import type { Rule } from '@/share/core/types'; +import RuleDetail from '@/share/components/rule-detail'; +import { APIs } from '@/share/core/constant'; + +let currentIPList = []; +let rules = []; +let enableRules = []; +let enable = false; +let title = '当前未启用规则'; +let titleColor = 'rgba(var(--semi-gray-3), 1)'; + +const basicStyle = css` + position: fixed; + bottom: 20px; + right: 20px; + z-index: 1020; + min-width: 300px; + user-select: none; + + .cell-enable { + padding-right: 0; + .switch-container { + display: flex; + align-items: center; + } + } +`; + +console.log('[Header Editor] content-script load.......................'); + +// contentScript.js +browser.runtime.onMessage.addListener((message, sender, sendResponse) => { + // console.log('[Header Editor] content-script收到的消息', message); + + switch (message.method) { + case APIs.SET_PREFS: + if (message.key === 'disable-all') { + enable = !message.value; + } + ReactDOM.render(, app); + break; + case APIs.SAVE_RULE: + setTimeout(() => { + getData(); + }, 500); + break; + default: + break; + } +}); + + +function getData() { + browser.runtime.sendMessage({ greeting: '我是content-script呀,我主动发消息给后台!', method: 'GetData' }).then((response) => { + // console.log('[Header Editor] getData 收到来自后台的回复', response); + if (response) { + rules = response.rules || []; + enableRules = response.enableRules || []; + enable = response.enable || false; + currentIPList = response.currentIPList || []; + + if (enableRules.length > 0) { + title = enableRules[enableRules.length - 1].name || '规则名称未定义'; + titleColor = 'rgba(var(--semi-green-4), 1)'; + } else { + title = '当前未启用规则'; + titleColor = 'rgba(var(--semi-gray-3), 1)'; + } + + ReactDOM.render(, app); + } + }); +} + +// 延迟获取数据,防止后台数据还未加载完成 +setTimeout(() => { + getData(); +}, 500); + +function Content() { + const { Meta } = Card; + + const [visible, setVisible] = useState(false); + + // 可拖动 + const [isDragging, setIsDragging] = useState(false); + const divRef = useRef(null); + const offsetRef = useRef<{ x: number; y: number }>({ x: 0, y: 0 }); + const [position, setPosition] = useState({ x: window.innerWidth - 300, y: window.innerHeight - 120 }); + + const handleMouseDown = (e: React.MouseEvent) => { + if (e.button === 0) { + e.preventDefault(); // 阻止默认的文本选择行为 + setIsDragging(true); + offsetRef.current = { + x: e.clientX - position.x, + y: e.clientY - position.y, + }; + } + }; + + const handleMouseMove = (e: MouseEvent) => { + if (isDragging) { + const newX = e.clientX - offsetRef.current.x; + const newY = e.clientY - offsetRef.current.y; + + // 限制拖动范围为窗口 + const maxX = window.innerWidth - divRef.current!.offsetWidth; + const maxY = window.innerHeight - divRef.current!.offsetHeight; + + const boundedX = Math.min(Math.max(newX, 10), maxX); + const boundedY = Math.min(Math.max(newY, 10), maxY); + + setPosition({ x: boundedX, y: boundedY }); + } + }; + + const handleMouseUp = () => { + setIsDragging(false); + }; + + const handleFocus = () => { + getData(); + }; + + useEffect(() => { + if (isDragging) { + window.addEventListener('mousemove', handleMouseMove); + window.addEventListener('mouseup', handleMouseUp); + } else { + window.removeEventListener('mousemove', handleMouseMove); + window.removeEventListener('mouseup', handleMouseUp); + } + window.addEventListener('focus', handleFocus); + + return () => { + window.removeEventListener('mousemove', handleMouseMove); + window.removeEventListener('mouseup', handleMouseUp); + window.removeEventListener('focus', handleFocus); + }; + }, [isDragging]); + + const goToOptions = () => { + browser.runtime.sendMessage({ url: browser.runtime.getURL('options.html'), method: APIs.OPEN_URL }).then(() => {}); + window.close(); + }; + + const goToDnsSetting = () => { + browser.runtime.sendMessage({ url: 'chrome://net-internals/#dns', method: APIs.OPEN_URL }).then(() => {}); + window.close(); + }; + + const goToclearBrowserData = () => { + browser.runtime.sendMessage({ url: 'chrome://settings/clearBrowserData', method: APIs.OPEN_URL }).then(() => {}); + window.close(); + }; + + const handleEnableChange = () => { + browser.runtime.sendMessage({ key: 'disable-all', value: enable, method: APIs.SET_PREFS }).then((response) => { + // console.log('[Header Editor] handleEnableChange收到来自后台的回复', response); + enable = !enable; + ReactDOM.render(, app); + }); + }; + + const onEnableChange = (value) => { + value.rule.enable = true; + browser.runtime.sendMessage({ rule: value.rule, method: APIs.SAVE_RULE }).then(() => { + Toast.success({ + content: '启用成功', + }); + getData(); + }); + }; + + return enable ? ( +
+ + + + { enableRules.length > 1 ? + + {title} + :
{ title }
} + + } + /> +
+ + 管理规则 + + } + > + + + + 关闭插件 + + } + > + + + setVisible(false)} + onEscKeyDown={() => setVisible(false)} + content={ +
+ + 启用的规则列表 + +